From aae0ccc5889fd26125e29e21834bfa8d2439ec8c Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 31 Mar 2021 16:10:24 -0400 Subject: [PATCH 0001/1317] Add config flow support to google_travel_time (#43509) * add config flow support to google_travel_time * fix bugs and add strings * fix import and add new test * address comments in #43419 since this is a similar PR * fix default name and test * add unique ID and device info * fix test * feedback from waze PR * continue incorporating feedback from waze PR * final fixes and update tests * call update in lambda * Update homeassistant/components/google_travel_time/sensor.py Co-authored-by: Martin Hjelmare * additional fixes * validate config entry data during config flow and config entry setup * don't store entity * patch dependency instead of HA code * fixes * improve tests by moving all patching to fixtures * use self.hass instead of setting self._hass * invert if * remove unnecessary else Co-authored-by: Martin Hjelmare --- .coveragerc | 2 + .../components/google_travel_time/__init__.py | 35 ++ .../google_travel_time/config_flow.py | 166 +++++++++ .../components/google_travel_time/const.py | 89 +++++ .../components/google_travel_time/helpers.py | 72 ++++ .../google_travel_time/manifest.json | 9 +- .../components/google_travel_time/sensor.py | 335 ++++++++---------- .../google_travel_time/strings.json | 38 ++ .../google_travel_time/translations/en.json | 32 ++ homeassistant/generated/config_flows.py | 1 + requirements_test_all.txt | 3 + .../components/google_travel_time/__init__.py | 1 + .../components/google_travel_time/conftest.py | 59 +++ .../google_travel_time/test_config_flow.py | 297 ++++++++++++++++ 14 files changed, 952 insertions(+), 187 deletions(-) create mode 100644 homeassistant/components/google_travel_time/config_flow.py create mode 100644 homeassistant/components/google_travel_time/const.py create mode 100644 homeassistant/components/google_travel_time/helpers.py create mode 100644 homeassistant/components/google_travel_time/strings.json create mode 100644 homeassistant/components/google_travel_time/translations/en.json create mode 100644 tests/components/google_travel_time/__init__.py create mode 100644 tests/components/google_travel_time/conftest.py create mode 100644 tests/components/google_travel_time/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 0cadadfc5e814..dcc26036c464a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -349,6 +349,8 @@ omit = homeassistant/components/google/* homeassistant/components/google_cloud/tts.py homeassistant/components/google_maps/device_tracker.py + homeassistant/components/google_travel_time/__init__.py + homeassistant/components/google_travel_time/helpers.py homeassistant/components/google_travel_time/sensor.py homeassistant/components/gpmdp/media_player.py homeassistant/components/gpsd/sensor.py diff --git a/homeassistant/components/google_travel_time/__init__.py b/homeassistant/components/google_travel_time/__init__.py index 9d9a7cffe1ddd..d9afaf46deee0 100644 --- a/homeassistant/components/google_travel_time/__init__.py +++ b/homeassistant/components/google_travel_time/__init__.py @@ -1 +1,36 @@ """The google_travel_time component.""" +import asyncio + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +PLATFORMS = ["sensor"] + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the Google Maps Travel Time component.""" + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Google Maps Travel Time from a config entry.""" + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + + return unload_ok diff --git a/homeassistant/components/google_travel_time/config_flow.py b/homeassistant/components/google_travel_time/config_flow.py new file mode 100644 index 0000000000000..5c66220af0219 --- /dev/null +++ b/homeassistant/components/google_travel_time/config_flow.py @@ -0,0 +1,166 @@ +"""Config flow for Google Maps Travel Time integration.""" +import logging + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_API_KEY, CONF_MODE, CONF_NAME +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv +from homeassistant.util import slugify + +from .const import ( + ALL_LANGUAGES, + ARRIVAL_TIME, + AVOID, + CONF_ARRIVAL_TIME, + CONF_AVOID, + CONF_DEPARTURE_TIME, + CONF_DESTINATION, + CONF_LANGUAGE, + CONF_ORIGIN, + CONF_TIME, + CONF_TIME_TYPE, + CONF_TRAFFIC_MODEL, + CONF_TRANSIT_MODE, + CONF_TRANSIT_ROUTING_PREFERENCE, + CONF_UNITS, + DEFAULT_NAME, + DEPARTURE_TIME, + DOMAIN, + TIME_TYPES, + TRANSIT_PREFS, + TRANSPORT_TYPE, + TRAVEL_MODE, + TRAVEL_MODEL, + UNITS, +) +from .helpers import is_valid_config_entry + +_LOGGER = logging.getLogger(__name__) + + +class GoogleOptionsFlow(config_entries.OptionsFlow): + """Handle an options flow for Google Travel Time.""" + + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + """Initialize google options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Handle the initial step.""" + if user_input is not None: + time_type = user_input.pop(CONF_TIME_TYPE) + if time := user_input.pop(CONF_TIME, None): + if time_type == ARRIVAL_TIME: + user_input[CONF_ARRIVAL_TIME] = time + else: + user_input[CONF_DEPARTURE_TIME] = time + return self.async_create_entry(title="", data=user_input) + + if CONF_ARRIVAL_TIME in self.config_entry.options: + default_time_type = ARRIVAL_TIME + default_time = self.config_entry.options[CONF_ARRIVAL_TIME] + else: + default_time_type = DEPARTURE_TIME + default_time = self.config_entry.options.get(CONF_ARRIVAL_TIME) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Optional( + CONF_MODE, default=self.config_entry.options[CONF_MODE] + ): vol.In(TRAVEL_MODE), + vol.Optional( + CONF_LANGUAGE, + default=self.config_entry.options.get(CONF_LANGUAGE), + ): vol.In(ALL_LANGUAGES), + vol.Optional( + CONF_AVOID, default=self.config_entry.options.get(CONF_AVOID) + ): vol.In(AVOID), + vol.Optional( + CONF_UNITS, default=self.config_entry.options[CONF_UNITS] + ): vol.In(UNITS), + vol.Optional(CONF_TIME_TYPE, default=default_time_type): vol.In( + TIME_TYPES + ), + vol.Optional(CONF_TIME, default=default_time): cv.string, + vol.Optional( + CONF_TRAFFIC_MODEL, + default=self.config_entry.options.get(CONF_TRAFFIC_MODEL), + ): vol.In(TRAVEL_MODEL), + vol.Optional( + CONF_TRANSIT_MODE, + default=self.config_entry.options.get(CONF_TRANSIT_MODE), + ): vol.In(TRANSPORT_TYPE), + vol.Optional( + CONF_TRANSIT_ROUTING_PREFERENCE, + default=self.config_entry.options.get( + CONF_TRANSIT_ROUTING_PREFERENCE + ), + ): vol.In(TRANSIT_PREFS), + } + ), + ) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Google Maps Travel Time.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + @staticmethod + @callback + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> GoogleOptionsFlow: + """Get the options flow for this handler.""" + return GoogleOptionsFlow(config_entry) + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + if await self.hass.async_add_executor_job( + is_valid_config_entry, + self.hass, + _LOGGER, + user_input[CONF_API_KEY], + user_input[CONF_ORIGIN], + user_input[CONF_DESTINATION], + ): + await self.async_set_unique_id( + slugify( + f"{DOMAIN}_{user_input[CONF_ORIGIN]}_{user_input[CONF_DESTINATION]}" + ) + ) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=user_input.get( + CONF_NAME, + ( + f"{DEFAULT_NAME}: {user_input[CONF_ORIGIN]} -> " + f"{user_input[CONF_DESTINATION]}" + ), + ), + data=user_input, + ) + + # If we get here, it's because we couldn't connect + errors["base"] = "cannot_connect" + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_DESTINATION): cv.string, + vol.Required(CONF_ORIGIN): cv.string, + } + ), + errors=errors, + ) + + async_step_import = async_step_user diff --git a/homeassistant/components/google_travel_time/const.py b/homeassistant/components/google_travel_time/const.py new file mode 100644 index 0000000000000..6b9b77242ba29 --- /dev/null +++ b/homeassistant/components/google_travel_time/const.py @@ -0,0 +1,89 @@ +"""Constants for Google Travel Time.""" +from homeassistant.const import CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC + +DOMAIN = "google_travel_time" + +ATTRIBUTION = "Powered by Google" + +CONF_DESTINATION = "destination" +CONF_OPTIONS = "options" +CONF_ORIGIN = "origin" +CONF_TRAVEL_MODE = "travel_mode" +CONF_LANGUAGE = "language" +CONF_AVOID = "avoid" +CONF_UNITS = "units" +CONF_ARRIVAL_TIME = "arrival_time" +CONF_DEPARTURE_TIME = "departure_time" +CONF_TRAFFIC_MODEL = "traffic_model" +CONF_TRANSIT_MODE = "transit_mode" +CONF_TRANSIT_ROUTING_PREFERENCE = "transit_routing_preference" +CONF_TIME_TYPE = "time_type" +CONF_TIME = "time" + +ARRIVAL_TIME = "Arrival Time" +DEPARTURE_TIME = "Departure Time" +TIME_TYPES = [ARRIVAL_TIME, DEPARTURE_TIME] + +DEFAULT_NAME = "Google Travel Time" + +TRACKABLE_DOMAINS = ["device_tracker", "sensor", "zone", "person"] + +ALL_LANGUAGES = [ + "ar", + "bg", + "bn", + "ca", + "cs", + "da", + "de", + "el", + "en", + "es", + "eu", + "fa", + "fi", + "fr", + "gl", + "gu", + "hi", + "hr", + "hu", + "id", + "it", + "iw", + "ja", + "kn", + "ko", + "lt", + "lv", + "ml", + "mr", + "nl", + "no", + "pl", + "pt", + "pt-BR", + "pt-PT", + "ro", + "ru", + "sk", + "sl", + "sr", + "sv", + "ta", + "te", + "th", + "tl", + "tr", + "uk", + "vi", + "zh-CN", + "zh-TW", +] + +AVOID = ["tolls", "highways", "ferries", "indoor"] +TRANSIT_PREFS = ["less_walking", "fewer_transfers"] +TRANSPORT_TYPE = ["bus", "subway", "train", "tram", "rail"] +TRAVEL_MODE = ["driving", "walking", "bicycling", "transit"] +TRAVEL_MODEL = ["best_guess", "pessimistic", "optimistic"] +UNITS = [CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL] diff --git a/homeassistant/components/google_travel_time/helpers.py b/homeassistant/components/google_travel_time/helpers.py new file mode 100644 index 0000000000000..425d21ee18154 --- /dev/null +++ b/homeassistant/components/google_travel_time/helpers.py @@ -0,0 +1,72 @@ +"""Helpers for Google Time Travel integration.""" +from googlemaps import Client +from googlemaps.distance_matrix import distance_matrix +from googlemaps.exceptions import ApiError + +from homeassistant.components.google_travel_time.const import TRACKABLE_DOMAINS +from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE +from homeassistant.helpers import location + + +def is_valid_config_entry(hass, logger, api_key, origin, destination): + """Return whether the config entry data is valid.""" + origin = resolve_location(hass, logger, origin) + destination = resolve_location(hass, logger, destination) + client = Client(api_key, timeout=10) + try: + distance_matrix(client, origin, destination, mode="driving") + except ApiError: + return False + return True + + +def resolve_location(hass, logger, loc): + """Resolve a location.""" + if loc.split(".", 1)[0] in TRACKABLE_DOMAINS: + return get_location_from_entity(hass, logger, loc) + + return resolve_zone(hass, loc) + + +def get_location_from_entity(hass, logger, entity_id): + """Get the location from the entity state or attributes.""" + entity = hass.states.get(entity_id) + + if entity is None: + logger.error("Unable to find entity %s", entity_id) + return None + + # Check if the entity has location attributes + if location.has_location(entity): + return get_location_from_attributes(entity) + + # Check if device is in a zone + zone_entity = hass.states.get("zone.%s" % entity.state) + if location.has_location(zone_entity): + logger.debug( + "%s is in %s, getting zone location", entity_id, zone_entity.entity_id + ) + return get_location_from_attributes(zone_entity) + + # If zone was not found in state then use the state as the location + if entity_id.startswith("sensor."): + return entity.state + + # When everything fails just return nothing + return None + + +def get_location_from_attributes(entity): + """Get the lat/long string from an entities attributes.""" + attr = entity.attributes + return f"{attr.get(ATTR_LATITUDE)},{attr.get(ATTR_LONGITUDE)}" + + +def resolve_zone(hass, friendly_name): + """Resolve a location from a zone's friendly name.""" + entities = hass.states.all() + for entity in entities: + if entity.domain == "zone" and entity.name == friendly_name: + return get_location_from_attributes(entity) + + return friendly_name diff --git a/homeassistant/components/google_travel_time/manifest.json b/homeassistant/components/google_travel_time/manifest.json index 2d97b92ccb6b6..d8981fe4283d4 100644 --- a/homeassistant/components/google_travel_time/manifest.json +++ b/homeassistant/components/google_travel_time/manifest.json @@ -2,6 +2,9 @@ "domain": "google_travel_time", "name": "Google Maps Travel Time", "documentation": "https://www.home-assistant.io/integrations/google_travel_time", - "requirements": ["googlemaps==2.5.1"], - "codeowners": [] -} + "requirements": [ + "googlemaps==2.5.1" + ], + "codeowners": [], + "config_flow": true +} \ No newline at end of file diff --git a/homeassistant/components/google_travel_time/sensor.py b/homeassistant/components/google_travel_time/sensor.py index 11bfb871a1b3d..3980d0323b2dc 100644 --- a/homeassistant/components/google_travel_time/sensor.py +++ b/homeassistant/components/google_travel_time/sensor.py @@ -1,98 +1,60 @@ """Support for Google travel time sensors.""" +from __future__ import annotations + from datetime import datetime, timedelta import logging +from typing import Callable -import googlemaps +from googlemaps import Client +from googlemaps.distance_matrix import distance_matrix import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( ATTR_ATTRIBUTION, - ATTR_LATITUDE, - ATTR_LONGITUDE, CONF_API_KEY, CONF_MODE, CONF_NAME, EVENT_HOMEASSISTANT_START, TIME_MINUTES, ) -from homeassistant.helpers import location +from homeassistant.core import CoreState, HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util -_LOGGER = logging.getLogger(__name__) - -ATTRIBUTION = "Powered by Google" - -CONF_DESTINATION = "destination" -CONF_OPTIONS = "options" -CONF_ORIGIN = "origin" -CONF_TRAVEL_MODE = "travel_mode" +from .const import ( + ALL_LANGUAGES, + ATTRIBUTION, + AVOID, + CONF_ARRIVAL_TIME, + CONF_AVOID, + CONF_DEPARTURE_TIME, + CONF_DESTINATION, + CONF_LANGUAGE, + CONF_OPTIONS, + CONF_ORIGIN, + CONF_TRAFFIC_MODEL, + CONF_TRANSIT_MODE, + CONF_TRANSIT_ROUTING_PREFERENCE, + CONF_TRAVEL_MODE, + CONF_UNITS, + DEFAULT_NAME, + DOMAIN, + TRACKABLE_DOMAINS, + TRANSIT_PREFS, + TRANSPORT_TYPE, + TRAVEL_MODE, + TRAVEL_MODEL, + UNITS, +) +from .helpers import get_location_from_entity, is_valid_config_entry, resolve_zone -DEFAULT_NAME = "Google Travel Time" +_LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(minutes=5) -ALL_LANGUAGES = [ - "ar", - "bg", - "bn", - "ca", - "cs", - "da", - "de", - "el", - "en", - "es", - "eu", - "fa", - "fi", - "fr", - "gl", - "gu", - "hi", - "hr", - "hu", - "id", - "it", - "iw", - "ja", - "kn", - "ko", - "lt", - "lv", - "ml", - "mr", - "nl", - "no", - "pl", - "pt", - "pt-BR", - "pt-PT", - "ro", - "ru", - "sk", - "sl", - "sr", - "sv", - "ta", - "te", - "th", - "tl", - "tr", - "uk", - "vi", - "zh-CN", - "zh-TW", -] - -AVOID = ["tolls", "highways", "ferries", "indoor"] -TRANSIT_PREFS = ["less_walking", "fewer_transfers"] -TRANSPORT_TYPE = ["bus", "subway", "train", "tram", "rail"] -TRAVEL_MODE = ["driving", "walking", "bicycling", "transit"] -TRAVEL_MODEL = ["best_guess", "pessimistic", "optimistic"] -UNITS = ["metric", "imperial"] - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, @@ -105,23 +67,22 @@ vol.Schema( { vol.Optional(CONF_MODE, default="driving"): vol.In(TRAVEL_MODE), - vol.Optional("language"): vol.In(ALL_LANGUAGES), - vol.Optional("avoid"): vol.In(AVOID), - vol.Optional("units"): vol.In(UNITS), - vol.Exclusive("arrival_time", "time"): cv.string, - vol.Exclusive("departure_time", "time"): cv.string, - vol.Optional("traffic_model"): vol.In(TRAVEL_MODEL), - vol.Optional("transit_mode"): vol.In(TRANSPORT_TYPE), - vol.Optional("transit_routing_preference"): vol.In(TRANSIT_PREFS), + vol.Optional(CONF_LANGUAGE): vol.In(ALL_LANGUAGES), + vol.Optional(CONF_AVOID): vol.In(AVOID), + vol.Optional(CONF_UNITS): vol.In(UNITS), + vol.Exclusive(CONF_ARRIVAL_TIME, "time"): cv.string, + vol.Exclusive(CONF_DEPARTURE_TIME, "time"): cv.string, + vol.Optional(CONF_TRAFFIC_MODEL): vol.In(TRAVEL_MODEL), + vol.Optional(CONF_TRANSIT_MODE): vol.In(TRANSPORT_TYPE), + vol.Optional(CONF_TRANSIT_ROUTING_PREFERENCE): vol.In( + TRANSIT_PREFS + ), } ), ), } ) -TRACKABLE_DOMAINS = ["device_tracker", "sensor", "zone", "person"] -DATA_KEY = "google_travel_time" - def convert_time_to_utc(timestr): """Take a string like 08:00:00 and convert it to a unix timestamp.""" @@ -133,63 +94,88 @@ def convert_time_to_utc(timestr): return dt_util.as_timestamp(combined) -def setup_platform(hass, config, add_entities_callback, discovery_info=None): - """Set up the Google travel time platform.""" - - def run_setup(event): - """ - Delay the setup until Home Assistant is fully initialized. - - This allows any entities to be created already - """ - hass.data.setdefault(DATA_KEY, []) - options = config.get(CONF_OPTIONS) +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: Callable[[list[SensorEntity], bool], None], +) -> None: + """Set up a Google travel time sensor entry.""" + name = None + if not config_entry.options: + new_data = config_entry.data.copy() + options = new_data.pop(CONF_OPTIONS, {}) + name = new_data.pop(CONF_NAME, None) - if options.get("units") is None: - options["units"] = hass.config.units.name + if CONF_UNITS not in options: + options[CONF_UNITS] = hass.config.units.name - travel_mode = config.get(CONF_TRAVEL_MODE) - mode = options.get(CONF_MODE) - - if travel_mode is not None: + if CONF_TRAVEL_MODE in new_data: wstr = ( "Google Travel Time: travel_mode is deprecated, please " "add mode to the options dictionary instead!" ) _LOGGER.warning(wstr) - if mode is None: + travel_mode = new_data.pop(CONF_TRAVEL_MODE) + if CONF_MODE not in options: options[CONF_MODE] = travel_mode - titled_mode = options.get(CONF_MODE).title() - formatted_name = f"{DEFAULT_NAME} - {titled_mode}" - name = config.get(CONF_NAME, formatted_name) - api_key = config.get(CONF_API_KEY) - origin = config.get(CONF_ORIGIN) - destination = config.get(CONF_DESTINATION) + if CONF_MODE not in options: + options[CONF_MODE] = "driving" - sensor = GoogleTravelTimeSensor( - hass, name, api_key, origin, destination, options + hass.config_entries.async_update_entry( + config_entry, data=new_data, options=options ) - hass.data[DATA_KEY].append(sensor) - if sensor.valid_api_connection: - add_entities_callback([sensor]) + api_key = config_entry.data[CONF_API_KEY] + origin = config_entry.data[CONF_ORIGIN] + destination = config_entry.data[CONF_DESTINATION] + name = name or f"{DEFAULT_NAME}: {origin} -> {destination}" + + if not await hass.async_add_executor_job( + is_valid_config_entry, hass, _LOGGER, api_key, origin, destination + ): + raise ConfigEntryNotReady + + client = Client(api_key, timeout=10) + + sensor = GoogleTravelTimeSensor( + config_entry, name, api_key, origin, destination, client + ) + + async_add_entities([sensor], False) - # Wait until start event is sent to load this component. - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, run_setup) + +async def async_setup_platform( + hass: HomeAssistant, config, add_entities_callback, discovery_info=None +): + """Set up the Google travel time platform.""" + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, + ) + ) + + _LOGGER.warning( + "Your Google travel time configuration has been imported into the UI; " + "please remove it from configuration.yaml as support for it will be " + "removed in a future release" + ) class GoogleTravelTimeSensor(SensorEntity): """Representation of a Google travel time sensor.""" - def __init__(self, hass, name, api_key, origin, destination, options): + def __init__(self, config_entry, name, api_key, origin, destination, client): """Initialize the sensor.""" - self._hass = hass self._name = name - self._options = options + self._config_entry = config_entry self._unit_of_measurement = TIME_MINUTES self._matrix = None - self.valid_api_connection = True + self._api_key = api_key + self._unique_id = config_entry.unique_id + self._client = client # Check if location is a trackable entity if origin.split(".", 1)[0] in TRACKABLE_DOMAINS: @@ -202,13 +188,14 @@ def __init__(self, hass, name, api_key, origin, destination, options): else: self._destination = destination - self._client = googlemaps.Client(api_key, timeout=10) - try: - self.update() - except googlemaps.exceptions.ApiError as exp: - _LOGGER.error(exp) - self.valid_api_connection = False - return + async def async_added_to_hass(self) -> None: + """Handle when entity is added.""" + if self.hass.state != CoreState.running: + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_START, self.first_update + ) + else: + await self.first_update() @property def state(self): @@ -223,6 +210,20 @@ def state(self): return round(_data["duration"]["value"] / 60) return None + @property + def device_info(self): + """Return device specific attributes.""" + return { + "name": DOMAIN, + "identifiers": {(DOMAIN, self._api_key)}, + "entry_type": "service", + } + + @property + def unique_id(self) -> str: + """Return unique ID of entity.""" + return self._unique_id + @property def name(self): """Get the name of the sensor.""" @@ -235,7 +236,8 @@ def extra_state_attributes(self): return None res = self._matrix.copy() - res.update(self._options) + options = self._config_entry.options.copy() + res.update(options) del res["rows"] _data = self._matrix["rows"][0]["elements"][0] if "duration_in_traffic" in _data: @@ -254,78 +256,43 @@ def unit_of_measurement(self): """Return the unit this state is expressed in.""" return self._unit_of_measurement + async def first_update(self, _=None): + """Run the first update and write the state.""" + await self.hass.async_add_executor_job(self.update) + self.async_write_ha_state() + def update(self): """Get the latest data from Google.""" - options_copy = self._options.copy() - dtime = options_copy.get("departure_time") - atime = options_copy.get("arrival_time") + options_copy = self._config_entry.options.copy() + dtime = options_copy.get(CONF_DEPARTURE_TIME) + atime = options_copy.get(CONF_ARRIVAL_TIME) if dtime is not None and ":" in dtime: - options_copy["departure_time"] = convert_time_to_utc(dtime) + options_copy[CONF_DEPARTURE_TIME] = convert_time_to_utc(dtime) elif dtime is not None: - options_copy["departure_time"] = dtime + options_copy[CONF_DEPARTURE_TIME] = dtime elif atime is None: - options_copy["departure_time"] = "now" + options_copy[CONF_DEPARTURE_TIME] = "now" if atime is not None and ":" in atime: - options_copy["arrival_time"] = convert_time_to_utc(atime) + options_copy[CONF_ARRIVAL_TIME] = convert_time_to_utc(atime) elif atime is not None: - options_copy["arrival_time"] = atime + options_copy[CONF_ARRIVAL_TIME] = atime # Convert device_trackers to google friendly location if hasattr(self, "_origin_entity_id"): - self._origin = self._get_location_from_entity(self._origin_entity_id) + self._origin = get_location_from_entity( + self.hass, _LOGGER, self._origin_entity_id + ) if hasattr(self, "_destination_entity_id"): - self._destination = self._get_location_from_entity( - self._destination_entity_id + self._destination = get_location_from_entity( + self.hass, _LOGGER, self._destination_entity_id ) - self._destination = self._resolve_zone(self._destination) - self._origin = self._resolve_zone(self._origin) + self._destination = resolve_zone(self.hass, self._destination) + self._origin = resolve_zone(self.hass, self._origin) if self._destination is not None and self._origin is not None: - self._matrix = self._client.distance_matrix( - self._origin, self._destination, **options_copy + self._matrix = distance_matrix( + self._client, self._origin, self._destination, **options_copy ) - - def _get_location_from_entity(self, entity_id): - """Get the location from the entity state or attributes.""" - entity = self._hass.states.get(entity_id) - - if entity is None: - _LOGGER.error("Unable to find entity %s", entity_id) - self.valid_api_connection = False - return None - - # Check if the entity has location attributes - if location.has_location(entity): - return self._get_location_from_attributes(entity) - - # Check if device is in a zone - zone_entity = self._hass.states.get("zone.%s" % entity.state) - if location.has_location(zone_entity): - _LOGGER.debug( - "%s is in %s, getting zone location", entity_id, zone_entity.entity_id - ) - return self._get_location_from_attributes(zone_entity) - - # If zone was not found in state then use the state as the location - if entity_id.startswith("sensor."): - return entity.state - - # When everything fails just return nothing - return None - - @staticmethod - def _get_location_from_attributes(entity): - """Get the lat/long string from an entities attributes.""" - attr = entity.attributes - return f"{attr.get(ATTR_LATITUDE)},{attr.get(ATTR_LONGITUDE)}" - - def _resolve_zone(self, friendly_name): - entities = self._hass.states.all() - for entity in entities: - if entity.domain == "zone" and entity.name == friendly_name: - return self._get_location_from_attributes(entity) - - return friendly_name diff --git a/homeassistant/components/google_travel_time/strings.json b/homeassistant/components/google_travel_time/strings.json new file mode 100644 index 0000000000000..8dcc8f2fa1bc6 --- /dev/null +++ b/homeassistant/components/google_travel_time/strings.json @@ -0,0 +1,38 @@ +{ + "title": "Google Maps Travel Time", + "config": { + "step": { + "user": { + "description": "When specifying the origin and destination, you can supply one or more locations separated by the pipe character, in the form of an address, latitude/longitude coordinates, or a Google place ID. When specifying the location using a Google place ID, the ID must be prefixed with `place_id:`.", + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]", + "origin": "Origin", + "destination": "Destination" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_location%]" + } + }, + "options": { + "step": { + "init": { + "description": "You can optionally specify either a Departure Time or Arrival Time. If specifying a departure time, you can enter `now`, a Unix timestamp, or a 24 hour time string like `08:00:00`. If specifying an arrival time, you can use a Unix timestamp or a 24 hour time string like `08:00:00`", + "data": { + "mode": "Travel Mode", + "language": "Language", + "time_type": "Time Type", + "time": "Time", + "avoid": "Avoid", + "transit_mode": "Transit Mode", + "transit_routing_preference": "Transit Routing Preference", + "units": "Units" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_travel_time/translations/en.json b/homeassistant/components/google_travel_time/translations/en.json new file mode 100644 index 0000000000000..1f2d2c549c92f --- /dev/null +++ b/homeassistant/components/google_travel_time/translations/en.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Location is already configured" + }, + "step": { + "options": { + "data": { + "arrival_time": "Arrival Time", + "avoid": "Avoid", + "departure_time": "Departure Time", + "language": "Language", + "mode": "Travel Mode", + "transit_mode": "Transit Mode", + "transit_routing_preference": "Transit Routing Preference", + "units": "Units" + }, + "description": "You can either specify Departure Time or Arrival Time, but not both" + }, + "user": { + "data": { + "api_key": "API Key", + "destination": "Destination", + "name": "Name", + "origin": "Origin" + }, + "description": "When specifying the origin and destination, you can supply one or more locations separated by the pipe character, in the form of an address, latitude/longitude coordinates, or a Google place ID. When specifying the location using a Google place ID, the ID must be prefixed with `place_id:`." + } + } + }, + "title": "Google Maps Travel Time" +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index d66736b2b3aed..b88da6aa271c9 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -84,6 +84,7 @@ "glances", "goalzero", "gogogate2", + "google_travel_time", "gpslogger", "gree", "guardian", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 71a5a9213f911..34ca346d86bb6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -372,6 +372,9 @@ google-cloud-pubsub==2.1.0 # homeassistant.components.nest google-nest-sdm==0.2.12 +# homeassistant.components.google_travel_time +googlemaps==2.5.1 + # homeassistant.components.gree greeclimate==0.10.3 diff --git a/tests/components/google_travel_time/__init__.py b/tests/components/google_travel_time/__init__.py new file mode 100644 index 0000000000000..7a24541000145 --- /dev/null +++ b/tests/components/google_travel_time/__init__.py @@ -0,0 +1 @@ +"""Tests for the Google Maps Travel Time integration.""" diff --git a/tests/components/google_travel_time/conftest.py b/tests/components/google_travel_time/conftest.py new file mode 100644 index 0000000000000..3c8d897aadd17 --- /dev/null +++ b/tests/components/google_travel_time/conftest.py @@ -0,0 +1,59 @@ +"""Fixtures for Google Time Travel tests.""" +from unittest.mock import Mock, patch + +from googlemaps.exceptions import ApiError +import pytest + + +@pytest.fixture(name="skip_notifications", autouse=True) +def skip_notifications_fixture(): + """Skip notification calls.""" + with patch("homeassistant.components.persistent_notification.async_create"), patch( + "homeassistant.components.persistent_notification.async_dismiss" + ): + yield + + +@pytest.fixture(name="validate_config_entry") +def validate_config_entry_fixture(): + """Return valid config entry.""" + with patch( + "homeassistant.components.google_travel_time.helpers.Client", + return_value=Mock(), + ), patch( + "homeassistant.components.google_travel_time.helpers.distance_matrix", + return_value=None, + ): + yield + + +@pytest.fixture(name="bypass_setup") +def bypass_setup_fixture(): + """Bypass entry setup.""" + with patch( + "homeassistant.components.google_travel_time.async_setup", return_value=True + ), patch( + "homeassistant.components.google_travel_time.async_setup_entry", + return_value=True, + ): + yield + + +@pytest.fixture(name="bypass_update") +def bypass_update_fixture(): + """Bypass sensor update.""" + with patch("homeassistant.components.google_travel_time.sensor.distance_matrix"): + yield + + +@pytest.fixture(name="invalidate_config_entry") +def invalidate_config_entry_fixture(): + """Return invalid config entry.""" + with patch( + "homeassistant.components.google_travel_time.helpers.Client", + return_value=Mock(), + ), patch( + "homeassistant.components.google_travel_time.helpers.distance_matrix", + side_effect=ApiError("test"), + ): + yield diff --git a/tests/components/google_travel_time/test_config_flow.py b/tests/components/google_travel_time/test_config_flow.py new file mode 100644 index 0000000000000..64dc77903ff51 --- /dev/null +++ b/tests/components/google_travel_time/test_config_flow.py @@ -0,0 +1,297 @@ +"""Test the Google Maps Travel Time config flow.""" +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.google_travel_time.const import ( + ARRIVAL_TIME, + CONF_ARRIVAL_TIME, + CONF_AVOID, + CONF_DEPARTURE_TIME, + CONF_DESTINATION, + CONF_LANGUAGE, + CONF_OPTIONS, + CONF_ORIGIN, + CONF_TIME, + CONF_TIME_TYPE, + CONF_TRAFFIC_MODEL, + CONF_TRANSIT_MODE, + CONF_TRANSIT_ROUTING_PREFERENCE, + CONF_UNITS, + DEFAULT_NAME, + DEPARTURE_TIME, + DOMAIN, +) +from homeassistant.const import ( + CONF_API_KEY, + CONF_MODE, + CONF_NAME, + CONF_UNIT_SYSTEM_IMPERIAL, +) + +from tests.common import MockConfigEntry + + +async def test_minimum_fields(hass, validate_config_entry, bypass_setup): + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_API_KEY: "api_key", + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + }, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == f"{DEFAULT_NAME}: location1 -> location2" + assert result2["data"] == { + CONF_API_KEY: "api_key", + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + } + + +async def test_invalid_config_entry(hass, invalidate_config_entry): + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {} + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_API_KEY: "api_key", + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + }, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_options_flow(hass, validate_config_entry, bypass_update): + """Test options flow.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_API_KEY: "api_key", + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + }, + options={ + CONF_MODE: "driving", + CONF_ARRIVAL_TIME: "test", + CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL, + }, + ) + 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, data=None) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_MODE: "driving", + CONF_LANGUAGE: "en", + CONF_AVOID: "tolls", + CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL, + CONF_TIME_TYPE: ARRIVAL_TIME, + CONF_TIME: "test", + CONF_TRAFFIC_MODEL: "best_guess", + CONF_TRANSIT_MODE: "train", + CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "" + assert result["data"] == { + CONF_MODE: "driving", + CONF_LANGUAGE: "en", + CONF_AVOID: "tolls", + CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL, + CONF_ARRIVAL_TIME: "test", + CONF_TRAFFIC_MODEL: "best_guess", + CONF_TRANSIT_MODE: "train", + CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", + } + + assert entry.options == { + CONF_MODE: "driving", + CONF_LANGUAGE: "en", + CONF_AVOID: "tolls", + CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL, + CONF_ARRIVAL_TIME: "test", + CONF_TRAFFIC_MODEL: "best_guess", + CONF_TRANSIT_MODE: "train", + CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", + } + + +async def test_options_flow_departure_time(hass, validate_config_entry, bypass_update): + """Test options flow wiith departure time.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_API_KEY: "api_key", + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + }, + ) + 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, data=None) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_MODE: "driving", + CONF_LANGUAGE: "en", + CONF_AVOID: "tolls", + CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL, + CONF_TIME_TYPE: DEPARTURE_TIME, + CONF_TIME: "test", + CONF_TRAFFIC_MODEL: "best_guess", + CONF_TRANSIT_MODE: "train", + CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "" + assert result["data"] == { + CONF_MODE: "driving", + CONF_LANGUAGE: "en", + CONF_AVOID: "tolls", + CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL, + CONF_DEPARTURE_TIME: "test", + CONF_TRAFFIC_MODEL: "best_guess", + CONF_TRANSIT_MODE: "train", + CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", + } + + assert entry.options == { + CONF_MODE: "driving", + CONF_LANGUAGE: "en", + CONF_AVOID: "tolls", + CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL, + CONF_DEPARTURE_TIME: "test", + CONF_TRAFFIC_MODEL: "best_guess", + CONF_TRANSIT_MODE: "train", + CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", + } + + +async def test_dupe_id(hass, validate_config_entry, bypass_setup): + """Test setting up the same entry twice fails.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_API_KEY: "test", + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + }, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_API_KEY: "test", + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["reason"] == "already_configured" + + +async def test_import_flow(hass, validate_config_entry, bypass_update): + """Test import_flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_API_KEY: "api_key", + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + CONF_NAME: "test_name", + CONF_OPTIONS: { + CONF_MODE: "driving", + CONF_LANGUAGE: "en", + CONF_AVOID: "tolls", + CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL, + CONF_ARRIVAL_TIME: "test", + CONF_TRAFFIC_MODEL: "best_guess", + CONF_TRANSIT_MODE: "train", + CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", + }, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "test_name" + assert result["data"] == { + CONF_API_KEY: "api_key", + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + CONF_NAME: "test_name", + CONF_OPTIONS: { + CONF_MODE: "driving", + CONF_LANGUAGE: "en", + CONF_AVOID: "tolls", + CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL, + CONF_ARRIVAL_TIME: "test", + CONF_TRAFFIC_MODEL: "best_guess", + CONF_TRANSIT_MODE: "train", + CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", + }, + } + + entry = hass.config_entries.async_entries(DOMAIN)[0] + assert entry.data == { + CONF_API_KEY: "api_key", + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + } + assert entry.options == { + CONF_MODE: "driving", + CONF_LANGUAGE: "en", + CONF_AVOID: "tolls", + CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL, + CONF_ARRIVAL_TIME: "test", + CONF_TRAFFIC_MODEL: "best_guess", + CONF_TRANSIT_MODE: "train", + CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", + } From b58d6a6293ef3f5bd537559516997858abcabcac Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 31 Mar 2021 23:16:50 +0200 Subject: [PATCH 0002/1317] Bump version to 2021.5.0dev0 (#48559) --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 5dbd9556cd93e..ea86400d963f1 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,6 +1,6 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 2021 -MINOR_VERSION = 4 +MINOR_VERSION = 5 PATCH_VERSION = "0.dev0" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" From 1de6fed4b645ed18e1a709c9036a4d98c6b96e12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 1 Apr 2021 01:58:48 +0200 Subject: [PATCH 0003/1317] Remove analytics from default_config (#48566) --- homeassistant/components/default_config/manifest.json | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/default_config/manifest.json b/homeassistant/components/default_config/manifest.json index fa7f547869df1..0f4b940cc3681 100644 --- a/homeassistant/components/default_config/manifest.json +++ b/homeassistant/components/default_config/manifest.json @@ -3,7 +3,6 @@ "name": "Default Config", "documentation": "https://www.home-assistant.io/integrations/default_config", "dependencies": [ - "analytics", "automation", "cloud", "counter", From a0483165daea4995c258d40dfff703f5777fe52f Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 1 Apr 2021 00:03:55 +0000 Subject: [PATCH 0004/1317] [ci skip] Translation update --- .../components/adguard/translations/en.json | 4 +- .../components/adguard/translations/et.json | 4 +- .../components/adguard/translations/nl.json | 4 +- .../adguard/translations/zh-Hant.json | 4 +- .../components/almond/translations/en.json | 4 +- .../components/almond/translations/et.json | 4 +- .../components/almond/translations/nl.json | 4 +- .../almond/translations/zh-Hant.json | 4 +- .../components/arcam_fmj/translations/de.json | 4 ++ .../components/awair/translations/de.json | 3 +- .../components/axis/translations/de.json | 10 +++++ .../components/blink/translations/de.json | 11 ++++++ .../components/braviatv/translations/de.json | 3 +- .../components/bsblan/translations/de.json | 4 +- .../components/cloud/translations/nl.json | 6 +-- .../components/deconz/translations/en.json | 4 +- .../components/deconz/translations/et.json | 4 +- .../components/deconz/translations/nl.json | 4 +- .../deconz/translations/zh-Hant.json | 4 +- .../components/denonavr/translations/de.json | 27 ++++++++++++- .../components/dexcom/translations/de.json | 4 +- .../flick_electric/translations/de.json | 3 +- .../forked_daapd/translations/de.json | 3 +- .../components/gios/translations/nl.json | 2 +- .../google_travel_time/translations/en.json | 32 +++++++++------- .../google_travel_time/translations/et.json | 38 +++++++++++++++++++ .../google_travel_time/translations/nl.json | 38 +++++++++++++++++++ .../home_plus_control/translations/de.json | 21 ++++++++++ .../home_plus_control/translations/et.json | 2 +- .../homeassistant/translations/nl.json | 2 +- .../components/homekit/translations/de.json | 3 ++ .../components/homekit/translations/en.json | 21 +++++++++- .../components/homekit/translations/et.json | 2 +- .../homekit/translations/zh-Hant.json | 2 +- .../huisbaasje/translations/de.json | 1 + .../components/isy994/translations/de.json | 1 + .../lutron_caseta/translations/de.json | 3 ++ .../components/metoffice/translations/de.json | 4 +- .../components/mqtt/translations/en.json | 4 +- .../components/mqtt/translations/et.json | 4 +- .../components/mqtt/translations/nl.json | 4 +- .../components/mqtt/translations/zh-Hant.json | 4 +- .../opentherm_gw/translations/nl.json | 3 +- .../opentherm_gw/translations/no.json | 3 +- .../components/poolsense/translations/de.json | 3 +- .../components/powerwall/translations/de.json | 3 +- .../components/roomba/translations/nl.json | 3 +- .../components/roomba/translations/no.json | 7 ++-- .../roomba/translations/zh-Hant.json | 7 ++-- .../components/sms/translations/de.json | 3 +- .../squeezebox/translations/de.json | 7 +++- .../components/upb/translations/de.json | 1 + .../components/verisure/translations/de.json | 5 ++- .../components/vizio/translations/de.json | 3 +- .../components/withings/translations/de.json | 2 + .../xiaomi_aqara/translations/de.json | 11 +++++- .../components/zha/translations/de.json | 1 + .../components/zha/translations/nl.json | 1 + .../components/zha/translations/no.json | 1 + .../components/zha/translations/zh-Hant.json | 1 + 60 files changed, 297 insertions(+), 82 deletions(-) create mode 100644 homeassistant/components/google_travel_time/translations/et.json create mode 100644 homeassistant/components/google_travel_time/translations/nl.json create mode 100644 homeassistant/components/home_plus_control/translations/de.json diff --git a/homeassistant/components/adguard/translations/en.json b/homeassistant/components/adguard/translations/en.json index 6c1ad2008cea6..5e09b42b9f2cc 100644 --- a/homeassistant/components/adguard/translations/en.json +++ b/homeassistant/components/adguard/translations/en.json @@ -9,8 +9,8 @@ }, "step": { "hassio_confirm": { - "description": "Do you want to configure Home Assistant to connect to the AdGuard Home provided by the Hass.io add-on: {addon}?", - "title": "AdGuard Home via Hass.io add-on" + "description": "Do you want to configure Home Assistant to connect to the AdGuard Home provided by the add-on: {addon}?", + "title": "AdGuard Home via Home Assistant add-on" }, "user": { "data": { diff --git a/homeassistant/components/adguard/translations/et.json b/homeassistant/components/adguard/translations/et.json index 800b7c37c493d..18e67dedb3658 100644 --- a/homeassistant/components/adguard/translations/et.json +++ b/homeassistant/components/adguard/translations/et.json @@ -9,8 +9,8 @@ }, "step": { "hassio_confirm": { - "description": "Kas soovid seadistada Home Assistant-i \u00fchenduse AdGuard Home'iga mida pakub Hass.io lisandmoodul: {addon} ?", - "title": "AdGuard Home Hass.io lisandmooduli abil" + "description": "Kas soovid seadistada Home Assistant-i \u00fchenduse AdGuard Home'iga mida pakub lisandmoodul: {addon} ?", + "title": "AdGuard Home Home Assistanti lisandmooduli abil" }, "user": { "data": { diff --git a/homeassistant/components/adguard/translations/nl.json b/homeassistant/components/adguard/translations/nl.json index 205193be8f8d9..a1bfaad6e05a1 100644 --- a/homeassistant/components/adguard/translations/nl.json +++ b/homeassistant/components/adguard/translations/nl.json @@ -9,8 +9,8 @@ }, "step": { "hassio_confirm": { - "description": "Wilt u Home Assistant configureren om verbinding te maken met AdGuard Home van de Supervisor-add-on: {addon}?", - "title": "AdGuard Home via Supervisor add-on" + "description": "Wilt u Home Assistant configureren om verbinding te maken met AdGuard Home van de Home Assistant add-on: {addon}?", + "title": "AdGuard Home via Home Assistant add-on" }, "user": { "data": { diff --git a/homeassistant/components/adguard/translations/zh-Hant.json b/homeassistant/components/adguard/translations/zh-Hant.json index 250b2e0d891d0..69d24d1fa7f35 100644 --- a/homeassistant/components/adguard/translations/zh-Hant.json +++ b/homeassistant/components/adguard/translations/zh-Hant.json @@ -9,8 +9,8 @@ }, "step": { "hassio_confirm": { - "description": "\u662f\u5426\u8981\u8a2d\u5b9a Home Assistant \u4ee5\u4f7f\u7528 Hass.io \u9644\u52a0\u5143\u4ef6\uff1a{addon} \u9023\u7dda\u81f3 AdGuard Home\uff1f", - "title": "\u4f7f\u7528 Hass.io \u9644\u52a0\u5143\u4ef6 AdGuard Home" + "description": "\u662f\u5426\u8981\u8a2d\u5b9a Home Assistant \u4ee5\u9023\u7dda\u81f3 AdGuard Home\u3002\u9644\u52a0\u5143\u4ef6\u70ba\uff1a{addon} \uff1f", + "title": "\u4f7f\u7528 Home Assistant \u9644\u52a0\u5143\u4ef6 AdGuard Home" }, "user": { "data": { diff --git a/homeassistant/components/almond/translations/en.json b/homeassistant/components/almond/translations/en.json index b7f76e8933b1c..fb7d41273524f 100644 --- a/homeassistant/components/almond/translations/en.json +++ b/homeassistant/components/almond/translations/en.json @@ -8,8 +8,8 @@ }, "step": { "hassio_confirm": { - "description": "Do you want to configure Home Assistant to connect to Almond provided by the Hass.io add-on: {addon}?", - "title": "Almond via Hass.io add-on" + "description": "Do you want to configure Home Assistant to connect to Almond provided by the add-on: {addon}?", + "title": "Almond via Home Assistant add-on" }, "pick_implementation": { "title": "Pick Authentication Method" diff --git a/homeassistant/components/almond/translations/et.json b/homeassistant/components/almond/translations/et.json index c8646d6f09072..5b15d9328cce7 100644 --- a/homeassistant/components/almond/translations/et.json +++ b/homeassistant/components/almond/translations/et.json @@ -8,8 +8,8 @@ }, "step": { "hassio_confirm": { - "description": "Kas soovid seadistada Home Assistant-i \u00fchendust Almondiga mida pakub Hass.io lisandmoodul: {addon} ?", - "title": "Almond Hass.io lisandmooduli abil" + "description": "Kas soovid seadistada Home Assistant-i \u00fchendust Almondiga mida pakub lisandmoodul: {addon} ?", + "title": "Almond Home Assistanti lisandmooduli abil" }, "pick_implementation": { "title": "Vali tuvastusmeetod" diff --git a/homeassistant/components/almond/translations/nl.json b/homeassistant/components/almond/translations/nl.json index 43d90100e935e..dbf4c485d345d 100644 --- a/homeassistant/components/almond/translations/nl.json +++ b/homeassistant/components/almond/translations/nl.json @@ -8,8 +8,8 @@ }, "step": { "hassio_confirm": { - "description": "Wilt u Home Assistant configureren om verbinding te maken met Almond die wordt aangeboden door de Supervisor add-on {addon} ?", - "title": "Almond via Supervisor add-on" + "description": "Wilt u Home Assistant configureren om verbinding te maken met Almond die wordt aangeboden door de add-on {addon} ?", + "title": "Almond via Home Assistant add-on" }, "pick_implementation": { "title": "Kies een authenticatie methode" diff --git a/homeassistant/components/almond/translations/zh-Hant.json b/homeassistant/components/almond/translations/zh-Hant.json index c8004ecde4f1d..9606a440aab9a 100644 --- a/homeassistant/components/almond/translations/zh-Hant.json +++ b/homeassistant/components/almond/translations/zh-Hant.json @@ -8,8 +8,8 @@ }, "step": { "hassio_confirm": { - "description": "\u662f\u5426\u8981\u8a2d\u5b9a Home Assistant \u4ee5\u4f7f\u7528 Hass.io \u9644\u52a0\u5143\u4ef6\uff1a{addon} \u9023\u7dda\u81f3 Almond\uff1f", - "title": "\u4f7f\u7528 Hass.io \u9644\u52a0\u5143\u4ef6 Almond" + "description": "\u662f\u5426\u8981\u8a2d\u5b9a Home Assistant \u4ee5\u9023\u7dda\u81f3 Almond\u3002\u9644\u52a0\u5143\u4ef6\u70ba\uff1a{addon} \uff1f", + "title": "\u4f7f\u7528 Home Assistant \u9644\u52a0\u5143\u4ef6 Almond" }, "pick_implementation": { "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" diff --git a/homeassistant/components/arcam_fmj/translations/de.json b/homeassistant/components/arcam_fmj/translations/de.json index b7270e730bb93..1f67a8d30a94e 100644 --- a/homeassistant/components/arcam_fmj/translations/de.json +++ b/homeassistant/components/arcam_fmj/translations/de.json @@ -5,7 +5,11 @@ "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", "cannot_connect": "Verbindung fehlgeschlagen" }, + "flow_title": "Arcam FMJ auf {host}", "step": { + "confirm": { + "description": "M\u00f6chtest du Arcam FMJ auf `{host}` zum Home Assistant hinzuf\u00fcgen?" + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/awair/translations/de.json b/homeassistant/components/awair/translations/de.json index 5b65ece083b06..1dacaf099dc76 100644 --- a/homeassistant/components/awair/translations/de.json +++ b/homeassistant/components/awair/translations/de.json @@ -21,7 +21,8 @@ "data": { "access_token": "Zugangstoken", "email": "E-Mail" - } + }, + "description": "Du musst dich f\u00fcr ein Awair Entwickler-Zugangs-Token registrieren unter: https://developer.getawair.com/onboard/login" } } } diff --git a/homeassistant/components/axis/translations/de.json b/homeassistant/components/axis/translations/de.json index 1f6aedf5d9c7e..ed95dea6fc12b 100644 --- a/homeassistant/components/axis/translations/de.json +++ b/homeassistant/components/axis/translations/de.json @@ -23,5 +23,15 @@ "title": "Axis Ger\u00e4t einrichten" } } + }, + "options": { + "step": { + "configure_stream": { + "data": { + "stream_profile": "Zu verwendendes Stream-Profil ausw\u00e4hlen" + }, + "title": "Optionen des Axis Videostream-Ger\u00e4ts" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/blink/translations/de.json b/homeassistant/components/blink/translations/de.json index d4f65329f9b07..86fa2b609ad6b 100644 --- a/homeassistant/components/blink/translations/de.json +++ b/homeassistant/components/blink/translations/de.json @@ -25,5 +25,16 @@ "title": "Anmelden mit Blink-Konto" } } + }, + "options": { + "step": { + "simple_options": { + "data": { + "scan_interval": "Scanintervall (Sekunden)" + }, + "description": "Blink-Integration konfigurieren", + "title": "Blink Optionen" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/braviatv/translations/de.json b/homeassistant/components/braviatv/translations/de.json index 8ac8c09e4fedc..7dfff8a1b440a 100644 --- a/homeassistant/components/braviatv/translations/de.json +++ b/homeassistant/components/braviatv/translations/de.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "no_ip_control": "IP-Steuerung ist auf deinen Fernseher deaktiviert oder der Fernseher wird nicht unterst\u00fctzt." }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", diff --git a/homeassistant/components/bsblan/translations/de.json b/homeassistant/components/bsblan/translations/de.json index 971e3c1ea8aaa..77a810844149a 100644 --- a/homeassistant/components/bsblan/translations/de.json +++ b/homeassistant/components/bsblan/translations/de.json @@ -13,7 +13,9 @@ "password": "Passwort", "port": "Port Nummer", "username": "Benutzername" - } + }, + "description": "Richte dein BSB-Lan Ger\u00e4t f\u00fcr die Integration mit dem Home Assistant ein.", + "title": "Verbinden mit dem BSB-Lan Ger\u00e4t" } } } diff --git a/homeassistant/components/cloud/translations/nl.json b/homeassistant/components/cloud/translations/nl.json index 0ad7a5288228e..7d02a04cd01e0 100644 --- a/homeassistant/components/cloud/translations/nl.json +++ b/homeassistant/components/cloud/translations/nl.json @@ -2,9 +2,9 @@ "system_health": { "info": { "alexa_enabled": "Alexa ingeschakeld", - "can_reach_cert_server": "Bereik Certificaatserver", - "can_reach_cloud": "Bereik Home Assistant Cloud", - "can_reach_cloud_auth": "Bereik authenticatieserver", + "can_reach_cert_server": "Certificaatserver bereikbaar", + "can_reach_cloud": "Home Assistant Cloud bereikbaar", + "can_reach_cloud_auth": "Authenticatieserver bereikbaar", "google_enabled": "Google ingeschakeld", "logged_in": "Ingelogd", "relayer_connected": "Relayer verbonden", diff --git a/homeassistant/components/deconz/translations/en.json b/homeassistant/components/deconz/translations/en.json index 014280d8cc4cd..132d8b60feaf0 100644 --- a/homeassistant/components/deconz/translations/en.json +++ b/homeassistant/components/deconz/translations/en.json @@ -14,8 +14,8 @@ "flow_title": "deCONZ Zigbee gateway ({host})", "step": { "hassio_confirm": { - "description": "Do you want to configure Home Assistant to connect to the deCONZ gateway provided by the Hass.io add-on {addon}?", - "title": "deCONZ Zigbee gateway via Hass.io add-on" + "description": "Do you want to configure Home Assistant to connect to the deCONZ gateway provided by the add-on {addon}?", + "title": "deCONZ Zigbee gateway via Home Assistant add-on" }, "link": { "description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ Settings -> Gateway -> Advanced\n2. Press \"Authenticate app\" button", diff --git a/homeassistant/components/deconz/translations/et.json b/homeassistant/components/deconz/translations/et.json index b949208a664fb..6a3b6d07592c8 100644 --- a/homeassistant/components/deconz/translations/et.json +++ b/homeassistant/components/deconz/translations/et.json @@ -14,8 +14,8 @@ "flow_title": "deCONZ Zigbee l\u00fc\u00fcs ( {host} )", "step": { "hassio_confirm": { - "description": "Kas soovid seadistada Home Assistant-i \u00fchenduse deCONZ-l\u00fc\u00fcsiga, mida pakub Hass.io lisandmoodul {addon} ?", - "title": "deCONZ Zigbee l\u00fc\u00fcs Hass.io lisandmooduli abil" + "description": "Kas soovid seadistada Home Assistant-i \u00fchenduse deCONZ-l\u00fc\u00fcsiga, mida pakub lisandmoodul {addon} ?", + "title": "deCONZ Zigbee l\u00fc\u00fcs Home Assistanti lisandmooduli abil" }, "link": { "description": "Home Assistanti registreerumiseks ava deCONZ-i l\u00fc\u00fcs.\n\n 1. Mine deCONZ Settings - > Gateway - > Advanced\n 2. Vajuta nuppu \"Authenticate app\"", diff --git a/homeassistant/components/deconz/translations/nl.json b/homeassistant/components/deconz/translations/nl.json index 833050eaf921f..18fcea974c355 100644 --- a/homeassistant/components/deconz/translations/nl.json +++ b/homeassistant/components/deconz/translations/nl.json @@ -14,8 +14,8 @@ "flow_title": "deCONZ Zigbee gateway ( {host} )", "step": { "hassio_confirm": { - "description": "Wilt u Home Assistant configureren om verbinding te maken met de deCONZ gateway van de Supervisor add-on {addon}?", - "title": "deCONZ Zigbee Gateway via Supervisor add-on" + "description": "Wilt u Home Assistant configureren om verbinding te maken met de deCONZ gateway van de Home Assistant add-on {addon}?", + "title": "deCONZ Zigbee Gateway via Home Assistant add-on" }, "link": { "description": "Ontgrendel je deCONZ gateway om te registreren met Home Assistant.\n\n1. Ga naar deCONZ systeeminstellingen (Instellingen -> Gateway -> Geavanceerd)\n2. Druk op de knop \"Gateway ontgrendelen\"", diff --git a/homeassistant/components/deconz/translations/zh-Hant.json b/homeassistant/components/deconz/translations/zh-Hant.json index c17d2038127a6..70642ace1bf9b 100644 --- a/homeassistant/components/deconz/translations/zh-Hant.json +++ b/homeassistant/components/deconz/translations/zh-Hant.json @@ -14,8 +14,8 @@ "flow_title": "deCONZ Zigbee \u9598\u9053\u5668\uff08{host}\uff09", "step": { "hassio_confirm": { - "description": "\u662f\u5426\u8981\u8a2d\u5b9a Home Assistant \u4ee5\u9023\u7dda\u81f3 Hass.io \u9644\u52a0\u5143\u4ef6 {addon} \u4e4b deCONZ \u9598\u9053\u5668\uff1f", - "title": "\u4f7f\u7528 Hass.io \u9644\u52a0\u5143\u4ef6 deCONZ Zigbee \u9598\u9053\u5668" + "description": "\u662f\u5426\u8981\u8a2d\u5b9a Home Assistant \u4ee5\u9023\u7dda\u81f3 deCONZ \u9598\u9053\u5668\u3002\u9644\u52a0\u5143\u4ef6\u70ba\uff1a{addon} \uff1f", + "title": "\u4f7f\u7528 Home Assistant \u9644\u52a0\u5143\u4ef6 deCONZ Zigbee \u9598\u9053\u5668" }, "link": { "description": "\u89e3\u9664 deCONZ \u9598\u9053\u5668\u9396\u5b9a\uff0c\u4ee5\u65bc Home Assistant \u9032\u884c\u8a3b\u518a\u3002\n\n1. \u9032\u5165 deCONZ \u7cfb\u7d71\u8a2d\u5b9a -> \u9598\u9053\u5668 -> \u9032\u968e\u8a2d\u5b9a\n2. \u6309\u4e0b\u300c\u8a8d\u8b49\u7a0b\u5f0f\uff08Authenticate app\uff09\u300d\u6309\u9215", diff --git a/homeassistant/components/denonavr/translations/de.json b/homeassistant/components/denonavr/translations/de.json index e95aeb10b1717..e1330024c5383 100644 --- a/homeassistant/components/denonavr/translations/de.json +++ b/homeassistant/components/denonavr/translations/de.json @@ -5,16 +5,39 @@ "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", "cannot_connect": "Verbindung fehlgeschlagen. Bitte versuchen Sie es noch einmal. Trennen Sie ggf. Strom- und Ethernetkabel und verbinden Sie diese erneut." }, + "error": { + "discovery_error": "Denon AVR-Netzwerk-Receiver konnte nicht gefunden werden" + }, + "flow_title": "Denon AVR-Netzwerk-Receiver: {name}", "step": { + "confirm": { + "description": "Bitte best\u00e4tige das Hinzuf\u00fcgen des Receivers", + "title": "Denon AVR-Netzwerk-Receiver" + }, "select": { "data": { "select_host": "IP-Adresse des Empf\u00e4ngers" - } + }, + "description": "F\u00fchre das Setup erneut aus, wenn du weitere Receiver verbinden m\u00f6chten", + "title": "W\u00e4hle den Receiver, den du verbinden m\u00f6chtest" }, "user": { "data": { "host": "IP-Adresse" - } + }, + "description": "Verbinde dich mit deinem Receiver, wenn die IP-Adresse nicht eingestellt ist, wird die automatische Erkennung verwendet", + "title": "Denon AVR-Netzwerk-Receiver" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_all_sources": "Alle Quellen anzeigen" + }, + "description": "Optionale Einstellungen festlegen", + "title": "Denon AVR-Netzwerk-Receiver" } } } diff --git a/homeassistant/components/dexcom/translations/de.json b/homeassistant/components/dexcom/translations/de.json index 64cdc05a08108..20e5ee22751f2 100644 --- a/homeassistant/components/dexcom/translations/de.json +++ b/homeassistant/components/dexcom/translations/de.json @@ -14,7 +14,9 @@ "password": "Passwort", "server": "Server", "username": "Benutzername" - } + }, + "description": "Anmeldedaten f\u00fcr Dexcom Share eingeben", + "title": "Dexcom-Integration einrichten" } } }, diff --git a/homeassistant/components/flick_electric/translations/de.json b/homeassistant/components/flick_electric/translations/de.json index 5cca1386ffb10..13ae855560840 100644 --- a/homeassistant/components/flick_electric/translations/de.json +++ b/homeassistant/components/flick_electric/translations/de.json @@ -15,7 +15,8 @@ "client_secret": "Client Secret (optional)", "password": "Passwort", "username": "Benutzername" - } + }, + "title": "Flick Anmeldedaten" } } } diff --git a/homeassistant/components/forked_daapd/translations/de.json b/homeassistant/components/forked_daapd/translations/de.json index fcbcf6b0df0d0..be581502398e9 100644 --- a/homeassistant/components/forked_daapd/translations/de.json +++ b/homeassistant/components/forked_daapd/translations/de.json @@ -25,7 +25,8 @@ "init": { "data": { "max_playlists": "Maximale Anzahl der als Quellen verwendeten Wiedergabelisten", - "tts_pause_time": "Sekunden bis zur Pause vor und nach der TTS" + "tts_pause_time": "Sekunden bis zur Pause vor und nach der TTS", + "tts_volume": "TTS-Lautst\u00e4rke (Float im Bereich [0,1])" } } } diff --git a/homeassistant/components/gios/translations/nl.json b/homeassistant/components/gios/translations/nl.json index baac3c6dc77ab..87104523a31cf 100644 --- a/homeassistant/components/gios/translations/nl.json +++ b/homeassistant/components/gios/translations/nl.json @@ -21,7 +21,7 @@ }, "system_health": { "info": { - "can_reach_server": "Bereik GIO\u015a server" + "can_reach_server": "GIO\u015a server bereikbaar" } } } \ No newline at end of file diff --git a/homeassistant/components/google_travel_time/translations/en.json b/homeassistant/components/google_travel_time/translations/en.json index 1f2d2c549c92f..464d518a70be8 100644 --- a/homeassistant/components/google_travel_time/translations/en.json +++ b/homeassistant/components/google_travel_time/translations/en.json @@ -3,28 +3,34 @@ "abort": { "already_configured": "Location is already configured" }, + "error": { + "cannot_connect": "Failed to connect" + }, "step": { - "options": { + "user": { + "data": { + "api_key": "API Key", + "destination": "Destination", + "origin": "Origin" + }, + "description": "When specifying the origin and destination, you can supply one or more locations separated by the pipe character, in the form of an address, latitude/longitude coordinates, or a Google place ID. When specifying the location using a Google place ID, the ID must be prefixed with `place_id:`." + } + } + }, + "options": { + "step": { + "init": { "data": { - "arrival_time": "Arrival Time", "avoid": "Avoid", - "departure_time": "Departure Time", "language": "Language", "mode": "Travel Mode", + "time": "Time", + "time_type": "Time Type", "transit_mode": "Transit Mode", "transit_routing_preference": "Transit Routing Preference", "units": "Units" }, - "description": "You can either specify Departure Time or Arrival Time, but not both" - }, - "user": { - "data": { - "api_key": "API Key", - "destination": "Destination", - "name": "Name", - "origin": "Origin" - }, - "description": "When specifying the origin and destination, you can supply one or more locations separated by the pipe character, in the form of an address, latitude/longitude coordinates, or a Google place ID. When specifying the location using a Google place ID, the ID must be prefixed with `place_id:`." + "description": "You can optionally specify either a Departure Time or Arrival Time. If specifying a departure time, you can enter `now`, a Unix timestamp, or a 24 hour time string like `08:00:00`. If specifying an arrival time, you can use a Unix timestamp or a 24 hour time string like `08:00:00`" } } }, diff --git a/homeassistant/components/google_travel_time/translations/et.json b/homeassistant/components/google_travel_time/translations/et.json new file mode 100644 index 0000000000000..488a473d14f75 --- /dev/null +++ b/homeassistant/components/google_travel_time/translations/et.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "Asukoht on juba m\u00e4\u00e4ratud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus" + }, + "step": { + "user": { + "data": { + "api_key": "API v\u00f5ti", + "destination": "Sihtkoht", + "origin": "L\u00e4htekoht" + }, + "description": "L\u00e4hte- ja sihtkoha m\u00e4\u00e4ramisel v\u00f5ib sisestada \u00fche v\u00f5i mitu eraldusm\u00e4rgiga eraldatud asukohta aadressi, laius- / pikkuskraadi koordinaatide v\u00f5i Google'i koha ID kujul. Asukoha m\u00e4\u00e4ramisel Google'i koha ID abil tuleb ID-le lisada eesliide \"place_id:\"." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "avoid": "V\u00e4ldi", + "language": "Keel", + "mode": "Reisimise viis", + "time": "Aeg", + "time_type": "Aja t\u00fc\u00fcp", + "transit_mode": "Liikumisviis", + "transit_routing_preference": "Marsruudi eelistus", + "units": "\u00dchikud" + }, + "description": "Soovi korral saad m\u00e4\u00e4rata kas v\u00e4ljumisaja v\u00f5i saabumisaja. V\u00e4ljumisaja m\u00e4\u00e4ramisel saad sisestada \"kohe\", Unix-ajatempli v\u00f5i 24-tunnise ajastringi (nt 08:00:00). Saabumisaja m\u00e4\u00e4ramisel saad kasutada Unix-ajatemplit v\u00f5i 24-tunnist ajastringi nagu '08:00:00'" + } + } + }, + "title": "Google Mapsi reisiaeg" +} \ No newline at end of file diff --git a/homeassistant/components/google_travel_time/translations/nl.json b/homeassistant/components/google_travel_time/translations/nl.json new file mode 100644 index 0000000000000..7341fd0a6a2ce --- /dev/null +++ b/homeassistant/components/google_travel_time/translations/nl.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "Locatie is al geconfigureerd." + }, + "error": { + "cannot_connect": "Kan geen verbinding maken" + }, + "step": { + "user": { + "data": { + "api_key": "API-sleutel", + "destination": "Bestemming", + "origin": "Vertrekpunt" + }, + "description": "Wanneer u de oorsprong en bestemming opgeeft, kunt u een of meer locaties opgeven, gescheiden door het pijp-symbool, in de vorm van een adres, lengte- / breedtegraadco\u00f6rdinaten of een Google-plaats-ID. Wanneer u de locatie opgeeft met behulp van een Google-plaats-ID, moet de ID worden voorafgegaan door `place_id:`." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "avoid": "Vermijd", + "language": "Taal", + "mode": "Reiswijze", + "time": "Tijd", + "time_type": "Tijd Type", + "transit_mode": "Transitmodus", + "transit_routing_preference": "Transit Route Voorkeur", + "units": "Eenheden" + }, + "description": "U kunt optioneel een vertrektijd of aankomsttijd opgeven. Als u een vertrektijd opgeeft, kunt u 'nu', een Unix-tijdstempel of een 24-uurs tijdreeks zoals '08: 00: 00' invoeren. Als u een aankomsttijd specificeert, kunt u een Unix-tijdstempel of een 24-uurs tijdreeks gebruiken, zoals '08: 00: 00'" + } + } + }, + "title": "Google Maps Reistijd" +} \ No newline at end of file diff --git a/homeassistant/components/home_plus_control/translations/de.json b/homeassistant/components/home_plus_control/translations/de.json new file mode 100644 index 0000000000000..8e7d9e9bc240d --- /dev/null +++ b/homeassistant/components/home_plus_control/translations/de.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Konto wurde bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", + "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.", + "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url}).", + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, + "create_entry": { + "default": "Erfolgreich authentifiziert" + }, + "step": { + "pick_implementation": { + "title": "W\u00e4hle die Authentifizierungsmethode" + } + } + }, + "title": "" +} \ No newline at end of file diff --git a/homeassistant/components/home_plus_control/translations/et.json b/homeassistant/components/home_plus_control/translations/et.json index 0046c1f520500..cfe40d86bcd78 100644 --- a/homeassistant/components/home_plus_control/translations/et.json +++ b/homeassistant/components/home_plus_control/translations/et.json @@ -5,7 +5,7 @@ "already_in_progress": "Seadistamine on juba k\u00e4imas", "authorize_url_timeout": "Kinnitus-URLi loomise ajal\u00f5pp", "missing_configuration": "Komponent pole seadistatud. Palun loe dokumentatsiooni.", - "no_url_available": "URL-i pole saadaval. Selle t\u00f5rke kohta teabe saamiseks vaata [spikrijaotis]({docs_url})", + "no_url_available": "URL-i pole saadaval. Selle t\u00f5rke kohta teabe saamiseks vaata [check the help section]({docs_url})", "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine." }, "create_entry": { diff --git a/homeassistant/components/homeassistant/translations/nl.json b/homeassistant/components/homeassistant/translations/nl.json index b4e33dcd7aad1..6dba5eec8b94b 100644 --- a/homeassistant/components/homeassistant/translations/nl.json +++ b/homeassistant/components/homeassistant/translations/nl.json @@ -9,7 +9,7 @@ "hassio": "Supervisor", "host_os": "Home Assistant OS", "installation_type": "Type installatie", - "os_name": "Besturingssysteemfamilie", + "os_name": "Besturingssysteem", "os_version": "Versie van het besturingssysteem", "python_version": "Python-versie", "supervisor": "Supervisor", diff --git a/homeassistant/components/homekit/translations/de.json b/homeassistant/components/homekit/translations/de.json index 88583d9ca804a..18fde7a7f91e5 100644 --- a/homeassistant/components/homekit/translations/de.json +++ b/homeassistant/components/homekit/translations/de.json @@ -23,6 +23,7 @@ }, "user": { "data": { + "auto_start": "Autostart (deaktivieren, wenn Z-Wave oder ein anderes verz\u00f6gertes Startsystem verwendet wird)", "include_domains": "Einzubeziehende Domains", "mode": "Modus" }, @@ -34,6 +35,7 @@ "step": { "advanced": { "data": { + "auto_start": "Autostart (deaktivieren, wenn du den homekit.start-Dienst manuell aufrufst)", "safe_mode": "Abgesicherter Modus (nur aktivieren, wenn das Pairing fehlschl\u00e4gt)" }, "description": "Diese Einstellungen m\u00fcssen nur angepasst werden, wenn HomeKit nicht funktioniert.", @@ -43,6 +45,7 @@ "data": { "camera_copy": "Kameras, die native H.264-Streams unterst\u00fctzen" }, + "description": "Pr\u00fcfe alle Kameras, die native H.264-Streams unterst\u00fctzen. Wenn die Kamera keinen H.264-Stream ausgibt, transkodiert das System das Video in H.264 f\u00fcr HomeKit. Die Transkodierung erfordert eine leistungsstarke CPU und wird wahrscheinlich nicht auf Einplatinencomputern funktionieren.", "title": "W\u00e4hlen Sie den Kamera-Video-Codec." }, "include_exclude": { diff --git a/homeassistant/components/homekit/translations/en.json b/homeassistant/components/homekit/translations/en.json index aa78c3e4adc79..a48b6fdee240e 100644 --- a/homeassistant/components/homekit/translations/en.json +++ b/homeassistant/components/homekit/translations/en.json @@ -4,13 +4,29 @@ "port_name_in_use": "An accessory or bridge with the same name or port is already configured." }, "step": { + "accessory_mode": { + "data": { + "entity_id": "Entity" + }, + "description": "Choose the entity to be included. In accessory mode, only a single entity is included.", + "title": "Select entity to be included" + }, + "bridge_mode": { + "data": { + "include_domains": "Domains to include" + }, + "description": "Choose the domains to be included. All supported entities in the domain will be included.", + "title": "Select domains to be included" + }, "pairing": { "description": "To complete pairing following the instructions in \u201cNotifications\u201d under \u201cHomeKit Pairing\u201d.", "title": "Pair HomeKit" }, "user": { "data": { - "include_domains": "Domains to include" + "auto_start": "Autostart (disable if using Z-Wave or other delayed start system)", + "include_domains": "Domains to include", + "mode": "Mode" }, "description": "Choose the domains to be included. All supported entities in the domain will be included. A separate HomeKit instance in accessory mode will be created for each tv media player and camera.", "title": "Select domains to be included" @@ -21,7 +37,8 @@ "step": { "advanced": { "data": { - "auto_start": "Autostart (disable if you are calling the homekit.start service manually)" + "auto_start": "Autostart (disable if you are calling the homekit.start service manually)", + "safe_mode": "Safe Mode (enable only if pairing fails)" }, "description": "These settings only need to be adjusted if HomeKit is not functional.", "title": "Advanced Configuration" diff --git a/homeassistant/components/homekit/translations/et.json b/homeassistant/components/homekit/translations/et.json index 2ae8d651eb186..38d063d9bf390 100644 --- a/homeassistant/components/homekit/translations/et.json +++ b/homeassistant/components/homekit/translations/et.json @@ -55,7 +55,7 @@ "entities": "Olemid", "mode": "Re\u017eiim" }, - "description": "Vali kaasatavad olemid. Tarvikute re\u017eiimis on kaasatav ainult \u00fcks olem. Silla re\u017eiimis kaasatakse k\u00f5ik domeeni olemid, v\u00e4lja arvatud juhul, kui valitud on kindlad olemid. Silla v\u00e4listamisre\u017eiimis kaasatakse k\u00f5ik domeeni olemid, v\u00e4lja arvatud v\u00e4listatud olemid. Parima kasutuskogemuse jaoks on eraldi HomeKit seadmed iga meediumim\u00e4ngija ja kaamera jaoks.", + "description": "Vali kaasatavad olemid. Tarvikute re\u017eiimis on kaasatav ainult \u00fcks olem. Silla re\u017eiimis kaasatakse k\u00f5ik domeeni olemid, v\u00e4lja arvatud juhul, kui valitud on kindlad olemid. Silla v\u00e4listamisre\u017eiimis kaasatakse k\u00f5ik domeeni olemid, v\u00e4lja arvatud v\u00e4listatud olemid. Parima kasutuskogemuse jaoks on eraldi HomeKit seadmed iga TV meediumim\u00e4ngija, luku, juhtpuldi ja kaamera jaoks.", "title": "Vali kaasatavd olemid" }, "init": { diff --git a/homeassistant/components/homekit/translations/zh-Hant.json b/homeassistant/components/homekit/translations/zh-Hant.json index 95a0782cf1283..09f9220c20f88 100644 --- a/homeassistant/components/homekit/translations/zh-Hant.json +++ b/homeassistant/components/homekit/translations/zh-Hant.json @@ -55,7 +55,7 @@ "entities": "\u5be6\u9ad4", "mode": "\u6a21\u5f0f" }, - "description": "\u9078\u64c7\u8981\u5305\u542b\u7684\u5be6\u9ad4\u3002\u65bc\u914d\u4ef6\u6a21\u5f0f\u4e0b\u3001\u50c5\u6709\u55ae\u4e00\u5be6\u9ad4\u5c07\u6703\u5305\u542b\u3002\u65bc\u6a4b\u63a5\u5305\u542b\u6a21\u5f0f\u4e0b\u3001\u6240\u6709\u7db2\u57df\u7684\u5be6\u9ad4\u90fd\u5c07\u5305\u542b\uff0c\u9664\u975e\u9078\u64c7\u7279\u5b9a\u7684\u5be6\u9ad4\u3002\u65bc\u6a4b\u63a5\u6392\u9664\u6a21\u5f0f\u4e2d\u3001\u6240\u6709\u7db2\u57df\u4e2d\u7684\u5be6\u9ad4\u90fd\u5c07\u5305\u542b\uff0c\u9664\u4e86\u6392\u9664\u7684\u5be6\u9ad4\u3002\u70ba\u53d6\u5f97\u6700\u4f73\u6548\u80fd\u3001\u96fb\u8996\u5a92\u9ad4\u64ad\u653e\u5668\u8207\u651d\u5f71\u6a5f\uff0c\u5c07\u65bc Homekit \u914d\u4ef6\u6a21\u5f0f\u9032\u884c\u3002", + "description": "\u9078\u64c7\u8981\u5305\u542b\u7684\u5be6\u9ad4\u3002\u65bc\u914d\u4ef6\u6a21\u5f0f\u4e0b\u3001\u50c5\u6709\u55ae\u4e00\u5be6\u9ad4\u5c07\u6703\u5305\u542b\u3002\u65bc\u6a4b\u63a5\u5305\u542b\u6a21\u5f0f\u4e0b\u3001\u6240\u6709\u7db2\u57df\u7684\u5be6\u9ad4\u90fd\u5c07\u5305\u542b\uff0c\u9664\u975e\u9078\u64c7\u7279\u5b9a\u7684\u5be6\u9ad4\u3002\u65bc\u6a4b\u63a5\u6392\u9664\u6a21\u5f0f\u4e2d\u3001\u6240\u6709\u7db2\u57df\u4e2d\u7684\u5be6\u9ad4\u90fd\u5c07\u5305\u542b\uff0c\u9664\u4e86\u6392\u9664\u7684\u5be6\u9ad4\u3002\u70ba\u53d6\u5f97\u6700\u4f73\u6548\u80fd\u3001\u6bcf\u4e00\u500b\u96fb\u8996\u5a92\u9ad4\u64ad\u653e\u5668\u3001\u9060\u7aef\u9059\u63a7\u5668\u3001\u9580\u9396\u8207\u651d\u5f71\u6a5f\uff0c\u5c07\u65bc Homekit \u914d\u4ef6\u6a21\u5f0f\u9032\u884c\u3002", "title": "\u9078\u64c7\u8981\u5305\u542b\u7684\u5be6\u9ad4" }, "init": { diff --git a/homeassistant/components/huisbaasje/translations/de.json b/homeassistant/components/huisbaasje/translations/de.json index ca3f90536d40f..5f8b8ef4c1a43 100644 --- a/homeassistant/components/huisbaasje/translations/de.json +++ b/homeassistant/components/huisbaasje/translations/de.json @@ -4,6 +4,7 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { + "cannot_connect": "Verbindung fehlgeschlagen", "connection_exception": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", "unauthenticated_exception": "Ung\u00fcltige Authentifizierung", diff --git a/homeassistant/components/isy994/translations/de.json b/homeassistant/components/isy994/translations/de.json index 18d6a1603c4ac..ef13c4318b330 100644 --- a/homeassistant/components/isy994/translations/de.json +++ b/homeassistant/components/isy994/translations/de.json @@ -29,6 +29,7 @@ "ignore_string": "Zeichenfolge ignorieren", "restore_light_state": "Lichthelligkeit wiederherstellen" }, + "description": "Stelle die Optionen f\u00fcr die ISY-Integration ein: \n - Node Sensor String: Jedes Ger\u00e4t oder jeder Ordner, der 'Node Sensor String' im Namen enth\u00e4lt, wird als Sensor oder bin\u00e4rer Sensor behandelt. \n - String ignorieren: Jedes Ger\u00e4t mit 'Ignore String' im Namen wird ignoriert. \n - Variable Sensor Zeichenfolge: Jede Variable, die 'Variable Sensor String' im Namen enth\u00e4lt, wird als Sensor hinzugef\u00fcgt. \n - Lichthelligkeit wiederherstellen: Wenn diese Option aktiviert ist, wird beim Einschalten eines Lichts die vorherige Helligkeit wiederhergestellt und nicht der integrierte Ein-Pegel des Ger\u00e4ts.", "title": "ISY994 Optionen" } } diff --git a/homeassistant/components/lutron_caseta/translations/de.json b/homeassistant/components/lutron_caseta/translations/de.json index 392648136bdda..84a3ade5ffc26 100644 --- a/homeassistant/components/lutron_caseta/translations/de.json +++ b/homeassistant/components/lutron_caseta/translations/de.json @@ -10,6 +10,9 @@ }, "flow_title": "Lutron Cas\u00e9ta {name} ({host})", "step": { + "import_failed": { + "title": "Import der Cas\u00e9ta-Bridge-Konfiguration fehlgeschlagen." + }, "link": { "title": "Mit der Bridge verbinden" }, diff --git a/homeassistant/components/metoffice/translations/de.json b/homeassistant/components/metoffice/translations/de.json index 7b92af96c992f..8f35c2aaeaaa9 100644 --- a/homeassistant/components/metoffice/translations/de.json +++ b/homeassistant/components/metoffice/translations/de.json @@ -13,7 +13,9 @@ "api_key": "API Key", "latitude": "Breitengrad", "longitude": "L\u00e4ngengrad" - } + }, + "description": "Der Breiten- und L\u00e4ngengrad wird verwendet, um die n\u00e4chstgelegene Wetterstation zu finden.", + "title": "Mit UK Met Office verbinden" } } } diff --git a/homeassistant/components/mqtt/translations/en.json b/homeassistant/components/mqtt/translations/en.json index 362e51b440537..c8d24b78fb786 100644 --- a/homeassistant/components/mqtt/translations/en.json +++ b/homeassistant/components/mqtt/translations/en.json @@ -21,8 +21,8 @@ "data": { "discovery": "Enable discovery" }, - "description": "Do you want to configure Home Assistant to connect to the MQTT broker provided by the Hass.io add-on {addon}?", - "title": "MQTT Broker via Hass.io add-on" + "description": "Do you want to configure Home Assistant to connect to the MQTT broker provided by the add-on {addon}?", + "title": "MQTT Broker via Home Assistant add-on" } } }, diff --git a/homeassistant/components/mqtt/translations/et.json b/homeassistant/components/mqtt/translations/et.json index f28b1f4f94ed5..4bc267450bb87 100644 --- a/homeassistant/components/mqtt/translations/et.json +++ b/homeassistant/components/mqtt/translations/et.json @@ -21,8 +21,8 @@ "data": { "discovery": "Luba automaatne avastamine" }, - "description": "Kas soovid seadistada Home Assistanti \u00fchenduse loomiseks Hass.io lisandmooduli {addon} pakutava MQTT vahendajaga?", - "title": "MQTT vahendaja Hass.io lisandmooduli abil" + "description": "Kas soovid seadistada Home Assistanti \u00fchenduse loomiseks lisandmooduli {addon} pakutava MQTT vahendajaga?", + "title": "MQTT vahendaja Home Assistanti lisandmooduli abil" } } }, diff --git a/homeassistant/components/mqtt/translations/nl.json b/homeassistant/components/mqtt/translations/nl.json index cac483b1bf050..712de14d330e1 100644 --- a/homeassistant/components/mqtt/translations/nl.json +++ b/homeassistant/components/mqtt/translations/nl.json @@ -21,8 +21,8 @@ "data": { "discovery": "Detectie inschakelen" }, - "description": "Wilt u Home Assistant configureren om verbinding te maken met de MQTT-broker die wordt aangeboden door de Supervisor add-on {addon} ?", - "title": "MQTT Broker via Supervisor add-on" + "description": "Wilt u Home Assistant configureren om verbinding te maken met de MQTT-broker die wordt aangeboden door de add-on {addon} ?", + "title": "MQTT Broker via Home Assistant add-on" } } }, diff --git a/homeassistant/components/mqtt/translations/zh-Hant.json b/homeassistant/components/mqtt/translations/zh-Hant.json index 807de2e2c0913..e24474ed7b6ae 100644 --- a/homeassistant/components/mqtt/translations/zh-Hant.json +++ b/homeassistant/components/mqtt/translations/zh-Hant.json @@ -21,8 +21,8 @@ "data": { "discovery": "\u958b\u555f\u641c\u5c0b" }, - "description": "\u662f\u5426\u8981\u8a2d\u5b9a Home Assistant \u4ee5\u9023\u7dda\u81f3 Hass.io \u9644\u52a0\u5143\u4ef6 {addon} \u4e4b MQTT broker\uff1f", - "title": "\u4f7f\u7528 Hass.io \u9644\u52a0\u5143\u4ef6 MQTT Broker" + "description": "\u662f\u5426\u8981\u8a2d\u5b9a Home Assistant \u4ee5\u9023\u7dda\u81f3 MQTT broker\u3002\u9644\u52a0\u5143\u4ef6\u70ba\uff1a{addon} \uff1f", + "title": "\u4f7f\u7528 Home Assistant \u9644\u52a0\u5143\u4ef6 MQTT Broker" } } }, diff --git a/homeassistant/components/opentherm_gw/translations/nl.json b/homeassistant/components/opentherm_gw/translations/nl.json index bdd3337d05b50..5a4d868e81efc 100644 --- a/homeassistant/components/opentherm_gw/translations/nl.json +++ b/homeassistant/components/opentherm_gw/translations/nl.json @@ -23,7 +23,8 @@ "floor_temperature": "Vloertemperatuur", "precision": "Precisie", "read_precision": "Lees Precisie", - "set_precision": "Precisie instellen" + "set_precision": "Precisie instellen", + "temporary_override_mode": "Tijdelijke setpoint-overschrijvingsmodus" }, "description": "Opties voor de OpenTherm Gateway" } diff --git a/homeassistant/components/opentherm_gw/translations/no.json b/homeassistant/components/opentherm_gw/translations/no.json index 07b7c77c5ccc4..8d82d3c6106b1 100644 --- a/homeassistant/components/opentherm_gw/translations/no.json +++ b/homeassistant/components/opentherm_gw/translations/no.json @@ -23,7 +23,8 @@ "floor_temperature": "Etasje Temperatur", "precision": "Presisjon", "read_precision": "Les presisjon", - "set_precision": "Angi presisjon" + "set_precision": "Angi presisjon", + "temporary_override_mode": "Midlertidig overstyringsmodus for settpunkt" }, "description": "Alternativer for OpenTherm Gateway" } diff --git a/homeassistant/components/poolsense/translations/de.json b/homeassistant/components/poolsense/translations/de.json index 6b64ec2eef1a4..dc569c2d9ad8d 100644 --- a/homeassistant/components/poolsense/translations/de.json +++ b/homeassistant/components/poolsense/translations/de.json @@ -12,7 +12,8 @@ "email": "E-Mail", "password": "Passwort" }, - "description": "M\u00f6chten Sie mit der Einrichtung beginnen?" + "description": "M\u00f6chten Sie mit der Einrichtung beginnen?", + "title": "" } } } diff --git a/homeassistant/components/powerwall/translations/de.json b/homeassistant/components/powerwall/translations/de.json index 0ccd42c812ba1..c916152637370 100644 --- a/homeassistant/components/powerwall/translations/de.json +++ b/homeassistant/components/powerwall/translations/de.json @@ -7,7 +7,8 @@ "error": { "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", - "unknown": "Unerwarteter Fehler" + "unknown": "Unerwarteter Fehler", + "wrong_version": "Deine Powerwall verwendet eine Softwareversion, die nicht unterst\u00fctzt wird. Bitte ziehe ein Upgrade in Betracht oder melde dieses Problem, damit es behoben werden kann." }, "flow_title": "Tesla Powerwall ({ip_address})", "step": { diff --git a/homeassistant/components/roomba/translations/nl.json b/homeassistant/components/roomba/translations/nl.json index f26b28d22486e..2af6e13b13f34 100644 --- a/homeassistant/components/roomba/translations/nl.json +++ b/homeassistant/components/roomba/translations/nl.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "Apparaat is al geconfigureerd", "cannot_connect": "Kan geen verbinding maken", - "not_irobot_device": "Het gevonden apparaat is geen iRobot-apparaat" + "not_irobot_device": "Het gevonden apparaat is geen iRobot-apparaat", + "short_blid": "De BLID is afgekapt" }, "error": { "cannot_connect": "Kan geen verbinding maken" diff --git a/homeassistant/components/roomba/translations/no.json b/homeassistant/components/roomba/translations/no.json index 67df735719c9b..4f051cfde3ff2 100644 --- a/homeassistant/components/roomba/translations/no.json +++ b/homeassistant/components/roomba/translations/no.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "Enheten er allerede konfigurert", "cannot_connect": "Tilkobling mislyktes", - "not_irobot_device": "Oppdaget enhet er ikke en iRobot-enhet" + "not_irobot_device": "Oppdaget enhet er ikke en iRobot-enhet", + "short_blid": "BLID ble avkortet" }, "error": { "cannot_connect": "Tilkobling mislyktes" @@ -18,7 +19,7 @@ "title": "Koble automatisk til enheten" }, "link": { - "description": "Trykk og hold inne Hjem-knappen p\u00e5 {name} til enheten genererer en lyd (omtrent to sekunder)", + "description": "Trykk og hold nede Hjem-knappen p\u00e5 {name} til enheten genererer en lyd (omtrent to sekunder), og send deretter innen 30 sekunder.", "title": "Hent passord" }, "link_manual": { @@ -33,7 +34,7 @@ "blid": "", "host": "Vert" }, - "description": "Ingen Roomba eller Braava har blitt oppdaget i nettverket ditt. BLID er delen av enhetens vertsnavn etter `iRobot-`. F\u00f8lg trinnene som er beskrevet i dokumentasjonen p\u00e5: {auth_help_url}", + "description": "Ingen Roomba eller Braava har blitt oppdaget i nettverket ditt. BLID er delen av enhetens vertsnavn etter `iRobot-` eller `Roomba-`. F\u00f8lg trinnene som er beskrevet i dokumentasjonen p\u00e5: {auth_help_url}", "title": "Koble til enheten manuelt" }, "user": { diff --git a/homeassistant/components/roomba/translations/zh-Hant.json b/homeassistant/components/roomba/translations/zh-Hant.json index 790eba79c032d..830258ff2b654 100644 --- a/homeassistant/components/roomba/translations/zh-Hant.json +++ b/homeassistant/components/roomba/translations/zh-Hant.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557", - "not_irobot_device": "\u6240\u767c\u73fe\u7684\u88dd\u7f6e\u4e26\u975e iRobot \u88dd\u7f6e" + "not_irobot_device": "\u6240\u767c\u73fe\u7684\u88dd\u7f6e\u4e26\u975e iRobot \u88dd\u7f6e", + "short_blid": "BLID \u906d\u622a\u77ed" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" @@ -18,7 +19,7 @@ "title": "\u81ea\u52d5\u9023\u7dda\u81f3\u88dd\u7f6e" }, "link": { - "description": "\u8acb\u6309\u4f4f {name} \u4e0a\u7684 Home \u9375\u76f4\u5230\u88dd\u7f6e\u767c\u51fa\u8072\u97f3\uff08\u7d04\u5169\u79d2\uff09\u3002", + "description": "\u8acb\u6309\u4f4f {name} \u4e0a\u7684 Home \u9375\u76f4\u5230\u88dd\u7f6e\u767c\u51fa\u8072\u97f3\uff08\u7d04\u5169\u79d2\uff09\uff0c\u7136\u5f8c\u65bc 30 \u79d2\u5167\u50b3\u9001\u3002", "title": "\u91cd\u7f6e\u5bc6\u78bc" }, "link_manual": { @@ -33,7 +34,7 @@ "blid": "BLID", "host": "\u4e3b\u6a5f\u7aef" }, - "description": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 Roomba \u6216 Braava\u3002BLID \u88dd\u7f6e\u65bc\u4e3b\u6a5f\u7aef\u7684\u90e8\u5206\u540d\u7a31\u70ba `iRobot-` \u958b\u982d\u3002\u8acb\u53c3\u95b1\u4ee5\u4e0b\u6587\u4ef6\u7684\u6b65\u9a5f\u9032\u884c\u8a2d\u5b9a\uff1a{auth_help_url}", + "description": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 Roomba \u6216 Braava\u3002BLID \u88dd\u7f6e\u65bc\u4e3b\u6a5f\u7aef\u7684\u90e8\u5206\u540d\u7a31\u70ba `iRobot-` \u6216 `Roomba-` \u958b\u982d\u3002\u8acb\u53c3\u95b1\u4ee5\u4e0b\u6587\u4ef6\u7684\u6b65\u9a5f\u9032\u884c\u8a2d\u5b9a\uff1a{auth_help_url}", "title": "\u624b\u52d5\u9023\u7dda\u81f3\u88dd\u7f6e" }, "user": { diff --git a/homeassistant/components/sms/translations/de.json b/homeassistant/components/sms/translations/de.json index b262df1486d24..3c5cc3c0490bf 100644 --- a/homeassistant/components/sms/translations/de.json +++ b/homeassistant/components/sms/translations/de.json @@ -12,7 +12,8 @@ "user": { "data": { "device": "Ger\u00e4t" - } + }, + "title": "Verbinden mit dem Modem" } } } diff --git a/homeassistant/components/squeezebox/translations/de.json b/homeassistant/components/squeezebox/translations/de.json index cdbc5c1426a34..c64e1ae3a1cdd 100644 --- a/homeassistant/components/squeezebox/translations/de.json +++ b/homeassistant/components/squeezebox/translations/de.json @@ -1,11 +1,13 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "no_server_found": "Kein LMS-Server gefunden." }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", + "no_server_found": "Konnte den Server nicht automatisch entdecken.", "unknown": "Unerwarteter Fehler" }, "flow_title": "Logitech Squeezebox", @@ -16,7 +18,8 @@ "password": "Passwort", "port": "Port", "username": "Benutzername" - } + }, + "title": "Verbindungsinformationen bearbeiten" }, "user": { "data": { diff --git a/homeassistant/components/upb/translations/de.json b/homeassistant/components/upb/translations/de.json index 908db20f22b6a..86e4d7409cfd4 100644 --- a/homeassistant/components/upb/translations/de.json +++ b/homeassistant/components/upb/translations/de.json @@ -15,6 +15,7 @@ "file_path": "Pfad und Name der UPStart UPB-Exportdatei.", "protocol": "Protokoll" }, + "description": "Schlie\u00dfe ein Universal Powerline Bus Powerline Interface Module (UPB PIM) an. Der Adress-String muss in der Form 'address[:port]' f\u00fcr 'TCP' vorliegen. Der Port ist optional und standardm\u00e4\u00dfig auf 2101 eingestellt. Beispiel: '192.168.1.42'. F\u00fcr das serielle Protokoll muss die Adresse die Form 'tty[:baud]' haben. Die Baudrate ist optional und standardm\u00e4\u00dfig auf 4800 eingestellt. Beispiel: '/dev/ttyS1'.", "title": "Stelle eine Verbindung zu UPB PIM her" } } diff --git a/homeassistant/components/verisure/translations/de.json b/homeassistant/components/verisure/translations/de.json index f9edd2e7b16d0..3eaf6ff04f6f8 100644 --- a/homeassistant/components/verisure/translations/de.json +++ b/homeassistant/components/verisure/translations/de.json @@ -12,16 +12,19 @@ "installation": { "data": { "giid": "Installation" - } + }, + "description": "Home Assistant hat mehrere Verisure-Installationen in deinen My Pages-Konto gefunden. Bitte w\u00e4hle die Installation aus, die du zu Home Assistant hinzuf\u00fcgen m\u00f6chtest." }, "reauth_confirm": { "data": { + "description": "Authentifiziere dich erneut mit deinem Verisure My Pages-Konto.", "email": "E-Mail", "password": "Passwort" } }, "user": { "data": { + "description": "Melde dich mit deinen Verisure My Pages-Konto an.", "email": "E-Mail", "password": "Passwort" } diff --git a/homeassistant/components/vizio/translations/de.json b/homeassistant/components/vizio/translations/de.json index ad0cc604d133d..913317c88d7e3 100644 --- a/homeassistant/components/vizio/translations/de.json +++ b/homeassistant/components/vizio/translations/de.json @@ -6,7 +6,8 @@ "updated_entry": "Dieser Eintrag wurde bereits eingerichtet, aber der Name, die Apps und / oder die in der Konfiguration definierten Optionen stimmen nicht mit der zuvor importierten Konfiguration \u00fcberein, sodass der Konfigurationseintrag entsprechend aktualisiert wurde." }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen" + "cannot_connect": "Verbindung fehlgeschlagen", + "complete_pairing_failed": "Das Pairing konnte nicht abgeschlossen werden. Vergewissere dich, dass der eingegebene PIN korrekt ist und dass der Fernseher noch mit Strom versorgt wird und mit dem Netzwerk verbunden ist, bevor du es erneut versuchst." }, "step": { "pair_tv": { diff --git a/homeassistant/components/withings/translations/de.json b/homeassistant/components/withings/translations/de.json index 05d3795a0b01f..b4bd6a0c449ca 100644 --- a/homeassistant/components/withings/translations/de.json +++ b/homeassistant/components/withings/translations/de.json @@ -12,6 +12,7 @@ "error": { "already_configured": "Konto wurde bereits konfiguriert" }, + "flow_title": "Withings: {profile}", "step": { "pick_implementation": { "title": "W\u00e4hle die Authentifizierungsmethode" @@ -24,6 +25,7 @@ "title": "Benutzerprofil" }, "reauth": { + "description": "Das Profil \"{profile}\" muss neu authentifiziert werden, um weiterhin Withings-Daten zu empfangen.", "title": "Integration erneut authentifizieren" } } diff --git a/homeassistant/components/xiaomi_aqara/translations/de.json b/homeassistant/components/xiaomi_aqara/translations/de.json index 1effe228de6a7..e72f00d5f5fe6 100644 --- a/homeassistant/components/xiaomi_aqara/translations/de.json +++ b/homeassistant/components/xiaomi_aqara/translations/de.json @@ -2,11 +2,14 @@ "config": { "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", - "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt" + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "not_xiaomi_aqara": "Kein Xiaomi Aqara Gateway, gefundenes Ger\u00e4t stimmt nicht mit bekannten Gateways \u00fcberein" }, "error": { "discovery_error": "Es konnte kein Xiaomi Aqara Gateway gefunden werden, versuche die IP von Home Assistant als Interface zu nutzen", "invalid_host": "Ung\u00fcltiger Hostname oder IP-Adresse, schau unter https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem", + "invalid_interface": "Ung\u00fcltige Netzwerkschnittstelle", + "invalid_key": "Ung\u00fcltiger Gateway-Schl\u00fcssel", "invalid_mac": "Ung\u00fcltige MAC-Adresse" }, "flow_title": "Xiaomi Aqara Gateway: {name}", @@ -15,17 +18,21 @@ "data": { "select_ip": "IP-Adresse" }, - "description": "F\u00fchre das Setup erneut aus, wenn du zus\u00e4tzliche Gateways verbinden m\u00f6chtest" + "description": "F\u00fchre das Setup erneut aus, wenn du zus\u00e4tzliche Gateways verbinden m\u00f6chtest", + "title": "W\u00e4hle das Xiaomi Aqara Gateway, das du verbinden m\u00f6chtest" }, "settings": { "data": { + "key": "Der Schl\u00fcssel deines Gateways", "name": "Name des Gateways" }, + "description": "Der Schl\u00fcssel (das Passwort) kann mithilfe dieser Anleitung abgerufen werden: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. Wenn der Schl\u00fcssel nicht angegeben wird, sind nur die Sensoren zug\u00e4nglich", "title": "Xiaomi Aqara Gateway, optionale Einstellungen" }, "user": { "data": { "host": "IP-Adresse", + "interface": "Die zu verwendende Netzwerkschnittstelle", "mac": "MAC-Adresse" }, "description": "Stelle eine Verbindung zu deinem Xiaomi Aqara Gateway her. Wenn die IP- und MAC-Adressen leer bleiben, wird die automatische Erkennung verwendet", diff --git a/homeassistant/components/zha/translations/de.json b/homeassistant/components/zha/translations/de.json index 61e9b8e37ba32..a0cc570a900db 100644 --- a/homeassistant/components/zha/translations/de.json +++ b/homeassistant/components/zha/translations/de.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "Verbindung fehlgeschlagen" }, + "flow_title": "ZHA: {name}", "step": { "pick_radio": { "data": { diff --git a/homeassistant/components/zha/translations/nl.json b/homeassistant/components/zha/translations/nl.json index ddf208c85771c..83e3426dcbb3a 100644 --- a/homeassistant/components/zha/translations/nl.json +++ b/homeassistant/components/zha/translations/nl.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "Kan geen verbinding maken" }, + "flow_title": "ZHA: {name}", "step": { "pick_radio": { "data": { diff --git a/homeassistant/components/zha/translations/no.json b/homeassistant/components/zha/translations/no.json index a99e74fc6c79d..3917ebd103b38 100644 --- a/homeassistant/components/zha/translations/no.json +++ b/homeassistant/components/zha/translations/no.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "Tilkobling mislyktes" }, + "flow_title": "ZHA: {name}", "step": { "pick_radio": { "data": { diff --git a/homeassistant/components/zha/translations/zh-Hant.json b/homeassistant/components/zha/translations/zh-Hant.json index 6582507431343..c1ad9b8226248 100644 --- a/homeassistant/components/zha/translations/zh-Hant.json +++ b/homeassistant/components/zha/translations/zh-Hant.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" }, + "flow_title": "ZHA\uff1a{name}", "step": { "pick_radio": { "data": { From efa6079c62361ac697d29dccf090f4e85b019b40 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 1 Apr 2021 00:00:39 -0600 Subject: [PATCH 0005/1317] Fix incorrect constant import in Ambient PWS (#48574) --- homeassistant/components/ambient_station/sensor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/ambient_station/sensor.py b/homeassistant/components/ambient_station/sensor.py index 732c28c8dc58a..7c60d1da9bc7d 100644 --- a/homeassistant/components/ambient_station/sensor.py +++ b/homeassistant/components/ambient_station/sensor.py @@ -1,6 +1,5 @@ """Support for Ambient Weather Station sensors.""" -from homeassistant.components.binary_sensor import DOMAIN as SENSOR -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import DOMAIN as SENSOR, SensorEntity from homeassistant.const import ATTR_NAME from homeassistant.core import callback From fdbef90a57e7505a1e35bb27c10f577d4263914c Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 1 Apr 2021 15:05:10 +0200 Subject: [PATCH 0006/1317] Remove device class timestamp from device condition and trigger (#48431) * Remove unit from garmin connect * Remove unit from hvv departures * Remove device class timestamp from device condition and trigger * Remove unit from systemmonitor * Use device class constant for timestamp in ring --- homeassistant/components/garmin_connect/const.py | 10 +++++----- homeassistant/components/hvv_departures/sensor.py | 1 - homeassistant/components/ring/sensor.py | 12 ++++++++---- homeassistant/components/sensor/device_condition.py | 4 ---- homeassistant/components/sensor/device_trigger.py | 4 ---- homeassistant/components/sensor/strings.json | 2 -- homeassistant/components/systemmonitor/sensor.py | 3 ++- tests/components/sensor/test_device_condition.py | 6 +++++- tests/components/sensor/test_device_trigger.py | 8 ++++++-- .../testing_config/custom_components/test/sensor.py | 1 - 10 files changed, 26 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/garmin_connect/const.py b/homeassistant/components/garmin_connect/const.py index 7a143e2e63ac1..991ac90526a9b 100644 --- a/homeassistant/components/garmin_connect/const.py +++ b/homeassistant/components/garmin_connect/const.py @@ -42,14 +42,14 @@ ], "wellnessStartTimeLocal": [ "Wellness Start Time", - "", + None, "mdi:clock", DEVICE_CLASS_TIMESTAMP, False, ], "wellnessEndTimeLocal": [ "Wellness End Time", - "", + None, "mdi:clock", DEVICE_CLASS_TIMESTAMP, False, @@ -299,7 +299,7 @@ "latestSpo2": ["Latest SPO2", PERCENTAGE, "mdi:diabetes", None, True], "latestSpo2ReadingTimeLocal": [ "Latest SPO2 Time", - "", + None, "mdi:diabetes", DEVICE_CLASS_TIMESTAMP, False, @@ -334,7 +334,7 @@ ], "latestRespirationTimeGMT": [ "Latest Respiration Update", - "", + None, "mdi:progress-clock", DEVICE_CLASS_TIMESTAMP, False, @@ -348,5 +348,5 @@ "physiqueRating": ["Physique Rating", "", "mdi:numeric", None, False], "visceralFat": ["Visceral Fat", "", "mdi:food", None, False], "metabolicAge": ["Metabolic Age", "", "mdi:calendar-heart", None, False], - "nextAlarm": ["Next Alarm Time", "", "mdi:alarm", DEVICE_CLASS_TIMESTAMP, True], + "nextAlarm": ["Next Alarm Time", None, "mdi:alarm", DEVICE_CLASS_TIMESTAMP, True], } diff --git a/homeassistant/components/hvv_departures/sensor.py b/homeassistant/components/hvv_departures/sensor.py index 35fc137a0f0d8..5bc70c7a3b4cd 100644 --- a/homeassistant/components/hvv_departures/sensor.py +++ b/homeassistant/components/hvv_departures/sensor.py @@ -18,7 +18,6 @@ MAX_LIST = 20 MAX_TIME_OFFSET = 360 ICON = "mdi:bus" -UNIT_OF_MEASUREMENT = "min" ATTR_DEPARTURE = "departure" ATTR_LINE = "line" diff --git a/homeassistant/components/ring/sensor.py b/homeassistant/components/ring/sensor.py index a20d484d3fe0d..a2b9e2300dc75 100644 --- a/homeassistant/components/ring/sensor.py +++ b/homeassistant/components/ring/sensor.py @@ -1,6 +1,10 @@ """This component provides HA sensor support for Ring Door Bell/Chimes.""" from homeassistant.components.sensor import SensorEntity -from homeassistant.const import PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT +from homeassistant.const import ( + DEVICE_CLASS_TIMESTAMP, + PERCENTAGE, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, +) from homeassistant.core import callback from homeassistant.helpers.icon import icon_for_battery_level @@ -210,7 +214,7 @@ def extra_state_attributes(self): None, "history", None, - "timestamp", + DEVICE_CLASS_TIMESTAMP, HistoryRingSensor, ], "last_ding": [ @@ -219,7 +223,7 @@ def extra_state_attributes(self): None, "history", "ding", - "timestamp", + DEVICE_CLASS_TIMESTAMP, HistoryRingSensor, ], "last_motion": [ @@ -228,7 +232,7 @@ def extra_state_attributes(self): None, "history", "motion", - "timestamp", + DEVICE_CLASS_TIMESTAMP, HistoryRingSensor, ], "volume": [ diff --git a/homeassistant/components/sensor/device_condition.py b/homeassistant/components/sensor/device_condition.py index fea7953048510..4d3d8a4b47734 100644 --- a/homeassistant/components/sensor/device_condition.py +++ b/homeassistant/components/sensor/device_condition.py @@ -25,7 +25,6 @@ DEVICE_CLASS_PRESSURE, DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_TIMESTAMP, DEVICE_CLASS_VOLTAGE, ) from homeassistant.core import HomeAssistant, callback @@ -54,7 +53,6 @@ CONF_IS_PRESSURE = "is_pressure" CONF_IS_SIGNAL_STRENGTH = "is_signal_strength" CONF_IS_TEMPERATURE = "is_temperature" -CONF_IS_TIMESTAMP = "is_timestamp" CONF_IS_VOLTAGE = "is_voltage" CONF_IS_VALUE = "is_value" @@ -71,7 +69,6 @@ DEVICE_CLASS_PRESSURE: [{CONF_TYPE: CONF_IS_PRESSURE}], DEVICE_CLASS_SIGNAL_STRENGTH: [{CONF_TYPE: CONF_IS_SIGNAL_STRENGTH}], DEVICE_CLASS_TEMPERATURE: [{CONF_TYPE: CONF_IS_TEMPERATURE}], - DEVICE_CLASS_TIMESTAMP: [{CONF_TYPE: CONF_IS_TIMESTAMP}], DEVICE_CLASS_VOLTAGE: [{CONF_TYPE: CONF_IS_VOLTAGE}], DEVICE_CLASS_NONE: [{CONF_TYPE: CONF_IS_VALUE}], } @@ -94,7 +91,6 @@ CONF_IS_PRESSURE, CONF_IS_SIGNAL_STRENGTH, CONF_IS_TEMPERATURE, - CONF_IS_TIMESTAMP, CONF_IS_VOLTAGE, CONF_IS_VALUE, ] diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index 9586261a19108..0bca1e299d633 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -28,7 +28,6 @@ DEVICE_CLASS_PRESSURE, DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_TIMESTAMP, DEVICE_CLASS_VOLTAGE, ) from homeassistant.helpers import config_validation as cv @@ -52,7 +51,6 @@ CONF_PRESSURE = "pressure" CONF_SIGNAL_STRENGTH = "signal_strength" CONF_TEMPERATURE = "temperature" -CONF_TIMESTAMP = "timestamp" CONF_VOLTAGE = "voltage" CONF_VALUE = "value" @@ -69,7 +67,6 @@ DEVICE_CLASS_PRESSURE: [{CONF_TYPE: CONF_PRESSURE}], DEVICE_CLASS_SIGNAL_STRENGTH: [{CONF_TYPE: CONF_SIGNAL_STRENGTH}], DEVICE_CLASS_TEMPERATURE: [{CONF_TYPE: CONF_TEMPERATURE}], - DEVICE_CLASS_TIMESTAMP: [{CONF_TYPE: CONF_TIMESTAMP}], DEVICE_CLASS_VOLTAGE: [{CONF_TYPE: CONF_VOLTAGE}], DEVICE_CLASS_NONE: [{CONF_TYPE: CONF_VALUE}], } @@ -93,7 +90,6 @@ CONF_PRESSURE, CONF_SIGNAL_STRENGTH, CONF_TEMPERATURE, - CONF_TIMESTAMP, CONF_VOLTAGE, CONF_VALUE, ] diff --git a/homeassistant/components/sensor/strings.json b/homeassistant/components/sensor/strings.json index 4298a367c2c0f..efe5366cfecf0 100644 --- a/homeassistant/components/sensor/strings.json +++ b/homeassistant/components/sensor/strings.json @@ -11,7 +11,6 @@ "is_pressure": "Current {entity_name} pressure", "is_signal_strength": "Current {entity_name} signal strength", "is_temperature": "Current {entity_name} temperature", - "is_timestamp": "Current {entity_name} timestamp", "is_current": "Current {entity_name} current", "is_energy": "Current {entity_name} energy", "is_power_factor": "Current {entity_name} power factor", @@ -28,7 +27,6 @@ "pressure": "{entity_name} pressure changes", "signal_strength": "{entity_name} signal strength changes", "temperature": "{entity_name} temperature changes", - "timestamp": "{entity_name} timestamp changes", "current": "{entity_name} current changes", "energy": "{entity_name} energy changes", "power_factor": "{entity_name} power factor changes", diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index 596f56d51a144..038d7c6e01451 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -14,6 +14,7 @@ DATA_GIBIBYTES, DATA_MEBIBYTES, DATA_RATE_MEGABYTES_PER_SECOND, + DEVICE_CLASS_TIMESTAMP, PERCENTAGE, STATE_OFF, STATE_ON, @@ -47,7 +48,7 @@ ], "ipv4_address": ["IPv4 address", "", "mdi:server-network", None, True], "ipv6_address": ["IPv6 address", "", "mdi:server-network", None, True], - "last_boot": ["Last boot", "", "mdi:clock", "timestamp", False], + "last_boot": ["Last boot", None, "mdi:clock", DEVICE_CLASS_TIMESTAMP, False], "load_15m": ["Load (15m)", " ", CPU_ICON, None, False], "load_1m": ["Load (1m)", " ", CPU_ICON, None, False], "load_5m": ["Load (5m)", " ", CPU_ICON, None, False], diff --git a/tests/components/sensor/test_device_condition.py b/tests/components/sensor/test_device_condition.py index 80c20cb8e2d0f..2de95d44eb146 100644 --- a/tests/components/sensor/test_device_condition.py +++ b/tests/components/sensor/test_device_condition.py @@ -17,7 +17,10 @@ mock_registry, ) from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401 -from tests.testing_config.custom_components.test.sensor import DEVICE_CLASSES +from tests.testing_config.custom_components.test.sensor import ( + DEVICE_CLASSES, + UNITS_OF_MEASUREMENT, +) @pytest.fixture @@ -69,6 +72,7 @@ async def test_get_conditions(hass, device_reg, entity_reg): "entity_id": platform.ENTITIES[device_class].entity_id, } for device_class in DEVICE_CLASSES + if device_class in UNITS_OF_MEASUREMENT for condition in ENTITY_CONDITIONS[device_class] if device_class != "none" ] diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py index ed1da9f86dda8..4c65eff34abe1 100644 --- a/tests/components/sensor/test_device_trigger.py +++ b/tests/components/sensor/test_device_trigger.py @@ -21,7 +21,10 @@ mock_registry, ) from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401 -from tests.testing_config.custom_components.test.sensor import DEVICE_CLASSES +from tests.testing_config.custom_components.test.sensor import ( + DEVICE_CLASSES, + UNITS_OF_MEASUREMENT, +) @pytest.fixture @@ -73,11 +76,12 @@ async def test_get_triggers(hass, device_reg, entity_reg): "entity_id": platform.ENTITIES[device_class].entity_id, } for device_class in DEVICE_CLASSES + if device_class in UNITS_OF_MEASUREMENT for trigger in ENTITY_TRIGGERS[device_class] if device_class != "none" ] triggers = await async_get_device_automations(hass, "trigger", device_entry.id) - assert len(triggers) == 14 + assert len(triggers) == 13 assert triggers == expected_triggers diff --git a/tests/testing_config/custom_components/test/sensor.py b/tests/testing_config/custom_components/test/sensor.py index de6f179daa76b..384db20d2d469 100644 --- a/tests/testing_config/custom_components/test/sensor.py +++ b/tests/testing_config/custom_components/test/sensor.py @@ -24,7 +24,6 @@ sensor.DEVICE_CLASS_ILLUMINANCE: "lm", # current light level (lx/lm) sensor.DEVICE_CLASS_SIGNAL_STRENGTH: SIGNAL_STRENGTH_DECIBELS, # signal strength (dB/dBm) sensor.DEVICE_CLASS_TEMPERATURE: "C", # temperature (C/F) - sensor.DEVICE_CLASS_TIMESTAMP: "hh:mm:ss", # timestamp (ISO8601) sensor.DEVICE_CLASS_PRESSURE: PRESSURE_HPA, # pressure (hPa/mbar) sensor.DEVICE_CLASS_POWER: "kW", # power (W/kW) sensor.DEVICE_CLASS_CURRENT: "A", # current (A) From 81bdd41fdc8644214d60c7ecb7d981dd6f709abd Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 1 Apr 2021 15:06:47 +0200 Subject: [PATCH 0007/1317] Cleanup orphan devices in onewire integration (#48581) * Cleanup orphan devices (https://github.com/home-assistant/core/issues/47438) * Refactor unit testing * Filter device entries for this config entry * Update logging * Cleanup check --- homeassistant/components/onewire/__init__.py | 43 +++- tests/components/onewire/__init__.py | 36 +++ .../{test_entity_owserver.py => const.py} | 216 ++++++++++++------ .../components/onewire/test_binary_sensor.py | 59 ++--- .../components/onewire/test_entity_sysbus.py | 175 -------------- tests/components/onewire/test_init.py | 52 ++++- tests/components/onewire/test_sensor.py | 159 +++++++++---- tests/components/onewire/test_switch.py | 90 ++------ 8 files changed, 420 insertions(+), 410 deletions(-) rename tests/components/onewire/{test_entity_owserver.py => const.py} (83%) delete mode 100644 tests/components/onewire/test_entity_sysbus.py diff --git a/homeassistant/components/onewire/__init__.py b/homeassistant/components/onewire/__init__.py index 779bc6dfd3a0b..e5a214ce8a4a3 100644 --- a/homeassistant/components/onewire/__init__.py +++ b/homeassistant/components/onewire/__init__.py @@ -1,13 +1,17 @@ """The 1-Wire component.""" import asyncio +import logging from homeassistant.config_entries import ConfigEntry from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.typing import HomeAssistantType from .const import DOMAIN, PLATFORMS from .onewirehub import CannotConnect, OneWireHub +_LOGGER = logging.getLogger(__name__) + async def async_setup(hass, config): """Set up 1-Wire integrations.""" @@ -26,10 +30,43 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry): hass.data[DOMAIN][config_entry.unique_id] = onewirehub - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) + async def cleanup_registry() -> None: + # Get registries + device_registry, entity_registry = await asyncio.gather( + hass.helpers.device_registry.async_get_registry(), + hass.helpers.entity_registry.async_get_registry(), ) + # Generate list of all device entries + registry_devices = [ + entry.id + for entry in dr.async_entries_for_config_entry( + device_registry, config_entry.entry_id + ) + ] + # Remove devices that don't belong to any entity + for device_id in registry_devices: + if not er.async_entries_for_device( + entity_registry, device_id, include_disabled_entities=True + ): + _LOGGER.debug( + "Removing device `%s` because it does not have any entities", + device_id, + ) + device_registry.async_remove_device(device_id) + + async def start_platforms() -> None: + """Start platforms and cleanup devices.""" + # wait until all required platforms are ready + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_setup(config_entry, platform) + for platform in PLATFORMS + ] + ) + await cleanup_registry() + + hass.async_create_task(start_platforms()) + return True diff --git a/tests/components/onewire/__init__.py b/tests/components/onewire/__init__.py index 716e73747f132..7b85c16d4c86a 100644 --- a/tests/components/onewire/__init__.py +++ b/tests/components/onewire/__init__.py @@ -2,6 +2,8 @@ from unittest.mock import patch +from pyownet.protocol import ProtocolError + from homeassistant.components.onewire.const import ( CONF_MOUNT_DIR, CONF_NAMES, @@ -13,6 +15,8 @@ from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE +from .const import MOCK_OWPROXY_DEVICES + from tests.common import MockConfigEntry @@ -89,3 +93,35 @@ async def setup_onewire_patched_owserver_integration(hass): await hass.async_block_till_done() return config_entry + + +def setup_owproxy_mock_devices(owproxy, domain, device_ids) -> None: + """Set up mock for owproxy.""" + dir_return_value = [] + main_read_side_effect = [] + sub_read_side_effect = [] + + for device_id in device_ids: + mock_device = MOCK_OWPROXY_DEVICES[device_id] + + # Setup directory listing + dir_return_value += [f"/{device_id}/"] + + # Setup device reads + main_read_side_effect += [device_id[0:2].encode()] + if "inject_reads" in mock_device: + main_read_side_effect += mock_device["inject_reads"] + + # Setup sub-device reads + device_sensors = mock_device.get(domain, []) + for expected_sensor in device_sensors: + sub_read_side_effect.append(expected_sensor["injected_value"]) + + # Ensure enough read side effect + read_side_effect = ( + main_read_side_effect + + sub_read_side_effect + + [ProtocolError("Missing injected value")] * 20 + ) + owproxy.return_value.dir.return_value = dir_return_value + owproxy.return_value.read.side_effect = read_side_effect diff --git a/tests/components/onewire/test_entity_owserver.py b/tests/components/onewire/const.py similarity index 83% rename from tests/components/onewire/test_entity_owserver.py rename to tests/components/onewire/const.py index a3a205795bfdd..8fa149c7adccb 100644 --- a/tests/components/onewire/test_entity_owserver.py +++ b/tests/components/onewire/const.py @@ -1,11 +1,10 @@ -"""Tests for 1-Wire devices connected on OWServer.""" -from unittest.mock import patch +"""Constants for 1-Wire integration.""" +from pi1wire import InvalidCRCException, UnsupportResponseException from pyownet.protocol import Error as ProtocolError -import pytest from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.components.onewire.const import DOMAIN, PLATFORMS, PRESSURE_CBAR +from homeassistant.components.onewire.const import DOMAIN, PRESSURE_CBAR from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.const import ( @@ -24,13 +23,8 @@ TEMP_CELSIUS, VOLT, ) -from homeassistant.setup import async_setup_component -from . import setup_onewire_patched_owserver_integration - -from tests.common import mock_device_registry, mock_registry - -MOCK_DEVICE_SENSORS = { +MOCK_OWPROXY_DEVICES = { "00.111111111111": { "inject_reads": [ b"", # read device type @@ -186,7 +180,42 @@ "model": "DS2409", "name": "1F.111111111111", }, - SENSOR_DOMAIN: [], + "branches": { + "aux": {}, + "main": { + "1D.111111111111": { + "inject_reads": [ + b"DS2423", # read device type + ], + "device_info": { + "identifiers": {(DOMAIN, "1D.111111111111")}, + "manufacturer": "Maxim Integrated", + "model": "DS2423", + "name": "1D.111111111111", + }, + SENSOR_DOMAIN: [ + { + "entity_id": "sensor.1d_111111111111_counter_a", + "device_file": "/1F.111111111111/main/1D.111111111111/counter.A", + "unique_id": "/1D.111111111111/counter.A", + "injected_value": b" 251123", + "result": "251123", + "unit": "count", + "class": None, + }, + { + "entity_id": "sensor.1d_111111111111_counter_b", + "device_file": "/1F.111111111111/main/1D.111111111111/counter.B", + "unique_id": "/1D.111111111111/counter.B", + "injected_value": b" 248125", + "result": "248125", + "unit": "count", + "class": None, + }, + ], + }, + }, + }, }, "22.111111111111": { "inject_reads": [ @@ -748,65 +777,106 @@ }, } - -@pytest.mark.parametrize("device_id", MOCK_DEVICE_SENSORS.keys()) -@pytest.mark.parametrize("platform", PLATFORMS) -@patch("homeassistant.components.onewire.onewirehub.protocol.proxy") -async def test_owserver_setup_valid_device(owproxy, hass, device_id, platform): - """Test for 1-Wire device. - - As they would be on a clean setup: all binary-sensors and switches disabled. - """ - await async_setup_component(hass, "persistent_notification", {}) - entity_registry = mock_registry(hass) - device_registry = mock_device_registry(hass) - - mock_device_sensor = MOCK_DEVICE_SENSORS[device_id] - - device_family = device_id[0:2] - dir_return_value = [f"/{device_id}/"] - read_side_effect = [device_family.encode()] - if "inject_reads" in mock_device_sensor: - read_side_effect += mock_device_sensor["inject_reads"] - - expected_sensors = mock_device_sensor.get(platform, []) - for expected_sensor in expected_sensors: - read_side_effect.append(expected_sensor["injected_value"]) - - # Ensure enough read side effect - read_side_effect.extend([ProtocolError("Missing injected value")] * 20) - owproxy.return_value.dir.return_value = dir_return_value - owproxy.return_value.read.side_effect = read_side_effect - - with patch("homeassistant.components.onewire.PLATFORMS", [platform]): - await setup_onewire_patched_owserver_integration(hass) - await hass.async_block_till_done() - - assert len(entity_registry.entities) == len(expected_sensors) - - if len(expected_sensors) > 0: - device_info = mock_device_sensor["device_info"] - assert len(device_registry.devices) == 1 - registry_entry = device_registry.async_get_device({(DOMAIN, device_id)}) - assert registry_entry is not None - assert registry_entry.identifiers == {(DOMAIN, device_id)} - assert registry_entry.manufacturer == device_info["manufacturer"] - assert registry_entry.name == device_info["name"] - assert registry_entry.model == device_info["model"] - - for expected_sensor in expected_sensors: - entity_id = expected_sensor["entity_id"] - registry_entry = entity_registry.entities.get(entity_id) - assert registry_entry is not None - assert registry_entry.unique_id == expected_sensor["unique_id"] - assert registry_entry.unit_of_measurement == expected_sensor["unit"] - assert registry_entry.device_class == expected_sensor["class"] - assert registry_entry.disabled == expected_sensor.get("disabled", False) - state = hass.states.get(entity_id) - if registry_entry.disabled: - assert state is None - else: - assert state.state == expected_sensor["result"] - assert state.attributes["device_file"] == expected_sensor.get( - "device_file", registry_entry.unique_id - ) +MOCK_SYSBUS_DEVICES = { + "00-111111111111": {"sensors": []}, + "10-111111111111": { + "device_info": { + "identifiers": {(DOMAIN, "10-111111111111")}, + "manufacturer": "Maxim Integrated", + "model": "10", + "name": "10-111111111111", + }, + "sensors": [ + { + "entity_id": "sensor.my_ds18b20_temperature", + "unique_id": "/sys/bus/w1/devices/10-111111111111/w1_slave", + "injected_value": 25.123, + "result": "25.1", + "unit": TEMP_CELSIUS, + "class": DEVICE_CLASS_TEMPERATURE, + }, + ], + }, + "12-111111111111": {"sensors": []}, + "1D-111111111111": {"sensors": []}, + "22-111111111111": { + "device_info": { + "identifiers": {(DOMAIN, "22-111111111111")}, + "manufacturer": "Maxim Integrated", + "model": "22", + "name": "22-111111111111", + }, + "sensors": [ + { + "entity_id": "sensor.22_111111111111_temperature", + "unique_id": "/sys/bus/w1/devices/22-111111111111/w1_slave", + "injected_value": FileNotFoundError, + "result": "unknown", + "unit": TEMP_CELSIUS, + "class": DEVICE_CLASS_TEMPERATURE, + }, + ], + }, + "26-111111111111": {"sensors": []}, + "28-111111111111": { + "device_info": { + "identifiers": {(DOMAIN, "28-111111111111")}, + "manufacturer": "Maxim Integrated", + "model": "28", + "name": "28-111111111111", + }, + "sensors": [ + { + "entity_id": "sensor.28_111111111111_temperature", + "unique_id": "/sys/bus/w1/devices/28-111111111111/w1_slave", + "injected_value": InvalidCRCException, + "result": "unknown", + "unit": TEMP_CELSIUS, + "class": DEVICE_CLASS_TEMPERATURE, + }, + ], + }, + "29-111111111111": {"sensors": []}, + "3B-111111111111": { + "device_info": { + "identifiers": {(DOMAIN, "3B-111111111111")}, + "manufacturer": "Maxim Integrated", + "model": "3B", + "name": "3B-111111111111", + }, + "sensors": [ + { + "entity_id": "sensor.3b_111111111111_temperature", + "unique_id": "/sys/bus/w1/devices/3B-111111111111/w1_slave", + "injected_value": 29.993, + "result": "30.0", + "unit": TEMP_CELSIUS, + "class": DEVICE_CLASS_TEMPERATURE, + }, + ], + }, + "42-111111111111": { + "device_info": { + "identifiers": {(DOMAIN, "42-111111111111")}, + "manufacturer": "Maxim Integrated", + "model": "42", + "name": "42-111111111111", + }, + "sensors": [ + { + "entity_id": "sensor.42_111111111111_temperature", + "unique_id": "/sys/bus/w1/devices/42-111111111111/w1_slave", + "injected_value": UnsupportResponseException, + "result": "unknown", + "unit": TEMP_CELSIUS, + "class": DEVICE_CLASS_TEMPERATURE, + }, + ], + }, + "EF-111111111111": { + "sensors": [], + }, + "EF-111111111112": { + "sensors": [], + }, +} diff --git a/tests/components/onewire/test_binary_sensor.py b/tests/components/onewire/test_binary_sensor.py index dd44510e0ad7d..91ae472278ab7 100644 --- a/tests/components/onewire/test_binary_sensor.py +++ b/tests/components/onewire/test_binary_sensor.py @@ -2,40 +2,25 @@ import copy from unittest.mock import patch -from pyownet.protocol import Error as ProtocolError import pytest from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.onewire.binary_sensor import DEVICE_BINARY_SENSORS -from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.setup import async_setup_component -from . import setup_onewire_patched_owserver_integration +from . import setup_onewire_patched_owserver_integration, setup_owproxy_mock_devices +from .const import MOCK_OWPROXY_DEVICES from tests.common import mock_registry -MOCK_DEVICE_SENSORS = { - "12.111111111111": { - "inject_reads": [ - b"DS2406", # read device type - ], - BINARY_SENSOR_DOMAIN: [ - { - "entity_id": "binary_sensor.12_111111111111_sensed_a", - "injected_value": b" 1", - "result": STATE_ON, - }, - { - "entity_id": "binary_sensor.12_111111111111_sensed_b", - "injected_value": b" 0", - "result": STATE_OFF, - }, - ], - }, +MOCK_BINARY_SENSORS = { + key: value + for (key, value) in MOCK_OWPROXY_DEVICES.items() + if BINARY_SENSOR_DOMAIN in value } -@pytest.mark.parametrize("device_id", MOCK_DEVICE_SENSORS.keys()) +@pytest.mark.parametrize("device_id", MOCK_BINARY_SENSORS.keys()) @patch("homeassistant.components.onewire.onewirehub.protocol.proxy") async def test_owserver_binary_sensor(owproxy, hass, device_id): """Test for 1-Wire binary sensor. @@ -45,26 +30,14 @@ async def test_owserver_binary_sensor(owproxy, hass, device_id): await async_setup_component(hass, "persistent_notification", {}) entity_registry = mock_registry(hass) - mock_device_sensor = MOCK_DEVICE_SENSORS[device_id] + setup_owproxy_mock_devices(owproxy, BINARY_SENSOR_DOMAIN, [device_id]) - device_family = device_id[0:2] - dir_return_value = [f"/{device_id}/"] - read_side_effect = [device_family.encode()] - if "inject_reads" in mock_device_sensor: - read_side_effect += mock_device_sensor["inject_reads"] - - expected_sensors = mock_device_sensor[BINARY_SENSOR_DOMAIN] - for expected_sensor in expected_sensors: - read_side_effect.append(expected_sensor["injected_value"]) - - # Ensure enough read side effect - read_side_effect.extend([ProtocolError("Missing injected value")] * 10) - owproxy.return_value.dir.return_value = dir_return_value - owproxy.return_value.read.side_effect = read_side_effect + mock_device = MOCK_BINARY_SENSORS[device_id] + expected_entities = mock_device[BINARY_SENSOR_DOMAIN] # Force enable binary sensors patch_device_binary_sensors = copy.deepcopy(DEVICE_BINARY_SENSORS) - for item in patch_device_binary_sensors[device_family]: + for item in patch_device_binary_sensors[device_id[0:2]]: item["default_disabled"] = False with patch( @@ -76,14 +49,14 @@ async def test_owserver_binary_sensor(owproxy, hass, device_id): await setup_onewire_patched_owserver_integration(hass) await hass.async_block_till_done() - assert len(entity_registry.entities) == len(expected_sensors) + assert len(entity_registry.entities) == len(expected_entities) - for expected_sensor in expected_sensors: - entity_id = expected_sensor["entity_id"] + for expected_entity in expected_entities: + entity_id = expected_entity["entity_id"] registry_entry = entity_registry.entities.get(entity_id) assert registry_entry is not None state = hass.states.get(entity_id) - assert state.state == expected_sensor["result"] - assert state.attributes["device_file"] == expected_sensor.get( + assert state.state == expected_entity["result"] + assert state.attributes["device_file"] == expected_entity.get( "device_file", registry_entry.unique_id ) diff --git a/tests/components/onewire/test_entity_sysbus.py b/tests/components/onewire/test_entity_sysbus.py deleted file mode 100644 index 61a38c10f733a..0000000000000 --- a/tests/components/onewire/test_entity_sysbus.py +++ /dev/null @@ -1,175 +0,0 @@ -"""Tests for 1-Wire devices connected on SysBus.""" -from unittest.mock import patch - -from pi1wire import InvalidCRCException, UnsupportResponseException -import pytest - -from homeassistant.components.onewire.const import DEFAULT_SYSBUS_MOUNT_DIR, DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS -from homeassistant.setup import async_setup_component - -from tests.common import mock_device_registry, mock_registry - -MOCK_CONFIG = { - SENSOR_DOMAIN: { - "platform": DOMAIN, - "mount_dir": DEFAULT_SYSBUS_MOUNT_DIR, - "names": { - "10-111111111111": "My DS18B20", - }, - } -} - -MOCK_DEVICE_SENSORS = { - "00-111111111111": {"sensors": []}, - "10-111111111111": { - "device_info": { - "identifiers": {(DOMAIN, "10-111111111111")}, - "manufacturer": "Maxim Integrated", - "model": "10", - "name": "10-111111111111", - }, - "sensors": [ - { - "entity_id": "sensor.my_ds18b20_temperature", - "unique_id": "/sys/bus/w1/devices/10-111111111111/w1_slave", - "injected_value": 25.123, - "result": "25.1", - "unit": TEMP_CELSIUS, - "class": DEVICE_CLASS_TEMPERATURE, - }, - ], - }, - "12-111111111111": {"sensors": []}, - "1D-111111111111": {"sensors": []}, - "22-111111111111": { - "device_info": { - "identifiers": {(DOMAIN, "22-111111111111")}, - "manufacturer": "Maxim Integrated", - "model": "22", - "name": "22-111111111111", - }, - "sensors": [ - { - "entity_id": "sensor.22_111111111111_temperature", - "unique_id": "/sys/bus/w1/devices/22-111111111111/w1_slave", - "injected_value": FileNotFoundError, - "result": "unknown", - "unit": TEMP_CELSIUS, - "class": DEVICE_CLASS_TEMPERATURE, - }, - ], - }, - "26-111111111111": {"sensors": []}, - "28-111111111111": { - "device_info": { - "identifiers": {(DOMAIN, "28-111111111111")}, - "manufacturer": "Maxim Integrated", - "model": "28", - "name": "28-111111111111", - }, - "sensors": [ - { - "entity_id": "sensor.28_111111111111_temperature", - "unique_id": "/sys/bus/w1/devices/28-111111111111/w1_slave", - "injected_value": InvalidCRCException, - "result": "unknown", - "unit": TEMP_CELSIUS, - "class": DEVICE_CLASS_TEMPERATURE, - }, - ], - }, - "29-111111111111": {"sensors": []}, - "3B-111111111111": { - "device_info": { - "identifiers": {(DOMAIN, "3B-111111111111")}, - "manufacturer": "Maxim Integrated", - "model": "3B", - "name": "3B-111111111111", - }, - "sensors": [ - { - "entity_id": "sensor.3b_111111111111_temperature", - "unique_id": "/sys/bus/w1/devices/3B-111111111111/w1_slave", - "injected_value": 29.993, - "result": "30.0", - "unit": TEMP_CELSIUS, - "class": DEVICE_CLASS_TEMPERATURE, - }, - ], - }, - "42-111111111111": { - "device_info": { - "identifiers": {(DOMAIN, "42-111111111111")}, - "manufacturer": "Maxim Integrated", - "model": "42", - "name": "42-111111111111", - }, - "sensors": [ - { - "entity_id": "sensor.42_111111111111_temperature", - "unique_id": "/sys/bus/w1/devices/42-111111111111/w1_slave", - "injected_value": UnsupportResponseException, - "result": "unknown", - "unit": TEMP_CELSIUS, - "class": DEVICE_CLASS_TEMPERATURE, - }, - ], - }, - "EF-111111111111": { - "sensors": [], - }, - "EF-111111111112": { - "sensors": [], - }, -} - - -@pytest.mark.parametrize("device_id", MOCK_DEVICE_SENSORS.keys()) -async def test_onewiredirect_setup_valid_device(hass, device_id): - """Test that sysbus config entry works correctly.""" - entity_registry = mock_registry(hass) - device_registry = mock_device_registry(hass) - - mock_device_sensor = MOCK_DEVICE_SENSORS[device_id] - - glob_result = [f"/{DEFAULT_SYSBUS_MOUNT_DIR}/{device_id}"] - read_side_effect = [] - expected_sensors = mock_device_sensor["sensors"] - for expected_sensor in expected_sensors: - read_side_effect.append(expected_sensor["injected_value"]) - - # Ensure enough read side effect - read_side_effect.extend([FileNotFoundError("Missing injected value")] * 20) - - with patch( - "homeassistant.components.onewire.onewirehub.os.path.isdir", return_value=True - ), patch("pi1wire._finder.glob.glob", return_value=glob_result,), patch( - "pi1wire.OneWire.get_temperature", - side_effect=read_side_effect, - ): - assert await async_setup_component(hass, SENSOR_DOMAIN, MOCK_CONFIG) - await hass.async_block_till_done() - - assert len(entity_registry.entities) == len(expected_sensors) - - if len(expected_sensors) > 0: - device_info = mock_device_sensor["device_info"] - assert len(device_registry.devices) == 1 - registry_entry = device_registry.async_get_device({(DOMAIN, device_id)}) - assert registry_entry is not None - assert registry_entry.identifiers == {(DOMAIN, device_id)} - assert registry_entry.manufacturer == device_info["manufacturer"] - assert registry_entry.name == device_info["name"] - assert registry_entry.model == device_info["model"] - - for expected_sensor in expected_sensors: - entity_id = expected_sensor["entity_id"] - registry_entry = entity_registry.entities.get(entity_id) - assert registry_entry is not None - assert registry_entry.unique_id == expected_sensor["unique_id"] - assert registry_entry.unit_of_measurement == expected_sensor["unit"] - assert registry_entry.device_class == expected_sensor["class"] - state = hass.states.get(entity_id) - assert state.state == expected_sensor["result"] diff --git a/tests/components/onewire/test_init.py b/tests/components/onewire/test_init.py index 38e97206698f9..5783b241a2f63 100644 --- a/tests/components/onewire/test_init.py +++ b/tests/components/onewire/test_init.py @@ -4,6 +4,7 @@ from pyownet.protocol import ConnError, OwnetError from homeassistant.components.onewire.const import CONF_TYPE_OWSERVER, DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ( CONN_CLASS_LOCAL_POLL, ENTRY_STATE_LOADED, @@ -11,10 +12,17 @@ ENTRY_STATE_SETUP_RETRY, ) from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.setup import async_setup_component + +from . import ( + setup_onewire_owserver_integration, + setup_onewire_patched_owserver_integration, + setup_onewire_sysbus_integration, + setup_owproxy_mock_devices, +) -from . import setup_onewire_owserver_integration, setup_onewire_sysbus_integration - -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, mock_device_registry, mock_registry async def test_owserver_connect_failure(hass): @@ -87,3 +95,41 @@ async def test_unload_entry(hass): assert config_entry_owserver.state == ENTRY_STATE_NOT_LOADED assert config_entry_sysbus.state == ENTRY_STATE_NOT_LOADED assert not hass.data.get(DOMAIN) + + +@patch("homeassistant.components.onewire.onewirehub.protocol.proxy") +async def test_registry_cleanup(owproxy, hass): + """Test for 1-Wire device. + + As they would be on a clean setup: all binary-sensors and switches disabled. + """ + await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) + device_registry = mock_device_registry(hass) + + # Initialise with two components + setup_owproxy_mock_devices( + owproxy, SENSOR_DOMAIN, ["10.111111111111", "28.111111111111"] + ) + with patch("homeassistant.components.onewire.PLATFORMS", [SENSOR_DOMAIN]): + await setup_onewire_patched_owserver_integration(hass) + await hass.async_block_till_done() + + assert len(dr.async_entries_for_config_entry(device_registry, "2")) == 2 + assert len(er.async_entries_for_config_entry(entity_registry, "2")) == 2 + + # Second item has disappeared from bus, and was removed manually from the front-end + setup_owproxy_mock_devices(owproxy, SENSOR_DOMAIN, ["10.111111111111"]) + entity_registry.async_remove("sensor.28_111111111111_temperature") + await hass.async_block_till_done() + + assert len(er.async_entries_for_config_entry(entity_registry, "2")) == 1 + assert len(dr.async_entries_for_config_entry(device_registry, "2")) == 2 + + # Second item has disappeared from bus, and was removed manually from the front-end + with patch("homeassistant.components.onewire.PLATFORMS", [SENSOR_DOMAIN]): + await hass.config_entries.async_reload("2") + await hass.async_block_till_done() + + assert len(er.async_entries_for_config_entry(entity_registry, "2")) == 1 + assert len(dr.async_entries_for_config_entry(device_registry, "2")) == 1 diff --git a/tests/components/onewire/test_sensor.py b/tests/components/onewire/test_sensor.py index 44351cf9a6313..f81044eb86d98 100644 --- a/tests/components/onewire/test_sensor.py +++ b/tests/components/onewire/test_sensor.py @@ -4,54 +4,29 @@ from pyownet.protocol import Error as ProtocolError import pytest -from homeassistant.components.onewire.const import DEFAULT_SYSBUS_MOUNT_DIR, DOMAIN +from homeassistant.components.onewire.const import ( + DEFAULT_SYSBUS_MOUNT_DIR, + DOMAIN, + PLATFORMS, +) from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.setup import async_setup_component -from . import setup_onewire_patched_owserver_integration +from . import setup_onewire_patched_owserver_integration, setup_owproxy_mock_devices +from .const import MOCK_OWPROXY_DEVICES, MOCK_SYSBUS_DEVICES -from tests.common import assert_setup_component, mock_registry +from tests.common import assert_setup_component, mock_device_registry, mock_registry MOCK_COUPLERS = { - "1F.111111111111": { - "inject_reads": [ - b"DS2409", # read device type - ], - "branches": { - "aux": {}, - "main": { - "1D.111111111111": { - "inject_reads": [ - b"DS2423", # read device type - ], - "device_info": { - "identifiers": {(DOMAIN, "1D.111111111111")}, - "manufacturer": "Maxim Integrated", - "model": "DS2423", - "name": "1D.111111111111", - }, - SENSOR_DOMAIN: [ - { - "entity_id": "sensor.1d_111111111111_counter_a", - "device_file": "/1F.111111111111/main/1D.111111111111/counter.A", - "unique_id": "/1D.111111111111/counter.A", - "injected_value": b" 251123", - "result": "251123", - "unit": "count", - "class": None, - }, - { - "entity_id": "sensor.1d_111111111111_counter_b", - "device_file": "/1F.111111111111/main/1D.111111111111/counter.B", - "unique_id": "/1D.111111111111/counter.B", - "injected_value": b" 248125", - "result": "248125", - "unit": "count", - "class": None, - }, - ], - }, - }, + key: value for (key, value) in MOCK_OWPROXY_DEVICES.items() if "branches" in value +} + +MOCK_SYSBUS_CONFIG = { + SENSOR_DOMAIN: { + "platform": DOMAIN, + "mount_dir": DEFAULT_SYSBUS_MOUNT_DIR, + "names": { + "10-111111111111": "My DS18B20", }, } } @@ -154,3 +129,103 @@ async def test_sensors_on_owserver_coupler(owproxy, hass, device_id): else: assert state.state == expected_sensor["result"] assert state.attributes["device_file"] == expected_sensor["device_file"] + + +@pytest.mark.parametrize("device_id", MOCK_OWPROXY_DEVICES.keys()) +@pytest.mark.parametrize("platform", PLATFORMS) +@patch("homeassistant.components.onewire.onewirehub.protocol.proxy") +async def test_owserver_setup_valid_device(owproxy, hass, device_id, platform): + """Test for 1-Wire device. + + As they would be on a clean setup: all binary-sensors and switches disabled. + """ + await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) + device_registry = mock_device_registry(hass) + + setup_owproxy_mock_devices(owproxy, platform, [device_id]) + + mock_device = MOCK_OWPROXY_DEVICES[device_id] + expected_entities = mock_device.get(platform, []) + + with patch("homeassistant.components.onewire.PLATFORMS", [platform]): + await setup_onewire_patched_owserver_integration(hass) + await hass.async_block_till_done() + + assert len(entity_registry.entities) == len(expected_entities) + + if len(expected_entities) > 0: + device_info = mock_device["device_info"] + assert len(device_registry.devices) == 1 + registry_entry = device_registry.async_get_device({(DOMAIN, device_id)}) + assert registry_entry is not None + assert registry_entry.identifiers == {(DOMAIN, device_id)} + assert registry_entry.manufacturer == device_info["manufacturer"] + assert registry_entry.name == device_info["name"] + assert registry_entry.model == device_info["model"] + + for expected_entity in expected_entities: + entity_id = expected_entity["entity_id"] + registry_entry = entity_registry.entities.get(entity_id) + assert registry_entry is not None + assert registry_entry.unique_id == expected_entity["unique_id"] + assert registry_entry.unit_of_measurement == expected_entity["unit"] + assert registry_entry.device_class == expected_entity["class"] + assert registry_entry.disabled == expected_entity.get("disabled", False) + state = hass.states.get(entity_id) + if registry_entry.disabled: + assert state is None + else: + assert state.state == expected_entity["result"] + assert state.attributes["device_file"] == expected_entity.get( + "device_file", registry_entry.unique_id + ) + + +@pytest.mark.parametrize("device_id", MOCK_SYSBUS_DEVICES.keys()) +async def test_onewiredirect_setup_valid_device(hass, device_id): + """Test that sysbus config entry works correctly.""" + entity_registry = mock_registry(hass) + device_registry = mock_device_registry(hass) + + mock_device_sensor = MOCK_SYSBUS_DEVICES[device_id] + + glob_result = [f"/{DEFAULT_SYSBUS_MOUNT_DIR}/{device_id}"] + read_side_effect = [] + expected_sensors = mock_device_sensor["sensors"] + for expected_sensor in expected_sensors: + read_side_effect.append(expected_sensor["injected_value"]) + + # Ensure enough read side effect + read_side_effect.extend([FileNotFoundError("Missing injected value")] * 20) + + with patch( + "homeassistant.components.onewire.onewirehub.os.path.isdir", return_value=True + ), patch("pi1wire._finder.glob.glob", return_value=glob_result,), patch( + "pi1wire.OneWire.get_temperature", + side_effect=read_side_effect, + ): + assert await async_setup_component(hass, SENSOR_DOMAIN, MOCK_SYSBUS_CONFIG) + await hass.async_block_till_done() + + assert len(entity_registry.entities) == len(expected_sensors) + + if len(expected_sensors) > 0: + device_info = mock_device_sensor["device_info"] + assert len(device_registry.devices) == 1 + registry_entry = device_registry.async_get_device({(DOMAIN, device_id)}) + assert registry_entry is not None + assert registry_entry.identifiers == {(DOMAIN, device_id)} + assert registry_entry.manufacturer == device_info["manufacturer"] + assert registry_entry.name == device_info["name"] + assert registry_entry.model == device_info["model"] + + for expected_sensor in expected_sensors: + entity_id = expected_sensor["entity_id"] + registry_entry = entity_registry.entities.get(entity_id) + assert registry_entry is not None + assert registry_entry.unique_id == expected_sensor["unique_id"] + assert registry_entry.unit_of_measurement == expected_sensor["unit"] + assert registry_entry.device_class == expected_sensor["class"] + state = hass.states.get(entity_id) + assert state.state == expected_sensor["result"] diff --git a/tests/components/onewire/test_switch.py b/tests/components/onewire/test_switch.py index 0d8c991871196..91a9e32e902ca 100644 --- a/tests/components/onewire/test_switch.py +++ b/tests/components/onewire/test_switch.py @@ -2,7 +2,6 @@ import copy from unittest.mock import patch -from pyownet.protocol import Error as ProtocolError import pytest from homeassistant.components.onewire.switch import DEVICE_SWITCHES @@ -10,58 +9,19 @@ from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TOGGLE, STATE_OFF, STATE_ON from homeassistant.setup import async_setup_component -from . import setup_onewire_patched_owserver_integration +from . import setup_onewire_patched_owserver_integration, setup_owproxy_mock_devices +from .const import MOCK_OWPROXY_DEVICES from tests.common import mock_registry -MOCK_DEVICE_SENSORS = { - "12.111111111111": { - "inject_reads": [ - b"DS2406", # read device type - ], - SWITCH_DOMAIN: [ - { - "entity_id": "switch.12_111111111111_pio_a", - "unique_id": "/12.111111111111/PIO.A", - "injected_value": b" 1", - "result": STATE_ON, - "unit": None, - "class": None, - "disabled": True, - }, - { - "entity_id": "switch.12_111111111111_pio_b", - "unique_id": "/12.111111111111/PIO.B", - "injected_value": b" 0", - "result": STATE_OFF, - "unit": None, - "class": None, - "disabled": True, - }, - { - "entity_id": "switch.12_111111111111_latch_a", - "unique_id": "/12.111111111111/latch.A", - "injected_value": b" 1", - "result": STATE_ON, - "unit": None, - "class": None, - "disabled": True, - }, - { - "entity_id": "switch.12_111111111111_latch_b", - "unique_id": "/12.111111111111/latch.B", - "injected_value": b" 0", - "result": STATE_OFF, - "unit": None, - "class": None, - "disabled": True, - }, - ], - } +MOCK_SWITCHES = { + key: value + for (key, value) in MOCK_OWPROXY_DEVICES.items() + if SWITCH_DOMAIN in value } -@pytest.mark.parametrize("device_id", ["12.111111111111"]) +@pytest.mark.parametrize("device_id", MOCK_SWITCHES.keys()) @patch("homeassistant.components.onewire.onewirehub.protocol.proxy") async def test_owserver_switch(owproxy, hass, device_id): """Test for 1-Wire switch. @@ -71,26 +31,14 @@ async def test_owserver_switch(owproxy, hass, device_id): await async_setup_component(hass, "persistent_notification", {}) entity_registry = mock_registry(hass) - mock_device_sensor = MOCK_DEVICE_SENSORS[device_id] + setup_owproxy_mock_devices(owproxy, SWITCH_DOMAIN, [device_id]) - device_family = device_id[0:2] - dir_return_value = [f"/{device_id}/"] - read_side_effect = [device_family.encode()] - if "inject_reads" in mock_device_sensor: - read_side_effect += mock_device_sensor["inject_reads"] - - expected_sensors = mock_device_sensor[SWITCH_DOMAIN] - for expected_sensor in expected_sensors: - read_side_effect.append(expected_sensor["injected_value"]) - - # Ensure enough read side effect - read_side_effect.extend([ProtocolError("Missing injected value")] * 10) - owproxy.return_value.dir.return_value = dir_return_value - owproxy.return_value.read.side_effect = read_side_effect + mock_device = MOCK_SWITCHES[device_id] + expected_entities = mock_device[SWITCH_DOMAIN] # Force enable switches patch_device_switches = copy.deepcopy(DEVICE_SWITCHES) - for item in patch_device_switches[device_family]: + for item in patch_device_switches[device_id[0:2]]: item["default_disabled"] = False with patch( @@ -101,21 +49,21 @@ async def test_owserver_switch(owproxy, hass, device_id): await setup_onewire_patched_owserver_integration(hass) await hass.async_block_till_done() - assert len(entity_registry.entities) == len(expected_sensors) + assert len(entity_registry.entities) == len(expected_entities) - for expected_sensor in expected_sensors: - entity_id = expected_sensor["entity_id"] + for expected_entity in expected_entities: + entity_id = expected_entity["entity_id"] registry_entry = entity_registry.entities.get(entity_id) assert registry_entry is not None state = hass.states.get(entity_id) - assert state.state == expected_sensor["result"] + assert state.state == expected_entity["result"] if state.state == STATE_ON: owproxy.return_value.read.side_effect = [b" 0"] - expected_sensor["result"] = STATE_OFF + expected_entity["result"] = STATE_OFF elif state.state == STATE_OFF: owproxy.return_value.read.side_effect = [b" 1"] - expected_sensor["result"] = STATE_ON + expected_entity["result"] = STATE_ON await hass.services.async_call( SWITCH_DOMAIN, @@ -126,7 +74,7 @@ async def test_owserver_switch(owproxy, hass, device_id): await hass.async_block_till_done() state = hass.states.get(entity_id) - assert state.state == expected_sensor["result"] - assert state.attributes["device_file"] == expected_sensor.get( + assert state.state == expected_entity["result"] + assert state.attributes["device_file"] == expected_entity.get( "device_file", registry_entry.unique_id ) From 2bf91fa35964a22c6f51ad3b96b6c347a718d02d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 1 Apr 2021 15:13:58 +0200 Subject: [PATCH 0008/1317] Move cast config flow tests to test_config_flow (#48362) --- tests/components/cast/test_config_flow.py | 238 +++++++++++++++++++++ tests/components/cast/test_init.py | 240 +--------------------- 2 files changed, 241 insertions(+), 237 deletions(-) create mode 100644 tests/components/cast/test_config_flow.py diff --git a/tests/components/cast/test_config_flow.py b/tests/components/cast/test_config_flow.py new file mode 100644 index 0000000000000..064406df717aa --- /dev/null +++ b/tests/components/cast/test_config_flow.py @@ -0,0 +1,238 @@ +"""Tests for the Cast config flow.""" +from unittest.mock import ANY, patch + +import pytest + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components import cast + +from tests.common import MockConfigEntry + + +async def test_creating_entry_sets_up_media_player(hass): + """Test setting up Cast loads the media player.""" + with patch( + "homeassistant.components.cast.media_player.async_setup_entry", + return_value=True, + ) as mock_setup, patch( + "pychromecast.discovery.discover_chromecasts", return_value=(True, None) + ), patch( + "pychromecast.discovery.stop_discovery" + ): + result = await hass.config_entries.flow.async_init( + cast.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + # Confirmation form + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + await hass.async_block_till_done() + + assert len(mock_setup.mock_calls) == 1 + + +@pytest.mark.parametrize("source", ["import", "user", "zeroconf"]) +async def test_single_instance(hass, source): + """Test we only allow a single config flow.""" + MockConfigEntry(domain="cast").add_to_hass(hass) + await hass.async_block_till_done() + + result = await hass.config_entries.flow.async_init( + "cast", context={"source": source} + ) + assert result["type"] == "abort" + assert result["reason"] == "single_instance_allowed" + + +async def test_user_setup(hass): + """Test we can finish a config flow.""" + result = await hass.config_entries.flow.async_init( + "cast", context={"source": "user"} + ) + assert result["type"] == "form" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + users = await hass.auth.async_get_users() + assert len(users) == 1 + assert result["type"] == "create_entry" + assert result["result"].data == { + "ignore_cec": [], + "known_hosts": [], + "uuid": [], + "user_id": users[0].id, # Home Assistant cast user + } + + +async def test_user_setup_options(hass): + """Test we can finish a config flow.""" + result = await hass.config_entries.flow.async_init( + "cast", context={"source": "user"} + ) + assert result["type"] == "form" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"known_hosts": "192.168.0.1, , 192.168.0.2 "} + ) + + users = await hass.auth.async_get_users() + assert len(users) == 1 + assert result["type"] == "create_entry" + assert result["result"].data == { + "ignore_cec": [], + "known_hosts": ["192.168.0.1", "192.168.0.2"], + "uuid": [], + "user_id": users[0].id, # Home Assistant cast user + } + + +async def test_zeroconf_setup(hass): + """Test we can finish a config flow through zeroconf.""" + result = await hass.config_entries.flow.async_init( + "cast", context={"source": "zeroconf"} + ) + assert result["type"] == "form" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + users = await hass.auth.async_get_users() + assert len(users) == 1 + assert result["type"] == "create_entry" + assert result["result"].data == { + "ignore_cec": [], + "known_hosts": [], + "uuid": [], + "user_id": users[0].id, # Home Assistant cast user + } + + +def get_suggested(schema, key): + """Get suggested value for key in voluptuous schema.""" + for k in schema.keys(): + if k == key: + if k.description is None or "suggested_value" not in k.description: + return None + return k.description["suggested_value"] + + +@pytest.mark.parametrize( + "parameter_data", + [ + ( + "known_hosts", + ["192.168.0.10", "192.168.0.11"], + "192.168.0.10,192.168.0.11", + "192.168.0.1, , 192.168.0.2 ", + ["192.168.0.1", "192.168.0.2"], + ), + ( + "uuid", + ["bla", "blu"], + "bla,blu", + "foo, , bar ", + ["foo", "bar"], + ), + ( + "ignore_cec", + ["cast1", "cast2"], + "cast1,cast2", + "other_cast, , some_cast ", + ["other_cast", "some_cast"], + ), + ], +) +async def test_option_flow(hass, parameter_data): + """Test config flow options.""" + all_parameters = ["ignore_cec", "known_hosts", "uuid"] + parameter, initial, suggested, user_input, updated = parameter_data + + data = { + "ignore_cec": [], + "known_hosts": [], + "uuid": [], + } + data[parameter] = initial + config_entry = MockConfigEntry(domain="cast", data=data) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + # Test ignore_cec and uuid options are hidden if advanced options are disabled + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "options" + data_schema = result["data_schema"].schema + assert set(data_schema) == {"known_hosts"} + + # Reconfigure ignore_cec, known_hosts, uuid + context = {"source": "user", "show_advanced_options": True} + result = await hass.config_entries.options.async_init( + config_entry.entry_id, context=context + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "options" + data_schema = result["data_schema"].schema + for other_param in all_parameters: + if other_param == parameter: + continue + assert get_suggested(data_schema, other_param) == "" + assert get_suggested(data_schema, parameter) == suggested + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={parameter: user_input}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"] is None + for other_param in all_parameters: + if other_param == parameter: + continue + assert config_entry.data[other_param] == [] + assert config_entry.data[parameter] == updated + + # Clear known_hosts + result = await hass.config_entries.options.async_init(config_entry.entry_id) + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"known_hosts": ""}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"] is None + assert config_entry.data == {"ignore_cec": [], "known_hosts": [], "uuid": []} + + +async def test_known_hosts(hass, castbrowser_mock, castbrowser_constructor_mock): + """Test known hosts is passed to pychromecasts.""" + result = await hass.config_entries.flow.async_init( + "cast", context={"source": "user"} + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"known_hosts": "192.168.0.1, 192.168.0.2"} + ) + assert result["type"] == "create_entry" + await hass.async_block_till_done() + config_entry = hass.config_entries.async_entries("cast")[0] + + assert castbrowser_mock.start_discovery.call_count == 1 + castbrowser_constructor_mock.assert_called_once_with( + ANY, ANY, ["192.168.0.1", "192.168.0.2"] + ) + castbrowser_mock.reset_mock() + castbrowser_constructor_mock.reset_mock() + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"known_hosts": "192.168.0.11, 192.168.0.12"}, + ) + + await hass.async_block_till_done() + + castbrowser_mock.start_discovery.assert_not_called() + castbrowser_constructor_mock.assert_not_called() + castbrowser_mock.host_browser.update_hosts.assert_called_once_with( + ["192.168.0.11", "192.168.0.12"] + ) diff --git a/tests/components/cast/test_init.py b/tests/components/cast/test_init.py index 888ef2ebcd797..178f721959f87 100644 --- a/tests/components/cast/test_init.py +++ b/tests/components/cast/test_init.py @@ -1,39 +1,9 @@ -"""Tests for the Cast config flow.""" -from unittest.mock import ANY, patch +"""Tests for the Cast integration.""" +from unittest.mock import patch -import pytest - -from homeassistant import config_entries, data_entry_flow from homeassistant.components import cast from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry - - -async def test_creating_entry_sets_up_media_player(hass): - """Test setting up Cast loads the media player.""" - with patch( - "homeassistant.components.cast.media_player.async_setup_entry", - return_value=True, - ) as mock_setup, patch( - "pychromecast.discovery.discover_chromecasts", return_value=(True, None) - ), patch( - "pychromecast.discovery.stop_discovery" - ): - result = await hass.config_entries.flow.async_init( - cast.DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - # Confirmation form - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - - await hass.async_block_till_done() - - assert len(mock_setup.mock_calls) == 1 - async def test_import(hass, caplog): """Test that specifying config will create an entry.""" @@ -67,7 +37,7 @@ async def test_import(hass, caplog): async def test_not_configuring_cast_not_creates_entry(hass): - """Test that no config will not create an entry.""" + """Test that an empty config does not create an entry.""" with patch( "homeassistant.components.cast.async_setup_entry", return_value=True ) as mock_setup: @@ -75,207 +45,3 @@ async def test_not_configuring_cast_not_creates_entry(hass): await hass.async_block_till_done() assert len(mock_setup.mock_calls) == 0 - - -@pytest.mark.parametrize("source", ["import", "user", "zeroconf"]) -async def test_single_instance(hass, source): - """Test we only allow a single config flow.""" - MockConfigEntry(domain="cast").add_to_hass(hass) - await hass.async_block_till_done() - - result = await hass.config_entries.flow.async_init( - "cast", context={"source": source} - ) - assert result["type"] == "abort" - assert result["reason"] == "single_instance_allowed" - - -async def test_user_setup(hass): - """Test we can finish a config flow.""" - result = await hass.config_entries.flow.async_init( - "cast", context={"source": "user"} - ) - assert result["type"] == "form" - - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - - users = await hass.auth.async_get_users() - assert len(users) == 1 - assert result["type"] == "create_entry" - assert result["result"].data == { - "ignore_cec": [], - "known_hosts": [], - "uuid": [], - "user_id": users[0].id, # Home Assistant cast user - } - - -async def test_user_setup_options(hass): - """Test we can finish a config flow.""" - result = await hass.config_entries.flow.async_init( - "cast", context={"source": "user"} - ) - assert result["type"] == "form" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"known_hosts": "192.168.0.1, , 192.168.0.2 "} - ) - - users = await hass.auth.async_get_users() - assert len(users) == 1 - assert result["type"] == "create_entry" - assert result["result"].data == { - "ignore_cec": [], - "known_hosts": ["192.168.0.1", "192.168.0.2"], - "uuid": [], - "user_id": users[0].id, # Home Assistant cast user - } - - -async def test_zeroconf_setup(hass): - """Test we can finish a config flow through zeroconf.""" - result = await hass.config_entries.flow.async_init( - "cast", context={"source": "zeroconf"} - ) - assert result["type"] == "form" - - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - - users = await hass.auth.async_get_users() - assert len(users) == 1 - assert result["type"] == "create_entry" - assert result["result"].data == { - "ignore_cec": [], - "known_hosts": [], - "uuid": [], - "user_id": users[0].id, # Home Assistant cast user - } - - -def get_suggested(schema, key): - """Get suggested value for key in voluptuous schema.""" - for k in schema.keys(): - if k == key: - if k.description is None or "suggested_value" not in k.description: - return None - return k.description["suggested_value"] - - -@pytest.mark.parametrize( - "parameter_data", - [ - ( - "known_hosts", - ["192.168.0.10", "192.168.0.11"], - "192.168.0.10,192.168.0.11", - "192.168.0.1, , 192.168.0.2 ", - ["192.168.0.1", "192.168.0.2"], - ), - ( - "uuid", - ["bla", "blu"], - "bla,blu", - "foo, , bar ", - ["foo", "bar"], - ), - ( - "ignore_cec", - ["cast1", "cast2"], - "cast1,cast2", - "other_cast, , some_cast ", - ["other_cast", "some_cast"], - ), - ], -) -async def test_option_flow(hass, parameter_data): - """Test config flow options.""" - all_parameters = ["ignore_cec", "known_hosts", "uuid"] - parameter, initial, suggested, user_input, updated = parameter_data - - data = { - "ignore_cec": [], - "known_hosts": [], - "uuid": [], - } - data[parameter] = initial - config_entry = MockConfigEntry(domain="cast", data=data) - config_entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - # Test ignore_cec and uuid options are hidden if advanced options are disabled - result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "options" - data_schema = result["data_schema"].schema - assert set(data_schema) == {"known_hosts"} - - # Reconfigure ignore_cec, known_hosts, uuid - context = {"source": "user", "show_advanced_options": True} - result = await hass.config_entries.options.async_init( - config_entry.entry_id, context=context - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "options" - data_schema = result["data_schema"].schema - for other_param in all_parameters: - if other_param == parameter: - continue - assert get_suggested(data_schema, other_param) == "" - assert get_suggested(data_schema, parameter) == suggested - - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={parameter: user_input}, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["data"] is None - for other_param in all_parameters: - if other_param == parameter: - continue - assert config_entry.data[other_param] == [] - assert config_entry.data[parameter] == updated - - # Clear known_hosts - result = await hass.config_entries.options.async_init(config_entry.entry_id) - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={"known_hosts": ""}, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["data"] is None - assert config_entry.data == {"ignore_cec": [], "known_hosts": [], "uuid": []} - - -async def test_known_hosts(hass, castbrowser_mock, castbrowser_constructor_mock): - """Test known hosts is passed to pychromecasts.""" - result = await hass.config_entries.flow.async_init( - "cast", context={"source": "user"} - ) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"known_hosts": "192.168.0.1, 192.168.0.2"} - ) - assert result["type"] == "create_entry" - await hass.async_block_till_done() - config_entry = hass.config_entries.async_entries("cast")[0] - - assert castbrowser_mock.start_discovery.call_count == 1 - castbrowser_constructor_mock.assert_called_once_with( - ANY, ANY, ["192.168.0.1", "192.168.0.2"] - ) - castbrowser_mock.reset_mock() - castbrowser_constructor_mock.reset_mock() - - result = await hass.config_entries.options.async_init(config_entry.entry_id) - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={"known_hosts": "192.168.0.11, 192.168.0.12"}, - ) - - await hass.async_block_till_done() - - castbrowser_mock.start_discovery.assert_not_called() - castbrowser_constructor_mock.assert_not_called() - castbrowser_mock.host_browser.update_hosts.assert_called_once_with( - ["192.168.0.11", "192.168.0.12"] - ) From d26d2a8446532ec2fef252b62d2a2540f5fa2db0 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 1 Apr 2021 16:20:53 +0200 Subject: [PATCH 0009/1317] Return config entry details for 1-step config flows (#48585) --- .../components/config/config_entries.py | 27 +++++++++---------- .../components/config/test_config_entries.py | 12 ++++++++- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index af90cdcba4b0c..edf9426874135 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -97,6 +97,17 @@ async def post(self, request, entry_id): return self.json({"require_restart": not result}) +def _prepare_config_flow_result_json(result, prepare_result_json): + """Convert result to JSON.""" + if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + return prepare_result_json(result) + + data = result.copy() + data["result"] = entry_json(result["result"]) + data.pop("data") + return data + + class ConfigManagerFlowIndexView(FlowManagerIndexView): """View to create config flows.""" @@ -118,13 +129,7 @@ async def post(self, request): def _prepare_result_json(self, result): """Convert result to JSON.""" - if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: - return super()._prepare_result_json(result) - - data = result.copy() - data["result"] = data["result"].entry_id - data.pop("data") - return data + return _prepare_config_flow_result_json(result, super()._prepare_result_json) class ConfigManagerFlowResourceView(FlowManagerResourceView): @@ -151,13 +156,7 @@ async def post(self, request, flow_id): def _prepare_result_json(self, result): """Convert result to JSON.""" - if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: - return super()._prepare_result_json(result) - - data = result.copy() - data["result"] = entry_json(result["result"]) - data.pop("data") - return data + return _prepare_config_flow_result_json(result, super()._prepare_result_json) class ConfigManagerAvailableFlowView(HomeAssistantView): diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 7e4df556fa59c..128d0798b660f 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -320,7 +320,17 @@ async def async_step_user(self, user_input=None): "title": "Test Entry", "type": "create_entry", "version": 1, - "result": entries[0].entry_id, + "result": { + "connection_class": "unknown", + "disabled_by": None, + "domain": "test", + "entry_id": entries[0].entry_id, + "source": "user", + "state": "loaded", + "supports_options": False, + "supports_unload": False, + "title": "Test Entry", + }, "description": None, "description_placeholders": None, } From 6ce96dcb634505ee6ac14177b809f4ecc7265935 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 1 Apr 2021 18:02:28 +0200 Subject: [PATCH 0010/1317] Don't care about DPI entries when looking for clients to be restored from UniFi (#48579) * DPI switches shouldnt be restored, they're not part of clients to be restored * Only care about Block and POE switch entries --- homeassistant/components/unifi/controller.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index dc56cd9d9e3d9..c77987bcbddf7 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -29,6 +29,7 @@ from homeassistant.components.device_tracker import DOMAIN as TRACKER_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.components.unifi.switch import BLOCK_SWITCH, POE_SWITCH from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.const import ( CONF_HOST, @@ -347,7 +348,10 @@ async def async_setup(self): ): if entry.domain == TRACKER_DOMAIN: mac = entry.unique_id.split("-", 1)[0] - elif entry.domain == SWITCH_DOMAIN: + elif entry.domain == SWITCH_DOMAIN and ( + entry.unique_id.startswith(BLOCK_SWITCH) + or entry.unique_id.startswith(POE_SWITCH) + ): mac = entry.unique_id.split("-", 1)[1] else: continue From 9d085778c270749a4079c415873dfe4b271b2ac7 Mon Sep 17 00:00:00 2001 From: youknowjack0 Date: Thu, 1 Apr 2021 09:32:59 -0700 Subject: [PATCH 0011/1317] Fix timer.finish to cancel callback (#48549) Timer.finish doesn't cancel the callback, which can lead to incorrect early cancellation of the timer if it is subsequently restarted. Bug reported here: https://community.home-assistant.io/t/timer-component-timer-stops-before-time-is-up/96038 --- homeassistant/components/timer/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/timer/__init__.py b/homeassistant/components/timer/__init__.py index 216ab3217a5e2..2ff408dcd819e 100644 --- a/homeassistant/components/timer/__init__.py +++ b/homeassistant/components/timer/__init__.py @@ -327,7 +327,9 @@ def async_finish(self): if self._state != STATUS_ACTIVE: return - self._listener = None + if self._listener: + self._listener() + self._listener = None self._state = STATUS_IDLE self._end = None self._remaining = None From 9f481e16422792b30ced30b3b0b884f091e23ec2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 1 Apr 2021 18:42:23 +0200 Subject: [PATCH 0012/1317] Include script script_execution in script and automation traces (#48576) --- .../components/automation/__init__.py | 2 + homeassistant/components/trace/__init__.py | 4 + homeassistant/helpers/script.py | 13 +- homeassistant/helpers/trace.py | 27 +++ tests/components/trace/test_websocket_api.py | 166 +++++++++++++++++- tests/helpers/test_script.py | 36 ++-- 6 files changed, 228 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 554b27fdb2fdf..6caa53dff7928 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -57,6 +57,7 @@ from homeassistant.helpers.service import async_register_admin_service from homeassistant.helpers.trace import ( TraceElement, + script_execution_set, trace_append_element, trace_get, trace_path, @@ -471,6 +472,7 @@ async def async_trigger(self, run_variables, context=None, skip_condition=False) "Conditions not met, aborting automation. Condition summary: %s", trace_get(clear=False), ) + script_execution_set("failed_conditions") return self.async_set_context(trigger_context) diff --git a/homeassistant/components/trace/__init__.py b/homeassistant/components/trace/__init__.py index cdae44fff6be5..c17cbf8671579 100644 --- a/homeassistant/components/trace/__init__.py +++ b/homeassistant/components/trace/__init__.py @@ -8,6 +8,7 @@ from homeassistant.core import Context from homeassistant.helpers.trace import ( TraceElement, + script_execution_get, trace_id_get, trace_id_set, trace_set_child_id, @@ -55,6 +56,7 @@ def __init__( self.context: Context = context self._error: Exception | None = None self._state: str = "running" + self._script_execution: str | None = None self.run_id: str = str(next(self._run_ids)) self._timestamp_finish: dt.datetime | None = None self._timestamp_start: dt.datetime = dt_util.utcnow() @@ -75,6 +77,7 @@ def finished(self) -> None: """Set finish time.""" self._timestamp_finish = dt_util.utcnow() self._state = "stopped" + self._script_execution = script_execution_get() def as_dict(self) -> dict[str, Any]: """Return dictionary version of this ActionTrace.""" @@ -109,6 +112,7 @@ def as_short_dict(self) -> dict[str, Any]: "last_step": last_step, "run_id": self.run_id, "state": self._state, + "script_execution": self._script_execution, "timestamp": { "start": self._timestamp_start, "finish": self._timestamp_finish, diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index e342f0ff9a89f..bf52fc81b6a39 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -63,6 +63,7 @@ ) from homeassistant.helpers.event import async_call_later, async_track_template from homeassistant.helpers.script_variables import ScriptVariables +from homeassistant.helpers.trace import script_execution_set from homeassistant.helpers.trigger import ( async_initialize_triggers, async_validate_trigger_config, @@ -332,15 +333,19 @@ def _step_log(self, default_message, timeout=None): async def async_run(self) -> None: """Run script.""" try: - if self._stop.is_set(): - return self._log("Running %s", self._script.running_description) for self._step, self._action in enumerate(self._script.sequence): if self._stop.is_set(): + script_execution_set("cancelled") break await self._async_step(log_exceptions=False) + else: + script_execution_set("finished") except _StopScript: - pass + script_execution_set("aborted") + except Exception: + script_execution_set("error") + raise finally: self._finish() @@ -1137,6 +1142,7 @@ async def async_run( if self.script_mode == SCRIPT_MODE_SINGLE: if self._max_exceeded != "SILENT": self._log("Already running", level=LOGSEVERITY[self._max_exceeded]) + script_execution_set("failed_single") return if self.script_mode == SCRIPT_MODE_RESTART: self._log("Restarting") @@ -1147,6 +1153,7 @@ async def async_run( "Maximum number of runs exceeded", level=LOGSEVERITY[self._max_exceeded], ) + script_execution_set("failed_max_runs") return # If this is a top level Script then make a copy of the variables in case they diff --git a/homeassistant/helpers/trace.py b/homeassistant/helpers/trace.py index 5d5a0f5ff033f..c92766036c639 100644 --- a/homeassistant/helpers/trace.py +++ b/homeassistant/helpers/trace.py @@ -88,6 +88,10 @@ def as_dict(self) -> dict[str, Any]: trace_id_cv: ContextVar[tuple[str, str] | None] = ContextVar( "trace_id_cv", default=None ) +# Reason for stopped script execution +script_execution_cv: ContextVar[StopReason | None] = ContextVar( + "script_execution_cv", default=None +) def trace_id_set(trace_id: tuple[str, str]) -> None: @@ -172,6 +176,7 @@ def trace_clear() -> None: trace_stack_cv.set(None) trace_path_stack_cv.set(None) variables_cv.set(None) + script_execution_cv.set(StopReason()) def trace_set_child_id(child_key: tuple[str, str], child_run_id: str) -> None: @@ -187,6 +192,28 @@ def trace_set_result(**kwargs: Any) -> None: node.set_result(**kwargs) +class StopReason: + """Mutable container class for script_execution.""" + + script_execution: str | None = None + + +def script_execution_set(reason: str) -> None: + """Set stop reason.""" + data = script_execution_cv.get() + if data is None: + return + data.script_execution = reason + + +def script_execution_get() -> str | None: + """Return the current trace.""" + data = script_execution_cv.get() + if data is None: + return None + return data.script_execution + + @contextmanager def trace_path(suffix: str | list[str]) -> Generator: """Go deeper in the config tree. diff --git a/tests/components/trace/test_websocket_api.py b/tests/components/trace/test_websocket_api.py index 84d7100d1c840..8e481dd34b94a 100644 --- a/tests/components/trace/test_websocket_api.py +++ b/tests/components/trace/test_websocket_api.py @@ -1,9 +1,11 @@ """Test Trace websocket API.""" +import asyncio + import pytest from homeassistant.bootstrap import async_setup_component from homeassistant.components.trace.const import STORED_TRACES -from homeassistant.core import Context +from homeassistant.core import Context, callback from homeassistant.helpers.typing import UNDEFINED from tests.common import assert_lists_same @@ -170,6 +172,7 @@ def next_id(): assert trace["context"] assert trace["error"] == "Unable to find service test.automation" assert trace["state"] == "stopped" + assert trace["script_execution"] == "error" assert trace["item_id"] == "sun" assert trace["context"][context_key] == context.id assert trace.get("trigger", UNDEFINED) == trigger[0] @@ -210,6 +213,7 @@ def next_id(): assert trace["context"] assert "error" not in trace assert trace["state"] == "stopped" + assert trace["script_execution"] == "finished" assert trace["item_id"] == "moon" assert trace.get("trigger", UNDEFINED) == trigger[1] @@ -260,6 +264,7 @@ def next_id(): assert trace["context"] assert "error" not in trace assert trace["state"] == "stopped" + assert trace["script_execution"] == "failed_conditions" assert trace["trigger"] == "event 'test_event3'" assert trace["item_id"] == "moon" contexts[trace["context"]["id"]] = { @@ -301,6 +306,7 @@ def next_id(): assert trace["context"] assert "error" not in trace assert trace["state"] == "stopped" + assert trace["script_execution"] == "finished" assert trace["trigger"] == "event 'test_event2'" assert trace["item_id"] == "moon" contexts[trace["context"]["id"]] = { @@ -391,7 +397,7 @@ def next_id(): @pytest.mark.parametrize( - "domain, prefix, trigger, last_step", + "domain, prefix, trigger, last_step, script_execution", [ ( "automation", @@ -403,16 +409,20 @@ def next_id(): "event 'test_event2'", ], ["{prefix}/0", "{prefix}/0", "condition/0", "{prefix}/0"], + ["error", "finished", "failed_conditions", "finished"], ), ( "script", "sequence", [UNDEFINED, UNDEFINED, UNDEFINED, UNDEFINED], ["{prefix}/0", "{prefix}/0", "{prefix}/0", "{prefix}/0"], + ["error", "finished", "finished", "finished"], ), ], ) -async def test_list_traces(hass, hass_ws_client, domain, prefix, trigger, last_step): +async def test_list_traces( + hass, hass_ws_client, domain, prefix, trigger, last_step, script_execution +): """Test listing script and automation traces.""" id = 1 @@ -458,7 +468,7 @@ def next_id(): await _run_automation_or_script(hass, domain, sun_config, "test_event") await hass.async_block_till_done() - # Get trace + # List traces await client.send_json({"id": next_id(), "type": "trace/list", "domain": domain}) response = await client.receive_json() assert response["success"] @@ -492,7 +502,7 @@ def next_id(): await _run_automation_or_script(hass, domain, moon_config, "test_event2") await hass.async_block_till_done() - # Get trace + # List traces await client.send_json({"id": next_id(), "type": "trace/list", "domain": domain}) response = await client.receive_json() assert response["success"] @@ -502,6 +512,7 @@ def next_id(): assert trace["last_step"] == last_step[0].format(prefix=prefix) assert trace["error"] == "Unable to find service test.automation" assert trace["state"] == "stopped" + assert trace["script_execution"] == script_execution[0] assert trace["timestamp"] assert trace["item_id"] == "sun" assert trace.get("trigger", UNDEFINED) == trigger[0] @@ -510,6 +521,7 @@ def next_id(): assert trace["last_step"] == last_step[1].format(prefix=prefix) assert "error" not in trace assert trace["state"] == "stopped" + assert trace["script_execution"] == script_execution[1] assert trace["timestamp"] assert trace["item_id"] == "moon" assert trace.get("trigger", UNDEFINED) == trigger[1] @@ -518,6 +530,7 @@ def next_id(): assert trace["last_step"] == last_step[2].format(prefix=prefix) assert "error" not in trace assert trace["state"] == "stopped" + assert trace["script_execution"] == script_execution[2] assert trace["timestamp"] assert trace["item_id"] == "moon" assert trace.get("trigger", UNDEFINED) == trigger[2] @@ -526,6 +539,7 @@ def next_id(): assert trace["last_step"] == last_step[3].format(prefix=prefix) assert "error" not in trace assert trace["state"] == "stopped" + assert trace["script_execution"] == script_execution[3] assert trace["timestamp"] assert trace["item_id"] == "moon" assert trace.get("trigger", UNDEFINED) == trigger[3] @@ -1006,3 +1020,145 @@ async def assert_last_step(item_id, expected_action, expected_state): "node": f"{prefix}/5", "run_id": run_id, } + + +@pytest.mark.parametrize( + "script_mode,max_runs,script_execution", + [ + ({"mode": "single"}, 1, "failed_single"), + ({"mode": "parallel", "max": 2}, 2, "failed_max_runs"), + ], +) +async def test_script_mode( + hass, hass_ws_client, script_mode, max_runs, script_execution +): + """Test overlapping runs with max_runs > 1.""" + id = 1 + + def next_id(): + nonlocal id + id += 1 + return id + + flag = asyncio.Event() + + @callback + def _handle_event(_): + flag.set() + + event = "test_event" + script_config = { + "script1": { + "sequence": [ + {"event": event, "event_data": {"value": 1}}, + {"wait_template": "{{ states.switch.test.state == 'off' }}"}, + {"event": event, "event_data": {"value": 2}}, + ], + **script_mode, + }, + } + client = await hass_ws_client() + hass.bus.async_listen(event, _handle_event) + assert await async_setup_component(hass, "script", {"script": script_config}) + + for _ in range(max_runs): + hass.states.async_set("switch.test", "on") + await hass.services.async_call("script", "script1") + await asyncio.wait_for(flag.wait(), 1) + + # List traces + await client.send_json({"id": next_id(), "type": "trace/list", "domain": "script"}) + response = await client.receive_json() + assert response["success"] + traces = _find_traces(response["result"], "script", "script1") + assert len(traces) == max_runs + for trace in traces: + assert trace["state"] == "running" + + # Start additional run of script while first runs are suspended in wait_template. + + flag.clear() + await hass.services.async_call("script", "script1") + + # List traces + await client.send_json({"id": next_id(), "type": "trace/list", "domain": "script"}) + response = await client.receive_json() + assert response["success"] + traces = _find_traces(response["result"], "script", "script1") + assert len(traces) == max_runs + 1 + assert traces[-1]["state"] == "stopped" + assert traces[-1]["script_execution"] == script_execution + + +@pytest.mark.parametrize( + "script_mode,script_execution", + [("restart", "cancelled"), ("parallel", "finished")], +) +async def test_script_mode_2(hass, hass_ws_client, script_mode, script_execution): + """Test overlapping runs with max_runs > 1.""" + id = 1 + + def next_id(): + nonlocal id + id += 1 + return id + + flag = asyncio.Event() + + @callback + def _handle_event(_): + flag.set() + + event = "test_event" + script_config = { + "script1": { + "sequence": [ + {"event": event, "event_data": {"value": 1}}, + {"wait_template": "{{ states.switch.test.state == 'off' }}"}, + {"event": event, "event_data": {"value": 2}}, + ], + "mode": script_mode, + } + } + client = await hass_ws_client() + hass.bus.async_listen(event, _handle_event) + assert await async_setup_component(hass, "script", {"script": script_config}) + + hass.states.async_set("switch.test", "on") + await hass.services.async_call("script", "script1") + await asyncio.wait_for(flag.wait(), 1) + + # List traces + await client.send_json({"id": next_id(), "type": "trace/list", "domain": "script"}) + response = await client.receive_json() + assert response["success"] + trace = _find_traces(response["result"], "script", "script1")[0] + assert trace["state"] == "running" + + # Start second run of script while first run is suspended in wait_template. + + flag.clear() + await hass.services.async_call("script", "script1") + await asyncio.wait_for(flag.wait(), 1) + + # List traces + await client.send_json({"id": next_id(), "type": "trace/list", "domain": "script"}) + response = await client.receive_json() + assert response["success"] + trace = _find_traces(response["result"], "script", "script1")[1] + assert trace["state"] == "running" + + # Let both scripts finish + hass.states.async_set("switch.test", "off") + await hass.async_block_till_done() + + # List traces + await client.send_json({"id": next_id(), "type": "trace/list", "domain": "script"}) + response = await client.receive_json() + assert response["success"] + trace = _find_traces(response["result"], "script", "script1")[0] + assert trace["state"] == "stopped" + assert trace["script_execution"] == script_execution + trace = _find_traces(response["result"], "script", "script1")[1] + assert trace["state"] == "stopped" + assert trace["script_execution"] == "finished" diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index e4170ccae20b6..7224dd706778e 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -86,9 +86,10 @@ def assert_element(trace_element, expected_element, path): assert not trace_element._variables -def assert_action_trace(expected): +def assert_action_trace(expected, expected_script_execution="finished"): """Assert a trace condition sequence is as expected.""" action_trace = trace.trace_get(clear=False) + script_execution = trace.script_execution_get() trace.trace_clear() expected_trace_keys = list(expected.keys()) assert list(action_trace.keys()) == expected_trace_keys @@ -98,6 +99,8 @@ def assert_action_trace(expected): path = f"[{trace_key_index}][{index}]" assert_element(action_trace[key][index], element, path) + assert script_execution == expected_script_execution + def async_watch_for_action(script_obj, message): """Watch for message in last_action.""" @@ -620,7 +623,8 @@ async def test_delay_template_invalid(hass, caplog): { "0": [{"result": {"event": "test_event", "event_data": {}}}], "1": [{"error_type": script._StopScript}], - } + }, + expected_script_execution="aborted", ) @@ -680,7 +684,8 @@ async def test_delay_template_complex_invalid(hass, caplog): { "0": [{"result": {"event": "test_event", "event_data": {}}}], "1": [{"error_type": script._StopScript}], - } + }, + expected_script_execution="aborted", ) @@ -717,7 +722,8 @@ async def test_cancel_delay(hass): assert_action_trace( { "0": [{"result": {"delay": 5.0, "done": False}}], - } + }, + expected_script_execution="cancelled", ) @@ -969,13 +975,15 @@ async def test_cancel_wait(hass, action_type): assert_action_trace( { "0": [{"result": {"wait": {"completed": False, "remaining": None}}}], - } + }, + expected_script_execution="cancelled", ) else: assert_action_trace( { "0": [{"result": {"wait": {"trigger": None, "remaining": None}}}], - } + }, + expected_script_execution="cancelled", ) @@ -1131,6 +1139,7 @@ async def test_wait_continue_on_timeout( if continue_on_timeout is False: expected_trace["0"][0]["result"]["timeout"] = True expected_trace["0"][0]["error_type"] = script._StopScript + expected_script_execution = "aborted" else: expected_trace["1"] = [ { @@ -1138,7 +1147,8 @@ async def test_wait_continue_on_timeout( "variables": variable_wait, } ] - assert_action_trace(expected_trace) + expected_script_execution = "finished" + assert_action_trace(expected_trace, expected_script_execution) async def test_wait_template_variables_in(hass): @@ -1404,7 +1414,8 @@ async def test_condition_warning(hass, caplog): "1": [{"error_type": script._StopScript, "result": {"result": False}}], "1/condition": [{"error_type": ConditionError}], "1/condition/entity_id/0": [{"error_type": ConditionError}], - } + }, + expected_script_execution="aborted", ) @@ -1456,7 +1467,8 @@ async def test_condition_basic(hass, caplog): "0": [{"result": {"event": "test_event", "event_data": {}}}], "1": [{"error_type": script._StopScript, "result": {"result": False}}], "1/condition": [{"result": {"result": False}}], - } + }, + expected_script_execution="aborted", ) @@ -2141,7 +2153,7 @@ async def test_propagate_error_service_not_found(hass): } ], } - assert_action_trace(expected_trace) + assert_action_trace(expected_trace, expected_script_execution="error") async def test_propagate_error_invalid_service_data(hass): @@ -2178,7 +2190,7 @@ async def test_propagate_error_invalid_service_data(hass): } ], } - assert_action_trace(expected_trace) + assert_action_trace(expected_trace, expected_script_execution="error") async def test_propagate_error_service_exception(hass): @@ -2219,7 +2231,7 @@ def record_call(service): } ], } - assert_action_trace(expected_trace) + assert_action_trace(expected_trace, expected_script_execution="error") async def test_referenced_entities(hass): From f8f0495319175313fe52aa3c434c0998d93c9df7 Mon Sep 17 00:00:00 2001 From: MatthewFlamm <39341281+MatthewFlamm@users.noreply.github.com> Date: Thu, 1 Apr 2021 12:50:37 -0400 Subject: [PATCH 0013/1317] Add nws sensor platform (#45027) * Resolve rebase conflict. Remove logging * lint: fix elif after return * fix attribution * add tests for None valuea * Remove Entity import Co-authored-by: Erik Montnemery * Import SensorEntity Co-authored-by: Erik Montnemery * Inherit SensorEntity Co-authored-by: Erik Montnemery * remove unused logging * Use CoordinatorEntity * Use type instead of name. * add all entities * add nice rounding to temperature and humidity Co-authored-by: Erik Montnemery --- homeassistant/components/nws/__init__.py | 2 +- homeassistant/components/nws/const.py | 105 +++++++++++++++ homeassistant/components/nws/sensor.py | 156 +++++++++++++++++++++++ homeassistant/components/nws/weather.py | 7 +- tests/components/nws/conftest.py | 18 +++ tests/components/nws/const.py | 41 +++++- tests/components/nws/test_sensor.py | 95 ++++++++++++++ tests/components/nws/test_weather.py | 34 ++--- 8 files changed, 435 insertions(+), 23 deletions(-) create mode 100644 homeassistant/components/nws/sensor.py create mode 100644 tests/components/nws/test_sensor.py diff --git a/homeassistant/components/nws/__init__.py b/homeassistant/components/nws/__init__.py index 569a8adf83b70..9cdf17fa264ac 100644 --- a/homeassistant/components/nws/__init__.py +++ b/homeassistant/components/nws/__init__.py @@ -28,7 +28,7 @@ _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["weather"] +PLATFORMS = ["sensor", "weather"] DEFAULT_SCAN_INTERVAL = datetime.timedelta(minutes=10) FAILED_SCAN_INTERVAL = datetime.timedelta(minutes=1) diff --git a/homeassistant/components/nws/const.py b/homeassistant/components/nws/const.py index f055bab020346..f82a70ea4e0ff 100644 --- a/homeassistant/components/nws/const.py +++ b/homeassistant/components/nws/const.py @@ -1,4 +1,6 @@ """Constants for National Weather Service Integration.""" +from datetime import timedelta + from homeassistant.components.weather import ( ATTR_CONDITION_CLOUDY, ATTR_CONDITION_EXCEPTIONAL, @@ -14,6 +16,21 @@ ATTR_CONDITION_WINDY, ATTR_CONDITION_WINDY_VARIANT, ) +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + DEGREE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + LENGTH_METERS, + LENGTH_MILES, + PERCENTAGE, + PRESSURE_INHG, + PRESSURE_PA, + SPEED_KILOMETERS_PER_HOUR, + SPEED_MILES_PER_HOUR, + TEMP_CELSIUS, +) DOMAIN = "nws" @@ -23,6 +40,11 @@ ATTR_FORECAST_DETAILED_DESCRIPTION = "detailed_description" ATTR_FORECAST_DAYTIME = "daytime" +ATTR_ICON = "icon" +ATTR_LABEL = "label" +ATTR_UNIT = "unit" +ATTR_UNIT_CONVERT = "unit_convert" +ATTR_UNIT_CONVERT_METHOD = "unit_convert_method" CONDITION_CLASSES = { ATTR_CONDITION_EXCEPTIONAL: [ @@ -75,3 +97,86 @@ COORDINATOR_OBSERVATION = "coordinator_observation" COORDINATOR_FORECAST = "coordinator_forecast" COORDINATOR_FORECAST_HOURLY = "coordinator_forecast_hourly" + +OBSERVATION_VALID_TIME = timedelta(minutes=20) +FORECAST_VALID_TIME = timedelta(minutes=45) + +SENSOR_TYPES = { + "dewpoint": { + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ICON: None, + ATTR_LABEL: "Dew Point", + ATTR_UNIT: TEMP_CELSIUS, + ATTR_UNIT_CONVERT: TEMP_CELSIUS, + }, + "temperature": { + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ICON: None, + ATTR_LABEL: "Temperature", + ATTR_UNIT: TEMP_CELSIUS, + ATTR_UNIT_CONVERT: TEMP_CELSIUS, + }, + "windChill": { + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ICON: None, + ATTR_LABEL: "Wind Chill", + ATTR_UNIT: TEMP_CELSIUS, + ATTR_UNIT_CONVERT: TEMP_CELSIUS, + }, + "heatIndex": { + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ICON: None, + ATTR_LABEL: "Heat Index", + ATTR_UNIT: TEMP_CELSIUS, + ATTR_UNIT_CONVERT: TEMP_CELSIUS, + }, + "relativeHumidity": { + ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + ATTR_ICON: None, + ATTR_LABEL: "Relative Humidity", + ATTR_UNIT: PERCENTAGE, + ATTR_UNIT_CONVERT: PERCENTAGE, + }, + "windSpeed": { + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:weather-windy", + ATTR_LABEL: "Wind Speed", + ATTR_UNIT: SPEED_KILOMETERS_PER_HOUR, + ATTR_UNIT_CONVERT: SPEED_MILES_PER_HOUR, + }, + "windGust": { + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:weather-windy", + ATTR_LABEL: "Wind Gust", + ATTR_UNIT: SPEED_KILOMETERS_PER_HOUR, + ATTR_UNIT_CONVERT: SPEED_MILES_PER_HOUR, + }, + "windDirection": { + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:compass-rose", + ATTR_LABEL: "Wind Direction", + ATTR_UNIT: DEGREE, + ATTR_UNIT_CONVERT: DEGREE, + }, + "barometricPressure": { + ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, + ATTR_ICON: None, + ATTR_LABEL: "Barometric Pressure", + ATTR_UNIT: PRESSURE_PA, + ATTR_UNIT_CONVERT: PRESSURE_INHG, + }, + "seaLevelPressure": { + ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, + ATTR_ICON: None, + ATTR_LABEL: "Sea Level Pressure", + ATTR_UNIT: PRESSURE_PA, + ATTR_UNIT_CONVERT: PRESSURE_INHG, + }, + "visibility": { + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:eye", + ATTR_LABEL: "Visibility", + ATTR_UNIT: LENGTH_METERS, + ATTR_UNIT_CONVERT: LENGTH_MILES, + }, +} diff --git a/homeassistant/components/nws/sensor.py b/homeassistant/components/nws/sensor.py new file mode 100644 index 0000000000000..bff5cdca58954 --- /dev/null +++ b/homeassistant/components/nws/sensor.py @@ -0,0 +1,156 @@ +"""Sensors for National Weather Service (NWS).""" +from homeassistant.components.sensor import SensorEntity +from homeassistant.const import ( + ATTR_ATTRIBUTION, + ATTR_DEVICE_CLASS, + CONF_LATITUDE, + CONF_LONGITUDE, + LENGTH_KILOMETERS, + LENGTH_METERS, + LENGTH_MILES, + PERCENTAGE, + PRESSURE_INHG, + PRESSURE_PA, + SPEED_MILES_PER_HOUR, + TEMP_CELSIUS, +) +from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.util.distance import convert as convert_distance +from homeassistant.util.dt import utcnow +from homeassistant.util.pressure import convert as convert_pressure + +from . import base_unique_id +from .const import ( + ATTR_ICON, + ATTR_LABEL, + ATTR_UNIT, + ATTR_UNIT_CONVERT, + ATTRIBUTION, + CONF_STATION, + COORDINATOR_OBSERVATION, + DOMAIN, + NWS_DATA, + OBSERVATION_VALID_TIME, + SENSOR_TYPES, +) + +PARALLEL_UPDATES = 0 + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the NWS weather platform.""" + hass_data = hass.data[DOMAIN][entry.entry_id] + station = entry.data[CONF_STATION] + + entities = [] + for sensor_type, sensor_data in SENSOR_TYPES.items(): + if hass.config.units.is_metric: + unit = sensor_data[ATTR_UNIT] + else: + unit = sensor_data[ATTR_UNIT_CONVERT] + entities.append( + NWSSensor( + entry.data, + hass_data, + sensor_type, + station, + sensor_data[ATTR_LABEL], + sensor_data[ATTR_ICON], + sensor_data[ATTR_DEVICE_CLASS], + unit, + ), + ) + + async_add_entities(entities, False) + + +class NWSSensor(CoordinatorEntity, SensorEntity): + """An NWS Sensor Entity.""" + + def __init__( + self, + entry_data, + hass_data, + sensor_type, + station, + label, + icon, + device_class, + unit, + ): + """Initialise the platform with a data instance.""" + super().__init__(hass_data[COORDINATOR_OBSERVATION]) + self._nws = hass_data[NWS_DATA] + self._latitude = entry_data[CONF_LATITUDE] + self._longitude = entry_data[CONF_LONGITUDE] + self._type = sensor_type + self._station = station + self._label = label + self._icon = icon + self._device_class = device_class + self._unit = unit + + @property + def state(self): + """Return the state.""" + value = self._nws.observation.get(self._type) + if value is None: + return None + if self._unit == SPEED_MILES_PER_HOUR: + return round(convert_distance(value, LENGTH_KILOMETERS, LENGTH_MILES)) + if self._unit == LENGTH_MILES: + return round(convert_distance(value, LENGTH_METERS, LENGTH_MILES)) + if self._unit == PRESSURE_INHG: + return round(convert_pressure(value, PRESSURE_PA, PRESSURE_INHG), 2) + if self._unit == TEMP_CELSIUS: + return round(value, 1) + if self._unit == PERCENTAGE: + return round(value) + return value + + @property + def icon(self): + """Return the icon.""" + return self._icon + + @property + def device_class(self): + """Return the device class.""" + return self._device_class + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return self._unit + + @property + def device_state_attributes(self): + """Return the attribution.""" + return {ATTR_ATTRIBUTION: ATTRIBUTION} + + @property + def name(self): + """Return the name of the station.""" + return f"{self._station} {self._label}" + + @property + def unique_id(self): + """Return a unique_id for this entity.""" + return f"{base_unique_id(self._latitude, self._longitude)}_{self._type}" + + @property + def available(self): + """Return if state is available.""" + if self.coordinator.last_update_success_time: + last_success_time = ( + utcnow() - self.coordinator.last_update_success_time + < OBSERVATION_VALID_TIME + ) + else: + last_success_time = False + return self.coordinator.last_update_success or last_success_time + + @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/nws/weather.py b/homeassistant/components/nws/weather.py index 9f4e69bdb8c06..c84d1b78ea2b7 100644 --- a/homeassistant/components/nws/weather.py +++ b/homeassistant/components/nws/weather.py @@ -1,6 +1,4 @@ """Support for NWS weather service.""" -from datetime import timedelta - from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_SUNNY, @@ -42,15 +40,14 @@ COORDINATOR_OBSERVATION, DAYNIGHT, DOMAIN, + FORECAST_VALID_TIME, HOURLY, NWS_DATA, + OBSERVATION_VALID_TIME, ) PARALLEL_UPDATES = 0 -OBSERVATION_VALID_TIME = timedelta(minutes=20) -FORECAST_VALID_TIME = timedelta(minutes=45) - def convert_condition(time, weather): """ diff --git a/tests/components/nws/conftest.py b/tests/components/nws/conftest.py index d01201bb4846a..98ac9191e0d0c 100644 --- a/tests/components/nws/conftest.py +++ b/tests/components/nws/conftest.py @@ -32,3 +32,21 @@ def mock_simple_nws_config(): instance.station = "ABC" instance.stations = ["ABC"] yield mock_nws + + +@pytest.fixture() +def no_sensor(): + """Remove sensors.""" + with patch( + "homeassistant.components.nws.sensor.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture() +def no_weather(): + """Remove weather.""" + with patch( + "homeassistant.components.nws.weather.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry diff --git a/tests/components/nws/const.py b/tests/components/nws/const.py index ae2f826294fec..4f4b140dbf91e 100644 --- a/tests/components/nws/const.py +++ b/tests/components/nws/const.py @@ -44,6 +44,7 @@ DEFAULT_OBSERVATION = { "temperature": 10, "seaLevelPressure": 100000, + "barometricPressure": 100000, "relativeHumidity": 10, "windSpeed": 10, "windDirection": 180, @@ -53,9 +54,45 @@ "timestamp": "2019-08-12T23:53:00+00:00", "iconTime": "day", "iconWeather": (("Fair/clear", None),), + "dewpoint": 5, + "windChill": 5, + "heatIndex": 15, + "windGust": 20, } -EXPECTED_OBSERVATION_IMPERIAL = { +SENSOR_EXPECTED_OBSERVATION_METRIC = { + "dewpoint": "5", + "temperature": "10", + "windChill": "5", + "heatIndex": "15", + "relativeHumidity": "10", + "windSpeed": "10", + "windGust": "20", + "windDirection": "180", + "barometricPressure": "100000", + "seaLevelPressure": "100000", + "visibility": "10000", +} + +SENSOR_EXPECTED_OBSERVATION_IMPERIAL = { + "dewpoint": str(round(convert_temperature(5, TEMP_CELSIUS, TEMP_FAHRENHEIT))), + "temperature": str(round(convert_temperature(10, TEMP_CELSIUS, TEMP_FAHRENHEIT))), + "windChill": str(round(convert_temperature(5, TEMP_CELSIUS, TEMP_FAHRENHEIT))), + "heatIndex": str(round(convert_temperature(15, TEMP_CELSIUS, TEMP_FAHRENHEIT))), + "relativeHumidity": "10", + "windSpeed": str(round(convert_distance(10, LENGTH_KILOMETERS, LENGTH_MILES))), + "windGust": str(round(convert_distance(20, LENGTH_KILOMETERS, LENGTH_MILES))), + "windDirection": "180", + "barometricPressure": str( + round(convert_pressure(100000, PRESSURE_PA, PRESSURE_INHG), 2) + ), + "seaLevelPressure": str( + round(convert_pressure(100000, PRESSURE_PA, PRESSURE_INHG), 2) + ), + "visibility": str(round(convert_distance(10000, LENGTH_METERS, LENGTH_MILES))), +} + +WEATHER_EXPECTED_OBSERVATION_IMPERIAL = { ATTR_WEATHER_TEMPERATURE: round( convert_temperature(10, TEMP_CELSIUS, TEMP_FAHRENHEIT) ), @@ -72,7 +109,7 @@ ATTR_WEATHER_HUMIDITY: 10, } -EXPECTED_OBSERVATION_METRIC = { +WEATHER_EXPECTED_OBSERVATION_METRIC = { ATTR_WEATHER_TEMPERATURE: 10, ATTR_WEATHER_WIND_BEARING: 180, ATTR_WEATHER_WIND_SPEED: 10, diff --git a/tests/components/nws/test_sensor.py b/tests/components/nws/test_sensor.py new file mode 100644 index 0000000000000..44b181b1ec44e --- /dev/null +++ b/tests/components/nws/test_sensor.py @@ -0,0 +1,95 @@ +"""Sensors for National Weather Service (NWS).""" +import pytest + +from homeassistant.components.nws.const import ( + ATTR_LABEL, + ATTRIBUTION, + DOMAIN, + SENSOR_TYPES, +) +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.const import ATTR_ATTRIBUTION, STATE_UNKNOWN +from homeassistant.util import slugify +from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM + +from tests.common import MockConfigEntry +from tests.components.nws.const import ( + EXPECTED_FORECAST_IMPERIAL, + EXPECTED_FORECAST_METRIC, + NONE_OBSERVATION, + NWS_CONFIG, + SENSOR_EXPECTED_OBSERVATION_IMPERIAL, + SENSOR_EXPECTED_OBSERVATION_METRIC, +) + + +@pytest.mark.parametrize( + "units,result_observation,result_forecast", + [ + ( + IMPERIAL_SYSTEM, + SENSOR_EXPECTED_OBSERVATION_IMPERIAL, + EXPECTED_FORECAST_IMPERIAL, + ), + (METRIC_SYSTEM, SENSOR_EXPECTED_OBSERVATION_METRIC, EXPECTED_FORECAST_METRIC), + ], +) +async def test_imperial_metric( + hass, units, result_observation, result_forecast, mock_simple_nws, no_weather +): + """Test with imperial and metric units.""" + registry = await hass.helpers.entity_registry.async_get_registry() + + for sensor_name, sensor_data in SENSOR_TYPES.items(): + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + f"35_-75_{sensor_name}", + suggested_object_id=f"abc_{sensor_data[ATTR_LABEL]}", + disabled_by=None, + ) + + hass.config.units = units + entry = MockConfigEntry( + domain=DOMAIN, + data=NWS_CONFIG, + ) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + for sensor_name, sensor_data in SENSOR_TYPES.items(): + state = hass.states.get(f"sensor.abc_{slugify(sensor_data[ATTR_LABEL])}") + assert state + assert state.state == result_observation[sensor_name] + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + + +async def test_none_values(hass, mock_simple_nws, no_weather): + """Test with no values.""" + instance = mock_simple_nws.return_value + instance.observation = NONE_OBSERVATION + + registry = await hass.helpers.entity_registry.async_get_registry() + + for sensor_name, sensor_data in SENSOR_TYPES.items(): + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + f"35_-75_{sensor_name}", + suggested_object_id=f"abc_{sensor_data[ATTR_LABEL]}", + disabled_by=None, + ) + + entry = MockConfigEntry( + domain=DOMAIN, + data=NWS_CONFIG, + ) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + for sensor_name, sensor_data in SENSOR_TYPES.items(): + state = hass.states.get(f"sensor.abc_{slugify(sensor_data[ATTR_LABEL])}") + assert state + assert state.state == STATE_UNKNOWN diff --git a/tests/components/nws/test_weather.py b/tests/components/nws/test_weather.py index 1679e489ab8ac..78ab7eb4ac58e 100644 --- a/tests/components/nws/test_weather.py +++ b/tests/components/nws/test_weather.py @@ -21,23 +21,27 @@ from tests.components.nws.const import ( EXPECTED_FORECAST_IMPERIAL, EXPECTED_FORECAST_METRIC, - EXPECTED_OBSERVATION_IMPERIAL, - EXPECTED_OBSERVATION_METRIC, NONE_FORECAST, NONE_OBSERVATION, NWS_CONFIG, + WEATHER_EXPECTED_OBSERVATION_IMPERIAL, + WEATHER_EXPECTED_OBSERVATION_METRIC, ) @pytest.mark.parametrize( "units,result_observation,result_forecast", [ - (IMPERIAL_SYSTEM, EXPECTED_OBSERVATION_IMPERIAL, EXPECTED_FORECAST_IMPERIAL), - (METRIC_SYSTEM, EXPECTED_OBSERVATION_METRIC, EXPECTED_FORECAST_METRIC), + ( + IMPERIAL_SYSTEM, + WEATHER_EXPECTED_OBSERVATION_IMPERIAL, + EXPECTED_FORECAST_IMPERIAL, + ), + (METRIC_SYSTEM, WEATHER_EXPECTED_OBSERVATION_METRIC, EXPECTED_FORECAST_METRIC), ], ) async def test_imperial_metric( - hass, units, result_observation, result_forecast, mock_simple_nws + hass, units, result_observation, result_forecast, mock_simple_nws, no_sensor ): """Test with imperial and metric units.""" # enable the hourly entity @@ -86,7 +90,7 @@ async def test_imperial_metric( assert forecast[0].get(key) == value -async def test_none_values(hass, mock_simple_nws): +async def test_none_values(hass, mock_simple_nws, no_sensor): """Test with none values in observation and forecast dicts.""" instance = mock_simple_nws.return_value instance.observation = NONE_OBSERVATION @@ -103,7 +107,7 @@ async def test_none_values(hass, mock_simple_nws): state = hass.states.get("weather.abc_daynight") assert state.state == STATE_UNKNOWN data = state.attributes - for key in EXPECTED_OBSERVATION_IMPERIAL: + for key in WEATHER_EXPECTED_OBSERVATION_IMPERIAL: assert data.get(key) is None forecast = data.get(ATTR_FORECAST) @@ -111,7 +115,7 @@ async def test_none_values(hass, mock_simple_nws): assert forecast[0].get(key) is None -async def test_none(hass, mock_simple_nws): +async def test_none(hass, mock_simple_nws, no_sensor): """Test with None as observation and forecast.""" instance = mock_simple_nws.return_value instance.observation = None @@ -130,14 +134,14 @@ async def test_none(hass, mock_simple_nws): assert state.state == STATE_UNKNOWN data = state.attributes - for key in EXPECTED_OBSERVATION_IMPERIAL: + for key in WEATHER_EXPECTED_OBSERVATION_IMPERIAL: assert data.get(key) is None forecast = data.get(ATTR_FORECAST) assert forecast is None -async def test_error_station(hass, mock_simple_nws): +async def test_error_station(hass, mock_simple_nws, no_sensor): """Test error in setting station.""" instance = mock_simple_nws.return_value @@ -155,7 +159,7 @@ async def test_error_station(hass, mock_simple_nws): assert hass.states.get("weather.abc_daynight") is None -async def test_entity_refresh(hass, mock_simple_nws): +async def test_entity_refresh(hass, mock_simple_nws, no_sensor): """Test manual refresh.""" instance = mock_simple_nws.return_value @@ -184,7 +188,7 @@ async def test_entity_refresh(hass, mock_simple_nws): instance.update_forecast_hourly.assert_called_once() -async def test_error_observation(hass, mock_simple_nws): +async def test_error_observation(hass, mock_simple_nws, no_sensor): """Test error during update observation.""" utc_time = dt_util.utcnow() with patch("homeassistant.components.nws.utcnow") as mock_utc, patch( @@ -248,7 +252,7 @@ def increment_time(time): assert state.state == STATE_UNAVAILABLE -async def test_error_forecast(hass, mock_simple_nws): +async def test_error_forecast(hass, mock_simple_nws, no_sensor): """Test error during update forecast.""" instance = mock_simple_nws.return_value instance.update_forecast.side_effect = aiohttp.ClientError @@ -279,7 +283,7 @@ async def test_error_forecast(hass, mock_simple_nws): assert state.state == ATTR_CONDITION_SUNNY -async def test_error_forecast_hourly(hass, mock_simple_nws): +async def test_error_forecast_hourly(hass, mock_simple_nws, no_sensor): """Test error during update forecast hourly.""" instance = mock_simple_nws.return_value instance.update_forecast_hourly.side_effect = aiohttp.ClientError @@ -320,7 +324,7 @@ async def test_error_forecast_hourly(hass, mock_simple_nws): assert state.state == ATTR_CONDITION_SUNNY -async def test_forecast_hourly_disable_enable(hass, mock_simple_nws): +async def test_forecast_hourly_disable_enable(hass, mock_simple_nws, no_sensor): """Test error during update forecast hourly.""" entry = MockConfigEntry( domain=nws.DOMAIN, From 125161df6bad66152c53d2af47f90f005c62d77c Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Thu, 1 Apr 2021 11:30:52 -0700 Subject: [PATCH 0014/1317] Only raise integrationnotfound for dependencies (#48241) Co-authored-by: J. Nick Koston Co-authored-by: Paulus Schoutsen --- homeassistant/requirements.py | 13 ++++++-- tests/common.py | 9 +++-- tests/test_requirements.py | 63 +++++++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 4 deletions(-) diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py index f073fd13df8d9..aaad5c1f25143 100644 --- a/homeassistant/requirements.py +++ b/homeassistant/requirements.py @@ -97,12 +97,21 @@ async def async_get_integration_with_requirements( deps_to_check.append(check_domain) if deps_to_check: - await asyncio.gather( + results = await asyncio.gather( *[ async_get_integration_with_requirements(hass, dep, done) for dep in deps_to_check - ] + ], + return_exceptions=True, ) + for result in results: + if not isinstance(result, BaseException): + continue + if not isinstance(result, IntegrationNotFound) or not ( + not integration.is_built_in + and result.domain in integration.after_dependencies + ): + raise result cache[domain] = integration event.set() diff --git a/tests/common.py b/tests/common.py index 32d2742f4d8f6..cc971ca4f13ff 100644 --- a/tests/common.py +++ b/tests/common.py @@ -1046,10 +1046,15 @@ async def get_system_health_info(hass, domain): return await hass.data["system_health"][domain].info_callback(hass) -def mock_integration(hass, module): +def mock_integration(hass, module, built_in=True): """Mock an integration.""" integration = loader.Integration( - hass, f"homeassistant.components.{module.DOMAIN}", None, module.mock_manifest() + hass, + f"{loader.PACKAGE_BUILTIN}.{module.DOMAIN}" + if built_in + else f"{loader.PACKAGE_CUSTOM_COMPONENTS}.{module.DOMAIN}", + None, + module.mock_manifest(), ) def mock_import_platform(platform_name): diff --git a/tests/test_requirements.py b/tests/test_requirements.py index 2c5b529467d2f..acc83afeec29a 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -139,6 +139,69 @@ async def test_get_integration_with_requirements(hass): ] +async def test_get_integration_with_missing_dependencies(hass): + """Check getting an integration with missing dependencies.""" + hass.config.skip_pip = False + mock_integration( + hass, + MockModule("test_component_after_dep"), + ) + mock_integration( + hass, + MockModule( + "test_component", + dependencies=["test_component_dep"], + partial_manifest={"after_dependencies": ["test_component_after_dep"]}, + ), + ) + mock_integration( + hass, + MockModule( + "test_custom_component", + dependencies=["test_component_dep"], + partial_manifest={"after_dependencies": ["test_component_after_dep"]}, + ), + built_in=False, + ) + with pytest.raises(loader.IntegrationNotFound): + await async_get_integration_with_requirements(hass, "test_component") + with pytest.raises(loader.IntegrationNotFound): + await async_get_integration_with_requirements(hass, "test_custom_component") + + +async def test_get_built_in_integration_with_missing_after_dependencies(hass): + """Check getting a built_in integration with missing after_dependencies results in exception.""" + hass.config.skip_pip = False + mock_integration( + hass, + MockModule( + "test_component", + partial_manifest={"after_dependencies": ["test_component_after_dep"]}, + ), + built_in=True, + ) + with pytest.raises(loader.IntegrationNotFound): + await async_get_integration_with_requirements(hass, "test_component") + + +async def test_get_custom_integration_with_missing_after_dependencies(hass): + """Check getting a custom integration with missing after_dependencies.""" + hass.config.skip_pip = False + mock_integration( + hass, + MockModule( + "test_custom_component", + partial_manifest={"after_dependencies": ["test_component_after_dep"]}, + ), + built_in=False, + ) + integration = await async_get_integration_with_requirements( + hass, "test_custom_component" + ) + assert integration + assert integration.domain == "test_custom_component" + + async def test_install_with_wheels_index(hass): """Test an install attempt with wheels index URL.""" hass.config.skip_pip = False From c9cd6b0fbb736c06d2c2cd7a71bdb2b3cc83006b Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 1 Apr 2021 20:34:01 +0200 Subject: [PATCH 0015/1317] Clean lazytox script (#48583) --- script/lazytox.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/script/lazytox.py b/script/lazytox.py index 5a8837b8154dc..1f2f4cf02b021 100755 --- a/script/lazytox.py +++ b/script/lazytox.py @@ -32,13 +32,14 @@ def printc(the_color, *args): return try: print(escape_codes[the_color] + msg + escape_codes["reset"]) - except KeyError: + except KeyError as err: print(msg) - raise ValueError(f"Invalid color {the_color}") + raise ValueError(f"Invalid color {the_color}") from err def validate_requirements_ok(): """Validate requirements, returns True of ok.""" + # pylint: disable=import-error,import-outside-toplevel from gen_requirements_all import main as req_main return req_main(True) == 0 @@ -67,7 +68,6 @@ async def async_exec(*args, display=False): printc("cyan", *argsp) try: kwargs = { - "loop": LOOP, "stdout": asyncio.subprocess.PIPE, "stderr": asyncio.subprocess.STDOUT, } @@ -232,15 +232,7 @@ async def main(): if __name__ == "__main__": - LOOP = ( - asyncio.ProactorEventLoop() - if sys.platform == "win32" - else asyncio.get_event_loop() - ) - try: - LOOP.run_until_complete(main()) + asyncio.run(main()) except (FileNotFoundError, KeyboardInterrupt): pass - finally: - LOOP.close() From 4e3c12883eabf2f3e2c92622c55f2c098c4e3e53 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 1 Apr 2021 22:10:01 +0200 Subject: [PATCH 0016/1317] Allow templatable service target to support scripts (#48600) --- homeassistant/helpers/config_validation.py | 2 +- homeassistant/helpers/service.py | 11 +++++++--- tests/helpers/test_service.py | 24 ++++++++++++++++++++++ 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 7f2f1550cfe7a..9b56bb068655c 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -916,7 +916,7 @@ def script_action(value: Any) -> dict: vol.Optional("data"): vol.All(dict, template_complex), vol.Optional("data_template"): vol.All(dict, template_complex), vol.Optional(CONF_ENTITY_ID): comp_entity_ids, - vol.Optional(CONF_TARGET): ENTITY_SERVICE_FIELDS, + vol.Optional(CONF_TARGET): vol.Any(ENTITY_SERVICE_FIELDS, dynamic_template), } ), has_at_least_one_key(CONF_SERVICE, CONF_SERVICE_TEMPLATE), diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 01992d43221a3..4e484c6aaabc0 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -204,10 +204,15 @@ def async_prepare_call_from_config( target = {} if CONF_TARGET in config: - conf = config.get(CONF_TARGET) + conf = config[CONF_TARGET] try: - template.attach(hass, conf) - target.update(template.render_complex(conf, variables)) + if isinstance(conf, template.Template): + conf.hass = hass + target.update(conf.async_render(variables)) + else: + template.attach(hass, conf) + target.update(template.render_complex(conf, variables)) + if CONF_ENTITY_ID in target: target[CONF_ENTITY_ID] = cv.comp_entity_ids(target[CONF_ENTITY_ID]) except TemplateError as ex: diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index d168c8b9cfc1e..7538c0f6f2c9a 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -213,6 +213,30 @@ def test_service_call(self): "entity_id": ["light.static", "light.dynamic"], } + config = { + "service": "{{ 'test_domain.test_service' }}", + "target": "{{ var_target }}", + } + + service.call_from_config( + self.hass, + config, + variables={ + "var_target": { + "entity_id": "light.static", + "area_id": ["area-42", "area-51"], + }, + }, + ) + + service.call_from_config(self.hass, config) + self.hass.block_till_done() + + assert dict(self.calls[2].data) == { + "area_id": ["area-42", "area-51"], + "entity_id": ["light.static"], + } + def test_service_template_service_call(self): """Test legacy service_template call with templating.""" config = { From 528095b9b60042bd6c65b86baf8858e42ad5c18b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 1 Apr 2021 22:32:49 +0200 Subject: [PATCH 0017/1317] Upgrade numpy to 1.20.2 (#48597) --- homeassistant/components/iqvia/manifest.json | 2 +- homeassistant/components/opencv/manifest.json | 2 +- homeassistant/components/tensorflow/manifest.json | 2 +- homeassistant/components/trend/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json index 6445b4ad91f84..145972e287555 100644 --- a/homeassistant/components/iqvia/manifest.json +++ b/homeassistant/components/iqvia/manifest.json @@ -3,6 +3,6 @@ "name": "IQVIA", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/iqvia", - "requirements": ["numpy==1.19.2", "pyiqvia==0.3.1"], + "requirements": ["numpy==1.20.2", "pyiqvia==0.3.1"], "codeowners": ["@bachya"] } diff --git a/homeassistant/components/opencv/manifest.json b/homeassistant/components/opencv/manifest.json index 24b84e305e7c2..a0294a7aa49ce 100644 --- a/homeassistant/components/opencv/manifest.json +++ b/homeassistant/components/opencv/manifest.json @@ -2,6 +2,6 @@ "domain": "opencv", "name": "OpenCV", "documentation": "https://www.home-assistant.io/integrations/opencv", - "requirements": ["numpy==1.19.2", "opencv-python-headless==4.3.0.36"], + "requirements": ["numpy==1.20.2", "opencv-python-headless==4.3.0.36"], "codeowners": [] } diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index 49ee22176a72e..84619680490c5 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -6,7 +6,7 @@ "tensorflow==2.3.0", "tf-models-official==2.3.0", "pycocotools==2.0.1", - "numpy==1.19.2", + "numpy==1.20.2", "pillow==8.1.2" ], "codeowners": [] diff --git a/homeassistant/components/trend/manifest.json b/homeassistant/components/trend/manifest.json index 88e32ce4a46f8..2bb3719fe9502 100644 --- a/homeassistant/components/trend/manifest.json +++ b/homeassistant/components/trend/manifest.json @@ -2,7 +2,7 @@ "domain": "trend", "name": "Trend", "documentation": "https://www.home-assistant.io/integrations/trend", - "requirements": ["numpy==1.19.2"], + "requirements": ["numpy==1.20.2"], "codeowners": [], "quality_scale": "internal" } diff --git a/requirements_all.txt b/requirements_all.txt index f7c309bfa2923..4a112c353629e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1019,7 +1019,7 @@ numato-gpio==0.10.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.19.2 +numpy==1.20.2 # homeassistant.components.oasa_telematics oasatelematics==0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 34ca346d86bb6..6281717efa902 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -534,7 +534,7 @@ numato-gpio==0.10.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.19.2 +numpy==1.20.2 # homeassistant.components.google oauth2client==4.0.0 From 76d0f93ec17904abde4428677460322b79332a90 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 1 Apr 2021 22:34:47 +0200 Subject: [PATCH 0018/1317] Include blueprint input in automation trace (#48575) --- .../components/automation/__init__.py | 11 ++- homeassistant/components/automation/trace.py | 7 +- homeassistant/components/script/trace.py | 2 +- homeassistant/components/trace/__init__.py | 3 + tests/components/trace/test_websocket_api.py | 70 +++++++++++++++++++ 5 files changed, 88 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 6caa53dff7928..36b7f1688f856 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -273,6 +273,7 @@ def __init__( variables, trigger_variables, raw_config, + blueprint_inputs, ): """Initialize an automation entity.""" self._id = automation_id @@ -290,6 +291,7 @@ def __init__( self._variables: ScriptVariables = variables self._trigger_variables: ScriptVariables = trigger_variables self._raw_config = raw_config + self._blueprint_inputs = blueprint_inputs @property def name(self): @@ -437,7 +439,11 @@ async def async_trigger(self, run_variables, context=None, skip_condition=False) trigger_context = Context(parent_id=parent_id) with trace_automation( - self.hass, self.unique_id, self._raw_config, trigger_context + self.hass, + self.unique_id, + self._raw_config, + self._blueprint_inputs, + trigger_context, ) as automation_trace: if self._variables: try: @@ -603,10 +609,12 @@ async def _async_process_config( ] for list_no, config_block in enumerate(conf): + raw_blueprint_inputs = None raw_config = None if isinstance(config_block, blueprint.BlueprintInputs): # type: ignore blueprints_used = True blueprint_inputs = config_block + raw_blueprint_inputs = blueprint_inputs.config_with_inputs try: raw_config = blueprint_inputs.async_substitute() @@ -675,6 +683,7 @@ async def _async_process_config( variables, config_block.get(CONF_TRIGGER_VARIABLES), raw_config, + raw_blueprint_inputs, ) entities.append(entity) diff --git a/homeassistant/components/automation/trace.py b/homeassistant/components/automation/trace.py index 0b335f7d87f8a..cfdbe02056b92 100644 --- a/homeassistant/components/automation/trace.py +++ b/homeassistant/components/automation/trace.py @@ -18,11 +18,12 @@ def __init__( self, item_id: str, config: dict[str, Any], + blueprint_inputs: dict[str, Any], context: Context, ): """Container for automation trace.""" key = ("automation", item_id) - super().__init__(key, config, context) + super().__init__(key, config, blueprint_inputs, context) self._trigger_description: str | None = None def set_trigger_description(self, trigger: str) -> None: @@ -37,9 +38,9 @@ def as_short_dict(self) -> dict[str, Any]: @contextmanager -def trace_automation(hass, automation_id, config, context): +def trace_automation(hass, automation_id, config, blueprint_inputs, context): """Trace action execution of automation with automation_id.""" - trace = AutomationTrace(automation_id, config, context) + trace = AutomationTrace(automation_id, config, blueprint_inputs, context) async_store_trace(hass, trace) try: diff --git a/homeassistant/components/script/trace.py b/homeassistant/components/script/trace.py index 1a7cc01e08471..a8053feaa1e4a 100644 --- a/homeassistant/components/script/trace.py +++ b/homeassistant/components/script/trace.py @@ -19,7 +19,7 @@ def __init__( ): """Container for automation trace.""" key = ("script", item_id) - super().__init__(key, config, context) + super().__init__(key, config, None, context) @contextmanager diff --git a/homeassistant/components/trace/__init__.py b/homeassistant/components/trace/__init__.py index c17cbf8671579..eca22a56da84a 100644 --- a/homeassistant/components/trace/__init__.py +++ b/homeassistant/components/trace/__init__.py @@ -48,11 +48,13 @@ def __init__( self, key: tuple[str, str], config: dict[str, Any], + blueprint_inputs: dict[str, Any], context: Context, ): """Container for script trace.""" self._trace: dict[str, Deque[TraceElement]] | None = None self._config: dict[str, Any] = config + self._blueprint_inputs: dict[str, Any] = blueprint_inputs self.context: Context = context self._error: Exception | None = None self._state: str = "running" @@ -93,6 +95,7 @@ def as_dict(self) -> dict[str, Any]: { "trace": traces, "config": self._config, + "blueprint_inputs": self._blueprint_inputs, "context": self.context, } ) diff --git a/tests/components/trace/test_websocket_api.py b/tests/components/trace/test_websocket_api.py index 8e481dd34b94a..0b7b78b3f1a56 100644 --- a/tests/components/trace/test_websocket_api.py +++ b/tests/components/trace/test_websocket_api.py @@ -169,6 +169,7 @@ def next_id(): assert trace["trace"][f"{prefix}/0"][0]["error"] assert trace["trace"][f"{prefix}/0"][0]["result"] == sun_action _assert_raw_config(domain, sun_config, trace) + assert trace["blueprint_inputs"] is None assert trace["context"] assert trace["error"] == "Unable to find service test.automation" assert trace["state"] == "stopped" @@ -210,6 +211,7 @@ def next_id(): assert "error" not in trace["trace"][f"{prefix}/0"][0] assert trace["trace"][f"{prefix}/0"][0]["result"] == moon_action _assert_raw_config(domain, moon_config, trace) + assert trace["blueprint_inputs"] is None assert trace["context"] assert "error" not in trace assert trace["state"] == "stopped" @@ -1162,3 +1164,71 @@ def _handle_event(_): trace = _find_traces(response["result"], "script", "script1")[1] assert trace["state"] == "stopped" assert trace["script_execution"] == "finished" + + +async def test_trace_blueprint_automation(hass, hass_ws_client): + """Test trace of blueprint automation.""" + id = 1 + + def next_id(): + nonlocal id + id += 1 + return id + + domain = "automation" + sun_config = { + "id": "sun", + "use_blueprint": { + "path": "test_event_service.yaml", + "input": { + "trigger_event": "blueprint_event", + "service_to_call": "test.automation", + }, + }, + } + sun_action = { + "limit": 10, + "params": { + "domain": "test", + "service": "automation", + "service_data": {}, + "target": {"entity_id": ["light.kitchen"]}, + }, + "running_script": False, + } + assert await async_setup_component(hass, "automation", {"automation": sun_config}) + client = await hass_ws_client() + hass.bus.async_fire("blueprint_event") + await hass.async_block_till_done() + + # List traces + await client.send_json({"id": next_id(), "type": "trace/list", "domain": domain}) + response = await client.receive_json() + assert response["success"] + run_id = _find_run_id(response["result"], domain, "sun") + + # Get trace + await client.send_json( + { + "id": next_id(), + "type": "trace/get", + "domain": domain, + "item_id": "sun", + "run_id": run_id, + } + ) + response = await client.receive_json() + assert response["success"] + trace = response["result"] + assert set(trace["trace"]) == {"trigger/0", "action/0"} + assert len(trace["trace"]["action/0"]) == 1 + assert trace["trace"]["action/0"][0]["error"] + assert trace["trace"]["action/0"][0]["result"] == sun_action + assert trace["config"]["id"] == "sun" + assert trace["blueprint_inputs"] == sun_config + assert trace["context"] + assert trace["error"] == "Unable to find service test.automation" + assert trace["state"] == "stopped" + assert trace["script_execution"] == "error" + assert trace["item_id"] == "sun" + assert trace.get("trigger", UNDEFINED) == "event 'blueprint_event'" From da54b9237b4190b92f5096d895111586f2364ef7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 1 Apr 2021 23:59:26 +0200 Subject: [PATCH 0019/1317] Typing improvements for SolarEdge (#48596) --- .../components/solaredge/__init__.py | 12 +- .../components/solaredge/config_flow.py | 26 ++-- homeassistant/components/solaredge/sensor.py | 118 +++++++++++------- .../components/solaredge/test_config_flow.py | 11 +- 4 files changed, 99 insertions(+), 68 deletions(-) diff --git a/homeassistant/components/solaredge/__init__.py b/homeassistant/components/solaredge/__init__.py index e054abfe8aee2..f01226bcb458b 100644 --- a/homeassistant/components/solaredge/__init__.py +++ b/homeassistant/components/solaredge/__init__.py @@ -1,10 +1,14 @@ -"""The solaredge component.""" +"""The solaredge integration.""" +from __future__ import annotations + +from typing import Any + import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_NAME +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import HomeAssistantType from .const import CONF_SITE_ID, DEFAULT_NAME, DOMAIN @@ -25,7 +29,7 @@ ) -async def async_setup(hass, config): +async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool: """Platform setup, do nothing.""" if DOMAIN not in config: return True @@ -38,7 +42,7 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Load the saved entities.""" hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, "sensor") diff --git a/homeassistant/components/solaredge/config_flow.py b/homeassistant/components/solaredge/config_flow.py index 49c265b4221b2..eecd11d7b128f 100644 --- a/homeassistant/components/solaredge/config_flow.py +++ b/homeassistant/components/solaredge/config_flow.py @@ -1,4 +1,8 @@ """Config flow for the SolarEdge platform.""" +from __future__ import annotations + +from typing import Any + from requests.exceptions import ConnectTimeout, HTTPError import solaredge import voluptuous as vol @@ -30,13 +34,11 @@ def __init__(self) -> None: """Initialize the config flow.""" self._errors = {} - def _site_in_configuration_exists(self, site_id) -> bool: + def _site_in_configuration_exists(self, site_id: str) -> bool: """Return True if site_id exists in configuration.""" - if site_id in solaredge_entries(self.hass): - return True - return False + return site_id in solaredge_entries(self.hass) - def _check_site(self, site_id, api_key) -> bool: + def _check_site(self, site_id: str, api_key: str) -> bool: """Check if we can connect to the soleredge api service.""" api = solaredge.Solaredge(api_key) try: @@ -52,7 +54,9 @@ def _check_site(self, site_id, api_key) -> bool: return False return True - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> dict[str, Any]: """Step when user initializes a integration.""" self._errors = {} if user_input is not None: @@ -71,11 +75,7 @@ async def async_step_user(self, user_input=None): ) else: - user_input = {} - user_input[CONF_NAME] = DEFAULT_NAME - user_input[CONF_SITE_ID] = "" - user_input[CONF_API_KEY] = "" - + user_input = {CONF_NAME: DEFAULT_NAME, CONF_SITE_ID: "", CONF_API_KEY: ""} return self.async_show_form( step_id="user", data_schema=vol.Schema( @@ -90,7 +90,9 @@ async def async_step_user(self, user_input=None): errors=self._errors, ) - async def async_step_import(self, user_input=None): + async def async_step_import( + self, user_input: dict[str, Any] | None = None + ) -> dict[str, Any]: """Import a config entry.""" if self._site_in_configuration_exists(user_input[CONF_SITE_ID]): return self.async_abort(reason="already_configured") diff --git a/homeassistant/components/solaredge/sensor.py b/homeassistant/components/solaredge/sensor.py index 7835fa9aee403..b93a84a77fb61 100644 --- a/homeassistant/components/solaredge/sensor.py +++ b/homeassistant/components/solaredge/sensor.py @@ -1,16 +1,21 @@ """Support for SolarEdge Monitoring API.""" +from __future__ import annotations + from abc import abstractmethod -from datetime import date, datetime +from datetime import date, datetime, timedelta import logging +from typing import Any, Callable, Iterable from requests.exceptions import ConnectTimeout, HTTPError -import solaredge +from solaredge import Solaredge from stringcase import snakecase from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, DEVICE_CLASS_BATTERY, DEVICE_CLASS_POWER -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.entity import Entity from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -30,10 +35,14 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass, entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: Callable[[Iterable[Entity]], None], +) -> None: """Add an solarEdge entry.""" # Add the needed sensors to hass - api = solaredge.Solaredge(entry.data[CONF_API_KEY]) + api = Solaredge(entry.data[CONF_API_KEY]) # Check if api can be reached and site is active try: @@ -69,7 +78,9 @@ async def async_setup_entry(hass, entry, async_add_entities): class SolarEdgeSensorFactory: """Factory which creates sensors based on the sensor_key.""" - def __init__(self, hass, platform_name, site_id, api): + def __init__( + self, hass: HomeAssistant, platform_name: str, site_id: str, api: Solaredge + ) -> None: """Initialize the factory.""" self.platform_name = platform_name @@ -81,7 +92,12 @@ def __init__(self, hass, platform_name, site_id, api): self.all_services = (details, overview, inventory, flow, energy) - self.services = {"site_details": (SolarEdgeDetailsSensor, details)} + self.services: dict[ + str, + tuple[ + type[SolarEdgeSensor | SolarEdgeOverviewSensor], SolarEdgeDataService + ], + ] = {"site_details": (SolarEdgeDetailsSensor, details)} for key in [ "lifetime_energy", @@ -110,7 +126,7 @@ def __init__(self, hass, platform_name, site_id, api): ]: self.services[key] = (SolarEdgeEnergyDetailsSensor, energy) - def create_sensor(self, sensor_key): + def create_sensor(self, sensor_key: str) -> SolarEdgeSensor: """Create and return a sensor based on the sensor_key.""" sensor_class, service = self.services[sensor_key] @@ -120,7 +136,9 @@ def create_sensor(self, sensor_key): class SolarEdgeSensor(CoordinatorEntity, SensorEntity): """Abstract class for a solaredge sensor.""" - def __init__(self, platform_name, sensor_key, data_service): + def __init__( + self, platform_name: str, sensor_key: str, data_service: SolarEdgeDataService + ) -> None: """Initialize the sensor.""" super().__init__(data_service.coordinator) self.platform_name = platform_name @@ -128,17 +146,17 @@ def __init__(self, platform_name, sensor_key, data_service): self.data_service = data_service @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str | None: """Return the unit of measurement.""" return SENSOR_TYPES[self.sensor_key][2] @property - def name(self): + def name(self) -> str: """Return the name.""" return "{} ({})".format(self.platform_name, SENSOR_TYPES[self.sensor_key][1]) @property - def icon(self): + def icon(self) -> str | None: """Return the sensor icon.""" return SENSOR_TYPES[self.sensor_key][3] @@ -146,14 +164,16 @@ def icon(self): class SolarEdgeOverviewSensor(SolarEdgeSensor): """Representation of an SolarEdge Monitoring API overview sensor.""" - def __init__(self, platform_name, sensor_key, data_service): + def __init__( + self, platform_name: str, sensor_key: str, data_service: SolarEdgeDataService + ) -> None: """Initialize the overview sensor.""" super().__init__(platform_name, sensor_key, data_service) self._json_key = SENSOR_TYPES[self.sensor_key][0] @property - def state(self): + def state(self) -> str | None: """Return the state of the sensor.""" return self.data_service.data.get(self._json_key) @@ -162,12 +182,12 @@ class SolarEdgeDetailsSensor(SolarEdgeSensor): """Representation of an SolarEdge Monitoring API details sensor.""" @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" return self.data_service.attributes @property - def state(self): + def state(self) -> str | None: """Return the state of the sensor.""" return self.data_service.data @@ -182,12 +202,12 @@ def __init__(self, platform_name, sensor_key, data_service): self._json_key = SENSOR_TYPES[self.sensor_key][0] @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" return self.data_service.attributes.get(self._json_key) @property - def state(self): + def state(self) -> str | None: """Return the state of the sensor.""" return self.data_service.data.get(self._json_key) @@ -202,17 +222,17 @@ def __init__(self, platform_name, sensor_key, data_service): self._json_key = SENSOR_TYPES[self.sensor_key][0] @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" return self.data_service.attributes.get(self._json_key) @property - def state(self): + def state(self) -> str | None: """Return the state of the sensor.""" return self.data_service.data.get(self._json_key) @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str | None: """Return the unit of measurement.""" return self.data_service.unit @@ -220,29 +240,31 @@ def unit_of_measurement(self): class SolarEdgePowerFlowSensor(SolarEdgeSensor): """Representation of an SolarEdge Monitoring API power flow sensor.""" - def __init__(self, platform_name, sensor_key, data_service): + def __init__( + self, platform_name: str, sensor_key: str, data_service: SolarEdgeDataService + ) -> None: """Initialize the power flow sensor.""" super().__init__(platform_name, sensor_key, data_service) self._json_key = SENSOR_TYPES[self.sensor_key][0] @property - def device_class(self): + def device_class(self) -> str: """Device Class.""" return DEVICE_CLASS_POWER @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" return self.data_service.attributes.get(self._json_key) @property - def state(self): + def state(self) -> str | None: """Return the state of the sensor.""" return self.data_service.data.get(self._json_key) @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str | None: """Return the unit of measurement.""" return self.data_service.unit @@ -250,19 +272,21 @@ def unit_of_measurement(self): class SolarEdgeStorageLevelSensor(SolarEdgeSensor): """Representation of an SolarEdge Monitoring API storage level sensor.""" - def __init__(self, platform_name, sensor_key, data_service): + def __init__( + self, platform_name: str, sensor_key: str, data_service: SolarEdgeDataService + ) -> None: """Initialize the storage level sensor.""" super().__init__(platform_name, sensor_key, data_service) self._json_key = SENSOR_TYPES[self.sensor_key][0] @property - def device_class(self): + def device_class(self) -> str: """Return the device_class of the device.""" return DEVICE_CLASS_BATTERY @property - def state(self): + def state(self) -> str | None: """Return the state of the sensor.""" attr = self.data_service.attributes.get(self._json_key) if attr and "soc" in attr: @@ -273,7 +297,7 @@ def state(self): class SolarEdgeDataService: """Get and update the latest data.""" - def __init__(self, hass, api, site_id): + def __init__(self, hass: HomeAssistant, api: Solaredge, site_id: str) -> None: """Initialize the data object.""" self.api = api self.site_id = site_id @@ -285,7 +309,7 @@ def __init__(self, hass, api, site_id): self.coordinator = None @callback - def async_setup(self): + def async_setup(self) -> None: """Coordinator creation.""" self.coordinator = DataUpdateCoordinator( self.hass, @@ -297,14 +321,14 @@ def async_setup(self): @property @abstractmethod - def update_interval(self): + def update_interval(self) -> timedelta: """Update interval.""" @abstractmethod - def update(self): + def update(self) -> None: """Update data in executor.""" - async def async_update_data(self): + async def async_update_data(self) -> None: """Update data.""" await self.hass.async_add_executor_job(self.update) @@ -313,11 +337,11 @@ class SolarEdgeOverviewDataService(SolarEdgeDataService): """Get and update the latest overview data.""" @property - def update_interval(self): + def update_interval(self) -> timedelta: """Update interval.""" return OVERVIEW_UPDATE_DELAY - def update(self): + def update(self) -> None: """Update the data from the SolarEdge Monitoring API.""" try: data = self.api.get_overview(self.site_id) @@ -342,18 +366,18 @@ def update(self): class SolarEdgeDetailsDataService(SolarEdgeDataService): """Get and update the latest details data.""" - def __init__(self, hass, api, site_id): + def __init__(self, hass: HomeAssistant, api: Solaredge, site_id: str) -> None: """Initialize the details data service.""" super().__init__(hass, api, site_id) self.data = None @property - def update_interval(self): + def update_interval(self) -> timedelta: """Update interval.""" return DETAILS_UPDATE_DELAY - def update(self): + def update(self) -> None: """Update the data from the SolarEdge Monitoring API.""" try: @@ -389,11 +413,11 @@ class SolarEdgeInventoryDataService(SolarEdgeDataService): """Get and update the latest inventory data.""" @property - def update_interval(self): + def update_interval(self) -> timedelta: """Update interval.""" return INVENTORY_UPDATE_DELAY - def update(self): + def update(self) -> None: """Update the data from the SolarEdge Monitoring API.""" try: data = self.api.get_inventory(self.site_id) @@ -414,18 +438,18 @@ def update(self): class SolarEdgeEnergyDetailsService(SolarEdgeDataService): """Get and update the latest power flow data.""" - def __init__(self, hass, api, site_id): + def __init__(self, hass: HomeAssistant, api: Solaredge, site_id: str) -> None: """Initialize the power flow data service.""" super().__init__(hass, api, site_id) self.unit = None @property - def update_interval(self): + def update_interval(self) -> timedelta: """Update interval.""" return ENERGY_DETAILS_DELAY - def update(self): + def update(self) -> None: """Update the data from the SolarEdge Monitoring API.""" try: now = datetime.now() @@ -475,18 +499,18 @@ def update(self): class SolarEdgePowerFlowDataService(SolarEdgeDataService): """Get and update the latest power flow data.""" - def __init__(self, hass, api, site_id): + def __init__(self, hass: HomeAssistant, api: Solaredge, site_id: str) -> None: """Initialize the power flow data service.""" super().__init__(hass, api, site_id) self.unit = None @property - def update_interval(self): + def update_interval(self) -> timedelta: """Update interval.""" return POWER_FLOW_UPDATE_DELAY - def update(self): + def update(self) -> None: """Update the data from the SolarEdge Monitoring API.""" try: data = self.api.get_current_power_flow(self.site_id) diff --git a/tests/components/solaredge/test_config_flow.py b/tests/components/solaredge/test_config_flow.py index 4caae0edcfeae..bb21607feadd6 100644 --- a/tests/components/solaredge/test_config_flow.py +++ b/tests/components/solaredge/test_config_flow.py @@ -8,6 +8,7 @@ from homeassistant.components.solaredge import config_flow from homeassistant.components.solaredge.const import CONF_SITE_ID, DEFAULT_NAME from homeassistant.const import CONF_API_KEY, CONF_NAME +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -25,14 +26,14 @@ def mock_controller(): yield api -def init_config_flow(hass): +def init_config_flow(hass: HomeAssistant) -> config_flow.SolarEdgeConfigFlow: """Init a configuration flow.""" flow = config_flow.SolarEdgeConfigFlow() flow.hass = hass return flow -async def test_user(hass, test_api): +async def test_user(hass: HomeAssistant, test_api: Mock) -> None: """Test user config.""" flow = init_config_flow(hass) @@ -50,7 +51,7 @@ async def test_user(hass, test_api): assert result["data"][CONF_API_KEY] == API_KEY -async def test_import(hass, test_api): +async def test_import(hass: HomeAssistant, test_api: Mock) -> None: """Test import step.""" flow = init_config_flow(hass) @@ -73,7 +74,7 @@ async def test_import(hass, test_api): assert result["data"][CONF_API_KEY] == API_KEY -async def test_abort_if_already_setup(hass, test_api): +async def test_abort_if_already_setup(hass: HomeAssistant, test_api: str) -> None: """Test we abort if the site_id is already setup.""" flow = init_config_flow(hass) MockConfigEntry( @@ -96,7 +97,7 @@ async def test_abort_if_already_setup(hass, test_api): assert result["errors"] == {CONF_SITE_ID: "already_configured"} -async def test_asserts(hass, test_api): +async def test_asserts(hass: HomeAssistant, test_api: Mock) -> None: """Test the _site_in_configuration_exists method.""" flow = init_config_flow(hass) From e76503ddc39ab239de6776cf89499df148038cd5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 2 Apr 2021 00:04:52 +0200 Subject: [PATCH 0020/1317] Remove Geizhals integration (ADR-0004) (#48594) --- .coveragerc | 1 - homeassistant/components/geizhals/__init__.py | 1 - .../components/geizhals/manifest.json | 7 -- homeassistant/components/geizhals/sensor.py | 92 ------------------- requirements_all.txt | 3 - 5 files changed, 104 deletions(-) delete mode 100644 homeassistant/components/geizhals/__init__.py delete mode 100644 homeassistant/components/geizhals/manifest.json delete mode 100644 homeassistant/components/geizhals/sensor.py diff --git a/.coveragerc b/.coveragerc index dcc26036c464a..b55fe3f5a3e7f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -335,7 +335,6 @@ omit = homeassistant/components/garmin_connect/alarm_util.py homeassistant/components/gc100/* homeassistant/components/geniushub/* - homeassistant/components/geizhals/sensor.py homeassistant/components/github/sensor.py homeassistant/components/gitlab_ci/sensor.py homeassistant/components/gitter/sensor.py diff --git a/homeassistant/components/geizhals/__init__.py b/homeassistant/components/geizhals/__init__.py deleted file mode 100644 index 28b1d62307358..0000000000000 --- a/homeassistant/components/geizhals/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The geizhals component.""" diff --git a/homeassistant/components/geizhals/manifest.json b/homeassistant/components/geizhals/manifest.json deleted file mode 100644 index 17b4b5e9df045..0000000000000 --- a/homeassistant/components/geizhals/manifest.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "domain": "geizhals", - "name": "Geizhals", - "documentation": "https://www.home-assistant.io/integrations/geizhals", - "requirements": ["geizhals==0.0.9"], - "codeowners": [] -} diff --git a/homeassistant/components/geizhals/sensor.py b/homeassistant/components/geizhals/sensor.py deleted file mode 100644 index 94d329a417e2c..0000000000000 --- a/homeassistant/components/geizhals/sensor.py +++ /dev/null @@ -1,92 +0,0 @@ -"""Parse prices of a device from geizhals.""" -from datetime import timedelta - -from geizhals import Device, Geizhals -import voluptuous as vol - -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import CONF_DESCRIPTION, CONF_NAME -import homeassistant.helpers.config_validation as cv -from homeassistant.util import Throttle - -CONF_PRODUCT_ID = "product_id" -CONF_LOCALE = "locale" - -ICON = "mdi:currency-usd-circle" - -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=120) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_PRODUCT_ID): cv.positive_int, - vol.Optional(CONF_DESCRIPTION, default="Price"): cv.string, - vol.Optional(CONF_LOCALE, default="DE"): vol.In(["AT", "EU", "DE", "UK", "PL"]), - } -) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Geizwatch sensor.""" - name = config.get(CONF_NAME) - description = config.get(CONF_DESCRIPTION) - product_id = config.get(CONF_PRODUCT_ID) - domain = config.get(CONF_LOCALE) - - add_entities([Geizwatch(name, description, product_id, domain)], True) - - -class Geizwatch(SensorEntity): - """Implementation of Geizwatch.""" - - def __init__(self, name, description, product_id, domain): - """Initialize the sensor.""" - - # internal - self._name = name - self._geizhals = Geizhals(product_id, domain) - self._device = Device() - - # external - self.description = description - self.product_id = product_id - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def icon(self): - """Return the icon for the frontend.""" - return ICON - - @property - def state(self): - """Return the best price of the selected product.""" - if not self._device.prices: - return None - - return self._device.prices[0] - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - while len(self._device.prices) < 4: - self._device.prices.append("None") - attrs = { - "device_name": self._device.name, - "description": self.description, - "unit_of_measurement": self._device.price_currency, - "product_id": self.product_id, - "price1": self._device.prices[0], - "price2": self._device.prices[1], - "price3": self._device.prices[2], - "price4": self._device.prices[3], - } - return attrs - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Get the latest price from geizhals and updates the state.""" - self._device = self._geizhals.parse() diff --git a/requirements_all.txt b/requirements_all.txt index 4a112c353629e..d5cce464db9d2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -628,9 +628,6 @@ gTTS==2.2.2 # homeassistant.components.garmin_connect garminconnect==0.1.19 -# homeassistant.components.geizhals -geizhals==0.0.9 - # homeassistant.components.geniushub geniushub-client==0.6.30 From 09eb74fd9db1a647571c8f97526229d67ed16700 Mon Sep 17 00:00:00 2001 From: FMKaiba Date: Thu, 1 Apr 2021 15:29:08 -0700 Subject: [PATCH 0021/1317] Upgrade Astral to 2.2 (#48573) --- homeassistant/components/moon/sensor.py | 5 +- homeassistant/components/sun/__init__.py | 20 +-- homeassistant/components/tod/binary_sensor.py | 14 --- homeassistant/helpers/sun.py | 45 ++++--- homeassistant/package_constraints.txt | 2 +- requirements.txt | 2 +- setup.py | 2 +- tests/components/sun/test_init.py | 37 +++--- tests/components/sun/test_trigger.py | 116 +++++++++--------- tests/components/tod/test_binary_sensor.py | 3 +- tests/helpers/test_event.py | 37 ++++-- tests/helpers/test_sun.py | 116 +++++++++--------- 12 files changed, 211 insertions(+), 188 deletions(-) diff --git a/homeassistant/components/moon/sensor.py b/homeassistant/components/moon/sensor.py index 4b373469cc6df..6213e218d24d0 100644 --- a/homeassistant/components/moon/sensor.py +++ b/homeassistant/components/moon/sensor.py @@ -1,5 +1,5 @@ """Support for tracking the moon phases.""" -from astral import Astral +from astral import moon import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity @@ -48,7 +48,6 @@ def __init__(self, name): """Initialize the moon sensor.""" self._name = name self._state = None - self._astral = Astral() @property def name(self): @@ -87,4 +86,4 @@ def icon(self): async def async_update(self): """Get the time and updates the states.""" today = dt_util.as_local(dt_util.utcnow()).date() - self._state = self._astral.moon_phase(today) + self._state = moon.phase(today) diff --git a/homeassistant/components/sun/__init__.py b/homeassistant/components/sun/__init__.py index dfe3b15c110a0..489eab6b5be19 100644 --- a/homeassistant/components/sun/__init__.py +++ b/homeassistant/components/sun/__init__.py @@ -92,6 +92,7 @@ def __init__(self, hass): """Initialize the sun.""" self.hass = hass self.location = None + self.elevation = 0.0 self._state = self.next_rising = self.next_setting = None self.next_dawn = self.next_dusk = None self.next_midnight = self.next_noon = None @@ -100,10 +101,11 @@ def __init__(self, hass): self._next_change = None def update_location(_event): - location = get_astral_location(self.hass) + location, elevation = get_astral_location(self.hass) if location == self.location: return self.location = location + self.elevation = elevation self.update_events() update_location(None) @@ -140,7 +142,7 @@ def extra_state_attributes(self): def _check_event(self, utc_point_in_time, sun_event, before): next_utc = get_location_astral_event_next( - self.location, sun_event, utc_point_in_time + self.location, self.elevation, sun_event, utc_point_in_time ) if next_utc < self._next_change: self._next_change = next_utc @@ -169,7 +171,7 @@ def update_events(self, now=None): ) self.location.solar_depression = -10 self._check_event(utc_point_in_time, "dawn", PHASE_SMALL_DAY) - self.next_noon = self._check_event(utc_point_in_time, "solar_noon", None) + self.next_noon = self._check_event(utc_point_in_time, "noon", None) self._check_event(utc_point_in_time, "dusk", PHASE_DAY) self.next_setting = self._check_event( utc_point_in_time, SUN_EVENT_SUNSET, PHASE_SMALL_DAY @@ -180,9 +182,7 @@ def update_events(self, now=None): self._check_event(utc_point_in_time, "dusk", PHASE_NAUTICAL_TWILIGHT) self.location.solar_depression = "astronomical" self._check_event(utc_point_in_time, "dusk", PHASE_ASTRONOMICAL_TWILIGHT) - self.next_midnight = self._check_event( - utc_point_in_time, "solar_midnight", None - ) + self.next_midnight = self._check_event(utc_point_in_time, "midnight", None) self.location.solar_depression = "civil" # if the event was solar midday or midnight, phase will now @@ -190,7 +190,7 @@ def update_events(self, now=None): # even in the day at the poles, so we can't rely on it. # Need to calculate phase if next is noon or midnight if self.phase is None: - elevation = self.location.solar_elevation(self._next_change) + elevation = self.location.solar_elevation(self._next_change, self.elevation) if elevation >= 10: self.phase = PHASE_DAY elif elevation >= 0: @@ -222,9 +222,11 @@ def update_sun_position(self, now=None): """Calculate the position of the sun.""" # Grab current time in case system clock changed since last time we ran. utc_point_in_time = dt_util.utcnow() - self.solar_azimuth = round(self.location.solar_azimuth(utc_point_in_time), 2) + self.solar_azimuth = round( + self.location.solar_azimuth(utc_point_in_time, self.elevation), 2 + ) self.solar_elevation = round( - self.location.solar_elevation(utc_point_in_time), 2 + self.location.solar_elevation(utc_point_in_time, self.elevation), 2 ) _LOGGER.debug( diff --git a/homeassistant/components/tod/binary_sensor.py b/homeassistant/components/tod/binary_sensor.py index 26e0ead680a01..a0fed1f803218 100644 --- a/homeassistant/components/tod/binary_sensor.py +++ b/homeassistant/components/tod/binary_sensor.py @@ -173,20 +173,6 @@ def _calculate_initial_boudary_time(self): self._time_before = before_event_date - # We are calculating the _time_after value assuming that it will happen today - # But that is not always true, e.g. after 23:00, before 12:00 and now is 10:00 - # If _time_before and _time_after are ahead of current_datetime: - # _time_before is set to 12:00 next day - # _time_after is set to 23:00 today - # current_datetime is set to 10:00 today - if ( - self._time_after > self.current_datetime - and self._time_before > self.current_datetime + timedelta(days=1) - ): - # remove one day from _time_before and _time_after - self._time_after -= timedelta(days=1) - self._time_before -= timedelta(days=1) - # Add offset to utc boundaries according to the configuration self._time_after += self._after_offset self._time_before += self._before_offset diff --git a/homeassistant/helpers/sun.py b/homeassistant/helpers/sun.py index b3a37d238f9cd..3c18dcc32784a 100644 --- a/homeassistant/helpers/sun.py +++ b/homeassistant/helpers/sun.py @@ -14,27 +14,32 @@ DATA_LOCATION_CACHE = "astral_location_cache" +ELEVATION_AGNOSTIC_EVENTS = ("noon", "midnight") + @callback @bind_hass -def get_astral_location(hass: HomeAssistant) -> astral.Location: +def get_astral_location( + hass: HomeAssistant, +) -> tuple[astral.location.Location, astral.Elevation]: """Get an astral location for the current Home Assistant configuration.""" - from astral import Location # pylint: disable=import-outside-toplevel + from astral import LocationInfo # pylint: disable=import-outside-toplevel + from astral.location import Location # pylint: disable=import-outside-toplevel latitude = hass.config.latitude longitude = hass.config.longitude timezone = str(hass.config.time_zone) elevation = hass.config.elevation - info = ("", "", latitude, longitude, timezone, elevation) + info = ("", "", timezone, latitude, longitude) # Cache astral locations so they aren't recreated with the same args if DATA_LOCATION_CACHE not in hass.data: hass.data[DATA_LOCATION_CACHE] = {} if info not in hass.data[DATA_LOCATION_CACHE]: - hass.data[DATA_LOCATION_CACHE][info] = Location(info) + hass.data[DATA_LOCATION_CACHE][info] = Location(LocationInfo(*info)) - return hass.data[DATA_LOCATION_CACHE][info] + return hass.data[DATA_LOCATION_CACHE][info], elevation @callback @@ -46,19 +51,21 @@ def get_astral_event_next( offset: datetime.timedelta | None = None, ) -> datetime.datetime: """Calculate the next specified solar event.""" - location = get_astral_location(hass) - return get_location_astral_event_next(location, event, utc_point_in_time, offset) + location, elevation = get_astral_location(hass) + return get_location_astral_event_next( + location, elevation, event, utc_point_in_time, offset + ) @callback def get_location_astral_event_next( - location: astral.Location, + location: astral.location.Location, + elevation: astral.Elevation, event: str, utc_point_in_time: datetime.datetime | None = None, offset: datetime.timedelta | None = None, ) -> datetime.datetime: """Calculate the next specified solar event.""" - from astral import AstralError # pylint: disable=import-outside-toplevel if offset is None: offset = datetime.timedelta() @@ -66,6 +73,10 @@ def get_location_astral_event_next( if utc_point_in_time is None: utc_point_in_time = dt_util.utcnow() + kwargs = {"local": False} + if event not in ELEVATION_AGNOSTIC_EVENTS: + kwargs["observer_elevation"] = elevation + mod = -1 while True: try: @@ -73,13 +84,13 @@ def get_location_astral_event_next( getattr(location, event)( dt_util.as_local(utc_point_in_time).date() + datetime.timedelta(days=mod), - local=False, + **kwargs, ) + offset ) if next_dt > utc_point_in_time: return next_dt - except AstralError: + except ValueError: pass mod += 1 @@ -92,9 +103,7 @@ def get_astral_event_date( date: datetime.date | datetime.datetime | None = None, ) -> datetime.datetime | None: """Calculate the astral event time for the specified date.""" - from astral import AstralError # pylint: disable=import-outside-toplevel - - location = get_astral_location(hass) + location, elevation = get_astral_location(hass) if date is None: date = dt_util.now().date() @@ -102,9 +111,13 @@ def get_astral_event_date( if isinstance(date, datetime.datetime): date = dt_util.as_local(date).date() + kwargs = {"local": False} + if event not in ELEVATION_AGNOSTIC_EVENTS: + kwargs["observer_elevation"] = elevation + try: - return getattr(location, event)(date, local=False) # type: ignore - except AstralError: + return getattr(location, event)(date, **kwargs) # type: ignore + except ValueError: # Event never occurs for specified date. return None diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index edb1b2c9cc7f4..a58a800287a73 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -3,7 +3,7 @@ PyNaCl==1.3.0 aiodiscover==1.3.2 aiohttp==3.7.4.post0 aiohttp_cors==0.7.0 -astral==1.10.1 +astral==2.2 async-upnp-client==0.16.0 async_timeout==3.0.1 attrs==20.3.0 diff --git a/requirements.txt b/requirements.txt index 5f633eaeb6910..a3facbe5ab23f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ # Home Assistant Core aiohttp==3.7.4.post0 -astral==1.10.1 +astral==2.2 async_timeout==3.0.1 attrs==20.3.0 awesomeversion==21.2.3 diff --git a/setup.py b/setup.py index 56e56391489f9..f74a913cb81ca 100755 --- a/setup.py +++ b/setup.py @@ -33,7 +33,7 @@ REQUIRES = [ "aiohttp==3.7.4.post0", - "astral==1.10.1", + "astral==2.2", "async_timeout==3.0.1", "attrs==20.3.0", "awesomeversion==21.2.3", diff --git a/tests/components/sun/test_init.py b/tests/components/sun/test_init.py index 1e95082b3581b..800d3ab82fd4f 100644 --- a/tests/components/sun/test_init.py +++ b/tests/components/sun/test_init.py @@ -22,18 +22,19 @@ async def test_setting_rising(hass, legacy_patchable_time): await hass.async_block_till_done() state = hass.states.get(sun.ENTITY_ID) - from astral import Astral + from astral import LocationInfo + import astral.sun - astral = Astral() utc_today = utc_now.date() - latitude = hass.config.latitude - longitude = hass.config.longitude + location = LocationInfo( + latitude=hass.config.latitude, longitude=hass.config.longitude + ) mod = -1 while True: - next_dawn = astral.dawn_utc( - utc_today + timedelta(days=mod), latitude, longitude + next_dawn = astral.sun.dawn( + location.observer, date=utc_today + timedelta(days=mod) ) if next_dawn > utc_now: break @@ -41,8 +42,8 @@ async def test_setting_rising(hass, legacy_patchable_time): mod = -1 while True: - next_dusk = astral.dusk_utc( - utc_today + timedelta(days=mod), latitude, longitude + next_dusk = astral.sun.dusk( + location.observer, date=utc_today + timedelta(days=mod) ) if next_dusk > utc_now: break @@ -50,8 +51,8 @@ async def test_setting_rising(hass, legacy_patchable_time): mod = -1 while True: - next_midnight = astral.solar_midnight_utc( - utc_today + timedelta(days=mod), longitude + next_midnight = astral.sun.midnight( + location.observer, date=utc_today + timedelta(days=mod) ) if next_midnight > utc_now: break @@ -59,15 +60,17 @@ async def test_setting_rising(hass, legacy_patchable_time): mod = -1 while True: - next_noon = astral.solar_noon_utc(utc_today + timedelta(days=mod), longitude) + next_noon = astral.sun.noon( + location.observer, date=utc_today + timedelta(days=mod) + ) if next_noon > utc_now: break mod += 1 mod = -1 while True: - next_rising = astral.sunrise_utc( - utc_today + timedelta(days=mod), latitude, longitude + next_rising = astral.sun.sunrise( + location.observer, date=utc_today + timedelta(days=mod) ) if next_rising > utc_now: break @@ -75,8 +78,8 @@ async def test_setting_rising(hass, legacy_patchable_time): mod = -1 while True: - next_setting = astral.sunset_utc( - utc_today + timedelta(days=mod), latitude, longitude + next_setting = astral.sun.sunset( + location.observer, date=utc_today + timedelta(days=mod) ) if next_setting > utc_now: break @@ -152,10 +155,10 @@ async def test_norway_in_june(hass): assert dt_util.parse_datetime( state.attributes[sun.STATE_ATTR_NEXT_RISING] - ) == datetime(2016, 7, 25, 23, 23, 39, tzinfo=dt_util.UTC) + ) == datetime(2016, 7, 24, 22, 59, 45, 689645, tzinfo=dt_util.UTC) assert dt_util.parse_datetime( state.attributes[sun.STATE_ATTR_NEXT_SETTING] - ) == datetime(2016, 7, 26, 22, 19, 1, tzinfo=dt_util.UTC) + ) == datetime(2016, 7, 25, 22, 17, 13, 503932, tzinfo=dt_util.UTC) assert state.state == sun.STATE_ABOVE_HORIZON diff --git a/tests/components/sun/test_trigger.py b/tests/components/sun/test_trigger.py index 3ed91d1d896de..54dcef96e28e0 100644 --- a/tests/components/sun/test_trigger.py +++ b/tests/components/sun/test_trigger.py @@ -188,17 +188,17 @@ async def test_if_action_before_sunrise_no_offset(hass, calls): }, ) - # sunrise: 2015-09-16 06:32:43 local, sunset: 2015-09-16 18:55:24 local - # sunrise: 2015-09-16 13:32:43 UTC, sunset: 2015-09-17 01:55:24 UTC + # sunrise: 2015-09-16 06:33:18 local, sunset: 2015-09-16 18:53:45 local + # sunrise: 2015-09-16 13:33:18 UTC, sunset: 2015-09-17 01:53:45 UTC # now = sunrise + 1s -> 'before sunrise' not true - now = datetime(2015, 9, 16, 13, 32, 44, tzinfo=dt_util.UTC) + now = datetime(2015, 9, 16, 13, 33, 19, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() assert len(calls) == 0 # now = sunrise -> 'before sunrise' true - now = datetime(2015, 9, 16, 13, 32, 43, tzinfo=dt_util.UTC) + now = datetime(2015, 9, 16, 13, 33, 18, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() @@ -237,17 +237,17 @@ async def test_if_action_after_sunrise_no_offset(hass, calls): }, ) - # sunrise: 2015-09-16 06:32:43 local, sunset: 2015-09-16 18:55:24 local - # sunrise: 2015-09-16 13:32:43 UTC, sunset: 2015-09-17 01:55:24 UTC + # sunrise: 2015-09-16 06:33:18 local, sunset: 2015-09-16 18:53:45 local + # sunrise: 2015-09-16 13:33:18 UTC, sunset: 2015-09-17 01:53:45 UTC # now = sunrise - 1s -> 'after sunrise' not true - now = datetime(2015, 9, 16, 13, 32, 42, tzinfo=dt_util.UTC) + now = datetime(2015, 9, 16, 13, 33, 17, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() assert len(calls) == 0 # now = sunrise + 1s -> 'after sunrise' true - now = datetime(2015, 9, 16, 13, 32, 43, tzinfo=dt_util.UTC) + now = datetime(2015, 9, 16, 13, 33, 19, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() @@ -290,17 +290,17 @@ async def test_if_action_before_sunrise_with_offset(hass, calls): }, ) - # sunrise: 2015-09-16 06:32:43 local, sunset: 2015-09-16 18:55:24 local - # sunrise: 2015-09-16 13:32:43 UTC, sunset: 2015-09-17 01:55:24 UTC + # sunrise: 2015-09-16 06:33:18 local, sunset: 2015-09-16 18:53:45 local + # sunrise: 2015-09-16 13:33:18 UTC, sunset: 2015-09-17 01:53:45 UTC # now = sunrise + 1s + 1h -> 'before sunrise' with offset +1h not true - now = datetime(2015, 9, 16, 14, 32, 44, tzinfo=dt_util.UTC) + now = datetime(2015, 9, 16, 14, 33, 19, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() assert len(calls) == 0 # now = sunrise + 1h -> 'before sunrise' with offset +1h true - now = datetime(2015, 9, 16, 14, 32, 43, tzinfo=dt_util.UTC) + now = datetime(2015, 9, 16, 14, 33, 18, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() @@ -335,14 +335,14 @@ async def test_if_action_before_sunrise_with_offset(hass, calls): assert len(calls) == 2 # now = sunset -> 'before sunrise' with offset +1h not true - now = datetime(2015, 9, 17, 1, 56, 48, tzinfo=dt_util.UTC) + now = datetime(2015, 9, 17, 1, 53, 45, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() assert len(calls) == 2 # now = sunset -1s -> 'before sunrise' with offset +1h not true - now = datetime(2015, 9, 17, 1, 56, 45, tzinfo=dt_util.UTC) + now = datetime(2015, 9, 17, 1, 53, 44, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() @@ -371,8 +371,8 @@ async def test_if_action_before_sunset_with_offset(hass, calls): }, ) - # sunrise: 2015-09-16 06:32:43 local, sunset: 2015-09-16 18:55:24 local - # sunrise: 2015-09-16 13:32:43 UTC, sunset: 2015-09-17 01:55:24 UTC + # sunrise: 2015-09-16 06:33:18 local, sunset: 2015-09-16 18:53:45 local + # sunrise: 2015-09-16 13:33:18 UTC, sunset: 2015-09-17 01:53:45 UTC # now = local midnight -> 'before sunset' with offset +1h true now = datetime(2015, 9, 16, 7, 0, 0, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): @@ -381,14 +381,14 @@ async def test_if_action_before_sunset_with_offset(hass, calls): assert len(calls) == 1 # now = sunset + 1s + 1h -> 'before sunset' with offset +1h not true - now = datetime(2015, 9, 17, 2, 55, 25, tzinfo=dt_util.UTC) + now = datetime(2015, 9, 17, 2, 53, 46, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() assert len(calls) == 1 # now = sunset + 1h -> 'before sunset' with offset +1h true - now = datetime(2015, 9, 17, 2, 55, 24, tzinfo=dt_util.UTC) + now = datetime(2015, 9, 17, 2, 53, 44, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() @@ -409,14 +409,14 @@ async def test_if_action_before_sunset_with_offset(hass, calls): assert len(calls) == 4 # now = sunrise -> 'before sunset' with offset +1h true - now = datetime(2015, 9, 16, 13, 32, 43, tzinfo=dt_util.UTC) + now = datetime(2015, 9, 16, 13, 33, 18, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() assert len(calls) == 5 # now = sunrise -1s -> 'before sunset' with offset +1h true - now = datetime(2015, 9, 16, 13, 32, 42, tzinfo=dt_util.UTC) + now = datetime(2015, 9, 16, 13, 33, 17, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() @@ -452,17 +452,17 @@ async def test_if_action_after_sunrise_with_offset(hass, calls): }, ) - # sunrise: 2015-09-16 06:32:43 local, sunset: 2015-09-16 18:55:24 local - # sunrise: 2015-09-16 13:32:43 UTC, sunset: 2015-09-17 01:55:24 UTC + # sunrise: 2015-09-16 06:33:18 local, sunset: 2015-09-16 18:53:45 local + # sunrise: 2015-09-16 13:33:18 UTC, sunset: 2015-09-17 01:53:45 UTC # now = sunrise - 1s + 1h -> 'after sunrise' with offset +1h not true - now = datetime(2015, 9, 16, 14, 32, 42, tzinfo=dt_util.UTC) + now = datetime(2015, 9, 16, 14, 33, 17, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() assert len(calls) == 0 # now = sunrise + 1h -> 'after sunrise' with offset +1h true - now = datetime(2015, 9, 16, 14, 32, 43, tzinfo=dt_util.UTC) + now = datetime(2015, 9, 16, 14, 33, 58, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() @@ -497,14 +497,14 @@ async def test_if_action_after_sunrise_with_offset(hass, calls): assert len(calls) == 3 # now = sunset -> 'after sunrise' with offset +1h true - now = datetime(2015, 9, 17, 1, 55, 24, tzinfo=dt_util.UTC) + now = datetime(2015, 9, 17, 1, 53, 45, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() assert len(calls) == 4 # now = sunset + 1s -> 'after sunrise' with offset +1h true - now = datetime(2015, 9, 17, 1, 55, 25, tzinfo=dt_util.UTC) + now = datetime(2015, 9, 17, 1, 53, 45, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() @@ -547,17 +547,17 @@ async def test_if_action_after_sunset_with_offset(hass, calls): }, ) - # sunrise: 2015-09-15 06:32:05 local, sunset: 2015-09-15 18:56:46 local - # sunrise: 2015-09-15 13:32:05 UTC, sunset: 2015-09-16 01:56:46 UTC + # sunrise: 2015-09-16 06:33:18 local, sunset: 2015-09-16 18:53:45 local + # sunrise: 2015-09-16 13:33:18 UTC, sunset: 2015-09-17 01:53:45 UTC # now = sunset - 1s + 1h -> 'after sunset' with offset +1h not true - now = datetime(2015, 9, 16, 2, 56, 45, tzinfo=dt_util.UTC) + now = datetime(2015, 9, 17, 2, 53, 44, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() assert len(calls) == 0 # now = sunset + 1h -> 'after sunset' with offset +1h true - now = datetime(2015, 9, 16, 2, 56, 46, tzinfo=dt_util.UTC) + now = datetime(2015, 9, 17, 2, 53, 45, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() @@ -600,31 +600,31 @@ async def test_if_action_before_and_after_during(hass, calls): }, ) - # sunrise: 2015-09-16 06:32:43 local, sunset: 2015-09-16 18:55:24 local - # sunrise: 2015-09-16 13:32:43 UTC, sunset: 2015-09-17 01:55:24 UTC + # sunrise: 2015-09-16 06:33:18 local, sunset: 2015-09-16 18:53:45 local + # sunrise: 2015-09-16 13:33:18 UTC, sunset: 2015-09-17 01:53:45 UTC # now = sunrise - 1s -> 'after sunrise' + 'before sunset' not true - now = datetime(2015, 9, 16, 13, 32, 42, tzinfo=dt_util.UTC) + now = datetime(2015, 9, 16, 13, 33, 17, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() assert len(calls) == 0 # now = sunset + 1s -> 'after sunrise' + 'before sunset' not true - now = datetime(2015, 9, 17, 1, 55, 25, tzinfo=dt_util.UTC) + now = datetime(2015, 9, 17, 1, 53, 46, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() assert len(calls) == 0 - # now = sunrise -> 'after sunrise' + 'before sunset' true - now = datetime(2015, 9, 16, 13, 32, 43, tzinfo=dt_util.UTC) + # now = sunrise + 1s -> 'after sunrise' + 'before sunset' true + now = datetime(2015, 9, 16, 13, 33, 19, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() assert len(calls) == 1 - # now = sunset -> 'after sunrise' + 'before sunset' true - now = datetime(2015, 9, 17, 1, 55, 24, tzinfo=dt_util.UTC) + # now = sunset - 1s -> 'after sunrise' + 'before sunset' true + now = datetime(2015, 9, 17, 1, 53, 44, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() @@ -663,17 +663,17 @@ async def test_if_action_before_sunrise_no_offset_kotzebue(hass, calls): }, ) - # sunrise: 2015-07-24 07:17:24 local, sunset: 2015-07-25 03:16:27 local - # sunrise: 2015-07-24 15:17:24 UTC, sunset: 2015-07-25 11:16:27 UTC + # sunrise: 2015-07-24 07:21:12 local, sunset: 2015-07-25 03:13:33 local + # sunrise: 2015-07-24 15:21:12 UTC, sunset: 2015-07-25 11:13:33 UTC # now = sunrise + 1s -> 'before sunrise' not true - now = datetime(2015, 7, 24, 15, 17, 25, tzinfo=dt_util.UTC) + now = datetime(2015, 7, 24, 15, 21, 13, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() assert len(calls) == 0 - # now = sunrise -> 'before sunrise' true - now = datetime(2015, 7, 24, 15, 17, 24, tzinfo=dt_util.UTC) + # now = sunrise - 1h -> 'before sunrise' true + now = datetime(2015, 7, 24, 14, 21, 12, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() @@ -719,17 +719,17 @@ async def test_if_action_after_sunrise_no_offset_kotzebue(hass, calls): }, ) - # sunrise: 2015-07-24 07:17:24 local, sunset: 2015-07-25 03:16:27 local - # sunrise: 2015-07-24 15:17:24 UTC, sunset: 2015-07-25 11:16:27 UTC + # sunrise: 2015-07-24 07:21:12 local, sunset: 2015-07-25 03:13:33 local + # sunrise: 2015-07-24 15:21:12 UTC, sunset: 2015-07-25 11:13:33 UTC # now = sunrise -> 'after sunrise' true - now = datetime(2015, 7, 24, 15, 17, 24, tzinfo=dt_util.UTC) + now = datetime(2015, 7, 24, 15, 21, 12, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() assert len(calls) == 1 - # now = sunrise - 1s -> 'after sunrise' not true - now = datetime(2015, 7, 24, 15, 17, 23, tzinfo=dt_util.UTC) + # now = sunrise - 1h -> 'after sunrise' not true + now = datetime(2015, 7, 24, 14, 21, 12, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() @@ -775,17 +775,17 @@ async def test_if_action_before_sunset_no_offset_kotzebue(hass, calls): }, ) - # sunrise: 2015-07-24 07:17:24 local, sunset: 2015-07-25 03:16:27 local - # sunrise: 2015-07-24 15:17:24 UTC, sunset: 2015-07-25 11:16:27 UTC - # now = sunrise + 1s -> 'before sunrise' not true - now = datetime(2015, 7, 25, 11, 16, 28, tzinfo=dt_util.UTC) + # sunrise: 2015-07-24 07:21:12 local, sunset: 2015-07-25 03:13:33 local + # sunrise: 2015-07-24 15:21:12 UTC, sunset: 2015-07-25 11:13:33 UTC + # now = sunset + 1s -> 'before sunset' not true + now = datetime(2015, 7, 25, 11, 13, 34, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() assert len(calls) == 0 - # now = sunrise -> 'before sunrise' true - now = datetime(2015, 7, 25, 11, 16, 27, tzinfo=dt_util.UTC) + # now = sunset - 1h-> 'before sunset' true + now = datetime(2015, 7, 25, 10, 13, 33, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() @@ -831,17 +831,17 @@ async def test_if_action_after_sunset_no_offset_kotzebue(hass, calls): }, ) - # sunrise: 2015-07-24 07:17:24 local, sunset: 2015-07-25 03:16:27 local - # sunrise: 2015-07-24 15:17:24 UTC, sunset: 2015-07-25 11:16:27 UTC + # sunrise: 2015-07-24 07:21:12 local, sunset: 2015-07-25 03:13:33 local + # sunrise: 2015-07-24 15:21:12 UTC, sunset: 2015-07-25 11:13:33 UTC # now = sunset -> 'after sunset' true - now = datetime(2015, 7, 25, 11, 16, 27, tzinfo=dt_util.UTC) + now = datetime(2015, 7, 25, 11, 13, 33, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() assert len(calls) == 1 # now = sunset - 1s -> 'after sunset' not true - now = datetime(2015, 7, 25, 11, 16, 26, tzinfo=dt_util.UTC) + now = datetime(2015, 7, 25, 11, 13, 32, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() diff --git a/tests/components/tod/test_binary_sensor.py b/tests/components/tod/test_binary_sensor.py index 363d159b811c6..2eb506f80f362 100644 --- a/tests/components/tod/test_binary_sensor.py +++ b/tests/components/tod/test_binary_sensor.py @@ -643,6 +643,7 @@ async def test_norwegian_case_summer(hass): """Test location in Norway where the sun doesn't set in summer.""" hass.config.latitude = 69.6 hass.config.longitude = 18.8 + hass.config.elevation = 10.0 test_time = hass.config.time_zone.localize(datetime(2010, 6, 1)).astimezone( pytz.UTC @@ -652,7 +653,7 @@ async def test_norwegian_case_summer(hass): get_astral_event_next(hass, "sunrise", dt_util.as_utc(test_time)) ) sunset = dt_util.as_local( - get_astral_event_next(hass, "sunset", dt_util.as_utc(test_time)) + get_astral_event_next(hass, "sunset", dt_util.as_utc(sunrise)) ) config = { "binary_sensor": [ diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index b0f58f76a661a..b8291b97efa88 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -4,7 +4,8 @@ from datetime import datetime, timedelta from unittest.mock import patch -from astral import Astral +from astral import LocationInfo +import astral.sun import jinja2 import pytest @@ -2433,15 +2434,18 @@ async def test_track_sunrise(hass, legacy_patchable_time): hass, sun.DOMAIN, {sun.DOMAIN: {sun.CONF_ELEVATION: 0}} ) + location = LocationInfo( + latitude=hass.config.latitude, longitude=hass.config.longitude + ) + # Get next sunrise/sunset - astral = Astral() utc_now = datetime(2014, 5, 24, 12, 0, 0, tzinfo=dt_util.UTC) utc_today = utc_now.date() mod = -1 while True: - next_rising = astral.sunrise_utc( - utc_today + timedelta(days=mod), latitude, longitude + next_rising = astral.sun.sunrise( + location.observer, date=utc_today + timedelta(days=mod) ) if next_rising > utc_now: break @@ -2493,15 +2497,18 @@ async def test_track_sunrise_update_location(hass, legacy_patchable_time): hass, sun.DOMAIN, {sun.DOMAIN: {sun.CONF_ELEVATION: 0}} ) + location = LocationInfo( + latitude=hass.config.latitude, longitude=hass.config.longitude + ) + # Get next sunrise - astral = Astral() utc_now = datetime(2014, 5, 24, 12, 0, 0, tzinfo=dt_util.UTC) utc_today = utc_now.date() mod = -1 while True: - next_rising = astral.sunrise_utc( - utc_today + timedelta(days=mod), hass.config.latitude, hass.config.longitude + next_rising = astral.sun.sunrise( + location.observer, date=utc_today + timedelta(days=mod) ) if next_rising > utc_now: break @@ -2522,6 +2529,11 @@ async def test_track_sunrise_update_location(hass, legacy_patchable_time): await hass.config.async_update(latitude=40.755931, longitude=-73.984606) await hass.async_block_till_done() + # update location for astral + location = LocationInfo( + latitude=hass.config.latitude, longitude=hass.config.longitude + ) + # Mimic sunrise async_fire_time_changed(hass, next_rising) await hass.async_block_till_done() @@ -2531,8 +2543,8 @@ async def test_track_sunrise_update_location(hass, legacy_patchable_time): # Get next sunrise mod = -1 while True: - next_rising = astral.sunrise_utc( - utc_today + timedelta(days=mod), hass.config.latitude, hass.config.longitude + next_rising = astral.sun.sunrise( + location.observer, date=utc_today + timedelta(days=mod) ) if next_rising > utc_now: break @@ -2549,6 +2561,8 @@ async def test_track_sunset(hass, legacy_patchable_time): latitude = 32.87336 longitude = 117.22743 + location = LocationInfo(latitude=latitude, longitude=longitude) + # Setup sun component hass.config.latitude = latitude hass.config.longitude = longitude @@ -2557,14 +2571,13 @@ async def test_track_sunset(hass, legacy_patchable_time): ) # Get next sunrise/sunset - astral = Astral() utc_now = datetime(2014, 5, 24, 12, 0, 0, tzinfo=dt_util.UTC) utc_today = utc_now.date() mod = -1 while True: - next_setting = astral.sunset_utc( - utc_today + timedelta(days=mod), latitude, longitude + next_setting = astral.sun.sunset( + location.observer, date=utc_today + timedelta(days=mod) ) if next_setting > utc_now: break diff --git a/tests/helpers/test_sun.py b/tests/helpers/test_sun.py index b8ecd1ed86aa9..84545bf43b60a 100644 --- a/tests/helpers/test_sun.py +++ b/tests/helpers/test_sun.py @@ -11,18 +11,19 @@ def test_next_events(hass): """Test retrieving next sun events.""" utc_now = datetime(2016, 11, 1, 8, 0, 0, tzinfo=dt_util.UTC) - from astral import Astral + from astral import LocationInfo + import astral.sun - astral = Astral() utc_today = utc_now.date() - latitude = hass.config.latitude - longitude = hass.config.longitude + location = LocationInfo( + latitude=hass.config.latitude, longitude=hass.config.longitude + ) mod = -1 while True: - next_dawn = astral.dawn_utc( - utc_today + timedelta(days=mod), latitude, longitude + next_dawn = astral.sun.dawn( + location.observer, date=utc_today + timedelta(days=mod) ) if next_dawn > utc_now: break @@ -30,8 +31,8 @@ def test_next_events(hass): mod = -1 while True: - next_dusk = astral.dusk_utc( - utc_today + timedelta(days=mod), latitude, longitude + next_dusk = astral.sun.dusk( + location.observer, date=utc_today + timedelta(days=mod) ) if next_dusk > utc_now: break @@ -39,8 +40,8 @@ def test_next_events(hass): mod = -1 while True: - next_midnight = astral.solar_midnight_utc( - utc_today + timedelta(days=mod), longitude + next_midnight = astral.sun.midnight( + location.observer, date=utc_today + timedelta(days=mod) ) if next_midnight > utc_now: break @@ -48,15 +49,17 @@ def test_next_events(hass): mod = -1 while True: - next_noon = astral.solar_noon_utc(utc_today + timedelta(days=mod), longitude) + next_noon = astral.sun.noon( + location.observer, date=utc_today + timedelta(days=mod) + ) if next_noon > utc_now: break mod += 1 mod = -1 while True: - next_rising = astral.sunrise_utc( - utc_today + timedelta(days=mod), latitude, longitude + next_rising = astral.sun.sunrise( + location.observer, date=utc_today + timedelta(days=mod) ) if next_rising > utc_now: break @@ -64,8 +67,8 @@ def test_next_events(hass): mod = -1 while True: - next_setting = astral.sunset_utc( - utc_today + timedelta(days=mod), latitude, longitude + next_setting = astral.sun.sunset( + location.observer, utc_today + timedelta(days=mod) ) if next_setting > utc_now: break @@ -74,8 +77,8 @@ def test_next_events(hass): with patch("homeassistant.helpers.condition.dt_util.utcnow", return_value=utc_now): assert next_dawn == sun.get_astral_event_next(hass, "dawn") assert next_dusk == sun.get_astral_event_next(hass, "dusk") - assert next_midnight == sun.get_astral_event_next(hass, "solar_midnight") - assert next_noon == sun.get_astral_event_next(hass, "solar_noon") + assert next_midnight == sun.get_astral_event_next(hass, "midnight") + assert next_noon == sun.get_astral_event_next(hass, "noon") assert next_rising == sun.get_astral_event_next(hass, SUN_EVENT_SUNRISE) assert next_setting == sun.get_astral_event_next(hass, SUN_EVENT_SUNSET) @@ -83,25 +86,26 @@ def test_next_events(hass): def test_date_events(hass): """Test retrieving next sun events.""" utc_now = datetime(2016, 11, 1, 8, 0, 0, tzinfo=dt_util.UTC) - from astral import Astral + from astral import LocationInfo + import astral.sun - astral = Astral() utc_today = utc_now.date() - latitude = hass.config.latitude - longitude = hass.config.longitude + location = LocationInfo( + latitude=hass.config.latitude, longitude=hass.config.longitude + ) - dawn = astral.dawn_utc(utc_today, latitude, longitude) - dusk = astral.dusk_utc(utc_today, latitude, longitude) - midnight = astral.solar_midnight_utc(utc_today, longitude) - noon = astral.solar_noon_utc(utc_today, longitude) - sunrise = astral.sunrise_utc(utc_today, latitude, longitude) - sunset = astral.sunset_utc(utc_today, latitude, longitude) + dawn = astral.sun.dawn(location.observer, utc_today) + dusk = astral.sun.dusk(location.observer, utc_today) + midnight = astral.sun.midnight(location.observer, utc_today) + noon = astral.sun.noon(location.observer, utc_today) + sunrise = astral.sun.sunrise(location.observer, utc_today) + sunset = astral.sun.sunset(location.observer, utc_today) assert dawn == sun.get_astral_event_date(hass, "dawn", utc_today) assert dusk == sun.get_astral_event_date(hass, "dusk", utc_today) - assert midnight == sun.get_astral_event_date(hass, "solar_midnight", utc_today) - assert noon == sun.get_astral_event_date(hass, "solar_noon", utc_today) + assert midnight == sun.get_astral_event_date(hass, "midnight", utc_today) + assert noon == sun.get_astral_event_date(hass, "noon", utc_today) assert sunrise == sun.get_astral_event_date(hass, SUN_EVENT_SUNRISE, utc_today) assert sunset == sun.get_astral_event_date(hass, SUN_EVENT_SUNSET, utc_today) @@ -109,26 +113,27 @@ def test_date_events(hass): def test_date_events_default_date(hass): """Test retrieving next sun events.""" utc_now = datetime(2016, 11, 1, 8, 0, 0, tzinfo=dt_util.UTC) - from astral import Astral + from astral import LocationInfo + import astral.sun - astral = Astral() utc_today = utc_now.date() - latitude = hass.config.latitude - longitude = hass.config.longitude + location = LocationInfo( + latitude=hass.config.latitude, longitude=hass.config.longitude + ) - dawn = astral.dawn_utc(utc_today, latitude, longitude) - dusk = astral.dusk_utc(utc_today, latitude, longitude) - midnight = astral.solar_midnight_utc(utc_today, longitude) - noon = astral.solar_noon_utc(utc_today, longitude) - sunrise = astral.sunrise_utc(utc_today, latitude, longitude) - sunset = astral.sunset_utc(utc_today, latitude, longitude) + dawn = astral.sun.dawn(location.observer, date=utc_today) + dusk = astral.sun.dusk(location.observer, date=utc_today) + midnight = astral.sun.midnight(location.observer, date=utc_today) + noon = astral.sun.noon(location.observer, date=utc_today) + sunrise = astral.sun.sunrise(location.observer, date=utc_today) + sunset = astral.sun.sunset(location.observer, date=utc_today) with patch("homeassistant.util.dt.now", return_value=utc_now): assert dawn == sun.get_astral_event_date(hass, "dawn", utc_today) assert dusk == sun.get_astral_event_date(hass, "dusk", utc_today) - assert midnight == sun.get_astral_event_date(hass, "solar_midnight", utc_today) - assert noon == sun.get_astral_event_date(hass, "solar_noon", utc_today) + assert midnight == sun.get_astral_event_date(hass, "midnight", utc_today) + assert noon == sun.get_astral_event_date(hass, "noon", utc_today) assert sunrise == sun.get_astral_event_date(hass, SUN_EVENT_SUNRISE, utc_today) assert sunset == sun.get_astral_event_date(hass, SUN_EVENT_SUNSET, utc_today) @@ -136,25 +141,26 @@ def test_date_events_default_date(hass): def test_date_events_accepts_datetime(hass): """Test retrieving next sun events.""" utc_now = datetime(2016, 11, 1, 8, 0, 0, tzinfo=dt_util.UTC) - from astral import Astral + from astral import LocationInfo + import astral.sun - astral = Astral() utc_today = utc_now.date() - latitude = hass.config.latitude - longitude = hass.config.longitude + location = LocationInfo( + latitude=hass.config.latitude, longitude=hass.config.longitude + ) - dawn = astral.dawn_utc(utc_today, latitude, longitude) - dusk = astral.dusk_utc(utc_today, latitude, longitude) - midnight = astral.solar_midnight_utc(utc_today, longitude) - noon = astral.solar_noon_utc(utc_today, longitude) - sunrise = astral.sunrise_utc(utc_today, latitude, longitude) - sunset = astral.sunset_utc(utc_today, latitude, longitude) + dawn = astral.sun.dawn(location.observer, date=utc_today) + dusk = astral.sun.dusk(location.observer, date=utc_today) + midnight = astral.sun.midnight(location.observer, date=utc_today) + noon = astral.sun.noon(location.observer, date=utc_today) + sunrise = astral.sun.sunrise(location.observer, date=utc_today) + sunset = astral.sun.sunset(location.observer, date=utc_today) assert dawn == sun.get_astral_event_date(hass, "dawn", utc_now) assert dusk == sun.get_astral_event_date(hass, "dusk", utc_now) - assert midnight == sun.get_astral_event_date(hass, "solar_midnight", utc_now) - assert noon == sun.get_astral_event_date(hass, "solar_noon", utc_now) + assert midnight == sun.get_astral_event_date(hass, "midnight", utc_now) + assert noon == sun.get_astral_event_date(hass, "noon", utc_now) assert sunrise == sun.get_astral_event_date(hass, SUN_EVENT_SUNRISE, utc_now) assert sunset == sun.get_astral_event_date(hass, SUN_EVENT_SUNSET, utc_now) @@ -184,10 +190,10 @@ def test_norway_in_june(hass): print(sun.get_astral_event_date(hass, SUN_EVENT_SUNSET, datetime(2017, 7, 26))) assert sun.get_astral_event_next(hass, SUN_EVENT_SUNRISE, june) == datetime( - 2016, 7, 25, 23, 23, 39, tzinfo=dt_util.UTC + 2016, 7, 24, 22, 59, 45, 689645, tzinfo=dt_util.UTC ) assert sun.get_astral_event_next(hass, SUN_EVENT_SUNSET, june) == datetime( - 2016, 7, 26, 22, 19, 1, tzinfo=dt_util.UTC + 2016, 7, 25, 22, 17, 13, 503932, tzinfo=dt_util.UTC ) assert sun.get_astral_event_date(hass, SUN_EVENT_SUNRISE, june) is None assert sun.get_astral_event_date(hass, SUN_EVENT_SUNSET, june) is None From ceeb060c054ea06f59350f0b1cff42151e7d8bab Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 2 Apr 2021 00:31:19 +0200 Subject: [PATCH 0022/1317] Fix websocket search for related (#48603) Co-authored-by: Paulus Schoutsen --- homeassistant/components/search/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/search/__init__.py b/homeassistant/components/search/__init__.py index 291ef0b52e28c..3198f40720b3b 100644 --- a/homeassistant/components/search/__init__.py +++ b/homeassistant/components/search/__init__.py @@ -19,7 +19,6 @@ async def async_setup(hass: HomeAssistant, config: dict): return True -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required("type"): "search/related", @@ -38,6 +37,7 @@ async def async_setup(hass: HomeAssistant, config: dict): vol.Required("item_id"): str, } ) +@callback def websocket_search_related(hass, connection, msg): """Handle search.""" searcher = Searcher( From ebb369e008e790c291c78f2c9356ffea38942385 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 1 Apr 2021 18:35:13 -0400 Subject: [PATCH 0023/1317] Add zwave_js WS API command to call node.refresh_info (#48564) --- homeassistant/components/zwave_js/api.py | 27 ++++++++++++++++ tests/components/zwave_js/test_api.py | 39 ++++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index eed04a34c7d96..2792fc6819b58 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -60,6 +60,7 @@ def async_register_api(hass: HomeAssistant) -> None: websocket_api.async_register_command(hass, websocket_stop_inclusion) websocket_api.async_register_command(hass, websocket_remove_node) websocket_api.async_register_command(hass, websocket_stop_exclusion) + websocket_api.async_register_command(hass, websocket_refresh_node_info) websocket_api.async_register_command(hass, websocket_update_log_config) websocket_api.async_register_command(hass, websocket_get_log_config) websocket_api.async_register_command(hass, websocket_get_config_parameters) @@ -301,6 +302,32 @@ def node_removed(event: dict) -> None: ) +@websocket_api.require_admin # type: ignore +@websocket_api.async_response +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/refresh_node_info", + vol.Required(ENTRY_ID): str, + vol.Required(NODE_ID): int, + }, +) +async def websocket_refresh_node_info( + hass: HomeAssistant, connection: ActiveConnection, msg: dict +) -> None: + """Re-interview a node.""" + entry_id = msg[ENTRY_ID] + node_id = msg[NODE_ID] + client = hass.data[DOMAIN][entry_id][DATA_CLIENT] + node = client.driver.controller.nodes.get(node_id) + + if node is None: + connection.send_error(msg[ID], ERR_NOT_FOUND, f"Node {node_id} not found") + return + + await node.async_refresh_info() + connection.send_result(msg[ID]) + + @websocket_api.require_admin # type:ignore @websocket_api.async_response @websocket_api.websocket_command( diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index 304e941a32af2..eb198b01f82a6 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -222,6 +222,45 @@ async def test_remove_node( assert device is None +async def test_refresh_node_info( + hass, client, integration, hass_ws_client, multisensor_6 +): + """Test that the refresh_node_info WS API call works.""" + entry = integration + ws_client = await hass_ws_client(hass) + + client.async_send_command_no_wait.return_value = None + await ws_client.send_json( + { + ID: 1, + TYPE: "zwave_js/refresh_node_info", + ENTRY_ID: entry.entry_id, + NODE_ID: 52, + } + ) + msg = await ws_client.receive_json() + assert msg["success"] + + assert len(client.async_send_command_no_wait.call_args_list) == 1 + args = client.async_send_command_no_wait.call_args[0][0] + assert args["command"] == "node.refresh_info" + assert args["nodeId"] == 52 + + client.async_send_command_no_wait.reset_mock() + + await ws_client.send_json( + { + ID: 2, + TYPE: "zwave_js/refresh_node_info", + ENTRY_ID: entry.entry_id, + NODE_ID: 999, + } + ) + msg = await ws_client.receive_json() + assert not msg["success"] + assert msg["error"]["code"] == "not_found" + + async def test_set_config_parameter( hass, client, hass_ws_client, multisensor_6, integration ): From 648280072411e1e80dfccd1834e8a153866f5511 Mon Sep 17 00:00:00 2001 From: Khole Date: Fri, 2 Apr 2021 00:14:40 +0100 Subject: [PATCH 0024/1317] Add hive heat on demand (#48591) --- homeassistant/components/hive/climate.py | 8 ++++---- homeassistant/components/hive/manifest.json | 2 +- homeassistant/components/hive/switch.py | 4 ++-- homeassistant/components/hive/water_heater.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/hive/climate.py b/homeassistant/components/hive/climate.py index 31b4bd273ad74..e6da78d921cb9 100644 --- a/homeassistant/components/hive/climate.py +++ b/homeassistant/components/hive/climate.py @@ -192,18 +192,18 @@ async def async_set_temperature(self, **kwargs): async def async_set_preset_mode(self, preset_mode): """Set new preset mode.""" if preset_mode == PRESET_NONE and self.preset_mode == PRESET_BOOST: - await self.hive.heating.turnBoostOff(self.device) + await self.hive.heating.setBoostOff(self.device) elif preset_mode == PRESET_BOOST: curtemp = round(self.current_temperature * 2) / 2 temperature = curtemp + 0.5 - await self.hive.heating.turnBoostOn(self.device, 30, temperature) + await self.hive.heating.setBoostOn(self.device, 30, temperature) @refresh_system async def async_heating_boost(self, time_period, temperature): """Handle boost heating service call.""" - await self.hive.heating.turnBoostOn(self.device, time_period, temperature) + await self.hive.heating.setBoostOn(self.device, time_period, temperature) async def async_update(self): """Update all Node data from Hive.""" await self.hive.session.updateData(self.device) - self.device = await self.hive.heating.getHeating(self.device) + self.device = await self.hive.heating.getClimate(self.device) diff --git a/homeassistant/components/hive/manifest.json b/homeassistant/components/hive/manifest.json index f8f40401599af..a1d74c023f1bb 100644 --- a/homeassistant/components/hive/manifest.json +++ b/homeassistant/components/hive/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hive", "requirements": [ - "pyhiveapi==0.3.9" + "pyhiveapi==0.4.1" ], "codeowners": [ "@Rendili", diff --git a/homeassistant/components/hive/switch.py b/homeassistant/components/hive/switch.py index acc2040db0025..1151fcf346b47 100644 --- a/homeassistant/components/hive/switch.py +++ b/homeassistant/components/hive/switch.py @@ -63,7 +63,7 @@ def extra_state_attributes(self): @property def current_power_w(self): """Return the current power usage in W.""" - return self.device["status"]["power_usage"] + return self.device["status"].get("power_usage") @property def is_on(self): @@ -83,4 +83,4 @@ async def async_turn_off(self, **kwargs): async def async_update(self): """Update all Node data from Hive.""" await self.hive.session.updateData(self.device) - self.device = await self.hive.switch.getPlug(self.device) + self.device = await self.hive.switch.getSwitch(self.device) diff --git a/homeassistant/components/hive/water_heater.py b/homeassistant/components/hive/water_heater.py index 5d8eb590ea7eb..0df10a9ed22cd 100644 --- a/homeassistant/components/hive/water_heater.py +++ b/homeassistant/components/hive/water_heater.py @@ -146,4 +146,4 @@ async def async_hot_water_boost(self, time_period, on_off): async def async_update(self): """Update all Node data from Hive.""" await self.hive.session.updateData(self.device) - self.device = await self.hive.hotwater.getHotwater(self.device) + self.device = await self.hive.hotwater.getWaterHeater(self.device) diff --git a/requirements_all.txt b/requirements_all.txt index d5cce464db9d2..fb85385527738 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1428,7 +1428,7 @@ pyheos==0.7.2 pyhik==0.2.8 # homeassistant.components.hive -pyhiveapi==0.3.9 +pyhiveapi==0.4.1 # homeassistant.components.homematic pyhomematic==0.1.72 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6281717efa902..c8429ea526b39 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -757,7 +757,7 @@ pyhaversion==21.3.0 pyheos==0.7.2 # homeassistant.components.hive -pyhiveapi==0.3.9 +pyhiveapi==0.4.1 # homeassistant.components.homematic pyhomematic==0.1.72 From 34ddea536e08b92ae0bdbcf05e15d6e20ae1d161 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 2 Apr 2021 01:19:57 +0200 Subject: [PATCH 0025/1317] Update frontend to 20210402.0 (#48609) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 234996afe4f6b..60ea0ff53b2e5 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20210331.0" + "home-assistant-frontend==20210402.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a58a800287a73..14910dacf760d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ defusedxml==0.6.0 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.42.0 -home-assistant-frontend==20210331.0 +home-assistant-frontend==20210402.0 httpx==0.17.1 jinja2>=2.11.3 netdisco==2.8.2 diff --git a/requirements_all.txt b/requirements_all.txt index fb85385527738..3823a420587ad 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -760,7 +760,7 @@ hole==0.5.1 holidays==0.10.5.2 # homeassistant.components.frontend -home-assistant-frontend==20210331.0 +home-assistant-frontend==20210402.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c8429ea526b39..675422d49c80a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -415,7 +415,7 @@ hole==0.5.1 holidays==0.10.5.2 # homeassistant.components.frontend -home-assistant-frontend==20210331.0 +home-assistant-frontend==20210402.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 051531d9c1230c44000686d9f5c61837171f0de8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 1 Apr 2021 16:22:08 -0700 Subject: [PATCH 0026/1317] Clean up mobile app (#48607) Co-authored-by: Martin Hjelmare --- .../components/mobile_app/binary_sensor.py | 6 ++-- homeassistant/components/mobile_app/entity.py | 12 +++----- homeassistant/components/mobile_app/notify.py | 11 +++---- homeassistant/components/mobile_app/sensor.py | 6 ++-- .../components/mobile_app/webhook.py | 8 ++--- homeassistant/util/logging.py | 6 +++- tests/util/test_logging.py | 29 +++++++++++++++++++ 7 files changed, 51 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/mobile_app/binary_sensor.py b/homeassistant/components/mobile_app/binary_sensor.py index 36897dd9f693f..616cd97a775f0 100644 --- a/homeassistant/components/mobile_app/binary_sensor.py +++ b/homeassistant/components/mobile_app/binary_sensor.py @@ -1,6 +1,4 @@ """Binary sensor platform for mobile_app.""" -from functools import partial - from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.const import CONF_NAME, CONF_UNIQUE_ID, CONF_WEBHOOK_ID, STATE_ON from homeassistant.core import callback @@ -48,7 +46,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities) @callback - def handle_sensor_registration(webhook_id, data): + def handle_sensor_registration(data): if data[CONF_WEBHOOK_ID] != webhook_id: return @@ -66,7 +64,7 @@ def handle_sensor_registration(webhook_id, data): async_dispatcher_connect( hass, f"{DOMAIN}_{ENTITY_TYPE}_register", - partial(handle_sensor_registration, webhook_id), + handle_sensor_registration, ) diff --git a/homeassistant/components/mobile_app/entity.py b/homeassistant/components/mobile_app/entity.py index 2f30c4b9f1bb7..46f4589fa2cb2 100644 --- a/homeassistant/components/mobile_app/entity.py +++ b/homeassistant/components/mobile_app/entity.py @@ -34,13 +34,14 @@ def __init__(self, config: dict, device: DeviceEntry, entry: ConfigEntry): self._registration = entry.data self._unique_id = config[CONF_UNIQUE_ID] self._entity_type = config[ATTR_SENSOR_TYPE] - self.unsub_dispatcher = None self._name = config[CONF_NAME] async def async_added_to_hass(self): """Register callbacks.""" - self.unsub_dispatcher = async_dispatcher_connect( - self.hass, SIGNAL_SENSOR_UPDATE, self._handle_update + self.async_on_remove( + async_dispatcher_connect( + self.hass, SIGNAL_SENSOR_UPDATE, self._handle_update + ) ) state = await self.async_get_last_state() @@ -49,11 +50,6 @@ async def async_added_to_hass(self): self.async_restore_last_state(state) - async def async_will_remove_from_hass(self): - """Disconnect dispatcher listener when removed.""" - if self.unsub_dispatcher is not None: - self.unsub_dispatcher() - @callback def async_restore_last_state(self, last_state): """Restore previous state.""" diff --git a/homeassistant/components/mobile_app/notify.py b/homeassistant/components/mobile_app/notify.py index 763186df998b9..803f00764e7cd 100644 --- a/homeassistant/components/mobile_app/notify.py +++ b/homeassistant/components/mobile_app/notify.py @@ -84,17 +84,16 @@ def log_rate_limits(hass, device_name, resp, level=logging.INFO): async def async_get_service(hass, config, discovery_info=None): """Get the mobile_app notification service.""" - session = async_get_clientsession(hass) - service = hass.data[DOMAIN][DATA_NOTIFY] = MobileAppNotificationService(session) + service = hass.data[DOMAIN][DATA_NOTIFY] = MobileAppNotificationService(hass) return service class MobileAppNotificationService(BaseNotificationService): """Implement the notification service for mobile_app.""" - def __init__(self, session): + def __init__(self, hass): """Initialize the service.""" - self._session = session + self._hass = hass @property def targets(self): @@ -141,7 +140,9 @@ async def async_send_message(self, message="", **kwargs): try: with async_timeout.timeout(10): - response = await self._session.post(push_url, json=data) + response = await async_get_clientsession(self._hass).post( + push_url, json=data + ) result = await response.json() if response.status in [HTTP_OK, HTTP_CREATED, HTTP_ACCEPTED]: diff --git a/homeassistant/components/mobile_app/sensor.py b/homeassistant/components/mobile_app/sensor.py index 3f4c7d56f3f5f..7e3c1c13148a8 100644 --- a/homeassistant/components/mobile_app/sensor.py +++ b/homeassistant/components/mobile_app/sensor.py @@ -1,6 +1,4 @@ """Sensor platform for mobile_app.""" -from functools import partial - from homeassistant.components.sensor import SensorEntity from homeassistant.const import CONF_NAME, CONF_UNIQUE_ID, CONF_WEBHOOK_ID from homeassistant.core import callback @@ -50,7 +48,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities) @callback - def handle_sensor_registration(webhook_id, data): + def handle_sensor_registration(data): if data[CONF_WEBHOOK_ID] != webhook_id: return @@ -68,7 +66,7 @@ def handle_sensor_registration(webhook_id, data): async_dispatcher_connect( hass, f"{DOMAIN}_{ENTITY_TYPE}_register", - partial(handle_sensor_registration, webhook_id), + handle_sensor_registration, ) diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index efef6eb1c8a5b..6be39f34f00fa 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -472,6 +472,7 @@ async def webhook_update_sensor_states(hass, config_entry, data): device_name = config_entry.data[ATTR_DEVICE_NAME] resp = {} + for sensor in data: entity_type = sensor[ATTR_SENSOR_TYPE] @@ -495,8 +496,6 @@ async def webhook_update_sensor_states(hass, config_entry, data): } continue - entry = {CONF_WEBHOOK_ID: config_entry.data[CONF_WEBHOOK_ID]} - try: sensor = sensor_schema_full(sensor) except vol.Invalid as err: @@ -513,9 +512,8 @@ async def webhook_update_sensor_states(hass, config_entry, data): } continue - new_state = {**entry, **sensor} - - async_dispatcher_send(hass, SIGNAL_SENSOR_UPDATE, new_state) + sensor[CONF_WEBHOOK_ID] = config_entry.data[CONF_WEBHOOK_ID] + async_dispatcher_send(hass, SIGNAL_SENSOR_UPDATE, sensor) resp[unique_id] = {"success": True} diff --git a/homeassistant/util/logging.py b/homeassistant/util/logging.py index 5653523b677b4..ba846c0e8b429 100644 --- a/homeassistant/util/logging.py +++ b/homeassistant/util/logging.py @@ -11,7 +11,7 @@ from typing import Any, Awaitable, Callable, Coroutine, cast, overload from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant, callback, is_callback class HideSensitiveDataFilter(logging.Filter): @@ -138,6 +138,7 @@ async def async_wrapper(*args: Any) -> None: log_exception(format_err, *args) wrapper_func = async_wrapper + else: @wraps(func) @@ -148,6 +149,9 @@ def wrapper(*args: Any) -> None: except Exception: # pylint: disable=broad-except log_exception(format_err, *args) + if is_callback(check_func): + wrapper = callback(wrapper) + wrapper_func = wrapper return wrapper_func diff --git a/tests/util/test_logging.py b/tests/util/test_logging.py index 1a82c35e82d20..9277d92f3686f 100644 --- a/tests/util/test_logging.py +++ b/tests/util/test_logging.py @@ -1,11 +1,13 @@ """Test Home Assistant logging util methods.""" import asyncio +from functools import partial import logging import queue from unittest.mock import patch import pytest +from homeassistant.core import callback, is_callback import homeassistant.util.logging as logging_util @@ -80,3 +82,30 @@ async def job(): await hass.async_block_till_done() assert "This is a bad coroutine" in caplog.text assert "in test_async_create_catching_coro" in caplog.text + + +def test_catch_log_exception(): + """Test it is still a callback after wrapping including partial.""" + + async def async_meth(): + pass + + assert asyncio.iscoroutinefunction( + logging_util.catch_log_exception(partial(async_meth), lambda: None) + ) + + @callback + def callback_meth(): + pass + + assert is_callback( + logging_util.catch_log_exception(partial(callback_meth), lambda: None) + ) + + def sync_meth(): + pass + + wrapped = logging_util.catch_log_exception(partial(sync_meth), lambda: None) + + assert not is_callback(wrapped) + assert not asyncio.iscoroutinefunction(wrapped) From a61d93adc2904dbff49dbe30335601a3c6f65974 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 2 Apr 2021 01:22:36 +0200 Subject: [PATCH 0027/1317] Increase time out for http requests done in Axis integration (#48610) --- homeassistant/components/axis/device.py | 2 +- homeassistant/components/axis/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/axis/device.py b/homeassistant/components/axis/device.py index 8c2a43c44ed07..f732ad2fb5d7e 100644 --- a/homeassistant/components/axis/device.py +++ b/homeassistant/components/axis/device.py @@ -304,7 +304,7 @@ async def get_device(hass, host, port, username, password): ) try: - with async_timeout.timeout(15): + with async_timeout.timeout(30): await device.vapix.initialize() return device diff --git a/homeassistant/components/axis/manifest.json b/homeassistant/components/axis/manifest.json index a78d916da9e9b..b709ac35da2b3 100644 --- a/homeassistant/components/axis/manifest.json +++ b/homeassistant/components/axis/manifest.json @@ -3,7 +3,7 @@ "name": "Axis", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/axis", - "requirements": ["axis==43"], + "requirements": ["axis==44"], "dhcp": [ { "hostname": "axis-00408c*", "macaddress": "00408C*" }, { "hostname": "axis-accc8e*", "macaddress": "ACCC8E*" }, diff --git a/requirements_all.txt b/requirements_all.txt index 3823a420587ad..44adb85ee0b49 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -313,7 +313,7 @@ av==8.0.3 # avion==0.10 # homeassistant.components.axis -axis==43 +axis==44 # homeassistant.components.azure_event_hub azure-eventhub==5.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 675422d49c80a..db0e4ae7704b0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -187,7 +187,7 @@ auroranoaa==0.0.2 av==8.0.3 # homeassistant.components.axis -axis==43 +axis==44 # homeassistant.components.azure_event_hub azure-eventhub==5.1.0 From a5dfbf9c44d3fc585c05bafdb8efbd9d71367ce2 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Fri, 2 Apr 2021 00:04:54 +0000 Subject: [PATCH 0028/1317] [ci skip] Translation update --- .../components/adguard/translations/ca.json | 4 +- .../components/adguard/translations/it.json | 4 +- .../components/adguard/translations/ko.json | 4 +- .../components/adguard/translations/ru.json | 2 +- .../components/almond/translations/ca.json | 4 +- .../components/almond/translations/it.json | 4 +- .../components/almond/translations/ko.json | 4 +- .../components/almond/translations/ru.json | 2 +- .../components/arcam_fmj/translations/ru.json | 2 +- .../components/deconz/translations/ca.json | 4 +- .../components/deconz/translations/it.json | 4 +- .../components/deconz/translations/ko.json | 4 +- .../components/deconz/translations/ru.json | 2 +- .../emulated_roku/translations/ru.json | 4 +- .../google_travel_time/translations/ca.json | 38 +++++++++++++++++++ .../google_travel_time/translations/it.json | 38 +++++++++++++++++++ .../google_travel_time/translations/ko.json | 38 +++++++++++++++++++ .../google_travel_time/translations/ru.json | 38 +++++++++++++++++++ .../translations/zh-Hant.json | 38 +++++++++++++++++++ .../components/homekit/translations/ca.json | 2 +- .../components/homekit/translations/it.json | 2 +- .../components/homekit/translations/ko.json | 2 +- .../components/homekit/translations/ru.json | 2 +- .../components/kodi/translations/ru.json | 4 +- .../components/mqtt/translations/ca.json | 4 +- .../components/mqtt/translations/it.json | 4 +- .../components/mqtt/translations/ko.json | 4 +- .../components/mqtt/translations/ru.json | 2 +- .../components/roomba/translations/ko.json | 4 +- .../components/zha/translations/ko.json | 1 + 30 files changed, 230 insertions(+), 39 deletions(-) create mode 100644 homeassistant/components/google_travel_time/translations/ca.json create mode 100644 homeassistant/components/google_travel_time/translations/it.json create mode 100644 homeassistant/components/google_travel_time/translations/ko.json create mode 100644 homeassistant/components/google_travel_time/translations/ru.json create mode 100644 homeassistant/components/google_travel_time/translations/zh-Hant.json diff --git a/homeassistant/components/adguard/translations/ca.json b/homeassistant/components/adguard/translations/ca.json index 8c8086813aace..0c7057a67eef4 100644 --- a/homeassistant/components/adguard/translations/ca.json +++ b/homeassistant/components/adguard/translations/ca.json @@ -9,8 +9,8 @@ }, "step": { "hassio_confirm": { - "description": "Vols configurar Home Assistant perqu\u00e8 es connecti amb l'AdGuard Home proporcionat pel complement de Hass.io: {addon}?", - "title": "AdGuard Home via complement de Hass.io" + "description": "Vols configurar Home Assistant perqu\u00e8 es connecti amb l'AdGuard Home proporcionat pel complement: {addon}?", + "title": "AdGuard Home via complement de Home Assistant" }, "user": { "data": { diff --git a/homeassistant/components/adguard/translations/it.json b/homeassistant/components/adguard/translations/it.json index 3758e0935473b..cbafb68a83450 100644 --- a/homeassistant/components/adguard/translations/it.json +++ b/homeassistant/components/adguard/translations/it.json @@ -9,8 +9,8 @@ }, "step": { "hassio_confirm": { - "description": "Vuoi configurare Home Assistant per connettersi ad AdGuard Home fornito dal componente aggiuntivo di Hass.io: {addon}?", - "title": "AdGuard Home tramite il componente aggiuntivo di Hass.io" + "description": "Vuoi configurare Home Assistant per connettersi ad AdGuard Home fornito dal componente aggiuntivo: {addon}?", + "title": "AdGuard Home tramite il componente aggiuntivo di Home Assistant" }, "user": { "data": { diff --git a/homeassistant/components/adguard/translations/ko.json b/homeassistant/components/adguard/translations/ko.json index 8564ba19f3c8c..6b1917cf73b2c 100644 --- a/homeassistant/components/adguard/translations/ko.json +++ b/homeassistant/components/adguard/translations/ko.json @@ -9,8 +9,8 @@ }, "step": { "hassio_confirm": { - "description": "Hass.io {addon} \uc560\ub4dc\uc628\uc73c\ub85c AdGuard Home\uc5d0 \uc5f0\uacb0\ud558\ub3c4\ub85d Home Assistant\ub97c \uad6c\uc131\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "title": "Hass.io \uc560\ub4dc\uc628\uc758 AdGuard Home" + "description": "{addon} \uc560\ub4dc\uc628\uc5d0\uc11c \uc81c\uacf5\ud558\ub294 AdGuard Home\uc5d0 \uc5f0\uacb0\ud558\ub3c4\ub85d Home Assistant\ub97c \uad6c\uc131\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "Home Assistant \uc560\ub4dc\uc628\uc758 AdGuard Home" }, "user": { "data": { diff --git a/homeassistant/components/adguard/translations/ru.json b/homeassistant/components/adguard/translations/ru.json index 97dc6505c3bf5..480204da0a185 100644 --- a/homeassistant/components/adguard/translations/ru.json +++ b/homeassistant/components/adguard/translations/ru.json @@ -9,7 +9,7 @@ }, "step": { "hassio_confirm": { - "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a AdGuard Home (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Home Assistant \"{addon}\")?", + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a AdGuard Home (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \"{addon}\")?", "title": "AdGuard Home (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Home Assistant)" }, "user": { diff --git a/homeassistant/components/almond/translations/ca.json b/homeassistant/components/almond/translations/ca.json index 3f9ce6353385b..c4dcc2e38e2c4 100644 --- a/homeassistant/components/almond/translations/ca.json +++ b/homeassistant/components/almond/translations/ca.json @@ -8,8 +8,8 @@ }, "step": { "hassio_confirm": { - "description": "Vols configurar Home Assistant perqu\u00e8 es connecti amb Almond proporcionat pel complement de Hass.io: {addon}?", - "title": "Almond via complement de Hass.io" + "description": "Vols configurar Home Assistant perqu\u00e8 es connecti amb Almond proporcionat pel complement: {addon}?", + "title": "Almond via complement de Home Assistant" }, "pick_implementation": { "title": "Selecciona el m\u00e8tode d'autenticaci\u00f3" diff --git a/homeassistant/components/almond/translations/it.json b/homeassistant/components/almond/translations/it.json index 7a41f00437b99..58eadad0d803d 100644 --- a/homeassistant/components/almond/translations/it.json +++ b/homeassistant/components/almond/translations/it.json @@ -8,8 +8,8 @@ }, "step": { "hassio_confirm": { - "description": "Vuoi configurare Home Assistant per connettersi a Almond fornito dal componente aggiuntivo di Hass.io: {addon}?", - "title": "Almond tramite il componente aggiuntivo di Hass.io" + "description": "Vuoi configurare Home Assistant per connettersi a Almond fornito dal componente aggiuntivo: {addon}?", + "title": "Almond tramite il componente aggiuntivo di Home Assistant" }, "pick_implementation": { "title": "Scegli il metodo di autenticazione" diff --git a/homeassistant/components/almond/translations/ko.json b/homeassistant/components/almond/translations/ko.json index cd9d4d67874f7..d18f5c914cccb 100644 --- a/homeassistant/components/almond/translations/ko.json +++ b/homeassistant/components/almond/translations/ko.json @@ -8,8 +8,8 @@ }, "step": { "hassio_confirm": { - "description": "Hass.io {addon} \uc560\ub4dc\uc628\uc73c\ub85c Almond\uc5d0 \uc5f0\uacb0\ud558\ub3c4\ub85d Home Assistant\ub97c \uad6c\uc131\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "title": "Hass.io \uc560\ub4dc\uc628\uc758 Almond" + "description": "{addon} \uc560\ub4dc\uc628\uc5d0\uc11c \uc81c\uacf5\ud558\ub294 Almond\uc5d0 \uc5f0\uacb0\ud558\ub3c4\ub85d Home Assistant\ub97c \uad6c\uc131\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "Home Assistant \uc560\ub4dc\uc628\uc758 Almond" }, "pick_implementation": { "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd\ud558\uae30" diff --git a/homeassistant/components/almond/translations/ru.json b/homeassistant/components/almond/translations/ru.json index e671651f65de1..62b5df122a176 100644 --- a/homeassistant/components/almond/translations/ru.json +++ b/homeassistant/components/almond/translations/ru.json @@ -8,7 +8,7 @@ }, "step": { "hassio_confirm": { - "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a Almond (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Home Assistant \"{addon}\")?", + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a Almond (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \"{addon}\")?", "title": "Almond (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Home Assistant)" }, "pick_implementation": { diff --git a/homeassistant/components/arcam_fmj/translations/ru.json b/homeassistant/components/arcam_fmj/translations/ru.json index bdd59b39067e6..8b3c309274553 100644 --- a/homeassistant/components/arcam_fmj/translations/ru.json +++ b/homeassistant/components/arcam_fmj/translations/ru.json @@ -21,7 +21,7 @@ }, "device_automation": { "trigger_type": { - "turn_on": "\u0437\u0430\u043f\u0440\u043e\u0448\u0435\u043d\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 {entity_name}" + "turn_on": "\u0417\u0430\u043f\u0440\u043e\u0448\u0435\u043d\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 {entity_name}" } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/translations/ca.json b/homeassistant/components/deconz/translations/ca.json index 5957dc88c033c..d5729f73444b8 100644 --- a/homeassistant/components/deconz/translations/ca.json +++ b/homeassistant/components/deconz/translations/ca.json @@ -14,8 +14,8 @@ "flow_title": "Passarel\u00b7la d'enlla\u00e7 deCONZ Zigbee ({host})", "step": { "hassio_confirm": { - "description": "Vols configurar Home Assistant perqu\u00e8 es connecti amb la passarel\u00b7la deCONZ proporcionada pel complement de Hass.io {addon}?", - "title": "Passarel\u00b7la d'enlla\u00e7 deCONZ Zigbee via complement de Hass.io" + "description": "Vols configurar Home Assistant perqu\u00e8 es connecti amb la passarel\u00b7la deCONZ proporcionada pel complement {addon}?", + "title": "Passarel\u00b7la d'enlla\u00e7 deCONZ Zigbee via complement de Home Assistant" }, "link": { "description": "Desbloqueja la teva passarel\u00b7la d'enlla\u00e7 deCONZ per a registrar-te amb Home Assistant.\n\n1. V\u00e9s a la configuraci\u00f3 del sistema deCONZ -> Passarel\u00b7la -> Avan\u00e7at\n2. Prem el bot\u00f3 \"Autenticar applicaci\u00f3\"", diff --git a/homeassistant/components/deconz/translations/it.json b/homeassistant/components/deconz/translations/it.json index fd81ebad8cf6d..1c5d02de09078 100644 --- a/homeassistant/components/deconz/translations/it.json +++ b/homeassistant/components/deconz/translations/it.json @@ -14,8 +14,8 @@ "flow_title": "Gateway Zigbee deCONZ ({host})", "step": { "hassio_confirm": { - "description": "Vuoi configurare Home Assistant per connettersi al gateway deCONZ fornito dal componente aggiuntivo di Hass.io: {addon}?", - "title": "Gateway deCONZ Zigbee tramite il componente aggiuntivo di Hass.io" + "description": "Vuoi configurare Home Assistant per connettersi al gateway deCONZ fornito dal componente aggiuntivo: {addon}?", + "title": "Gateway deCONZ Zigbee tramite il componente aggiuntivo di Home Assistant" }, "link": { "description": "Sblocca il tuo gateway deCONZ per registrarti con Home Assistant.\n\n1. Vai a Impostazioni deCONZ -> Gateway -> Avanzate\n2. Premere il pulsante \"Autentica app\"", diff --git a/homeassistant/components/deconz/translations/ko.json b/homeassistant/components/deconz/translations/ko.json index 811b2400ddd49..30597cf3af6ff 100644 --- a/homeassistant/components/deconz/translations/ko.json +++ b/homeassistant/components/deconz/translations/ko.json @@ -14,8 +14,8 @@ "flow_title": "deCONZ Zigbee \uac8c\uc774\ud2b8\uc6e8\uc774 ({host})", "step": { "hassio_confirm": { - "description": "Hass.io {addon} \uc560\ub4dc\uc628\uc5d0\uc11c \uc81c\uacf5\ub41c deCONZ \uac8c\uc774\ud2b8\uc6e8\uc774\uc5d0 \uc5f0\uacb0\ud558\ub3c4\ub85d Home Assistant\ub97c \uad6c\uc131\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "title": "Hass.io \uc560\ub4dc\uc628\uc758 deCONZ Zigbee \uac8c\uc774\ud2b8\uc6e8\uc774" + "description": "{addon} \uc560\ub4dc\uc628\uc5d0\uc11c \uc81c\uacf5\ud558\ub294 deCONZ \uac8c\uc774\ud2b8\uc6e8\uc774\uc5d0 \uc5f0\uacb0\ud558\ub3c4\ub85d Home Assistant\ub97c \uad6c\uc131\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "Home Assistant \uc560\ub4dc\uc628\uc758 deCONZ Zigbee \uac8c\uc774\ud2b8\uc6e8\uc774" }, "link": { "description": "Home Assistant\uc5d0 \ub4f1\ub85d\ud558\ub824\uba74 deCONZ \uac8c\uc774\ud2b8\uc6e8\uc774\ub97c \uc7a0\uae08 \ud574\uc81c\ud574\uc8fc\uc138\uc694.\n\n 1. deCONZ \uc124\uc815 -> \uac8c\uc774\ud2b8\uc6e8\uc774 -> \uace0\uae09\uc73c\ub85c \uc774\ub3d9\ud574\uc8fc\uc138\uc694\n 2. \"\uc571 \uc778\uc99d\ud558\uae30\" \ubc84\ud2bc\uc744 \ub20c\ub7ec\uc8fc\uc138\uc694", diff --git a/homeassistant/components/deconz/translations/ru.json b/homeassistant/components/deconz/translations/ru.json index f22975530d832..7a78c671f5ffd 100644 --- a/homeassistant/components/deconz/translations/ru.json +++ b/homeassistant/components/deconz/translations/ru.json @@ -14,7 +14,7 @@ "flow_title": "\u0428\u043b\u044e\u0437 Zigbee deCONZ ({host})", "step": { "hassio_confirm": { - "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a deCONZ (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Home Assistant \"{addon}\")?", + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a deCONZ (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \"{addon}\")?", "title": "Zigbee \u0448\u043b\u044e\u0437 deCONZ (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Home Assistant)" }, "link": { diff --git a/homeassistant/components/emulated_roku/translations/ru.json b/homeassistant/components/emulated_roku/translations/ru.json index 2d4c4a7d93533..f0094930f839a 100644 --- a/homeassistant/components/emulated_roku/translations/ru.json +++ b/homeassistant/components/emulated_roku/translations/ru.json @@ -13,9 +13,9 @@ "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", "upnp_bind_multicast": "\u041f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c multicast (True/False)" }, - "title": "EmulatedRoku" + "title": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0441\u0435\u0440\u0432\u0435\u0440\u0430" } } }, - "title": "Emulated Roku" + "title": "\u042d\u043c\u0443\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 Roku" } \ No newline at end of file diff --git a/homeassistant/components/google_travel_time/translations/ca.json b/homeassistant/components/google_travel_time/translations/ca.json new file mode 100644 index 0000000000000..0edced2969040 --- /dev/null +++ b/homeassistant/components/google_travel_time/translations/ca.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "La ubicaci\u00f3 ja est\u00e0 configurada" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "step": { + "user": { + "data": { + "api_key": "Clau API", + "destination": "Destinaci\u00f3", + "origin": "Origen" + }, + "description": "Quan especifiquis l'origen i la destinaci\u00f3, pots proporcionar m\u00e9s d'una ubicaci\u00f3 (les has de separar pel car\u00e0cter 'pipe'); poden ser en forma d'adre\u00e7a, coordenades de latitud/longitud o un identificador de lloc de Google. En especificar la ubicaci\u00f3 mitjan\u00e7ant un ID de lloc de Google, l'identificador ha de tenir el prefix `place_id:`." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "avoid": "Evita", + "language": "Idioma", + "mode": "Mode de transport", + "time": "Temps", + "time_type": "Tipus de temps", + "transit_mode": "Tipus de transport", + "transit_routing_preference": "Prefer\u00e8ncia de rutes de tr\u00e0nsit", + "units": "Unitats" + }, + "description": "Opcionalment, pots especificar una hora de sortida o una hora d'arribada. Si especifiques una hora de sortida, pots introduir `ara`, una marca de temps Unix o una cadena de temps de 24 hores com per exemple `08:00:00`. Si especifiques una hora d'arribada, pots utilitzar els mateixos formats excepte `ara`." + } + } + }, + "title": "Temps de viatge de Google Maps" +} \ No newline at end of file diff --git a/homeassistant/components/google_travel_time/translations/it.json b/homeassistant/components/google_travel_time/translations/it.json new file mode 100644 index 0000000000000..426e7f96c3c5c --- /dev/null +++ b/homeassistant/components/google_travel_time/translations/it.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "La posizione \u00e8 gi\u00e0 configurata" + }, + "error": { + "cannot_connect": "Impossibile connettersi" + }, + "step": { + "user": { + "data": { + "api_key": "Chiave API", + "destination": "Destinazione", + "origin": "Origine" + }, + "description": "Quando specifichi l'origine e la destinazione, puoi fornire una o pi\u00f9 posizioni separate dal carattere barra verticale, sotto forma di un indirizzo, coordinate di latitudine/longitudine o un ID luogo di Google. Quando si specifica la posizione utilizzando un ID luogo di Google, l'ID deve essere preceduto da \"place_id:\"." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "avoid": "Evitare", + "language": "Lingua", + "mode": "Modalit\u00e0 di viaggio", + "time": "Ora", + "time_type": "Tipo di ora", + "transit_mode": "Modalit\u00e0 di transito", + "transit_routing_preference": "Preferenza percorso di transito", + "units": "Unit\u00e0" + }, + "description": "Facoltativamente, \u00e8 possibile specificare un orario di partenza o un orario di arrivo. Se si specifica un orario di partenza, \u00e8 possibile inserire \"now\", un timestamp Unix o una stringa di 24 ore come \"08: 00: 00\". Se si specifica un'ora di arrivo, \u00e8 possibile utilizzare un timestamp Unix o una stringa di 24 ore come \"08: 00: 00\"" + } + } + }, + "title": "Tempo di viaggio di Google Maps" +} \ No newline at end of file diff --git a/homeassistant/components/google_travel_time/translations/ko.json b/homeassistant/components/google_travel_time/translations/ko.json new file mode 100644 index 0000000000000..41873626ea561 --- /dev/null +++ b/homeassistant/components/google_travel_time/translations/ko.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "api_key": "API \ud0a4", + "destination": "\ubaa9\uc801\uc9c0", + "origin": "\ucd9c\ubc1c\uc9c0" + }, + "description": "\ucd9c\ubc1c\uc9c0\uc640 \ubaa9\uc801\uc9c0\ub97c \uc9c0\uc815\ud560 \ub54c \uc8fc\uc18c, \uc704\ub3c4/\uacbd\ub3c4 \uc88c\ud45c \ub610\ub294 Google Place ID \ud615\uc2dd\uc73c\ub85c \ud30c\uc774\ud504 \ubb38\uc790(|)\ub85c \uad6c\ubd84\ub41c \ud558\ub098 \uc774\uc0c1\uc758 \uc704\uce58\ub97c \uc785\ub825\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. Google Place ID\ub97c \uc0ac\uc6a9\ud558\uc5ec \uc704\uce58\ub97c \uc9c0\uc815\ud560 \ub54c\ub294 ID \uc55e\uc5d0 `place_id:`\ub97c \ubd99\uc5ec\uc57c \ud569\ub2c8\ub2e4." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "avoid": "\ud68c\ud53c", + "language": "\uc5b8\uc5b4", + "mode": "\uae38\ucc3e\uae30 \ubaa8\ub4dc", + "time": "\uc2dc\uac04", + "time_type": "\uc2dc\uac04 \uc720\ud615", + "transit_mode": "\ub300\uc911\uad50\ud1b5 \ubaa8\ub4dc", + "transit_routing_preference": "\ub300\uc911\uad50\ud1b5 \uacbd\ub85c \uae30\ubcf8 \uc124\uc815", + "units": "\ub2e8\uc704" + }, + "description": "\uc120\ud0dd\uc801\uc73c\ub85c \ucd9c\ubc1c \uc2dc\uac04 \ub610\ub294 \ub3c4\ucc29 \uc2dc\uac04\uc744 \uc9c0\uc815\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \ucd9c\ubc1c \uc2dc\uac04\uc744 \uc9c0\uc815\ud558\ub294 \uacbd\uc6b0 'now' \ub610\ub294 Unix \ud0c0\uc784\uc2a4\ud0ec\ud504 \ub610\ub294 '08:00:00'\uacfc \uac19\uc740 24\uc2dc\uac04 \ubb38\uc790\uc5f4\uc744 \uc785\ub825\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \ub3c4\ucc29 \uc2dc\uac04\uc744 \uc9c0\uc815\ud558\ub294 \uacbd\uc6b0 Unix \ud0c0\uc784\uc2a4\ud0ec\ud504 \ub610\ub294 '08:00:00'\uacfc \uac19\uc740 24\uc2dc\uac04 \ubb38\uc790\uc5f4\uc744 \uc0ac\uc6a9\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + } + } + }, + "title": "Google Maps \uc774\ub3d9 \uc2dc\uac04" +} \ No newline at end of file diff --git a/homeassistant/components/google_travel_time/translations/ru.json b/homeassistant/components/google_travel_time/translations/ru.json new file mode 100644 index 0000000000000..1198c3d62f9b0 --- /dev/null +++ b/homeassistant/components/google_travel_time/translations/ru.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "destination": "\u041f\u0443\u043d\u043a\u0442 \u043d\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f", + "origin": "\u041f\u0443\u043d\u043a\u0442 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f" + }, + "description": "\u041f\u0440\u0438 \u0443\u043a\u0430\u0437\u0430\u043d\u0438\u0438 \u043f\u0443\u043d\u043a\u0442\u043e\u0432 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u0438 \u043d\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f, \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0443\u043a\u0430\u0437\u0430\u0442\u044c \u043e\u0434\u043d\u043e \u0438\u043b\u0438 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0439, \u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u0445 \u0432\u0435\u0440\u0442\u0438\u043a\u0430\u043b\u044c\u043d\u043e\u0439 \u0447\u0435\u0440\u0442\u043e\u0439, \u0432 \u0432\u0438\u0434\u0435 \u0430\u0434\u0440\u0435\u0441\u0430, \u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442 \u0448\u0438\u0440\u043e\u0442\u044b/\u0434\u043e\u043b\u0433\u043e\u0442\u044b \u0438\u043b\u0438 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u0430 \u043c\u0435\u0441\u0442\u0430 Google. \u041f\u0440\u0438 \u0443\u043a\u0430\u0437\u0430\u043d\u0438\u0438 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u0430 \u043c\u0435\u0441\u0442\u0430 Google, \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0434\u043e\u043b\u0436\u0435\u043d \u043d\u0430\u0447\u0438\u043d\u0430\u0442\u044c\u0441\u044f \u0441 \u043f\u0440\u0435\u0444\u0438\u043a\u0441\u0430 `place_id:`." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "avoid": "\u0418\u0437\u0431\u0435\u0433\u0430\u0442\u044c", + "language": "\u042f\u0437\u044b\u043a", + "mode": "\u0421\u043f\u043e\u0441\u043e\u0431 \u043f\u0435\u0440\u0435\u0434\u0432\u0438\u0436\u0435\u043d\u0438\u044f", + "time": "\u0412\u0440\u0435\u043c\u044f", + "time_type": "\u0422\u0438\u043f \u0432\u0440\u0435\u043c\u0435\u043d\u0438", + "transit_mode": "\u0420\u0435\u0436\u0438\u043c \u0442\u0440\u0430\u043d\u0437\u0438\u0442\u0430", + "transit_routing_preference": "\u041f\u0440\u0435\u0434\u043f\u043e\u0447\u0442\u0435\u043d\u0438\u0435 \u043f\u043e \u0442\u0440\u0430\u043d\u0437\u0438\u0442\u043d\u043e\u043c\u0443 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0443", + "units": "\u0415\u0434\u0438\u043d\u0438\u0446\u044b \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f" + }, + "description": "\u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u0443\u043a\u0430\u0437\u0430\u0442\u044c \u0432\u0440\u0435\u043c\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u0438\u043b\u0438 \u0432\u0440\u0435\u043c\u044f \u043f\u0440\u0438\u0431\u044b\u0442\u0438\u044f. \u041f\u0440\u0438 \u0443\u043a\u0430\u0437\u0430\u043d\u0438\u0438 \u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u043c\u043e\u0436\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0432\u0440\u0435\u043c\u044f ('now'), Unix-\u0432\u0440\u0435\u043c\u044f \u0438\u043b\u0438 \u0441\u0442\u0440\u043e\u043a\u0443 \u0432 24-\u0447\u0430\u0441\u043e\u0432\u043e\u043c \u0444\u043e\u0440\u043c\u0430\u0442\u0435 \u0438\u0441\u0447\u0438\u0441\u043b\u0435\u043d\u0438\u044f, \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440 '08:00:00'. \u041f\u0440\u0438 \u0443\u043a\u0430\u0437\u0430\u043d\u0438\u0438 \u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u043f\u0440\u0438\u0431\u044b\u0442\u0438\u044f \u043c\u043e\u0436\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c Unix-\u0432\u0440\u0435\u043c\u044f \u0438\u043b\u0438 \u0441\u0442\u0440\u043e\u043a\u0443 \u0432 24-\u0447\u0430\u0441\u043e\u0432\u043e\u043c \u0444\u043e\u0440\u043c\u0430\u0442\u0435 \u0438\u0441\u0447\u0438\u0441\u043b\u0435\u043d\u0438\u044f, \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440 '08:00:00'." + } + } + }, + "title": "Google Maps Travel Time" +} \ No newline at end of file diff --git a/homeassistant/components/google_travel_time/translations/zh-Hant.json b/homeassistant/components/google_travel_time/translations/zh-Hant.json new file mode 100644 index 0000000000000..e834d3b2f36a7 --- /dev/null +++ b/homeassistant/components/google_travel_time/translations/zh-Hant.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "\u5ea7\u6a19\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "step": { + "user": { + "data": { + "api_key": "API \u5bc6\u9470", + "destination": "\u76ee\u7684\u5730", + "origin": "\u51fa\u767c\u5730" + }, + "description": "\u7576\u6307\u5b9a\u51fa\u767c\u5730\u8207\u76ee\u7684\u5730\u6642\uff0c\u53ef\u4ee5\u5305\u542b\u4e00\u500b\u6216\u4ee5\u4e0a\u7684\u4f4d\u7f6e\u3001\u4f9d\u5730\u5740\u683c\u5f0f\u3001\u7d93\u7def\u5ea6\u6216\u8005 Goolge Place ID\uff0c\u4ee5\u8c4e\u7dda\u5206\u9694\u9032\u884c\u3002\u7576\u4ee5 Google Place ID \u6307\u5b9a\u4f4d\u7f6e\u6642\uff0c\u5fc5\u9808\u5305\u542b\u683c\u5f0f\u70ba `place_id:`\u3002" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "avoid": "\u8ff4\u907f", + "language": "\u8a9e\u8a00", + "mode": "\u65c5\u884c\u6a21\u5f0f", + "time": "\u6642\u9593", + "time_type": "\u6642\u9593\u985e\u578b", + "transit_mode": "\u79fb\u52d5\u6a21\u5f0f", + "transit_routing_preference": "\u504f\u597d\u79fb\u52d5\u8def\u7dda", + "units": "\u55ae\u4f4d" + }, + "description": "\u53ef\u9078\u9805\u6307\u5b9a\u51fa\u767c\u6642\u9593\u6216\u62b5\u9054\u6642\u9593\u3002\u5047\u5982\u6b32\u6307\u5b9a\u51fa\u767c\u6642\u9593\u3001\u53ef\u4ee5\u8f38\u5165\u70ba `\u7acb\u5373\u51fa\u767c`\u3001Unix \u6642\u9593\u6a19\u8a18\u6216 24 \u5c0f\u6642\u6642\u9593\u5236\uff0c\u5982 `08:00:00`\u3002\u5047\u5982\u6b32\u6307\u5b9a\u62b5\u9054\u6642\u9593\uff0c\u53ef\u4f7f\u7528 Unix \u6642\u9593\u6a19\u8a18\u6216 24 \u5c0f\u6642\u6642\u9593\u5236\u5982 `08:00:00`" + } + } + }, + "title": "Google Maps \u65c5\u7a0b\u6642\u9593" +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/ca.json b/homeassistant/components/homekit/translations/ca.json index 1ad6d63a0ffe8..8093cb1792f27 100644 --- a/homeassistant/components/homekit/translations/ca.json +++ b/homeassistant/components/homekit/translations/ca.json @@ -55,7 +55,7 @@ "entities": "Entitats", "mode": "Mode" }, - "description": "Tria les entitats a incloure. En mode accessori, nom\u00e9s s'inclou una sola entitat. En mode enlla\u00e7 inclusiu, s'exposaran totes les entitats del domini tret de que se'n seleccionin algunes en concret. En mode enlla\u00e7 excusiu, s'inclouran totes les entitats del domini excepte les entitats excloses. Per obtenir el millor rendiment, es crea una inst\u00e0ncia HomeKit per a cada repoductor multim\u00e8dia/TV i c\u00e0mera.", + "description": "Tria les entitats a incloure. En mode accessori, nom\u00e9s s'inclou una sola entitat. En mode enlla\u00e7 inclusiu, s'inclouran totes les entitats del domini tret de que se'n seleccionin algunes en concret. En mode enlla\u00e7 excusiu, s'inclouran totes les entitats del domini excepte les entitats excloses. Per obtenir el millor rendiment, es crea una inst\u00e0ncia HomeKit per a cada repoductor multim\u00e8dia/TV i c\u00e0mera.", "title": "Selecciona les entitats a incloure" }, "init": { diff --git a/homeassistant/components/homekit/translations/it.json b/homeassistant/components/homekit/translations/it.json index f92c61d493f85..c61aececec7e9 100644 --- a/homeassistant/components/homekit/translations/it.json +++ b/homeassistant/components/homekit/translations/it.json @@ -55,7 +55,7 @@ "entities": "Entit\u00e0", "mode": "Modalit\u00e0" }, - "description": "Scegliere le entit\u00e0 da includere. In modalit\u00e0 accessorio, \u00e8 inclusa una sola entit\u00e0. In modalit\u00e0 di inclusione bridge, tutte le entit\u00e0 nel dominio saranno incluse, a meno che non siano selezionate entit\u00e0 specifiche. In modalit\u00e0 di esclusione bridge, tutte le entit\u00e0 nel dominio saranno incluse, ad eccezione delle entit\u00e0 escluse. Per prestazioni ottimali, sar\u00e0 creata una HomeKit separata accessoria per ogni lettore multimediale, TV e videocamera.", + "description": "Scegliere le entit\u00e0 da includere. In modalit\u00e0 accessorio, \u00e8 inclusa una sola entit\u00e0. In modalit\u00e0 di inclusione bridge, tutte le entit\u00e0 nel dominio saranno incluse, a meno che non siano selezionate entit\u00e0 specifiche. In modalit\u00e0 di esclusione bridge, tutte le entit\u00e0 nel dominio saranno incluse, ad eccezione delle entit\u00e0 escluse. Per prestazioni ottimali, sar\u00e0 creata una HomeKit separata accessoria per ogni lettore multimediale TV, telecomando basato sulle attivit\u00e0, serratura e videocamera.", "title": "Seleziona le entit\u00e0 da includere" }, "init": { diff --git a/homeassistant/components/homekit/translations/ko.json b/homeassistant/components/homekit/translations/ko.json index 274898425cb90..b9b04aec0a7fd 100644 --- a/homeassistant/components/homekit/translations/ko.json +++ b/homeassistant/components/homekit/translations/ko.json @@ -55,7 +55,7 @@ "entities": "\uad6c\uc131\uc694\uc18c", "mode": "\ubaa8\ub4dc" }, - "description": "\ud3ec\ud568\ud560 \uad6c\uc131\uc694\uc18c\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694. \uc561\uc138\uc11c\ub9ac \ubaa8\ub4dc\uc5d0\uc11c\ub294 \ub2e8\uc77c \uad6c\uc131\uc694\uc18c\ub9cc \ud3ec\ud568\ub429\ub2c8\ub2e4. \ube0c\ub9ac\uc9c0 \ud3ec\ud568 \ubaa8\ub4dc\uc5d0\uc11c\ub294 \ud2b9\uc815 \uad6c\uc131\uc694\uc18c\ub97c \uc120\ud0dd\ud558\uc9c0 \uc54a\uc73c\uba74 \ub3c4\uba54\uc778\uc758 \ubaa8\ub4e0 \uad6c\uc131\uc694\uc18c\uac00 \ud3ec\ud568\ub429\ub2c8\ub2e4. \ube0c\ub9ac\uc9c0 \uc81c\uc678 \ubaa8\ub4dc\uc5d0\uc11c\ub294 \uc81c\uc678\ub41c \uad6c\uc131\uc694\uc18c\ub97c \ube80 \ub3c4\uba54\uc778\uc758 \ub098\uba38\uc9c0 \uad6c\uc131\uc694\uc18c\uac00 \ud3ec\ud568\ub429\ub2c8\ub2e4. \ucd5c\uc0c1\uc758 \uc131\ub2a5\uc744 \uc704\ud574 \uac01 TV \ubbf8\ub514\uc5b4 \ud50c\ub808\uc774\uc5b4\uc640 \uce74\uba54\ub77c\ub294 \ubcc4\ub3c4\uc758 HomeKit \uc561\uc138\uc11c\ub9ac\ub85c \uc0dd\uc131\ub429\ub2c8\ub2e4.", + "description": "\ud3ec\ud568\ud560 \uad6c\uc131\uc694\uc18c\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694. \uc561\uc138\uc11c\ub9ac \ubaa8\ub4dc\uc5d0\uc11c\ub294 \ub2e8\uc77c \uad6c\uc131\uc694\uc18c\ub9cc \ud3ec\ud568\ub429\ub2c8\ub2e4. \ube0c\ub9ac\uc9c0 \ud3ec\ud568 \ubaa8\ub4dc\uc5d0\uc11c\ub294 \ud2b9\uc815 \uad6c\uc131\uc694\uc18c\ub97c \uc120\ud0dd\ud558\uc9c0 \uc54a\ub294 \ud55c \ub3c4\uba54\uc778\uc758 \ubaa8\ub4e0 \uad6c\uc131\uc694\uc18c\uac00 \ud3ec\ud568\ub429\ub2c8\ub2e4. \ube0c\ub9ac\uc9c0 \uc81c\uc678 \ubaa8\ub4dc\uc5d0\uc11c\ub294 \uc81c\uc678\ub41c \uad6c\uc131\uc694\uc18c\ub97c \uc81c\uc678\ud55c \ub3c4\uba54\uc778\uc758 \ub098\uba38\uc9c0 \uad6c\uc131\uc694\uc18c\uac00 \ud3ec\ud568\ub429\ub2c8\ub2e4. \ucd5c\uc0c1\uc758 \uc131\ub2a5\uc744 \uc704\ud574 \uac01 TV \ubbf8\ub514\uc5b4 \ud50c\ub808\uc774\uc5b4\uc640 \ud65c\ub3d9 \uae30\ubc18 \ub9ac\ubaa8\ucf58, \uc7a0\uae08\uae30\uae30, \uce74\uba54\ub77c\ub294 \ubcc4\ub3c4\uc758 HomeKit \uc561\uc138\uc11c\ub9ac\ub85c \uc0dd\uc131\ub429\ub2c8\ub2e4.", "title": "\ud3ec\ud568\ud560 \ub3c4\uba54\uc778 \uc120\ud0dd\ud558\uae30" }, "init": { diff --git a/homeassistant/components/homekit/translations/ru.json b/homeassistant/components/homekit/translations/ru.json index ffc0ac34eae4a..81199b2971ce1 100644 --- a/homeassistant/components/homekit/translations/ru.json +++ b/homeassistant/components/homekit/translations/ru.json @@ -55,7 +55,7 @@ "entities": "\u041e\u0431\u044a\u0435\u043a\u0442\u044b", "mode": "\u0420\u0435\u0436\u0438\u043c" }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0438 \u0432 HomeKit. \u0412 \u0440\u0435\u0436\u0438\u043c\u0435 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u0430 \u043c\u043e\u0436\u043d\u043e \u043f\u0435\u0440\u0435\u0434\u0430\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u043e\u0431\u044a\u0435\u043a\u0442. \u0412 \u0440\u0435\u0436\u0438\u043c\u0435 \u043c\u043e\u0441\u0442\u0430 \u0431\u0443\u0434\u0443\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u044b \u0432\u0441\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b, \u043f\u0440\u0438\u043d\u0430\u0434\u043b\u0435\u0436\u0430\u0449\u0438\u0435 \u0434\u043e\u043c\u0435\u043d\u0443, \u0435\u0441\u043b\u0438 \u043d\u0435 \u0432\u044b\u0431\u0440\u0430\u043d\u044b \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b. \u0412 \u0440\u0435\u0436\u0438\u043c\u0435 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u0431\u0443\u0434\u0443\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u044b \u0432\u0441\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b, \u043f\u0440\u0438\u043d\u0430\u0434\u043b\u0435\u0436\u0430\u0449\u0438\u0435 \u0434\u043e\u043c\u0435\u043d\u0443, \u043a\u0440\u043e\u043c\u0435 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044b\u0445. \u0414\u043b\u044f \u0443\u043b\u0443\u0447\u0448\u0435\u043d\u0438\u044f \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u0438 \u043c\u0435\u0434\u0438\u0430\u043f\u043b\u0435\u0435\u0440\u044b \u0438 \u043a\u0430\u043c\u0435\u0440\u044b \u0431\u0443\u0434\u0443\u0442 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b \u043a\u0430\u043a \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u0430\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0432 \u0440\u0435\u0436\u0438\u043c\u0435 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u0430.", + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0438 \u0432 HomeKit. \u0412 \u0440\u0435\u0436\u0438\u043c\u0435 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u0430 \u043c\u043e\u0436\u043d\u043e \u043f\u0435\u0440\u0435\u0434\u0430\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u043e\u0431\u044a\u0435\u043a\u0442. \u0412 \u0440\u0435\u0436\u0438\u043c\u0435 \u043c\u043e\u0441\u0442\u0430 \u0431\u0443\u0434\u0443\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u044b \u0432\u0441\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b, \u043f\u0440\u0438\u043d\u0430\u0434\u043b\u0435\u0436\u0430\u0449\u0438\u0435 \u0434\u043e\u043c\u0435\u043d\u0443, \u0435\u0441\u043b\u0438 \u043d\u0435 \u0432\u044b\u0431\u0440\u0430\u043d\u044b \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b. \u0412 \u0440\u0435\u0436\u0438\u043c\u0435 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u0431\u0443\u0434\u0443\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u044b \u0432\u0441\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b, \u043f\u0440\u0438\u043d\u0430\u0434\u043b\u0435\u0436\u0430\u0449\u0438\u0435 \u0434\u043e\u043c\u0435\u043d\u0443, \u043a\u0440\u043e\u043c\u0435 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044b\u0445. \u0414\u043b\u044f \u0443\u043b\u0443\u0447\u0448\u0435\u043d\u0438\u044f \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u0438 \u043c\u0435\u0434\u0438\u0430\u043f\u043b\u0435\u0435\u0440\u044b, \u043f\u0443\u043b\u044c\u0442\u044b \u0414\u0423 \u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0439, \u0437\u0430\u043c\u043a\u0438 \u0438 \u043a\u0430\u043c\u0435\u0440\u044b \u0431\u0443\u0434\u0443\u0442 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b \u043a\u0430\u043a \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u0430\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0432 \u0440\u0435\u0436\u0438\u043c\u0435 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u0430.", "title": "\u0412\u044b\u0431\u043e\u0440 \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432 \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0438 \u0432 HomeKit" }, "init": { diff --git a/homeassistant/components/kodi/translations/ru.json b/homeassistant/components/kodi/translations/ru.json index b6f7443f061fb..f0ec31654ddb1 100644 --- a/homeassistant/components/kodi/translations/ru.json +++ b/homeassistant/components/kodi/translations/ru.json @@ -43,8 +43,8 @@ }, "device_automation": { "trigger_type": { - "turn_off": "\u0437\u0430\u043f\u0440\u043e\u0448\u0435\u043d\u043e \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 {entity_name}", - "turn_on": "\u0437\u0430\u043f\u0440\u043e\u0448\u0435\u043d\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 {entity_name}" + "turn_off": "\u0417\u0430\u043f\u0440\u043e\u0448\u0435\u043d\u043e \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 {entity_name}", + "turn_on": "\u0417\u0430\u043f\u0440\u043e\u0448\u0435\u043d\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 {entity_name}" } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/ca.json b/homeassistant/components/mqtt/translations/ca.json index 23b7cd5dfa9f1..8a314f33d9448 100644 --- a/homeassistant/components/mqtt/translations/ca.json +++ b/homeassistant/components/mqtt/translations/ca.json @@ -21,8 +21,8 @@ "data": { "discovery": "Habilitar descobriment autom\u00e0tic" }, - "description": "Vols configurar Home Assistant perqu\u00e8 es connecti amb el broker MQTT proporcionat pel complement de Hass.io {addon}?", - "title": "Broker MQTT via complement de Hass.io" + "description": "Vols configurar Home Assistant perqu\u00e8 es connecti amb el broker MQTT proporcionat pel complement {addon}?", + "title": "Broker MQTT via complement de Home Assistant" } } }, diff --git a/homeassistant/components/mqtt/translations/it.json b/homeassistant/components/mqtt/translations/it.json index 845d0efabc787..a7cad033cdbed 100644 --- a/homeassistant/components/mqtt/translations/it.json +++ b/homeassistant/components/mqtt/translations/it.json @@ -21,8 +21,8 @@ "data": { "discovery": "Attiva l'individuazione" }, - "description": "Vuoi configurare Home Assistant per connettersi al broker MQTT fornito dal componente aggiuntivo di Hass.io: {addon}?", - "title": "Broker MQTT tramite il componente aggiuntivo di Hass.io" + "description": "Vuoi configurare Home Assistant per connettersi al broker MQTT fornito dal componente aggiuntivo: {addon}?", + "title": "Broker MQTT tramite il componente aggiuntivo di Home Assistant" } } }, diff --git a/homeassistant/components/mqtt/translations/ko.json b/homeassistant/components/mqtt/translations/ko.json index e7631c5805d37..dccd49b2ef3eb 100644 --- a/homeassistant/components/mqtt/translations/ko.json +++ b/homeassistant/components/mqtt/translations/ko.json @@ -21,8 +21,8 @@ "data": { "discovery": "\uae30\uae30 \uac80\uc0c9 \ud65c\uc131\ud654" }, - "description": "Hass.io {addon} \uc560\ub4dc\uc628\uc5d0\uc11c \uc81c\uacf5\ub41c MQTT \ube0c\ub85c\ucee4\uc5d0 \uc5f0\uacb0\ud558\ub3c4\ub85d Home Assistant\ub97c \uad6c\uc131\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "title": "Hass.io \uc560\ub4dc\uc628\uc758 MQTT \ube0c\ub85c\ucee4" + "description": "{addon} \uc560\ub4dc\uc628\uc5d0\uc11c \uc81c\uacf5\ud558\ub294 MQTT \ube0c\ub85c\ucee4\uc5d0 \uc5f0\uacb0\ud558\ub3c4\ub85d Home Assistant\ub97c \uad6c\uc131\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "Home Assistant \uc560\ub4dc\uc628\uc758 MQTT \ube0c\ub85c\ucee4" } } }, diff --git a/homeassistant/components/mqtt/translations/ru.json b/homeassistant/components/mqtt/translations/ru.json index 4357a0902c6ec..8ff5a13138cba 100644 --- a/homeassistant/components/mqtt/translations/ru.json +++ b/homeassistant/components/mqtt/translations/ru.json @@ -21,7 +21,7 @@ "data": { "discovery": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0430\u0432\u0442\u043e\u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432" }, - "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0431\u0440\u043e\u043a\u0435\u0440\u0443 MQTT (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Home Assistant \"{addon}\")?", + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0431\u0440\u043e\u043a\u0435\u0440\u0443 MQTT (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \"{addon}\")?", "title": "\u0411\u0440\u043e\u043a\u0435\u0440 MQTT (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Home Assistant)" } } diff --git a/homeassistant/components/roomba/translations/ko.json b/homeassistant/components/roomba/translations/ko.json index 5066225100bb9..4e2db24ba7384 100644 --- a/homeassistant/components/roomba/translations/ko.json +++ b/homeassistant/components/roomba/translations/ko.json @@ -18,7 +18,7 @@ "title": "\uae30\uae30\uc5d0 \uc790\ub3d9\uc73c\ub85c \uc5f0\uacb0\ud558\uae30" }, "link": { - "description": "\uae30\uae30\uc5d0\uc11c \uc18c\ub9ac\uac00 \ub0a0 \ub54c\uae4c\uc9c0 {name}\uc758 \ud648 \ubc84\ud2bc\uc744 \uae38\uac8c \ub20c\ub7ec\uc8fc\uc138\uc694 (\uc57d 2\ucd08).", + "description": "\uae30\uae30\uc5d0\uc11c \uc18c\ub9ac\uac00 \ub0a0 \ub54c\uae4c\uc9c0 {name}\uc758 \ud648 \ubc84\ud2bc\uc744 \uae38\uac8c \ub204\ub978 \ub2e4\uc74c(\uc57d 2\ucd08) 30\ucd08 \uc774\ub0b4\uc5d0 \ud655\uc778 \ubc84\ud2bc\uc744 \ub20c\ub7ec\uc8fc\uc138\uc694.", "title": "\ube44\ubc00\ubc88\ud638 \uac00\uc838\uc624\uae30" }, "link_manual": { @@ -33,7 +33,7 @@ "blid": "BLID", "host": "\ud638\uc2a4\ud2b8" }, - "description": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ub8f8\ubc14 \ub610\ub294 \ube0c\ub77c\ubc14\uac00 \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. BLID\ub294 `iRobot-` \ub4a4\uc758 \uae30\uae30 \ud638\uc2a4\ud2b8 \uc774\ub984 \ubd80\ubd84\uc785\ub2c8\ub2e4. \uad00\ub828 \ubb38\uc11c\uc5d0 \ub098\uc640 \uc788\ub294 {auth_help_url} \ub2e8\uacc4\ub97c \ub530\ub77c\uc8fc\uc138\uc694.", + "description": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ub8f8\ubc14 \ub610\ub294 \ube0c\ub77c\ubc14\uac00 \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. BLID\ub294 `iRobot-` \ub610\ub294 `Roomba-` \ub4a4\uc5d0 \uc788\ub294 \uae30\uae30 \ud638\uc2a4\ud2b8 \uc774\ub984\uc758 \uc77c\ubd80\uc785\ub2c8\ub2e4. \uad00\ub828 \ubb38\uc11c\uc5d0 \uc124\uba85\ub41c \ub2e8\uacc4\ub97c \ub530\ub77c\uc8fc\uc138\uc694: {auth_help_url}", "title": "\uae30\uae30\uc5d0 \uc218\ub3d9\uc73c\ub85c \uc5f0\uacb0\ud558\uae30" }, "user": { diff --git a/homeassistant/components/zha/translations/ko.json b/homeassistant/components/zha/translations/ko.json index 4ec9790b35742..89dfaeba9be6f 100644 --- a/homeassistant/components/zha/translations/ko.json +++ b/homeassistant/components/zha/translations/ko.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" }, + "flow_title": "ZHA: {name}", "step": { "pick_radio": { "data": { From e76b653246659fb98ef142417ee5dfd6a43bbb78 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Fri, 2 Apr 2021 12:48:57 +0300 Subject: [PATCH 0029/1317] Bump aioshelly to 0.6.2 (#48620) --- homeassistant/components/shelly/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index a757947c5cfbb..1ae274d6dfd85 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -3,7 +3,7 @@ "name": "Shelly", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/shelly", - "requirements": ["aioshelly==0.6.1"], + "requirements": ["aioshelly==0.6.2"], "zeroconf": [{ "type": "_http._tcp.local.", "name": "shelly*" }], "codeowners": ["@balloob", "@bieniu", "@thecode", "@chemelli74"] } diff --git a/requirements_all.txt b/requirements_all.txt index 44adb85ee0b49..050ca2f26d94f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -224,7 +224,7 @@ aiopylgtv==0.4.0 aiorecollect==1.0.1 # homeassistant.components.shelly -aioshelly==0.6.1 +aioshelly==0.6.2 # homeassistant.components.switcher_kis aioswitcher==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index db0e4ae7704b0..cecb3cd93526a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -143,7 +143,7 @@ aiopylgtv==0.4.0 aiorecollect==1.0.1 # homeassistant.components.shelly -aioshelly==0.6.1 +aioshelly==0.6.2 # homeassistant.components.switcher_kis aioswitcher==1.2.1 From bdbb4f939f34682b2eca993bb041cfb21214015c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 2 Apr 2021 06:27:41 -0700 Subject: [PATCH 0030/1317] Add variables to execute script (#48613) --- .../components/websocket_api/commands.py | 3 +- .../components/websocket_api/test_commands.py | 38 ++++++++++++++----- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 74251e1bf24b2..2912512fa62bf 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -424,6 +424,7 @@ async def handle_test_condition(hass, connection, msg): { vol.Required("type"): "execute_script", vol.Required("sequence"): cv.SCRIPT_SCHEMA, + vol.Optional("variables"): dict, } ) @decorators.require_admin @@ -436,5 +437,5 @@ async def handle_execute_script(hass, connection, msg): context = connection.context(msg) script_obj = Script(hass, msg["sequence"], f"{const.DOMAIN} script", const.DOMAIN) - await script_obj.async_run(context=context) + await script_obj.async_run(msg.get("variables"), context=context) connection.send_message(messages.result_message(msg["id"], {"context": context})) diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index b9e1e149dd191..da42e175ff360 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -1086,21 +1086,41 @@ async def test_execute_script(hass, websocket_client): } ) - await hass.async_block_till_done() - await hass.async_block_till_done() + msg_no_var = await websocket_client.receive_json() + assert msg_no_var["id"] == 5 + assert msg_no_var["type"] == const.TYPE_RESULT + assert msg_no_var["success"] - msg = await websocket_client.receive_json() - assert msg["id"] == 5 - assert msg["type"] == const.TYPE_RESULT - assert msg["success"] + await websocket_client.send_json( + { + "id": 6, + "type": "execute_script", + "sequence": { + "service": "domain_test.test_service", + "data": {"hello": "{{ name }}"}, + }, + "variables": {"name": "From variable"}, + } + ) + + msg_var = await websocket_client.receive_json() + assert msg_var["id"] == 6 + assert msg_var["type"] == const.TYPE_RESULT + assert msg_var["success"] await hass.async_block_till_done() await hass.async_block_till_done() - assert len(calls) == 1 - call = calls[0] + assert len(calls) == 2 + call = calls[0] assert call.domain == "domain_test" assert call.service == "test_service" assert call.data == {"hello": "world"} - assert call.context.as_dict() == msg["result"]["context"] + assert call.context.as_dict() == msg_no_var["result"]["context"] + + call = calls[1] + assert call.domain == "domain_test" + assert call.service == "test_service" + assert call.data == {"hello": "From variable"} + assert call.context.as_dict() == msg_var["result"]["context"] From 212d9aa748d7805a3cdb0448a7e79b5369558a1d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 2 Apr 2021 10:24:38 -0700 Subject: [PATCH 0031/1317] Fix trigger template entities without a unique ID (#48631) --- homeassistant/components/template/sensor.py | 2 ++ tests/components/template/test_sensor.py | 14 +++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index 9714d147e018b..a5f5d669b169e 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -5,6 +5,7 @@ from homeassistant.components.sensor import ( DEVICE_CLASSES_SCHEMA, + DOMAIN as SENSOR_DOMAIN, ENTITY_ID_FORMAT, PLATFORM_SCHEMA, SensorEntity, @@ -201,6 +202,7 @@ def unit_of_measurement(self): class TriggerSensorEntity(TriggerEntity, SensorEntity): """Sensor entity based on trigger data.""" + domain = SENSOR_DOMAIN extra_template_keys = (CONF_VALUE_TEMPLATE,) @property diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py index 11945a3b027d9..6aa1e75cc1f0e 100644 --- a/tests/components/template/test_sensor.py +++ b/tests/components/template/test_sensor.py @@ -1014,7 +1014,15 @@ async def test_trigger_entity(hass): "attribute_templates": { "plus_one": "{{ trigger.event.data.beer + 1 }}" }, - } + }, + }, + }, + { + "trigger": [], + "sensors": { + "bare_minimum": { + "value_template": "{{ trigger.event.data.beer }}" + }, }, }, ], @@ -1027,6 +1035,10 @@ async def test_trigger_entity(hass): assert state is not None assert state.state == STATE_UNKNOWN + state = hass.states.get("sensor.bare_minimum") + assert state is not None + assert state.state == STATE_UNKNOWN + context = Context() hass.bus.async_fire("test_event", {"beer": 2}, context=context) await hass.async_block_till_done() From eed3bfc7620dffabded58ec4d188c66d3f961a52 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 2 Apr 2021 19:47:16 +0200 Subject: [PATCH 0032/1317] Going async with denonavr (#47920) Co-authored-by: J. Nick Koston --- .coveragerc | 1 + homeassistant/components/denonavr/__init__.py | 47 +-- .../components/denonavr/config_flow.py | 65 ++-- .../components/denonavr/manifest.json | 2 +- .../components/denonavr/media_player.py | 349 ++++++++++-------- homeassistant/components/denonavr/receiver.py | 51 +-- .../components/denonavr/services.yaml | 2 +- requirements_all.txt | 3 +- requirements_test_all.txt | 3 +- tests/components/denonavr/test_config_flow.py | 174 ++------- .../components/denonavr/test_media_player.py | 15 +- 11 files changed, 308 insertions(+), 404 deletions(-) diff --git a/.coveragerc b/.coveragerc index b55fe3f5a3e7f..22855a26dd945 100644 --- a/.coveragerc +++ b/.coveragerc @@ -174,6 +174,7 @@ omit = homeassistant/components/deluge/sensor.py homeassistant/components/deluge/switch.py homeassistant/components/denon/media_player.py + homeassistant/components/denonavr/__init__.py homeassistant/components/denonavr/media_player.py homeassistant/components/denonavr/receiver.py homeassistant/components/deutsche_bahn/sensor.py diff --git a/homeassistant/components/denonavr/__init__.py b/homeassistant/components/denonavr/__init__.py index 3946a0d6171ed..fa4d161269731 100644 --- a/homeassistant/components/denonavr/__init__.py +++ b/homeassistant/components/denonavr/__init__.py @@ -1,13 +1,13 @@ """The denonavr component.""" import logging -import voluptuous as vol +from denonavr.exceptions import AvrNetworkError, AvrTimoutError from homeassistant import config_entries, core -from homeassistant.const import ATTR_COMMAND, ATTR_ENTITY_ID, CONF_HOST +from homeassistant.const import CONF_HOST from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import config_validation as cv, entity_registry as er -from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.httpx_client import get_async_client from .config_flow import ( CONF_SHOW_ALL_SOURCES, @@ -23,34 +23,9 @@ CONF_RECEIVER = "receiver" UNDO_UPDATE_LISTENER = "undo_update_listener" -SERVICE_GET_COMMAND = "get_command" _LOGGER = logging.getLogger(__name__) -CALL_SCHEMA = vol.Schema({vol.Required(ATTR_ENTITY_ID): cv.comp_entity_ids}) - -GET_COMMAND_SCHEMA = CALL_SCHEMA.extend({vol.Required(ATTR_COMMAND): cv.string}) - -SERVICE_TO_METHOD = { - SERVICE_GET_COMMAND: {"method": "get_command", "schema": GET_COMMAND_SCHEMA} -} - - -def setup(hass: core.HomeAssistant, config: dict): - """Set up the denonavr platform.""" - - def service_handler(service): - method = SERVICE_TO_METHOD.get(service.service) - data = service.data.copy() - data["method"] = method["method"] - dispatcher_send(hass, DOMAIN, data) - - for service in SERVICE_TO_METHOD: - schema = SERVICE_TO_METHOD[service]["schema"] - hass.services.register(DOMAIN, service, service_handler, schema=schema) - - return True - async def async_setup_entry( hass: core.HomeAssistant, entry: config_entries.ConfigEntry @@ -60,15 +35,18 @@ async def async_setup_entry( # Connect to receiver connect_denonavr = ConnectDenonAVR( - hass, entry.data[CONF_HOST], DEFAULT_TIMEOUT, entry.options.get(CONF_SHOW_ALL_SOURCES, DEFAULT_SHOW_SOURCES), entry.options.get(CONF_ZONE2, DEFAULT_ZONE2), entry.options.get(CONF_ZONE3, DEFAULT_ZONE3), + lambda: get_async_client(hass), + entry.state, ) - if not await connect_denonavr.async_connect_receiver(): - raise ConfigEntryNotReady + try: + await connect_denonavr.async_connect_receiver() + except (AvrNetworkError, AvrTimoutError) as ex: + raise ConfigEntryNotReady from ex receiver = connect_denonavr.receiver undo_listener = entry.add_update_listener(update_listener) @@ -98,8 +76,9 @@ async def async_unload_entry( # Remove zone2 and zone3 entities if needed entity_registry = await er.async_get_registry(hass) entries = er.async_entries_for_config_entry(entity_registry, config_entry.entry_id) - zone2_id = f"{config_entry.unique_id}-Zone2" - zone3_id = f"{config_entry.unique_id}-Zone3" + unique_id = config_entry.unique_id or config_entry.entry_id + zone2_id = f"{unique_id}-Zone2" + zone3_id = f"{unique_id}-Zone3" for entry in entries: if entry.unique_id == zone2_id and not config_entry.options.get(CONF_ZONE2): entity_registry.async_remove(entry.entity_id) diff --git a/homeassistant/components/denonavr/config_flow.py b/homeassistant/components/denonavr/config_flow.py index 0b7c0b718471d..f2c37d9fc7567 100644 --- a/homeassistant/components/denonavr/config_flow.py +++ b/homeassistant/components/denonavr/config_flow.py @@ -1,17 +1,17 @@ """Config flow to configure Denon AVR receivers using their HTTP interface.""" -from functools import partial import logging +from typing import Any, Dict, Optional from urllib.parse import urlparse import denonavr -from getmac import get_mac_address +from denonavr.exceptions import AvrNetworkError, AvrTimoutError import voluptuous as vol from homeassistant import config_entries from homeassistant.components import ssdp -from homeassistant.const import CONF_HOST, CONF_MAC, CONF_TYPE +from homeassistant.const import CONF_HOST, CONF_TYPE from homeassistant.core import callback -from homeassistant.helpers.device_registry import format_mac +from homeassistant.helpers.httpx_client import get_async_client from .receiver import ConnectDenonAVR @@ -44,7 +44,7 @@ def __init__(self, config_entry: config_entries.ConfigEntry): """Init object.""" self.config_entry = config_entry - async def async_step_init(self, user_input=None): + async def async_step_init(self, user_input: Optional[Dict[str, Any]] = None): """Manage the options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) @@ -90,11 +90,13 @@ def __init__(self): @staticmethod @callback - def async_get_options_flow(config_entry) -> OptionsFlowHandler: + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> OptionsFlowHandler: """Get the options flow.""" return OptionsFlowHandler(config_entry) - async def async_step_user(self, user_input=None): + async def async_step_user(self, user_input: Optional[Dict[str, Any]] = None): """Handle a flow initialized by the user.""" errors = {} if user_input is not None: @@ -105,7 +107,7 @@ async def async_step_user(self, user_input=None): return await self.async_step_connect() # discovery using denonavr library - self.d_receivers = await self.hass.async_add_executor_job(denonavr.discover) + self.d_receivers = await denonavr.async_discover() # More than one receiver could be discovered by that method if len(self.d_receivers) == 1: self.host = self.d_receivers[0]["host"] @@ -120,7 +122,9 @@ async def async_step_user(self, user_input=None): step_id="user", data_schema=CONFIG_SCHEMA, errors=errors ) - async def async_step_select(self, user_input=None): + async def async_step_select( + self, user_input: Optional[Dict[str, Any]] = None + ) -> Dict[str, Any]: """Handle multiple receivers found.""" errors = {} if user_input is not None: @@ -139,29 +143,37 @@ async def async_step_select(self, user_input=None): step_id="select", data_schema=select_scheme, errors=errors ) - async def async_step_confirm(self, user_input=None): + async def async_step_confirm( + self, user_input: Optional[Dict[str, Any]] = None + ) -> Dict[str, Any]: """Allow the user to confirm adding the device.""" if user_input is not None: return await self.async_step_connect() + self._set_confirm_only() return self.async_show_form(step_id="confirm") - async def async_step_connect(self, user_input=None): + async def async_step_connect( + self, user_input: Optional[Dict[str, Any]] = None + ) -> Dict[str, Any]: """Connect to the receiver.""" connect_denonavr = ConnectDenonAVR( - self.hass, self.host, self.timeout, self.show_all_sources, self.zone2, self.zone3, + lambda: get_async_client(self.hass), ) - if not await connect_denonavr.async_connect_receiver(): + + try: + success = await connect_denonavr.async_connect_receiver() + except (AvrNetworkError, AvrTimoutError): + success = False + if not success: return self.async_abort(reason="cannot_connect") receiver = connect_denonavr.receiver - mac_address = await self.async_get_mac(self.host) - if not self.serial_number: self.serial_number = receiver.serial_number if not self.model_name: @@ -185,7 +197,6 @@ async def async_step_connect(self, user_input=None): title=receiver.name, data={ CONF_HOST: self.host, - CONF_MAC: mac_address, CONF_TYPE: receiver.receiver_type, CONF_MODEL: self.model_name, CONF_MANUFACTURER: receiver.manufacturer, @@ -193,7 +204,7 @@ async def async_step_connect(self, user_input=None): }, ) - async def async_step_ssdp(self, discovery_info): + async def async_step_ssdp(self, discovery_info: Dict[str, Any]) -> Dict[str, Any]: """Handle a discovered Denon AVR. This flow is triggered by the SSDP component. It will check if the @@ -235,24 +246,6 @@ async def async_step_ssdp(self, discovery_info): return await self.async_step_confirm() @staticmethod - def construct_unique_id(model_name, serial_number): + def construct_unique_id(model_name: str, serial_number: str) -> str: """Construct the unique id from the ssdp discovery or user_step.""" return f"{model_name}-{serial_number}" - - async def async_get_mac(self, host): - """Get the mac address of the DenonAVR receiver.""" - try: - mac_address = await self.hass.async_add_executor_job( - partial(get_mac_address, **{"ip": host}) - ) - if not mac_address: - mac_address = await self.hass.async_add_executor_job( - partial(get_mac_address, **{"hostname": host}) - ) - except Exception as err: # pylint: disable=broad-except - _LOGGER.error("Unable to get mac address: %s", err) - mac_address = None - - if mac_address is not None: - mac_address = format_mac(mac_address) - return mac_address diff --git a/homeassistant/components/denonavr/manifest.json b/homeassistant/components/denonavr/manifest.json index 8d2052181f872..e4cdaa0372403 100644 --- a/homeassistant/components/denonavr/manifest.json +++ b/homeassistant/components/denonavr/manifest.json @@ -3,7 +3,7 @@ "name": "Denon AVR Network Receivers", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/denonavr", - "requirements": ["denonavr==0.9.10", "getmac==0.8.2"], + "requirements": ["denonavr==0.10.5"], "codeowners": ["@scarface-4711", "@starkillerOG"], "ssdp": [ { diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index ea484a1087774..799f07ed71b6a 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -1,8 +1,22 @@ """Support for Denon AVR receivers using their HTTP interface.""" -from contextlib import suppress +from datetime import timedelta +from functools import wraps import logging +from typing import Coroutine + +from denonavr import DenonAVR +from denonavr.const import POWER_ON +from denonavr.exceptions import ( + AvrCommandError, + AvrForbiddenError, + AvrNetworkError, + AvrTimoutError, + DenonAvrError, +) +import voluptuous as vol +from homeassistant import config_entries from homeassistant.components.media_player import MediaPlayerEntity from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, @@ -20,18 +34,9 @@ SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, ) -from homeassistant.const import ( - ATTR_ENTITY_ID, - CONF_MAC, - ENTITY_MATCH_ALL, - ENTITY_MATCH_NONE, - STATE_OFF, - STATE_ON, - STATE_PAUSED, - STATE_PLAYING, -) -from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.const import ATTR_COMMAND, STATE_PAUSED, STATE_PLAYING +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_validation as cv, entity_platform from . import CONF_RECEIVER from .config_flow import ( @@ -64,8 +69,18 @@ | SUPPORT_PLAY ) +SCAN_INTERVAL = timedelta(seconds=10) +PARALLEL_UPDATES = 1 + +# Services +SERVICE_GET_COMMAND = "get_command" -async def async_setup_entry(hass, config_entry, async_add_entities): + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: config_entries.ConfigEntry, + async_add_entities: entity_platform.EntityPlatform.async_add_entities, +): """Set up the DenonAVR receiver from a config entry.""" entities = [] receiver = hass.data[DOMAIN][config_entry.entry_id][CONF_RECEIVER] @@ -73,93 +88,116 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if config_entry.data[CONF_SERIAL_NUMBER] is not None: unique_id = f"{config_entry.unique_id}-{receiver_zone.zone}" else: - unique_id = None + unique_id = f"{config_entry.entry_id}-{receiver_zone.zone}" + await receiver_zone.async_setup() entities.append(DenonDevice(receiver_zone, unique_id, config_entry)) _LOGGER.debug( "%s receiver at host %s initialized", receiver.manufacturer, receiver.host ) - async_add_entities(entities) + + # Register additional services + platform = entity_platform.current_platform.get() + platform.async_register_entity_service( + SERVICE_GET_COMMAND, + {vol.Required(ATTR_COMMAND): cv.string}, + f"async_{SERVICE_GET_COMMAND}", + ) + + async_add_entities(entities, update_before_add=True) class DenonDevice(MediaPlayerEntity): """Representation of a Denon Media Player Device.""" - def __init__(self, receiver, unique_id, config_entry): + def __init__( + self, + receiver: DenonAVR, + unique_id: str, + config_entry: config_entries.ConfigEntry, + ): """Initialize the device.""" self._receiver = receiver - self._name = self._receiver.name self._unique_id = unique_id self._config_entry = config_entry - self._muted = self._receiver.muted - self._volume = self._receiver.volume - self._current_source = self._receiver.input_func - self._source_list = self._receiver.input_func_list - self._state = self._receiver.state - self._power = self._receiver.power - self._media_image_url = self._receiver.image_url - self._title = self._receiver.title - self._artist = self._receiver.artist - self._album = self._receiver.album - self._band = self._receiver.band - self._frequency = self._receiver.frequency - self._station = self._receiver.station - - self._sound_mode_support = self._receiver.support_sound_mode - if self._sound_mode_support: - self._sound_mode = self._receiver.sound_mode - self._sound_mode_raw = self._receiver.sound_mode_raw - self._sound_mode_list = self._receiver.sound_mode_list - else: - self._sound_mode = None - self._sound_mode_raw = None - self._sound_mode_list = None self._supported_features_base = SUPPORT_DENON self._supported_features_base |= ( - self._sound_mode_support and SUPPORT_SELECT_SOUND_MODE + self._receiver.support_sound_mode and SUPPORT_SELECT_SOUND_MODE ) - - async def async_added_to_hass(self): - """Register signal handler.""" - self.async_on_remove( - async_dispatcher_connect(self.hass, DOMAIN, self.signal_handler) - ) - - def signal_handler(self, data): - """Handle domain-specific signal by calling appropriate method.""" - entity_ids = data[ATTR_ENTITY_ID] - - if entity_ids == ENTITY_MATCH_NONE: - return - - if entity_ids == ENTITY_MATCH_ALL or self.entity_id in entity_ids: - params = { - key: value - for key, value in data.items() - if key not in ["entity_id", "method"] - } - getattr(self, data["method"])(**params) - - def update(self): + self._available = True + + def async_log_errors( # pylint: disable=no-self-argument + func: Coroutine, + ) -> Coroutine: + """ + Log errors occurred when calling a Denon AVR receiver. + + Decorates methods of DenonDevice class. + Declaration of staticmethod for this method is at the end of this class. + """ + + @wraps(func) + async def wrapper(self, *args, **kwargs): + # pylint: disable=protected-access + available = True + try: + return await func(self, *args, **kwargs) # pylint: disable=not-callable + except AvrTimoutError: + available = False + if self._available is True: + _LOGGER.warning( + "Timeout connecting to Denon AVR receiver at host %s. Device is unavailable", + self._receiver.host, + ) + self._available = False + except AvrNetworkError: + available = False + if self._available is True: + _LOGGER.warning( + "Network error connecting to Denon AVR receiver at host %s. Device is unavailable", + self._receiver.host, + ) + self._available = False + except AvrForbiddenError: + available = False + if self._available is True: + _LOGGER.warning( + "Denon AVR receiver at host %s responded with HTTP 403 error. Device is unavailable. Please consider power cycling your receiver", + self._receiver.host, + ) + self._available = False + except AvrCommandError as err: + _LOGGER.error( + "Command %s failed with error: %s", + func.__name__, + err, + ) + except DenonAvrError as err: + _LOGGER.error( + "Error %s occurred in method %s for Denon AVR receiver", + err, + func.__name__, # pylint: disable=no-member + exc_info=True, + ) + finally: + if available is True and self._available is False: + _LOGGER.info( + "Denon AVR receiver at host %s is available again", + self._receiver.host, + ) + self._available = True + + return wrapper + + @async_log_errors + async def async_update(self) -> None: """Get the latest status information from device.""" - self._receiver.update() - self._name = self._receiver.name - self._muted = self._receiver.muted - self._volume = self._receiver.volume - self._current_source = self._receiver.input_func - self._source_list = self._receiver.input_func_list - self._state = self._receiver.state - self._power = self._receiver.power - self._media_image_url = self._receiver.image_url - self._title = self._receiver.title - self._artist = self._receiver.artist - self._album = self._receiver.album - self._band = self._receiver.band - self._frequency = self._receiver.frequency - self._station = self._receiver.station - if self._sound_mode_support: - self._sound_mode = self._receiver.sound_mode - self._sound_mode_raw = self._receiver.sound_mode_raw + await self._receiver.async_update() + + @property + def available(self): + """Return True if entity is available.""" + return self._available @property def unique_id(self): @@ -177,60 +215,59 @@ def device_info(self): "manufacturer": self._config_entry.data[CONF_MANUFACTURER], "name": self._config_entry.title, "model": f"{self._config_entry.data[CONF_MODEL]}-{self._config_entry.data[CONF_TYPE]}", + "serial_number": self._config_entry.data[CONF_SERIAL_NUMBER], } - if self._config_entry.data[CONF_MAC] is not None: - device_info["connections"] = { - (dr.CONNECTION_NETWORK_MAC, self._config_entry.data[CONF_MAC]) - } return device_info @property def name(self): """Return the name of the device.""" - return self._name + return self._receiver.name @property def state(self): """Return the state of the device.""" - return self._state + return self._receiver.state @property def is_volume_muted(self): """Return boolean if volume is currently muted.""" - return self._muted + return self._receiver.muted @property def volume_level(self): """Volume level of the media player (0..1).""" # Volume is sent in a format like -50.0. Minimum is -80.0, # maximum is 18.0 - return (float(self._volume) + 80) / 100 + if self._receiver.volume is None: + return None + return (float(self._receiver.volume) + 80) / 100 @property def source(self): """Return the current input source.""" - return self._current_source + return self._receiver.input_func @property def source_list(self): """Return a list of available input sources.""" - return self._source_list + return self._receiver.input_func_list @property def sound_mode(self): """Return the current matched sound mode.""" - return self._sound_mode + return self._receiver.sound_mode @property def sound_mode_list(self): """Return a list of available sound modes.""" - return self._sound_mode_list + return self._receiver.sound_mode_list @property def supported_features(self): """Flag media player features that are supported.""" - if self._current_source in self._receiver.netaudio_func_list: + if self._receiver.input_func in self._receiver.netaudio_func_list: return self._supported_features_base | SUPPORT_MEDIA_MODES return self._supported_features_base @@ -242,7 +279,10 @@ def media_content_id(self): @property def media_content_type(self): """Content type of current playing media.""" - if self._state == STATE_PLAYING or self._state == STATE_PAUSED: + if ( + self._receiver.state == STATE_PLAYING + or self._receiver.state == STATE_PAUSED + ): return MEDIA_TYPE_MUSIC return MEDIA_TYPE_CHANNEL @@ -254,32 +294,32 @@ def media_duration(self): @property def media_image_url(self): """Image url of current playing media.""" - if self._current_source in self._receiver.playing_func_list: - return self._media_image_url + if self._receiver.input_func in self._receiver.playing_func_list: + return self._receiver.image_url return None @property def media_title(self): """Title of current playing media.""" - if self._current_source not in self._receiver.playing_func_list: - return self._current_source - if self._title is not None: - return self._title - return self._frequency + if self._receiver.input_func not in self._receiver.playing_func_list: + return self._receiver.input_func + if self._receiver.title is not None: + return self._receiver.title + return self._receiver.frequency @property def media_artist(self): """Artist of current playing media, music track only.""" - if self._artist is not None: - return self._artist - return self._band + if self._receiver.artist is not None: + return self._receiver.artist + return self._receiver.band @property def media_album_name(self): """Album name of current playing media, music track only.""" - if self._album is not None: - return self._album - return self._station + if self._receiver.album is not None: + return self._receiver.album + return self._receiver.station @property def media_album_artist(self): @@ -310,77 +350,92 @@ def media_episode(self): def extra_state_attributes(self): """Return device specific state attributes.""" if ( - self._sound_mode_raw is not None - and self._sound_mode_support - and self._power == "ON" + self._receiver.sound_mode_raw is not None + and self._receiver.support_sound_mode + and self._receiver.power == POWER_ON ): - return {ATTR_SOUND_MODE_RAW: self._sound_mode_raw} + return {ATTR_SOUND_MODE_RAW: self._receiver.sound_mode_raw} return {} - def media_play_pause(self): + @async_log_errors + async def async_media_play_pause(self): """Play or pause the media player.""" - return self._receiver.toggle_play_pause() + await self._receiver.async_toggle_play_pause() - def media_play(self): + @async_log_errors + async def async_media_play(self): """Send play command.""" - return self._receiver.play() + await self._receiver.async_play() - def media_pause(self): + @async_log_errors + async def async_media_pause(self): """Send pause command.""" - return self._receiver.pause() + await self._receiver.async_pause() - def media_previous_track(self): + @async_log_errors + async def async_media_previous_track(self): """Send previous track command.""" - return self._receiver.previous_track() + await self._receiver.async_previous_track() - def media_next_track(self): + @async_log_errors + async def async_media_next_track(self): """Send next track command.""" - return self._receiver.next_track() + await self._receiver.async_next_track() - def select_source(self, source): + @async_log_errors + async def async_select_source(self, source: str): """Select input source.""" # Ensure that the AVR is turned on, which is necessary for input # switch to work. - self.turn_on() - return self._receiver.set_input_func(source) + await self.async_turn_on() + await self._receiver.async_set_input_func(source) - def select_sound_mode(self, sound_mode): + @async_log_errors + async def async_select_sound_mode(self, sound_mode: str): """Select sound mode.""" - return self._receiver.set_sound_mode(sound_mode) + await self._receiver.async_set_sound_mode(sound_mode) - def turn_on(self): + @async_log_errors + async def async_turn_on(self): """Turn on media player.""" - if self._receiver.power_on(): - self._state = STATE_ON + await self._receiver.async_power_on() - def turn_off(self): + @async_log_errors + async def async_turn_off(self): """Turn off media player.""" - if self._receiver.power_off(): - self._state = STATE_OFF + await self._receiver.async_power_off() - def volume_up(self): + @async_log_errors + async def async_volume_up(self): """Volume up the media player.""" - return self._receiver.volume_up() + await self._receiver.async_volume_up() - def volume_down(self): + @async_log_errors + async def async_volume_down(self): """Volume down media player.""" - return self._receiver.volume_down() + await self._receiver.async_volume_down() - def set_volume_level(self, volume): + @async_log_errors + async def async_set_volume_level(self, volume: int): """Set volume level, range 0..1.""" # Volume has to be sent in a format like -50.0. Minimum is -80.0, # maximum is 18.0 volume_denon = float((volume * 100) - 80) if volume_denon > 18: volume_denon = float(18) - with suppress(ValueError): - if self._receiver.set_volume(volume_denon): - self._volume = volume_denon + await self._receiver.async_set_volume(volume_denon) - def mute_volume(self, mute): + @async_log_errors + async def async_mute_volume(self, mute: bool): """Send mute command.""" - return self._receiver.mute(mute) + await self._receiver.async_mute(mute) - def get_command(self, command, **kwargs): + @async_log_errors + async def async_get_command(self, command: str, **kwargs): """Send generic command.""" - self._receiver.send_get_command(command) + return await self._receiver.async_get_command(command) + + # Decorator defined before is a staticmethod + async_log_errors = staticmethod( # pylint: disable=no-staticmethod-decorator + async_log_errors + ) diff --git a/homeassistant/components/denonavr/receiver.py b/homeassistant/components/denonavr/receiver.py index f30469961dfe8..31d91c0a9baab 100644 --- a/homeassistant/components/denonavr/receiver.py +++ b/homeassistant/components/denonavr/receiver.py @@ -1,7 +1,8 @@ """Code to handle a DenonAVR receiver.""" import logging +from typing import Callable, Optional -import denonavr +from denonavr import DenonAVR _LOGGER = logging.getLogger(__name__) @@ -9,13 +10,23 @@ class ConnectDenonAVR: """Class to async connect to a DenonAVR receiver.""" - def __init__(self, hass, host, timeout, show_all_inputs, zone2, zone3): + def __init__( + self, + host: str, + timeout: float, + show_all_inputs: bool, + zone2: bool, + zone3: bool, + async_client_getter: Callable, + entry_state: Optional[str] = None, + ): """Initialize the class.""" - self._hass = hass + self._async_client_getter = async_client_getter self._receiver = None self._host = host self._show_all_inputs = show_all_inputs self._timeout = timeout + self._entry_state = entry_state self._zones = {} if zone2: @@ -24,14 +35,13 @@ def __init__(self, hass, host, timeout, show_all_inputs, zone2, zone3): self._zones["Zone3"] = None @property - def receiver(self): + def receiver(self) -> Optional[DenonAVR]: """Return the class containing all connections to the receiver.""" return self._receiver - async def async_connect_receiver(self): + async def async_connect_receiver(self) -> bool: """Connect to the DenonAVR receiver.""" - if not await self._hass.async_add_executor_job(self.init_receiver_class): - return False + await self.async_init_receiver_class() if ( self._receiver.manufacturer is None @@ -60,19 +70,16 @@ async def async_connect_receiver(self): return True - def init_receiver_class(self): - """Initialize the DenonAVR class in a way that can called by async_add_executor_job.""" - try: - self._receiver = denonavr.DenonAVR( - host=self._host, - show_all_inputs=self._show_all_inputs, - timeout=self._timeout, - add_zones=self._zones, - ) - except ConnectionError: - _LOGGER.error( - "ConnectionError during setup of denonavr with host %s", self._host - ) - return False + async def async_init_receiver_class(self) -> bool: + """Initialize the DenonAVR class asynchronously.""" + receiver = DenonAVR( + host=self._host, + show_all_inputs=self._show_all_inputs, + timeout=self._timeout, + add_zones=self._zones, + ) + # Use httpx.AsyncClient getter provided by Home Assistant + receiver.set_async_client_getter(self._async_client_getter) + await receiver.async_setup() - return True + self._receiver = receiver diff --git a/homeassistant/components/denonavr/services.yaml b/homeassistant/components/denonavr/services.yaml index 35dedd8fb7f26..62157426bb221 100644 --- a/homeassistant/components/denonavr/services.yaml +++ b/homeassistant/components/denonavr/services.yaml @@ -1,4 +1,4 @@ -# Describes the format for available webostv services +# Describes the format for available denonavr services get_command: description: "Send a generic HTTP get command." diff --git a/requirements_all.txt b/requirements_all.txt index 050ca2f26d94f..7b60bcd4ee912 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -476,7 +476,7 @@ defusedxml==0.6.0 deluge-client==1.7.1 # homeassistant.components.denonavr -denonavr==0.9.10 +denonavr==0.10.5 # homeassistant.components.devolo_home_control devolo-home-control-api==0.17.1 @@ -647,7 +647,6 @@ georss_ign_sismologia_client==0.2 # homeassistant.components.qld_bushfire georss_qld_bushfire_alert_client==0.3 -# homeassistant.components.denonavr # homeassistant.components.huawei_lte # homeassistant.components.kef # homeassistant.components.minecraft_server diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cecb3cd93526a..cc7367159562d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -258,7 +258,7 @@ debugpy==1.2.1 defusedxml==0.6.0 # homeassistant.components.denonavr -denonavr==0.9.10 +denonavr==0.10.5 # homeassistant.components.devolo_home_control devolo-home-control-api==0.17.1 @@ -344,7 +344,6 @@ georss_ign_sismologia_client==0.2 # homeassistant.components.qld_bushfire georss_qld_bushfire_alert_client==0.3 -# homeassistant.components.denonavr # homeassistant.components.huawei_lte # homeassistant.components.kef # homeassistant.components.minecraft_server diff --git a/tests/components/denonavr/test_config_flow.py b/tests/components/denonavr/test_config_flow.py index 67d1a4e10db1e..74ce77f1db760 100644 --- a/tests/components/denonavr/test_config_flow.py +++ b/tests/components/denonavr/test_config_flow.py @@ -14,13 +14,13 @@ CONF_ZONE2, CONF_ZONE3, DOMAIN, + AvrTimoutError, ) -from homeassistant.const import CONF_HOST, CONF_MAC +from homeassistant.const import CONF_HOST from tests.common import MockConfigEntry TEST_HOST = "1.2.3.4" -TEST_MAC = "ab:cd:ef:gh" TEST_HOST2 = "5.6.7.8" TEST_NAME = "Test_Receiver" TEST_MODEL = "model5" @@ -38,41 +38,29 @@ def denonavr_connect_fixture(): """Mock denonavr connection and entry setup.""" with patch( - "homeassistant.components.denonavr.receiver.denonavr.DenonAVR._update_input_func_list", - return_value=True, - ), patch( - "homeassistant.components.denonavr.receiver.denonavr.DenonAVR._get_receiver_name", - return_value=TEST_NAME, - ), patch( - "homeassistant.components.denonavr.receiver.denonavr.DenonAVR._get_support_sound_mode", - return_value=True, - ), patch( - "homeassistant.components.denonavr.receiver.denonavr.DenonAVR._update_avr_2016", - return_value=True, + "homeassistant.components.denonavr.receiver.DenonAVR.async_setup", + return_value=None, ), patch( - "homeassistant.components.denonavr.receiver.denonavr.DenonAVR._update_avr", - return_value=True, + "homeassistant.components.denonavr.receiver.DenonAVR.async_update", + return_value=None, ), patch( - "homeassistant.components.denonavr.receiver.denonavr.DenonAVR.get_device_info", + "homeassistant.components.denonavr.receiver.DenonAVR.support_sound_mode", return_value=True, ), patch( - "homeassistant.components.denonavr.receiver.denonavr.DenonAVR.name", + "homeassistant.components.denonavr.receiver.DenonAVR.name", TEST_NAME, ), patch( - "homeassistant.components.denonavr.receiver.denonavr.DenonAVR.model_name", + "homeassistant.components.denonavr.receiver.DenonAVR.model_name", TEST_MODEL, ), patch( - "homeassistant.components.denonavr.receiver.denonavr.DenonAVR.serial_number", + "homeassistant.components.denonavr.receiver.DenonAVR.serial_number", TEST_SERIALNUMBER, ), patch( - "homeassistant.components.denonavr.receiver.denonavr.DenonAVR.manufacturer", + "homeassistant.components.denonavr.receiver.DenonAVR.manufacturer", TEST_MANUFACTURER, ), patch( - "homeassistant.components.denonavr.receiver.denonavr.DenonAVR.receiver_type", + "homeassistant.components.denonavr.receiver.DenonAVR.receiver_type", TEST_RECEIVER_TYPE, - ), patch( - "homeassistant.components.denonavr.config_flow.get_mac_address", - return_value=TEST_MAC, ), patch( "homeassistant.components.denonavr.async_setup_entry", return_value=True ): @@ -102,7 +90,6 @@ async def test_config_flow_manual_host_success(hass): assert result["title"] == TEST_NAME assert result["data"] == { CONF_HOST: TEST_HOST, - CONF_MAC: TEST_MAC, CONF_MODEL: TEST_MODEL, CONF_TYPE: TEST_RECEIVER_TYPE, CONF_MANUFACTURER: TEST_MANUFACTURER, @@ -125,7 +112,7 @@ async def test_config_flow_manual_discover_1_success(hass): assert result["errors"] == {} with patch( - "homeassistant.components.denonavr.config_flow.denonavr.ssdp.identify_denonavr_receivers", + "homeassistant.components.denonavr.config_flow.denonavr.async_discover", return_value=TEST_DISCOVER_1_RECEIVER, ): result = await hass.config_entries.flow.async_configure( @@ -137,7 +124,6 @@ async def test_config_flow_manual_discover_1_success(hass): assert result["title"] == TEST_NAME assert result["data"] == { CONF_HOST: TEST_HOST, - CONF_MAC: TEST_MAC, CONF_MODEL: TEST_MODEL, CONF_TYPE: TEST_RECEIVER_TYPE, CONF_MANUFACTURER: TEST_MANUFACTURER, @@ -160,7 +146,7 @@ async def test_config_flow_manual_discover_2_success(hass): assert result["errors"] == {} with patch( - "homeassistant.components.denonavr.config_flow.denonavr.ssdp.identify_denonavr_receivers", + "homeassistant.components.denonavr.config_flow.denonavr.async_discover", return_value=TEST_DISCOVER_2_RECEIVER, ): result = await hass.config_entries.flow.async_configure( @@ -181,7 +167,6 @@ async def test_config_flow_manual_discover_2_success(hass): assert result["title"] == TEST_NAME assert result["data"] == { CONF_HOST: TEST_HOST2, - CONF_MAC: TEST_MAC, CONF_MODEL: TEST_MODEL, CONF_TYPE: TEST_RECEIVER_TYPE, CONF_MANUFACTURER: TEST_MANUFACTURER, @@ -204,7 +189,7 @@ async def test_config_flow_manual_discover_error(hass): assert result["errors"] == {} with patch( - "homeassistant.components.denonavr.config_flow.denonavr.ssdp.identify_denonavr_receivers", + "homeassistant.components.denonavr.config_flow.denonavr.async_discover", return_value=[], ): result = await hass.config_entries.flow.async_configure( @@ -232,119 +217,8 @@ async def test_config_flow_manual_host_no_serial(hass): assert result["errors"] == {} with patch( - "homeassistant.components.denonavr.receiver.denonavr.DenonAVR.serial_number", - None, - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_HOST: TEST_HOST}, - ) - - assert result["type"] == "create_entry" - assert result["title"] == TEST_NAME - assert result["data"] == { - CONF_HOST: TEST_HOST, - CONF_MAC: TEST_MAC, - CONF_MODEL: TEST_MODEL, - CONF_TYPE: TEST_RECEIVER_TYPE, - CONF_MANUFACTURER: TEST_MANUFACTURER, - CONF_SERIAL_NUMBER: None, - } - - -async def test_config_flow_manual_host_no_mac(hass): - """ - Successful flow manually initialized by the user. - - Host specified and an error getting the mac address. - """ - 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.denonavr.config_flow.get_mac_address", - return_value=None, - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_HOST: TEST_HOST}, - ) - - assert result["type"] == "create_entry" - assert result["title"] == TEST_NAME - assert result["data"] == { - CONF_HOST: TEST_HOST, - CONF_MAC: None, - CONF_MODEL: TEST_MODEL, - CONF_TYPE: TEST_RECEIVER_TYPE, - CONF_MANUFACTURER: TEST_MANUFACTURER, - CONF_SERIAL_NUMBER: TEST_SERIALNUMBER, - } - - -async def test_config_flow_manual_host_no_serial_no_mac(hass): - """ - Successful flow manually initialized by the user. - - Host specified and an error getting the serial number and mac address. - """ - 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.denonavr.receiver.denonavr.DenonAVR.serial_number", + "homeassistant.components.denonavr.receiver.DenonAVR.serial_number", None, - ), patch( - "homeassistant.components.denonavr.config_flow.get_mac_address", - return_value=None, - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_HOST: TEST_HOST}, - ) - - assert result["type"] == "create_entry" - assert result["title"] == TEST_NAME - assert result["data"] == { - CONF_HOST: TEST_HOST, - CONF_MAC: None, - CONF_MODEL: TEST_MODEL, - CONF_TYPE: TEST_RECEIVER_TYPE, - CONF_MANUFACTURER: TEST_MANUFACTURER, - CONF_SERIAL_NUMBER: None, - } - - -async def test_config_flow_manual_host_no_serial_no_mac_exception(hass): - """ - Successful flow manually initialized by the user. - - Host specified and an error getting the serial number and exception getting mac address. - """ - 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.denonavr.receiver.denonavr.DenonAVR.serial_number", - None, - ), patch( - "homeassistant.components.denonavr.config_flow.get_mac_address", - side_effect=OSError, ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -355,7 +229,6 @@ async def test_config_flow_manual_host_no_serial_no_mac_exception(hass): assert result["title"] == TEST_NAME assert result["data"] == { CONF_HOST: TEST_HOST, - CONF_MAC: None, CONF_MODEL: TEST_MODEL, CONF_TYPE: TEST_RECEIVER_TYPE, CONF_MANUFACTURER: TEST_MANUFACTURER, @@ -378,10 +251,10 @@ async def test_config_flow_manual_host_connection_error(hass): assert result["errors"] == {} with patch( - "homeassistant.components.denonavr.receiver.denonavr.DenonAVR.get_device_info", - side_effect=ConnectionError, + "homeassistant.components.denonavr.receiver.DenonAVR.async_setup", + side_effect=AvrTimoutError("Timeout", "async_setup"), ), patch( - "homeassistant.components.denonavr.receiver.denonavr.DenonAVR.receiver_type", + "homeassistant.components.denonavr.receiver.DenonAVR.receiver_type", None, ): result = await hass.config_entries.flow.async_configure( @@ -408,7 +281,7 @@ async def test_config_flow_manual_host_no_device_info(hass): assert result["errors"] == {} with patch( - "homeassistant.components.denonavr.receiver.denonavr.DenonAVR.receiver_type", + "homeassistant.components.denonavr.receiver.DenonAVR.receiver_type", None, ): result = await hass.config_entries.flow.async_configure( @@ -445,7 +318,6 @@ async def test_config_flow_ssdp(hass): assert result["title"] == TEST_NAME assert result["data"] == { CONF_HOST: TEST_HOST, - CONF_MAC: TEST_MAC, CONF_MODEL: TEST_MODEL, CONF_TYPE: TEST_RECEIVER_TYPE, CONF_MANUFACTURER: TEST_MANUFACTURER, @@ -521,7 +393,6 @@ async def test_options_flow(hass): unique_id=TEST_UNIQUE_ID, data={ CONF_HOST: TEST_HOST, - CONF_MAC: TEST_MAC, CONF_MODEL: TEST_MODEL, CONF_TYPE: TEST_RECEIVER_TYPE, CONF_MANUFACTURER: TEST_MANUFACTURER, @@ -567,7 +438,7 @@ async def test_config_flow_manual_host_no_serial_double_config(hass): assert result["errors"] == {} with patch( - "homeassistant.components.denonavr.receiver.denonavr.DenonAVR.serial_number", + "homeassistant.components.denonavr.receiver.DenonAVR.serial_number", None, ): result = await hass.config_entries.flow.async_configure( @@ -579,7 +450,6 @@ async def test_config_flow_manual_host_no_serial_double_config(hass): assert result["title"] == TEST_NAME assert result["data"] == { CONF_HOST: TEST_HOST, - CONF_MAC: TEST_MAC, CONF_MODEL: TEST_MODEL, CONF_TYPE: TEST_RECEIVER_TYPE, CONF_MANUFACTURER: TEST_MANUFACTURER, @@ -595,7 +465,7 @@ async def test_config_flow_manual_host_no_serial_double_config(hass): assert result["errors"] == {} with patch( - "homeassistant.components.denonavr.receiver.denonavr.DenonAVR.serial_number", + "homeassistant.components.denonavr.receiver.DenonAVR.serial_number", None, ): result = await hass.config_entries.flow.async_configure( diff --git a/tests/components/denonavr/test_media_player.py b/tests/components/denonavr/test_media_player.py index bb9f83b58d747..71c873a2b9d72 100644 --- a/tests/components/denonavr/test_media_player.py +++ b/tests/components/denonavr/test_media_player.py @@ -4,7 +4,6 @@ import pytest from homeassistant.components import media_player -from homeassistant.components.denonavr import ATTR_COMMAND, SERVICE_GET_COMMAND from homeassistant.components.denonavr.config_flow import ( CONF_MANUFACTURER, CONF_MODEL, @@ -12,12 +11,15 @@ CONF_TYPE, DOMAIN, ) -from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_MAC +from homeassistant.components.denonavr.media_player import ( + ATTR_COMMAND, + SERVICE_GET_COMMAND, +) +from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST from tests.common import MockConfigEntry TEST_HOST = "1.2.3.4" -TEST_MAC = "ab:cd:ef:gh" TEST_NAME = "Test_Receiver" TEST_MODEL = "model5" TEST_SERIALNUMBER = "123456789" @@ -36,10 +38,10 @@ def client_fixture(): """Patch of client library for tests.""" with patch( - "homeassistant.components.denonavr.receiver.denonavr.DenonAVR", + "homeassistant.components.denonavr.receiver.DenonAVR", autospec=True, ) as mock_client_class, patch( - "homeassistant.components.denonavr.receiver.denonavr.discover" + "homeassistant.components.denonavr.config_flow.denonavr.async_discover" ): mock_client_class.return_value.name = TEST_NAME mock_client_class.return_value.model_name = TEST_MODEL @@ -57,7 +59,6 @@ async def setup_denonavr(hass): """Initialize media_player for tests.""" entry_data = { CONF_HOST: TEST_HOST, - CONF_MAC: TEST_MAC, CONF_MODEL: TEST_MODEL, CONF_TYPE: TEST_RECEIVER_TYPE, CONF_MANUFACTURER: TEST_MANUFACTURER, @@ -92,4 +93,4 @@ async def test_get_command(hass, client): await hass.services.async_call(DOMAIN, SERVICE_GET_COMMAND, data) await hass.async_block_till_done() - client.send_get_command.assert_called_with("test_command") + client.async_get_command.assert_awaited_with("test_command") From 0d7168a6679ed1cf71dfed7ba9a74553a71f8017 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 2 Apr 2021 22:09:27 +0200 Subject: [PATCH 0033/1317] Remove duplicate test case in modbus switch (#48636) --- tests/components/modbus/test_modbus_switch.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/components/modbus/test_modbus_switch.py b/tests/components/modbus/test_modbus_switch.py index 8af8f3067e12b..59d87e146b9b9 100644 --- a/tests/components/modbus/test_modbus_switch.py +++ b/tests/components/modbus/test_modbus_switch.py @@ -40,12 +40,6 @@ CONF_ADDRESS: 1234, }, ), - ( - None, - { - CONF_ADDRESS: 1234, - }, - ), ( None, { From cffdbfe13cf87241645b38fe6b639f84983a149e Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 2 Apr 2021 23:11:39 +0200 Subject: [PATCH 0034/1317] Updated frontend to 20210402.1 (#48639) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 60ea0ff53b2e5..55392323f3d91 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20210402.0" + "home-assistant-frontend==20210402.1" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 14910dacf760d..4dc35a119d36b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ defusedxml==0.6.0 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.42.0 -home-assistant-frontend==20210402.0 +home-assistant-frontend==20210402.1 httpx==0.17.1 jinja2>=2.11.3 netdisco==2.8.2 diff --git a/requirements_all.txt b/requirements_all.txt index 7b60bcd4ee912..3ea0358d62d03 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -759,7 +759,7 @@ hole==0.5.1 holidays==0.10.5.2 # homeassistant.components.frontend -home-assistant-frontend==20210402.0 +home-assistant-frontend==20210402.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cc7367159562d..f892b6d369b3b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -414,7 +414,7 @@ hole==0.5.1 holidays==0.10.5.2 # homeassistant.components.frontend -home-assistant-frontend==20210402.0 +home-assistant-frontend==20210402.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From e882460933f4f8dda3a55fa26ade3ef1125f8c9b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 2 Apr 2021 16:57:16 -0700 Subject: [PATCH 0035/1317] Support modern config for the trigger based template entity (#48635) --- homeassistant/components/template/__init__.py | 20 ++-- homeassistant/components/template/config.py | 91 +++++++++++++++++-- homeassistant/components/template/const.py | 4 + homeassistant/components/template/sensor.py | 11 ++- .../components/template/trigger_entity.py | 79 +++++++--------- homeassistant/helpers/template.py | 4 +- tests/components/template/test_sensor.py | 40 ++++++-- 7 files changed, 173 insertions(+), 76 deletions(-) diff --git a/homeassistant/components/template/__init__.py b/homeassistant/components/template/__init__.py index 3481b5adac646..f9b6b3b497577 100644 --- a/homeassistant/components/template/__init__.py +++ b/homeassistant/components/template/__init__.py @@ -2,7 +2,8 @@ import logging from typing import Optional -from homeassistant.const import CONF_SENSORS, EVENT_HOMEASSISTANT_START +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.core import CoreState, callback from homeassistant.helpers import ( discovery, @@ -51,15 +52,16 @@ async def async_setup(self, hass_config): EVENT_HOMEASSISTANT_START, self._attach_triggers ) - self.hass.async_create_task( - discovery.async_load_platform( - self.hass, - "sensor", - DOMAIN, - {"coordinator": self, "entities": self.config[CONF_SENSORS]}, - hass_config, + for platform_domain in (SENSOR_DOMAIN,): + self.hass.async_create_task( + discovery.async_load_platform( + self.hass, + platform_domain, + DOMAIN, + {"coordinator": self, "entities": self.config[platform_domain]}, + hass_config, + ) ) - ) async def _attach_triggers(self, start_event=None) -> None: """Attach the triggers.""" diff --git a/homeassistant/components/template/config.py b/homeassistant/components/template/config.py index fa0d9a867d1ab..edef5673f31c9 100644 --- a/homeassistant/components/template/config.py +++ b/homeassistant/components/template/config.py @@ -1,23 +1,72 @@ """Template config validator.""" +import logging import voluptuous as vol +from homeassistant.components.sensor import ( + DEVICE_CLASSES_SCHEMA as SENSOR_DEVICE_CLASSES_SCHEMA, + DOMAIN as SENSOR_DOMAIN, +) from homeassistant.config import async_log_exception, config_without_domain -from homeassistant.const import CONF_SENSORS, CONF_UNIQUE_ID -from homeassistant.helpers import config_validation as cv +from homeassistant.const import ( + CONF_DEVICE_CLASS, + CONF_ENTITY_PICTURE_TEMPLATE, + CONF_FRIENDLY_NAME, + CONF_FRIENDLY_NAME_TEMPLATE, + CONF_ICON, + CONF_ICON_TEMPLATE, + CONF_NAME, + CONF_SENSORS, + CONF_STATE, + CONF_UNIQUE_ID, + CONF_UNIT_OF_MEASUREMENT, + CONF_VALUE_TEMPLATE, +) +from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.trigger import async_validate_trigger_config -from .const import CONF_TRIGGER, DOMAIN -from .sensor import SENSOR_SCHEMA +from .const import ( + CONF_ATTRIBUTE_TEMPLATES, + CONF_ATTRIBUTES, + CONF_AVAILABILITY, + CONF_AVAILABILITY_TEMPLATE, + CONF_PICTURE, + CONF_TRIGGER, + DOMAIN, +) +from .sensor import SENSOR_SCHEMA as PLATFORM_SENSOR_SCHEMA + +CONVERSION_PLATFORM = { + CONF_ICON_TEMPLATE: CONF_ICON, + CONF_ENTITY_PICTURE_TEMPLATE: CONF_PICTURE, + CONF_AVAILABILITY_TEMPLATE: CONF_AVAILABILITY, + CONF_ATTRIBUTE_TEMPLATES: CONF_ATTRIBUTES, + CONF_FRIENDLY_NAME_TEMPLATE: CONF_NAME, + CONF_FRIENDLY_NAME: CONF_NAME, + CONF_VALUE_TEMPLATE: CONF_STATE, +} -CONF_STATE = "state" +SENSOR_SCHEMA = vol.Schema( + { + vol.Optional(CONF_NAME): cv.template, + vol.Required(CONF_STATE): cv.template, + vol.Optional(CONF_ICON): cv.template, + vol.Optional(CONF_PICTURE): cv.template, + vol.Optional(CONF_AVAILABILITY): cv.template, + vol.Optional(CONF_ATTRIBUTES): vol.Schema({cv.string: cv.template}), + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + vol.Optional(CONF_DEVICE_CLASS): SENSOR_DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_UNIQUE_ID): cv.string, + } +) TRIGGER_ENTITY_SCHEMA = vol.Schema( { vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Required(CONF_TRIGGER): cv.TRIGGER_SCHEMA, - vol.Required(CONF_SENSORS): cv.schema_with_slug_keys(SENSOR_SCHEMA), + vol.Optional(SENSOR_DOMAIN): vol.All(cv.ensure_list, [SENSOR_SCHEMA]), + vol.Optional(CONF_SENSORS): cv.schema_with_slug_keys(PLATFORM_SENSOR_SCHEMA), } ) @@ -37,9 +86,37 @@ async def async_validate_config(hass, config): ) except vol.Invalid as err: async_log_exception(err, DOMAIN, cfg, hass) + continue - else: + if CONF_SENSORS not in cfg: trigger_entity_configs.append(cfg) + continue + + logging.getLogger(__name__).warning( + "The entity definition format under template: differs from the platform configuration format. See https://www.home-assistant.io/integrations/template#configuration-for-trigger-based-template-sensors" + ) + sensor = list(cfg[SENSOR_DOMAIN]) if SENSOR_DOMAIN in cfg else [] + + for device_id, entity_cfg in cfg[CONF_SENSORS].items(): + entity_cfg = {**entity_cfg} + + for from_key, to_key in CONVERSION_PLATFORM.items(): + if from_key not in entity_cfg or to_key in entity_cfg: + continue + + val = entity_cfg.pop(from_key) + if isinstance(val, str): + val = template.Template(val) + entity_cfg[to_key] = val + + if CONF_NAME not in entity_cfg: + entity_cfg[CONF_NAME] = template.Template(device_id) + + sensor.append(entity_cfg) + + cfg = {**cfg, "sensor": sensor} + + trigger_entity_configs.append(cfg) # Create a copy of the configuration with all config for current # component removed and add validated config back in. diff --git a/homeassistant/components/template/const.py b/homeassistant/components/template/const.py index 2f2bc3127d72a..971d4a864c95c 100644 --- a/homeassistant/components/template/const.py +++ b/homeassistant/components/template/const.py @@ -20,3 +20,7 @@ "vacuum", "weather", ] + +CONF_AVAILABILITY = "availability" +CONF_ATTRIBUTES = "attributes" +CONF_PICTURE = "picture" diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index a5f5d669b169e..4631a77584710 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -18,6 +18,7 @@ CONF_FRIENDLY_NAME_TEMPLATE, CONF_ICON_TEMPLATE, CONF_SENSORS, + CONF_STATE, CONF_UNIQUE_ID, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, @@ -89,7 +90,7 @@ def _async_create_template_tracking_entities(hass, config): friendly_name_template = device_config.get(CONF_FRIENDLY_NAME_TEMPLATE) unit_of_measurement = device_config.get(CONF_UNIT_OF_MEASUREMENT) device_class = device_config.get(CONF_DEVICE_CLASS) - attribute_templates = device_config[CONF_ATTRIBUTE_TEMPLATES] + attribute_templates = device_config.get(CONF_ATTRIBUTE_TEMPLATES, {}) unique_id = device_config.get(CONF_UNIQUE_ID) sensors.append( @@ -118,8 +119,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(_async_create_template_tracking_entities(hass, config)) else: async_add_entities( - TriggerSensorEntity(hass, discovery_info["coordinator"], device_id, config) - for device_id, config in discovery_info["entities"].items() + TriggerSensorEntity(hass, discovery_info["coordinator"], config) + for config in discovery_info["entities"] ) @@ -203,9 +204,9 @@ class TriggerSensorEntity(TriggerEntity, SensorEntity): """Sensor entity based on trigger data.""" domain = SENSOR_DOMAIN - extra_template_keys = (CONF_VALUE_TEMPLATE,) + extra_template_keys = (CONF_STATE,) @property def state(self) -> str | None: """Return state of the sensor.""" - return self._rendered.get(CONF_VALUE_TEMPLATE) + return self._rendered.get(CONF_STATE) diff --git a/homeassistant/components/template/trigger_entity.py b/homeassistant/components/template/trigger_entity.py index 3874409dc7805..418fa976304d7 100644 --- a/homeassistant/components/template/trigger_entity.py +++ b/homeassistant/components/template/trigger_entity.py @@ -6,20 +6,16 @@ from homeassistant.const import ( CONF_DEVICE_CLASS, - CONF_ENTITY_PICTURE_TEMPLATE, - CONF_FRIENDLY_NAME, - CONF_FRIENDLY_NAME_TEMPLATE, - CONF_ICON_TEMPLATE, + CONF_ICON, + CONF_NAME, CONF_UNIQUE_ID, CONF_UNIT_OF_MEASUREMENT, - CONF_VALUE_TEMPLATE, ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import template, update_coordinator -from homeassistant.helpers.entity import async_generate_entity_id from . import TriggerUpdateCoordinator -from .const import CONF_ATTRIBUTE_TEMPLATES, CONF_AVAILABILITY_TEMPLATE +from .const import CONF_ATTRIBUTES, CONF_AVAILABILITY, CONF_PICTURE class TriggerEntity(update_coordinator.CoordinatorEntity): @@ -32,23 +28,13 @@ def __init__( self, hass: HomeAssistant, coordinator: TriggerUpdateCoordinator, - device_id: str, config: dict, ): """Initialize the entity.""" super().__init__(coordinator) - self.entity_id = async_generate_entity_id( - self.domain + ".{}", device_id, hass=hass - ) - - self._name = config.get(CONF_FRIENDLY_NAME, device_id) - entity_unique_id = config.get(CONF_UNIQUE_ID) - if entity_unique_id is None and coordinator.unique_id: - entity_unique_id = device_id - if entity_unique_id and coordinator.unique_id: self._unique_id = f"{coordinator.unique_id}-{entity_unique_id}" else: @@ -56,32 +42,33 @@ def __init__( self._config = config - self._to_render = [ - itm - for itm in ( - CONF_VALUE_TEMPLATE, - CONF_ICON_TEMPLATE, - CONF_ENTITY_PICTURE_TEMPLATE, - CONF_FRIENDLY_NAME_TEMPLATE, - CONF_AVAILABILITY_TEMPLATE, - ) - if itm in config - ] + self._static_rendered = {} + self._to_render = [] + + for itm in ( + CONF_NAME, + CONF_ICON, + CONF_PICTURE, + CONF_AVAILABILITY, + ): + if itm not in config: + continue + + if config[itm].is_static: + self._static_rendered[itm] = config[itm].template + else: + self._to_render.append(itm) if self.extra_template_keys is not None: self._to_render.extend(self.extra_template_keys) - self._rendered = {} + # We make a copy so our initial render is 'unknown' and not 'unavailable' + self._rendered = dict(self._static_rendered) @property def name(self): """Name of the entity.""" - if ( - self._rendered is not None - and (name := self._rendered.get(CONF_FRIENDLY_NAME_TEMPLATE)) is not None - ): - return name - return self._name + return self._rendered.get(CONF_NAME) @property def unique_id(self): @@ -101,29 +88,27 @@ def unit_of_measurement(self) -> str | None: @property def icon(self) -> str | None: """Return icon.""" - return self._rendered is not None and self._rendered.get(CONF_ICON_TEMPLATE) + return self._rendered.get(CONF_ICON) @property def entity_picture(self) -> str | None: """Return entity picture.""" - return self._rendered is not None and self._rendered.get( - CONF_ENTITY_PICTURE_TEMPLATE - ) + return self._rendered.get(CONF_PICTURE) @property def available(self): """Return availability of the entity.""" return ( - self._rendered is not None + self._rendered is not self._static_rendered and # Check against False so `None` is ok - self._rendered.get(CONF_AVAILABILITY_TEMPLATE) is not False + self._rendered.get(CONF_AVAILABILITY) is not False ) @property def extra_state_attributes(self) -> dict[str, Any] | None: """Return extra attributes.""" - return self._rendered.get(CONF_ATTRIBUTE_TEMPLATES) + return self._rendered.get(CONF_ATTRIBUTES) async def async_added_to_hass(self) -> None: """Handle being added to Home Assistant.""" @@ -136,16 +121,16 @@ async def async_added_to_hass(self) -> None: def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" try: - rendered = {} + rendered = dict(self._static_rendered) for key in self._to_render: rendered[key] = self._config[key].async_render( self.coordinator.data["run_variables"], parse_result=False ) - if CONF_ATTRIBUTE_TEMPLATES in self._config: - rendered[CONF_ATTRIBUTE_TEMPLATES] = template.render_complex( - self._config[CONF_ATTRIBUTE_TEMPLATES], + if CONF_ATTRIBUTES in self._config: + rendered[CONF_ATTRIBUTES] = template.render_complex( + self._config[CONF_ATTRIBUTES], self.coordinator.data["run_variables"], ) @@ -154,7 +139,7 @@ def _handle_coordinator_update(self) -> None: logging.getLogger(f"{__package__}.{self.entity_id.split('.')[0]}").error( "Error rendering %s template for %s: %s", key, self.entity_id, err ) - self._rendered = None + self._rendered = self._static_rendered self.async_set_context(self.coordinator.data["context"]) self.async_write_ha_state() diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 315efd1451684..4989c4172aea5 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -336,7 +336,7 @@ def render( If limited is True, the template is not allowed to access any function or filter depending on hass or the state machine. """ if self.is_static: - if self.hass.config.legacy_templates or not parse_result: + if not parse_result or self.hass.config.legacy_templates: return self.template return self._parse_result(self.template) @@ -360,7 +360,7 @@ def async_render( If limited is True, the template is not allowed to access any function or filter depending on hass or the state machine. """ if self.is_static: - if self.hass.config.legacy_templates or not parse_result: + if not parse_result or self.hass.config.legacy_templates: return self.template return self._parse_result(self.template) diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py index 6aa1e75cc1f0e..d146f5d88defa 100644 --- a/tests/components/template/test_sensor.py +++ b/tests/components/template/test_sensor.py @@ -998,14 +998,14 @@ async def test_trigger_entity(hass): { "template": [ {"invalid": "config"}, - # This one should still be set up + # Config after invalid should still be set up { "unique_id": "listening-test-event", "trigger": {"platform": "event", "event_type": "test_event"}, "sensors": { "hello": { "friendly_name": "Hello Name", - "unique_id": "just_a_test", + "unique_id": "hello_name-id", "device_class": "battery", "unit_of_measurement": "%", "value_template": "{{ trigger.event.data.beer }}", @@ -1016,6 +1016,20 @@ async def test_trigger_entity(hass): }, }, }, + "sensor": [ + { + "name": "via list", + "unique_id": "via_list-id", + "device_class": "battery", + "unit_of_measurement": "%", + "state": "{{ trigger.event.data.beer + 1 }}", + "picture": "{{ '/local/dogs.png' }}", + "icon": "{{ 'mdi:pirate' }}", + "attributes": { + "plus_one": "{{ trigger.event.data.beer + 1 }}" + }, + } + ], }, { "trigger": [], @@ -1031,7 +1045,7 @@ async def test_trigger_entity(hass): await hass.async_block_till_done() - state = hass.states.get("sensor.hello") + state = hass.states.get("sensor.hello_name") assert state is not None assert state.state == STATE_UNKNOWN @@ -1043,7 +1057,7 @@ async def test_trigger_entity(hass): hass.bus.async_fire("test_event", {"beer": 2}, context=context) await hass.async_block_till_done() - state = hass.states.get("sensor.hello") + state = hass.states.get("sensor.hello_name") assert state.state == "2" assert state.attributes.get("device_class") == "battery" assert state.attributes.get("icon") == "mdi:pirate" @@ -1053,10 +1067,24 @@ async def test_trigger_entity(hass): assert state.context is context ent_reg = entity_registry.async_get(hass) - assert len(ent_reg.entities) == 1 + assert len(ent_reg.entities) == 2 assert ( - ent_reg.entities["sensor.hello"].unique_id == "listening-test-event-just_a_test" + ent_reg.entities["sensor.hello_name"].unique_id + == "listening-test-event-hello_name-id" ) + assert ( + ent_reg.entities["sensor.via_list"].unique_id + == "listening-test-event-via_list-id" + ) + + state = hass.states.get("sensor.via_list") + assert state.state == "3" + assert state.attributes.get("device_class") == "battery" + assert state.attributes.get("icon") == "mdi:pirate" + assert state.attributes.get("entity_picture") == "/local/dogs.png" + assert state.attributes.get("plus_one") == 3 + assert state.attributes.get("unit_of_measurement") == "%" + assert state.context is context async def test_trigger_entity_render_error(hass): From 176b6daf2a5e61da1dd7d1324adc69442565a243 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sat, 3 Apr 2021 00:03:39 +0000 Subject: [PATCH 0036/1317] [ci skip] Translation update --- homeassistant/components/blink/translations/nl.json | 2 +- homeassistant/components/emulated_roku/translations/nl.json | 4 ++-- homeassistant/components/enocean/translations/nl.json | 2 +- homeassistant/components/homekit/translations/nl.json | 2 +- homeassistant/components/rachio/translations/nl.json | 2 +- homeassistant/components/toon/translations/nl.json | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/blink/translations/nl.json b/homeassistant/components/blink/translations/nl.json index 3160ffe8ddd43..bce18bfda4756 100644 --- a/homeassistant/components/blink/translations/nl.json +++ b/homeassistant/components/blink/translations/nl.json @@ -14,7 +14,7 @@ "data": { "2fa": "Twee-factor code" }, - "description": "Voer de pincode in die naar uw e-mail is gestuurd. Als de e-mail geen pincode bevat, laat u dit leeg", + "description": "Voer de pincode in die naar uw e-mail is gestuurd.", "title": "Tweestapsverificatie" }, "user": { diff --git a/homeassistant/components/emulated_roku/translations/nl.json b/homeassistant/components/emulated_roku/translations/nl.json index dd98898525034..d9510824ecf62 100644 --- a/homeassistant/components/emulated_roku/translations/nl.json +++ b/homeassistant/components/emulated_roku/translations/nl.json @@ -6,8 +6,8 @@ "step": { "user": { "data": { - "advertise_ip": "IP-adres zichtbaar", - "advertise_port": "Adverteer Poort", + "advertise_ip": "Toegekend IP-adres", + "advertise_port": "Toegekende Poort", "host_ip": "Host IP", "listen_port": "Luisterpoort", "name": "Naam", diff --git a/homeassistant/components/enocean/translations/nl.json b/homeassistant/components/enocean/translations/nl.json index c7dd498513371..79e0ab6dfec13 100644 --- a/homeassistant/components/enocean/translations/nl.json +++ b/homeassistant/components/enocean/translations/nl.json @@ -16,7 +16,7 @@ }, "manual": { "data": { - "path": "USB-dongle-pad" + "path": "USB-dongle pad" }, "title": "Voer het pad naar uw ENOcean dongle in" } diff --git a/homeassistant/components/homekit/translations/nl.json b/homeassistant/components/homekit/translations/nl.json index 1c65188ee6df9..154f271e1a33b 100644 --- a/homeassistant/components/homekit/translations/nl.json +++ b/homeassistant/components/homekit/translations/nl.json @@ -28,7 +28,7 @@ "include_domains": "Domeinen om op te nemen", "mode": "Mode" }, - "description": "De HomeKit-integratie geeft u toegang tot uw Home Assistant-entiteiten in HomeKit. In bridge-modus zijn HomeKit-bruggen beperkt tot 150 accessoires per exemplaar, inclusief de brug zelf. Als u meer dan het maximale aantal accessoires wilt overbruggen, is het aan te raden om meerdere HomeKit-bridges voor verschillende domeinen te gebruiken. Gedetailleerde entiteitsconfiguratie is alleen beschikbaar via YAML voor de primaire bridge.", + "description": "Kies de domeinen die moeten worden opgenomen. Alle ondersteunde entiteiten in het domein zullen worden opgenomen. Voor elke tv-mediaspeler en camera wordt een afzonderlijke HomeKit-instantie in accessoiremodus aangemaakt.", "title": "Selecteer domeinen die u wilt opnemen" } } diff --git a/homeassistant/components/rachio/translations/nl.json b/homeassistant/components/rachio/translations/nl.json index 6a94ac2dcd41e..7071401a1672d 100644 --- a/homeassistant/components/rachio/translations/nl.json +++ b/homeassistant/components/rachio/translations/nl.json @@ -22,7 +22,7 @@ "step": { "init": { "data": { - "manual_run_mins": "Hoe lang, in minuten, om een station in te schakelen wanneer de schakelaar is ingeschakeld." + "manual_run_mins": "Looptijd in minuten bij activering van een zoneschakelaar" } } } diff --git a/homeassistant/components/toon/translations/nl.json b/homeassistant/components/toon/translations/nl.json index 69ae8aa127f72..687efce4a4235 100644 --- a/homeassistant/components/toon/translations/nl.json +++ b/homeassistant/components/toon/translations/nl.json @@ -18,7 +18,7 @@ "title": "Kies uw overeenkomst" }, "pick_implementation": { - "title": "Kies uw tenant om mee te authenticeren" + "title": "Kies uw leverancier om mee te authenticeren" } } } From cee43b0670b37362a9e74984e472e05df1dd53aa Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sat, 3 Apr 2021 11:00:06 +0200 Subject: [PATCH 0037/1317] Add modbus CONF_VERIFY_STATE to new switch config (#48632) Missed CONF_VERIFY_STATE in new switch config, when copying from old switch config. --- homeassistant/components/modbus/__init__.py | 2 ++ tests/components/modbus/test_modbus_switch.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 98b1b17090553..acb31a7a7303a 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -78,6 +78,7 @@ CONF_TARGET_TEMP, CONF_UNIT, CONF_VERIFY_REGISTER, + CONF_VERIFY_STATE, DATA_TYPE_CUSTOM, DATA_TYPE_FLOAT, DATA_TYPE_INT, @@ -178,6 +179,7 @@ def number(value: Any) -> Union[int, float]: vol.Optional(CONF_STATE_OFF): cv.positive_int, vol.Optional(CONF_STATE_ON): cv.positive_int, vol.Optional(CONF_VERIFY_REGISTER): cv.positive_int, + vol.Optional(CONF_VERIFY_STATE, default=True): cv.boolean, } ) diff --git a/tests/components/modbus/test_modbus_switch.py b/tests/components/modbus/test_modbus_switch.py index 59d87e146b9b9..a6ec1eb86fd89 100644 --- a/tests/components/modbus/test_modbus_switch.py +++ b/tests/components/modbus/test_modbus_switch.py @@ -55,6 +55,7 @@ CONF_STATE_OFF: 0, CONF_STATE_ON: 1, CONF_VERIFY_REGISTER: 1235, + CONF_VERIFY_STATE: False, CONF_COMMAND_OFF: 0x00, CONF_COMMAND_ON: 0x01, CONF_DEVICE_CLASS: "switch", @@ -69,6 +70,7 @@ CONF_STATE_OFF: 0, CONF_STATE_ON: 1, CONF_VERIFY_REGISTER: 1235, + CONF_VERIFY_STATE: True, CONF_COMMAND_OFF: 0x00, CONF_COMMAND_ON: 0x01, CONF_DEVICE_CLASS: "switch", From 2c61c0f258f28918fc985ccbe50572471d2e3077 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Sat, 3 Apr 2021 11:17:17 +0200 Subject: [PATCH 0038/1317] Fix AEMET town timestamp format (#48647) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Datetime should be converted to ISO format. Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/aemet/weather_update_coordinator.py | 2 +- tests/components/aemet/test_sensor.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/aemet/weather_update_coordinator.py b/homeassistant/components/aemet/weather_update_coordinator.py index a9af8f25f1c09..a7ca0a1242247 100644 --- a/homeassistant/components/aemet/weather_update_coordinator.py +++ b/homeassistant/components/aemet/weather_update_coordinator.py @@ -283,7 +283,7 @@ def _convert_weather_response(self, weather_response): temperature_feeling = None town_id = None town_name = None - town_timestamp = dt_util.as_utc(elaborated) + town_timestamp = dt_util.as_utc(elaborated).isoformat() wind_bearing = None wind_max_speed = None wind_speed = None diff --git a/tests/components/aemet/test_sensor.py b/tests/components/aemet/test_sensor.py index b265b9967097e..7887139a38609 100644 --- a/tests/components/aemet/test_sensor.py +++ b/tests/components/aemet/test_sensor.py @@ -127,7 +127,7 @@ async def test_aemet_weather_create_sensors(hass): assert state.state == "Getafe" state = hass.states.get("sensor.aemet_town_timestamp") - assert state.state == "2021-01-09 11:47:45+00:00" + assert state.state == "2021-01-09T11:47:45+00:00" state = hass.states.get("sensor.aemet_wind_bearing") assert state.state == "90.0" From b7ae06f1bbd7dcc266f53266838936d817506828 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 2 Apr 2021 23:33:45 -1000 Subject: [PATCH 0039/1317] Bump aiodiscover to 1.3.3 for dhcp (#48644) fixes #48615 --- homeassistant/components/dhcp/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/dhcp/manifest.json b/homeassistant/components/dhcp/manifest.json index 817ee9acac54b..80cc6b116c96e 100644 --- a/homeassistant/components/dhcp/manifest.json +++ b/homeassistant/components/dhcp/manifest.json @@ -3,7 +3,7 @@ "name": "DHCP Discovery", "documentation": "https://www.home-assistant.io/integrations/dhcp", "requirements": [ - "scapy==2.4.4", "aiodiscover==1.3.2" + "scapy==2.4.4", "aiodiscover==1.3.3" ], "codeowners": [ "@bdraco" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 4dc35a119d36b..7c8c7baf34182 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,6 +1,6 @@ PyJWT==1.7.1 PyNaCl==1.3.0 -aiodiscover==1.3.2 +aiodiscover==1.3.3 aiohttp==3.7.4.post0 aiohttp_cors==0.7.0 astral==2.2 diff --git a/requirements_all.txt b/requirements_all.txt index 3ea0358d62d03..d3f18a7c8777e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -147,7 +147,7 @@ aioazuredevops==1.3.5 aiobotocore==0.11.1 # homeassistant.components.dhcp -aiodiscover==1.3.2 +aiodiscover==1.3.3 # homeassistant.components.dnsip # homeassistant.components.minecraft_server diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f892b6d369b3b..fe42e3bf73c58 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -84,7 +84,7 @@ aioazuredevops==1.3.5 aiobotocore==0.11.1 # homeassistant.components.dhcp -aiodiscover==1.3.2 +aiodiscover==1.3.3 # homeassistant.components.dnsip # homeassistant.components.minecraft_server From 23fae255fff9aac56f760f59c998d5ac51bcb3b6 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sat, 3 Apr 2021 13:15:01 +0200 Subject: [PATCH 0040/1317] Make modbus WRITE_COIL use write_coils in case of an array (#48633) * WRITE_COIL uses write_coils in case of an array. WRITE_REGISTER uses write_register/write_registers depending on whether value is singular or an array. WRITE_COIL is modified to be similar and uses write_coil/write_coils depending on whether value is singular or an array. * Update SERVICE_WRITE_COIL to allow list. --- homeassistant/components/modbus/__init__.py | 4 +++- homeassistant/components/modbus/modbus.py | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index acb31a7a7303a..f1f1e65680549 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -282,7 +282,9 @@ def number(value: Any) -> Union[int, float]: vol.Optional(ATTR_HUB, default=DEFAULT_HUB): cv.string, vol.Required(ATTR_UNIT): cv.positive_int, vol.Required(ATTR_ADDRESS): cv.positive_int, - vol.Required(ATTR_STATE): cv.boolean, + vol.Required(ATTR_STATE): vol.Any( + cv.boolean, vol.All(cv.ensure_list, [cv.boolean]) + ), } ) diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index 554b7bfb85eab..f55e77c911950 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -93,7 +93,10 @@ def write_coil(service): address = service.data[ATTR_ADDRESS] state = service.data[ATTR_STATE] client_name = service.data[ATTR_HUB] - hub_collect[client_name].write_coil(unit, address, state) + if isinstance(state, list): + hub_collect[client_name].write_coils(unit, address, state) + else: + hub_collect[client_name].write_coil(unit, address, state) # register function to gracefully stop modbus hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_modbus) From 545fe7a7bee2369a8dc5acc9e056b8f7815e811a Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Sat, 3 Apr 2021 16:42:09 -0400 Subject: [PATCH 0041/1317] Add Compensation Integration (#41675) * Add Compensation Integration Adds the Compensation Integration * Add Requirements add missing requirements to compensation integration * Fix for tests Fix files after tests * Fix isort ran isort * Handle ADR-0007 Change the configuration to deal with ADR-0007 * fix flake8 Fix flake8 * Added Error Trapping Catch errors. Raise Rank Warnings but continue. Fixed bad imports * fix flake8 & pylint * fix isort.... again * fix tests & comments fix tests and comments * fix flake8 * remove discovery message * Fixed Review changes * Fixed review requests. * Added test to test get more coverage. * Roll back numpy requirement Roll back numpy requirement to match other integrations. * Fix flake8 * Fix requested changes Removed some necessary comments. Changed a test case to be more readable. * Fix doc strings and continue * Fixed a few test case doc strings * Removed a continue/else * Remove periods from logger Removed periods from _LOGGER errors. * Fixes changed name to unqiue_id. implemented suggested changes. * Add name and fix unique_id * removed conf name and auto construct it --- CODEOWNERS | 1 + .../components/compensation/__init__.py | 120 +++++++++ .../components/compensation/const.py | 16 ++ .../components/compensation/manifest.json | 7 + .../components/compensation/sensor.py | 162 +++++++++++++ requirements_all.txt | 1 + requirements_test_all.txt | 1 + tests/components/compensation/__init__.py | 1 + tests/components/compensation/test_sensor.py | 228 ++++++++++++++++++ 9 files changed, 537 insertions(+) create mode 100644 homeassistant/components/compensation/__init__.py create mode 100644 homeassistant/components/compensation/const.py create mode 100644 homeassistant/components/compensation/manifest.json create mode 100644 homeassistant/components/compensation/sensor.py create mode 100644 tests/components/compensation/__init__.py create mode 100644 tests/components/compensation/test_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 70ea2385da8fe..62e1871192cc8 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -89,6 +89,7 @@ homeassistant/components/cloud/* @home-assistant/cloud homeassistant/components/cloudflare/* @ludeeus @ctalkington homeassistant/components/color_extractor/* @GenericStudent homeassistant/components/comfoconnect/* @michaelarnauts +homeassistant/components/compensation/* @Petro31 homeassistant/components/config/* @home-assistant/core homeassistant/components/configurator/* @home-assistant/core homeassistant/components/control4/* @lawtancool diff --git a/homeassistant/components/compensation/__init__.py b/homeassistant/components/compensation/__init__.py new file mode 100644 index 0000000000000..7d96905efa0ae --- /dev/null +++ b/homeassistant/components/compensation/__init__.py @@ -0,0 +1,120 @@ +"""The Compensation integration.""" +import logging +import warnings + +import numpy as np +import voluptuous as vol + +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.const import ( + CONF_ATTRIBUTE, + CONF_SOURCE, + CONF_UNIQUE_ID, + CONF_UNIT_OF_MEASUREMENT, +) +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.discovery import async_load_platform + +from .const import ( + CONF_COMPENSATION, + CONF_DATAPOINTS, + CONF_DEGREE, + CONF_POLYNOMIAL, + CONF_PRECISION, + DATA_COMPENSATION, + DEFAULT_DEGREE, + DEFAULT_PRECISION, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + + +def datapoints_greater_than_degree(value: dict) -> dict: + """Validate data point list is greater than polynomial degrees.""" + if len(value[CONF_DATAPOINTS]) <= value[CONF_DEGREE]: + raise vol.Invalid( + f"{CONF_DATAPOINTS} must have at least {value[CONF_DEGREE]+1} {CONF_DATAPOINTS}" + ) + + return value + + +COMPENSATION_SCHEMA = vol.Schema( + { + vol.Required(CONF_SOURCE): cv.entity_id, + vol.Required(CONF_DATAPOINTS): [ + vol.ExactSequence([vol.Coerce(float), vol.Coerce(float)]) + ], + vol.Optional(CONF_UNIQUE_ID): cv.string, + vol.Optional(CONF_ATTRIBUTE): cv.string, + vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): cv.positive_int, + vol.Optional(CONF_DEGREE, default=DEFAULT_DEGREE): vol.All( + vol.Coerce(int), + vol.Range(min=1, max=7), + ), + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + } +) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + {cv.slug: vol.All(COMPENSATION_SCHEMA, datapoints_greater_than_degree)} + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass, config): + """Set up the Compensation sensor.""" + hass.data[DATA_COMPENSATION] = {} + + for compensation, conf in config.get(DOMAIN).items(): + _LOGGER.debug("Setup %s.%s", DOMAIN, compensation) + + degree = conf[CONF_DEGREE] + + # get x values and y values from the x,y point pairs + x_values, y_values = zip(*conf[CONF_DATAPOINTS]) + + # try to get valid coefficients for a polynomial + coefficients = None + with np.errstate(all="raise"): + with warnings.catch_warnings(record=True) as all_warnings: + warnings.simplefilter("always") + try: + coefficients = np.polyfit(x_values, y_values, degree) + except FloatingPointError as error: + _LOGGER.error( + "Setup of %s encountered an error, %s", + compensation, + error, + ) + for warning in all_warnings: + _LOGGER.warning( + "Setup of %s encountered a warning, %s", + compensation, + str(warning.message).lower(), + ) + + if coefficients is not None: + data = { + k: v for k, v in conf.items() if k not in [CONF_DEGREE, CONF_DATAPOINTS] + } + data[CONF_POLYNOMIAL] = np.poly1d(coefficients) + + hass.data[DATA_COMPENSATION][compensation] = data + + hass.async_create_task( + async_load_platform( + hass, + SENSOR_DOMAIN, + DOMAIN, + {CONF_COMPENSATION: compensation}, + config, + ) + ) + + return True diff --git a/homeassistant/components/compensation/const.py b/homeassistant/components/compensation/const.py new file mode 100644 index 0000000000000..f116725883ecd --- /dev/null +++ b/homeassistant/components/compensation/const.py @@ -0,0 +1,16 @@ +"""Compensation constants.""" +DOMAIN = "compensation" + +SENSOR = "compensation" + +CONF_COMPENSATION = "compensation" +CONF_DATAPOINTS = "data_points" +CONF_DEGREE = "degree" +CONF_PRECISION = "precision" +CONF_POLYNOMIAL = "polynomial" + +DATA_COMPENSATION = "compensation_data" + +DEFAULT_DEGREE = 1 +DEFAULT_NAME = "Compensation" +DEFAULT_PRECISION = 2 diff --git a/homeassistant/components/compensation/manifest.json b/homeassistant/components/compensation/manifest.json new file mode 100644 index 0000000000000..86efbce72c87b --- /dev/null +++ b/homeassistant/components/compensation/manifest.json @@ -0,0 +1,7 @@ +{ + "domain": "compensation", + "name": "Compensation", + "documentation": "https://www.home-assistant.io/integrations/compensation", + "requirements": ["numpy==1.20.2"], + "codeowners": ["@Petro31"] +} diff --git a/homeassistant/components/compensation/sensor.py b/homeassistant/components/compensation/sensor.py new file mode 100644 index 0000000000000..35ca07ce52215 --- /dev/null +++ b/homeassistant/components/compensation/sensor.py @@ -0,0 +1,162 @@ +"""Support for compensation sensor.""" +import logging + +from homeassistant.components.sensor import SensorEntity +from homeassistant.const import ( + ATTR_UNIT_OF_MEASUREMENT, + CONF_ATTRIBUTE, + CONF_SOURCE, + CONF_UNIQUE_ID, + CONF_UNIT_OF_MEASUREMENT, + STATE_UNKNOWN, +) +from homeassistant.core import callback +from homeassistant.helpers.event import async_track_state_change_event + +from .const import ( + CONF_COMPENSATION, + CONF_POLYNOMIAL, + CONF_PRECISION, + DATA_COMPENSATION, + DEFAULT_NAME, +) + +_LOGGER = logging.getLogger(__name__) + +ATTR_COEFFICIENTS = "coefficients" +ATTR_SOURCE = "source" +ATTR_SOURCE_ATTRIBUTE = "source_attribute" + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the Compensation sensor.""" + if discovery_info is None: + return + + compensation = discovery_info[CONF_COMPENSATION] + conf = hass.data[DATA_COMPENSATION][compensation] + + source = conf[CONF_SOURCE] + attribute = conf.get(CONF_ATTRIBUTE) + name = f"{DEFAULT_NAME} {source}" + if attribute is not None: + name = f"{name} {attribute}" + + async_add_entities( + [ + CompensationSensor( + conf.get(CONF_UNIQUE_ID), + name, + source, + attribute, + conf[CONF_PRECISION], + conf[CONF_POLYNOMIAL], + conf.get(CONF_UNIT_OF_MEASUREMENT), + ) + ] + ) + + +class CompensationSensor(SensorEntity): + """Representation of a Compensation sensor.""" + + def __init__( + self, + unique_id, + name, + source, + attribute, + precision, + polynomial, + unit_of_measurement, + ): + """Initialize the Compensation sensor.""" + self._source_entity_id = source + self._precision = precision + self._source_attribute = attribute + self._unit_of_measurement = unit_of_measurement + self._poly = polynomial + self._coefficients = polynomial.coefficients.tolist() + self._state = None + self._unique_id = unique_id + self._name = name + + async def async_added_to_hass(self): + """Handle added to Hass.""" + self.async_on_remove( + async_track_state_change_event( + self.hass, + [self._source_entity_id], + self._async_compensation_sensor_state_listener, + ) + ) + + @property + def unique_id(self): + """Return the unique id of this sensor.""" + return self._unique_id + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def extra_state_attributes(self): + """Return the state attributes of the sensor.""" + ret = { + ATTR_SOURCE: self._source_entity_id, + ATTR_COEFFICIENTS: self._coefficients, + } + if self._source_attribute: + ret[ATTR_SOURCE_ATTRIBUTE] = self._source_attribute + return ret + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return self._unit_of_measurement + + @callback + def _async_compensation_sensor_state_listener(self, event): + """Handle sensor state changes.""" + new_state = event.data.get("new_state") + if new_state is None: + return + + if self._unit_of_measurement is None and self._source_attribute is None: + self._unit_of_measurement = new_state.attributes.get( + ATTR_UNIT_OF_MEASUREMENT + ) + + try: + if self._source_attribute: + value = float(new_state.attributes.get(self._source_attribute)) + else: + value = ( + None if new_state.state == STATE_UNKNOWN else float(new_state.state) + ) + self._state = round(self._poly(value), self._precision) + + except (ValueError, TypeError): + self._state = None + if self._source_attribute: + _LOGGER.warning( + "%s attribute %s is not numerical", + self._source_entity_id, + self._source_attribute, + ) + else: + _LOGGER.warning("%s state is not numerical", self._source_entity_id) + + self.async_write_ha_state() diff --git a/requirements_all.txt b/requirements_all.txt index d3f18a7c8777e..93a6601ee6b83 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1011,6 +1011,7 @@ nuheat==0.3.0 # homeassistant.components.numato numato-gpio==0.10.0 +# homeassistant.components.compensation # homeassistant.components.iqvia # homeassistant.components.opencv # homeassistant.components.tensorflow diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fe42e3bf73c58..cd9eb8db0338f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -529,6 +529,7 @@ nuheat==0.3.0 # homeassistant.components.numato numato-gpio==0.10.0 +# homeassistant.components.compensation # homeassistant.components.iqvia # homeassistant.components.opencv # homeassistant.components.tensorflow diff --git a/tests/components/compensation/__init__.py b/tests/components/compensation/__init__.py new file mode 100644 index 0000000000000..55d365adc0e58 --- /dev/null +++ b/tests/components/compensation/__init__.py @@ -0,0 +1 @@ +"""Tests for the compensation component.""" diff --git a/tests/components/compensation/test_sensor.py b/tests/components/compensation/test_sensor.py new file mode 100644 index 0000000000000..3bd86280750ff --- /dev/null +++ b/tests/components/compensation/test_sensor.py @@ -0,0 +1,228 @@ +"""The tests for the integration sensor platform.""" + +from homeassistant.components.compensation.const import CONF_PRECISION, DOMAIN +from homeassistant.components.compensation.sensor import ATTR_COEFFICIENTS +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.const import ( + ATTR_UNIT_OF_MEASUREMENT, + EVENT_HOMEASSISTANT_START, + EVENT_STATE_CHANGED, + STATE_UNKNOWN, +) +from homeassistant.setup import async_setup_component + + +async def test_linear_state(hass): + """Test compensation sensor state.""" + config = { + "compensation": { + "test": { + "source": "sensor.uncompensated", + "data_points": [ + [1.0, 2.0], + [2.0, 3.0], + ], + "precision": 2, + "unit_of_measurement": "a", + } + } + } + expected_entity_id = "sensor.compensation_sensor_uncompensated" + + assert await async_setup_component(hass, DOMAIN, config) + assert await async_setup_component(hass, SENSOR_DOMAIN, config) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + entity_id = config[DOMAIN]["test"]["source"] + hass.states.async_set(entity_id, 4, {}) + await hass.async_block_till_done() + + state = hass.states.get(expected_entity_id) + assert state is not None + + assert round(float(state.state), config[DOMAIN]["test"][CONF_PRECISION]) == 5.0 + + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "a" + + coefs = [round(v, 1) for v in state.attributes.get(ATTR_COEFFICIENTS)] + assert coefs == [1.0, 1.0] + + hass.states.async_set(entity_id, "foo", {}) + await hass.async_block_till_done() + + state = hass.states.get(expected_entity_id) + assert state is not None + + assert state.state == STATE_UNKNOWN + + +async def test_linear_state_from_attribute(hass): + """Test compensation sensor state that pulls from attribute.""" + config = { + "compensation": { + "test": { + "source": "sensor.uncompensated", + "attribute": "value", + "data_points": [ + [1.0, 2.0], + [2.0, 3.0], + ], + "precision": 2, + } + } + } + expected_entity_id = "sensor.compensation_sensor_uncompensated_value" + + assert await async_setup_component(hass, DOMAIN, config) + assert await async_setup_component(hass, SENSOR_DOMAIN, config) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + + entity_id = config[DOMAIN]["test"]["source"] + hass.states.async_set(entity_id, 3, {"value": 4}) + await hass.async_block_till_done() + + state = hass.states.get(expected_entity_id) + assert state is not None + + assert round(float(state.state), config[DOMAIN]["test"][CONF_PRECISION]) == 5.0 + + coefs = [round(v, 1) for v in state.attributes.get(ATTR_COEFFICIENTS)] + assert coefs == [1.0, 1.0] + + hass.states.async_set(entity_id, 3, {"value": "bar"}) + await hass.async_block_till_done() + + state = hass.states.get(expected_entity_id) + assert state is not None + + assert state.state == STATE_UNKNOWN + + +async def test_quadratic_state(hass): + """Test 3 degree polynominial compensation sensor.""" + config = { + "compensation": { + "test": { + "source": "sensor.temperature", + "data_points": [ + [50, 3.3], + [50, 2.8], + [50, 2.9], + [70, 2.3], + [70, 2.6], + [70, 2.1], + [80, 2.5], + [80, 2.9], + [80, 2.4], + [90, 3.0], + [90, 3.1], + [90, 2.8], + [100, 3.3], + [100, 3.5], + [100, 3.0], + ], + "degree": 2, + "precision": 3, + } + } + } + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + entity_id = config[DOMAIN]["test"]["source"] + hass.states.async_set(entity_id, 43.2, {}) + await hass.async_block_till_done() + + state = hass.states.get("sensor.compensation_sensor_temperature") + + assert state is not None + + assert round(float(state.state), config[DOMAIN]["test"][CONF_PRECISION]) == 3.327 + + +async def test_numpy_errors(hass, caplog): + """Tests bad polyfits.""" + config = { + "compensation": { + "test": { + "source": "sensor.uncompensated", + "data_points": [ + [1.0, 1.0], + [1.0, 1.0], + ], + }, + "test2": { + "source": "sensor.uncompensated2", + "data_points": [ + [0.0, 1.0], + [0.0, 1.0], + ], + }, + } + } + await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + assert "polyfit may be poorly conditioned" in caplog.text + + assert "invalid value encountered in true_divide" in caplog.text + + +async def test_datapoints_greater_than_degree(hass, caplog): + """Tests 3 bad data points.""" + config = { + "compensation": { + "test": { + "source": "sensor.uncompensated", + "data_points": [ + [1.0, 2.0], + [2.0, 3.0], + ], + "degree": 2, + }, + } + } + await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + assert "data_points must have at least 3 data_points" in caplog.text + + +async def test_new_state_is_none(hass): + """Tests catch for empty new states.""" + config = { + "compensation": { + "test": { + "source": "sensor.uncompensated", + "data_points": [ + [1.0, 2.0], + [2.0, 3.0], + ], + "precision": 2, + "unit_of_measurement": "a", + } + } + } + expected_entity_id = "sensor.compensation_sensor_uncompensated" + + await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + last_changed = hass.states.get(expected_entity_id).last_changed + + hass.bus.async_fire( + EVENT_STATE_CHANGED, event_data={"entity_id": "sensor.uncompensated"} + ) + + assert last_changed == hass.states.get(expected_entity_id).last_changed From 86176f1bf9ed90650693d608de4255bcbdb2f4a7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 3 Apr 2021 23:08:35 +0200 Subject: [PATCH 0042/1317] Add retry mechanism on onewire sysbus devices (#48614) * Add retry mechanism on sysbus * Update tests * Move to async * Move blocking calls on the executor --- homeassistant/components/onewire/sensor.py | 25 ++++++++- tests/components/onewire/__init__.py | 30 ++++++++++- tests/components/onewire/const.py | 60 +++++++++++++++++----- tests/components/onewire/test_sensor.py | 29 ++++++----- 4 files changed, 115 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/onewire/sensor.py b/homeassistant/components/onewire/sensor.py index 02af7a89ae3fa..b3a5be0a1ca77 100644 --- a/homeassistant/components/onewire/sensor.py +++ b/homeassistant/components/onewire/sensor.py @@ -1,6 +1,7 @@ """Support for 1-Wire environment sensors.""" from __future__ import annotations +import asyncio from glob import glob import logging import os @@ -426,11 +427,31 @@ def state(self) -> StateType: """Return the state of the entity.""" return self._state - def update(self): + async def get_temperature(self): + """Get the latest data from the device.""" + attempts = 1 + while True: + try: + return await self.hass.async_add_executor_job( + self._owsensor.get_temperature + ) + except UnsupportResponseException as ex: + _LOGGER.debug( + "Cannot read from sensor %s (retry attempt %s): %s", + self._device_file, + attempts, + ex, + ) + await asyncio.sleep(0.2) + attempts += 1 + if attempts > 10: + raise + + async def async_update(self): """Get the latest data from the device.""" value = None try: - self._value_raw = self._owsensor.get_temperature() + self._value_raw = await self.get_temperature() value = round(float(self._value_raw), 1) except ( FileNotFoundError, diff --git a/tests/components/onewire/__init__.py b/tests/components/onewire/__init__.py index 7b85c16d4c86a..f133f89d5d626 100644 --- a/tests/components/onewire/__init__.py +++ b/tests/components/onewire/__init__.py @@ -1,5 +1,6 @@ """Tests for 1-Wire integration.""" +from typing import Any, List, Tuple from unittest.mock import patch from pyownet.protocol import ProtocolError @@ -15,7 +16,7 @@ from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE -from .const import MOCK_OWPROXY_DEVICES +from .const import MOCK_OWPROXY_DEVICES, MOCK_SYSBUS_DEVICES from tests.common import MockConfigEntry @@ -125,3 +126,30 @@ def setup_owproxy_mock_devices(owproxy, domain, device_ids) -> None: ) owproxy.return_value.dir.return_value = dir_return_value owproxy.return_value.read.side_effect = read_side_effect + + +def setup_sysbus_mock_devices( + domain: str, device_ids: List[str] +) -> Tuple[List[str], List[Any]]: + """Set up mock for sysbus.""" + glob_result = [] + read_side_effect = [] + + for device_id in device_ids: + mock_device = MOCK_SYSBUS_DEVICES[device_id] + + # Setup directory listing + glob_result += [f"/{DEFAULT_SYSBUS_MOUNT_DIR}/{device_id}"] + + # Setup sub-device reads + device_sensors = mock_device.get(domain, []) + for expected_sensor in device_sensors: + if isinstance(expected_sensor["injected_value"], list): + read_side_effect += expected_sensor["injected_value"] + else: + read_side_effect.append(expected_sensor["injected_value"]) + + # Ensure enough read side effect + read_side_effect.extend([FileNotFoundError("Missing injected value")] * 20) + + return (glob_result, read_side_effect) diff --git a/tests/components/onewire/const.py b/tests/components/onewire/const.py index 8fa149c7adccb..ccae8e695ce3d 100644 --- a/tests/components/onewire/const.py +++ b/tests/components/onewire/const.py @@ -778,7 +778,7 @@ } MOCK_SYSBUS_DEVICES = { - "00-111111111111": {"sensors": []}, + "00-111111111111": {SENSOR_DOMAIN: []}, "10-111111111111": { "device_info": { "identifiers": {(DOMAIN, "10-111111111111")}, @@ -786,7 +786,7 @@ "model": "10", "name": "10-111111111111", }, - "sensors": [ + SENSOR_DOMAIN: [ { "entity_id": "sensor.my_ds18b20_temperature", "unique_id": "/sys/bus/w1/devices/10-111111111111/w1_slave", @@ -797,8 +797,8 @@ }, ], }, - "12-111111111111": {"sensors": []}, - "1D-111111111111": {"sensors": []}, + "12-111111111111": {SENSOR_DOMAIN: []}, + "1D-111111111111": {SENSOR_DOMAIN: []}, "22-111111111111": { "device_info": { "identifiers": {(DOMAIN, "22-111111111111")}, @@ -806,7 +806,7 @@ "model": "22", "name": "22-111111111111", }, - "sensors": [ + "sensor": [ { "entity_id": "sensor.22_111111111111_temperature", "unique_id": "/sys/bus/w1/devices/22-111111111111/w1_slave", @@ -817,7 +817,7 @@ }, ], }, - "26-111111111111": {"sensors": []}, + "26-111111111111": {SENSOR_DOMAIN: []}, "28-111111111111": { "device_info": { "identifiers": {(DOMAIN, "28-111111111111")}, @@ -825,7 +825,7 @@ "model": "28", "name": "28-111111111111", }, - "sensors": [ + SENSOR_DOMAIN: [ { "entity_id": "sensor.28_111111111111_temperature", "unique_id": "/sys/bus/w1/devices/28-111111111111/w1_slave", @@ -836,7 +836,7 @@ }, ], }, - "29-111111111111": {"sensors": []}, + "29-111111111111": {SENSOR_DOMAIN: []}, "3B-111111111111": { "device_info": { "identifiers": {(DOMAIN, "3B-111111111111")}, @@ -844,7 +844,7 @@ "model": "3B", "name": "3B-111111111111", }, - "sensors": [ + SENSOR_DOMAIN: [ { "entity_id": "sensor.3b_111111111111_temperature", "unique_id": "/sys/bus/w1/devices/3B-111111111111/w1_slave", @@ -862,7 +862,7 @@ "model": "42", "name": "42-111111111111", }, - "sensors": [ + SENSOR_DOMAIN: [ { "entity_id": "sensor.42_111111111111_temperature", "unique_id": "/sys/bus/w1/devices/42-111111111111/w1_slave", @@ -873,10 +873,46 @@ }, ], }, + "42-111111111112": { + "device_info": { + "identifiers": {(DOMAIN, "42-111111111112")}, + "manufacturer": "Maxim Integrated", + "model": "42", + "name": "42-111111111112", + }, + SENSOR_DOMAIN: [ + { + "entity_id": "sensor.42_111111111112_temperature", + "unique_id": "/sys/bus/w1/devices/42-111111111112/w1_slave", + "injected_value": [UnsupportResponseException] * 9 + ["27.993"], + "result": "28.0", + "unit": TEMP_CELSIUS, + "class": DEVICE_CLASS_TEMPERATURE, + }, + ], + }, + "42-111111111113": { + "device_info": { + "identifiers": {(DOMAIN, "42-111111111113")}, + "manufacturer": "Maxim Integrated", + "model": "42", + "name": "42-111111111113", + }, + SENSOR_DOMAIN: [ + { + "entity_id": "sensor.42_111111111113_temperature", + "unique_id": "/sys/bus/w1/devices/42-111111111113/w1_slave", + "injected_value": [UnsupportResponseException] * 10 + ["27.993"], + "result": "unknown", + "unit": TEMP_CELSIUS, + "class": DEVICE_CLASS_TEMPERATURE, + }, + ], + }, "EF-111111111111": { - "sensors": [], + SENSOR_DOMAIN: [], }, "EF-111111111112": { - "sensors": [], + SENSOR_DOMAIN: [], }, } diff --git a/tests/components/onewire/test_sensor.py b/tests/components/onewire/test_sensor.py index f81044eb86d98..f3063dfc128d3 100644 --- a/tests/components/onewire/test_sensor.py +++ b/tests/components/onewire/test_sensor.py @@ -12,7 +12,11 @@ from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.setup import async_setup_component -from . import setup_onewire_patched_owserver_integration, setup_owproxy_mock_devices +from . import ( + setup_onewire_patched_owserver_integration, + setup_owproxy_mock_devices, + setup_sysbus_mock_devices, +) from .const import MOCK_OWPROXY_DEVICES, MOCK_SYSBUS_DEVICES from tests.common import assert_setup_component, mock_device_registry, mock_registry @@ -185,19 +189,16 @@ async def test_owserver_setup_valid_device(owproxy, hass, device_id, platform): @pytest.mark.parametrize("device_id", MOCK_SYSBUS_DEVICES.keys()) async def test_onewiredirect_setup_valid_device(hass, device_id): """Test that sysbus config entry works correctly.""" + await async_setup_component(hass, "persistent_notification", {}) entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) - mock_device_sensor = MOCK_SYSBUS_DEVICES[device_id] + glob_result, read_side_effect = setup_sysbus_mock_devices( + SENSOR_DOMAIN, [device_id] + ) - glob_result = [f"/{DEFAULT_SYSBUS_MOUNT_DIR}/{device_id}"] - read_side_effect = [] - expected_sensors = mock_device_sensor["sensors"] - for expected_sensor in expected_sensors: - read_side_effect.append(expected_sensor["injected_value"]) - - # Ensure enough read side effect - read_side_effect.extend([FileNotFoundError("Missing injected value")] * 20) + mock_device = MOCK_SYSBUS_DEVICES[device_id] + expected_entities = mock_device.get(SENSOR_DOMAIN, []) with patch( "homeassistant.components.onewire.onewirehub.os.path.isdir", return_value=True @@ -208,10 +209,10 @@ async def test_onewiredirect_setup_valid_device(hass, device_id): assert await async_setup_component(hass, SENSOR_DOMAIN, MOCK_SYSBUS_CONFIG) await hass.async_block_till_done() - assert len(entity_registry.entities) == len(expected_sensors) + assert len(entity_registry.entities) == len(expected_entities) - if len(expected_sensors) > 0: - device_info = mock_device_sensor["device_info"] + if len(expected_entities) > 0: + device_info = mock_device["device_info"] assert len(device_registry.devices) == 1 registry_entry = device_registry.async_get_device({(DOMAIN, device_id)}) assert registry_entry is not None @@ -220,7 +221,7 @@ async def test_onewiredirect_setup_valid_device(hass, device_id): assert registry_entry.name == device_info["name"] assert registry_entry.model == device_info["model"] - for expected_sensor in expected_sensors: + for expected_sensor in expected_entities: entity_id = expected_sensor["entity_id"] registry_entry = entity_registry.entities.get(entity_id) assert registry_entry is not None From cfe2df9ebd3a7b84ae2bdc6b4ae853b3736a7d5f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Apr 2021 14:00:22 -1000 Subject: [PATCH 0043/1317] Prevent config entry retry from blocking startup (#48660) - If there are two integrations doing long retries async_block_till_done() will never be done --- homeassistant/config_entries.py | 16 +++++++++++----- tests/test_config_entries.py | 30 +++++++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 65cae7942a69a..23758cf88f2ad 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -11,7 +11,8 @@ import attr from homeassistant import data_entry_flow, loader -from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.const import EVENT_HOMEASSISTANT_STARTED +from homeassistant.core import CALLBACK_TYPE, CoreState, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError from homeassistant.helpers import device_registry, entity_registry from homeassistant.helpers.event import Event @@ -276,14 +277,19 @@ async def async_setup( wait_time, ) - async def setup_again(now: Any) -> None: + async def setup_again(*_: Any) -> None: """Run setup again.""" self._async_cancel_retry_setup = None await self.async_setup(hass, integration=integration, tries=tries) - self._async_cancel_retry_setup = hass.helpers.event.async_call_later( - wait_time, setup_again - ) + if hass.state == CoreState.running: + self._async_cancel_retry_setup = hass.helpers.event.async_call_later( + wait_time, setup_again + ) + else: + self._async_cancel_retry_setup = hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STARTED, setup_again + ) return except Exception: # pylint: disable=broad-except _LOGGER.exception( diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 4db1952dbfb2d..c35ba61a7670a 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -6,7 +6,8 @@ import pytest from homeassistant import config_entries, data_entry_flow, loader -from homeassistant.core import callback +from homeassistant.const import EVENT_HOMEASSISTANT_STARTED +from homeassistant.core import CoreState, callback from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -904,6 +905,33 @@ async def test_setup_retrying_during_unload(hass): assert len(mock_call.return_value.mock_calls) == 1 +async def test_setup_retrying_during_unload_before_started(hass): + """Test if we unload an entry that is in retry mode before started.""" + entry = MockConfigEntry(domain="test") + hass.state = CoreState.starting + initial_listeners = hass.bus.async_listeners()[EVENT_HOMEASSISTANT_STARTED] + + mock_setup_entry = AsyncMock(side_effect=ConfigEntryNotReady) + mock_integration(hass, MockModule("test", async_setup_entry=mock_setup_entry)) + mock_entity_platform(hass, "config_flow.test", None) + + await entry.async_setup(hass) + await hass.async_block_till_done() + + assert entry.state == config_entries.ENTRY_STATE_SETUP_RETRY + assert ( + hass.bus.async_listeners()[EVENT_HOMEASSISTANT_STARTED] == initial_listeners + 1 + ) + + await entry.async_unload(hass) + await hass.async_block_till_done() + + assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED + assert ( + hass.bus.async_listeners()[EVENT_HOMEASSISTANT_STARTED] == initial_listeners + 0 + ) + + async def test_entry_options(hass, manager): """Test that we can set options on an entry.""" entry = MockConfigEntry(domain="test", data={"first": True}, options=None) From d3b4a30e18234138dd584ad69e737493637b8818 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sun, 4 Apr 2021 00:04:56 +0000 Subject: [PATCH 0044/1317] [ci skip] Translation update --- .../components/cast/translations/hu.json | 3 +- .../google_travel_time/translations/hu.json | 38 +++++++++++++++++++ .../home_plus_control/translations/hu.json | 2 +- .../opentherm_gw/translations/hu.json | 3 +- .../components/roomba/translations/hu.json | 3 +- .../components/zha/translations/hu.json | 1 + 6 files changed, 46 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/google_travel_time/translations/hu.json diff --git a/homeassistant/components/cast/translations/hu.json b/homeassistant/components/cast/translations/hu.json index 4a6ef76f33cf6..7e5625c925d5f 100644 --- a/homeassistant/components/cast/translations/hu.json +++ b/homeassistant/components/cast/translations/hu.json @@ -27,7 +27,8 @@ "step": { "options": { "data": { - "known_hosts": "Opcion\u00e1lis lista az ismert hosztokr\u00f3l, ha az mDNS felder\u00edt\u00e9s nem m\u0171k\u00f6dik." + "known_hosts": "Opcion\u00e1lis lista az ismert hosztokr\u00f3l, ha az mDNS felder\u00edt\u00e9s nem m\u0171k\u00f6dik.", + "uuid": "Az UUID-k opcion\u00e1lis list\u00e1ja. A felsorol\u00e1sban nem szerepl\u0151 szerepl\u0151g\u00e1rd\u00e1k nem ker\u00fclnek hozz\u00e1ad\u00e1sra." }, "description": "K\u00e9rj\u00fck, add meg a Google Cast konfigur\u00e1ci\u00f3t." } diff --git a/homeassistant/components/google_travel_time/translations/hu.json b/homeassistant/components/google_travel_time/translations/hu.json new file mode 100644 index 0000000000000..5bee8045c4fe1 --- /dev/null +++ b/homeassistant/components/google_travel_time/translations/hu.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "A hely m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Csatlakoz\u00e1si hiba" + }, + "step": { + "user": { + "data": { + "api_key": "Api kucs", + "destination": "C\u00e9l", + "origin": "Eredet" + }, + "description": "Az eredet \u00e9s a c\u00e9l megad\u00e1sakor megadhat egy vagy t\u00f6bb helyet a pipa karakterrel elv\u00e1lasztva, c\u00edm, sz\u00e9less\u00e9gi / hossz\u00fas\u00e1gi koordin\u00e1t\u00e1k vagy Google helyazonos\u00edt\u00f3 form\u00e1j\u00e1ban. Amikor a helyet megadja egy Google helyazonos\u00edt\u00f3val, akkor az azonos\u00edt\u00f3t el\u0151taggal kell ell\u00e1tni a `hely_azonos\u00edt\u00f3:` sz\u00f3val." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "avoid": "Elker\u00fcl", + "language": "Nyelv", + "mode": "Utaz\u00e1si m\u00f3d", + "time": "Id\u0151", + "time_type": "Id\u0151 t\u00edpusa", + "transit_mode": "Tranzit m\u00f3d", + "transit_routing_preference": "Tranzit \u00fatv\u00e1laszt\u00e1si be\u00e1ll\u00edt\u00e1s", + "units": "Egys\u00e9gek" + }, + "description": "Opcion\u00e1lisan megadhatja az indul\u00e1si id\u0151t vagy az \u00e9rkez\u00e9si id\u0151t. Indul\u00e1si id\u0151 megad\u00e1sakor megadhatja a \"most\", a Unix id\u0151b\u00e9lyegz\u0151t vagy a 24 \u00f3r\u00e1s id\u0151l\u00e1ncot, p\u00e9ld\u00e1ul a \"08:00:00\" karakterl\u00e1ncot. \u00c9rkez\u00e9si id\u0151 megad\u00e1sakor unix id\u0151b\u00e9lyeget vagy 24 \u00f3r\u00e1s id\u0151l\u00e1ncot haszn\u00e1lhat, p\u00e9ld\u00e1ul \"08:00:00\"" + } + } + }, + "title": "Google T\u00e9rk\u00e9p utaz\u00e1si id\u0151" +} \ No newline at end of file diff --git a/homeassistant/components/home_plus_control/translations/hu.json b/homeassistant/components/home_plus_control/translations/hu.json index 2a4775a0b586c..7bc04beb0578a 100644 --- a/homeassistant/components/home_plus_control/translations/hu.json +++ b/homeassistant/components/home_plus_control/translations/hu.json @@ -17,5 +17,5 @@ } } }, - "title": "Legrand Home+ Control" + "title": "Legrand Home+ vez\u00e9rl\u00e9s" } \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/translations/hu.json b/homeassistant/components/opentherm_gw/translations/hu.json index b8f51f4bb20c6..9ca79a3ccddd5 100644 --- a/homeassistant/components/opentherm_gw/translations/hu.json +++ b/homeassistant/components/opentherm_gw/translations/hu.json @@ -21,7 +21,8 @@ "init": { "data": { "floor_temperature": "Padl\u00f3 h\u0151m\u00e9rs\u00e9klete", - "precision": "Pontoss\u00e1g" + "precision": "Pontoss\u00e1g", + "temporary_override_mode": "Ideiglenes be\u00e1ll\u00edt\u00e1s fel\u00fclb\u00edr\u00e1l\u00e1si m\u00f3dja" } } } diff --git a/homeassistant/components/roomba/translations/hu.json b/homeassistant/components/roomba/translations/hu.json index 8f7c2c97884ab..931671f92d210 100644 --- a/homeassistant/components/roomba/translations/hu.json +++ b/homeassistant/components/roomba/translations/hu.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "not_irobot_device": "A felfedezett eszk\u00f6z nem iRobot eszk\u00f6z" + "not_irobot_device": "A felfedezett eszk\u00f6z nem iRobot eszk\u00f6z", + "short_blid": "fel lett oldva" }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s" diff --git a/homeassistant/components/zha/translations/hu.json b/homeassistant/components/zha/translations/hu.json index 844f2dd719111..aaa41429fde75 100644 --- a/homeassistant/components/zha/translations/hu.json +++ b/homeassistant/components/zha/translations/hu.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, + "flow_title": "ZHA: {n\u00e9v}", "step": { "port_config": { "data": { From bc06100dd8223e0659db374b8b2c5ec410f85359 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Apr 2021 14:10:48 -1000 Subject: [PATCH 0045/1317] Make sonos event asyncio (#48618) --- homeassistant/components/sonos/manifest.json | 2 +- .../components/sonos/media_player.py | 123 +++++++++++------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/sonos/conftest.py | 5 +- 5 files changed, 80 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index e208a0e7a32d1..cd32a3dab2608 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -3,7 +3,7 @@ "name": "Sonos", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sonos", - "requirements": ["pysonos==0.0.40"], + "requirements": ["pysonos==0.0.41"], "after_dependencies": ["plex"], "ssdp": [ { diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 5e59918650ea4..4f265bc6f567c 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -9,7 +9,7 @@ import async_timeout import pysonos -from pysonos import alarms +from pysonos import alarms, events_asyncio from pysonos.core import ( MUSIC_SRC_LINE_IN, MUSIC_SRC_RADIO, @@ -162,6 +162,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): config = hass.data[SONOS_DOMAIN].get("media_player", {}) _LOGGER.debug("Reached async_setup_entry, config=%s", config) + pysonos.config.EVENTS_MODULE = events_asyncio advertise_addr = config.get(CONF_ADVERTISE_ADDR) if advertise_addr: @@ -224,6 +225,7 @@ def _discovered_player(soco): interval=DISCOVERY_INTERVAL, interface_addr=config.get(CONF_INTERFACE_ADDR), ) + hass.data[DATA_SONOS].discovery_thread.name = "Sonos-Discovery" _LOGGER.debug("Adding discovery job") hass.async_add_executor_job(_discovery) @@ -446,12 +448,8 @@ async def async_added_to_hass(self): self.hass.data[DATA_SONOS].entities.append(self) - def _rebuild_groups(): - """Build the current group topology.""" - for entity in self.hass.data[DATA_SONOS].entities: - entity.update_groups() - - self.hass.async_add_executor_job(_rebuild_groups) + for entity in self.hass.data[DATA_SONOS].entities: + await entity.async_update_groups_coro() @property def unique_id(self): @@ -515,6 +513,7 @@ def coordinator(self): async def async_seen(self, player): """Record that this player was seen right now.""" was_available = self.available + _LOGGER.debug("Async seen: %s, was_available: %s", player, was_available) self._player = player @@ -532,15 +531,14 @@ async def async_seen(self, player): self.update, datetime.timedelta(seconds=SCAN_INTERVAL) ) - done = await self.hass.async_add_executor_job(self._attach_player) + done = await self._async_attach_player() if not done: self._seen_timer() - self.async_unseen() + await self.async_unseen() self.async_write_ha_state() - @callback - def async_unseen(self, now=None): + async def async_unseen(self, now=None): """Make this player unavailable when it was not seen recently.""" self._seen_timer = None @@ -548,11 +546,8 @@ def async_unseen(self, now=None): self._poll_timer() self._poll_timer = None - def _unsub(subscriptions): - for subscription in subscriptions: - subscription.unsubscribe() - - self.hass.async_add_executor_job(_unsub, self._subscriptions) + for subscription in self._subscriptions: + await subscription.unsubscribe() self._subscriptions = [] @@ -581,29 +576,39 @@ def _set_favorites(self): _LOGGER.error("Unhandled favorite '%s': %s", fav.title, ex) def _attach_player(self): + """Get basic information and add event subscriptions.""" + self._play_mode = self.soco.play_mode + self.update_volume() + self._set_favorites() + + async def _async_attach_player(self): """Get basic information and add event subscriptions.""" try: - self._play_mode = self.soco.play_mode - self.update_volume() - self._set_favorites() + await self.hass.async_add_executor_job(self._attach_player) player = self.soco - def subscribe(sonos_service, action): - """Add a subscription to a pysonos service.""" - queue = _ProcessSonosEventQueue(action) - sub = sonos_service.subscribe(auto_renew=True, event_queue=queue) - self._subscriptions.append(sub) + if self._subscriptions: + raise RuntimeError( + f"Attempted to attach subscriptions to player: {player} " + f"when existing subscriptions exist: {self._subscriptions}" + ) - subscribe(player.avTransport, self.update_media) - subscribe(player.renderingControl, self.update_volume) - subscribe(player.zoneGroupTopology, self.update_groups) - subscribe(player.contentDirectory, self.update_content) + await self._subscribe(player.avTransport, self.async_update_media) + await self._subscribe(player.renderingControl, self.async_update_volume) + await self._subscribe(player.zoneGroupTopology, self.async_update_groups) + await self._subscribe(player.contentDirectory, self.async_update_content) return True except SoCoException as ex: _LOGGER.warning("Could not connect %s: %s", self.entity_id, ex) return False + async def _subscribe(self, target, sub_callback): + """Create a sonos subscription.""" + subscription = await target.subscribe(auto_renew=True) + subscription.callback = sub_callback + self._subscriptions.append(subscription) + @property def should_poll(self): """Return that we should not be polled (we handle that internally).""" @@ -619,6 +624,11 @@ def update(self, now=None): except SoCoException: pass + @callback + def async_update_media(self, event=None): + """Update information about currently playing media.""" + self.hass.async_add_job(self.update_media, event) + def update_media(self, event=None): """Update information about currently playing media.""" variables = event and event.variables @@ -759,32 +769,47 @@ def update_media_music(self, update_media_position, track_info): if playlist_position > 0: self._queue_position = playlist_position - 1 - def update_volume(self, event=None): + @callback + def async_update_volume(self, event): """Update information about currently volume settings.""" - if event: - variables = event.variables + variables = event.variables - if "volume" in variables: - self._player_volume = int(variables["volume"]["Master"]) + if "volume" in variables: + self._player_volume = int(variables["volume"]["Master"]) - if "mute" in variables: - self._player_muted = variables["mute"]["Master"] == "1" + if "mute" in variables: + self._player_muted = variables["mute"]["Master"] == "1" - if "night_mode" in variables: - self._night_sound = variables["night_mode"] == "1" + if "night_mode" in variables: + self._night_sound = variables["night_mode"] == "1" - if "dialog_level" in variables: - self._speech_enhance = variables["dialog_level"] == "1" + if "dialog_level" in variables: + self._speech_enhance = variables["dialog_level"] == "1" - self.schedule_update_ha_state() - else: - self._player_volume = self.soco.volume - self._player_muted = self.soco.mute - self._night_sound = self.soco.night_mode - self._speech_enhance = self.soco.dialog_mode + self.async_write_ha_state() + + def update_volume(self): + """Update information about currently volume settings.""" + self._player_volume = self.soco.volume + self._player_muted = self.soco.mute + self._night_sound = self.soco.night_mode + self._speech_enhance = self.soco.dialog_mode def update_groups(self, event=None): """Handle callback for topology change event.""" + coro = self.async_update_groups_coro(event) + if coro: + self.hass.add_job(coro) + + @callback + def async_update_groups(self, event=None): + """Handle callback for topology change event.""" + coro = self.async_update_groups_coro(event) + if coro: + self.hass.async_add_job(coro) + + def async_update_groups_coro(self, event=None): + """Handle callback for topology change event.""" def _get_soco_group(): """Ask SoCo cache for existing topology.""" @@ -849,13 +874,13 @@ async def _async_handle_group_event(event): if event and not hasattr(event, "zone_player_uui_ds_in_group"): return - self.hass.add_job(_async_handle_group_event(event)) + return _async_handle_group_event(event) - def update_content(self, event=None): + def async_update_content(self, event=None): """Update information about available content.""" if event and "favorites_update_id" in event.variables: - self._set_favorites() - self.schedule_update_ha_state() + self.hass.async_add_job(self._set_favorites) + self.async_write_ha_state() @property def volume_level(self): diff --git a/requirements_all.txt b/requirements_all.txt index 93a6601ee6b83..3f7079d03b2fe 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1717,7 +1717,7 @@ pysnmp==4.4.12 pysoma==0.0.10 # homeassistant.components.sonos -pysonos==0.0.40 +pysonos==0.0.41 # homeassistant.components.spc pyspcwebgw==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cd9eb8db0338f..8b73a2fada062 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -926,7 +926,7 @@ pysmartthings==0.7.6 pysoma==0.0.10 # homeassistant.components.sonos -pysonos==0.0.40 +pysonos==0.0.41 # homeassistant.components.spc pyspcwebgw==0.4.0 diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index 1ce2205813b1f..7b6393559dc77 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -1,5 +1,5 @@ """Configuration for Sonos tests.""" -from unittest.mock import Mock, patch as patch +from unittest.mock import AsyncMock, MagicMock, Mock, patch as patch import pytest @@ -41,6 +41,7 @@ def discover_fixture(soco): def do_callback(callback, **kwargs): callback(soco) + return MagicMock() with patch("pysonos.discover_thread", side_effect=do_callback) as mock: yield mock @@ -56,7 +57,7 @@ def config_fixture(): def dummy_soco_service_fixture(): """Create dummy_soco_service fixture.""" service = Mock() - service.subscribe = Mock() + service.subscribe = AsyncMock() return service From c1e788e6655284ef4276638665948d9602bac63f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Apr 2021 14:11:32 -1000 Subject: [PATCH 0046/1317] Only listen for zeroconf when the esphome device cannot connect (#48645) --- homeassistant/components/esphome/__init__.py | 30 +++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 02d6309fe7f02..0caf00af8ef0d 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -239,6 +239,8 @@ def __init__( # Flag to check if the device is connected self._connected = True self._connected_lock = asyncio.Lock() + self._zc_lock = asyncio.Lock() + self._zc_listening = False # Event the different strategies use for issuing a reconnect attempt. self._reconnect_event = asyncio.Event() # The task containing the infinite reconnect loop while running @@ -270,6 +272,7 @@ async def _on_disconnect(self): self._entry_data.disconnect_callbacks = [] self._entry_data.available = False self._entry_data.async_update_device_state(self._hass) + await self._start_zc_listen() # Reset tries async with self._tries_lock: @@ -315,6 +318,7 @@ async def _try_connect(self): self._host, error, ) + await self._start_zc_listen() # Schedule re-connect in event loop in order not to delay HA # startup. First connect is scheduled in tracked tasks. async with self._wait_task_lock: @@ -332,6 +336,7 @@ async def _try_connect(self): self._tries = 0 async with self._connected_lock: self._connected = True + await self._stop_zc_listen() self._hass.async_create_task(self._on_login()) async def _reconnect_once(self): @@ -375,9 +380,6 @@ async def start(self): # Create reconnection loop outside of HA's tracked tasks in order # not to delay startup. self._loop_task = self._hass.loop.create_task(self._reconnect_loop()) - # Listen for mDNS records so we can reconnect directly if a received mDNS record - # indicates the node is up again - await self._hass.async_add_executor_job(self._zc.add_listener, self, None) async with self._connected_lock: self._connected = False @@ -388,11 +390,31 @@ async def stop(self): if self._loop_task is not None: self._loop_task.cancel() self._loop_task = None - await self._hass.async_add_executor_job(self._zc.remove_listener, self) async with self._wait_task_lock: if self._wait_task is not None: self._wait_task.cancel() self._wait_task = None + await self._stop_zc_listen() + + async def _start_zc_listen(self): + """Listen for mDNS records. + + This listener allows us to schedule a reconnect as soon as a + received mDNS record indicates the node is up again. + """ + async with self._zc_lock: + if not self._zc_listening: + await self._hass.async_add_executor_job( + self._zc.add_listener, self, None + ) + self._zc_listening = True + + async def _stop_zc_listen(self): + """Stop listening for zeroconf updates.""" + async with self._zc_lock: + if self._zc_listening: + await self._hass.async_add_executor_job(self._zc.remove_listener, self) + self._zc_listening = False @callback def stop_callback(self): From 3bc583607fff48f70e2e8a092b5e11bcbbafbc23 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Apr 2021 23:35:33 -1000 Subject: [PATCH 0047/1317] Optimize storage collection entity operations with asyncio.gather (#48352) --- homeassistant/helpers/collection.py | 86 +++++++++++++++++++++-------- 1 file changed, 64 insertions(+), 22 deletions(-) diff --git a/homeassistant/helpers/collection.py b/homeassistant/helpers/collection.py index 6185b74068ded..248059f7f93f6 100644 --- a/homeassistant/helpers/collection.py +++ b/homeassistant/helpers/collection.py @@ -4,8 +4,9 @@ from abc import ABC, abstractmethod import asyncio from dataclasses import dataclass +from itertools import groupby import logging -from typing import Any, Awaitable, Callable, Iterable, Optional, cast +from typing import Any, Awaitable, Callable, Coroutine, Iterable, Optional, cast import voluptuous as vol from voluptuous.humanize import humanize_error @@ -54,6 +55,8 @@ class CollectionChangeSet: Awaitable[None], ] +ChangeSetListener = Callable[[Iterable[CollectionChangeSet]], Awaitable[None]] + class CollectionError(HomeAssistantError): """Base class for collection related errors.""" @@ -105,6 +108,7 @@ def __init__(self, logger: logging.Logger, id_manager: IDManager | None = None): self.id_manager = id_manager or IDManager() self.data: dict[str, dict] = {} self.listeners: list[ChangeListener] = [] + self.change_set_listeners: list[ChangeSetListener] = [] self.id_manager.add_collection(self.data) @@ -121,6 +125,14 @@ def async_add_listener(self, listener: ChangeListener) -> None: """ self.listeners.append(listener) + @callback + def async_add_change_set_listener(self, listener: ChangeSetListener) -> None: + """Add a listener for a full change set. + + Will be called with [(change_type, item_id, updated_config), ...] + """ + self.change_set_listeners.append(listener) + async def notify_changes(self, change_sets: Iterable[CollectionChangeSet]) -> None: """Notify listeners of a change.""" await asyncio.gather( @@ -128,7 +140,11 @@ async def notify_changes(self, change_sets: Iterable[CollectionChangeSet]) -> No listener(change_set.change_type, change_set.item_id, change_set.item) for listener in self.listeners for change_set in change_sets - ] + ], + *[ + change_set_listener(change_sets) + for change_set_listener in self.change_set_listeners + ], ) @@ -311,29 +327,55 @@ def sync_entity_lifecycle( ) -> None: """Map a collection to an entity component.""" entities = {} + ent_reg = entity_registry.async_get(hass) - async def _collection_changed(change_type: str, item_id: str, config: dict) -> None: - """Handle a collection change.""" - if change_type == CHANGE_ADDED: - entity = create_entity(config) - await entity_component.async_add_entities([entity]) - entities[item_id] = entity - return - - if change_type == CHANGE_REMOVED: - ent_reg = await entity_registry.async_get_registry(hass) - ent_to_remove = ent_reg.async_get_entity_id(domain, platform, item_id) - if ent_to_remove is not None: - ent_reg.async_remove(ent_to_remove) - else: - await entities[item_id].async_remove(force_remove=True) - entities.pop(item_id) - return + async def _add_entity(change_set: CollectionChangeSet) -> Entity: + entities[change_set.item_id] = create_entity(change_set.item) + return entities[change_set.item_id] - # CHANGE_UPDATED - await entities[item_id].async_update_config(config) # type: ignore + async def _remove_entity(change_set: CollectionChangeSet) -> None: + ent_to_remove = ent_reg.async_get_entity_id( + domain, platform, change_set.item_id + ) + if ent_to_remove is not None: + ent_reg.async_remove(ent_to_remove) + else: + await entities[change_set.item_id].async_remove(force_remove=True) + entities.pop(change_set.item_id) + + async def _update_entity(change_set: CollectionChangeSet) -> None: + await entities[change_set.item_id].async_update_config(change_set.item) # type: ignore + + _func_map: dict[ + str, Callable[[CollectionChangeSet], Coroutine[Any, Any, Entity | None]] + ] = { + CHANGE_ADDED: _add_entity, + CHANGE_REMOVED: _remove_entity, + CHANGE_UPDATED: _update_entity, + } + + async def _collection_changed(change_sets: Iterable[CollectionChangeSet]) -> None: + """Handle a collection change.""" + # Create a new bucket every time we have a different change type + # to ensure operations happen in order. We only group + # the same change type. + for _, grouped in groupby( + change_sets, lambda change_set: change_set.change_type + ): + new_entities = [ + entity + for entity in await asyncio.gather( + *[ + _func_map[change_set.change_type](change_set) + for change_set in grouped + ] + ) + if entity is not None + ] + if new_entities: + await entity_component.async_add_entities(new_entities) - collection.async_add_listener(_collection_changed) + collection.async_add_change_set_listener(_collection_changed) class StorageCollectionWebsocket: From ecec3c8ab9d39d6b1b43f6c5c2aeaef355fab840 Mon Sep 17 00:00:00 2001 From: mburget <77898400+mburget@users.noreply.github.com> Date: Sun, 4 Apr 2021 12:22:43 +0200 Subject: [PATCH 0048/1317] Fix Raspi GPIO binary_sensor produces unreliable responses (#48170) * Fix for issue #10498 Raspi GPIO binary_sensor produces unreliable responses ("Doorbell Scenario") Changes overtaken from PR#31788 which was somehow never finished * Fix for issue #10498 Raspi GPIO binary_sensor produces unreliable response. Changes taken over from PR31788 which was somehow never finished * Remove unused code (pylint warning) --- .../components/rpi_gpio/binary_sensor.py | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/rpi_gpio/binary_sensor.py b/homeassistant/components/rpi_gpio/binary_sensor.py index 36d7ae50f3255..318b29131b607 100644 --- a/homeassistant/components/rpi_gpio/binary_sensor.py +++ b/homeassistant/components/rpi_gpio/binary_sensor.py @@ -1,4 +1,7 @@ """Support for binary sensor using RPi GPIO.""" + +import asyncio + import voluptuous as vol from homeassistant.components import rpi_gpio @@ -52,6 +55,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class RPiGPIOBinarySensor(BinarySensorEntity): """Represent a binary sensor that uses Raspberry Pi GPIO.""" + async def async_read_gpio(self): + """Read state from GPIO.""" + await asyncio.sleep(float(self._bouncetime) / 1000) + self._state = await self.hass.async_add_executor_job( + rpi_gpio.read_input, self._port + ) + self.async_write_ha_state() + def __init__(self, name, port, pull_mode, bouncetime, invert_logic): """Initialize the RPi binary sensor.""" self._name = name or DEVICE_DEFAULT_NAME @@ -63,12 +74,11 @@ def __init__(self, name, port, pull_mode, bouncetime, invert_logic): rpi_gpio.setup_input(self._port, self._pull_mode) - def read_gpio(port): - """Read state from GPIO.""" - self._state = rpi_gpio.read_input(self._port) - self.schedule_update_ha_state() + def edge_detected(port): + """Edge detection handler.""" + self.hass.add_job(self.async_read_gpio) - rpi_gpio.edge_detect(self._port, read_gpio, self._bouncetime) + rpi_gpio.edge_detect(self._port, edge_detected, self._bouncetime) @property def should_poll(self): From b5c679f3d039a72836ebaf9c03868129f45ff5a9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Apr 2021 00:31:58 -1000 Subject: [PATCH 0049/1317] Apply ConfigEntryNotReady improvements to PlatformNotReady (#48665) * Apply ConfigEntryNotReady improvements to PlatformNotReady - Limit log spam #47201 - Log exception reason #48449 - Prevent startup blockage #48660 * coverage --- homeassistant/helpers/entity_platform.py | 45 ++++++++++---- tests/helpers/test_entity_platform.py | 75 +++++++++++++++++++++++- 2 files changed, 107 insertions(+), 13 deletions(-) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index b9d603ba5e123..00783b072c9a2 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -9,9 +9,14 @@ from typing import TYPE_CHECKING, Callable, Coroutine, Iterable from homeassistant import config_entries -from homeassistant.const import ATTR_RESTORED, DEVICE_DEFAULT_NAME +from homeassistant.const import ( + ATTR_RESTORED, + DEVICE_DEFAULT_NAME, + EVENT_HOMEASSISTANT_STARTED, +) from homeassistant.core import ( CALLBACK_TYPE, + CoreState, HomeAssistant, ServiceCall, callback, @@ -215,23 +220,41 @@ async def _async_setup_platform( hass.config.components.add(full_name) self._setup_complete = True return True - except PlatformNotReady: + except PlatformNotReady as ex: tries += 1 wait_time = min(tries, 6) * PLATFORM_NOT_READY_BASE_WAIT_TIME - logger.warning( - "Platform %s not ready yet. Retrying in %d seconds.", - self.platform_name, - wait_time, - ) + message = str(ex) + if not message and ex.__cause__: + message = str(ex.__cause__) + ready_message = f"ready yet: {message}" if message else "ready yet" + if tries == 1: + logger.warning( + "Platform %s not %s; Retrying in background in %d seconds", + self.platform_name, + ready_message, + wait_time, + ) + else: + logger.debug( + "Platform %s not %s; Retrying in %d seconds", + self.platform_name, + ready_message, + wait_time, + ) - async def setup_again(now): # type: ignore[no-untyped-def] + async def setup_again(*_): # type: ignore[no-untyped-def] """Run setup again.""" self._async_cancel_retry_setup = None await self._async_setup_platform(async_create_setup_task, tries) - self._async_cancel_retry_setup = async_call_later( - hass, wait_time, setup_again - ) + if hass.state == CoreState.running: + self._async_cancel_retry_setup = async_call_later( + hass, wait_time, setup_again + ) + else: + self._async_cancel_retry_setup = hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STARTED, setup_again + ) return False except asyncio.TimeoutError: logger.error( diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 3f26535de1879..e842d5aa1ae6b 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -6,8 +6,8 @@ import pytest -from homeassistant.const import PERCENTAGE -from homeassistant.core import callback +from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, PERCENTAGE +from homeassistant.core import CoreState, callback from homeassistant.exceptions import HomeAssistantError, PlatformNotReady from homeassistant.helpers import ( device_registry as dr, @@ -592,6 +592,52 @@ async def test_setup_entry_platform_not_ready(hass, caplog): assert len(mock_call_later.mock_calls) == 1 +async def test_setup_entry_platform_not_ready_with_message(hass, caplog): + """Test when an entry is not ready yet that includes a message.""" + async_setup_entry = Mock(side_effect=PlatformNotReady("lp0 on fire")) + platform = MockPlatform(async_setup_entry=async_setup_entry) + config_entry = MockConfigEntry() + ent_platform = MockEntityPlatform( + hass, platform_name=config_entry.domain, platform=platform + ) + + with patch.object(entity_platform, "async_call_later") as mock_call_later: + assert not await ent_platform.async_setup_entry(config_entry) + + full_name = f"{ent_platform.domain}.{config_entry.domain}" + assert full_name not in hass.config.components + assert len(async_setup_entry.mock_calls) == 1 + + assert "Platform test not ready yet" in caplog.text + assert "lp0 on fire" in caplog.text + assert len(mock_call_later.mock_calls) == 1 + + +async def test_setup_entry_platform_not_ready_from_exception(hass, caplog): + """Test when an entry is not ready yet that includes the causing exception string.""" + original_exception = HomeAssistantError("The device dropped the connection") + platform_exception = PlatformNotReady() + platform_exception.__cause__ = original_exception + + async_setup_entry = Mock(side_effect=platform_exception) + platform = MockPlatform(async_setup_entry=async_setup_entry) + config_entry = MockConfigEntry() + ent_platform = MockEntityPlatform( + hass, platform_name=config_entry.domain, platform=platform + ) + + with patch.object(entity_platform, "async_call_later") as mock_call_later: + assert not await ent_platform.async_setup_entry(config_entry) + + full_name = f"{ent_platform.domain}.{config_entry.domain}" + assert full_name not in hass.config.components + assert len(async_setup_entry.mock_calls) == 1 + + assert "Platform test not ready yet" in caplog.text + assert "The device dropped the connection" in caplog.text + assert len(mock_call_later.mock_calls) == 1 + + async def test_reset_cancels_retry_setup(hass): """Test that resetting a platform will cancel scheduled a setup retry.""" async_setup_entry = Mock(side_effect=PlatformNotReady) @@ -614,6 +660,31 @@ async def test_reset_cancels_retry_setup(hass): assert ent_platform._async_cancel_retry_setup is None +async def test_reset_cancels_retry_setup_when_not_started(hass): + """Test that resetting a platform will cancel scheduled a setup retry when not yet started.""" + hass.state = CoreState.starting + async_setup_entry = Mock(side_effect=PlatformNotReady) + initial_listeners = hass.bus.async_listeners()[EVENT_HOMEASSISTANT_STARTED] + + platform = MockPlatform(async_setup_entry=async_setup_entry) + config_entry = MockConfigEntry() + ent_platform = MockEntityPlatform( + hass, platform_name=config_entry.domain, platform=platform + ) + + assert not await ent_platform.async_setup_entry(config_entry) + await hass.async_block_till_done() + assert ( + hass.bus.async_listeners()[EVENT_HOMEASSISTANT_STARTED] == initial_listeners + 1 + ) + assert ent_platform._async_cancel_retry_setup is not None + + await ent_platform.async_reset() + await hass.async_block_till_done() + assert hass.bus.async_listeners()[EVENT_HOMEASSISTANT_STARTED] == initial_listeners + assert ent_platform._async_cancel_retry_setup is None + + async def test_not_fails_with_adding_empty_entities_(hass): """Test for not fails on empty entities list.""" component = EntityComponent(_LOGGER, DOMAIN, hass) From 1876e84d71fd2716612ad103d32e4b0d85b0c335 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 4 Apr 2021 13:06:49 +0200 Subject: [PATCH 0050/1317] Upgrade pytest to 6.2.3 (#48672) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 6bec7e60c3caa..81c8819d44933 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -20,7 +20,7 @@ pytest-test-groups==1.0.3 pytest-sugar==0.9.4 pytest-timeout==1.4.2 pytest-xdist==2.1.0 -pytest==6.2.2 +pytest==6.2.3 requests_mock==1.8.0 responses==0.12.0 respx==0.16.2 From d75f8255302e44976c935b47ea1d228dd698640d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 4 Apr 2021 13:28:08 +0200 Subject: [PATCH 0051/1317] Upgrade holidays to 0.11.1 (#48673) --- homeassistant/components/workday/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index 3351d796e93ef..b87704cde679c 100644 --- a/homeassistant/components/workday/manifest.json +++ b/homeassistant/components/workday/manifest.json @@ -2,7 +2,7 @@ "domain": "workday", "name": "Workday", "documentation": "https://www.home-assistant.io/integrations/workday", - "requirements": ["holidays==0.10.5.2"], + "requirements": ["holidays==0.11.1"], "codeowners": ["@fabaff"], "quality_scale": "internal" } diff --git a/requirements_all.txt b/requirements_all.txt index 3f7079d03b2fe..8025f6b1e6fd8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -756,7 +756,7 @@ hlk-sw16==0.0.9 hole==0.5.1 # homeassistant.components.workday -holidays==0.10.5.2 +holidays==0.11.1 # homeassistant.components.frontend home-assistant-frontend==20210402.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8b73a2fada062..8cc3dbfa6bf02 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -411,7 +411,7 @@ hlk-sw16==0.0.9 hole==0.5.1 # homeassistant.components.workday -holidays==0.10.5.2 +holidays==0.11.1 # homeassistant.components.frontend home-assistant-frontend==20210402.1 From 2511e1f22933792a727c4f90992249e4554a7bad Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sun, 4 Apr 2021 14:02:47 +0200 Subject: [PATCH 0052/1317] Remove modbus duplicate strings (#48654) * Reuse HA constants for serial configuration. Reusing HA consts reduces the need for translation. Sort/group constants in const. * Change const name ATTR_* to CONF_* * Correct wrong import * ATTR_* for service and CONF_* for schemas. * Revert change to service call. * Rename CONF_TEMPERATURE -> ATTR_TEMPERATURE Avoid possible division problem in set_temperature. --- homeassistant/components/modbus/__init__.py | 14 +-- .../components/modbus/binary_sensor.py | 2 +- homeassistant/components/modbus/climate.py | 10 +- homeassistant/components/modbus/const.py | 104 ++++++++---------- homeassistant/components/modbus/modbus.py | 8 +- homeassistant/components/modbus/sensor.py | 4 +- .../modbus/test_modbus_binary_sensor.py | 2 +- tests/components/modbus/test_modbus_sensor.py | 4 +- tests/components/modbus/test_modbus_switch.py | 2 +- 9 files changed, 66 insertions(+), 84 deletions(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index f1f1e65680549..a4e0c21ec5f62 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -16,10 +16,11 @@ DEVICE_CLASSES_SCHEMA as SWITCH_DEVICE_CLASSES_SCHEMA, ) from homeassistant.const import ( - ATTR_STATE, CONF_ADDRESS, + CONF_BINARY_SENSORS, CONF_COMMAND_OFF, CONF_COMMAND_ON, + CONF_COUNT, CONF_COVERS, CONF_DELAY, CONF_DEVICE_CLASS, @@ -29,8 +30,11 @@ CONF_OFFSET, CONF_PORT, CONF_SCAN_INTERVAL, + CONF_SENSORS, CONF_SLAVE, CONF_STRUCTURE, + CONF_SWITCHES, + CONF_TEMPERATURE_UNIT, CONF_TIMEOUT, CONF_TYPE, CONF_UNIT_OF_MEASUREMENT, @@ -40,6 +44,7 @@ from .const import ( ATTR_ADDRESS, ATTR_HUB, + ATTR_STATE, ATTR_UNIT, ATTR_VALUE, CALL_TYPE_COIL, @@ -47,10 +52,8 @@ CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT, CONF_BAUDRATE, - CONF_BINARY_SENSORS, CONF_BYTESIZE, CONF_CLIMATES, - CONF_COUNT, CONF_CURRENT_TEMP, CONF_CURRENT_TEMP_REGISTER_TYPE, CONF_DATA_COUNT, @@ -63,7 +66,6 @@ CONF_REGISTER, CONF_REVERSE_ORDER, CONF_SCALE, - CONF_SENSORS, CONF_STATE_CLOSED, CONF_STATE_CLOSING, CONF_STATE_OFF, @@ -74,9 +76,7 @@ CONF_STATUS_REGISTER_TYPE, CONF_STEP, CONF_STOPBITS, - CONF_SWITCHES, CONF_TARGET_TEMP, - CONF_UNIT, CONF_VERIFY_REGISTER, CONF_VERIFY_STATE, DATA_TYPE_CUSTOM, @@ -143,7 +143,7 @@ def number(value: Any) -> Union[int, float]: vol.Optional(CONF_MIN_TEMP, default=5): cv.positive_int, vol.Optional(CONF_STEP, default=0.5): vol.Coerce(float), vol.Optional(CONF_STRUCTURE, default=DEFAULT_STRUCTURE_PREFIX): cv.string, - vol.Optional(CONF_UNIT, default=DEFAULT_TEMP_UNIT): cv.string, + vol.Optional(CONF_TEMPERATURE_UNIT, default=DEFAULT_TEMP_UNIT): cv.string, } ) diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py index 909f0088c38f7..e422eb7528ea8 100644 --- a/homeassistant/components/modbus/binary_sensor.py +++ b/homeassistant/components/modbus/binary_sensor.py @@ -15,6 +15,7 @@ ) from homeassistant.const import ( CONF_ADDRESS, + CONF_BINARY_SENSORS, CONF_DEVICE_CLASS, CONF_NAME, CONF_SCAN_INTERVAL, @@ -31,7 +32,6 @@ from .const import ( CALL_TYPE_COIL, CALL_TYPE_DISCRETE, - CONF_BINARY_SENSORS, CONF_COILS, CONF_HUB, CONF_INPUT_TYPE, diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index 6ca1d5d63d310..6140ac038f747 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -15,12 +15,12 @@ SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ( - ATTR_TEMPERATURE, CONF_NAME, CONF_OFFSET, CONF_SCAN_INTERVAL, CONF_SLAVE, CONF_STRUCTURE, + CONF_TEMPERATURE_UNIT, TEMP_CELSIUS, TEMP_FAHRENHEIT, ) @@ -32,6 +32,7 @@ ) from .const import ( + ATTR_TEMPERATURE, CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT, CONF_CLIMATES, @@ -45,7 +46,6 @@ CONF_SCALE, CONF_STEP, CONF_TARGET_TEMP, - CONF_UNIT, DATA_TYPE_CUSTOM, DEFAULT_STRUCT_FORMAT, MODBUS_DOMAIN, @@ -130,7 +130,7 @@ def __init__( self._scale = config[CONF_SCALE] self._scan_interval = timedelta(seconds=config[CONF_SCAN_INTERVAL]) self._offset = config[CONF_OFFSET] - self._unit = config[CONF_UNIT] + self._unit = config[CONF_TEMPERATURE_UNIT] self._max_temp = config[CONF_MAX_TEMP] self._min_temp = config[CONF_MIN_TEMP] self._temp_step = config[CONF_STEP] @@ -208,11 +208,11 @@ def target_temperature_step(self): def set_temperature(self, **kwargs): """Set new target temperature.""" + if ATTR_TEMPERATURE not in kwargs: + return target_temperature = int( (kwargs.get(ATTR_TEMPERATURE) - self._offset) / self._scale ) - if target_temperature is None: - return byte_string = struct.pack(self._structure, target_temperature) register_value = struct.unpack(">h", byte_string[0:2])[0] self._write_register(self._target_temperature_register, register_value) diff --git a/homeassistant/components/modbus/const.py b/homeassistant/components/modbus/const.py index fde593aa9666b..ffe89757ef127 100644 --- a/homeassistant/components/modbus/const.py +++ b/homeassistant/components/modbus/const.py @@ -2,22 +2,51 @@ # configuration names CONF_BAUDRATE = "baudrate" +CONF_BINARY_SENSOR = "binary_sensor" CONF_BYTESIZE = "bytesize" +CONF_CLIMATE = "climate" +CONF_CLIMATES = "climates" +CONF_COILS = "coils" +CONF_COVER = "cover" +CONF_CURRENT_TEMP = "current_temp_register" +CONF_CURRENT_TEMP_REGISTER_TYPE = "current_temp_register_type" +CONF_DATA_COUNT = "data_count" +CONF_DATA_TYPE = "data_type" CONF_HUB = "hub" +CONF_INPUTS = "inputs" +CONF_INPUT_TYPE = "input_type" +CONF_MAX_TEMP = "max_temp" +CONF_MIN_TEMP = "min_temp" CONF_PARITY = "parity" -CONF_STOPBITS = "stopbits" CONF_REGISTER = "register" CONF_REGISTER_TYPE = "register_type" CONF_REGISTERS = "registers" CONF_REVERSE_ORDER = "reverse_order" -CONF_SCALE = "scale" -CONF_COUNT = "count" CONF_PRECISION = "precision" -CONF_COILS = "coils" +CONF_SCALE = "scale" +CONF_SENSOR = "sensor" +CONF_STATE_CLOSED = "state_closed" +CONF_STATE_CLOSING = "state_closing" +CONF_STATE_OFF = "state_off" +CONF_STATE_ON = "state_on" +CONF_STATE_OPEN = "state_open" +CONF_STATE_OPENING = "state_opening" +CONF_STATUS_REGISTER = "status_register" +CONF_STATUS_REGISTER_TYPE = "status_register_type" +CONF_STEP = "temp_step" +CONF_STOPBITS = "stopbits" +CONF_SWITCH = "switch" +CONF_TARGET_TEMP = "target_temp_register" +CONF_VERIFY_REGISTER = "verify_register" +CONF_VERIFY_STATE = "verify_state" -# integration names -DEFAULT_HUB = "modbus_hub" -MODBUS_DOMAIN = "modbus" +# service call attributes +ATTR_ADDRESS = "address" +ATTR_HUB = "hub" +ATTR_UNIT = "unit" +ATTR_VALUE = "value" +ATTR_STATE = "state" +ATTR_TEMPERATURE = "temperature" # data types DATA_TYPE_CUSTOM = "custom" @@ -32,66 +61,19 @@ CALL_TYPE_REGISTER_HOLDING = "holding" CALL_TYPE_REGISTER_INPUT = "input" -# the following constants are TBD. -# changing those in general causes a breaking change, because -# the contents of configuration.yaml needs to be updated, -# therefore they are left to a later date. -# but kept here, with a reference to the file using them. - -# __init.py -ATTR_ADDRESS = "address" -ATTR_HUB = "hub" -ATTR_UNIT = "unit" -ATTR_VALUE = "value" +# service calls SERVICE_WRITE_COIL = "write_coil" SERVICE_WRITE_REGISTER = "write_register" -DEFAULT_SCAN_INTERVAL = 15 # seconds -# binary_sensor.py -CONF_INPUTS = "inputs" -CONF_INPUT_TYPE = "input_type" -CONF_BINARY_SENSORS = "binary_sensors" -CONF_BINARY_SENSOR = "binary_sensor" - -# sensor.py -# CONF_DATA_TYPE = "data_type" +# integration names +DEFAULT_HUB = "modbus_hub" +DEFAULT_SCAN_INTERVAL = 15 # seconds +DEFAULT_SLAVE = 1 +DEFAULT_STRUCTURE_PREFIX = ">f" DEFAULT_STRUCT_FORMAT = { DATA_TYPE_INT: {1: "h", 2: "i", 4: "q"}, DATA_TYPE_UINT: {1: "H", 2: "I", 4: "Q"}, DATA_TYPE_FLOAT: {1: "e", 2: "f", 4: "d"}, } -CONF_SENSOR = "sensor" -CONF_SENSORS = "sensors" - -# switch.py -CONF_STATE_OFF = "state_off" -CONF_STATE_ON = "state_on" -CONF_VERIFY_REGISTER = "verify_register" -CONF_VERIFY_STATE = "verify_state" -CONF_SWITCH = "switch" -CONF_SWITCHES = "switches" - -# climate.py -CONF_CLIMATES = "climates" -CONF_CLIMATE = "climate" -CONF_TARGET_TEMP = "target_temp_register" -CONF_CURRENT_TEMP = "current_temp_register" -CONF_CURRENT_TEMP_REGISTER_TYPE = "current_temp_register_type" -CONF_DATA_TYPE = "data_type" -CONF_DATA_COUNT = "data_count" -CONF_UNIT = "temperature_unit" -CONF_MAX_TEMP = "max_temp" -CONF_MIN_TEMP = "min_temp" -CONF_STEP = "temp_step" -DEFAULT_STRUCTURE_PREFIX = ">f" DEFAULT_TEMP_UNIT = "C" - -# cover.py -CONF_COVER = "cover" -CONF_STATE_OPEN = "state_open" -CONF_STATE_CLOSED = "state_closed" -CONF_STATE_OPENING = "state_opening" -CONF_STATE_CLOSING = "state_closing" -CONF_STATUS_REGISTER = "status_register" -CONF_STATUS_REGISTER_TYPE = "status_register_type" -DEFAULT_SLAVE = 1 +MODBUS_DOMAIN = "modbus" diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index f55e77c911950..099289d8472af 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -6,13 +6,15 @@ from pymodbus.transaction import ModbusRtuFramer from homeassistant.const import ( - ATTR_STATE, + CONF_BINARY_SENSORS, CONF_COVERS, CONF_DELAY, CONF_HOST, CONF_METHOD, CONF_NAME, CONF_PORT, + CONF_SENSORS, + CONF_SWITCHES, CONF_TIMEOUT, CONF_TYPE, EVENT_HOMEASSISTANT_STOP, @@ -22,21 +24,19 @@ from .const import ( ATTR_ADDRESS, ATTR_HUB, + ATTR_STATE, ATTR_UNIT, ATTR_VALUE, CONF_BAUDRATE, CONF_BINARY_SENSOR, - CONF_BINARY_SENSORS, CONF_BYTESIZE, CONF_CLIMATE, CONF_CLIMATES, CONF_COVER, CONF_PARITY, CONF_SENSOR, - CONF_SENSORS, CONF_STOPBITS, CONF_SWITCH, - CONF_SWITCHES, MODBUS_DOMAIN as DOMAIN, SERVICE_WRITE_COIL, SERVICE_WRITE_REGISTER, diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index 7aa08070d6765..21069d8642773 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -17,10 +17,12 @@ ) from homeassistant.const import ( CONF_ADDRESS, + CONF_COUNT, CONF_DEVICE_CLASS, CONF_NAME, CONF_OFFSET, CONF_SCAN_INTERVAL, + CONF_SENSORS, CONF_SLAVE, CONF_STRUCTURE, CONF_UNIT_OF_MEASUREMENT, @@ -37,7 +39,6 @@ from .const import ( CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT, - CONF_COUNT, CONF_DATA_TYPE, CONF_HUB, CONF_INPUT_TYPE, @@ -47,7 +48,6 @@ CONF_REGISTERS, CONF_REVERSE_ORDER, CONF_SCALE, - CONF_SENSORS, DATA_TYPE_CUSTOM, DATA_TYPE_FLOAT, DATA_TYPE_INT, diff --git a/tests/components/modbus/test_modbus_binary_sensor.py b/tests/components/modbus/test_modbus_binary_sensor.py index bc91e3714bbfd..5c4e71cd66936 100644 --- a/tests/components/modbus/test_modbus_binary_sensor.py +++ b/tests/components/modbus/test_modbus_binary_sensor.py @@ -5,12 +5,12 @@ from homeassistant.components.modbus.const import ( CALL_TYPE_COIL, CALL_TYPE_DISCRETE, - CONF_BINARY_SENSORS, CONF_INPUT_TYPE, CONF_INPUTS, ) from homeassistant.const import ( CONF_ADDRESS, + CONF_BINARY_SENSORS, CONF_DEVICE_CLASS, CONF_NAME, CONF_SLAVE, diff --git a/tests/components/modbus/test_modbus_sensor.py b/tests/components/modbus/test_modbus_sensor.py index dd485e59835a2..ce9889d8aaa10 100644 --- a/tests/components/modbus/test_modbus_sensor.py +++ b/tests/components/modbus/test_modbus_sensor.py @@ -4,7 +4,6 @@ from homeassistant.components.modbus.const import ( CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT, - CONF_COUNT, CONF_DATA_TYPE, CONF_INPUT_TYPE, CONF_PRECISION, @@ -13,7 +12,6 @@ CONF_REGISTERS, CONF_REVERSE_ORDER, CONF_SCALE, - CONF_SENSORS, DATA_TYPE_FLOAT, DATA_TYPE_INT, DATA_TYPE_STRING, @@ -22,9 +20,11 @@ from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import ( CONF_ADDRESS, + CONF_COUNT, CONF_DEVICE_CLASS, CONF_NAME, CONF_OFFSET, + CONF_SENSORS, CONF_SLAVE, ) diff --git a/tests/components/modbus/test_modbus_switch.py b/tests/components/modbus/test_modbus_switch.py index a6ec1eb86fd89..91ab5bf97df8a 100644 --- a/tests/components/modbus/test_modbus_switch.py +++ b/tests/components/modbus/test_modbus_switch.py @@ -12,7 +12,6 @@ CONF_REGISTERS, CONF_STATE_OFF, CONF_STATE_ON, - CONF_SWITCHES, CONF_VERIFY_REGISTER, CONF_VERIFY_STATE, ) @@ -24,6 +23,7 @@ CONF_DEVICE_CLASS, CONF_NAME, CONF_SLAVE, + CONF_SWITCHES, STATE_OFF, STATE_ON, ) From b34cc7ef2caf519f6767ee13b7722d52f682d96f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 4 Apr 2021 20:40:08 +0200 Subject: [PATCH 0053/1317] Remove Social Blade integration (ADR-0004) (#48677) * Remove Social Blade integration (ADR-0004) * Cleanup coveragerc --- .coveragerc | 1 - .../components/socialblade/__init__.py | 1 - .../components/socialblade/manifest.json | 7 -- .../components/socialblade/sensor.py | 84 ------------------- requirements_all.txt | 3 - 5 files changed, 96 deletions(-) delete mode 100644 homeassistant/components/socialblade/__init__.py delete mode 100644 homeassistant/components/socialblade/manifest.json delete mode 100644 homeassistant/components/socialblade/sensor.py diff --git a/.coveragerc b/.coveragerc index 22855a26dd945..b7458cdff1da1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -895,7 +895,6 @@ omit = homeassistant/components/snapcast/* homeassistant/components/snmp/* homeassistant/components/sochain/sensor.py - homeassistant/components/socialblade/sensor.py homeassistant/components/solaredge/__init__.py homeassistant/components/solaredge/sensor.py homeassistant/components/solaredge_local/sensor.py diff --git a/homeassistant/components/socialblade/__init__.py b/homeassistant/components/socialblade/__init__.py deleted file mode 100644 index c497d99d32caf..0000000000000 --- a/homeassistant/components/socialblade/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The socialblade component.""" diff --git a/homeassistant/components/socialblade/manifest.json b/homeassistant/components/socialblade/manifest.json deleted file mode 100644 index d73e76869476c..0000000000000 --- a/homeassistant/components/socialblade/manifest.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "domain": "socialblade", - "name": "Social Blade", - "documentation": "https://www.home-assistant.io/integrations/socialblade", - "requirements": ["socialbladeclient==0.5"], - "codeowners": [] -} diff --git a/homeassistant/components/socialblade/sensor.py b/homeassistant/components/socialblade/sensor.py deleted file mode 100644 index e38c45d10b488..0000000000000 --- a/homeassistant/components/socialblade/sensor.py +++ /dev/null @@ -1,84 +0,0 @@ -"""Support for Social Blade.""" -from datetime import timedelta -import logging - -import socialbladeclient -import voluptuous as vol - -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import CONF_NAME -from homeassistant.helpers import config_validation as cv -from homeassistant.util import Throttle - -_LOGGER = logging.getLogger(__name__) - -CHANNEL_ID = "channel_id" - -DEFAULT_NAME = "Social Blade" - -MIN_TIME_BETWEEN_UPDATES = timedelta(hours=2) - -SUBSCRIBERS = "subscribers" - -TOTAL_VIEWS = "total_views" - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CHANNEL_ID): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - } -) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Social Blade sensor.""" - social_blade = SocialBladeSensor(config[CHANNEL_ID], config[CONF_NAME]) - - social_blade.update() - if social_blade.valid_channel_id is False: - return - - add_entities([social_blade]) - - -class SocialBladeSensor(SensorEntity): - """Representation of a Social Blade Sensor.""" - - def __init__(self, case, name): - """Initialize the Social Blade sensor.""" - self._state = None - self.channel_id = case - self._attributes = None - self.valid_channel_id = None - self._name = name - - @property - def name(self): - """Return the name.""" - return self._name - - @property - def state(self): - """Return the state.""" - return self._state - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - if self._attributes: - return self._attributes - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Get the latest data from Social Blade.""" - - try: - data = socialbladeclient.get_data(self.channel_id) - self._attributes = {TOTAL_VIEWS: data[TOTAL_VIEWS]} - self._state = data[SUBSCRIBERS] - self.valid_channel_id = True - - except (ValueError, IndexError): - _LOGGER.error("Unable to find valid channel ID") - self.valid_channel_id = False - self._attributes = None diff --git a/requirements_all.txt b/requirements_all.txt index 8025f6b1e6fd8..88bcfb05e90e5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2080,9 +2080,6 @@ smhi-pkg==1.0.13 # homeassistant.components.snapcast snapcast==2.1.2 -# homeassistant.components.socialblade -socialbladeclient==0.5 - # homeassistant.components.solaredge_local solaredge-local==0.2.0 From d5ef382fd5f58dc032c86a73fb02c0651b1da991 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sun, 4 Apr 2021 21:53:52 +0200 Subject: [PATCH 0054/1317] Add modbus write coils (#48676) * Add missing function in class. write_coils was missing. * Remove dead code. The HA configuration secures that CONF_TYPE only contains legal values, so having an empty assert to catch unknown values is dead code. An empty assert is not informative. --- homeassistant/components/modbus/modbus.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index 099289d8472af..0a5422ff6be0e 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -182,8 +182,6 @@ def setup(self): port=self._config_port, timeout=self._config_timeout, ) - else: - assert False # Connect device self.connect() @@ -228,6 +226,12 @@ def write_coil(self, unit, address, value): kwargs = {"unit": unit} if unit else {} self._client.write_coil(address, value, **kwargs) + def write_coils(self, unit, address, value): + """Write coil.""" + with self._lock: + kwargs = {"unit": unit} if unit else {} + self._client.write_coils(address, value, **kwargs) + def write_register(self, unit, address, value): """Write register.""" with self._lock: From 95e1daa4519e2e9e5dec4c5843ddc5ff09a634c0 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sun, 4 Apr 2021 16:09:07 -0400 Subject: [PATCH 0055/1317] Bump zwave_js dependency to 0.23.1 (#48682) --- homeassistant/components/zwave_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 4d3f5c5f42db3..e6b4ed7c2a830 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave JS", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave_js", - "requirements": ["zwave-js-server-python==0.23.0"], + "requirements": ["zwave-js-server-python==0.23.1"], "codeowners": ["@home-assistant/z-wave"], "dependencies": ["http", "websocket_api"] } diff --git a/requirements_all.txt b/requirements_all.txt index 88bcfb05e90e5..c41c9fda21097 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2396,4 +2396,4 @@ zigpy==0.33.0 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.23.0 +zwave-js-server-python==0.23.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8cc3dbfa6bf02..d7ad137c69b53 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1254,4 +1254,4 @@ zigpy-znp==0.4.0 zigpy==0.33.0 # homeassistant.components.zwave_js -zwave-js-server-python==0.23.0 +zwave-js-server-python==0.23.1 From e008e80bcf39c28dab65638180772e1447e0c6b0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Apr 2021 11:28:29 -1000 Subject: [PATCH 0056/1317] Cleanup sonos (#48684) - Remove unused code - Use async_add_executor_job - Enforce typing --- .../components/sonos/media_player.py | 493 ++++++++++-------- setup.cfg | 2 +- 2 files changed, 265 insertions(+), 230 deletions(-) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 4f265bc6f567c..6d594b906eac5 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -1,10 +1,13 @@ """Support to interface with Sonos players.""" +from __future__ import annotations + import asyncio from contextlib import suppress import datetime import functools as ft import logging import socket +from typing import Any, Callable, Coroutine import urllib.parse import async_timeout @@ -16,7 +19,10 @@ MUSIC_SRC_TV, PLAY_MODE_BY_MEANING, PLAY_MODES, + SoCo, ) +from pysonos.data_structures import DidlFavorite +from pysonos.events_base import Event, SubscriptionBase from pysonos.exceptions import SoCoException, SoCoUPnPException import pysonos.music_library import pysonos.snapshot @@ -51,20 +57,22 @@ from homeassistant.components.media_player.errors import BrowseError from homeassistant.components.plex.const import PLEX_URI_SCHEME from homeassistant.components.plex.services import play_on_sonos +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_TIME, + CONF_HOSTS, EVENT_HOMEASSISTANT_STOP, STATE_IDLE, STATE_PAUSED, STATE_PLAYING, ) -from homeassistant.core import ServiceCall, callback +from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers import config_validation as cv, entity_platform, service import homeassistant.helpers.device_registry as dr from homeassistant.helpers.network import is_internal_request from homeassistant.util.dt import utcnow -from . import CONF_ADVERTISE_ADDR, CONF_HOSTS, CONF_INTERFACE_ADDR +from . import CONF_ADVERTISE_ADDR, CONF_INTERFACE_ADDR from .const import ( DATA_SONOS, DOMAIN as SONOS_DOMAIN, @@ -77,6 +85,7 @@ SCAN_INTERVAL = 10 DISCOVERY_INTERVAL = 60 +SEEN_EXPIRE_TIME = 3.5 * DISCOVERY_INTERVAL SUPPORT_SONOS = ( SUPPORT_BROWSE_MEDIA @@ -139,23 +148,18 @@ class SonosData: """Storage class for platform global data.""" - def __init__(self): + def __init__(self) -> None: """Initialize the data.""" - self.entities = [] - self.discovered = [] + self.entities: list[SonosEntity] = [] + self.discovered: list[str] = [] self.topology_condition = asyncio.Condition() self.discovery_thread = None self.hosts_heartbeat = None -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Sonos platform. Obsolete.""" - _LOGGER.error( - "Loading Sonos by media_player platform configuration is no longer supported" - ) - - -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable +) -> None: """Set up Sonos from a config entry.""" if DATA_SONOS not in hass.data: hass.data[DATA_SONOS] = SonosData() @@ -168,7 +172,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if advertise_addr: pysonos.config.EVENT_ADVERTISE_IP = advertise_addr - def _stop_discovery(event): + def _stop_discovery(event: Event) -> None: data = hass.data[DATA_SONOS] if data.discovery_thread: data.discovery_thread.stop() @@ -177,11 +181,11 @@ def _stop_discovery(event): data.hosts_heartbeat() data.hosts_heartbeat = None - def _discovery(now=None): + def _discovery(now: datetime.datetime | None = None) -> None: """Discover players from network or configuration.""" hosts = config.get(CONF_HOSTS) - def _discovered_player(soco): + def _discovered_player(soco: SoCo) -> None: """Handle a (re)discovered player.""" try: _LOGGER.debug("Reached _discovered_player, soco=%s", soco) @@ -194,7 +198,7 @@ def _discovered_player(soco): entity = _get_entity_from_soco_uid(hass, soco.uid) if entity and (entity.soco == soco or not entity.available): _LOGGER.debug("Seen %s", entity) - hass.add_job(entity.async_seen(soco)) + hass.add_job(entity.async_seen(soco)) # type: ignore except SoCoException as ex: _LOGGER.debug("SoCoException, ex=%s", ex) @@ -234,31 +238,35 @@ def _discovered_player(soco): platform = entity_platform.current_platform.get() @service.verify_domain_control(hass, SONOS_DOMAIN) - async def async_service_handle(service_call: ServiceCall): + async def async_service_handle(service_call: ServiceCall) -> None: """Handle dispatched services.""" + assert platform is not None entities = await platform.async_extract_from_service(service_call) if not entities: return + for entity in entities: + assert isinstance(entity, SonosEntity) + if service_call.service == SERVICE_JOIN: master = platform.entities.get(service_call.data[ATTR_MASTER]) if master: - await SonosEntity.join_multi(hass, master, entities) + await SonosEntity.join_multi(hass, master, entities) # type: ignore[arg-type] else: _LOGGER.error( "Invalid master specified for join service: %s", service_call.data[ATTR_MASTER], ) elif service_call.service == SERVICE_UNJOIN: - await SonosEntity.unjoin_multi(hass, entities) + await SonosEntity.unjoin_multi(hass, entities) # type: ignore[arg-type] elif service_call.service == SERVICE_SNAPSHOT: await SonosEntity.snapshot_multi( - hass, entities, service_call.data[ATTR_WITH_GROUP] + hass, entities, service_call.data[ATTR_WITH_GROUP] # type: ignore[arg-type] ) elif service_call.service == SERVICE_RESTORE: await SonosEntity.restore_multi( - hass, entities, service_call.data[ATTR_WITH_GROUP] + hass, entities, service_call.data[ATTR_WITH_GROUP] # type: ignore[arg-type] ) hass.services.async_register( @@ -287,7 +295,7 @@ async def async_service_handle(service_call: ServiceCall): SONOS_DOMAIN, SERVICE_RESTORE, async_service_handle, join_unjoin_schema ) - platform.async_register_entity_service( + platform.async_register_entity_service( # type: ignore SERVICE_SET_TIMER, { vol.Required(ATTR_SLEEP_TIME): vol.All( @@ -297,9 +305,9 @@ async def async_service_handle(service_call: ServiceCall): "set_sleep_timer", ) - platform.async_register_entity_service(SERVICE_CLEAR_TIMER, {}, "clear_sleep_timer") + platform.async_register_entity_service(SERVICE_CLEAR_TIMER, {}, "clear_sleep_timer") # type: ignore - platform.async_register_entity_service( + platform.async_register_entity_service( # type: ignore SERVICE_UPDATE_ALARM, { vol.Required(ATTR_ALARM_ID): cv.positive_int, @@ -311,7 +319,7 @@ async def async_service_handle(service_call: ServiceCall): "set_alarm", ) - platform.async_register_entity_service( + platform.async_register_entity_service( # type: ignore SERVICE_SET_OPTION, { vol.Optional(ATTR_NIGHT_SOUND): cv.boolean, @@ -321,50 +329,36 @@ async def async_service_handle(service_call: ServiceCall): "set_option", ) - platform.async_register_entity_service( + platform.async_register_entity_service( # type: ignore SERVICE_PLAY_QUEUE, {vol.Optional(ATTR_QUEUE_POSITION): cv.positive_int}, "play_queue", ) - platform.async_register_entity_service( + platform.async_register_entity_service( # type: ignore SERVICE_REMOVE_FROM_QUEUE, {vol.Optional(ATTR_QUEUE_POSITION): cv.positive_int}, "remove_from_queue", ) -class _ProcessSonosEventQueue: - """Queue like object for dispatching sonos events.""" - - def __init__(self, handler): - """Initialize Sonos event queue.""" - self._handler = handler - - def put(self, item, block=True, timeout=None): - """Process event.""" - try: - self._handler(item) - except SoCoException as ex: - _LOGGER.warning("Error calling %s: %s", self._handler, ex) - - -def _get_entity_from_soco_uid(hass, uid): +def _get_entity_from_soco_uid(hass: HomeAssistant, uid: str) -> SonosEntity | None: """Return SonosEntity from SoCo uid.""" - for entity in hass.data[DATA_SONOS].entities: + entities: list[SonosEntity] = hass.data[DATA_SONOS].entities + for entity in entities: if uid == entity.unique_id: return entity return None -def soco_error(errorcodes=None): +def soco_error(errorcodes: list[str] | None = None) -> Callable: """Filter out specified UPnP errors from logs and avoid exceptions.""" - def decorator(funct): + def decorator(funct: Callable) -> Callable: """Decorate functions.""" @ft.wraps(funct) - def wrapper(*args, **kwargs): + def wrapper(*args: Any, **kwargs: Any) -> Any: """Wrap for all soco UPnP exception.""" try: return funct(*args, **kwargs) @@ -379,11 +373,11 @@ def wrapper(*args, **kwargs): return decorator -def soco_coordinator(funct): +def soco_coordinator(funct: Callable) -> Callable: """Call function on coordinator.""" @ft.wraps(funct) - def wrapper(entity, *args, **kwargs): + def wrapper(entity: SonosEntity, *args: Any, **kwargs: Any) -> Any: """Wrap for call to coordinator.""" if entity.is_coordinator: return funct(entity, *args, **kwargs) @@ -392,81 +386,82 @@ def wrapper(entity, *args, **kwargs): return wrapper -def _timespan_secs(timespan): +def _timespan_secs(timespan: str | None) -> None | float: """Parse a time-span into number of seconds.""" if timespan in UNAVAILABLE_VALUES: return None + assert timespan is not None return sum(60 ** x[0] * int(x[1]) for x in enumerate(reversed(timespan.split(":")))) class SonosEntity(MediaPlayerEntity): """Representation of a Sonos entity.""" - def __init__(self, player): + def __init__(self, player: SoCo) -> None: """Initialize the Sonos entity.""" - self._subscriptions = [] - self._poll_timer = None - self._seen_timer = None + self._subscriptions: list[SubscriptionBase] = [] + self._poll_timer: Callable | None = None + self._seen_timer: Callable | None = None self._volume_increment = 2 - self._unique_id = player.uid - self._player = player - self._player_volume = None - self._player_muted = None - self._play_mode = None - self._coordinator = None - self._sonos_group = [self] - self._status = None - self._uri = None + self._unique_id: str = player.uid + self._player: SoCo = player + self._player_volume: int | None = None + self._player_muted: bool | None = None + self._play_mode: str | None = None + self._coordinator: SonosEntity | None = None + self._sonos_group: list[SonosEntity] = [self] + self._status: str | None = None + self._uri: str | None = None self._media_library = pysonos.music_library.MusicLibrary(self.soco) - self._media_duration = None - self._media_position = None - self._media_position_updated_at = None - self._media_image_url = None - self._media_channel = None - self._media_artist = None - self._media_album_name = None - self._media_title = None - self._queue_position = None - self._night_sound = None - self._speech_enhance = None - self._source_name = None - self._favorites = [] - self._soco_snapshot = None - self._snapshot_group = None + self._media_duration: float | None = None + self._media_position: float | None = None + self._media_position_updated_at: datetime.datetime | None = None + self._media_image_url: str | None = None + self._media_channel: str | None = None + self._media_artist: str | None = None + self._media_album_name: str | None = None + self._media_title: str | None = None + self._queue_position: int | None = None + self._night_sound: bool | None = None + self._speech_enhance: bool | None = None + self._source_name: str | None = None + self._favorites: list[DidlFavorite] = [] + self._soco_snapshot: pysonos.snapshot.Snapshot | None = None + self._snapshot_group: list[SonosEntity] | None = None # Set these early since device_info() needs them - speaker_info = self.soco.get_speaker_info(True) - self._name = speaker_info["zone_name"] - self._model = speaker_info["model_name"] - self._sw_version = speaker_info["software_version"] - self._mac_address = speaker_info["mac_address"] + speaker_info: dict = self.soco.get_speaker_info(True) + self._name: str = speaker_info["zone_name"] + self._model: str = speaker_info["model_name"] + self._sw_version: str = speaker_info["software_version"] + self._mac_address: str = speaker_info["mac_address"] - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Subscribe sonos events.""" await self.async_seen(self.soco) self.hass.data[DATA_SONOS].entities.append(self) for entity in self.hass.data[DATA_SONOS].entities: - await entity.async_update_groups_coro() + await entity.create_update_groups_coro() @property - def unique_id(self): + def unique_id(self) -> str: """Return a unique ID.""" return self._unique_id - def __hash__(self): + def __hash__(self) -> int: """Return a hash of self.""" return hash(self.unique_id) @property - def name(self): + def name(self) -> str: """Return the name of the entity.""" return self._name @property - def device_info(self): + def device_info(self) -> dict: """Return information about the device.""" return { "identifiers": {(SONOS_DOMAIN, self._unique_id)}, @@ -478,9 +473,9 @@ def device_info(self): "suggested_area": self._name, } - @property + @property # type: ignore[misc] @soco_coordinator - def state(self): + def state(self) -> str: """Return the state of the entity.""" if self._status in ( "PAUSED_PLAYBACK", @@ -496,21 +491,21 @@ def state(self): return STATE_IDLE @property - def is_coordinator(self): + def is_coordinator(self) -> bool: """Return true if player is a coordinator.""" return self._coordinator is None @property - def soco(self): + def soco(self) -> SoCo: """Return soco object.""" return self._player @property - def coordinator(self): + def coordinator(self) -> SoCo: """Return coordinator of this player.""" return self._coordinator - async def async_seen(self, player): + async def async_seen(self, player: SoCo) -> None: """Record that this player was seen right now.""" was_available = self.available _LOGGER.debug("Async seen: %s, was_available: %s", player, was_available) @@ -521,7 +516,7 @@ async def async_seen(self, player): self._seen_timer() self._seen_timer = self.hass.helpers.event.async_call_later( - 2.5 * DISCOVERY_INTERVAL, self.async_unseen + SEEN_EXPIRE_TIME, self.async_unseen ) if was_available: @@ -533,12 +528,13 @@ async def async_seen(self, player): done = await self._async_attach_player() if not done: + assert self._seen_timer is not None self._seen_timer() await self.async_unseen() self.async_write_ha_state() - async def async_unseen(self, now=None): + async def async_unseen(self, now: datetime.datetime | None = None) -> None: """Make this player unavailable when it was not seen recently.""" self._seen_timer = None @@ -558,12 +554,12 @@ def available(self) -> bool: """Return True if entity is available.""" return self._seen_timer is not None - def _clear_media_position(self): + def _clear_media_position(self) -> None: """Clear the media_position.""" self._media_position = None self._media_position_updated_at = None - def _set_favorites(self): + def _set_favorites(self) -> None: """Set available favorites.""" self._favorites = [] for fav in self.soco.music_library.get_sonos_favorites(): @@ -575,13 +571,13 @@ def _set_favorites(self): # Skip unknown types _LOGGER.error("Unhandled favorite '%s': %s", fav.title, ex) - def _attach_player(self): + def _attach_player(self) -> None: """Get basic information and add event subscriptions.""" self._play_mode = self.soco.play_mode self.update_volume() self._set_favorites() - async def _async_attach_player(self): + async def _async_attach_player(self) -> bool: """Get basic information and add event subscriptions.""" try: await self.hass.async_add_executor_job(self._attach_player) @@ -603,18 +599,20 @@ async def _async_attach_player(self): _LOGGER.warning("Could not connect %s: %s", self.entity_id, ex) return False - async def _subscribe(self, target, sub_callback): + async def _subscribe( + self, target: SubscriptionBase, sub_callback: Callable + ) -> None: """Create a sonos subscription.""" subscription = await target.subscribe(auto_renew=True) subscription.callback = sub_callback self._subscriptions.append(subscription) @property - def should_poll(self): + def should_poll(self) -> bool: """Return that we should not be polled (we handle that internally).""" return False - def update(self, now=None): + def update(self, now: datetime.datetime | None = None) -> None: """Retrieve latest state.""" try: self.update_groups() @@ -625,11 +623,11 @@ def update(self, now=None): pass @callback - def async_update_media(self, event=None): + def async_update_media(self, event: Event | None = None) -> None: """Update information about currently playing media.""" - self.hass.async_add_job(self.update_media, event) + self.hass.async_add_executor_job(self.update_media, event) - def update_media(self, event=None): + def update_media(self, event: Event | None = None) -> None: """Update information about currently playing media.""" variables = event and event.variables @@ -679,7 +677,7 @@ def update_media(self, event=None): self._media_title = track_info.get("title") if music_source == MUSIC_SRC_RADIO: - self.update_media_radio(variables, track_info) + self.update_media_radio(variables) else: self.update_media_music(update_position, track_info) @@ -691,14 +689,14 @@ def update_media(self, event=None): if coordinator and coordinator.unique_id == self.unique_id: entity.schedule_update_ha_state() - def update_media_linein(self, source): + def update_media_linein(self, source: str) -> None: """Update state when playing from line-in/tv.""" self._clear_media_position() self._media_title = source self._source_name = source - def update_media_radio(self, variables, track_info): + def update_media_radio(self, variables: dict) -> None: """Update state when streaming radio.""" self._clear_media_position() @@ -720,7 +718,8 @@ def update_media_radio(self, variables, track_info): ) and ( self.state != STATE_PLAYING or self.soco.music_source_from_uri(self._media_title) == MUSIC_SRC_RADIO - or self._media_title in self._uri + and self._uri is not None + and self._media_title in self._uri # type: ignore[operator] ): self._media_title = uri_meta_data.title except (TypeError, KeyError, AttributeError): @@ -735,7 +734,7 @@ def update_media_radio(self, variables, track_info): if fav.reference.get_uri() == media_info["uri"]: self._source_name = fav.title - def update_media_music(self, update_media_position, track_info): + def update_media_music(self, update_media_position: bool, track_info: dict) -> None: """Update state when playing music tracks.""" self._media_duration = _timespan_secs(track_info.get("duration")) current_position = _timespan_secs(track_info.get("position")) @@ -747,8 +746,9 @@ def update_media_music(self, update_media_position, track_info): # position jumped? if current_position is not None and self._media_position is not None: if self.state == STATE_PLAYING: - time_diff = utcnow() - self._media_position_updated_at - time_diff = time_diff.total_seconds() + assert self._media_position_updated_at is not None + time_delta = utcnow() - self._media_position_updated_at + time_diff = time_delta.total_seconds() else: time_diff = 0 @@ -765,12 +765,12 @@ def update_media_music(self, update_media_position, track_info): self._media_image_url = track_info.get("album_art") - playlist_position = int(track_info.get("playlist_position")) + playlist_position = int(track_info.get("playlist_position")) # type: ignore if playlist_position > 0: self._queue_position = playlist_position - 1 @callback - def async_update_volume(self, event): + def async_update_volume(self, event: Event) -> None: """Update information about currently volume settings.""" variables = event.variables @@ -788,30 +788,30 @@ def async_update_volume(self, event): self.async_write_ha_state() - def update_volume(self): + def update_volume(self) -> None: """Update information about currently volume settings.""" self._player_volume = self.soco.volume self._player_muted = self.soco.mute self._night_sound = self.soco.night_mode self._speech_enhance = self.soco.dialog_mode - def update_groups(self, event=None): + def update_groups(self, event: Event | None = None) -> None: """Handle callback for topology change event.""" - coro = self.async_update_groups_coro(event) + coro = self.create_update_groups_coro(event) if coro: - self.hass.add_job(coro) + self.hass.add_job(coro) # type: ignore @callback - def async_update_groups(self, event=None): + def async_update_groups(self, event: Event | None = None) -> None: """Handle callback for topology change event.""" - coro = self.async_update_groups_coro(event) + coro = self.create_update_groups_coro(event) if coro: - self.hass.async_add_job(coro) + self.hass.async_add_job(coro) # type: ignore - def async_update_groups_coro(self, event=None): + def create_update_groups_coro(self, event: Event | None = None) -> Coroutine | None: """Handle callback for topology change event.""" - def _get_soco_group(): + def _get_soco_group() -> list[str]: """Ask SoCo cache for existing topology.""" coordinator_uid = self.unique_id slave_uids = [] @@ -827,16 +827,17 @@ def _get_soco_group(): return [coordinator_uid] + slave_uids - async def _async_extract_group(event): + async def _async_extract_group(event: Event) -> list[str]: """Extract group layout from a topology event.""" group = event and event.zone_player_uui_ds_in_group if group: + assert isinstance(group, str) return group.split(",") return await self.hass.async_add_executor_job(_get_soco_group) @callback - def _async_regroup(group): + def _async_regroup(group: list[str]) -> None: """Rebuild internal group layout.""" sonos_group = [] for uid in group: @@ -856,7 +857,7 @@ def _async_regroup(group): slave._sonos_group = sonos_group slave.async_schedule_update_ha_state() - async def _async_handle_group_event(event): + async def _async_handle_group_event(event: Event) -> None: """Get async lock and handle event.""" if event and self._poll_timer: # Cancel poll timer since we do receive events @@ -872,136 +873,136 @@ async def _async_handle_group_event(event): self.hass.data[DATA_SONOS].topology_condition.notify_all() if event and not hasattr(event, "zone_player_uui_ds_in_group"): - return + return None return _async_handle_group_event(event) - def async_update_content(self, event=None): + @callback + def async_update_content(self, event: Event | None = None) -> None: """Update information about available content.""" if event and "favorites_update_id" in event.variables: self.hass.async_add_job(self._set_favorites) self.async_write_ha_state() @property - def volume_level(self): + def volume_level(self) -> int | None: """Volume level of the media player (0..1).""" - if self._player_volume is None: - return None - return self._player_volume / 100 + return self._player_volume and int(self._player_volume / 100) @property - def is_volume_muted(self): + def is_volume_muted(self) -> bool | None: """Return true if volume is muted.""" return self._player_muted - @property + @property # type: ignore[misc] @soco_coordinator - def shuffle(self): + def shuffle(self) -> str | None: """Shuffling state.""" - return PLAY_MODES[self._play_mode][0] + shuffle: str = PLAY_MODES[self._play_mode][0] + return shuffle - @property + @property # type: ignore[misc] @soco_coordinator - def repeat(self): + def repeat(self) -> str | None: """Return current repeat mode.""" sonos_repeat = PLAY_MODES[self._play_mode][1] return SONOS_TO_REPEAT[sonos_repeat] - @property + @property # type: ignore[misc] @soco_coordinator - def media_content_id(self): + def media_content_id(self) -> str | None: """Content id of current playing media.""" return self._uri @property - def media_content_type(self): + def media_content_type(self) -> str: """Content type of current playing media.""" return MEDIA_TYPE_MUSIC - @property + @property # type: ignore[misc] @soco_coordinator - def media_duration(self): + def media_duration(self) -> float | None: """Duration of current playing media in seconds.""" return self._media_duration - @property + @property # type: ignore[misc] @soco_coordinator - def media_position(self): + def media_position(self) -> float | None: """Position of current playing media in seconds.""" return self._media_position - @property + @property # type: ignore[misc] @soco_coordinator - def media_position_updated_at(self): + def media_position_updated_at(self) -> datetime.datetime | None: """When was the position of the current playing media valid.""" return self._media_position_updated_at - @property + @property # type: ignore[misc] @soco_coordinator - def media_image_url(self): + def media_image_url(self) -> str | None: """Image url of current playing media.""" return self._media_image_url or None - @property + @property # type: ignore[misc] @soco_coordinator - def media_channel(self): + def media_channel(self) -> str | None: """Channel currently playing.""" return self._media_channel or None - @property + @property # type: ignore[misc] @soco_coordinator - def media_artist(self): + def media_artist(self) -> str | None: """Artist of current playing media, music track only.""" return self._media_artist or None - @property + @property # type: ignore[misc] @soco_coordinator - def media_album_name(self): + def media_album_name(self) -> str | None: """Album name of current playing media, music track only.""" return self._media_album_name or None - @property + @property # type: ignore[misc] @soco_coordinator - def media_title(self): + def media_title(self) -> str | None: """Title of current playing media.""" return self._media_title or None - @property + @property # type: ignore[misc] @soco_coordinator - def queue_position(self): + def queue_position(self) -> int | None: """If playing local queue return the position in the queue else None.""" return self._queue_position - @property + @property # type: ignore[misc] @soco_coordinator - def source(self): + def source(self) -> str | None: """Name of the current input source.""" return self._source_name or None - @property + @property # type: ignore[misc] @soco_coordinator - def supported_features(self): + def supported_features(self) -> int: """Flag media player features that are supported.""" return SUPPORT_SONOS @soco_error() - def volume_up(self): + def volume_up(self) -> None: """Volume up media player.""" self._player.volume += self._volume_increment @soco_error() - def volume_down(self): + def volume_down(self) -> None: """Volume down media player.""" self._player.volume -= self._volume_increment @soco_error() - def set_volume_level(self, volume): + def set_volume_level(self, volume: str) -> None: """Set volume level, range 0..1.""" self.soco.volume = str(int(volume * 100)) @soco_error(UPNP_ERRORS_TO_IGNORE) @soco_coordinator - def set_shuffle(self, shuffle): + def set_shuffle(self, shuffle: str) -> None: """Enable/Disable shuffle mode.""" sonos_shuffle = shuffle sonos_repeat = PLAY_MODES[self._play_mode][1] @@ -1009,20 +1010,20 @@ def set_shuffle(self, shuffle): @soco_error(UPNP_ERRORS_TO_IGNORE) @soco_coordinator - def set_repeat(self, repeat): + def set_repeat(self, repeat: str) -> None: """Set repeat mode.""" sonos_shuffle = PLAY_MODES[self._play_mode][0] sonos_repeat = REPEAT_TO_SONOS[repeat] self.soco.play_mode = PLAY_MODE_BY_MEANING[(sonos_shuffle, sonos_repeat)] @soco_error() - def mute_volume(self, mute): + def mute_volume(self, mute: bool) -> None: """Mute (true) or unmute (false) media player.""" self.soco.mute = mute @soco_error() @soco_coordinator - def select_source(self, source): + def select_source(self, source: str) -> None: """Select input source.""" if source == SOURCE_LINEIN: self.soco.switch_to_line_in() @@ -1043,9 +1044,9 @@ def select_source(self, source): self.soco.add_to_queue(src.reference) self.soco.play_from_queue(0) - @property + @property # type: ignore[misc] @soco_coordinator - def source_list(self): + def source_list(self) -> list[str]: """List of available input sources.""" sources = [fav.title for fav in self._favorites] @@ -1061,49 +1062,49 @@ def source_list(self): @soco_error(UPNP_ERRORS_TO_IGNORE) @soco_coordinator - def media_play(self): + def media_play(self) -> None: """Send play command.""" self.soco.play() @soco_error(UPNP_ERRORS_TO_IGNORE) @soco_coordinator - def media_stop(self): + def media_stop(self) -> None: """Send stop command.""" self.soco.stop() @soco_error(UPNP_ERRORS_TO_IGNORE) @soco_coordinator - def media_pause(self): + def media_pause(self) -> None: """Send pause command.""" self.soco.pause() @soco_error(UPNP_ERRORS_TO_IGNORE) @soco_coordinator - def media_next_track(self): + def media_next_track(self) -> None: """Send next track command.""" self.soco.next() @soco_error(UPNP_ERRORS_TO_IGNORE) @soco_coordinator - def media_previous_track(self): + def media_previous_track(self) -> None: """Send next track command.""" self.soco.previous() @soco_error(UPNP_ERRORS_TO_IGNORE) @soco_coordinator - def media_seek(self, position): + def media_seek(self, position: str) -> None: """Send seek command.""" self.soco.seek(str(datetime.timedelta(seconds=int(position)))) @soco_error() @soco_coordinator - def clear_playlist(self): + def clear_playlist(self) -> None: """Clear players playlist.""" self.soco.clear_queue() @soco_error() @soco_coordinator - def play_media(self, media_type, media_id, **kwargs): + def play_media(self, media_type: str, media_id: str, **kwargs: Any) -> None: """ Send the play_media command to the media player. @@ -1116,7 +1117,7 @@ def play_media(self, media_type, media_id, **kwargs): """ if media_id and media_id.startswith(PLEX_URI_SCHEME): media_id = media_id[len(PLEX_URI_SCHEME) :] - play_on_sonos(self.hass, media_type, media_id, self.name) + play_on_sonos(self.hass, media_type, media_id, self.name) # type: ignore[no-untyped-call] elif media_type in (MEDIA_TYPE_MUSIC, MEDIA_TYPE_TRACK): if kwargs.get(ATTR_MEDIA_ENQUEUE): try: @@ -1140,7 +1141,7 @@ def play_media(self, media_type, media_id, **kwargs): self.soco.play_uri(media_id) elif media_type == MEDIA_TYPE_PLAYLIST: if media_id.startswith("S:"): - item = get_media(self._media_library, media_id, media_type) + item = get_media(self._media_library, media_id, media_type) # type: ignore[no-untyped-call] self.soco.play_uri(item.get_uri()) return try: @@ -1152,7 +1153,7 @@ def play_media(self, media_type, media_id, **kwargs): except StopIteration: _LOGGER.error('Could not find a Sonos playlist named "%s"', media_id) elif media_type in PLAYABLE_MEDIA_TYPES: - item = get_media(self._media_library, media_id, media_type) + item = get_media(self._media_library, media_id, media_type) # type: ignore[no-untyped-call] if not item: _LOGGER.error('Could not find "%s" in the library', media_id) @@ -1163,7 +1164,7 @@ def play_media(self, media_type, media_id, **kwargs): _LOGGER.error('Sonos does not support a media type of "%s"', media_type) @soco_error() - def join(self, slaves): + def join(self, slaves: list[SonosEntity]) -> list[SonosEntity]: """Form a group with other players.""" if self._coordinator: self.unjoin() @@ -1182,23 +1183,27 @@ def join(self, slaves): return group @staticmethod - async def join_multi(hass, master, entities): + async def join_multi( + hass: HomeAssistant, master: SonosEntity, entities: list[SonosEntity] + ) -> None: """Form a group with other players.""" async with hass.data[DATA_SONOS].topology_condition: - group = await hass.async_add_executor_job(master.join, entities) + group: list[SonosEntity] = await hass.async_add_executor_job( + master.join, entities + ) await SonosEntity.wait_for_groups(hass, [group]) @soco_error() - def unjoin(self): + def unjoin(self) -> None: """Unjoin the player from a group.""" self.soco.unjoin() self._coordinator = None @staticmethod - async def unjoin_multi(hass, entities): + async def unjoin_multi(hass: HomeAssistant, entities: list[SonosEntity]) -> None: """Unjoin several players from their group.""" - def _unjoin_all(entities): + def _unjoin_all(entities: list[SonosEntity]) -> None: """Sync helper.""" # Unjoin slaves first to prevent inheritance of queues coordinators = [e for e in entities if e.is_coordinator] @@ -1212,7 +1217,7 @@ def _unjoin_all(entities): await SonosEntity.wait_for_groups(hass, [[e] for e in entities]) @soco_error() - def snapshot(self, with_group): + def snapshot(self, with_group: bool) -> None: """Snapshot the state of a player.""" self._soco_snapshot = pysonos.snapshot.Snapshot(self.soco) self._soco_snapshot.snapshot() @@ -1222,30 +1227,33 @@ def snapshot(self, with_group): self._snapshot_group = None @staticmethod - async def snapshot_multi(hass, entities, with_group): + async def snapshot_multi( + hass: HomeAssistant, entities: list[SonosEntity], with_group: bool + ) -> None: """Snapshot all the entities and optionally their groups.""" # pylint: disable=protected-access - def _snapshot_all(entities): + def _snapshot_all(entities: list[SonosEntity]) -> None: """Sync helper.""" for entity in entities: entity.snapshot(with_group) # Find all affected players - entities = set(entities) + entities_set = set(entities) if with_group: - for entity in list(entities): - entities.update(entity._sonos_group) + for entity in list(entities_set): + entities_set.update(entity._sonos_group) async with hass.data[DATA_SONOS].topology_condition: - await hass.async_add_executor_job(_snapshot_all, entities) + await hass.async_add_executor_job(_snapshot_all, entities_set) @soco_error() - def restore(self): + def restore(self) -> None: """Restore a snapshotted state to a player.""" try: + assert self._soco_snapshot is not None self._soco_snapshot.restore() - except (TypeError, AttributeError, SoCoException) as ex: + except (TypeError, AssertionError, AttributeError, SoCoException) as ex: # Can happen if restoring a coordinator onto a current slave _LOGGER.warning("Error on restore %s: %s", self.entity_id, ex) @@ -1253,11 +1261,15 @@ def restore(self): self._snapshot_group = None @staticmethod - async def restore_multi(hass, entities, with_group): + async def restore_multi( + hass: HomeAssistant, entities: list[SonosEntity], with_group: bool + ) -> None: """Restore snapshots for all the entities.""" # pylint: disable=protected-access - def _restore_groups(entities, with_group): + def _restore_groups( + entities: list[SonosEntity], with_group: bool + ) -> list[list[SonosEntity]]: """Pause all current coordinators and restore groups.""" for entity in (e for e in entities if e.is_coordinator): if entity.state == STATE_PLAYING: @@ -1273,13 +1285,14 @@ def _restore_groups(entities, with_group): # Bring back the original group topology for entity in (e for e in entities if e._snapshot_group): + assert entity._snapshot_group is not None if entity._snapshot_group[0] == entity: entity.join(entity._snapshot_group) groups.append(entity._snapshot_group.copy()) return groups - def _restore_players(entities): + def _restore_players(entities: list[SonosEntity]) -> None: """Restore state of all players.""" for entity in (e for e in entities if not e.is_coordinator): entity.restore() @@ -1288,26 +1301,29 @@ def _restore_players(entities): entity.restore() # Find all affected players - entities = {e for e in entities if e._soco_snapshot} + entities_set = {e for e in entities if e._soco_snapshot} if with_group: - for entity in [e for e in entities if e._snapshot_group]: - entities.update(entity._snapshot_group) + for entity in [e for e in entities_set if e._snapshot_group]: + assert entity._snapshot_group is not None + entities_set.update(entity._snapshot_group) async with hass.data[DATA_SONOS].topology_condition: groups = await hass.async_add_executor_job( - _restore_groups, entities, with_group + _restore_groups, entities_set, with_group ) await SonosEntity.wait_for_groups(hass, groups) - await hass.async_add_executor_job(_restore_players, entities) + await hass.async_add_executor_job(_restore_players, entities_set) @staticmethod - async def wait_for_groups(hass, groups): + async def wait_for_groups( + hass: HomeAssistant, groups: list[list[SonosEntity]] + ) -> None: """Wait until all groups are present, or timeout.""" # pylint: disable=protected-access - def _test_groups(groups): + def _test_groups(groups: list[list[SonosEntity]]) -> bool: """Return whether all groups exist now.""" for group in groups: coordinator = group[0] @@ -1335,21 +1351,26 @@ def _test_groups(groups): @soco_error() @soco_coordinator - def set_sleep_timer(self, sleep_time): + def set_sleep_timer(self, sleep_time: int) -> None: """Set the timer on the player.""" self.soco.set_sleep_timer(sleep_time) @soco_error() @soco_coordinator - def clear_sleep_timer(self): + def clear_sleep_timer(self) -> None: """Clear the timer on the player.""" self.soco.set_sleep_timer(None) @soco_error() @soco_coordinator def set_alarm( - self, alarm_id, time=None, volume=None, enabled=None, include_linked_zones=None - ): + self, + alarm_id: int, + time: datetime.datetime | None = None, + volume: float | None = None, + enabled: bool | None = None, + include_linked_zones: bool | None = None, + ) -> None: """Set the alarm clock on the player.""" alarm = None for one_alarm in alarms.get_alarms(self.soco): @@ -1370,7 +1391,12 @@ def set_alarm( alarm.save() @soco_error() - def set_option(self, night_sound=None, speech_enhance=None, status_light=None): + def set_option( + self, + night_sound: bool | None = None, + speech_enhance: bool | None = None, + status_light: bool | None = None, + ) -> None: """Modify playback options.""" if night_sound is not None and self._night_sound is not None: self.soco.night_mode = night_sound @@ -1382,20 +1408,22 @@ def set_option(self, night_sound=None, speech_enhance=None, status_light=None): self.soco.status_light = status_light @soco_error() - def play_queue(self, queue_position=0): + def play_queue(self, queue_position: int = 0) -> None: """Start playing the queue.""" self.soco.play_from_queue(queue_position) @soco_error() @soco_coordinator - def remove_from_queue(self, queue_position=0): + def remove_from_queue(self, queue_position: int = 0) -> None: """Remove item from the queue.""" self.soco.remove_from_queue(queue_position) @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return entity specific state attributes.""" - attributes = {ATTR_SONOS_GROUP: [e.entity_id for e in self._sonos_group]} + attributes: dict[str, Any] = { + ATTR_SONOS_GROUP: [e.entity_id for e in self._sonos_group] + } if self._night_sound is not None: attributes[ATTR_NIGHT_SOUND] = self._night_sound @@ -1409,8 +1437,11 @@ def extra_state_attributes(self): return attributes async def async_get_browse_image( - self, media_content_type, media_content_id, media_image_id=None - ): + self, + media_content_type: str | None, + media_content_id: str | None, + media_image_id: str | None = None, + ) -> tuple[None | str, None | str]: """Fetch media browser image to serve via proxy.""" if ( media_content_type in [MEDIA_TYPE_ALBUM, MEDIA_TYPE_ARTIST] @@ -1424,25 +1455,29 @@ async def async_get_browse_image( ) image_url = getattr(item, "album_art_uri", None) if image_url: - result = await self._async_fetch_image(image_url) - return result + result = await self._async_fetch_image(image_url) # type: ignore[no-untyped-call] + return result # type: ignore return (None, None) - async def async_browse_media(self, media_content_type=None, media_content_id=None): + async def async_browse_media( + self, media_content_type: str | None = None, media_content_id: str | None = None + ) -> Any: """Implement the websocket media browsing helper.""" is_internal = is_internal_request(self.hass) def _get_thumbnail_url( - media_content_type, media_content_id, media_image_id=None - ): + media_content_type: str, + media_content_id: str, + media_image_id: str | None = None, + ) -> str | None: if is_internal: - item = get_media( + item = get_media( # type: ignore[no-untyped-call] self._media_library, media_content_id, media_content_type, ) - return getattr(item, "album_art_uri", None) + return getattr(item, "album_art_uri", None) # type: ignore[no-any-return] return self.get_browse_image_url( media_content_type, diff --git a/setup.cfg b/setup.cfg index 65b598f4f6f7e..d8569ad218857 100644 --- a/setup.cfg +++ b/setup.cfg @@ -43,7 +43,7 @@ warn_redundant_casts = true warn_unused_configs = true -[mypy-homeassistant.block_async_io,homeassistant.bootstrap,homeassistant.components,homeassistant.config_entries,homeassistant.config,homeassistant.const,homeassistant.core,homeassistant.data_entry_flow,homeassistant.exceptions,homeassistant.__init__,homeassistant.loader,homeassistant.__main__,homeassistant.requirements,homeassistant.runner,homeassistant.setup,homeassistant.util,homeassistant.auth.*,homeassistant.components.automation.*,homeassistant.components.binary_sensor.*,homeassistant.components.bond.*,homeassistant.components.calendar.*,homeassistant.components.cover.*,homeassistant.components.device_automation.*,homeassistant.components.frontend.*,homeassistant.components.geo_location.*,homeassistant.components.group.*,homeassistant.components.history.*,homeassistant.components.http.*,homeassistant.components.huawei_lte.*,homeassistant.components.hyperion.*,homeassistant.components.image_processing.*,homeassistant.components.integration.*,homeassistant.components.knx.*,homeassistant.components.light.*,homeassistant.components.lock.*,homeassistant.components.mailbox.*,homeassistant.components.media_player.*,homeassistant.components.notify.*,homeassistant.components.number.*,homeassistant.components.persistent_notification.*,homeassistant.components.proximity.*,homeassistant.components.recorder.purge,homeassistant.components.recorder.repack,homeassistant.components.remote.*,homeassistant.components.scene.*,homeassistant.components.sensor.*,homeassistant.components.slack.*,homeassistant.components.sun.*,homeassistant.components.switch.*,homeassistant.components.systemmonitor.*,homeassistant.components.tts.*,homeassistant.components.vacuum.*,homeassistant.components.water_heater.*,homeassistant.components.weather.*,homeassistant.components.websocket_api.*,homeassistant.components.zeroconf.*,homeassistant.components.zone.*,homeassistant.components.zwave_js.*,homeassistant.helpers.*,homeassistant.scripts.*,homeassistant.util.*,tests.components.hyperion.*] +[mypy-homeassistant.block_async_io,homeassistant.bootstrap,homeassistant.components,homeassistant.config_entries,homeassistant.config,homeassistant.const,homeassistant.core,homeassistant.data_entry_flow,homeassistant.exceptions,homeassistant.__init__,homeassistant.loader,homeassistant.__main__,homeassistant.requirements,homeassistant.runner,homeassistant.setup,homeassistant.util,homeassistant.auth.*,homeassistant.components.automation.*,homeassistant.components.binary_sensor.*,homeassistant.components.bond.*,homeassistant.components.calendar.*,homeassistant.components.cover.*,homeassistant.components.device_automation.*,homeassistant.components.frontend.*,homeassistant.components.geo_location.*,homeassistant.components.group.*,homeassistant.components.history.*,homeassistant.components.http.*,homeassistant.components.huawei_lte.*,homeassistant.components.hyperion.*,homeassistant.components.image_processing.*,homeassistant.components.integration.*,homeassistant.components.knx.*,homeassistant.components.light.*,homeassistant.components.lock.*,homeassistant.components.mailbox.*,homeassistant.components.media_player.*,homeassistant.components.notify.*,homeassistant.components.number.*,homeassistant.components.persistent_notification.*,homeassistant.components.proximity.*,homeassistant.components.recorder.purge,homeassistant.components.recorder.repack,homeassistant.components.remote.*,homeassistant.components.scene.*,homeassistant.components.sensor.*,homeassistant.components.slack.*,homeassistant.components.sonos.media_player,homeassistant.components.sun.*,homeassistant.components.switch.*,homeassistant.components.systemmonitor.*,homeassistant.components.tts.*,homeassistant.components.vacuum.*,homeassistant.components.water_heater.*,homeassistant.components.weather.*,homeassistant.components.websocket_api.*,homeassistant.components.zeroconf.*,homeassistant.components.zone.*,homeassistant.components.zwave_js.*,homeassistant.helpers.*,homeassistant.scripts.*,homeassistant.util.*,tests.components.hyperion.*] strict = true ignore_errors = false warn_unreachable = true From 9553ae81962b1f70cbfd14d2e5234242fab83739 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 4 Apr 2021 23:44:43 +0200 Subject: [PATCH 0057/1317] Upgrade wakonlan to 2.0.0 (#48683) --- homeassistant/components/wake_on_lan/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wake_on_lan/manifest.json b/homeassistant/components/wake_on_lan/manifest.json index bcd7ef58c8cd3..b98414257720a 100644 --- a/homeassistant/components/wake_on_lan/manifest.json +++ b/homeassistant/components/wake_on_lan/manifest.json @@ -2,6 +2,6 @@ "domain": "wake_on_lan", "name": "Wake on LAN", "documentation": "https://www.home-assistant.io/integrations/wake_on_lan", - "requirements": ["wakeonlan==1.1.6"], + "requirements": ["wakeonlan==2.0.0"], "codeowners": ["@ntilley905"] } diff --git a/requirements_all.txt b/requirements_all.txt index c41c9fda21097..f1f5128a79038 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2287,7 +2287,7 @@ vtjp==0.1.14 vultr==0.1.2 # homeassistant.components.wake_on_lan -wakeonlan==1.1.6 +wakeonlan==2.0.0 # homeassistant.components.waqi waqiasync==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d7ad137c69b53..cfbaa1fa0b5a5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1190,7 +1190,7 @@ vsure==1.7.3 vultr==0.1.2 # homeassistant.components.wake_on_lan -wakeonlan==1.1.6 +wakeonlan==2.0.0 # homeassistant.components.folder_watcher watchdog==1.0.2 From 32daa63265e7ee093683aec9d14dfddc386c2fed Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Apr 2021 12:12:58 -1000 Subject: [PATCH 0058/1317] Use shared aiohttp.ClientSession in bond (#48669) --- homeassistant/components/bond/__init__.py | 8 +++++++- homeassistant/components/bond/config_flow.py | 18 ++++++++++++------ homeassistant/components/bond/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 22 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/bond/__init__.py b/homeassistant/components/bond/__init__.py index 0fafb61df35ae..800e130251742 100644 --- a/homeassistant/components/bond/__init__.py +++ b/homeassistant/components/bond/__init__.py @@ -10,6 +10,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import SLOW_UPDATE_WARNING from .const import BPUP_STOP, BPUP_SUBS, BRIDGE_MAKE, DOMAIN, HUB @@ -25,7 +26,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: token = entry.data[CONF_ACCESS_TOKEN] config_entry_id = entry.entry_id - bond = Bond(host=host, token=token, timeout=ClientTimeout(total=_API_TIMEOUT)) + bond = Bond( + host=host, + token=token, + timeout=ClientTimeout(total=_API_TIMEOUT), + session=async_get_clientsession(hass), + ) hub = BondHub(bond) try: await hub.setup() diff --git a/homeassistant/components/bond/config_flow.py b/homeassistant/components/bond/config_flow.py index 763a095787621..2e1f106193efe 100644 --- a/homeassistant/components/bond/config_flow.py +++ b/homeassistant/components/bond/config_flow.py @@ -15,6 +15,8 @@ CONF_NAME, HTTP_UNAUTHORIZED, ) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import DiscoveryInfoType from .const import DOMAIN @@ -30,10 +32,12 @@ TOKEN_SCHEMA = vol.Schema({}) -async def _validate_input(data: dict[str, Any]) -> tuple[str, str]: +async def _validate_input(hass: HomeAssistant, data: dict[str, Any]) -> tuple[str, str]: """Validate the user input allows us to connect.""" - bond = Bond(data[CONF_HOST], data[CONF_ACCESS_TOKEN]) + bond = Bond( + data[CONF_HOST], data[CONF_ACCESS_TOKEN], session=async_get_clientsession(hass) + ) try: hub = BondHub(bond) await hub.setup(max_devices=1) @@ -71,7 +75,9 @@ async def _async_try_automatic_configure(self) -> None: online longer then the allowed setup period, and we will instead ask them to manually enter the token. """ - bond = Bond(self._discovered[CONF_HOST], "") + bond = Bond( + self._discovered[CONF_HOST], "", session=async_get_clientsession(self.hass) + ) try: response = await bond.token() except ClientConnectionError: @@ -82,7 +88,7 @@ async def _async_try_automatic_configure(self) -> None: return self._discovered[CONF_ACCESS_TOKEN] = token - _, hub_name = await _validate_input(self._discovered) + _, hub_name = await _validate_input(self.hass, self._discovered) self._discovered[CONF_NAME] = hub_name async def async_step_zeroconf(self, discovery_info: DiscoveryInfoType) -> dict[str, Any]: # type: ignore @@ -127,7 +133,7 @@ async def async_step_confirm( CONF_HOST: self._discovered[CONF_HOST], } try: - _, hub_name = await _validate_input(data) + _, hub_name = await _validate_input(self.hass, data) except InputValidationError as error: errors["base"] = error.base else: @@ -155,7 +161,7 @@ async def async_step_user( errors = {} if user_input is not None: try: - bond_id, hub_name = await _validate_input(user_input) + bond_id, hub_name = await _validate_input(self.hass, user_input) except InputValidationError as error: errors["base"] = error.base else: diff --git a/homeassistant/components/bond/manifest.json b/homeassistant/components/bond/manifest.json index 65cb6a83bb213..7204ac7e91df1 100644 --- a/homeassistant/components/bond/manifest.json +++ b/homeassistant/components/bond/manifest.json @@ -3,7 +3,7 @@ "name": "Bond", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/bond", - "requirements": ["bond-api==0.1.11"], + "requirements": ["bond-api==0.1.12"], "zeroconf": ["_bond._tcp.local."], "codeowners": ["@prystupa"], "quality_scale": "platinum" diff --git a/requirements_all.txt b/requirements_all.txt index f1f5128a79038..df497eb889a34 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -374,7 +374,7 @@ blockchain==1.4.4 # bme680==1.0.5 # homeassistant.components.bond -bond-api==0.1.11 +bond-api==0.1.12 # homeassistant.components.amazon_polly # homeassistant.components.route53 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cfbaa1fa0b5a5..91dc8ff600e74 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -208,7 +208,7 @@ blebox_uniapi==1.3.2 blinkpy==0.17.0 # homeassistant.components.bond -bond-api==0.1.11 +bond-api==0.1.12 # homeassistant.components.braviatv bravia-tv==1.0.8 From d5e54505400062bb04b6c9ffe0372219460a800f Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Mon, 5 Apr 2021 00:05:16 +0000 Subject: [PATCH 0059/1317] [ci skip] Translation update --- .../google_travel_time/translations/fr.json | 23 +++++++++++++++++++ .../home_plus_control/translations/fr.json | 5 +++- .../huisbaasje/translations/fr.json | 1 + .../components/mqtt/translations/nl.json | 2 +- .../components/roomba/translations/fr.json | 3 ++- .../screenlogic/translations/fr.json | 2 +- 6 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/google_travel_time/translations/fr.json diff --git a/homeassistant/components/google_travel_time/translations/fr.json b/homeassistant/components/google_travel_time/translations/fr.json new file mode 100644 index 0000000000000..b5b59b5329c61 --- /dev/null +++ b/homeassistant/components/google_travel_time/translations/fr.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "common::config_flow::data::api_key", + "origin": "Origine" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "language": "Langue", + "units": "Unit\u00e9s" + } + } + } + }, + "title": "Temps de trajet Google Maps" +} \ No newline at end of file diff --git a/homeassistant/components/home_plus_control/translations/fr.json b/homeassistant/components/home_plus_control/translations/fr.json index dbdea8cca563d..c39d4a2867eaf 100644 --- a/homeassistant/components/home_plus_control/translations/fr.json +++ b/homeassistant/components/home_plus_control/translations/fr.json @@ -2,8 +2,11 @@ "config": { "abort": { "already_configured": "Compte d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", + "authorize_url_timeout": "[%key::common::config_flow::abort::oauth2_authorize_url_timeout%]", "missing_configuration": "Le composant n'est pas configur\u00e9. Merci de suivre la documentation.", - "no_url_available": "Aucune URL disponible. Pour plus d'information sur cette erreur, [v\u00e9rifier la section d'aide]({docs_url})" + "no_url_available": "Aucune URL disponible. Pour plus d'information sur cette erreur, [v\u00e9rifier la section d'aide]({docs_url})", + "single_instance_allowed": "[%key::common::config_flow::abort::single_instance_allowed%]" }, "create_entry": { "default": "Authentification r\u00e9ussie" diff --git a/homeassistant/components/huisbaasje/translations/fr.json b/homeassistant/components/huisbaasje/translations/fr.json index 9f78d7d882605..567f4a08f4ad1 100644 --- a/homeassistant/components/huisbaasje/translations/fr.json +++ b/homeassistant/components/huisbaasje/translations/fr.json @@ -4,6 +4,7 @@ "already_configured": "L'appareil est d\u00e9ja configur\u00e9 " }, "error": { + "cannot_connect": "[%key::common::config_flow::error::cannot_connect%]", "connection_exception": "\u00c9chec de la connexion ", "invalid_auth": "Authentification invalide ", "unauthenticated_exception": "Authentification invalide ", diff --git a/homeassistant/components/mqtt/translations/nl.json b/homeassistant/components/mqtt/translations/nl.json index 712de14d330e1..b56ef2413d77e 100644 --- a/homeassistant/components/mqtt/translations/nl.json +++ b/homeassistant/components/mqtt/translations/nl.json @@ -21,7 +21,7 @@ "data": { "discovery": "Detectie inschakelen" }, - "description": "Wilt u Home Assistant configureren om verbinding te maken met de MQTT-broker die wordt aangeboden door de add-on {addon} ?", + "description": "Wilt u Home Assistant configureren om verbinding te maken met de MQTT-broker die wordt aangeboden door de add-on {addon}?", "title": "MQTT Broker via Home Assistant add-on" } } diff --git a/homeassistant/components/roomba/translations/fr.json b/homeassistant/components/roomba/translations/fr.json index b4bc615e4e3d7..1f0e0b029c0c4 100644 --- a/homeassistant/components/roomba/translations/fr.json +++ b/homeassistant/components/roomba/translations/fr.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "L'appareil est d\u00e9ja configur\u00e9 ", "cannot_connect": "Echec de connection", - "not_irobot_device": "L'appareil d\u00e9couvert n'est pas un appareil iRobot" + "not_irobot_device": "L'appareil d\u00e9couvert n'est pas un appareil iRobot", + "short_blid": "La BLID a \u00e9t\u00e9 tronqu\u00e9" }, "error": { "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer" diff --git a/homeassistant/components/screenlogic/translations/fr.json b/homeassistant/components/screenlogic/translations/fr.json index 968045e059797..efd9740ac3127 100644 --- a/homeassistant/components/screenlogic/translations/fr.json +++ b/homeassistant/components/screenlogic/translations/fr.json @@ -18,7 +18,7 @@ }, "gateway_select": { "data": { - "selected_gateway": "passerelle" + "selected_gateway": "Passerelle" }, "description": "Les passerelles ScreenLogic suivantes ont \u00e9t\u00e9 d\u00e9couvertes. S\u2019il vous pla\u00eet s\u00e9lectionner un \u00e0 configurer, ou choisissez de configurer manuellement une passerelle ScreenLogic.", "title": "ScreenLogic" From 9ba66fe2326f6fdddfe28111c9abed08cc46ea3a Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 5 Apr 2021 05:25:57 +0200 Subject: [PATCH 0060/1317] Add more device triggers to deCONZ integration (#48680) --- .../components/deconz/device_trigger.py | 180 ++++++++++++++++++ homeassistant/components/deconz/strings.json | 4 + 2 files changed, 184 insertions(+) diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index e8e43d384b14c..2703adbc13952 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -64,6 +64,10 @@ CONF_BUTTON_2 = "button_2" CONF_BUTTON_3 = "button_3" CONF_BUTTON_4 = "button_4" +CONF_BUTTON_5 = "button_5" +CONF_BUTTON_6 = "button_6" +CONF_BUTTON_7 = "button_7" +CONF_BUTTON_8 = "button_8" CONF_SIDE_1 = "side_1" CONF_SIDE_2 = "side_2" CONF_SIDE_3 = "side_3" @@ -138,6 +142,22 @@ (CONF_LONG_RELEASE, CONF_BOTTOM_BUTTONS): {CONF_EVENT: 6003}, } +STYRBAR_REMOTE_MODEL = "Remote Control N2" +STYRBAR_REMOTE = { + (CONF_SHORT_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1002}, + (CONF_LONG_PRESS, CONF_TURN_ON): {CONF_EVENT: 1001}, + (CONF_LONG_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1003}, + (CONF_SHORT_RELEASE, CONF_DIM_UP): {CONF_EVENT: 2002}, + (CONF_LONG_PRESS, CONF_DIM_UP): {CONF_EVENT: 2001}, + (CONF_LONG_RELEASE, CONF_DIM_UP): {CONF_EVENT: 2003}, + (CONF_SHORT_RELEASE, CONF_DIM_DOWN): {CONF_EVENT: 3002}, + (CONF_LONG_PRESS, CONF_DIM_DOWN): {CONF_EVENT: 3001}, + (CONF_LONG_RELEASE, CONF_DIM_DOWN): {CONF_EVENT: 3003}, + (CONF_SHORT_RELEASE, CONF_TURN_OFF): {CONF_EVENT: 4002}, + (CONF_LONG_PRESS, CONF_TURN_OFF): {CONF_EVENT: 4001}, + (CONF_LONG_RELEASE, CONF_TURN_OFF): {CONF_EVENT: 4003}, +} + SYMFONISK_SOUND_CONTROLLER_MODEL = "SYMFONISK Sound Controller" SYMFONISK_SOUND_CONTROLLER = { (CONF_SHORT_PRESS, CONF_TURN_ON): {CONF_EVENT: 1002}, @@ -270,6 +290,21 @@ (CONF_SHORT_PRESS, CONF_BOTH_BUTTONS): {CONF_EVENT: 3002}, } +AQARA_DOUBLE_WALL_SWITCH_QBKG12LM_MODEL = "lumi.ctrl_ln2.aq1" +AQARA_DOUBLE_WALL_SWITCH_QBKG12LM = { + (CONF_SHORT_PRESS, CONF_LEFT): {CONF_EVENT: 1002}, + (CONF_DOUBLE_PRESS, CONF_LEFT): {CONF_EVENT: 1004}, + (CONF_SHORT_PRESS, CONF_RIGHT): {CONF_EVENT: 2002}, + (CONF_DOUBLE_PRESS, CONF_RIGHT): {CONF_EVENT: 2004}, + (CONF_SHORT_PRESS, CONF_BOTH_BUTTONS): {CONF_EVENT: 3002}, +} + +AQARA_SINGLE_WALL_SWITCH_QBKG11LM_MODEL = "lumi.ctrl_ln1.aq1" +AQARA_SINGLE_WALL_SWITCH_QBKG11LM = { + (CONF_SHORT_PRESS, CONF_TURN_ON): {CONF_EVENT: 1002}, + (CONF_DOUBLE_PRESS, CONF_TURN_ON): {CONF_EVENT: 1004}, +} + AQARA_SINGLE_WALL_SWITCH_WXKG03LM_MODEL = "lumi.remote.b186acn01" AQARA_SINGLE_WALL_SWITCH_WXKG06LM_MODEL = "lumi.remote.b186acn02" AQARA_SINGLE_WALL_SWITCH = { @@ -286,6 +321,7 @@ (CONF_LONG_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1003}, } + AQARA_ROUND_SWITCH_MODEL = "lumi.sensor_switch" AQARA_ROUND_SWITCH = { (CONF_SHORT_PRESS, CONF_TURN_ON): {CONF_EVENT: 1000}, @@ -359,6 +395,133 @@ (CONF_TRIPLE_PRESS, CONF_RIGHT): {CONF_EVENT: 6005}, } +DRESDEN_ELEKTRONIK_LIGHTING_SWITCH_MODEL = "Lighting Switch" +DRESDEN_ELEKTRONIK_LIGHTING_SWITCH = { + (CONF_SHORT_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1002}, + (CONF_LONG_PRESS, CONF_TURN_ON): {CONF_EVENT: 1001}, + (CONF_LONG_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1003}, + (CONF_SHORT_RELEASE, CONF_DIM_UP): {CONF_EVENT: 2002}, + (CONF_LONG_PRESS, CONF_DIM_UP): {CONF_EVENT: 2001}, + (CONF_LONG_RELEASE, CONF_DIM_UP): {CONF_EVENT: 2003}, + (CONF_SHORT_RELEASE, CONF_DIM_DOWN): {CONF_EVENT: 3002}, + (CONF_LONG_PRESS, CONF_DIM_DOWN): {CONF_EVENT: 3001}, + (CONF_LONG_RELEASE, CONF_DIM_DOWN): {CONF_EVENT: 3003}, + (CONF_SHORT_RELEASE, CONF_TURN_OFF): {CONF_EVENT: 4002}, + (CONF_LONG_PRESS, CONF_TURN_OFF): {CONF_EVENT: 4001}, + (CONF_LONG_RELEASE, CONF_TURN_OFF): {CONF_EVENT: 4003}, +} + +DRESDEN_ELEKTRONIK_SCENE_SWITCH_MODEL = "Scene Switch" +DRESDEN_ELEKTRONIK_SCENE_SWITCH = { + (CONF_SHORT_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1002}, + (CONF_LONG_PRESS, CONF_TURN_ON): {CONF_EVENT: 1001}, + (CONF_LONG_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1003}, + (CONF_SHORT_RELEASE, CONF_DIM_UP): {CONF_EVENT: 2002}, + (CONF_LONG_PRESS, CONF_DIM_UP): {CONF_EVENT: 2001}, + (CONF_LONG_RELEASE, CONF_DIM_UP): {CONF_EVENT: 2003}, + (CONF_SHORT_RELEASE, CONF_BUTTON_1): {CONF_EVENT: 3002}, + (CONF_SHORT_RELEASE, CONF_BUTTON_2): {CONF_EVENT: 4002}, + (CONF_SHORT_RELEASE, CONF_BUTTON_3): {CONF_EVENT: 5002}, + (CONF_SHORT_RELEASE, CONF_BUTTON_4): {CONF_EVENT: 6002}, +} + +GIRA_JUNG_SWITCH_MODEL = "HS_4f_GJ_1" +GIRA_SWITCH_MODEL = "WS_4f_J_1" +JUNG_SWITCH_MODEL = "WS_3f_G_1" +GIRA_JUNG_SWITCH = { + (CONF_SHORT_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1002}, + (CONF_LONG_PRESS, CONF_TURN_ON): {CONF_EVENT: 1001}, + (CONF_LONG_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1003}, + (CONF_SHORT_RELEASE, CONF_DIM_UP): {CONF_EVENT: 2002}, + (CONF_LONG_PRESS, CONF_DIM_UP): {CONF_EVENT: 2001}, + (CONF_LONG_RELEASE, CONF_DIM_UP): {CONF_EVENT: 2003}, + (CONF_SHORT_RELEASE, CONF_BUTTON_3): {CONF_EVENT: 3002}, + (CONF_SHORT_RELEASE, CONF_BUTTON_4): {CONF_EVENT: 4002}, + (CONF_SHORT_RELEASE, CONF_BUTTON_5): {CONF_EVENT: 5002}, + (CONF_SHORT_RELEASE, CONF_BUTTON_6): {CONF_EVENT: 6002}, + (CONF_SHORT_RELEASE, CONF_BUTTON_7): {CONF_EVENT: 7002}, + (CONF_SHORT_RELEASE, CONF_BUTTON_8): {CONF_EVENT: 8002}, +} + +LIGHTIFIY_FOUR_BUTTON_REMOTE_MODEL = "Switch-LIGHTIFY" +LIGHTIFIY_FOUR_BUTTON_REMOTE_4X_MODEL = "Switch 4x-LIGHTIFY" +LIGHTIFIY_FOUR_BUTTON_REMOTE_4X_EU_MODEL = "Switch 4x EU-LIGHTIFY" +LIGHTIFIY_FOUR_BUTTON_REMOTE = { + (CONF_SHORT_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1002}, + (CONF_LONG_PRESS, CONF_TURN_ON): {CONF_EVENT: 1001}, + (CONF_LONG_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1003}, + (CONF_SHORT_RELEASE, CONF_DIM_UP): {CONF_EVENT: 2002}, + (CONF_LONG_PRESS, CONF_DIM_UP): {CONF_EVENT: 2001}, + (CONF_LONG_RELEASE, CONF_DIM_UP): {CONF_EVENT: 2003}, + (CONF_SHORT_RELEASE, CONF_DIM_DOWN): {CONF_EVENT: 3002}, + (CONF_LONG_PRESS, CONF_DIM_DOWN): {CONF_EVENT: 3001}, + (CONF_LONG_RELEASE, CONF_DIM_DOWN): {CONF_EVENT: 3003}, + (CONF_SHORT_RELEASE, CONF_TURN_OFF): {CONF_EVENT: 4002}, + (CONF_LONG_PRESS, CONF_TURN_OFF): {CONF_EVENT: 4001}, + (CONF_LONG_RELEASE, CONF_TURN_OFF): {CONF_EVENT: 4003}, +} + +BUSCH_JAEGER_REMOTE_1_MODEL = "RB01" +BUSCH_JAEGER_REMOTE_2_MODEL = "RM01" +BUSCH_JAEGER_REMOTE = { + (CONF_SHORT_RELEASE, CONF_BUTTON_1): {CONF_EVENT: 1002}, + (CONF_LONG_PRESS, CONF_BUTTON_1): {CONF_EVENT: 1001}, + (CONF_LONG_RELEASE, CONF_BUTTON_1): {CONF_EVENT: 1003}, + (CONF_SHORT_RELEASE, CONF_BUTTON_2): {CONF_EVENT: 2002}, + (CONF_LONG_PRESS, CONF_BUTTON_2): {CONF_EVENT: 2001}, + (CONF_LONG_RELEASE, CONF_BUTTON_2): {CONF_EVENT: 2003}, + (CONF_SHORT_RELEASE, CONF_BUTTON_3): {CONF_EVENT: 3002}, + (CONF_LONG_PRESS, CONF_BUTTON_3): {CONF_EVENT: 3001}, + (CONF_LONG_RELEASE, CONF_BUTTON_3): {CONF_EVENT: 3003}, + (CONF_SHORT_RELEASE, CONF_BUTTON_4): {CONF_EVENT: 4002}, + (CONF_LONG_PRESS, CONF_BUTTON_4): {CONF_EVENT: 4001}, + (CONF_LONG_RELEASE, CONF_BUTTON_4): {CONF_EVENT: 4003}, + (CONF_SHORT_RELEASE, CONF_BUTTON_5): {CONF_EVENT: 5002}, + (CONF_LONG_PRESS, CONF_BUTTON_5): {CONF_EVENT: 5001}, + (CONF_LONG_RELEASE, CONF_BUTTON_5): {CONF_EVENT: 5003}, + (CONF_SHORT_RELEASE, CONF_BUTTON_6): {CONF_EVENT: 6002}, + (CONF_LONG_PRESS, CONF_BUTTON_6): {CONF_EVENT: 6001}, + (CONF_LONG_RELEASE, CONF_BUTTON_6): {CONF_EVENT: 6003}, + (CONF_SHORT_RELEASE, CONF_BUTTON_7): {CONF_EVENT: 7002}, + (CONF_LONG_PRESS, CONF_BUTTON_7): {CONF_EVENT: 7001}, + (CONF_LONG_RELEASE, CONF_BUTTON_7): {CONF_EVENT: 7003}, + (CONF_SHORT_RELEASE, CONF_BUTTON_8): {CONF_EVENT: 8002}, + (CONF_LONG_PRESS, CONF_BUTTON_8): {CONF_EVENT: 8001}, + (CONF_LONG_RELEASE, CONF_BUTTON_8): {CONF_EVENT: 8003}, +} + +TRUST_ZYCT_202_MODEL = "ZYCT-202" +TRUST_ZYCT_202_ZLL_MODEL = "ZLL-NonColorController" +TRUST_ZYCT_202 = { + (CONF_SHORT_RELEASE, CONF_BUTTON_1): {CONF_EVENT: 1002}, + (CONF_LONG_PRESS, CONF_BUTTON_2): {CONF_EVENT: 2001}, + (CONF_LONG_RELEASE, CONF_BUTTON_2): {CONF_EVENT: 2003}, + (CONF_LONG_PRESS, CONF_BUTTON_3): {CONF_EVENT: 3001}, + (CONF_LONG_RELEASE, CONF_BUTTON_3): {CONF_EVENT: 3003}, + (CONF_SHORT_RELEASE, CONF_BUTTON_4): {CONF_EVENT: 4002}, +} + +UBISYS_POWER_SWITCH_S2_MODEL = "S2" +UBISYS_POWER_SWITCH_S2 = { + (CONF_SHORT_RELEASE, CONF_BUTTON_1): {CONF_EVENT: 1002}, + (CONF_LONG_PRESS, CONF_BUTTON_1): {CONF_EVENT: 1001}, + (CONF_LONG_RELEASE, CONF_BUTTON_1): {CONF_EVENT: 1003}, + (CONF_SHORT_RELEASE, CONF_BUTTON_2): {CONF_EVENT: 2002}, + (CONF_LONG_PRESS, CONF_BUTTON_2): {CONF_EVENT: 2001}, + (CONF_LONG_RELEASE, CONF_BUTTON_2): {CONF_EVENT: 2003}, +} + +UBISYS_CONTROL_UNIT_C4_MODEL = "C4" +UBISYS_CONTROL_UNIT_C4 = { + **UBISYS_POWER_SWITCH_S2, + (CONF_SHORT_RELEASE, CONF_BUTTON_3): {CONF_EVENT: 3002}, + (CONF_LONG_PRESS, CONF_BUTTON_3): {CONF_EVENT: 3001}, + (CONF_LONG_RELEASE, CONF_BUTTON_3): {CONF_EVENT: 3003}, + (CONF_SHORT_RELEASE, CONF_BUTTON_4): {CONF_EVENT: 4002}, + (CONF_LONG_PRESS, CONF_BUTTON_4): {CONF_EVENT: 4001}, + (CONF_LONG_RELEASE, CONF_BUTTON_4): {CONF_EVENT: 4003}, +} + REMOTES = { HUE_DIMMER_REMOTE_MODEL_GEN1: HUE_DIMMER_REMOTE, HUE_DIMMER_REMOTE_MODEL_GEN2: HUE_DIMMER_REMOTE, @@ -366,6 +529,7 @@ HUE_BUTTON_REMOTE_MODEL: HUE_BUTTON_REMOTE, HUE_TAP_REMOTE_MODEL: HUE_TAP_REMOTE, FRIENDS_OF_HUE_SWITCH_MODEL: FRIENDS_OF_HUE_SWITCH, + STYRBAR_REMOTE_MODEL: STYRBAR_REMOTE, SYMFONISK_SOUND_CONTROLLER_MODEL: SYMFONISK_SOUND_CONTROLLER, TRADFRI_ON_OFF_SWITCH_MODEL: TRADFRI_ON_OFF_SWITCH, TRADFRI_OPEN_CLOSE_REMOTE_MODEL: TRADFRI_OPEN_CLOSE_REMOTE, @@ -376,6 +540,8 @@ AQARA_DOUBLE_WALL_SWITCH_MODEL: AQARA_DOUBLE_WALL_SWITCH, AQARA_DOUBLE_WALL_SWITCH_MODEL_2020: AQARA_DOUBLE_WALL_SWITCH, AQARA_DOUBLE_WALL_SWITCH_WXKG02LM_MODEL: AQARA_DOUBLE_WALL_SWITCH_WXKG02LM, + AQARA_DOUBLE_WALL_SWITCH_QBKG12LM_MODEL: AQARA_DOUBLE_WALL_SWITCH_QBKG12LM, + AQARA_SINGLE_WALL_SWITCH_QBKG11LM_MODEL: AQARA_SINGLE_WALL_SWITCH_QBKG11LM, AQARA_SINGLE_WALL_SWITCH_WXKG03LM_MODEL: AQARA_SINGLE_WALL_SWITCH, AQARA_SINGLE_WALL_SWITCH_WXKG06LM_MODEL: AQARA_SINGLE_WALL_SWITCH, AQARA_MINI_SWITCH_MODEL: AQARA_MINI_SWITCH, @@ -385,6 +551,20 @@ AQARA_OPPLE_2_BUTTONS_MODEL: AQARA_OPPLE_2_BUTTONS, AQARA_OPPLE_4_BUTTONS_MODEL: AQARA_OPPLE_4_BUTTONS, AQARA_OPPLE_6_BUTTONS_MODEL: AQARA_OPPLE_6_BUTTONS, + DRESDEN_ELEKTRONIK_LIGHTING_SWITCH_MODEL: DRESDEN_ELEKTRONIK_LIGHTING_SWITCH, + DRESDEN_ELEKTRONIK_SCENE_SWITCH_MODEL: DRESDEN_ELEKTRONIK_SCENE_SWITCH, + GIRA_JUNG_SWITCH_MODEL: GIRA_JUNG_SWITCH_MODEL, + GIRA_SWITCH_MODEL: GIRA_JUNG_SWITCH_MODEL, + JUNG_SWITCH_MODEL: GIRA_JUNG_SWITCH_MODEL, + LIGHTIFIY_FOUR_BUTTON_REMOTE_MODEL: LIGHTIFIY_FOUR_BUTTON_REMOTE, + LIGHTIFIY_FOUR_BUTTON_REMOTE_4X_MODEL: LIGHTIFIY_FOUR_BUTTON_REMOTE, + LIGHTIFIY_FOUR_BUTTON_REMOTE_4X_EU_MODEL: LIGHTIFIY_FOUR_BUTTON_REMOTE, + BUSCH_JAEGER_REMOTE_1_MODEL: BUSCH_JAEGER_REMOTE, + BUSCH_JAEGER_REMOTE_2_MODEL: BUSCH_JAEGER_REMOTE, + TRUST_ZYCT_202_MODEL: TRUST_ZYCT_202, + TRUST_ZYCT_202_ZLL_MODEL: TRUST_ZYCT_202, + UBISYS_POWER_SWITCH_S2_MODEL: UBISYS_POWER_SWITCH_S2, + UBISYS_CONTROL_UNIT_C4_MODEL: UBISYS_CONTROL_UNIT_C4, } TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json index 258de620a549e..fbb321959c16c 100644 --- a/homeassistant/components/deconz/strings.json +++ b/homeassistant/components/deconz/strings.json @@ -94,6 +94,10 @@ "button_2": "Second button", "button_3": "Third button", "button_4": "Fourth button", + "button_5": "Fifth button", + "button_6": "Sixth button", + "button_7": "Seventh button", + "button_8": "Eighth button", "side_1": "Side 1", "side_2": "Side 2", "side_3": "Side 3", From 30382c3dbe60387083b9215159d6ba704e744c40 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Apr 2021 17:26:18 -1000 Subject: [PATCH 0061/1317] Limit log spam from rest and include reason in platform retry (#48666) - Each retry was logging the error again - Now we set the cause of the PlatformNotReady to allow Home Assistant to log as needed --- homeassistant/components/rest/binary_sensor.py | 4 +++- homeassistant/components/rest/data.py | 9 +++++++-- homeassistant/components/rest/sensor.py | 4 +++- tests/components/rest/test_binary_sensor.py | 10 +++++++--- tests/components/rest/test_sensor.py | 9 ++++++--- 5 files changed, 26 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/rest/binary_sensor.py b/homeassistant/components/rest/binary_sensor.py index 9692f5b9339a8..a90c5bd7c7706 100644 --- a/homeassistant/components/rest/binary_sensor.py +++ b/homeassistant/components/rest/binary_sensor.py @@ -40,9 +40,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= conf = config coordinator = None rest = create_rest_data_from_config(hass, conf) - await rest.async_update() + await rest.async_update(log_errors=False) if rest.data is None: + if rest.last_exception: + raise PlatformNotReady from rest.last_exception raise PlatformNotReady name = conf.get(CONF_NAME) diff --git a/homeassistant/components/rest/data.py b/homeassistant/components/rest/data.py index dd2e29616c768..8b03bcfb12876 100644 --- a/homeassistant/components/rest/data.py +++ b/homeassistant/components/rest/data.py @@ -37,13 +37,14 @@ def __init__( self._verify_ssl = verify_ssl self._async_client = None self.data = None + self.last_exception = None self.headers = None def set_url(self, url): """Set url.""" self._resource = url - async def async_update(self): + async def async_update(self, log_errors=True): """Get the latest data from REST service with provided method.""" if not self._async_client: self._async_client = get_async_client( @@ -64,6 +65,10 @@ async def async_update(self): self.data = response.text self.headers = response.headers except httpx.RequestError as ex: - _LOGGER.error("Error fetching data: %s failed with %s", self._resource, ex) + if log_errors: + _LOGGER.error( + "Error fetching data: %s failed with %s", self._resource, ex + ) + self.last_exception = ex self.data = None self.headers = None diff --git a/homeassistant/components/rest/sensor.py b/homeassistant/components/rest/sensor.py index d303f7a57b30f..7727b5f09ab46 100644 --- a/homeassistant/components/rest/sensor.py +++ b/homeassistant/components/rest/sensor.py @@ -50,9 +50,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= conf = config coordinator = None rest = create_rest_data_from_config(hass, conf) - await rest.async_update() + await rest.async_update(log_errors=False) if rest.data is None: + if rest.last_exception: + raise PlatformNotReady from rest.last_exception raise PlatformNotReady name = conf.get(CONF_NAME) diff --git a/tests/components/rest/test_binary_sensor.py b/tests/components/rest/test_binary_sensor.py index 9adb04ea40c2e..f6445c250224c 100644 --- a/tests/components/rest/test_binary_sensor.py +++ b/tests/components/rest/test_binary_sensor.py @@ -2,7 +2,7 @@ import asyncio from os import path -from unittest.mock import patch +from unittest.mock import MagicMock, patch import httpx import respx @@ -47,9 +47,12 @@ async def test_setup_missing_config(hass): @respx.mock -async def test_setup_failed_connect(hass): +async def test_setup_failed_connect(hass, caplog): """Test setup when connection error occurs.""" - respx.get("http://localhost").mock(side_effect=httpx.RequestError) + + respx.get("http://localhost").mock( + side_effect=httpx.RequestError("server offline", request=MagicMock()) + ) assert await async_setup_component( hass, binary_sensor.DOMAIN, @@ -63,6 +66,7 @@ async def test_setup_failed_connect(hass): ) await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 + assert "server offline" in caplog.text @respx.mock diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py index 2e308f69384d0..50b959be36b4b 100644 --- a/tests/components/rest/test_sensor.py +++ b/tests/components/rest/test_sensor.py @@ -1,7 +1,7 @@ """The tests for the REST sensor platform.""" import asyncio from os import path -from unittest.mock import patch +from unittest.mock import MagicMock, patch import httpx import respx @@ -41,9 +41,11 @@ async def test_setup_missing_schema(hass): @respx.mock -async def test_setup_failed_connect(hass): +async def test_setup_failed_connect(hass, caplog): """Test setup when connection error occurs.""" - respx.get("http://localhost").mock(side_effect=httpx.RequestError) + respx.get("http://localhost").mock( + side_effect=httpx.RequestError("server offline", request=MagicMock()) + ) assert await async_setup_component( hass, sensor.DOMAIN, @@ -57,6 +59,7 @@ async def test_setup_failed_connect(hass): ) await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 + assert "server offline" in caplog.text @respx.mock From 6dc1414b69db0782627909ceb8623f03ffb3a132 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Apr 2021 17:26:55 -1000 Subject: [PATCH 0062/1317] Fix sonos volume always showing 0 (#48685) --- homeassistant/components/sonos/media_player.py | 4 ++-- tests/components/sonos/conftest.py | 4 ++++ tests/components/sonos/test_media_player.py | 15 +++++++++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 6d594b906eac5..6e0fe6c7293b4 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -885,9 +885,9 @@ def async_update_content(self, event: Event | None = None) -> None: self.async_write_ha_state() @property - def volume_level(self) -> int | None: + def volume_level(self) -> float | None: """Volume level of the media player (0..1).""" - return self._player_volume and int(self._player_volume / 100) + return self._player_volume and self._player_volume / 100 @property def is_volume_muted(self) -> bool | None: diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index 7b6393559dc77..3562d991e985b 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -31,6 +31,10 @@ def soco_fixture(music_library, speaker_info, dummy_soco_service): mock_soco.renderingControl = dummy_soco_service mock_soco.zoneGroupTopology = dummy_soco_service mock_soco.contentDirectory = dummy_soco_service + mock_soco.mute = False + mock_soco.night_mode = True + mock_soco.dialog_mode = True + mock_soco.volume = 19 yield mock_soco diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index 466a0df5905e2..d5b0158d6c446 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -2,6 +2,7 @@ import pytest from homeassistant.components.sonos import DOMAIN, media_player +from homeassistant.const import STATE_IDLE from homeassistant.core import Context from homeassistant.exceptions import Unauthorized from homeassistant.helpers import device_registry as dr @@ -59,3 +60,17 @@ async def test_device_registry(hass, config_entry, config, soco): assert reg_device.manufacturer == "Sonos" assert reg_device.suggested_area == "Zone A" assert reg_device.name == "Zone A" + + +async def test_entity_basic(hass, config_entry, discover): + """Test basic state and attributes.""" + await setup_platform(hass, config_entry, {}) + + state = hass.states.get("media_player.zone_a") + assert state.state == STATE_IDLE + attributes = state.attributes + assert attributes["friendly_name"] == "Zone A" + assert attributes["is_volume_muted"] is False + assert attributes["night_sound"] is True + assert attributes["speech_enhance"] is True + assert attributes["volume_level"] == 0.19 From 6204765835ca81f4812c76ea704d68335982e242 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Mon, 5 Apr 2021 00:21:47 -0400 Subject: [PATCH 0063/1317] Implement Ignore list for poll control configuration on Ikea devices (#48667) Co-authored-by: Hmmbob <33529490+hmmbob@users.noreply.github.com> --- .../components/zha/core/channels/general.py | 11 ++++++- tests/components/zha/test_channels.py | 32 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py index 626596e1a3e6b..6ef0bd9e66515 100644 --- a/homeassistant/components/zha/core/channels/general.py +++ b/homeassistant/components/zha/core/channels/general.py @@ -391,6 +391,9 @@ class PollControl(ZigbeeChannel): CHECKIN_INTERVAL = 55 * 60 * 4 # 55min CHECKIN_FAST_POLL_TIMEOUT = 2 * 4 # 2s LONG_POLL = 6 * 4 # 6s + _IGNORED_MANUFACTURER_ID = { + 4476, + } # IKEA async def async_configure_channel_specific(self) -> None: """Configure channel: set check-in interval.""" @@ -416,7 +419,13 @@ def cluster_command( async def check_in_response(self, tsn: int) -> None: """Respond to checkin command.""" await self.checkin_response(True, self.CHECKIN_FAST_POLL_TIMEOUT, tsn=tsn) - await self.set_long_poll_interval(self.LONG_POLL) + if self._ch_pool.manufacturer_code not in self._IGNORED_MANUFACTURER_ID: + await self.set_long_poll_interval(self.LONG_POLL) + + @callback + def skip_manufacturer_id(self, manufacturer_code: int) -> None: + """Block a specific manufacturer id from changing default polling.""" + self._IGNORED_MANUFACTURER_ID.add(manufacturer_code) @registries.ZIGBEE_CHANNEL_REGISTRY.register(general.PowerConfiguration.cluster_id) diff --git a/tests/components/zha/test_channels.py b/tests/components/zha/test_channels.py index ec5128fdb5e11..a391439a23973 100644 --- a/tests/components/zha/test_channels.py +++ b/tests/components/zha/test_channels.py @@ -492,6 +492,38 @@ async def test_poll_control_cluster_command(hass, poll_control_device): assert data["device_id"] == poll_control_device.device_id +async def test_poll_control_ignore_list(hass, poll_control_device): + """Test poll control channel ignore list.""" + set_long_poll_mock = AsyncMock() + poll_control_ch = poll_control_device.channels.pools[0].all_channels["1:0x0020"] + cluster = poll_control_ch.cluster + + with mock.patch.object(cluster, "set_long_poll_interval", set_long_poll_mock): + await poll_control_ch.check_in_response(33) + + assert set_long_poll_mock.call_count == 1 + + set_long_poll_mock.reset_mock() + poll_control_ch.skip_manufacturer_id(4151) + with mock.patch.object(cluster, "set_long_poll_interval", set_long_poll_mock): + await poll_control_ch.check_in_response(33) + + assert set_long_poll_mock.call_count == 0 + + +async def test_poll_control_ikea(hass, poll_control_device): + """Test poll control channel ignore list for ikea.""" + set_long_poll_mock = AsyncMock() + poll_control_ch = poll_control_device.channels.pools[0].all_channels["1:0x0020"] + cluster = poll_control_ch.cluster + + poll_control_device.device.node_desc.manufacturer_code = 4476 + with mock.patch.object(cluster, "set_long_poll_interval", set_long_poll_mock): + await poll_control_ch.check_in_response(33) + + assert set_long_poll_mock.call_count == 0 + + @pytest.fixture def zigpy_zll_device(zigpy_device_mock): """ZLL device fixture.""" From 94fde73addc1aee8d34a49ddce2958e5c97bd2a4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Apr 2021 22:11:06 -1000 Subject: [PATCH 0064/1317] Add config flow for enphase envoy (#48517) Co-authored-by: Paulus Schoutsen --- .coveragerc | 1 + .../components/enphase_envoy/__init__.py | 101 +++++- .../components/enphase_envoy/config_flow.py | 162 ++++++++++ .../components/enphase_envoy/const.py | 30 ++ .../components/enphase_envoy/manifest.json | 10 +- .../components/enphase_envoy/sensor.py | 149 ++++----- .../components/enphase_envoy/strings.json | 22 ++ .../enphase_envoy/translations/en.json | 22 ++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/zeroconf.py | 5 + requirements_test_all.txt | 3 + tests/components/enphase_envoy/__init__.py | 1 + .../enphase_envoy/test_config_flow.py | 304 ++++++++++++++++++ 13 files changed, 719 insertions(+), 92 deletions(-) create mode 100644 homeassistant/components/enphase_envoy/config_flow.py create mode 100644 homeassistant/components/enphase_envoy/const.py create mode 100644 homeassistant/components/enphase_envoy/strings.json create mode 100644 homeassistant/components/enphase_envoy/translations/en.json create mode 100644 tests/components/enphase_envoy/__init__.py create mode 100644 tests/components/enphase_envoy/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index b7458cdff1da1..519e3a80fd917 100644 --- a/.coveragerc +++ b/.coveragerc @@ -247,6 +247,7 @@ omit = homeassistant/components/enocean/light.py homeassistant/components/enocean/sensor.py homeassistant/components/enocean/switch.py + homeassistant/components/enphase_envoy/__init__.py homeassistant/components/enphase_envoy/sensor.py homeassistant/components/entur_public_transport/* homeassistant/components/environment_canada/* diff --git a/homeassistant/components/enphase_envoy/__init__.py b/homeassistant/components/enphase_envoy/__init__.py index c4101fbcdf23f..1b8d09b1f1d81 100644 --- a/homeassistant/components/enphase_envoy/__init__.py +++ b/homeassistant/components/enphase_envoy/__init__.py @@ -1 +1,100 @@ -"""The enphase_envoy component.""" +"""The Enphase Envoy integration.""" +from __future__ import annotations + +import asyncio +from datetime import timedelta +import logging + +import async_timeout +from envoy_reader.envoy_reader import EnvoyReader +import httpx + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import COORDINATOR, DOMAIN, NAME, PLATFORMS, SENSORS + +SCAN_INTERVAL = timedelta(seconds=60) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Enphase Envoy from a config entry.""" + + config = entry.data + name = config[CONF_NAME] + envoy_reader = EnvoyReader( + config[CONF_HOST], config[CONF_USERNAME], config[CONF_PASSWORD] + ) + + try: + await envoy_reader.getData() + except httpx.HTTPStatusError as err: + _LOGGER.error("Authentication failure during setup: %s", err) + return + except (AttributeError, httpx.HTTPError) as err: + raise ConfigEntryNotReady from err + + async def async_update_data(): + """Fetch data from API endpoint.""" + data = {} + async with async_timeout.timeout(30): + try: + await envoy_reader.getData() + except httpx.HTTPError as err: + raise UpdateFailed(f"Error communicating with API: {err}") from err + + for condition in SENSORS: + if condition != "inverters": + data[condition] = await getattr(envoy_reader, condition)() + else: + data[ + "inverters_production" + ] = await envoy_reader.inverters_production() + + _LOGGER.debug("Retrieved data from API: %s", data) + + return data + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name="envoy {name}", + update_method=async_update_data, + update_interval=SCAN_INTERVAL, + ) + + envoy_reader.get_inverters = True + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { + COORDINATOR: coordinator, + NAME: name, + } + + for platform in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, platform) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/enphase_envoy/config_flow.py b/homeassistant/components/enphase_envoy/config_flow.py new file mode 100644 index 0000000000000..41d72c09a31b7 --- /dev/null +++ b/homeassistant/components/enphase_envoy/config_flow.py @@ -0,0 +1,162 @@ +"""Config flow for Enphase Envoy integration.""" +from __future__ import annotations + +import logging +from typing import Any + +from envoy_reader.envoy_reader import EnvoyReader +import httpx +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import ( + CONF_HOST, + CONF_IP_ADDRESS, + CONF_NAME, + CONF_PASSWORD, + CONF_USERNAME, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +ENVOY = "Envoy" + +CONF_SERIAL = "serial" + + +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: + """Validate the user input allows us to connect.""" + envoy_reader = EnvoyReader( + data[CONF_HOST], data[CONF_USERNAME], data[CONF_PASSWORD], inverters=True + ) + + try: + await envoy_reader.getData() + except httpx.HTTPStatusError as err: + raise InvalidAuth from err + except (AttributeError, httpx.HTTPError) as err: + raise CannotConnect from err + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Enphase Envoy.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + def __init__(self): + """Initialize an envoy flow.""" + self.ip_address = None + self.name = None + self.username = None + self.serial = None + + @callback + def _async_generate_schema(self): + """Generate schema.""" + schema = {} + + if self.ip_address: + schema[vol.Required(CONF_HOST, default=self.ip_address)] = vol.In( + [self.ip_address] + ) + else: + schema[vol.Required(CONF_HOST)] = str + + schema[vol.Optional(CONF_USERNAME, default=self.username or "envoy")] = str + schema[vol.Optional(CONF_PASSWORD, default="")] = str + return vol.Schema(schema) + + async def async_step_import(self, import_config): + """Handle a flow import.""" + self.ip_address = import_config[CONF_IP_ADDRESS] + self.username = import_config[CONF_USERNAME] + self.name = import_config[CONF_NAME] + return await self.async_step_user( + { + CONF_HOST: import_config[CONF_IP_ADDRESS], + CONF_USERNAME: import_config[CONF_USERNAME], + CONF_PASSWORD: import_config[CONF_PASSWORD], + } + ) + + @callback + def _async_current_hosts(self): + """Return a set of hosts.""" + return { + entry.data[CONF_HOST] + for entry in self._async_current_entries(include_ignore=False) + if CONF_HOST in entry.data + } + + async def async_step_zeroconf(self, discovery_info): + """Handle a flow initialized by zeroconf discovery.""" + self.serial = discovery_info["properties"]["serialnum"] + await self.async_set_unique_id(self.serial) + self.ip_address = discovery_info[CONF_HOST] + self._abort_if_unique_id_configured({CONF_HOST: self.ip_address}) + for entry in self._async_current_entries(include_ignore=False): + if ( + entry.unique_id is None + and CONF_HOST in entry.data + and entry.data[CONF_HOST] == self.ip_address + ): + title = f"{ENVOY} {self.serial}" if entry.title == ENVOY else ENVOY + self.hass.config_entries.async_update_entry( + entry, title=title, unique_id=self.serial + ) + self.hass.async_create_task( + self.hass.config_entries.async_reload(entry.entry_id) + ) + return self.async_abort(reason="already_configured") + + return await self.async_step_user() + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> dict[str, Any]: + """Handle the initial step.""" + errors = {} + + if user_input is not None: + if user_input[CONF_HOST] in self._async_current_hosts(): + return self.async_abort(reason="already_configured") + try: + await validate_input(self.hass, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + data = user_input.copy() + if self.serial: + data[CONF_NAME] = f"{ENVOY} {self.serial}" + else: + data[CONF_NAME] = self.name or ENVOY + return self.async_create_entry(title=data[CONF_NAME], data=data) + + if self.serial: + self.context["title_placeholders"] = { + CONF_SERIAL: self.serial, + CONF_HOST: self.ip_address, + } + return self.async_show_form( + step_id="user", + data_schema=self._async_generate_schema(), + errors=errors, + ) + + +class CannotConnect(HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/enphase_envoy/const.py b/homeassistant/components/enphase_envoy/const.py new file mode 100644 index 0000000000000..89803d32351c9 --- /dev/null +++ b/homeassistant/components/enphase_envoy/const.py @@ -0,0 +1,30 @@ +"""The enphase_envoy component.""" + + +from homeassistant.const import ENERGY_WATT_HOUR, POWER_WATT + +DOMAIN = "enphase_envoy" + +PLATFORMS = ["sensor"] + + +COORDINATOR = "coordinator" +NAME = "name" + +SENSORS = { + "production": ("Current Energy Production", POWER_WATT), + "daily_production": ("Today's Energy Production", ENERGY_WATT_HOUR), + "seven_days_production": ( + "Last Seven Days Energy Production", + ENERGY_WATT_HOUR, + ), + "lifetime_production": ("Lifetime Energy Production", ENERGY_WATT_HOUR), + "consumption": ("Current Energy Consumption", POWER_WATT), + "daily_consumption": ("Today's Energy Consumption", ENERGY_WATT_HOUR), + "seven_days_consumption": ( + "Last Seven Days Energy Consumption", + ENERGY_WATT_HOUR, + ), + "lifetime_consumption": ("Lifetime Energy Consumption", ENERGY_WATT_HOUR), + "inverters": ("Inverter", POWER_WATT), +} diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index 9e9760560d5ab..236010607372b 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -2,8 +2,12 @@ "domain": "enphase_envoy", "name": "Enphase Envoy", "documentation": "https://www.home-assistant.io/integrations/enphase_envoy", - "requirements": ["envoy_reader==0.18.3"], + "requirements": [ + "envoy_reader==0.18.3" + ], "codeowners": [ "@gtdiehl" - ] -} + ], + "config_flow": true, + "zeroconf": [{ "type": "_enphase-envoy._tcp.local."}] +} \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/sensor.py b/homeassistant/components/enphase_envoy/sensor.py index dd1b10c870b8c..050a497f69e4f 100644 --- a/homeassistant/components/enphase_envoy/sensor.py +++ b/homeassistant/components/enphase_envoy/sensor.py @@ -1,55 +1,27 @@ """Support for Enphase Envoy solar energy monitor.""" -from datetime import timedelta import logging -import async_timeout -from envoy_reader.envoy_reader import EnvoyReader -import httpx import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( CONF_IP_ADDRESS, CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_PASSWORD, CONF_USERNAME, - ENERGY_WATT_HOUR, - POWER_WATT, ) -from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, - UpdateFailed, -) - -_LOGGER = logging.getLogger(__name__) +from homeassistant.helpers.update_coordinator import CoordinatorEntity -SENSORS = { - "production": ("Envoy Current Energy Production", POWER_WATT), - "daily_production": ("Envoy Today's Energy Production", ENERGY_WATT_HOUR), - "seven_days_production": ( - "Envoy Last Seven Days Energy Production", - ENERGY_WATT_HOUR, - ), - "lifetime_production": ("Envoy Lifetime Energy Production", ENERGY_WATT_HOUR), - "consumption": ("Envoy Current Energy Consumption", POWER_WATT), - "daily_consumption": ("Envoy Today's Energy Consumption", ENERGY_WATT_HOUR), - "seven_days_consumption": ( - "Envoy Last Seven Days Energy Consumption", - ENERGY_WATT_HOUR, - ), - "lifetime_consumption": ("Envoy Lifetime Energy Consumption", ENERGY_WATT_HOUR), - "inverters": ("Envoy Inverter", POWER_WATT), -} +from .const import COORDINATOR, DOMAIN, NAME, SENSORS ICON = "mdi:flash" CONST_DEFAULT_HOST = "envoy" +_LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(seconds=60) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -64,89 +36,59 @@ ) -async def async_setup_platform( - homeassistant, config, async_add_entities, discovery_info=None -): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Enphase Envoy sensor.""" - ip_address = config[CONF_IP_ADDRESS] - monitored_conditions = config[CONF_MONITORED_CONDITIONS] - name = config[CONF_NAME] - username = config[CONF_USERNAME] - password = config[CONF_PASSWORD] - - if "inverters" in monitored_conditions: - envoy_reader = EnvoyReader(ip_address, username, password, inverters=True) - else: - envoy_reader = EnvoyReader(ip_address, username, password) - - try: - await envoy_reader.getData() - except httpx.HTTPStatusError as err: - _LOGGER.error("Authentication failure during setup: %s", err) - return - except httpx.HTTPError as err: - raise PlatformNotReady from err - - async def async_update_data(): - """Fetch data from API endpoint.""" - data = {} - async with async_timeout.timeout(30): - try: - await envoy_reader.getData() - except httpx.HTTPError as err: - raise UpdateFailed(f"Error communicating with API: {err}") from err - - for condition in monitored_conditions: - if condition != "inverters": - data[condition] = await getattr(envoy_reader, condition)() - else: - data["inverters_production"] = await getattr( - envoy_reader, "inverters_production" - )() - - _LOGGER.debug("Retrieved data from API: %s", data) - - return data - - coordinator = DataUpdateCoordinator( - homeassistant, - _LOGGER, - name="sensor", - update_method=async_update_data, - update_interval=SCAN_INTERVAL, + _LOGGER.warning( + "Loading enphase_envoy via platform config is deprecated; The configuration" + " has been migrated to a config entry and can be safely removed" + ) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config + ) ) - await coordinator.async_refresh() - if coordinator.data is None: - raise PlatformNotReady +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up envoy sensor platform.""" + data = hass.data[DOMAIN][config_entry.entry_id] + coordinator = data[COORDINATOR] + name = data[NAME] entities = [] - for condition in monitored_conditions: + for condition in SENSORS: entity_name = "" if ( condition == "inverters" and coordinator.data.get("inverters_production") is not None ): for inverter in coordinator.data["inverters_production"]: - entity_name = f"{name}{SENSORS[condition][0]} {inverter}" + entity_name = f"{name} {SENSORS[condition][0]} {inverter}" split_name = entity_name.split(" ") serial_number = split_name[-1] entities.append( Envoy( condition, entity_name, + name, + config_entry.unique_id, serial_number, SENSORS[condition][1], coordinator, ) ) elif condition != "inverters": - entity_name = f"{name}{SENSORS[condition][0]}" + data = coordinator.data.get(condition) + if isinstance(data, str) and "not available" in data: + continue + + entity_name = f"{name} {SENSORS[condition][0]}" entities.append( Envoy( condition, entity_name, + name, + config_entry.unique_id, None, SENSORS[condition][1], coordinator, @@ -159,11 +101,22 @@ async def async_update_data(): class Envoy(CoordinatorEntity, SensorEntity): """Envoy entity.""" - def __init__(self, sensor_type, name, serial_number, unit, coordinator): + def __init__( + self, + sensor_type, + name, + device_name, + device_serial_number, + serial_number, + unit, + coordinator, + ): """Initialize Envoy entity.""" self._type = sensor_type self._name = name self._serial_number = serial_number + self._device_name = device_name + self._device_serial_number = device_serial_number self._unit_of_measurement = unit super().__init__(coordinator) @@ -173,6 +126,14 @@ def name(self): """Return the name of the sensor.""" return self._name + @property + def unique_id(self): + """Return the unique id of the sensor.""" + if self._serial_number: + return self._serial_number + if self._device_serial_number: + return f"{self._device_serial_number}_{self._type}" + @property def state(self): """Return the state of the sensor.""" @@ -214,3 +175,15 @@ def extra_state_attributes(self): return {"last_reported": value} return None + + @property + def device_info(self): + """Return the device_info of the device.""" + if not self._device_serial_number: + return None + return { + "identifiers": {(DOMAIN, str(self._device_serial_number))}, + "name": self._device_name, + "model": "Envoy", + "manufacturer": "Enphase", + } diff --git a/homeassistant/components/enphase_envoy/strings.json b/homeassistant/components/enphase_envoy/strings.json new file mode 100644 index 0000000000000..399358659d70e --- /dev/null +++ b/homeassistant/components/enphase_envoy/strings.json @@ -0,0 +1,22 @@ +{ + "config": { + "flow_title": "Envoy {serial} ({host})", + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/en.json b/homeassistant/components/enphase_envoy/translations/en.json new file mode 100644 index 0000000000000..7c138727cd72d --- /dev/null +++ b/homeassistant/components/enphase_envoy/translations/en.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "flow_title": "Envoy {serial} ({host})", + "step": { + "user": { + "data": { + "host": "Host", + "password": "Password", + "username": "Username" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index b88da6aa271c9..fd385b21ca089 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -62,6 +62,7 @@ "elkm1", "emulated_roku", "enocean", + "enphase_envoy", "epson", "esphome", "faa_delays", diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index a6af4d93fb82c..b3fa7064aee34 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -54,6 +54,11 @@ "domain": "elgato" } ], + "_enphase-envoy._tcp.local.": [ + { + "domain": "enphase_envoy" + } + ], "_esphomelib._tcp.local.": [ { "domain": "esphome" diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 91dc8ff600e74..3594e316958e9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -296,6 +296,9 @@ emulated_roku==0.2.1 # homeassistant.components.enocean enocean==0.50 +# homeassistant.components.enphase_envoy +envoy_reader==0.18.3 + # homeassistant.components.season ephem==3.7.7.0 diff --git a/tests/components/enphase_envoy/__init__.py b/tests/components/enphase_envoy/__init__.py new file mode 100644 index 0000000000000..6c6293ab76b4c --- /dev/null +++ b/tests/components/enphase_envoy/__init__.py @@ -0,0 +1 @@ +"""Tests for the Enphase Envoy integration.""" diff --git a/tests/components/enphase_envoy/test_config_flow.py b/tests/components/enphase_envoy/test_config_flow.py new file mode 100644 index 0000000000000..99efca883c8ce --- /dev/null +++ b/tests/components/enphase_envoy/test_config_flow.py @@ -0,0 +1,304 @@ +"""Test the Enphase Envoy config flow.""" +from unittest.mock import MagicMock, patch + +import httpx + +from homeassistant import config_entries, setup +from homeassistant.components.enphase_envoy.const import DOMAIN +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def test_form(hass: HomeAssistant) -> None: + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.getData", + return_value=True, + ), patch( + "homeassistant.components.enphase_envoy.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "Envoy" + assert result2["data"] == { + "host": "1.1.1.1", + "name": "Envoy", + "username": "test-username", + "password": "test-password", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_invalid_auth(hass: HomeAssistant) -> None: + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.getData", + side_effect=httpx.HTTPStatusError( + "any", request=MagicMock(), response=MagicMock() + ), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_cannot_connect(hass: HomeAssistant) -> None: + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.getData", + side_effect=httpx.HTTPError("any", request=MagicMock()), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_unknown_error(hass: HomeAssistant) -> None: + """Test we handle unknown error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.getData", + side_effect=ValueError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "unknown"} + + +async def test_import(hass: HomeAssistant) -> None: + """Test we can import from yaml.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + with patch( + "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.getData", + return_value=True, + ), patch( + "homeassistant.components.enphase_envoy.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "import"}, + data={ + "ip_address": "1.1.1.1", + "name": "Pool Envoy", + "username": "test-username", + "password": "test-password", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "Pool Envoy" + assert result2["data"] == { + "host": "1.1.1.1", + "name": "Pool Envoy", + "username": "test-username", + "password": "test-password", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_zeroconf(hass: HomeAssistant) -> None: + """Test we can setup from zeroconf.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "zeroconf"}, + data={ + "properties": {"serialnum": "1234"}, + "host": "1.1.1.1", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == "form" + assert result["step_id"] == "user" + + with patch( + "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.getData", + return_value=True, + ), patch( + "homeassistant.components.enphase_envoy.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "Envoy 1234" + assert result2["result"].unique_id == "1234" + assert result2["data"] == { + "host": "1.1.1.1", + "name": "Envoy 1234", + "username": "test-username", + "password": "test-password", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_host_already_exists(hass: HomeAssistant) -> None: + """Test host already exists.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + "host": "1.1.1.1", + "name": "Envoy", + "username": "test-username", + "password": "test-password", + }, + title="Envoy", + ) + config_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["errors"] == {} + + with patch( + "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.getData", + return_value=True, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "abort" + assert result2["reason"] == "already_configured" + + +async def test_zeroconf_serial_already_exists(hass: HomeAssistant) -> None: + """Test serial number already exists from zeroconf.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + "host": "1.1.1.1", + "name": "Envoy", + "username": "test-username", + "password": "test-password", + }, + unique_id="1234", + title="Envoy", + ) + config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "zeroconf"}, + data={ + "properties": {"serialnum": "1234"}, + "host": "1.1.1.1", + }, + ) + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + +async def test_zeroconf_host_already_exists(hass: HomeAssistant) -> None: + """Test hosts already exists from zeroconf.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + "host": "1.1.1.1", + "name": "Envoy", + "username": "test-username", + "password": "test-password", + }, + title="Envoy", + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.getData", + return_value=True, + ), patch( + "homeassistant.components.enphase_envoy.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "zeroconf"}, + data={ + "properties": {"serialnum": "1234"}, + "host": "1.1.1.1", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + assert config_entry.unique_id == "1234" + assert config_entry.title == "Envoy 1234" + assert len(mock_setup_entry.mock_calls) == 1 From e925fd2228be092db4392cf4fcccc221aedf2cc7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Apr 2021 22:11:23 -1000 Subject: [PATCH 0065/1317] Add emonitor integration (#48310) Co-authored-by: Paulus Schoutsen --- .coveragerc | 2 + CODEOWNERS | 1 + homeassistant/components/emonitor/__init__.py | 67 +++++++ .../components/emonitor/config_flow.py | 104 ++++++++++ homeassistant/components/emonitor/const.py | 3 + .../components/emonitor/manifest.json | 13 ++ homeassistant/components/emonitor/sensor.py | 108 ++++++++++ .../components/emonitor/strings.json | 23 +++ .../components/emonitor/translations/en.json | 23 +++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/dhcp.py | 5 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/emonitor/__init__.py | 1 + tests/components/emonitor/test_config_flow.py | 187 ++++++++++++++++++ 15 files changed, 544 insertions(+) create mode 100644 homeassistant/components/emonitor/__init__.py create mode 100644 homeassistant/components/emonitor/config_flow.py create mode 100644 homeassistant/components/emonitor/const.py create mode 100644 homeassistant/components/emonitor/manifest.json create mode 100644 homeassistant/components/emonitor/sensor.py create mode 100644 homeassistant/components/emonitor/strings.json create mode 100644 homeassistant/components/emonitor/translations/en.json create mode 100644 tests/components/emonitor/__init__.py create mode 100644 tests/components/emonitor/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 519e3a80fd917..f3cdf62ff73ca 100644 --- a/.coveragerc +++ b/.coveragerc @@ -238,6 +238,8 @@ omit = homeassistant/components/emby/media_player.py homeassistant/components/emoncms/sensor.py homeassistant/components/emoncms_history/* + homeassistant/components/emonitor/__init__.py + homeassistant/components/emonitor/sensor.py homeassistant/components/enigma2/media_player.py homeassistant/components/enocean/__init__.py homeassistant/components/enocean/binary_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 62e1871192cc8..a863b469cdf84 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -134,6 +134,7 @@ homeassistant/components/elkm1/* @gwww @bdraco homeassistant/components/elv/* @majuss homeassistant/components/emby/* @mezz64 homeassistant/components/emoncms/* @borpin +homeassistant/components/emonitor/* @bdraco homeassistant/components/emulated_kasa/* @kbickar homeassistant/components/enigma2/* @fbradyirl homeassistant/components/enocean/* @bdurrer diff --git a/homeassistant/components/emonitor/__init__.py b/homeassistant/components/emonitor/__init__.py new file mode 100644 index 0000000000000..74630a193a406 --- /dev/null +++ b/homeassistant/components/emonitor/__init__.py @@ -0,0 +1,67 @@ +"""The SiteSage Emonitor integration.""" +import asyncio +from datetime import timedelta +import logging + +from aioemonitor import Emonitor + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_UPDATE_RATE = 60 + +PLATFORMS = ["sensor"] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up SiteSage Emonitor from a config entry.""" + + session = aiohttp_client.async_get_clientsession(hass) + emonitor = Emonitor(entry.data[CONF_HOST], session) + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name=entry.title, + update_method=emonitor.async_get_status, + update_interval=timedelta(seconds=DEFAULT_UPDATE_RATE), + ) + + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + + for platform in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, platform) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +def name_short_mac(short_mac): + """Name from short mac.""" + return f"Emonitor {short_mac}" diff --git a/homeassistant/components/emonitor/config_flow.py b/homeassistant/components/emonitor/config_flow.py new file mode 100644 index 0000000000000..bb18f03e3afdc --- /dev/null +++ b/homeassistant/components/emonitor/config_flow.py @@ -0,0 +1,104 @@ +"""Config flow for SiteSage Emonitor integration.""" +import logging + +from aioemonitor import Emonitor +import aiohttp +import voluptuous as vol + +from homeassistant import config_entries, core +from homeassistant.components.dhcp import IP_ADDRESS, MAC_ADDRESS +from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.device_registry import format_mac + +from . import name_short_mac +from .const import DOMAIN # pylint:disable=unused-import + +_LOGGER = logging.getLogger(__name__) + + +async def fetch_mac_and_title(hass: core.HomeAssistant, host): + """Validate the user input allows us to connect.""" + session = aiohttp_client.async_get_clientsession(hass) + emonitor = Emonitor(host, session) + status = await emonitor.async_get_status() + mac_address = status.network.mac_address + return {"title": name_short_mac(mac_address[-6:]), "mac_address": mac_address} + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for SiteSage Emonitor.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + def __init__(self): + """Initialize Emonitor ConfigFlow.""" + self.discovered_ip = None + self.discovered_info = None + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + try: + info = await fetch_mac_and_title(self.hass, user_input[CONF_HOST]) + except aiohttp.ClientError: + errors[CONF_HOST] = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + await self.async_set_unique_id( + format_mac(info["mac_address"]), raise_on_progress=False + ) + return self.async_create_entry(title=info["title"], data=user_input) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + {vol.Required("host", default=self.discovered_ip): str} + ), + errors=errors, + ) + + async def async_step_dhcp(self, dhcp_discovery): + """Handle dhcp discovery.""" + self.discovered_ip = dhcp_discovery[IP_ADDRESS] + await self.async_set_unique_id(format_mac(dhcp_discovery[MAC_ADDRESS])) + self._abort_if_unique_id_configured(updates={CONF_HOST: self.discovered_ip}) + name = name_short_mac(short_mac(dhcp_discovery[MAC_ADDRESS])) + self.context["title_placeholders"] = {"name": name} + try: + self.discovered_info = await fetch_mac_and_title( + self.hass, self.discovered_ip + ) + except Exception as ex: # pylint: disable=broad-except + _LOGGER.debug( + "Unable to fetch status, falling back to manual entry", exc_info=ex + ) + return await self.async_step_user() + return await self.async_step_confirm() + + async def async_step_confirm(self, user_input=None): + """Attempt to confim.""" + if user_input is not None: + return self.async_create_entry( + title=self.discovered_info["title"], + data={CONF_HOST: self.discovered_ip}, + ) + + self._set_confirm_only() + self.context["title_placeholders"] = {"name": self.discovered_info["title"]} + return self.async_show_form( + step_id="confirm", + description_placeholders={ + CONF_HOST: self.discovered_ip, + CONF_NAME: self.discovered_info["title"], + }, + ) + + +def short_mac(mac): + """Short version of the mac.""" + return "".join(mac.split(":")[3:]).upper() diff --git a/homeassistant/components/emonitor/const.py b/homeassistant/components/emonitor/const.py new file mode 100644 index 0000000000000..e39aea462843b --- /dev/null +++ b/homeassistant/components/emonitor/const.py @@ -0,0 +1,3 @@ +"""Constants for the SiteSage Emonitor integration.""" + +DOMAIN = "emonitor" diff --git a/homeassistant/components/emonitor/manifest.json b/homeassistant/components/emonitor/manifest.json new file mode 100644 index 0000000000000..b6cf3526bd8ed --- /dev/null +++ b/homeassistant/components/emonitor/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "emonitor", + "name": "SiteSage Emonitor", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/emonitor", + "requirements": [ + "aioemonitor==1.0.5" + ], + "dhcp": [{"hostname":"emonitor*","macaddress":"0090C2*"}], + "codeowners": [ + "@bdraco" + ] +} \ No newline at end of file diff --git a/homeassistant/components/emonitor/sensor.py b/homeassistant/components/emonitor/sensor.py new file mode 100644 index 0000000000000..3b075f7cbaacf --- /dev/null +++ b/homeassistant/components/emonitor/sensor.py @@ -0,0 +1,108 @@ +"""Support for a Emonitor channel sensor.""" + +from aioemonitor.monitor import EmonitorChannel + +from homeassistant.components.sensor import DEVICE_CLASS_POWER, SensorEntity +from homeassistant.const import POWER_WATT +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.typing import StateType +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from . import name_short_mac +from .const import DOMAIN + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up entry.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id] + channels = coordinator.data.channels + entities = [] + seen_channels = set() + for channel_number, channel in channels.items(): + seen_channels.add(channel_number) + if not channel.active: + continue + if channel.paired_with_channel in seen_channels: + continue + + entities.append(EmonitorPowerSensor(coordinator, channel_number)) + + async_add_entities(entities) + + +class EmonitorPowerSensor(CoordinatorEntity, SensorEntity): + """Representation of an Emonitor power sensor entity.""" + + def __init__(self, coordinator: DataUpdateCoordinator, channel_number: int): + """Initialize the channel sensor.""" + self.channel_number = channel_number + super().__init__(coordinator) + + @property + def unique_id(self) -> str: + """Channel unique id.""" + return f"{self.mac_address}_{self.channel_number}" + + @property + def channel_data(self) -> EmonitorChannel: + """Channel data.""" + return self.coordinator.data.channels[self.channel_number] + + @property + def paired_channel_data(self) -> EmonitorChannel: + """Channel data.""" + return self.coordinator.data.channels[self.channel_data.paired_with_channel] + + @property + def name(self) -> str: + """Name of the sensor.""" + return self.channel_data.label + + @property + def unit_of_measurement(self) -> str: + """Return the unit of measurement.""" + return POWER_WATT + + @property + def device_class(self) -> str: + """Device class of the sensor.""" + return DEVICE_CLASS_POWER + + def _paired_attr(self, attr_name: str) -> float: + """Cumulative attributes for channel and paired channel.""" + attr_val = getattr(self.channel_data, attr_name) + if self.channel_data.paired_with_channel: + attr_val += getattr(self.paired_channel_data, attr_name) + return attr_val + + @property + def state(self) -> StateType: + """State of the sensor.""" + return self._paired_attr("inst_power") + + @property + def extra_state_attributes(self) -> dict: + """Return the device specific state attributes.""" + return { + "channel": self.channel_number, + "avg_power": self._paired_attr("avg_power"), + "max_power": self._paired_attr("max_power"), + } + + @property + def mac_address(self) -> str: + """Return the mac address of the device.""" + return self.coordinator.data.network.mac_address + + @property + def device_info(self) -> dict: + """Return info about the emonitor device.""" + return { + "name": name_short_mac(self.mac_address[-6:]), + "connections": {(dr.CONNECTION_NETWORK_MAC, self.mac_address)}, + "manufacturer": "Powerhouse Dynamics, Inc.", + "sw_version": self.coordinator.data.hardware.firmware_version, + } diff --git a/homeassistant/components/emonitor/strings.json b/homeassistant/components/emonitor/strings.json new file mode 100644 index 0000000000000..aac15dfaae257 --- /dev/null +++ b/homeassistant/components/emonitor/strings.json @@ -0,0 +1,23 @@ +{ + "config": { + "flow_title": "SiteSage {name}", + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]" + } + }, + "confirm": { + "title": "Setup SiteSage Emonitor", + "description": "Do you want to setup {name} ({host})?" + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/emonitor/translations/en.json b/homeassistant/components/emonitor/translations/en.json new file mode 100644 index 0000000000000..6e24bbac7a3b9 --- /dev/null +++ b/homeassistant/components/emonitor/translations/en.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "unknown": "Unexpected error" + }, + "flow_title": "SiteSage {name}", + "step": { + "confirm": { + "description": "Do you want to setup {name} ({host})?", + "title": "Setup SiteSage Emonitor" + }, + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index fd385b21ca089..4095993346ec6 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -60,6 +60,7 @@ "econet", "elgato", "elkm1", + "emonitor", "emulated_roku", "enocean", "enphase_envoy", diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 83622545551f6..4d4e3688c1ba4 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -57,6 +57,11 @@ "domain": "broadlink", "macaddress": "B4430D*" }, + { + "domain": "emonitor", + "hostname": "emonitor*", + "macaddress": "0090C2*" + }, { "domain": "flume", "hostname": "flume-gw-*", diff --git a/requirements_all.txt b/requirements_all.txt index df497eb889a34..22e136f255ab9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -156,6 +156,9 @@ aiodns==2.0.0 # homeassistant.components.eafm aioeafm==0.1.2 +# homeassistant.components.emonitor +aioemonitor==1.0.5 + # homeassistant.components.esphome aioesphomeapi==2.6.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3594e316958e9..d573457b0f591 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -93,6 +93,9 @@ aiodns==2.0.0 # homeassistant.components.eafm aioeafm==0.1.2 +# homeassistant.components.emonitor +aioemonitor==1.0.5 + # homeassistant.components.esphome aioesphomeapi==2.6.6 diff --git a/tests/components/emonitor/__init__.py b/tests/components/emonitor/__init__.py new file mode 100644 index 0000000000000..6415078299f41 --- /dev/null +++ b/tests/components/emonitor/__init__.py @@ -0,0 +1 @@ +"""Tests for the SiteSage Emonitor integration.""" diff --git a/tests/components/emonitor/test_config_flow.py b/tests/components/emonitor/test_config_flow.py new file mode 100644 index 0000000000000..65fc471786f16 --- /dev/null +++ b/tests/components/emonitor/test_config_flow.py @@ -0,0 +1,187 @@ +"""Test the SiteSage Emonitor config flow.""" +from unittest.mock import MagicMock, patch + +from aioemonitor.monitor import EmonitorNetwork, EmonitorStatus +import aiohttp + +from homeassistant import config_entries, setup +from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS +from homeassistant.components.emonitor.const import DOMAIN +from homeassistant.const import CONF_HOST + +from tests.common import MockConfigEntry + + +def _mock_emonitor(): + return EmonitorStatus( + MagicMock(), EmonitorNetwork("AABBCCDDEEFF", "1.2.3.4"), MagicMock() + ) + + +async def test_form(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.emonitor.config_flow.Emonitor.async_get_status", + return_value=_mock_emonitor(), + ), patch( + "homeassistant.components.emonitor.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.2.3.4", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "Emonitor DDEEFF" + assert result2["data"] == { + "host": "1.2.3.4", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_unknown_error(hass): + """Test we handle unknown error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.emonitor.config_flow.Emonitor.async_get_status", + side_effect=Exception, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.2.3.4", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "unknown"} + + +async def test_form_cannot_connect(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.emonitor.config_flow.Emonitor.async_get_status", + side_effect=aiohttp.ClientError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.2.3.4", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {CONF_HOST: "cannot_connect"} + + +async def test_dhcp_can_confirm(hass): + """Test DHCP discovery flow can confirm right away.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + with patch( + "homeassistant.components.emonitor.config_flow.Emonitor.async_get_status", + return_value=_mock_emonitor(), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "dhcp"}, + data={ + HOSTNAME: "emonitor", + IP_ADDRESS: "1.2.3.4", + MAC_ADDRESS: "aa:bb:cc:dd:ee:ff", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == "form" + assert result["step_id"] == "confirm" + assert result["description_placeholders"] == { + "host": "1.2.3.4", + "name": "Emonitor DDEEFF", + } + + with patch( + "homeassistant.components.emonitor.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "Emonitor DDEEFF" + assert result2["data"] == { + "host": "1.2.3.4", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_dhcp_fails_to_connect(hass): + """Test DHCP discovery flow that fails to connect.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + with patch( + "homeassistant.components.emonitor.config_flow.Emonitor.async_get_status", + side_effect=aiohttp.ClientError, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "dhcp"}, + data={ + HOSTNAME: "emonitor", + IP_ADDRESS: "1.2.3.4", + MAC_ADDRESS: "aa:bb:cc:dd:ee:ff", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == "form" + assert result["step_id"] == "user" + + +async def test_dhcp_already_exists(hass): + """Test DHCP discovery flow that fails to connect.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "1.2.3.4"}, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.emonitor.config_flow.Emonitor.async_get_status", + return_value=_mock_emonitor(), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "dhcp"}, + data={ + HOSTNAME: "emonitor", + IP_ADDRESS: "1.2.3.4", + MAC_ADDRESS: "aa:bb:cc:dd:ee:ff", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == "abort" From 12e3bc81018424ee30298214eede5651abfbc599 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Apr 2021 22:11:44 -1000 Subject: [PATCH 0066/1317] Provide api to see which integrations are being loaded (#48274) Co-authored-by: Paulus Schoutsen --- homeassistant/bootstrap.py | 63 ++++--- .../components/device_tracker/legacy.py | 90 ++++----- homeassistant/components/notify/__init__.py | 77 ++++---- .../components/websocket_api/commands.py | 40 +++- homeassistant/helpers/entity_platform.py | 138 +++++++------- homeassistant/setup.py | 171 ++++++++++-------- .../components/websocket_api/test_commands.py | 46 ++++- tests/test_bootstrap.py | 4 +- tests/test_setup.py | 29 +++ 9 files changed, 415 insertions(+), 243 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index d19ddaf4f5d6e..b43e789005b5e 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -20,14 +20,17 @@ from homeassistant.const import REQUIRED_NEXT_PYTHON_DATE, REQUIRED_NEXT_PYTHON_VER from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import area_registry, device_registry, entity_registry +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.typing import ConfigType from homeassistant.setup import ( DATA_SETUP, DATA_SETUP_STARTED, + DATA_SETUP_TIME, async_set_domains_to_be_loaded, async_setup_component, ) from homeassistant.util.async_ import gather_with_concurrency +import homeassistant.util.dt as dt_util from homeassistant.util.logging import async_activate_log_queue_handler from homeassistant.util.package import async_get_user_site, is_virtual_env @@ -42,6 +45,8 @@ DATA_LOGGING = "logging" LOG_SLOW_STARTUP_INTERVAL = 60 +SLOW_STARTUP_CHECK_INTERVAL = 1 +SIGNAL_BOOTSTRAP_INTEGRATONS = "bootstrap_integrations" STAGE_1_TIMEOUT = 120 STAGE_2_TIMEOUT = 300 @@ -380,19 +385,29 @@ def _get_domains(hass: core.HomeAssistant, config: dict[str, Any]) -> set[str]: return domains -async def _async_log_pending_setups( - hass: core.HomeAssistant, domains: set[str], setup_started: dict[str, datetime] -) -> None: +async def _async_watch_pending_setups(hass: core.HomeAssistant) -> None: """Periodic log of setups that are pending for longer than LOG_SLOW_STARTUP_INTERVAL.""" + loop_count = 0 + setup_started: dict[str, datetime] = hass.data[DATA_SETUP_STARTED] while True: - await asyncio.sleep(LOG_SLOW_STARTUP_INTERVAL) - remaining = [domain for domain in domains if domain in setup_started] + now = dt_util.utcnow() + remaining_with_setup_started = { + domain: (now - setup_started[domain]).total_seconds() + for domain in setup_started + } + _LOGGER.debug("Integration remaining: %s", remaining_with_setup_started) + async_dispatcher_send( + hass, SIGNAL_BOOTSTRAP_INTEGRATONS, remaining_with_setup_started + ) + await asyncio.sleep(SLOW_STARTUP_CHECK_INTERVAL) + loop_count += SLOW_STARTUP_CHECK_INTERVAL - if remaining: + if loop_count >= LOG_SLOW_STARTUP_INTERVAL and setup_started: _LOGGER.warning( "Waiting on integrations to complete setup: %s", - ", ".join(remaining), + ", ".join(setup_started), ) + loop_count = 0 _LOGGER.debug("Running timeout Zones: %s", hass.timeout.zones) @@ -400,18 +415,13 @@ async def async_setup_multi_components( hass: core.HomeAssistant, domains: set[str], config: dict[str, Any], - setup_started: dict[str, datetime], ) -> None: """Set up multiple domains. Log on failure.""" futures = { domain: hass.async_create_task(async_setup_component(hass, domain, config)) for domain in domains } - log_task = asyncio.create_task( - _async_log_pending_setups(hass, domains, setup_started) - ) await asyncio.wait(futures.values()) - log_task.cancel() errors = [domain for domain in domains if futures[domain].exception()] for domain in errors: exception = futures[domain].exception() @@ -427,7 +437,11 @@ async def _async_set_up_integrations( hass: core.HomeAssistant, config: dict[str, Any] ) -> None: """Set up all the integrations.""" - setup_started = hass.data[DATA_SETUP_STARTED] = {} + hass.data[DATA_SETUP_STARTED] = {} + setup_time = hass.data[DATA_SETUP_TIME] = {} + + log_task = asyncio.create_task(_async_watch_pending_setups(hass)) + domains_to_setup = _get_domains(hass, config) # Resolve all dependencies so we know all integrations @@ -476,14 +490,14 @@ async def _async_set_up_integrations( # Load logging as soon as possible if logging_domains: _LOGGER.info("Setting up logging: %s", logging_domains) - await async_setup_multi_components(hass, logging_domains, config, setup_started) + await async_setup_multi_components(hass, logging_domains, config) # Start up debuggers. Start these first in case they want to wait. debuggers = domains_to_setup & DEBUGGER_INTEGRATIONS if debuggers: _LOGGER.debug("Setting up debuggers: %s", debuggers) - await async_setup_multi_components(hass, debuggers, config, setup_started) + await async_setup_multi_components(hass, debuggers, config) # calculate what components to setup in what stage stage_1_domains = set() @@ -524,9 +538,7 @@ async def _async_set_up_integrations( async with hass.timeout.async_timeout( STAGE_1_TIMEOUT, cool_down=COOLDOWN_TIME ): - await async_setup_multi_components( - hass, stage_1_domains, config, setup_started - ) + await async_setup_multi_components(hass, stage_1_domains, config) except asyncio.TimeoutError: _LOGGER.warning("Setup timed out for stage 1 - moving forward") @@ -539,12 +551,21 @@ async def _async_set_up_integrations( async with hass.timeout.async_timeout( STAGE_2_TIMEOUT, cool_down=COOLDOWN_TIME ): - await async_setup_multi_components( - hass, stage_2_domains, config, setup_started - ) + await async_setup_multi_components(hass, stage_2_domains, config) except asyncio.TimeoutError: _LOGGER.warning("Setup timed out for stage 2 - moving forward") + log_task.cancel() + _LOGGER.debug( + "Integration setup times: %s", + { + integration: timedelta.total_seconds() + for integration, timedelta in sorted( + setup_time.items(), key=lambda item: item[1].total_seconds() # type: ignore + ) + }, + ) + # Wrap up startup _LOGGER.debug("Waiting for startup to wrap up") try: diff --git a/homeassistant/components/device_tracker/legacy.py b/homeassistant/components/device_tracker/legacy.py index eae133965c6d9..a90d92944a4d8 100644 --- a/homeassistant/components/device_tracker/legacy.py +++ b/homeassistant/components/device_tracker/legacy.py @@ -38,7 +38,7 @@ ) from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, GPSType -from homeassistant.setup import async_prepare_setup_platform +from homeassistant.setup import async_prepare_setup_platform, async_start_setup from homeassistant.util import dt as dt_util from homeassistant.util.yaml import dump @@ -221,48 +221,54 @@ def type(self): async def async_setup_legacy(self, hass, tracker, discovery_info=None): """Set up a legacy platform.""" - LOGGER.info("Setting up %s.%s", DOMAIN, self.name) - try: - scanner = None - setup = None - if hasattr(self.platform, "async_get_scanner"): - scanner = await self.platform.async_get_scanner( - hass, {DOMAIN: self.config} - ) - elif hasattr(self.platform, "get_scanner"): - scanner = await hass.async_add_executor_job( - self.platform.get_scanner, hass, {DOMAIN: self.config} - ) - elif hasattr(self.platform, "async_setup_scanner"): - setup = await self.platform.async_setup_scanner( - hass, self.config, tracker.async_see, discovery_info - ) - elif hasattr(self.platform, "setup_scanner"): - setup = await hass.async_add_executor_job( - self.platform.setup_scanner, - hass, - self.config, - tracker.see, - discovery_info, - ) - else: - raise HomeAssistantError("Invalid legacy device_tracker platform.") - - if setup: - hass.config.components.add(f"{DOMAIN}.{self.name}") - - if scanner: - async_setup_scanner_platform( - hass, self.config, scanner, tracker.async_see, self.type + full_name = f"{DOMAIN}.{self.name}" + LOGGER.info("Setting up %s", full_name) + with async_start_setup(hass, [full_name]): + try: + scanner = None + setup = None + if hasattr(self.platform, "async_get_scanner"): + scanner = await self.platform.async_get_scanner( + hass, {DOMAIN: self.config} + ) + elif hasattr(self.platform, "get_scanner"): + scanner = await hass.async_add_executor_job( + self.platform.get_scanner, hass, {DOMAIN: self.config} + ) + elif hasattr(self.platform, "async_setup_scanner"): + setup = await self.platform.async_setup_scanner( + hass, self.config, tracker.async_see, discovery_info + ) + elif hasattr(self.platform, "setup_scanner"): + setup = await hass.async_add_executor_job( + self.platform.setup_scanner, + hass, + self.config, + tracker.see, + discovery_info, + ) + else: + raise HomeAssistantError("Invalid legacy device_tracker platform.") + + if setup: + hass.config.components.add(full_name) + + if scanner: + async_setup_scanner_platform( + hass, self.config, scanner, tracker.async_see, self.type + ) + return + + if not setup: + LOGGER.error( + "Error setting up platform %s %s", self.type, self.name + ) + return + + except Exception: # pylint: disable=broad-except + LOGGER.exception( + "Error setting up platform %s %s", self.type, self.name ) - return - - if not setup: - LOGGER.error("Error setting up platform %s %s", self.type, self.name) - return - - except Exception: # pylint: disable=broad-except - LOGGER.exception("Error setting up platform %s %s", self.type, self.name) async def async_extract_config(hass, config): diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index e64ceb48a217a..118579fb0c0c7 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -16,7 +16,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.service import async_set_service_schema from homeassistant.loader import async_get_integration, bind_hass -from homeassistant.setup import async_prepare_setup_platform +from homeassistant.setup import async_prepare_setup_platform, async_start_setup from homeassistant.util import slugify from homeassistant.util.yaml import load_yaml @@ -289,47 +289,52 @@ async def async_setup_platform( _LOGGER.error("Unknown notification service specified") return - _LOGGER.info("Setting up %s.%s", DOMAIN, integration_name) - notify_service = None - try: - if hasattr(platform, "async_get_service"): - notify_service = await platform.async_get_service( - hass, p_config, discovery_info - ) - elif hasattr(platform, "get_service"): - notify_service = await hass.async_add_executor_job( - platform.get_service, hass, p_config, discovery_info - ) - else: - raise HomeAssistantError("Invalid notify platform.") - - if notify_service is None: - # Platforms can decide not to create a service based - # on discovery data. - if discovery_info is None: - _LOGGER.error( - "Failed to initialize notification service %s", integration_name + full_name = f"{DOMAIN}.{integration_name}" + _LOGGER.info("Setting up %s", full_name) + with async_start_setup(hass, [full_name]): + notify_service = None + try: + if hasattr(platform, "async_get_service"): + notify_service = await platform.async_get_service( + hass, p_config, discovery_info + ) + elif hasattr(platform, "get_service"): + notify_service = await hass.async_add_executor_job( + platform.get_service, hass, p_config, discovery_info ) + else: + raise HomeAssistantError("Invalid notify platform.") + + if notify_service is None: + # Platforms can decide not to create a service based + # on discovery data. + if discovery_info is None: + _LOGGER.error( + "Failed to initialize notification service %s", + integration_name, + ) + return + + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error setting up platform %s", integration_name) return - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Error setting up platform %s", integration_name) - return - - if discovery_info is None: - discovery_info = {} + if discovery_info is None: + discovery_info = {} - conf_name = p_config.get(CONF_NAME) or discovery_info.get(CONF_NAME) - target_service_name_prefix = conf_name or integration_name - service_name = slugify(conf_name or SERVICE_NOTIFY) + conf_name = p_config.get(CONF_NAME) or discovery_info.get(CONF_NAME) + target_service_name_prefix = conf_name or integration_name + service_name = slugify(conf_name or SERVICE_NOTIFY) - await notify_service.async_setup(hass, service_name, target_service_name_prefix) - await notify_service.async_register_services() + await notify_service.async_setup( + hass, service_name, target_service_name_prefix + ) + await notify_service.async_register_services() - hass.data[NOTIFY_SERVICES].setdefault(integration_name, []).append( - notify_service - ) - hass.config.components.add(f"{DOMAIN}.{integration_name}") + hass.data[NOTIFY_SERVICES].setdefault(integration_name, []).append( + notify_service + ) + hass.config.components.add(f"{DOMAIN}.{integration_name}") return True diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 2912512fa62bf..f7961046043a6 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -4,6 +4,7 @@ import voluptuous as vol from homeassistant.auth.permissions.const import CAT_ENTITIES, POLICY_READ +from homeassistant.bootstrap import SIGNAL_BOOTSTRAP_INTEGRATONS from homeassistant.components.websocket_api.const import ERR_NOT_FOUND from homeassistant.const import EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL from homeassistant.core import DOMAIN as HASS_DOMAIN, callback @@ -14,10 +15,11 @@ Unauthorized, ) from homeassistant.helpers import config_validation as cv, entity, template +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.event import TrackTemplate, async_track_template_result from homeassistant.helpers.service import async_get_all_descriptions from homeassistant.loader import IntegrationNotFound, async_get_integration -from homeassistant.setup import async_get_loaded_integrations +from homeassistant.setup import DATA_SETUP_TIME, async_get_loaded_integrations from . import const, decorators, messages @@ -34,9 +36,11 @@ def async_register_commands(hass, async_reg): async_reg(hass, handle_get_services) async_reg(hass, handle_get_states) async_reg(hass, handle_manifest_get) + async_reg(hass, handle_integration_setup_info) async_reg(hass, handle_manifest_list) async_reg(hass, handle_ping) async_reg(hass, handle_render_template) + async_reg(hass, handle_subscribe_bootstrap_integrations) async_reg(hass, handle_subscribe_events) async_reg(hass, handle_subscribe_trigger) async_reg(hass, handle_test_condition) @@ -95,6 +99,27 @@ def forward_events(event): connection.send_message(messages.result_message(msg["id"])) +@callback +@decorators.websocket_command( + { + vol.Required("type"): "subscribe_bootstrap_integrations", + } +) +def handle_subscribe_bootstrap_integrations(hass, connection, msg): + """Handle subscribe bootstrap integrations command.""" + + @callback + def forward_bootstrap_integrations(message): + """Forward bootstrap integrations to websocket.""" + connection.send_message(messages.result_message(msg["id"], message)) + + connection.subscriptions[msg["id"]] = async_dispatcher_connect( + hass, SIGNAL_BOOTSTRAP_INTEGRATONS, forward_bootstrap_integrations + ) + + connection.send_message(messages.result_message(msg["id"])) + + @callback @decorators.websocket_command( { @@ -238,6 +263,19 @@ async def handle_manifest_get(hass, connection, msg): connection.send_error(msg["id"], const.ERR_NOT_FOUND, "Integration not found") +@decorators.websocket_command({vol.Required("type"): "integration/setup_info"}) +@decorators.async_response +async def handle_integration_setup_info(hass, connection, msg): + """Handle integrations command.""" + connection.send_result( + msg["id"], + [ + {"domain": integration, "seconds": timedelta.total_seconds()} + for integration, timedelta in hass.data[DATA_SETUP_TIME].items() + ], + ) + + @callback @decorators.websocket_command({vol.Required("type"): "ping"}) def handle_ping(hass, connection, msg): diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 00783b072c9a2..dc7386c18a8cb 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -4,6 +4,7 @@ import asyncio from contextvars import ContextVar from datetime import datetime, timedelta +import logging from logging import Logger from types import ModuleType from typing import TYPE_CHECKING, Callable, Coroutine, Iterable @@ -30,6 +31,7 @@ entity_registry as ent_reg, service, ) +from homeassistant.setup import async_start_setup from homeassistant.util.async_ import run_callback_threadsafe from .entity_registry import DISABLED_INTEGRATION @@ -48,6 +50,8 @@ DATA_ENTITY_PLATFORM = "entity_platform" PLATFORM_NOT_READY_BASE_WAIT_TIME = 30 # seconds +_LOGGER = logging.getLogger(__name__) + class EntityPlatform: """Manage the entities for a single platform.""" @@ -202,77 +206,77 @@ async def _async_setup_platform( self.platform_name, SLOW_SETUP_WARNING, ) - - try: - task = async_create_setup_task() - - async with hass.timeout.async_timeout(SLOW_SETUP_MAX_WAIT, self.domain): - await asyncio.shield(task) - - # Block till all entities are done - while self._tasks: - pending = [task for task in self._tasks if not task.done()] - self._tasks.clear() - - if pending: - await asyncio.gather(*pending) - - hass.config.components.add(full_name) - self._setup_complete = True - return True - except PlatformNotReady as ex: - tries += 1 - wait_time = min(tries, 6) * PLATFORM_NOT_READY_BASE_WAIT_TIME - message = str(ex) - if not message and ex.__cause__: - message = str(ex.__cause__) - ready_message = f"ready yet: {message}" if message else "ready yet" - if tries == 1: - logger.warning( - "Platform %s not %s; Retrying in background in %d seconds", + with async_start_setup(hass, [full_name]): + try: + task = async_create_setup_task() + + async with hass.timeout.async_timeout(SLOW_SETUP_MAX_WAIT, self.domain): + await asyncio.shield(task) + + # Block till all entities are done + while self._tasks: + pending = [task for task in self._tasks if not task.done()] + self._tasks.clear() + + if pending: + await asyncio.gather(*pending) + + hass.config.components.add(full_name) + self._setup_complete = True + return True + except PlatformNotReady as ex: + tries += 1 + wait_time = min(tries, 6) * PLATFORM_NOT_READY_BASE_WAIT_TIME + message = str(ex) + if not message and ex.__cause__: + message = str(ex.__cause__) + ready_message = f"ready yet: {message}" if message else "ready yet" + if tries == 1: + logger.warning( + "Platform %s not %s; Retrying in background in %d seconds", + self.platform_name, + ready_message, + wait_time, + ) + else: + logger.debug( + "Platform %s not %s; Retrying in %d seconds", + self.platform_name, + ready_message, + wait_time, + ) + + async def setup_again(*_): # type: ignore[no-untyped-def] + """Run setup again.""" + self._async_cancel_retry_setup = None + await self._async_setup_platform(async_create_setup_task, tries) + + if hass.state == CoreState.running: + self._async_cancel_retry_setup = async_call_later( + hass, wait_time, setup_again + ) + else: + self._async_cancel_retry_setup = hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STARTED, setup_again + ) + return False + except asyncio.TimeoutError: + logger.error( + "Setup of platform %s is taking longer than %s seconds." + " Startup will proceed without waiting any longer.", self.platform_name, - ready_message, - wait_time, + SLOW_SETUP_MAX_WAIT, ) - else: - logger.debug( - "Platform %s not %s; Retrying in %d seconds", + return False + except Exception: # pylint: disable=broad-except + logger.exception( + "Error while setting up %s platform for %s", self.platform_name, - ready_message, - wait_time, + self.domain, ) - - async def setup_again(*_): # type: ignore[no-untyped-def] - """Run setup again.""" - self._async_cancel_retry_setup = None - await self._async_setup_platform(async_create_setup_task, tries) - - if hass.state == CoreState.running: - self._async_cancel_retry_setup = async_call_later( - hass, wait_time, setup_again - ) - else: - self._async_cancel_retry_setup = hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STARTED, setup_again - ) - return False - except asyncio.TimeoutError: - logger.error( - "Setup of platform %s is taking longer than %s seconds." - " Startup will proceed without waiting any longer.", - self.platform_name, - SLOW_SETUP_MAX_WAIT, - ) - return False - except Exception: # pylint: disable=broad-except - logger.exception( - "Error while setting up %s platform for %s", - self.platform_name, - self.domain, - ) - return False - finally: - warn_task.cancel() + return False + finally: + warn_task.cancel() def _schedule_add_entities( self, new_entities: Iterable[Entity], update_before_add: bool = False diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 6b306995dfc28..c65e428e03a45 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -2,17 +2,18 @@ from __future__ import annotations import asyncio +import contextlib import logging.handlers from timeit import default_timer as timer from types import ModuleType -from typing import Awaitable, Callable +from typing import Awaitable, Callable, Generator, Iterable from homeassistant import config as conf_util, core, loader, requirements from homeassistant.config import async_notify_setup_error from homeassistant.const import EVENT_COMPONENT_LOADED, PLATFORM_FORMAT from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.typing import ConfigType -from homeassistant.util import dt as dt_util +from homeassistant.util import dt as dt_util, ensure_unique_string _LOGGER = logging.getLogger(__name__) @@ -42,6 +43,8 @@ DATA_SETUP_DONE = "setup_done" DATA_SETUP_STARTED = "setup_started" +DATA_SETUP_TIME = "setup_time" + DATA_SETUP = "setup_tasks" DATA_DEPS_REQS = "deps_reqs_processed" @@ -205,84 +208,77 @@ def log_error(msg: str, link: str | None = None) -> None: start = timer() _LOGGER.info("Setting up %s", domain) - hass.data.setdefault(DATA_SETUP_STARTED, {})[domain] = dt_util.utcnow() - - if hasattr(component, "PLATFORM_SCHEMA"): - # Entity components have their own warning - warn_task = None - else: - warn_task = hass.loop.call_later( - SLOW_SETUP_WARNING, - _LOGGER.warning, - "Setup of %s is taking over %s seconds.", - domain, - SLOW_SETUP_WARNING, - ) + with async_start_setup(hass, [domain]): + if hasattr(component, "PLATFORM_SCHEMA"): + # Entity components have their own warning + warn_task = None + else: + warn_task = hass.loop.call_later( + SLOW_SETUP_WARNING, + _LOGGER.warning, + "Setup of %s is taking over %s seconds.", + domain, + SLOW_SETUP_WARNING, + ) - task = None - result = True - try: - if hasattr(component, "async_setup"): - task = component.async_setup(hass, processed_config) # type: ignore - elif hasattr(component, "setup"): - # This should not be replaced with hass.async_add_executor_job because - # we don't want to track this task in case it blocks startup. - task = hass.loop.run_in_executor( - None, component.setup, hass, processed_config # type: ignore + task = None + result = True + try: + if hasattr(component, "async_setup"): + task = component.async_setup(hass, processed_config) # type: ignore + elif hasattr(component, "setup"): + # This should not be replaced with hass.async_add_executor_job because + # we don't want to track this task in case it blocks startup. + task = hass.loop.run_in_executor( + None, component.setup, hass, processed_config # type: ignore + ) + elif not hasattr(component, "async_setup_entry"): + log_error("No setup or config entry setup function defined.") + return False + + if task: + async with hass.timeout.async_timeout(SLOW_SETUP_MAX_WAIT, domain): + result = await task + except asyncio.TimeoutError: + _LOGGER.error( + "Setup of %s is taking longer than %s seconds." + " Startup will proceed without waiting any longer", + domain, + SLOW_SETUP_MAX_WAIT, + ) + return False + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error during setup of component %s", domain) + async_notify_setup_error(hass, domain, integration.documentation) + return False + finally: + end = timer() + if warn_task: + warn_task.cancel() + _LOGGER.info("Setup of domain %s took %.1f seconds", domain, end - start) + + if result is False: + log_error("Integration failed to initialize.") + return False + if result is not True: + log_error( + f"Integration {domain!r} did not return boolean if setup was " + "successful. Disabling component." ) - elif not hasattr(component, "async_setup_entry"): - log_error("No setup or config entry setup function defined.") - hass.data[DATA_SETUP_STARTED].pop(domain) return False - if task: - async with hass.timeout.async_timeout(SLOW_SETUP_MAX_WAIT, domain): - result = await task - except asyncio.TimeoutError: - _LOGGER.error( - "Setup of %s is taking longer than %s seconds." - " Startup will proceed without waiting any longer", - domain, - SLOW_SETUP_MAX_WAIT, - ) - hass.data[DATA_SETUP_STARTED].pop(domain) - return False - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Error during setup of component %s", domain) - async_notify_setup_error(hass, domain, integration.documentation) - hass.data[DATA_SETUP_STARTED].pop(domain) - return False - finally: - end = timer() - if warn_task: - warn_task.cancel() - _LOGGER.info("Setup of domain %s took %.1f seconds", domain, end - start) - - if result is False: - log_error("Integration failed to initialize.") - hass.data[DATA_SETUP_STARTED].pop(domain) - return False - if result is not True: - log_error( - f"Integration {domain!r} did not return boolean if setup was " - "successful. Disabling component." - ) - hass.data[DATA_SETUP_STARTED].pop(domain) - return False - - # Flush out async_setup calling create_task. Fragile but covered by test. - await asyncio.sleep(0) - await hass.config_entries.flow.async_wait_init_flow_finish(domain) + # Flush out async_setup calling create_task. Fragile but covered by test. + await asyncio.sleep(0) + await hass.config_entries.flow.async_wait_init_flow_finish(domain) - await asyncio.gather( - *[ - entry.async_setup(hass, integration=integration) - for entry in hass.config_entries.async_entries(domain) - ] - ) + await asyncio.gather( + *[ + entry.async_setup(hass, integration=integration) + for entry in hass.config_entries.async_entries(domain) + ] + ) - hass.config.components.add(domain) - hass.data[DATA_SETUP_STARTED].pop(domain) + hass.config.components.add(domain) # Cleanup if domain in hass.data[DATA_SETUP]: @@ -420,3 +416,30 @@ def async_get_loaded_integrations(hass: core.HomeAssistant) -> set: if domain in BASE_PLATFORMS: integrations.add(platform) return integrations + + +@contextlib.contextmanager +def async_start_setup(hass: core.HomeAssistant, components: Iterable) -> Generator: + """Keep track of when setup starts and finishes.""" + setup_started = hass.data.setdefault(DATA_SETUP_STARTED, {}) + started = dt_util.utcnow() + unique_components = {} + for domain in components: + unique = ensure_unique_string(domain, setup_started) + unique_components[unique] = domain + setup_started[unique] = started + + yield + + setup_time = hass.data.setdefault(DATA_SETUP_TIME, {}) + time_taken = dt_util.utcnow() - started + for unique, domain in unique_components.items(): + del setup_started[unique] + if "." in domain: + _, integration = domain.split(".", 1) + else: + integration = domain + if integration in setup_time: + setup_time[integration] += time_taken + else: + setup_time[integration] = time_taken diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index da42e175ff360..09123db457997 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -1,10 +1,12 @@ """Tests for WebSocket API commands.""" +import datetime from unittest.mock import ANY, patch from async_timeout import timeout import pytest import voluptuous as vol +from homeassistant.bootstrap import SIGNAL_BOOTSTRAP_INTEGRATONS from homeassistant.components.websocket_api import const from homeassistant.components.websocket_api.auth import ( TYPE_AUTH, @@ -15,9 +17,10 @@ from homeassistant.core import Context, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.typing import HomeAssistantType from homeassistant.loader import async_get_integration -from homeassistant.setup import async_setup_component +from homeassistant.setup import DATA_SETUP_TIME, async_setup_component from tests.common import MockEntity, MockEntityPlatform, async_mock_service @@ -1124,3 +1127,44 @@ async def test_execute_script(hass, websocket_client): assert call.service == "test_service" assert call.data == {"hello": "From variable"} assert call.context.as_dict() == msg_var["result"]["context"] + + +async def test_subscribe_unsubscribe_bootstrap_integrations( + hass, websocket_client, hass_admin_user +): + """Test subscribe/unsubscribe bootstrap_integrations.""" + await websocket_client.send_json( + {"id": 7, "type": "subscribe_bootstrap_integrations"} + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + + message = {"august": 12.5, "isy994": 12.8} + + async_dispatcher_send(hass, SIGNAL_BOOTSTRAP_INTEGRATONS, message) + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["success"] is True + assert msg["type"] == "result" + assert msg["result"] == message + + +async def test_integration_setup_info(hass, websocket_client, hass_admin_user): + """Test subscribe/unsubscribe bootstrap_integrations.""" + hass.data[DATA_SETUP_TIME] = { + "august": datetime.timedelta(seconds=12.5), + "isy994": datetime.timedelta(seconds=12.8), + } + await websocket_client.send_json({"id": 7, "type": "integration/setup_info"}) + + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + assert msg["result"] == [ + {"domain": "august", "seconds": 12.5}, + {"domain": "isy994", "seconds": 12.8}, + ] diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index c035f6f1d1d79..2464638627897 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -431,7 +431,9 @@ async def _async_setup_that_blocks_startup(*args, **kwargs): with patch( "homeassistant.config.async_hass_config_yaml", return_value={"browser": {}, "frontend": {}}, - ), patch.object(bootstrap, "LOG_SLOW_STARTUP_INTERVAL", 0.3), patch( + ), patch.object(bootstrap, "LOG_SLOW_STARTUP_INTERVAL", 0.3), patch.object( + bootstrap, "SLOW_STARTUP_CHECK_INTERVAL", 0.05 + ), patch( "homeassistant.components.frontend.async_setup", side_effect=_async_setup_that_blocks_startup, ): diff --git a/tests/test_setup.py b/tests/test_setup.py index 9b68dbf4eab5d..72613722ca1f1 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -1,6 +1,7 @@ """Test component/platform setup.""" # pylint: disable=protected-access import asyncio +import datetime import os import threading from unittest.mock import AsyncMock, Mock, patch @@ -644,3 +645,31 @@ async def test_integration_only_setup_entry(hass): ), ) assert await setup.async_setup_component(hass, "test_integration_only_entry", {}) + + +async def test_async_start_setup(hass): + """Test setup started context manager keeps track of setup times.""" + with setup.async_start_setup(hass, ["august"]): + assert isinstance( + hass.data[setup.DATA_SETUP_STARTED]["august"], datetime.datetime + ) + with setup.async_start_setup(hass, ["august"]): + assert isinstance( + hass.data[setup.DATA_SETUP_STARTED]["august_2"], datetime.datetime + ) + + assert "august" not in hass.data[setup.DATA_SETUP_STARTED] + assert isinstance(hass.data[setup.DATA_SETUP_TIME]["august"], datetime.timedelta) + assert "august_2" not in hass.data[setup.DATA_SETUP_TIME] + + +async def test_async_start_setup_platforms(hass): + """Test setup started context manager keeps track of setup times for platforms.""" + with setup.async_start_setup(hass, ["sensor.august"]): + assert isinstance( + hass.data[setup.DATA_SETUP_STARTED]["sensor.august"], datetime.datetime + ) + + assert "august" not in hass.data[setup.DATA_SETUP_STARTED] + assert isinstance(hass.data[setup.DATA_SETUP_TIME]["august"], datetime.timedelta) + assert "sensor" not in hass.data[setup.DATA_SETUP_TIME] From 0544d94bd01aabb7b2f25d90002acc602e095ad0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Apr 2021 22:11:57 -1000 Subject: [PATCH 0067/1317] Update all systemmonitor sensors in one executor call (#48689) Co-authored-by: Paulus Schoutsen --- .../components/systemmonitor/sensor.py | 465 +++++++++++------- 1 file changed, 297 insertions(+), 168 deletions(-) diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index 038d7c6e01451..a9cb2edb4c831 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -1,8 +1,14 @@ """Support for monitoring the local system.""" +from __future__ import annotations + +import asyncio +from dataclasses import dataclass +import datetime import logging import os import socket import sys +from typing import Any, Callable, cast import psutil import voluptuous as vol @@ -10,22 +16,30 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_RESOURCES, + CONF_SCAN_INTERVAL, CONF_TYPE, DATA_GIBIBYTES, DATA_MEBIBYTES, DATA_RATE_MEGABYTES_PER_SECOND, DEVICE_CLASS_TIMESTAMP, + EVENT_HOMEASSISTANT_STOP, PERCENTAGE, STATE_OFF, STATE_ON, TEMP_CELSIUS, ) +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) +from homeassistant.helpers.entity_component import DEFAULT_SCAN_INTERVAL +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.typing import ConfigType from homeassistant.util import slugify import homeassistant.util.dt as dt_util -# mypy: allow-untyped-defs, no-check-untyped-defs - _LOGGER = logging.getLogger(__name__) CONF_ARG = "arg" @@ -35,71 +49,80 @@ else: CPU_ICON = "mdi:cpu-32-bit" +SENSOR_TYPE_NAME = 0 +SENSOR_TYPE_UOM = 1 +SENSOR_TYPE_ICON = 2 +SENSOR_TYPE_DEVICE_CLASS = 3 +SENSOR_TYPE_MANDATORY_ARG = 4 + +SIGNAL_SYSTEMMONITOR_UPDATE = "systemmonitor_update" + # Schema: [name, unit of measurement, icon, device class, flag if mandatory arg] -SENSOR_TYPES = { - "disk_free": ["Disk free", DATA_GIBIBYTES, "mdi:harddisk", None, False], - "disk_use": ["Disk use", DATA_GIBIBYTES, "mdi:harddisk", None, False], - "disk_use_percent": [ +SENSOR_TYPES: dict[str, tuple[str, str | None, str | None, str | None, bool]] = { + "disk_free": ("Disk free", DATA_GIBIBYTES, "mdi:harddisk", None, False), + "disk_use": ("Disk use", DATA_GIBIBYTES, "mdi:harddisk", None, False), + "disk_use_percent": ( "Disk use (percent)", PERCENTAGE, "mdi:harddisk", None, False, - ], - "ipv4_address": ["IPv4 address", "", "mdi:server-network", None, True], - "ipv6_address": ["IPv6 address", "", "mdi:server-network", None, True], - "last_boot": ["Last boot", None, "mdi:clock", DEVICE_CLASS_TIMESTAMP, False], - "load_15m": ["Load (15m)", " ", CPU_ICON, None, False], - "load_1m": ["Load (1m)", " ", CPU_ICON, None, False], - "load_5m": ["Load (5m)", " ", CPU_ICON, None, False], - "memory_free": ["Memory free", DATA_MEBIBYTES, "mdi:memory", None, False], - "memory_use": ["Memory use", DATA_MEBIBYTES, "mdi:memory", None, False], - "memory_use_percent": [ + ), + "ipv4_address": ("IPv4 address", "", "mdi:server-network", None, True), + "ipv6_address": ("IPv6 address", "", "mdi:server-network", None, True), + "last_boot": ("Last boot", None, "mdi:clock", DEVICE_CLASS_TIMESTAMP, False), + "load_15m": ("Load (15m)", " ", CPU_ICON, None, False), + "load_1m": ("Load (1m)", " ", CPU_ICON, None, False), + "load_5m": ("Load (5m)", " ", CPU_ICON, None, False), + "memory_free": ("Memory free", DATA_MEBIBYTES, "mdi:memory", None, False), + "memory_use": ("Memory use", DATA_MEBIBYTES, "mdi:memory", None, False), + "memory_use_percent": ( "Memory use (percent)", PERCENTAGE, "mdi:memory", None, False, - ], - "network_in": ["Network in", DATA_MEBIBYTES, "mdi:server-network", None, True], - "network_out": ["Network out", DATA_MEBIBYTES, "mdi:server-network", None, True], - "packets_in": ["Packets in", " ", "mdi:server-network", None, True], - "packets_out": ["Packets out", " ", "mdi:server-network", None, True], - "throughput_network_in": [ + ), + "network_in": ("Network in", DATA_MEBIBYTES, "mdi:server-network", None, True), + "network_out": ("Network out", DATA_MEBIBYTES, "mdi:server-network", None, True), + "packets_in": ("Packets in", " ", "mdi:server-network", None, True), + "packets_out": ("Packets out", " ", "mdi:server-network", None, True), + "throughput_network_in": ( "Network throughput in", DATA_RATE_MEGABYTES_PER_SECOND, "mdi:server-network", None, True, - ], - "throughput_network_out": [ + ), + "throughput_network_out": ( "Network throughput out", DATA_RATE_MEGABYTES_PER_SECOND, "mdi:server-network", + None, True, - ], - "process": ["Process", " ", CPU_ICON, None, True], - "processor_use": ["Processor use (percent)", PERCENTAGE, CPU_ICON, None, False], - "processor_temperature": [ + ), + "process": ("Process", " ", CPU_ICON, None, True), + "processor_use": ("Processor use (percent)", PERCENTAGE, CPU_ICON, None, False), + "processor_temperature": ( "Processor temperature", TEMP_CELSIUS, CPU_ICON, None, False, - ], - "swap_free": ["Swap free", DATA_MEBIBYTES, "mdi:harddisk", None, False], - "swap_use": ["Swap use", DATA_MEBIBYTES, "mdi:harddisk", None, False], - "swap_use_percent": ["Swap use (percent)", PERCENTAGE, "mdi:harddisk", None, False], + ), + "swap_free": ("Swap free", DATA_MEBIBYTES, "mdi:harddisk", None, False), + "swap_use": ("Swap use", DATA_MEBIBYTES, "mdi:harddisk", None, False), + "swap_use_percent": ("Swap use (percent)", PERCENTAGE, "mdi:harddisk", None, False), } -def check_required_arg(value): +def check_required_arg(value: Any) -> Any: """Validate that the required "arg" for the sensor types that need it are set.""" for sensor in value: sensor_type = sensor[CONF_TYPE] sensor_arg = sensor.get(CONF_ARG) - if sensor_arg is None and SENSOR_TYPES[sensor_type][4]: + if sensor_arg is None and SENSOR_TYPES[sensor_type][SENSOR_TYPE_MANDATORY_ARG]: raise vol.RequiredFieldInvalid( f"Mandatory 'arg' is missing for sensor type '{sensor_type}'." ) @@ -158,183 +181,289 @@ def check_required_arg(value): ] -def setup_platform(hass, config, add_entities, discovery_info=None): +@dataclass +class SensorData: + """Data for a sensor.""" + + argument: Any + state: str | None + value: Any | None + update_time: datetime.datetime | None + last_exception: BaseException | None + + +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: Callable, + discovery_info: Any | None = None, +) -> None: """Set up the system monitor sensors.""" - dev = [] + entities = [] + sensor_registry: dict[str, SensorData] = {} + for resource in config[CONF_RESOURCES]: + type_ = resource[CONF_TYPE] # Initialize the sensor argument if none was provided. # For disk monitoring default to "/" (root) to prevent runtime errors, if argument was not specified. if CONF_ARG not in resource: - resource[CONF_ARG] = "" + argument = "" if resource[CONF_TYPE].startswith("disk_"): - resource[CONF_ARG] = "/" + argument = "/" + else: + argument = resource[CONF_ARG] # Verify if we can retrieve CPU / processor temperatures. # If not, do not create the entity and add a warning to the log if ( - resource[CONF_TYPE] == "processor_temperature" - and SystemMonitorSensor.read_cpu_temperature() is None + type_ == "processor_temperature" + and await hass.async_add_executor_job(_read_cpu_temperature) is None ): _LOGGER.warning("Cannot read CPU / processor temperature information") continue - dev.append(SystemMonitorSensor(resource[CONF_TYPE], resource[CONF_ARG])) + sensor_registry[type_] = SensorData(argument, None, None, None, None) + entities.append(SystemMonitorSensor(sensor_registry, type_, argument)) + + scan_interval = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) + await async_setup_sensor_registry_updates(hass, sensor_registry, scan_interval) + + async_add_entities(entities) + + +async def async_setup_sensor_registry_updates( + hass: HomeAssistant, + sensor_registry: dict[str, SensorData], + scan_interval: datetime.timedelta, +) -> None: + """Update the registry and create polling.""" + + _update_lock = asyncio.Lock() + + def _update_sensors() -> None: + """Update sensors and store the result in the registry.""" + for type_, data in sensor_registry.items(): + try: + state, value, update_time = _update(type_, data) + except Exception as ex: # pylint: disable=broad-except + _LOGGER.exception("Error updating sensor: %s", type_, exc_info=ex) + data.last_exception = ex + else: + data.state = state + data.value = value + data.update_time = update_time + data.last_exception = None + + async def _async_update_data(*_: Any) -> None: + """Update all sensors in one executor jump.""" + if _update_lock.locked(): + _LOGGER.warning( + "Updating systemmonitor took longer than the scheduled update interval %s", + scan_interval, + ) + return + + async with _update_lock: + await hass.async_add_executor_job(_update_sensors) + async_dispatcher_send(hass, SIGNAL_SYSTEMMONITOR_UPDATE) + + polling_remover = async_track_time_interval(hass, _async_update_data, scan_interval) + + @callback + def _async_stop_polling(*_: Any) -> None: + polling_remover() - add_entities(dev, True) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop_polling) + + await _async_update_data() class SystemMonitorSensor(SensorEntity): """Implementation of a system monitor sensor.""" - def __init__(self, sensor_type, argument=""): + def __init__( + self, + sensor_registry: dict[str, SensorData], + sensor_type: str, + argument: str = "", + ) -> None: """Initialize the sensor.""" - self._name = "{} {}".format(SENSOR_TYPES[sensor_type][0], argument) - self._unique_id = slugify(f"{sensor_type}_{argument}") - self.argument = argument - self.type = sensor_type - self._state = None - self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] - self._available = True - if sensor_type in ["throughput_network_out", "throughput_network_in"]: - self._last_value = None - self._last_update_time = None + self._type: str = sensor_type + self._name: str = "{} {}".format(self.sensor_type[SENSOR_TYPE_NAME], argument) + self._unique_id: str = slugify(f"{sensor_type}_{argument}") + self._sensor_registry = sensor_registry @property - def name(self): + def name(self) -> str: """Return the name of the sensor.""" return self._name.rstrip() @property - def unique_id(self): + def unique_id(self) -> str: """Return the unique ID.""" return self._unique_id @property - def device_class(self): + def device_class(self) -> str | None: """Return the class of this sensor.""" - return SENSOR_TYPES[self.type][3] + return self.sensor_type[SENSOR_TYPE_DEVICE_CLASS] # type: ignore[no-any-return] @property - def icon(self): + def icon(self) -> str | None: """Icon to use in the frontend, if any.""" - return SENSOR_TYPES[self.type][2] + return self.sensor_type[SENSOR_TYPE_ICON] # type: ignore[no-any-return] @property - def state(self): + def state(self) -> str | None: """Return the state of the device.""" - return self._state + return self.data.state @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str | None: """Return the unit of measurement of this entity, if any.""" - return self._unit_of_measurement + return self.sensor_type[SENSOR_TYPE_UOM] # type: ignore[no-any-return] @property - def available(self): + def available(self) -> bool: """Return True if entity is available.""" - return self._available - - def update(self): - """Get the latest system information.""" - if self.type == "disk_use_percent": - self._state = psutil.disk_usage(self.argument).percent - elif self.type == "disk_use": - self._state = round(psutil.disk_usage(self.argument).used / 1024 ** 3, 1) - elif self.type == "disk_free": - self._state = round(psutil.disk_usage(self.argument).free / 1024 ** 3, 1) - elif self.type == "memory_use_percent": - self._state = psutil.virtual_memory().percent - elif self.type == "memory_use": - virtual_memory = psutil.virtual_memory() - self._state = round( - (virtual_memory.total - virtual_memory.available) / 1024 ** 2, 1 + return self.data.last_exception is None + + @property + def should_poll(self) -> bool: + """Entity does not poll.""" + return False + + @property + def sensor_type(self) -> list: + """Return sensor type data for the sensor.""" + return SENSOR_TYPES[self._type] # type: ignore + + @property + def data(self) -> SensorData: + """Return registry entry for the data.""" + return self._sensor_registry[self._type] + + async def async_added_to_hass(self) -> None: + """When entity is added to hass.""" + await super().async_added_to_hass() + self.async_on_remove( + async_dispatcher_connect( + self.hass, SIGNAL_SYSTEMMONITOR_UPDATE, self.async_write_ha_state ) - elif self.type == "memory_free": - self._state = round(psutil.virtual_memory().available / 1024 ** 2, 1) - elif self.type == "swap_use_percent": - self._state = psutil.swap_memory().percent - elif self.type == "swap_use": - self._state = round(psutil.swap_memory().used / 1024 ** 2, 1) - elif self.type == "swap_free": - self._state = round(psutil.swap_memory().free / 1024 ** 2, 1) - elif self.type == "processor_use": - self._state = round(psutil.cpu_percent(interval=None)) - elif self.type == "processor_temperature": - self._state = self.read_cpu_temperature() - elif self.type == "process": - for proc in psutil.process_iter(): - try: - if self.argument == proc.name(): - self._state = STATE_ON - return - except psutil.NoSuchProcess as err: - _LOGGER.warning( - "Failed to load process with ID: %s, old name: %s", - err.pid, - err.name, - ) - self._state = STATE_OFF - elif self.type in ["network_out", "network_in"]: - counters = psutil.net_io_counters(pernic=True) - if self.argument in counters: - counter = counters[self.argument][IO_COUNTER[self.type]] - self._state = round(counter / 1024 ** 2, 1) - else: - self._state = None - elif self.type in ["packets_out", "packets_in"]: - counters = psutil.net_io_counters(pernic=True) - if self.argument in counters: - self._state = counters[self.argument][IO_COUNTER[self.type]] - else: - self._state = None - elif self.type in ["throughput_network_out", "throughput_network_in"]: - counters = psutil.net_io_counters(pernic=True) - if self.argument in counters: - counter = counters[self.argument][IO_COUNTER[self.type]] - now = dt_util.utcnow() - if self._last_value and self._last_value < counter: - self._state = round( - (counter - self._last_value) - / 1000 ** 2 - / (now - self._last_update_time).seconds, - 3, - ) - else: - self._state = None - self._last_update_time = now - self._last_value = counter - else: - self._state = None - elif self.type in ["ipv4_address", "ipv6_address"]: - addresses = psutil.net_if_addrs() - if self.argument in addresses: - for addr in addresses[self.argument]: - if addr.family == IF_ADDRS_FAMILY[self.type]: - self._state = addr.address + ) + + +def _update( + type_: str, data: SensorData +) -> tuple[str | None, str | None, datetime.datetime | None]: + """Get the latest system information.""" + state = None + value = None + update_time = None + + if type_ == "disk_use_percent": + state = psutil.disk_usage(data.argument).percent + elif type_ == "disk_use": + state = round(psutil.disk_usage(data.argument).used / 1024 ** 3, 1) + elif type_ == "disk_free": + state = round(psutil.disk_usage(data.argument).free / 1024 ** 3, 1) + elif type_ == "memory_use_percent": + state = psutil.virtual_memory().percent + elif type_ == "memory_use": + virtual_memory = psutil.virtual_memory() + state = round((virtual_memory.total - virtual_memory.available) / 1024 ** 2, 1) + elif type_ == "memory_free": + state = round(psutil.virtual_memory().available / 1024 ** 2, 1) + elif type_ == "swap_use_percent": + state = psutil.swap_memory().percent + elif type_ == "swap_use": + state = round(psutil.swap_memory().used / 1024 ** 2, 1) + elif type_ == "swap_free": + state = round(psutil.swap_memory().free / 1024 ** 2, 1) + elif type_ == "processor_use": + state = round(psutil.cpu_percent(interval=None)) + elif type_ == "processor_temperature": + state = _read_cpu_temperature() + elif type_ == "process": + state = STATE_OFF + for proc in psutil.process_iter(): + try: + if data.argument == proc.name(): + state = STATE_ON + break + except psutil.NoSuchProcess as err: + _LOGGER.warning( + "Failed to load process with ID: %s, old name: %s", + err.pid, + err.name, + ) + elif type_ in ["network_out", "network_in"]: + counters = psutil.net_io_counters(pernic=True) + if data.argument in counters: + counter = counters[data.argument][IO_COUNTER[type_]] + state = round(counter / 1024 ** 2, 1) + else: + state = None + elif type_ in ["packets_out", "packets_in"]: + counters = psutil.net_io_counters(pernic=True) + if data.argument in counters: + state = counters[data.argument][IO_COUNTER[type_]] + else: + state = None + elif type_ in ["throughput_network_out", "throughput_network_in"]: + counters = psutil.net_io_counters(pernic=True) + if data.argument in counters: + counter = counters[data.argument][IO_COUNTER[type_]] + now = dt_util.utcnow() + if data.value and data.value < counter: + state = round( + (counter - data.value) + / 1000 ** 2 + / (now - (data.update_time or now)).seconds, + 3, + ) else: - self._state = None - elif self.type == "last_boot": - # Only update on initial setup - if self._state is None: - self._state = dt_util.as_local( - dt_util.utc_from_timestamp(psutil.boot_time()) - ).isoformat() - elif self.type == "load_1m": - self._state = round(os.getloadavg()[0], 2) - elif self.type == "load_5m": - self._state = round(os.getloadavg()[1], 2) - elif self.type == "load_15m": - self._state = round(os.getloadavg()[2], 2) - - @staticmethod - def read_cpu_temperature(): - """Attempt to read CPU / processor temperature.""" - temps = psutil.sensors_temperatures() - - for name, entries in temps.items(): - for i, entry in enumerate(entries, start=1): - # In case the label is empty (e.g. on Raspberry PI 4), - # construct it ourself here based on the sensor key name. - _label = f"{name} {i}" if not entry.label else entry.label - if _label in CPU_SENSOR_PREFIXES: - return round(entry.current, 1) + state = None + update_time = now + value = counter + else: + state = None + elif type_ in ["ipv4_address", "ipv6_address"]: + addresses = psutil.net_if_addrs() + if data.argument in addresses: + for addr in addresses[data.argument]: + if addr.family == IF_ADDRS_FAMILY[type_]: + state = addr.address + else: + state = None + elif type_ == "last_boot": + # Only update on initial setup + if data.state is None: + state = dt_util.as_local( + dt_util.utc_from_timestamp(psutil.boot_time()) + ).isoformat() + else: + state = data.state + elif type_ == "load_1m": + state = round(os.getloadavg()[0], 2) + elif type_ == "load_5m": + state = round(os.getloadavg()[1], 2) + elif type_ == "load_15m": + state = round(os.getloadavg()[2], 2) + + return state, value, update_time + + +def _read_cpu_temperature() -> float | None: + """Attempt to read CPU / processor temperature.""" + temps = psutil.sensors_temperatures() + + for name, entries in temps.items(): + for i, entry in enumerate(entries, start=1): + # In case the label is empty (e.g. on Raspberry PI 4), + # construct it ourself here based on the sensor key name. + _label = f"{name} {i}" if not entry.label else entry.label + if _label in CPU_SENSOR_PREFIXES: + return cast(float, round(entry.current, 1)) + + return None From 0f757c3db21c02747668bac89d03715b3fb106f2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 5 Apr 2021 03:22:25 -0700 Subject: [PATCH 0068/1317] Fix verisure deadlock (#48691) --- homeassistant/components/verisure/camera.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/verisure/camera.py b/homeassistant/components/verisure/camera.py index c97d7f8c76c4b..cb159027c16b0 100644 --- a/homeassistant/components/verisure/camera.py +++ b/homeassistant/components/verisure/camera.py @@ -36,7 +36,7 @@ async def async_setup_entry( assert hass.config.config_dir async_add_entities( - VerisureSmartcam(hass, coordinator, serial_number, hass.config.config_dir) + VerisureSmartcam(coordinator, serial_number, hass.config.config_dir) for serial_number in coordinator.data["cameras"] ) @@ -48,7 +48,6 @@ class VerisureSmartcam(CoordinatorEntity, Camera): def __init__( self, - hass: HomeAssistant, coordinator: VerisureDataUpdateCoordinator, serial_number: str, directory_path: str, @@ -60,7 +59,6 @@ def __init__( self._directory_path = directory_path self._image = None self._image_id = None - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, self.delete_image) @property def name(self) -> str: @@ -126,7 +124,7 @@ def check_imagelist(self) -> None: self._image_id = new_image_id self._image = new_image_path - def delete_image(self) -> None: + def delete_image(self, _=None) -> None: """Delete an old image.""" remove_image = os.path.join( self._directory_path, "{}{}".format(self._image_id, ".jpg") @@ -145,3 +143,8 @@ def capture_smartcam(self) -> None: LOGGER.debug("Capturing new image from %s", self.serial_number) except VerisureError as ex: LOGGER.error("Could not capture image, %s", ex) + + async def async_added_to_hass(self) -> None: + """Entity added to Home Assistant.""" + await super().async_added_to_hass() + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.delete_image) From d0b3f76a6f2b78e89b792d2880f3f873bee3eaf6 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 5 Apr 2021 13:39:39 -0400 Subject: [PATCH 0069/1317] Add ClimaCell v4 API support (#47575) * Add ClimaCell v4 API support * fix tests * use constants * fix logic and update tests * revert accidental changes and enable hourly and nowcast forecast entities in test * use variable instead of accessing dictionary multiple times * only grab necessary fields * add _translate_condition method ot base class * bump pyclimacell again to fix bug * switch typehints back to new format * more typehint fixes * fix tests * revert merge conflict change * handle 'migration' in async_setup_entry so we don't have to bump config entry versions * parametrize timestep test --- .coveragerc | 1 - .../components/climacell/__init__.py | 218 +- .../components/climacell/config_flow.py | 34 +- homeassistant/components/climacell/const.py | 94 +- .../components/climacell/manifest.json | 2 +- .../components/climacell/strings.json | 4 +- homeassistant/components/climacell/weather.py | 364 ++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/climacell/conftest.py | 27 +- tests/components/climacell/const.py | 31 +- .../components/climacell/test_config_flow.py | 44 +- tests/components/climacell/test_init.py | 61 +- tests/components/climacell/test_weather.py | 382 +++ .../fixtures/climacell/v3_forecast_daily.json | 992 +++++++ .../climacell/v3_forecast_hourly.json | 752 ++++++ .../climacell/v3_forecast_nowcast.json | 782 ++++++ tests/fixtures/climacell/v3_realtime.json | 38 + tests/fixtures/climacell/v4.json | 2360 +++++++++++++++++ 19 files changed, 5972 insertions(+), 218 deletions(-) create mode 100644 tests/components/climacell/test_weather.py create mode 100644 tests/fixtures/climacell/v3_forecast_daily.json create mode 100644 tests/fixtures/climacell/v3_forecast_hourly.json create mode 100644 tests/fixtures/climacell/v3_forecast_nowcast.json create mode 100644 tests/fixtures/climacell/v3_realtime.json create mode 100644 tests/fixtures/climacell/v4.json diff --git a/.coveragerc b/.coveragerc index f3cdf62ff73ca..2dcf43ef6975e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -145,7 +145,6 @@ omit = homeassistant/components/clickatell/notify.py homeassistant/components/clicksend/notify.py homeassistant/components/clicksend_tts/notify.py - homeassistant/components/climacell/weather.py homeassistant/components/cmus/media_player.py homeassistant/components/co2signal/* homeassistant/components/coinbase/* diff --git a/homeassistant/components/climacell/__init__.py b/homeassistant/components/climacell/__init__.py index 1498c51f54a63..8095f7991bde5 100644 --- a/homeassistant/components/climacell/__init__.py +++ b/homeassistant/components/climacell/__init__.py @@ -7,14 +7,9 @@ from math import ceil from typing import Any -from pyclimacell import ClimaCell -from pyclimacell.const import ( - FORECAST_DAILY, - FORECAST_HOURLY, - FORECAST_NOWCAST, - REALTIME, -) -from pyclimacell.pyclimacell import ( +from pyclimacell import ClimaCellV3, ClimaCellV4 +from pyclimacell.const import CURRENT, DAILY, FORECASTS, HOURLY, NOWCAST +from pyclimacell.exceptions import ( CantConnectException, InvalidAPIKeyException, RateLimitedException, @@ -23,7 +18,13 @@ from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.const import ( + CONF_API_KEY, + CONF_API_VERSION, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_NAME, +) from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import ( @@ -34,15 +35,34 @@ from .const import ( ATTRIBUTION, + CC_ATTR_CONDITION, + CC_ATTR_HUMIDITY, + CC_ATTR_OZONE, + CC_ATTR_PRECIPITATION, + CC_ATTR_PRECIPITATION_PROBABILITY, + CC_ATTR_PRESSURE, + CC_ATTR_TEMPERATURE, + CC_ATTR_TEMPERATURE_HIGH, + CC_ATTR_TEMPERATURE_LOW, + CC_ATTR_VISIBILITY, + CC_ATTR_WIND_DIRECTION, + CC_ATTR_WIND_SPEED, + CC_V3_ATTR_CONDITION, + CC_V3_ATTR_HUMIDITY, + CC_V3_ATTR_OZONE, + CC_V3_ATTR_PRECIPITATION, + CC_V3_ATTR_PRECIPITATION_DAILY, + CC_V3_ATTR_PRECIPITATION_PROBABILITY, + CC_V3_ATTR_PRESSURE, + CC_V3_ATTR_TEMPERATURE, + CC_V3_ATTR_VISIBILITY, + CC_V3_ATTR_WIND_DIRECTION, + CC_V3_ATTR_WIND_SPEED, CONF_TIMESTEP, - CURRENT, - DAILY, + DEFAULT_FORECAST_TYPE, DEFAULT_TIMESTEP, DOMAIN, - FORECASTS, - HOURLY, MAX_REQUESTS_PER_DAY, - NOWCAST, ) _LOGGER = logging.getLogger(__name__) @@ -54,6 +74,7 @@ def _set_update_interval( hass: HomeAssistantType, current_entry: ConfigEntry ) -> timedelta: """Recalculate update_interval based on existing ClimaCell instances and update them.""" + api_calls = 4 if current_entry.data[CONF_API_VERSION] == 3 else 2 # We check how many ClimaCell configured instances are using the same API key and # calculate interval to not exceed allowed numbers of requests. Divide 90% of # MAX_REQUESTS_PER_DAY by 4 because every update requires four API calls and we want @@ -68,7 +89,7 @@ def _set_update_interval( interval = timedelta( minutes=( ceil( - (24 * 60 * (len(other_instance_entry_ids) + 1) * 4) + (24 * 60 * (len(other_instance_entry_ids) + 1) * api_calls) / (MAX_REQUESTS_PER_DAY * 0.9) ) ) @@ -85,24 +106,48 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) """Set up ClimaCell API from a config entry.""" hass.data.setdefault(DOMAIN, {}) + params = {} # If config entry options not set up, set them up if not config_entry.options: - hass.config_entries.async_update_entry( - config_entry, - options={ - CONF_TIMESTEP: DEFAULT_TIMESTEP, - }, - ) + params["options"] = { + CONF_TIMESTEP: DEFAULT_TIMESTEP, + } + else: + # Use valid timestep if it's invalid + timestep = config_entry.options[CONF_TIMESTEP] + if timestep not in (1, 5, 15, 30): + if timestep <= 2: + timestep = 1 + elif timestep <= 7: + timestep = 5 + elif timestep <= 20: + timestep = 15 + else: + timestep = 30 + new_options = config_entry.options.copy() + new_options[CONF_TIMESTEP] = timestep + params["options"] = new_options + # Add API version if not found + if CONF_API_VERSION not in config_entry.data: + new_data = config_entry.data.copy() + new_data[CONF_API_VERSION] = 3 + params["data"] = new_data + + if params: + hass.config_entries.async_update_entry(config_entry, **params) + + api_class = ClimaCellV3 if config_entry.data[CONF_API_VERSION] == 3 else ClimaCellV4 + api = api_class( + config_entry.data[CONF_API_KEY], + config_entry.data.get(CONF_LATITUDE, hass.config.latitude), + config_entry.data.get(CONF_LONGITUDE, hass.config.longitude), + session=async_get_clientsession(hass), + ) coordinator = ClimaCellDataUpdateCoordinator( hass, config_entry, - ClimaCell( - config_entry.data[CONF_API_KEY], - config_entry.data.get(CONF_LATITUDE, hass.config.latitude), - config_entry.data.get(CONF_LONGITUDE, hass.config.longitude), - session=async_get_clientsession(hass), - ), + api, _set_update_interval(hass, config_entry), ) @@ -145,12 +190,13 @@ def __init__( self, hass: HomeAssistantType, config_entry: ConfigEntry, - api: ClimaCell, + api: ClimaCellV3 | ClimaCellV4, update_interval: timedelta, ) -> None: """Initialize.""" self._config_entry = config_entry + self._api_version = config_entry.data[CONF_API_VERSION] self._api = api self.name = config_entry.data[CONF_NAME] self.data = {CURRENT: {}, FORECASTS: {}} @@ -166,27 +212,81 @@ async def _async_update_data(self) -> dict[str, Any]: """Update data via library.""" data = {FORECASTS: {}} try: - data[CURRENT] = await self._api.realtime( - self._api.available_fields(REALTIME) - ) - data[FORECASTS][HOURLY] = await self._api.forecast_hourly( - self._api.available_fields(FORECAST_HOURLY), - None, - timedelta(hours=24), - ) - - data[FORECASTS][DAILY] = await self._api.forecast_daily( - self._api.available_fields(FORECAST_DAILY), None, timedelta(days=14) - ) - - data[FORECASTS][NOWCAST] = await self._api.forecast_nowcast( - self._api.available_fields(FORECAST_NOWCAST), - None, - timedelta( - minutes=min(300, self._config_entry.options[CONF_TIMESTEP] * 30) - ), - self._config_entry.options[CONF_TIMESTEP], - ) + if self._api_version == 3: + data[CURRENT] = await self._api.realtime( + [ + CC_V3_ATTR_TEMPERATURE, + CC_V3_ATTR_HUMIDITY, + CC_V3_ATTR_PRESSURE, + CC_V3_ATTR_WIND_SPEED, + CC_V3_ATTR_WIND_DIRECTION, + CC_V3_ATTR_CONDITION, + CC_V3_ATTR_VISIBILITY, + CC_V3_ATTR_OZONE, + ] + ) + data[FORECASTS][HOURLY] = await self._api.forecast_hourly( + [ + CC_V3_ATTR_TEMPERATURE, + CC_V3_ATTR_WIND_SPEED, + CC_V3_ATTR_WIND_DIRECTION, + CC_V3_ATTR_CONDITION, + CC_V3_ATTR_PRECIPITATION, + CC_V3_ATTR_PRECIPITATION_PROBABILITY, + ], + None, + timedelta(hours=24), + ) + + data[FORECASTS][DAILY] = await self._api.forecast_daily( + [ + CC_V3_ATTR_TEMPERATURE, + CC_V3_ATTR_WIND_SPEED, + CC_V3_ATTR_WIND_DIRECTION, + CC_V3_ATTR_CONDITION, + CC_V3_ATTR_PRECIPITATION_DAILY, + CC_V3_ATTR_PRECIPITATION_PROBABILITY, + ], + None, + timedelta(days=14), + ) + + data[FORECASTS][NOWCAST] = await self._api.forecast_nowcast( + [ + CC_V3_ATTR_TEMPERATURE, + CC_V3_ATTR_WIND_SPEED, + CC_V3_ATTR_WIND_DIRECTION, + CC_V3_ATTR_CONDITION, + CC_V3_ATTR_PRECIPITATION, + ], + None, + timedelta( + minutes=min(300, self._config_entry.options[CONF_TIMESTEP] * 30) + ), + self._config_entry.options[CONF_TIMESTEP], + ) + else: + return await self._api.realtime_and_all_forecasts( + [ + CC_ATTR_TEMPERATURE, + CC_ATTR_HUMIDITY, + CC_ATTR_PRESSURE, + CC_ATTR_WIND_SPEED, + CC_ATTR_WIND_DIRECTION, + CC_ATTR_CONDITION, + CC_ATTR_VISIBILITY, + CC_ATTR_OZONE, + ], + [ + CC_ATTR_TEMPERATURE_LOW, + CC_ATTR_TEMPERATURE_HIGH, + CC_ATTR_WIND_SPEED, + CC_ATTR_WIND_DIRECTION, + CC_ATTR_CONDITION, + CC_ATTR_PRECIPITATION, + CC_ATTR_PRECIPITATION_PROBABILITY, + ], + ) except ( CantConnectException, InvalidAPIKeyException, @@ -202,10 +302,16 @@ class ClimaCellEntity(CoordinatorEntity): """Base ClimaCell Entity.""" def __init__( - self, config_entry: ConfigEntry, coordinator: ClimaCellDataUpdateCoordinator + self, + config_entry: ConfigEntry, + coordinator: ClimaCellDataUpdateCoordinator, + forecast_type: str, + api_version: int, ) -> None: """Initialize ClimaCell Entity.""" super().__init__(coordinator) + self.api_version = api_version + self.forecast_type = forecast_type self._config_entry = config_entry @staticmethod @@ -229,15 +335,23 @@ def _get_cc_value( return items.get("value") + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + if self.forecast_type == DEFAULT_FORECAST_TYPE: + return True + + return False + @property def name(self) -> str: """Return the name of the entity.""" - return self._config_entry.data[CONF_NAME] + return f"{self._config_entry.data[CONF_NAME]} - {self.forecast_type.title()}" @property def unique_id(self) -> str: """Return the unique id of the entity.""" - return self._config_entry.unique_id + return f"{self._config_entry.unique_id}_{self.forecast_type}" @property def attribution(self): diff --git a/homeassistant/components/climacell/config_flow.py b/homeassistant/components/climacell/config_flow.py index ebf63abcae4b3..1457479e62aca 100644 --- a/homeassistant/components/climacell/config_flow.py +++ b/homeassistant/components/climacell/config_flow.py @@ -4,23 +4,36 @@ import logging from typing import Any -from pyclimacell import ClimaCell -from pyclimacell.const import REALTIME +from pyclimacell import ClimaCellV3 from pyclimacell.exceptions import ( CantConnectException, InvalidAPIKeyException, RateLimitedException, ) +from pyclimacell.pyclimacell import ClimaCellV4 import voluptuous as vol from homeassistant import config_entries, core -from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.const import ( + CONF_API_KEY, + CONF_API_VERSION, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_NAME, +) from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import HomeAssistantType -from .const import CONF_TIMESTEP, DEFAULT_NAME, DEFAULT_TIMESTEP, DOMAIN +from .const import ( + CC_ATTR_TEMPERATURE, + CC_V3_ATTR_TEMPERATURE, + CONF_TIMESTEP, + DEFAULT_NAME, + DEFAULT_TIMESTEP, + DOMAIN, +) _LOGGER = logging.getLogger(__name__) @@ -43,6 +56,7 @@ def _get_config_schema( CONF_NAME, default=input_dict.get(CONF_NAME, DEFAULT_NAME) ): str, vol.Required(CONF_API_KEY, default=input_dict.get(CONF_API_KEY)): str, + vol.Required(CONF_API_VERSION, default=4): vol.In([3, 4]), vol.Inclusive( CONF_LATITUDE, "location", @@ -85,7 +99,7 @@ async def async_step_init( vol.Required( CONF_TIMESTEP, default=self._config_entry.options.get(CONF_TIMESTEP, DEFAULT_TIMESTEP), - ): vol.All(vol.Coerce(int), vol.Range(min=1, max=60)), + ): vol.In([1, 5, 15, 30]), } return self.async_show_form( @@ -119,12 +133,18 @@ async def async_step_user( self._abort_if_unique_id_configured() try: - await ClimaCell( + if user_input[CONF_API_VERSION] == 3: + api_class = ClimaCellV3 + field = CC_V3_ATTR_TEMPERATURE + else: + api_class = ClimaCellV4 + field = CC_ATTR_TEMPERATURE + await api_class( user_input[CONF_API_KEY], str(user_input.get(CONF_LATITUDE, self.hass.config.latitude)), str(user_input.get(CONF_LONGITUDE, self.hass.config.longitude)), session=async_get_clientsession(self.hass), - ).realtime(ClimaCell.first_field(REALTIME)) + ).realtime([field]) return self.async_create_entry( title=user_input[CONF_NAME], data=user_input diff --git a/homeassistant/components/climacell/const.py b/homeassistant/components/climacell/const.py index f2d0a59612190..01d85dcc16136 100644 --- a/homeassistant/components/climacell/const.py +++ b/homeassistant/components/climacell/const.py @@ -1,4 +1,5 @@ """Constants for the ClimaCell integration.""" +from pyclimacell.const import DAILY, HOURLY, NOWCAST, WeatherCode from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, @@ -16,15 +17,8 @@ ) CONF_TIMESTEP = "timestep" - -DAILY = "daily" -HOURLY = "hourly" -NOWCAST = "nowcast" FORECAST_TYPES = [DAILY, HOURLY, NOWCAST] -CURRENT = "current" -FORECASTS = "forecasts" - DEFAULT_NAME = "ClimaCell" DEFAULT_TIMESTEP = 15 DEFAULT_FORECAST_TYPE = DAILY @@ -33,7 +27,58 @@ MAX_REQUESTS_PER_DAY = 1000 +CLEAR_CONDITIONS = {"night": ATTR_CONDITION_CLEAR_NIGHT, "day": ATTR_CONDITION_SUNNY} + +MAX_FORECASTS = { + DAILY: 14, + HOURLY: 24, + NOWCAST: 30, +} + +# V4 constants CONDITIONS = { + WeatherCode.WIND: ATTR_CONDITION_WINDY, + WeatherCode.LIGHT_WIND: ATTR_CONDITION_WINDY, + WeatherCode.STRONG_WIND: ATTR_CONDITION_WINDY, + WeatherCode.FREEZING_RAIN: ATTR_CONDITION_SNOWY_RAINY, + WeatherCode.HEAVY_FREEZING_RAIN: ATTR_CONDITION_SNOWY_RAINY, + WeatherCode.LIGHT_FREEZING_RAIN: ATTR_CONDITION_SNOWY_RAINY, + WeatherCode.FREEZING_DRIZZLE: ATTR_CONDITION_SNOWY_RAINY, + WeatherCode.ICE_PELLETS: ATTR_CONDITION_HAIL, + WeatherCode.HEAVY_ICE_PELLETS: ATTR_CONDITION_HAIL, + WeatherCode.LIGHT_ICE_PELLETS: ATTR_CONDITION_HAIL, + WeatherCode.SNOW: ATTR_CONDITION_SNOWY, + WeatherCode.HEAVY_SNOW: ATTR_CONDITION_SNOWY, + WeatherCode.LIGHT_SNOW: ATTR_CONDITION_SNOWY, + WeatherCode.FLURRIES: ATTR_CONDITION_SNOWY, + WeatherCode.THUNDERSTORM: ATTR_CONDITION_LIGHTNING, + WeatherCode.RAIN: ATTR_CONDITION_POURING, + WeatherCode.HEAVY_RAIN: ATTR_CONDITION_RAINY, + WeatherCode.LIGHT_RAIN: ATTR_CONDITION_RAINY, + WeatherCode.DRIZZLE: ATTR_CONDITION_RAINY, + WeatherCode.FOG: ATTR_CONDITION_FOG, + WeatherCode.LIGHT_FOG: ATTR_CONDITION_FOG, + WeatherCode.CLOUDY: ATTR_CONDITION_CLOUDY, + WeatherCode.MOSTLY_CLOUDY: ATTR_CONDITION_CLOUDY, + WeatherCode.PARTLY_CLOUDY: ATTR_CONDITION_PARTLYCLOUDY, +} + +CC_ATTR_TIMESTAMP = "startTime" +CC_ATTR_TEMPERATURE = "temperature" +CC_ATTR_TEMPERATURE_HIGH = "temperatureMax" +CC_ATTR_TEMPERATURE_LOW = "temperatureMin" +CC_ATTR_PRESSURE = "pressureSeaLevel" +CC_ATTR_HUMIDITY = "humidity" +CC_ATTR_WIND_SPEED = "windSpeed" +CC_ATTR_WIND_DIRECTION = "windDirection" +CC_ATTR_OZONE = "pollutantO3" +CC_ATTR_CONDITION = "weatherCode" +CC_ATTR_VISIBILITY = "visibility" +CC_ATTR_PRECIPITATION = "precipitationIntensityAvg" +CC_ATTR_PRECIPITATION_PROBABILITY = "precipitationProbability" + +# V3 constants +CONDITIONS_V3 = { "breezy": ATTR_CONDITION_WINDY, "freezing_rain_heavy": ATTR_CONDITION_SNOWY_RAINY, "freezing_rain": ATTR_CONDITION_SNOWY_RAINY, @@ -58,24 +103,17 @@ "partly_cloudy": ATTR_CONDITION_PARTLYCLOUDY, } -CLEAR_CONDITIONS = {"night": ATTR_CONDITION_CLEAR_NIGHT, "day": ATTR_CONDITION_SUNNY} - -CC_ATTR_TIMESTAMP = "observation_time" -CC_ATTR_TEMPERATURE = "temp" -CC_ATTR_TEMPERATURE_HIGH = "max" -CC_ATTR_TEMPERATURE_LOW = "min" -CC_ATTR_PRESSURE = "baro_pressure" -CC_ATTR_HUMIDITY = "humidity" -CC_ATTR_WIND_SPEED = "wind_speed" -CC_ATTR_WIND_DIRECTION = "wind_direction" -CC_ATTR_OZONE = "o3" -CC_ATTR_CONDITION = "weather_code" -CC_ATTR_VISIBILITY = "visibility" -CC_ATTR_PRECIPITATION = "precipitation" -CC_ATTR_PRECIPITATION_DAILY = "precipitation_accumulation" -CC_ATTR_PRECIPITATION_PROBABILITY = "precipitation_probability" -CC_ATTR_PM_2_5 = "pm25" -CC_ATTR_PM_10 = "pm10" -CC_ATTR_CARBON_MONOXIDE = "co" -CC_ATTR_SULPHUR_DIOXIDE = "so2" -CC_ATTR_NITROGEN_DIOXIDE = "no2" +CC_V3_ATTR_TIMESTAMP = "observation_time" +CC_V3_ATTR_TEMPERATURE = "temp" +CC_V3_ATTR_TEMPERATURE_HIGH = "max" +CC_V3_ATTR_TEMPERATURE_LOW = "min" +CC_V3_ATTR_PRESSURE = "baro_pressure" +CC_V3_ATTR_HUMIDITY = "humidity" +CC_V3_ATTR_WIND_SPEED = "wind_speed" +CC_V3_ATTR_WIND_DIRECTION = "wind_direction" +CC_V3_ATTR_OZONE = "o3" +CC_V3_ATTR_CONDITION = "weather_code" +CC_V3_ATTR_VISIBILITY = "visibility" +CC_V3_ATTR_PRECIPITATION = "precipitation" +CC_V3_ATTR_PRECIPITATION_DAILY = "precipitation_accumulation" +CC_V3_ATTR_PRECIPITATION_PROBABILITY = "precipitation_probability" diff --git a/homeassistant/components/climacell/manifest.json b/homeassistant/components/climacell/manifest.json index f410c2275a970..1df0b3613bba5 100644 --- a/homeassistant/components/climacell/manifest.json +++ b/homeassistant/components/climacell/manifest.json @@ -3,6 +3,6 @@ "name": "ClimaCell", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/climacell", - "requirements": ["pyclimacell==0.14.0"], + "requirements": ["pyclimacell==0.18.0"], "codeowners": ["@raman325"] } diff --git a/homeassistant/components/climacell/strings.json b/homeassistant/components/climacell/strings.json index be80ac4e506a9..f4347d254b704 100644 --- a/homeassistant/components/climacell/strings.json +++ b/homeassistant/components/climacell/strings.json @@ -7,6 +7,7 @@ "data": { "name": "[%key:common::config_flow::data::name%]", "api_key": "[%key:common::config_flow::data::api_key%]", + "api_version": "API Version", "latitude": "[%key:common::config_flow::data::latitude%]", "longitude": "[%key:common::config_flow::data::longitude%]" } @@ -25,8 +26,7 @@ "title": "Update [%key:component::climacell::title%] Options", "description": "If you choose to enable the `nowcast` forecast entity, you can configure the number of minutes between each forecast. The number of forecasts provided depends on the number of minutes chosen between forecasts.", "data": { - "timestep": "Min. Between NowCast Forecasts", - "forecast_types": "Forecast Type(s)" + "timestep": "Min. Between NowCast Forecasts" } } } diff --git a/homeassistant/components/climacell/weather.py b/homeassistant/components/climacell/weather.py index b9da5431dd058..012f987171ed3 100644 --- a/homeassistant/components/climacell/weather.py +++ b/homeassistant/components/climacell/weather.py @@ -5,6 +5,8 @@ import logging from typing import Any, Callable +from pyclimacell.const import CURRENT, DAILY, FORECASTS, HOURLY, NOWCAST, WeatherCode + from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, ATTR_FORECAST_PRECIPITATION, @@ -18,6 +20,7 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + CONF_API_VERSION, LENGTH_FEET, LENGTH_KILOMETERS, LENGTH_METERS, @@ -33,13 +36,12 @@ from homeassistant.util.distance import convert as distance_convert from homeassistant.util.pressure import convert as pressure_convert -from . import ClimaCellDataUpdateCoordinator, ClimaCellEntity +from . import ClimaCellEntity from .const import ( CC_ATTR_CONDITION, CC_ATTR_HUMIDITY, CC_ATTR_OZONE, CC_ATTR_PRECIPITATION, - CC_ATTR_PRECIPITATION_DAILY, CC_ATTR_PRECIPITATION_PROBABILITY, CC_ATTR_PRESSURE, CC_ATTR_TEMPERATURE, @@ -49,16 +51,26 @@ CC_ATTR_VISIBILITY, CC_ATTR_WIND_DIRECTION, CC_ATTR_WIND_SPEED, + CC_V3_ATTR_CONDITION, + CC_V3_ATTR_HUMIDITY, + CC_V3_ATTR_OZONE, + CC_V3_ATTR_PRECIPITATION, + CC_V3_ATTR_PRECIPITATION_DAILY, + CC_V3_ATTR_PRECIPITATION_PROBABILITY, + CC_V3_ATTR_PRESSURE, + CC_V3_ATTR_TEMPERATURE, + CC_V3_ATTR_TEMPERATURE_HIGH, + CC_V3_ATTR_TEMPERATURE_LOW, + CC_V3_ATTR_TIMESTAMP, + CC_V3_ATTR_VISIBILITY, + CC_V3_ATTR_WIND_DIRECTION, + CC_V3_ATTR_WIND_SPEED, CLEAR_CONDITIONS, CONDITIONS, + CONDITIONS_V3, CONF_TIMESTEP, - CURRENT, - DAILY, - DEFAULT_FORECAST_TYPE, DOMAIN, - FORECASTS, - HOURLY, - NOWCAST, + MAX_FORECASTS, ) # mypy: allow-untyped-defs, no-check-untyped-defs @@ -66,57 +78,6 @@ _LOGGER = logging.getLogger(__name__) -def _translate_condition(condition: str | None, sun_is_up: bool = True) -> str | None: - """Translate ClimaCell condition into an HA condition.""" - if not condition: - return None - if "clear" in condition.lower(): - if sun_is_up: - return CLEAR_CONDITIONS["day"] - return CLEAR_CONDITIONS["night"] - return CONDITIONS[condition] - - -def _forecast_dict( - hass: HomeAssistantType, - forecast_dt: datetime, - use_datetime: bool, - condition: str, - precipitation: float | None, - precipitation_probability: float | None, - temp: float | None, - temp_low: float | None, - wind_direction: float | None, - wind_speed: float | None, -) -> dict[str, Any]: - """Return formatted Forecast dict from ClimaCell forecast data.""" - if use_datetime: - translated_condition = _translate_condition(condition, is_up(hass, forecast_dt)) - else: - translated_condition = _translate_condition(condition, True) - - if hass.config.units.is_metric: - if precipitation: - precipitation = ( - distance_convert(precipitation / 12, LENGTH_FEET, LENGTH_METERS) * 1000 - ) - if wind_speed: - wind_speed = distance_convert(wind_speed, LENGTH_MILES, LENGTH_KILOMETERS) - - data = { - ATTR_FORECAST_TIME: forecast_dt.isoformat(), - ATTR_FORECAST_CONDITION: translated_condition, - ATTR_FORECAST_PRECIPITATION: precipitation, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: precipitation_probability, - ATTR_FORECAST_TEMP: temp, - ATTR_FORECAST_TEMP_LOW: temp_low, - ATTR_FORECAST_WIND_BEARING: wind_direction, - ATTR_FORECAST_WIND_SPEED: wind_speed, - } - - return {k: v for k, v in data.items() if v is not None} - - async def async_setup_entry( hass: HomeAssistantType, config_entry: ConfigEntry, @@ -124,49 +85,238 @@ async def async_setup_entry( ) -> None: """Set up a config entry.""" coordinator = hass.data[DOMAIN][config_entry.entry_id] + api_version = config_entry.data[CONF_API_VERSION] + api_class = ClimaCellV3WeatherEntity if api_version == 3 else ClimaCellWeatherEntity entities = [ - ClimaCellWeatherEntity(config_entry, coordinator, forecast_type) + api_class(config_entry, coordinator, forecast_type, api_version) for forecast_type in [DAILY, HOURLY, NOWCAST] ] async_add_entities(entities) -class ClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity): - """Entity that talks to ClimaCell API to retrieve weather data.""" +class BaseClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity): + """Base ClimaCell weather entity.""" + + @staticmethod + def _translate_condition( + condition: int | None, sun_is_up: bool = True + ) -> str | None: + """Translate ClimaCell condition into an HA condition.""" + raise NotImplementedError() - def __init__( + def _forecast_dict( self, - config_entry: ConfigEntry, - coordinator: ClimaCellDataUpdateCoordinator, - forecast_type: str, - ) -> None: - """Initialize ClimaCell weather entity.""" - super().__init__(config_entry, coordinator) - self.forecast_type = forecast_type + forecast_dt: datetime, + use_datetime: bool, + condition: str, + precipitation: float | None, + precipitation_probability: float | None, + temp: float | None, + temp_low: float | None, + wind_direction: float | None, + wind_speed: float | None, + ) -> dict[str, Any]: + """Return formatted Forecast dict from ClimaCell forecast data.""" + if use_datetime: + translated_condition = self._translate_condition( + condition, is_up(self.hass, forecast_dt) + ) + else: + translated_condition = self._translate_condition(condition, True) + + if self.hass.config.units.is_metric: + if precipitation: + precipitation = ( + distance_convert(precipitation / 12, LENGTH_FEET, LENGTH_METERS) + * 1000 + ) + if wind_speed: + wind_speed = distance_convert( + wind_speed, LENGTH_MILES, LENGTH_KILOMETERS + ) + + data = { + ATTR_FORECAST_TIME: forecast_dt.isoformat(), + ATTR_FORECAST_CONDITION: translated_condition, + ATTR_FORECAST_PRECIPITATION: precipitation, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: precipitation_probability, + ATTR_FORECAST_TEMP: temp, + ATTR_FORECAST_TEMP_LOW: temp_low, + ATTR_FORECAST_WIND_BEARING: wind_direction, + ATTR_FORECAST_WIND_SPEED: wind_speed, + } + + return {k: v for k, v in data.items() if v is not None} + + +class ClimaCellWeatherEntity(BaseClimaCellWeatherEntity): + """Entity that talks to ClimaCell v4 API to retrieve weather data.""" + + @staticmethod + def _translate_condition( + condition: int | None, sun_is_up: bool = True + ) -> str | None: + """Translate ClimaCell condition into an HA condition.""" + if condition is None: + return None + # We won't guard here, instead we will fail hard + condition = WeatherCode(condition) + if condition in (WeatherCode.CLEAR, WeatherCode.MOSTLY_CLEAR): + if sun_is_up: + return CLEAR_CONDITIONS["day"] + return CLEAR_CONDITIONS["night"] + return CONDITIONS[condition] + + def _get_current_property(self, property_name: str) -> int | str | float | None: + """Get property from current conditions.""" + return self.coordinator.data.get(CURRENT, {}).get(property_name) @property - def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" - if self.forecast_type == DEFAULT_FORECAST_TYPE: - return True + def temperature(self): + """Return the platform temperature.""" + return self._get_current_property(CC_ATTR_TEMPERATURE) - return False + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_FAHRENHEIT + + @property + def pressure(self): + """Return the pressure.""" + pressure = self._get_current_property(CC_ATTR_PRESSURE) + if self.hass.config.units.is_metric and pressure: + return pressure_convert(pressure, PRESSURE_INHG, PRESSURE_HPA) + return pressure + + @property + def humidity(self): + """Return the humidity.""" + return self._get_current_property(CC_ATTR_HUMIDITY) + + @property + def wind_speed(self): + """Return the wind speed.""" + wind_speed = self._get_current_property(CC_ATTR_WIND_SPEED) + if self.hass.config.units.is_metric and wind_speed: + return distance_convert(wind_speed, LENGTH_MILES, LENGTH_KILOMETERS) + return wind_speed + + @property + def wind_bearing(self): + """Return the wind bearing.""" + return self._get_current_property(CC_ATTR_WIND_DIRECTION) + + @property + def ozone(self): + """Return the O3 (ozone) level.""" + return self._get_current_property(CC_ATTR_OZONE) + + @property + def condition(self): + """Return the condition.""" + return self._translate_condition( + self._get_current_property(CC_ATTR_CONDITION), + is_up(self.hass), + ) @property - def name(self) -> str: - """Return the name of the entity.""" - return f"{super().name} - {self.forecast_type.title()}" + def visibility(self): + """Return the visibility.""" + visibility = self._get_current_property(CC_ATTR_VISIBILITY) + if self.hass.config.units.is_metric and visibility: + return distance_convert(visibility, LENGTH_MILES, LENGTH_KILOMETERS) + return visibility @property - def unique_id(self) -> str: - """Return the unique id of the entity.""" - return f"{super().unique_id}_{self.forecast_type}" + def forecast(self): + """Return the forecast.""" + # Check if forecasts are available + raw_forecasts = self.coordinator.data.get(FORECASTS, {}).get(self.forecast_type) + if not raw_forecasts: + return None + + forecasts = [] + max_forecasts = MAX_FORECASTS[self.forecast_type] + forecast_count = 0 + + # Set default values (in cases where keys don't exist), None will be + # returned. Override properties per forecast type as needed + for forecast in raw_forecasts: + forecast_dt = dt_util.parse_datetime(forecast[CC_ATTR_TIMESTAMP]) + + # Throw out past data + if forecast_dt.date() < dt_util.utcnow().date(): + continue + + values = forecast["values"] + use_datetime = True + + condition = values.get(CC_ATTR_CONDITION) + precipitation = values.get(CC_ATTR_PRECIPITATION) + precipitation_probability = values.get(CC_ATTR_PRECIPITATION_PROBABILITY) + + temp = values.get(CC_ATTR_TEMPERATURE_HIGH) + temp_low = values.get(CC_ATTR_TEMPERATURE_LOW) + wind_direction = values.get(CC_ATTR_WIND_DIRECTION) + wind_speed = values.get(CC_ATTR_WIND_SPEED) + + if self.forecast_type == DAILY: + use_datetime = False + if precipitation: + precipitation = precipitation * 24 + elif self.forecast_type == NOWCAST: + # Precipitation is forecasted in CONF_TIMESTEP increments but in a + # per hour rate, so value needs to be converted to an amount. + if precipitation: + precipitation = ( + precipitation / 60 * self._config_entry.options[CONF_TIMESTEP] + ) + + forecasts.append( + self._forecast_dict( + forecast_dt, + use_datetime, + condition, + precipitation, + precipitation_probability, + temp, + temp_low, + wind_direction, + wind_speed, + ) + ) + + forecast_count += 1 + if forecast_count == max_forecasts: + break + + return forecasts + + +class ClimaCellV3WeatherEntity(BaseClimaCellWeatherEntity): + """Entity that talks to ClimaCell v3 API to retrieve weather data.""" + + @staticmethod + def _translate_condition( + condition: str | None, sun_is_up: bool = True + ) -> str | None: + """Translate ClimaCell condition into an HA condition.""" + if not condition: + return None + if "clear" in condition.lower(): + if sun_is_up: + return CLEAR_CONDITIONS["day"] + return CLEAR_CONDITIONS["night"] + return CONDITIONS_V3[condition] @property def temperature(self): """Return the platform temperature.""" - return self._get_cc_value(self.coordinator.data[CURRENT], CC_ATTR_TEMPERATURE) + return self._get_cc_value( + self.coordinator.data[CURRENT], CC_V3_ATTR_TEMPERATURE + ) @property def temperature_unit(self): @@ -176,7 +326,9 @@ def temperature_unit(self): @property def pressure(self): """Return the pressure.""" - pressure = self._get_cc_value(self.coordinator.data[CURRENT], CC_ATTR_PRESSURE) + pressure = self._get_cc_value( + self.coordinator.data[CURRENT], CC_V3_ATTR_PRESSURE + ) if self.hass.config.units.is_metric and pressure: return pressure_convert(pressure, PRESSURE_INHG, PRESSURE_HPA) return pressure @@ -184,13 +336,13 @@ def pressure(self): @property def humidity(self): """Return the humidity.""" - return self._get_cc_value(self.coordinator.data[CURRENT], CC_ATTR_HUMIDITY) + return self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_HUMIDITY) @property def wind_speed(self): """Return the wind speed.""" wind_speed = self._get_cc_value( - self.coordinator.data[CURRENT], CC_ATTR_WIND_SPEED + self.coordinator.data[CURRENT], CC_V3_ATTR_WIND_SPEED ) if self.hass.config.units.is_metric and wind_speed: return distance_convert(wind_speed, LENGTH_MILES, LENGTH_KILOMETERS) @@ -200,19 +352,19 @@ def wind_speed(self): def wind_bearing(self): """Return the wind bearing.""" return self._get_cc_value( - self.coordinator.data[CURRENT], CC_ATTR_WIND_DIRECTION + self.coordinator.data[CURRENT], CC_V3_ATTR_WIND_DIRECTION ) @property def ozone(self): """Return the O3 (ozone) level.""" - return self._get_cc_value(self.coordinator.data[CURRENT], CC_ATTR_OZONE) + return self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_OZONE) @property def condition(self): """Return the condition.""" - return _translate_condition( - self._get_cc_value(self.coordinator.data[CURRENT], CC_ATTR_CONDITION), + return self._translate_condition( + self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_CONDITION), is_up(self.hass), ) @@ -220,7 +372,7 @@ def condition(self): def visibility(self): """Return the visibility.""" visibility = self._get_cc_value( - self.coordinator.data[CURRENT], CC_ATTR_VISIBILITY + self.coordinator.data[CURRENT], CC_V3_ATTR_VISIBILITY ) if self.hass.config.units.is_metric and visibility: return distance_convert(visibility, LENGTH_MILES, LENGTH_KILOMETERS) @@ -230,46 +382,47 @@ def visibility(self): def forecast(self): """Return the forecast.""" # Check if forecasts are available - if not self.coordinator.data[FORECASTS].get(self.forecast_type): + raw_forecasts = self.coordinator.data.get(FORECASTS, {}).get(self.forecast_type) + if not raw_forecasts: return None forecasts = [] # Set default values (in cases where keys don't exist), None will be # returned. Override properties per forecast type as needed - for forecast in self.coordinator.data[FORECASTS][self.forecast_type]: + for forecast in raw_forecasts: forecast_dt = dt_util.parse_datetime( - self._get_cc_value(forecast, CC_ATTR_TIMESTAMP) + self._get_cc_value(forecast, CC_V3_ATTR_TIMESTAMP) ) use_datetime = True - condition = self._get_cc_value(forecast, CC_ATTR_CONDITION) - precipitation = self._get_cc_value(forecast, CC_ATTR_PRECIPITATION) + condition = self._get_cc_value(forecast, CC_V3_ATTR_CONDITION) + precipitation = self._get_cc_value(forecast, CC_V3_ATTR_PRECIPITATION) precipitation_probability = self._get_cc_value( - forecast, CC_ATTR_PRECIPITATION_PROBABILITY + forecast, CC_V3_ATTR_PRECIPITATION_PROBABILITY ) - temp = self._get_cc_value(forecast, CC_ATTR_TEMPERATURE) + temp = self._get_cc_value(forecast, CC_V3_ATTR_TEMPERATURE) temp_low = None - wind_direction = self._get_cc_value(forecast, CC_ATTR_WIND_DIRECTION) - wind_speed = self._get_cc_value(forecast, CC_ATTR_WIND_SPEED) + wind_direction = self._get_cc_value(forecast, CC_V3_ATTR_WIND_DIRECTION) + wind_speed = self._get_cc_value(forecast, CC_V3_ATTR_WIND_SPEED) if self.forecast_type == DAILY: use_datetime = False forecast_dt = dt_util.start_of_local_day(forecast_dt) precipitation = self._get_cc_value( - forecast, CC_ATTR_PRECIPITATION_DAILY + forecast, CC_V3_ATTR_PRECIPITATION_DAILY ) temp = next( ( - self._get_cc_value(item, CC_ATTR_TEMPERATURE_HIGH) - for item in forecast[CC_ATTR_TEMPERATURE] + self._get_cc_value(item, CC_V3_ATTR_TEMPERATURE_HIGH) + for item in forecast[CC_V3_ATTR_TEMPERATURE] if "max" in item ), temp, ) temp_low = next( ( - self._get_cc_value(item, CC_ATTR_TEMPERATURE_LOW) - for item in forecast[CC_ATTR_TEMPERATURE] + self._get_cc_value(item, CC_V3_ATTR_TEMPERATURE_LOW) + for item in forecast[CC_V3_ATTR_TEMPERATURE] if "min" in item ), temp_low, @@ -282,8 +435,7 @@ def forecast(self): ) forecasts.append( - _forecast_dict( - self.hass, + self._forecast_dict( forecast_dt, use_datetime, condition, diff --git a/requirements_all.txt b/requirements_all.txt index 22e136f255ab9..50e2bfc40bbc2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1310,7 +1310,7 @@ pychromecast==9.1.1 pycketcasts==1.0.0 # homeassistant.components.climacell -pyclimacell==0.14.0 +pyclimacell==0.18.0 # homeassistant.components.cmus pycmus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d573457b0f591..0dce23f3374b5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -702,7 +702,7 @@ pycfdns==1.2.1 pychromecast==9.1.1 # homeassistant.components.climacell -pyclimacell==0.14.0 +pyclimacell==0.18.0 # homeassistant.components.comfoconnect pycomfoconnect==0.4 diff --git a/tests/components/climacell/conftest.py b/tests/components/climacell/conftest.py index 3666243b4b4a4..d4c77c58879f0 100644 --- a/tests/components/climacell/conftest.py +++ b/tests/components/climacell/conftest.py @@ -1,8 +1,11 @@ """Configure py.test.""" +import json from unittest.mock import patch import pytest +from tests.common import load_fixture + @pytest.fixture(name="skip_notifications", autouse=True) def skip_notifications_fixture(): @@ -17,7 +20,10 @@ def skip_notifications_fixture(): def climacell_config_flow_connect(): """Mock valid climacell config flow setup.""" with patch( - "homeassistant.components.climacell.config_flow.ClimaCell.realtime", + "homeassistant.components.climacell.config_flow.ClimaCellV3.realtime", + return_value={}, + ), patch( + "homeassistant.components.climacell.config_flow.ClimaCellV4.realtime", return_value={}, ): yield @@ -27,16 +33,19 @@ def climacell_config_flow_connect(): def climacell_config_entry_update_fixture(): """Mock valid climacell config entry setup.""" with patch( - "homeassistant.components.climacell.ClimaCell.realtime", - return_value={}, + "homeassistant.components.climacell.ClimaCellV3.realtime", + return_value=json.loads(load_fixture("climacell/v3_realtime.json")), + ), patch( + "homeassistant.components.climacell.ClimaCellV3.forecast_hourly", + return_value=json.loads(load_fixture("climacell/v3_forecast_hourly.json")), ), patch( - "homeassistant.components.climacell.ClimaCell.forecast_hourly", - return_value=[], + "homeassistant.components.climacell.ClimaCellV3.forecast_daily", + return_value=json.loads(load_fixture("climacell/v3_forecast_daily.json")), ), patch( - "homeassistant.components.climacell.ClimaCell.forecast_daily", - return_value=[], + "homeassistant.components.climacell.ClimaCellV3.forecast_nowcast", + return_value=json.loads(load_fixture("climacell/v3_forecast_nowcast.json")), ), patch( - "homeassistant.components.climacell.ClimaCell.forecast_nowcast", - return_value=[], + "homeassistant.components.climacell.ClimaCellV4.realtime_and_all_forecasts", + return_value=json.loads(load_fixture("climacell/v4.json")), ): yield diff --git a/tests/components/climacell/const.py b/tests/components/climacell/const.py index ada0ebd1eb5c3..be933ecde290f 100644 --- a/tests/components/climacell/const.py +++ b/tests/components/climacell/const.py @@ -1,9 +1,38 @@ """Constants for climacell tests.""" -from homeassistant.const import CONF_API_KEY +from homeassistant.const import ( + CONF_API_KEY, + CONF_API_VERSION, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_NAME, +) API_KEY = "aa" MIN_CONFIG = { CONF_API_KEY: API_KEY, } + +V1_ENTRY_DATA = { + CONF_NAME: "ClimaCell", + CONF_API_KEY: API_KEY, + CONF_LATITUDE: 80, + CONF_LONGITUDE: 80, +} + +API_V3_ENTRY_DATA = { + CONF_NAME: "ClimaCell", + CONF_API_KEY: API_KEY, + CONF_LATITUDE: 80, + CONF_LONGITUDE: 80, + CONF_API_VERSION: 3, +} + +API_V4_ENTRY_DATA = { + CONF_NAME: "ClimaCell", + CONF_API_KEY: API_KEY, + CONF_LATITUDE: 80, + CONF_LONGITUDE: 80, + CONF_API_VERSION: 4, +} diff --git a/tests/components/climacell/test_config_flow.py b/tests/components/climacell/test_config_flow.py index a34bf6fd0fde4..6cd5fb8579437 100644 --- a/tests/components/climacell/test_config_flow.py +++ b/tests/components/climacell/test_config_flow.py @@ -21,7 +21,13 @@ DOMAIN, ) from homeassistant.config_entries import SOURCE_USER -from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.const import ( + CONF_API_KEY, + CONF_API_VERSION, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_NAME, +) from homeassistant.helpers.typing import HomeAssistantType from .const import API_KEY, MIN_CONFIG @@ -48,6 +54,32 @@ async def test_user_flow_minimum_fields(hass: HomeAssistantType) -> None: assert result["title"] == DEFAULT_NAME assert result["data"][CONF_NAME] == DEFAULT_NAME assert result["data"][CONF_API_KEY] == API_KEY + assert result["data"][CONF_API_VERSION] == 4 + assert result["data"][CONF_LATITUDE] == hass.config.latitude + assert result["data"][CONF_LONGITUDE] == hass.config.longitude + + +async def test_user_flow_v3(hass: HomeAssistantType) -> None: + """Test user config flow with v3 API.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + data = _get_config_schema(hass, MIN_CONFIG)(MIN_CONFIG) + data[CONF_API_VERSION] = 3 + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=data, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == DEFAULT_NAME + assert result["data"][CONF_NAME] == DEFAULT_NAME + assert result["data"][CONF_API_KEY] == API_KEY + assert result["data"][CONF_API_VERSION] == 3 assert result["data"][CONF_LATITUDE] == hass.config.latitude assert result["data"][CONF_LONGITUDE] == hass.config.longitude @@ -60,6 +92,7 @@ async def test_user_flow_same_unique_ids(hass: HomeAssistantType) -> None: data=user_input, source=SOURCE_USER, unique_id=_get_unique_id(hass, user_input), + version=2, ).add_to_hass(hass) result = await hass.config_entries.flow.async_init( @@ -75,7 +108,7 @@ async def test_user_flow_same_unique_ids(hass: HomeAssistantType) -> None: async def test_user_flow_cannot_connect(hass: HomeAssistantType) -> None: """Test user config flow when ClimaCell can't connect.""" with patch( - "homeassistant.components.climacell.config_flow.ClimaCell.realtime", + "homeassistant.components.climacell.config_flow.ClimaCellV4.realtime", side_effect=CantConnectException, ): result = await hass.config_entries.flow.async_init( @@ -91,7 +124,7 @@ async def test_user_flow_cannot_connect(hass: HomeAssistantType) -> None: async def test_user_flow_invalid_api(hass: HomeAssistantType) -> None: """Test user config flow when API key is invalid.""" with patch( - "homeassistant.components.climacell.config_flow.ClimaCell.realtime", + "homeassistant.components.climacell.config_flow.ClimaCellV4.realtime", side_effect=InvalidAPIKeyException, ): result = await hass.config_entries.flow.async_init( @@ -107,7 +140,7 @@ async def test_user_flow_invalid_api(hass: HomeAssistantType) -> None: async def test_user_flow_rate_limited(hass: HomeAssistantType) -> None: """Test user config flow when API key is rate limited.""" with patch( - "homeassistant.components.climacell.config_flow.ClimaCell.realtime", + "homeassistant.components.climacell.config_flow.ClimaCellV4.realtime", side_effect=RateLimitedException, ): result = await hass.config_entries.flow.async_init( @@ -123,7 +156,7 @@ async def test_user_flow_rate_limited(hass: HomeAssistantType) -> None: async def test_user_flow_unknown_exception(hass: HomeAssistantType) -> None: """Test user config flow when unknown error occurs.""" with patch( - "homeassistant.components.climacell.config_flow.ClimaCell.realtime", + "homeassistant.components.climacell.config_flow.ClimaCellV4.realtime", side_effect=UnknownException, ): result = await hass.config_entries.flow.async_init( @@ -144,6 +177,7 @@ async def test_options_flow(hass: HomeAssistantType) -> None: data=user_config, source=SOURCE_USER, unique_id=_get_unique_id(hass, user_config), + version=1, ) entry.add_to_hass(hass) diff --git a/tests/components/climacell/test_init.py b/tests/components/climacell/test_init.py index f3d7e490090ad..33a18d553f34a 100644 --- a/tests/components/climacell/test_init.py +++ b/tests/components/climacell/test_init.py @@ -7,11 +7,12 @@ _get_config_schema, _get_unique_id, ) -from homeassistant.components.climacell.const import DOMAIN +from homeassistant.components.climacell.const import CONF_TIMESTEP, DOMAIN from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN +from homeassistant.const import CONF_API_VERSION from homeassistant.helpers.typing import HomeAssistantType -from .const import MIN_CONFIG +from .const import API_V3_ENTRY_DATA, MIN_CONFIG, V1_ENTRY_DATA from tests.common import MockConfigEntry @@ -23,10 +24,12 @@ async def test_load_and_unload( climacell_config_entry_update: pytest.fixture, ) -> None: """Test loading and unloading entry.""" + data = _get_config_schema(hass)(MIN_CONFIG) config_entry = MockConfigEntry( domain=DOMAIN, - data=_get_config_schema(hass)(MIN_CONFIG), - unique_id=_get_unique_id(hass, _get_config_schema(hass)(MIN_CONFIG)), + data=data, + unique_id=_get_unique_id(hass, data), + version=1, ) config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) @@ -36,3 +39,53 @@ async def test_load_and_unload( assert await hass.config_entries.async_remove(config_entry.entry_id) await hass.async_block_till_done() assert len(hass.states.async_entity_ids(WEATHER_DOMAIN)) == 0 + + +async def test_v3_load_and_unload( + hass: HomeAssistantType, + climacell_config_entry_update: pytest.fixture, +) -> None: + """Test loading and unloading v3 entry.""" + data = _get_config_schema(hass)(API_V3_ENTRY_DATA) + config_entry = MockConfigEntry( + domain=DOMAIN, + data=data, + unique_id=_get_unique_id(hass, data), + version=1, + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert len(hass.states.async_entity_ids(WEATHER_DOMAIN)) == 1 + + assert await hass.config_entries.async_remove(config_entry.entry_id) + await hass.async_block_till_done() + assert len(hass.states.async_entity_ids(WEATHER_DOMAIN)) == 0 + + +@pytest.mark.parametrize( + "old_timestep, new_timestep", [(2, 1), (7, 5), (20, 15), (21, 30)] +) +async def test_migrate_timestep( + hass: HomeAssistantType, + climacell_config_entry_update: pytest.fixture, + old_timestep: int, + new_timestep: int, +) -> None: + """Test migration to standardized timestep.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data=V1_ENTRY_DATA, + options={CONF_TIMESTEP: old_timestep}, + unique_id=_get_unique_id(hass, V1_ENTRY_DATA), + version=1, + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.version == 1 + assert ( + CONF_API_VERSION in config_entry.data + and config_entry.data[CONF_API_VERSION] == 3 + ) + assert config_entry.options[CONF_TIMESTEP] == new_timestep diff --git a/tests/components/climacell/test_weather.py b/tests/components/climacell/test_weather.py new file mode 100644 index 0000000000000..c49ad8b3c484c --- /dev/null +++ b/tests/components/climacell/test_weather.py @@ -0,0 +1,382 @@ +"""Tests for Climacell weather entity.""" +from datetime import datetime +import logging +from typing import Any, Dict +from unittest.mock import patch + +import pytest +import pytz + +from homeassistant.components.climacell.config_flow import ( + _get_config_schema, + _get_unique_id, +) +from homeassistant.components.climacell.const import ATTRIBUTION, DOMAIN +from homeassistant.components.weather import ( + ATTR_CONDITION_CLOUDY, + ATTR_CONDITION_RAINY, + ATTR_CONDITION_SNOWY, + ATTR_CONDITION_SUNNY, + ATTR_FORECAST, + ATTR_FORECAST_CONDITION, + ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_FORECAST_TEMP, + ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_TIME, + ATTR_FORECAST_WIND_BEARING, + ATTR_FORECAST_WIND_SPEED, + ATTR_WEATHER_HUMIDITY, + ATTR_WEATHER_OZONE, + ATTR_WEATHER_PRESSURE, + ATTR_WEATHER_TEMPERATURE, + ATTR_WEATHER_VISIBILITY, + ATTR_WEATHER_WIND_BEARING, + ATTR_WEATHER_WIND_SPEED, + DOMAIN as WEATHER_DOMAIN, +) +from homeassistant.const import ATTR_ATTRIBUTION, ATTR_FRIENDLY_NAME +from homeassistant.core import State +from homeassistant.helpers.entity_registry import async_get +from homeassistant.helpers.typing import HomeAssistantType + +from .const import API_V3_ENTRY_DATA, API_V4_ENTRY_DATA + +from tests.common import MockConfigEntry + +_LOGGER = logging.getLogger(__name__) + + +async def _enable_entity(hass: HomeAssistantType, entity_name: str) -> None: + """Enable disabled entity.""" + ent_reg = async_get(hass) + entry = ent_reg.async_get(entity_name) + updated_entry = ent_reg.async_update_entity( + entry.entity_id, **{"disabled_by": None} + ) + assert updated_entry != entry + assert updated_entry.disabled is False + + +async def _setup(hass: HomeAssistantType, config: Dict[str, Any]) -> State: + """Set up entry and return entity state.""" + with patch( + "homeassistant.util.dt.utcnow", + return_value=datetime(2021, 3, 6, 23, 59, 59, tzinfo=pytz.UTC), + ): + data = _get_config_schema(hass)(config) + config_entry = MockConfigEntry( + domain=DOMAIN, + data=data, + unique_id=_get_unique_id(hass, data), + version=1, + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + await _enable_entity(hass, "weather.climacell_hourly") + await _enable_entity(hass, "weather.climacell_nowcast") + await hass.async_block_till_done() + assert len(hass.states.async_entity_ids(WEATHER_DOMAIN)) == 3 + + return hass.states.get("weather.climacell_daily") + + +async def test_v3_weather( + hass: HomeAssistantType, + climacell_config_entry_update: pytest.fixture, +) -> None: + """Test v3 weather data.""" + weather_state = await _setup(hass, API_V3_ENTRY_DATA) + assert weather_state.state == ATTR_CONDITION_SUNNY + assert weather_state.attributes[ATTR_ATTRIBUTION] == ATTRIBUTION + assert weather_state.attributes[ATTR_FORECAST] == [ + { + ATTR_FORECAST_CONDITION: ATTR_CONDITION_SUNNY, + ATTR_FORECAST_TIME: "2021-03-07T00:00:00+00:00", + ATTR_FORECAST_PRECIPITATION: 0, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, + ATTR_FORECAST_TEMP: 7, + ATTR_FORECAST_TEMP_LOW: -5, + }, + { + ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, + ATTR_FORECAST_TIME: "2021-03-08T00:00:00+00:00", + ATTR_FORECAST_PRECIPITATION: 0, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, + ATTR_FORECAST_TEMP: 10, + ATTR_FORECAST_TEMP_LOW: -4, + }, + { + ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, + ATTR_FORECAST_TIME: "2021-03-09T00:00:00+00:00", + ATTR_FORECAST_PRECIPITATION: 0, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, + ATTR_FORECAST_TEMP: 19, + ATTR_FORECAST_TEMP_LOW: 0, + }, + { + ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, + ATTR_FORECAST_TIME: "2021-03-10T00:00:00+00:00", + ATTR_FORECAST_PRECIPITATION: 0, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, + ATTR_FORECAST_TEMP: 18, + ATTR_FORECAST_TEMP_LOW: 3, + }, + { + ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, + ATTR_FORECAST_TIME: "2021-03-11T00:00:00+00:00", + ATTR_FORECAST_PRECIPITATION: 0, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: 5, + ATTR_FORECAST_TEMP: 20, + ATTR_FORECAST_TEMP_LOW: 9, + }, + { + ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, + ATTR_FORECAST_TIME: "2021-03-12T00:00:00+00:00", + ATTR_FORECAST_PRECIPITATION: 0.04572, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: 25, + ATTR_FORECAST_TEMP: 20, + ATTR_FORECAST_TEMP_LOW: 12, + }, + { + ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, + ATTR_FORECAST_TIME: "2021-03-13T00:00:00+00:00", + ATTR_FORECAST_PRECIPITATION: 0, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: 25, + ATTR_FORECAST_TEMP: 16, + ATTR_FORECAST_TEMP_LOW: 7, + }, + { + ATTR_FORECAST_CONDITION: ATTR_CONDITION_RAINY, + ATTR_FORECAST_TIME: "2021-03-14T00:00:00+00:00", + ATTR_FORECAST_PRECIPITATION: 1.07442, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: 75, + ATTR_FORECAST_TEMP: 6, + ATTR_FORECAST_TEMP_LOW: 3, + }, + { + ATTR_FORECAST_CONDITION: ATTR_CONDITION_SNOWY, + ATTR_FORECAST_TIME: "2021-03-15T00:00:00+00:00", + ATTR_FORECAST_PRECIPITATION: 7.305040000000001, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: 95, + ATTR_FORECAST_TEMP: 1, + ATTR_FORECAST_TEMP_LOW: 0, + }, + { + ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, + ATTR_FORECAST_TIME: "2021-03-16T00:00:00+00:00", + ATTR_FORECAST_PRECIPITATION: 0.00508, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: 5, + ATTR_FORECAST_TEMP: 6, + ATTR_FORECAST_TEMP_LOW: -2, + }, + { + ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, + ATTR_FORECAST_TIME: "2021-03-17T00:00:00+00:00", + ATTR_FORECAST_PRECIPITATION: 0, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, + ATTR_FORECAST_TEMP: 11, + ATTR_FORECAST_TEMP_LOW: 1, + }, + { + ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, + ATTR_FORECAST_TIME: "2021-03-18T00:00:00+00:00", + ATTR_FORECAST_PRECIPITATION: 0, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: 5, + ATTR_FORECAST_TEMP: 12, + ATTR_FORECAST_TEMP_LOW: 6, + }, + { + ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, + ATTR_FORECAST_TIME: "2021-03-19T00:00:00+00:00", + ATTR_FORECAST_PRECIPITATION: 0.1778, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: 45, + ATTR_FORECAST_TEMP: 9, + ATTR_FORECAST_TEMP_LOW: 5, + }, + { + ATTR_FORECAST_CONDITION: ATTR_CONDITION_RAINY, + ATTR_FORECAST_TIME: "2021-03-20T00:00:00+00:00", + ATTR_FORECAST_PRECIPITATION: 1.2319, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: 55, + ATTR_FORECAST_TEMP: 5, + ATTR_FORECAST_TEMP_LOW: 3, + }, + { + ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, + ATTR_FORECAST_TIME: "2021-03-21T00:00:00+00:00", + ATTR_FORECAST_PRECIPITATION: 0.043179999999999996, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: 20, + ATTR_FORECAST_TEMP: 7, + ATTR_FORECAST_TEMP_LOW: 1, + }, + ] + assert weather_state.attributes[ATTR_FRIENDLY_NAME] == "ClimaCell - Daily" + assert weather_state.attributes[ATTR_WEATHER_HUMIDITY] == 24 + assert weather_state.attributes[ATTR_WEATHER_OZONE] == 52.625 + assert weather_state.attributes[ATTR_WEATHER_PRESSURE] == 1028.124632345 + assert weather_state.attributes[ATTR_WEATHER_TEMPERATURE] == 7 + assert weather_state.attributes[ATTR_WEATHER_VISIBILITY] == 9.994026240000002 + assert weather_state.attributes[ATTR_WEATHER_WIND_BEARING] == 320.31 + assert weather_state.attributes[ATTR_WEATHER_WIND_SPEED] == 14.62893696 + + +async def test_v4_weather( + hass: HomeAssistantType, + climacell_config_entry_update: pytest.fixture, +) -> None: + """Test v4 weather data.""" + weather_state = await _setup(hass, API_V4_ENTRY_DATA) + assert weather_state.state == ATTR_CONDITION_SUNNY + assert weather_state.attributes[ATTR_ATTRIBUTION] == ATTRIBUTION + assert weather_state.attributes[ATTR_FORECAST] == [ + { + ATTR_FORECAST_CONDITION: ATTR_CONDITION_SUNNY, + ATTR_FORECAST_TIME: "2021-03-07T11:00:00+00:00", + ATTR_FORECAST_PRECIPITATION: 0, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, + ATTR_FORECAST_TEMP: 8, + ATTR_FORECAST_TEMP_LOW: -3, + ATTR_FORECAST_WIND_BEARING: 239.6, + ATTR_FORECAST_WIND_SPEED: 15.272674560000002, + }, + { + ATTR_FORECAST_CONDITION: "cloudy", + ATTR_FORECAST_TIME: "2021-03-08T11:00:00+00:00", + ATTR_FORECAST_PRECIPITATION: 0, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, + ATTR_FORECAST_TEMP: 10, + ATTR_FORECAST_TEMP_LOW: -3, + ATTR_FORECAST_WIND_BEARING: 262.82, + ATTR_FORECAST_WIND_SPEED: 11.65165056, + }, + { + ATTR_FORECAST_CONDITION: "cloudy", + ATTR_FORECAST_TIME: "2021-03-09T11:00:00+00:00", + ATTR_FORECAST_PRECIPITATION: 0, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, + ATTR_FORECAST_TEMP: 19, + ATTR_FORECAST_TEMP_LOW: 0, + ATTR_FORECAST_WIND_BEARING: 229.3, + ATTR_FORECAST_WIND_SPEED: 11.3458752, + }, + { + ATTR_FORECAST_CONDITION: "cloudy", + ATTR_FORECAST_TIME: "2021-03-10T11:00:00+00:00", + ATTR_FORECAST_PRECIPITATION: 0, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, + ATTR_FORECAST_TEMP: 18, + ATTR_FORECAST_TEMP_LOW: 3, + ATTR_FORECAST_WIND_BEARING: 149.91, + ATTR_FORECAST_WIND_SPEED: 17.123420160000002, + }, + { + ATTR_FORECAST_CONDITION: "cloudy", + ATTR_FORECAST_TIME: "2021-03-11T11:00:00+00:00", + ATTR_FORECAST_PRECIPITATION: 0, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, + ATTR_FORECAST_TEMP: 19, + ATTR_FORECAST_TEMP_LOW: 9, + ATTR_FORECAST_WIND_BEARING: 210.45, + ATTR_FORECAST_WIND_SPEED: 25.250607360000004, + }, + { + ATTR_FORECAST_CONDITION: "rainy", + ATTR_FORECAST_TIME: "2021-03-12T11:00:00+00:00", + ATTR_FORECAST_PRECIPITATION: 0.12192000000000001, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: 25, + ATTR_FORECAST_TEMP: 20, + ATTR_FORECAST_TEMP_LOW: 12, + ATTR_FORECAST_WIND_BEARING: 217.98, + ATTR_FORECAST_WIND_SPEED: 19.794931200000004, + }, + { + ATTR_FORECAST_CONDITION: "cloudy", + ATTR_FORECAST_TIME: "2021-03-13T11:00:00+00:00", + ATTR_FORECAST_PRECIPITATION: 0, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: 25, + ATTR_FORECAST_TEMP: 12, + ATTR_FORECAST_TEMP_LOW: 6, + ATTR_FORECAST_WIND_BEARING: 58.79, + ATTR_FORECAST_WIND_SPEED: 15.642823680000001, + }, + { + ATTR_FORECAST_CONDITION: "snowy", + ATTR_FORECAST_TIME: "2021-03-14T10:00:00+00:00", + ATTR_FORECAST_PRECIPITATION: 23.95728, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: 95, + ATTR_FORECAST_TEMP: 6, + ATTR_FORECAST_TEMP_LOW: 1, + ATTR_FORECAST_WIND_BEARING: 70.25, + ATTR_FORECAST_WIND_SPEED: 26.15184, + }, + { + ATTR_FORECAST_CONDITION: "snowy", + ATTR_FORECAST_TIME: "2021-03-15T10:00:00+00:00", + ATTR_FORECAST_PRECIPITATION: 1.46304, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: 55, + ATTR_FORECAST_TEMP: 6, + ATTR_FORECAST_TEMP_LOW: -1, + ATTR_FORECAST_WIND_BEARING: 84.47, + ATTR_FORECAST_WIND_SPEED: 25.57247616, + }, + { + ATTR_FORECAST_CONDITION: "cloudy", + ATTR_FORECAST_TIME: "2021-03-16T10:00:00+00:00", + ATTR_FORECAST_PRECIPITATION: 0, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, + ATTR_FORECAST_TEMP: 6, + ATTR_FORECAST_TEMP_LOW: -2, + ATTR_FORECAST_WIND_BEARING: 103.85, + ATTR_FORECAST_WIND_SPEED: 10.79869824, + }, + { + ATTR_FORECAST_CONDITION: "cloudy", + ATTR_FORECAST_TIME: "2021-03-17T10:00:00+00:00", + ATTR_FORECAST_PRECIPITATION: 0, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, + ATTR_FORECAST_TEMP: 11, + ATTR_FORECAST_TEMP_LOW: 1, + ATTR_FORECAST_WIND_BEARING: 145.41, + ATTR_FORECAST_WIND_SPEED: 11.69993088, + }, + { + ATTR_FORECAST_CONDITION: "cloudy", + ATTR_FORECAST_TIME: "2021-03-18T10:00:00+00:00", + ATTR_FORECAST_PRECIPITATION: 0, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: 10, + ATTR_FORECAST_TEMP: 12, + ATTR_FORECAST_TEMP_LOW: 5, + ATTR_FORECAST_WIND_BEARING: 62.99, + ATTR_FORECAST_WIND_SPEED: 10.58948352, + }, + { + ATTR_FORECAST_CONDITION: "rainy", + ATTR_FORECAST_TIME: "2021-03-19T10:00:00+00:00", + ATTR_FORECAST_PRECIPITATION: 2.92608, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: 55, + ATTR_FORECAST_TEMP: 9, + ATTR_FORECAST_TEMP_LOW: 4, + ATTR_FORECAST_WIND_BEARING: 68.54, + ATTR_FORECAST_WIND_SPEED: 22.38597504, + }, + { + ATTR_FORECAST_CONDITION: "snowy", + ATTR_FORECAST_TIME: "2021-03-20T10:00:00+00:00", + ATTR_FORECAST_PRECIPITATION: 1.2192, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: 33.3, + ATTR_FORECAST_TEMP: 5, + ATTR_FORECAST_TEMP_LOW: 2, + ATTR_FORECAST_WIND_BEARING: 56.98, + ATTR_FORECAST_WIND_SPEED: 27.922118400000002, + }, + ] + assert weather_state.attributes[ATTR_FRIENDLY_NAME] == "ClimaCell - Daily" + assert weather_state.attributes[ATTR_WEATHER_HUMIDITY] == 23 + assert weather_state.attributes[ATTR_WEATHER_OZONE] == 46.53 + assert weather_state.attributes[ATTR_WEATHER_PRESSURE] == 1027.7690615000001 + assert weather_state.attributes[ATTR_WEATHER_TEMPERATURE] == 7 + assert weather_state.attributes[ATTR_WEATHER_VISIBILITY] == 13.116153600000002 + assert weather_state.attributes[ATTR_WEATHER_WIND_BEARING] == 315.14 + assert weather_state.attributes[ATTR_WEATHER_WIND_SPEED] == 15.01517952 diff --git a/tests/fixtures/climacell/v3_forecast_daily.json b/tests/fixtures/climacell/v3_forecast_daily.json new file mode 100644 index 0000000000000..18f2d77e0cf97 --- /dev/null +++ b/tests/fixtures/climacell/v3_forecast_daily.json @@ -0,0 +1,992 @@ +[ + { + "temp": [ + { + "observation_time": "2021-03-07T11:00:00Z", + "min": { + "value": 23.47, + "units": "F" + } + }, + { + "observation_time": "2021-03-07T21:00:00Z", + "max": { + "value": 44.88, + "units": "F" + } + } + ], + "precipitation_accumulation": { + "value": 0, + "units": "in" + }, + "precipitation_probability": { + "value": 0, + "units": "%" + }, + "wind_speed": [ + { + "observation_time": "2021-03-08T00:00:00Z", + "min": { + "value": 2.58, + "units": "mph" + } + }, + { + "observation_time": "2021-03-07T19:00:00Z", + "max": { + "value": 7.67, + "units": "mph" + } + } + ], + "wind_direction": [ + { + "observation_time": "2021-03-08T00:00:00Z", + "min": { + "value": 72.1, + "units": "degrees" + } + }, + { + "observation_time": "2021-03-07T19:00:00Z", + "max": { + "value": 313.49, + "units": "degrees" + } + } + ], + "weather_code": { + "value": "clear" + }, + "observation_time": { + "value": "2021-03-07" + }, + "lat": 38.90694, + "lon": -77.03012 + }, + { + "temp": [ + { + "observation_time": "2021-03-08T11:00:00Z", + "min": { + "value": 24.79, + "units": "F" + } + }, + { + "observation_time": "2021-03-08T21:00:00Z", + "max": { + "value": 49.42, + "units": "F" + } + } + ], + "precipitation_accumulation": { + "value": 0, + "units": "in" + }, + "precipitation_probability": { + "value": 0, + "units": "%" + }, + "wind_speed": [ + { + "observation_time": "2021-03-08T22:00:00Z", + "min": { + "value": 1.97, + "units": "mph" + } + }, + { + "observation_time": "2021-03-08T13:00:00Z", + "max": { + "value": 7.24, + "units": "mph" + } + } + ], + "wind_direction": [ + { + "observation_time": "2021-03-08T22:00:00Z", + "min": { + "value": 268.74, + "units": "degrees" + } + }, + { + "observation_time": "2021-03-08T13:00:00Z", + "max": { + "value": 324.8, + "units": "degrees" + } + } + ], + "weather_code": { + "value": "cloudy" + }, + "observation_time": { + "value": "2021-03-08" + }, + "lat": 38.90694, + "lon": -77.03012 + }, + { + "temp": [ + { + "observation_time": "2021-03-09T11:00:00Z", + "min": { + "value": 31.48, + "units": "F" + } + }, + { + "observation_time": "2021-03-09T21:00:00Z", + "max": { + "value": 66.98, + "units": "F" + } + } + ], + "precipitation_accumulation": { + "value": 0, + "units": "in" + }, + "precipitation_probability": { + "value": 0, + "units": "%" + }, + "wind_speed": [ + { + "observation_time": "2021-03-09T22:00:00Z", + "min": { + "value": 3.35, + "units": "mph" + } + }, + { + "observation_time": "2021-03-09T19:00:00Z", + "max": { + "value": 7.05, + "units": "mph" + } + } + ], + "wind_direction": [ + { + "observation_time": "2021-03-09T22:00:00Z", + "min": { + "value": 279.37, + "units": "degrees" + } + }, + { + "observation_time": "2021-03-09T19:00:00Z", + "max": { + "value": 253.12, + "units": "degrees" + } + } + ], + "weather_code": { + "value": "mostly_cloudy" + }, + "observation_time": { + "value": "2021-03-09" + }, + "lat": 38.90694, + "lon": -77.03012 + }, + { + "temp": [ + { + "observation_time": "2021-03-10T11:00:00Z", + "min": { + "value": 37.32, + "units": "F" + } + }, + { + "observation_time": "2021-03-10T20:00:00Z", + "max": { + "value": 65.28, + "units": "F" + } + } + ], + "precipitation_accumulation": { + "value": 0, + "units": "in" + }, + "precipitation_probability": { + "value": 0, + "units": "%" + }, + "wind_speed": [ + { + "observation_time": "2021-03-10T05:00:00Z", + "min": { + "value": 2.13, + "units": "mph" + } + }, + { + "observation_time": "2021-03-10T21:00:00Z", + "max": { + "value": 9.42, + "units": "mph" + } + } + ], + "wind_direction": [ + { + "observation_time": "2021-03-10T05:00:00Z", + "min": { + "value": 342.01, + "units": "degrees" + } + }, + { + "observation_time": "2021-03-10T21:00:00Z", + "max": { + "value": 193.22, + "units": "degrees" + } + } + ], + "weather_code": { + "value": "cloudy" + }, + "observation_time": { + "value": "2021-03-10" + }, + "lat": 38.90694, + "lon": -77.03012 + }, + { + "temp": [ + { + "observation_time": "2021-03-11T12:00:00Z", + "min": { + "value": 48.69, + "units": "F" + } + }, + { + "observation_time": "2021-03-11T21:00:00Z", + "max": { + "value": 67.37, + "units": "F" + } + } + ], + "precipitation_accumulation": { + "value": 0, + "units": "in" + }, + "precipitation_probability": { + "value": 5, + "units": "%" + }, + "wind_speed": [ + { + "observation_time": "2021-03-11T02:00:00Z", + "min": { + "value": 8.82, + "units": "mph" + } + }, + { + "observation_time": "2021-03-12T01:00:00Z", + "max": { + "value": 14.47, + "units": "mph" + } + } + ], + "wind_direction": [ + { + "observation_time": "2021-03-11T02:00:00Z", + "min": { + "value": 176.84, + "units": "degrees" + } + }, + { + "observation_time": "2021-03-12T01:00:00Z", + "max": { + "value": 210.63, + "units": "degrees" + } + } + ], + "weather_code": { + "value": "cloudy" + }, + "observation_time": { + "value": "2021-03-11" + }, + "lat": 38.90694, + "lon": -77.03012 + }, + { + "temp": [ + { + "observation_time": "2021-03-12T12:00:00Z", + "min": { + "value": 53.83, + "units": "F" + } + }, + { + "observation_time": "2021-03-12T18:00:00Z", + "max": { + "value": 67.91, + "units": "F" + } + } + ], + "precipitation_accumulation": { + "value": 0.0018, + "units": "in" + }, + "precipitation_probability": { + "value": 25, + "units": "%" + }, + "wind_speed": [ + { + "observation_time": "2021-03-13T00:00:00Z", + "min": { + "value": 4.98, + "units": "mph" + } + }, + { + "observation_time": "2021-03-12T02:00:00Z", + "max": { + "value": 15.69, + "units": "mph" + } + } + ], + "wind_direction": [ + { + "observation_time": "2021-03-13T00:00:00Z", + "min": { + "value": 329.35, + "units": "degrees" + } + }, + { + "observation_time": "2021-03-12T02:00:00Z", + "max": { + "value": 211.47, + "units": "degrees" + } + } + ], + "weather_code": { + "value": "cloudy" + }, + "observation_time": { + "value": "2021-03-12" + }, + "lat": 38.90694, + "lon": -77.03012 + }, + { + "temp": [ + { + "observation_time": "2021-03-14T00:00:00Z", + "min": { + "value": 45.48, + "units": "F" + } + }, + { + "observation_time": "2021-03-13T03:00:00Z", + "max": { + "value": 60.42, + "units": "F" + } + } + ], + "precipitation_accumulation": { + "value": 0, + "units": "in" + }, + "precipitation_probability": { + "value": 25, + "units": "%" + }, + "wind_speed": [ + { + "observation_time": "2021-03-13T03:00:00Z", + "min": { + "value": 2.91, + "units": "mph" + } + }, + { + "observation_time": "2021-03-13T21:00:00Z", + "max": { + "value": 9.72, + "units": "mph" + } + } + ], + "wind_direction": [ + { + "observation_time": "2021-03-13T03:00:00Z", + "min": { + "value": 202.04, + "units": "degrees" + } + }, + { + "observation_time": "2021-03-13T21:00:00Z", + "max": { + "value": 64.38, + "units": "degrees" + } + } + ], + "weather_code": { + "value": "cloudy" + }, + "observation_time": { + "value": "2021-03-13" + }, + "lat": 38.90694, + "lon": -77.03012 + }, + { + "temp": [ + { + "observation_time": "2021-03-15T00:00:00Z", + "min": { + "value": 37.81, + "units": "F" + } + }, + { + "observation_time": "2021-03-14T03:00:00Z", + "max": { + "value": 43.58, + "units": "F" + } + } + ], + "precipitation_accumulation": { + "value": 0.0423, + "units": "in" + }, + "precipitation_probability": { + "value": 75, + "units": "%" + }, + "wind_speed": [ + { + "observation_time": "2021-03-14T06:00:00Z", + "min": { + "value": 5.34, + "units": "mph" + } + }, + { + "observation_time": "2021-03-14T21:00:00Z", + "max": { + "value": 16.25, + "units": "mph" + } + } + ], + "wind_direction": [ + { + "observation_time": "2021-03-14T06:00:00Z", + "min": { + "value": 57.52, + "units": "degrees" + } + }, + { + "observation_time": "2021-03-14T21:00:00Z", + "max": { + "value": 83.23, + "units": "degrees" + } + } + ], + "weather_code": { + "value": "rain_light" + }, + "observation_time": { + "value": "2021-03-14" + }, + "lat": 38.90694, + "lon": -77.03012 + }, + { + "temp": [ + { + "observation_time": "2021-03-16T00:00:00Z", + "min": { + "value": 32.31, + "units": "F" + } + }, + { + "observation_time": "2021-03-15T09:00:00Z", + "max": { + "value": 34.21, + "units": "F" + } + } + ], + "precipitation_accumulation": { + "value": 0.2876, + "units": "in" + }, + "precipitation_probability": { + "value": 95, + "units": "%" + }, + "wind_speed": [ + { + "observation_time": "2021-03-16T00:00:00Z", + "min": { + "value": 11.7, + "units": "mph" + } + }, + { + "observation_time": "2021-03-15T18:00:00Z", + "max": { + "value": 15.89, + "units": "mph" + } + } + ], + "wind_direction": [ + { + "observation_time": "2021-03-16T00:00:00Z", + "min": { + "value": 63.67, + "units": "degrees" + } + }, + { + "observation_time": "2021-03-15T18:00:00Z", + "max": { + "value": 59.49, + "units": "degrees" + } + } + ], + "weather_code": { + "value": "snow_heavy" + }, + "observation_time": { + "value": "2021-03-15" + }, + "lat": 38.90694, + "lon": -77.03012 + }, + { + "temp": [ + { + "observation_time": "2021-03-16T12:00:00Z", + "min": { + "value": 29.1, + "units": "F" + } + }, + { + "observation_time": "2021-03-16T21:00:00Z", + "max": { + "value": 43, + "units": "F" + } + } + ], + "precipitation_accumulation": { + "value": 0.0002, + "units": "in" + }, + "precipitation_probability": { + "value": 5, + "units": "%" + }, + "wind_speed": [ + { + "observation_time": "2021-03-16T18:00:00Z", + "min": { + "value": 4.98, + "units": "mph" + } + }, + { + "observation_time": "2021-03-16T03:00:00Z", + "max": { + "value": 9.77, + "units": "mph" + } + } + ], + "wind_direction": [ + { + "observation_time": "2021-03-16T18:00:00Z", + "min": { + "value": 80.47, + "units": "degrees" + } + }, + { + "observation_time": "2021-03-16T03:00:00Z", + "max": { + "value": 58.98, + "units": "degrees" + } + } + ], + "weather_code": { + "value": "cloudy" + }, + "observation_time": { + "value": "2021-03-16" + }, + "lat": 38.90694, + "lon": -77.03012 + }, + { + "temp": [ + { + "observation_time": "2021-03-17T12:00:00Z", + "min": { + "value": 34.32, + "units": "F" + } + }, + { + "observation_time": "2021-03-17T21:00:00Z", + "max": { + "value": 52.4, + "units": "F" + } + } + ], + "precipitation_accumulation": { + "value": 0, + "units": "in" + }, + "precipitation_probability": { + "value": 0, + "units": "%" + }, + "wind_speed": [ + { + "observation_time": "2021-03-18T00:00:00Z", + "min": { + "value": 4.49, + "units": "mph" + } + }, + { + "observation_time": "2021-03-17T03:00:00Z", + "max": { + "value": 6.71, + "units": "mph" + } + } + ], + "wind_direction": [ + { + "observation_time": "2021-03-18T00:00:00Z", + "min": { + "value": 116.64, + "units": "degrees" + } + }, + { + "observation_time": "2021-03-17T03:00:00Z", + "max": { + "value": 111.51, + "units": "degrees" + } + } + ], + "weather_code": { + "value": "cloudy" + }, + "observation_time": { + "value": "2021-03-17" + }, + "lat": 38.90694, + "lon": -77.03012 + }, + { + "temp": [ + { + "observation_time": "2021-03-18T12:00:00Z", + "min": { + "value": 41.99, + "units": "F" + } + }, + { + "observation_time": "2021-03-18T21:00:00Z", + "max": { + "value": 54.07, + "units": "F" + } + } + ], + "precipitation_accumulation": { + "value": 0, + "units": "in" + }, + "precipitation_probability": { + "value": 5, + "units": "%" + }, + "wind_speed": [ + { + "observation_time": "2021-03-18T06:00:00Z", + "min": { + "value": 2.77, + "units": "mph" + } + }, + { + "observation_time": "2021-03-18T03:00:00Z", + "max": { + "value": 5.22, + "units": "mph" + } + } + ], + "wind_direction": [ + { + "observation_time": "2021-03-18T06:00:00Z", + "min": { + "value": 119.5, + "units": "degrees" + } + }, + { + "observation_time": "2021-03-18T03:00:00Z", + "max": { + "value": 135.5, + "units": "degrees" + } + } + ], + "weather_code": { + "value": "cloudy" + }, + "observation_time": { + "value": "2021-03-18" + }, + "lat": 38.90694, + "lon": -77.03012 + }, + { + "temp": [ + { + "observation_time": "2021-03-19T12:00:00Z", + "min": { + "value": 40.48, + "units": "F" + } + }, + { + "observation_time": "2021-03-19T18:00:00Z", + "max": { + "value": 48.94, + "units": "F" + } + } + ], + "precipitation_accumulation": { + "value": 0.007, + "units": "in" + }, + "precipitation_probability": { + "value": 45, + "units": "%" + }, + "wind_speed": [ + { + "observation_time": "2021-03-19T03:00:00Z", + "min": { + "value": 5.43, + "units": "mph" + } + }, + { + "observation_time": "2021-03-20T00:00:00Z", + "max": { + "value": 11.1, + "units": "mph" + } + } + ], + "wind_direction": [ + { + "observation_time": "2021-03-19T03:00:00Z", + "min": { + "value": 50.18, + "units": "degrees" + } + }, + { + "observation_time": "2021-03-20T00:00:00Z", + "max": { + "value": 86.96, + "units": "degrees" + } + } + ], + "weather_code": { + "value": "cloudy" + }, + "observation_time": { + "value": "2021-03-19" + }, + "lat": 38.90694, + "lon": -77.03012 + }, + { + "temp": [ + { + "observation_time": "2021-03-21T00:00:00Z", + "min": { + "value": 37.56, + "units": "F" + } + }, + { + "observation_time": "2021-03-20T03:00:00Z", + "max": { + "value": 41.05, + "units": "F" + } + } + ], + "precipitation_accumulation": { + "value": 0.0485, + "units": "in" + }, + "precipitation_probability": { + "value": 55, + "units": "%" + }, + "wind_speed": [ + { + "observation_time": "2021-03-20T03:00:00Z", + "min": { + "value": 10.9, + "units": "mph" + } + }, + { + "observation_time": "2021-03-20T21:00:00Z", + "max": { + "value": 17.35, + "units": "mph" + } + } + ], + "wind_direction": [ + { + "observation_time": "2021-03-20T03:00:00Z", + "min": { + "value": 70.56, + "units": "degrees" + } + }, + { + "observation_time": "2021-03-20T21:00:00Z", + "max": { + "value": 58.55, + "units": "degrees" + } + } + ], + "weather_code": { + "value": "drizzle" + }, + "observation_time": { + "value": "2021-03-20" + }, + "lat": 38.90694, + "lon": -77.03012 + }, + { + "temp": [ + { + "observation_time": "2021-03-21T12:00:00Z", + "min": { + "value": 33.66, + "units": "F" + } + }, + { + "observation_time": "2021-03-21T21:00:00Z", + "max": { + "value": 44.3, + "units": "F" + } + } + ], + "precipitation_accumulation": { + "value": 0.0017, + "units": "in" + }, + "precipitation_probability": { + "value": 20, + "units": "%" + }, + "wind_speed": [ + { + "observation_time": "2021-03-22T00:00:00Z", + "min": { + "value": 8.65, + "units": "mph" + } + }, + { + "observation_time": "2021-03-21T03:00:00Z", + "max": { + "value": 16.53, + "units": "mph" + } + } + ], + "wind_direction": [ + { + "observation_time": "2021-03-22T00:00:00Z", + "min": { + "value": 64.92, + "units": "degrees" + } + }, + { + "observation_time": "2021-03-21T03:00:00Z", + "max": { + "value": 57.74, + "units": "degrees" + } + } + ], + "weather_code": { + "value": "cloudy" + }, + "observation_time": { + "value": "2021-03-21" + }, + "lat": 38.90694, + "lon": -77.03012 + } +] \ No newline at end of file diff --git a/tests/fixtures/climacell/v3_forecast_hourly.json b/tests/fixtures/climacell/v3_forecast_hourly.json new file mode 100644 index 0000000000000..a550c7f4302fc --- /dev/null +++ b/tests/fixtures/climacell/v3_forecast_hourly.json @@ -0,0 +1,752 @@ +[ + { + "lon": -77.03012, + "lat": 38.90694, + "temp": { + "value": 42.75, + "units": "F" + }, + "precipitation": { + "value": 0, + "units": "in/hr" + }, + "precipitation_probability": { + "value": 0, + "units": "%" + }, + "wind_speed": { + "value": 8.99, + "units": "mph" + }, + "wind_direction": { + "value": 320.22, + "units": "degrees" + }, + "weather_code": { + "value": "clear" + }, + "observation_time": { + "value": "2021-03-07T18:00:00.000Z" + } + }, + { + "lon": -77.03012, + "lat": 38.90694, + "temp": { + "value": 44.29, + "units": "F" + }, + "precipitation": { + "value": 0, + "units": "in/hr" + }, + "precipitation_probability": { + "value": 0, + "units": "%" + }, + "wind_speed": { + "value": 9.65, + "units": "mph" + }, + "wind_direction": { + "value": 326.14, + "units": "degrees" + }, + "weather_code": { + "value": "clear" + }, + "observation_time": { + "value": "2021-03-07T19:00:00.000Z" + } + }, + { + "lon": -77.03012, + "lat": 38.90694, + "temp": { + "value": 45.3, + "units": "F" + }, + "precipitation": { + "value": 0, + "units": "in/hr" + }, + "precipitation_probability": { + "value": 0, + "units": "%" + }, + "wind_speed": { + "value": 9.28, + "units": "mph" + }, + "wind_direction": { + "value": 322.01, + "units": "degrees" + }, + "weather_code": { + "value": "clear" + }, + "observation_time": { + "value": "2021-03-07T20:00:00.000Z" + } + }, + { + "lon": -77.03012, + "lat": 38.90694, + "temp": { + "value": 45.26, + "units": "F" + }, + "precipitation": { + "value": 0, + "units": "in/hr" + }, + "precipitation_probability": { + "value": 0, + "units": "%" + }, + "wind_speed": { + "value": 9.12, + "units": "mph" + }, + "wind_direction": { + "value": 323.71, + "units": "degrees" + }, + "weather_code": { + "value": "clear" + }, + "observation_time": { + "value": "2021-03-07T21:00:00.000Z" + } + }, + { + "lon": -77.03012, + "lat": 38.90694, + "temp": { + "value": 44.83, + "units": "F" + }, + "precipitation": { + "value": 0, + "units": "in/hr" + }, + "precipitation_probability": { + "value": 0, + "units": "%" + }, + "wind_speed": { + "value": 7.27, + "units": "mph" + }, + "wind_direction": { + "value": 319.88, + "units": "degrees" + }, + "weather_code": { + "value": "clear" + }, + "observation_time": { + "value": "2021-03-07T22:00:00.000Z" + } + }, + { + "lon": -77.03012, + "lat": 38.90694, + "temp": { + "value": 41.7, + "units": "F" + }, + "precipitation": { + "value": 0, + "units": "in/hr" + }, + "precipitation_probability": { + "value": 0, + "units": "%" + }, + "wind_speed": { + "value": 4.37, + "units": "mph" + }, + "wind_direction": { + "value": 320.69, + "units": "degrees" + }, + "weather_code": { + "value": "clear" + }, + "observation_time": { + "value": "2021-03-07T23:00:00.000Z" + } + }, + { + "lon": -77.03012, + "lat": 38.90694, + "temp": { + "value": 38.04, + "units": "F" + }, + "precipitation": { + "value": 0, + "units": "in/hr" + }, + "precipitation_probability": { + "value": 0, + "units": "%" + }, + "wind_speed": { + "value": 5.45, + "units": "mph" + }, + "wind_direction": { + "value": 351.54, + "units": "degrees" + }, + "weather_code": { + "value": "clear" + }, + "observation_time": { + "value": "2021-03-08T00:00:00.000Z" + } + }, + { + "lon": -77.03012, + "lat": 38.90694, + "temp": { + "value": 35.88, + "units": "F" + }, + "precipitation": { + "value": 0, + "units": "in/hr" + }, + "precipitation_probability": { + "value": 0, + "units": "%" + }, + "wind_speed": { + "value": 5.31, + "units": "mph" + }, + "wind_direction": { + "value": 20.6, + "units": "degrees" + }, + "weather_code": { + "value": "clear" + }, + "observation_time": { + "value": "2021-03-08T01:00:00.000Z" + } + }, + { + "lon": -77.03012, + "lat": 38.90694, + "temp": { + "value": 34.34, + "units": "F" + }, + "precipitation": { + "value": 0, + "units": "in/hr" + }, + "precipitation_probability": { + "value": 0, + "units": "%" + }, + "wind_speed": { + "value": 5.78, + "units": "mph" + }, + "wind_direction": { + "value": 11.22, + "units": "degrees" + }, + "weather_code": { + "value": "clear" + }, + "observation_time": { + "value": "2021-03-08T02:00:00.000Z" + } + }, + { + "lon": -77.03012, + "lat": 38.90694, + "temp": { + "value": 33.3, + "units": "F" + }, + "precipitation": { + "value": 0, + "units": "in/hr" + }, + "precipitation_probability": { + "value": 0, + "units": "%" + }, + "wind_speed": { + "value": 5.73, + "units": "mph" + }, + "wind_direction": { + "value": 15.46, + "units": "degrees" + }, + "weather_code": { + "value": "clear" + }, + "observation_time": { + "value": "2021-03-08T03:00:00.000Z" + } + }, + { + "lon": -77.03012, + "lat": 38.90694, + "temp": { + "value": 31.74, + "units": "F" + }, + "precipitation": { + "value": 0, + "units": "in/hr" + }, + "precipitation_probability": { + "value": 0, + "units": "%" + }, + "wind_speed": { + "value": 4.44, + "units": "mph" + }, + "wind_direction": { + "value": 26.07, + "units": "degrees" + }, + "weather_code": { + "value": "clear" + }, + "observation_time": { + "value": "2021-03-08T04:00:00.000Z" + } + }, + { + "lon": -77.03012, + "lat": 38.90694, + "temp": { + "value": 29.98, + "units": "F" + }, + "precipitation": { + "value": 0, + "units": "in/hr" + }, + "precipitation_probability": { + "value": 0, + "units": "%" + }, + "wind_speed": { + "value": 4.33, + "units": "mph" + }, + "wind_direction": { + "value": 23.7, + "units": "degrees" + }, + "weather_code": { + "value": "clear" + }, + "observation_time": { + "value": "2021-03-08T05:00:00.000Z" + } + }, + { + "lon": -77.03012, + "lat": 38.90694, + "temp": { + "value": 27.34, + "units": "F" + }, + "precipitation": { + "value": 0, + "units": "in/hr" + }, + "precipitation_probability": { + "value": 0, + "units": "%" + }, + "wind_speed": { + "value": 4.7, + "units": "mph" + }, + "wind_direction": { + "value": 354.56, + "units": "degrees" + }, + "weather_code": { + "value": "clear" + }, + "observation_time": { + "value": "2021-03-08T06:00:00.000Z" + } + }, + { + "lon": -77.03012, + "lat": 38.90694, + "temp": { + "value": 26.61, + "units": "F" + }, + "precipitation": { + "value": 0, + "units": "in/hr" + }, + "precipitation_probability": { + "value": 0, + "units": "%" + }, + "wind_speed": { + "value": 4.94, + "units": "mph" + }, + "wind_direction": { + "value": 349.63, + "units": "degrees" + }, + "weather_code": { + "value": "clear" + }, + "observation_time": { + "value": "2021-03-08T07:00:00.000Z" + } + }, + { + "lon": -77.03012, + "lat": 38.90694, + "temp": { + "value": 25.96, + "units": "F" + }, + "precipitation": { + "value": 0, + "units": "in/hr" + }, + "precipitation_probability": { + "value": 0, + "units": "%" + }, + "wind_speed": { + "value": 4.61, + "units": "mph" + }, + "wind_direction": { + "value": 336.74, + "units": "degrees" + }, + "weather_code": { + "value": "clear" + }, + "observation_time": { + "value": "2021-03-08T08:00:00.000Z" + } + }, + { + "lon": -77.03012, + "lat": 38.90694, + "temp": { + "value": 25.72, + "units": "F" + }, + "precipitation": { + "value": 0, + "units": "in/hr" + }, + "precipitation_probability": { + "value": 0, + "units": "%" + }, + "wind_speed": { + "value": 4.22, + "units": "mph" + }, + "wind_direction": { + "value": 332.71, + "units": "degrees" + }, + "weather_code": { + "value": "clear" + }, + "observation_time": { + "value": "2021-03-08T09:00:00.000Z" + } + }, + { + "lon": -77.03012, + "lat": 38.90694, + "temp": { + "value": 25.68, + "units": "F" + }, + "precipitation": { + "value": 0, + "units": "in/hr" + }, + "precipitation_probability": { + "value": 0, + "units": "%" + }, + "wind_speed": { + "value": 4.56, + "units": "mph" + }, + "wind_direction": { + "value": 328.58, + "units": "degrees" + }, + "weather_code": { + "value": "clear" + }, + "observation_time": { + "value": "2021-03-08T10:00:00.000Z" + } + }, + { + "lon": -77.03012, + "lat": 38.90694, + "temp": { + "value": 31.02, + "units": "F" + }, + "precipitation": { + "value": 0, + "units": "in/hr" + }, + "precipitation_probability": { + "value": 0, + "units": "%" + }, + "wind_speed": { + "value": 2.8, + "units": "mph" + }, + "wind_direction": { + "value": 322.27, + "units": "degrees" + }, + "weather_code": { + "value": "clear" + }, + "observation_time": { + "value": "2021-03-08T11:00:00.000Z" + } + }, + { + "lon": -77.03012, + "lat": 38.90694, + "temp": { + "value": 31.04, + "units": "F" + }, + "precipitation": { + "value": 0, + "units": "in/hr" + }, + "precipitation_probability": { + "value": 0, + "units": "%" + }, + "wind_speed": { + "value": 2.82, + "units": "mph" + }, + "wind_direction": { + "value": 325.27, + "units": "degrees" + }, + "weather_code": { + "value": "clear" + }, + "observation_time": { + "value": "2021-03-08T12:00:00.000Z" + } + }, + { + "lat": 38.90694, + "lon": -77.03012, + "temp": { + "value": 29.95, + "units": "F" + }, + "precipitation": { + "value": 0, + "units": "in/hr" + }, + "precipitation_probability": { + "value": 0, + "units": "%" + }, + "wind_speed": { + "value": 7.24, + "units": "mph" + }, + "wind_direction": { + "value": 324.8, + "units": "degrees" + }, + "weather_code": { + "value": "mostly_clear" + }, + "observation_time": { + "value": "2021-03-08T13:00:00.000Z" + } + }, + { + "lat": 38.90694, + "lon": -77.03012, + "temp": { + "value": 34.02, + "units": "F" + }, + "precipitation": { + "value": 0, + "units": "in/hr" + }, + "precipitation_probability": { + "value": 0, + "units": "%" + }, + "wind_speed": { + "value": 6.28, + "units": "mph" + }, + "wind_direction": { + "value": 335.16, + "units": "degrees" + }, + "weather_code": { + "value": "partly_cloudy" + }, + "observation_time": { + "value": "2021-03-08T14:00:00.000Z" + } + }, + { + "lat": 38.90694, + "lon": -77.03012, + "temp": { + "value": 37.78, + "units": "F" + }, + "precipitation": { + "value": 0, + "units": "in/hr" + }, + "precipitation_probability": { + "value": 0, + "units": "%" + }, + "wind_speed": { + "value": 5.8, + "units": "mph" + }, + "wind_direction": { + "value": 324.49, + "units": "degrees" + }, + "weather_code": { + "value": "cloudy" + }, + "observation_time": { + "value": "2021-03-08T15:00:00.000Z" + } + }, + { + "lat": 38.90694, + "lon": -77.03012, + "temp": { + "value": 40.57, + "units": "F" + }, + "precipitation": { + "value": 0, + "units": "in/hr" + }, + "precipitation_probability": { + "value": 0, + "units": "%" + }, + "wind_speed": { + "value": 5.5, + "units": "mph" + }, + "wind_direction": { + "value": 310.68, + "units": "degrees" + }, + "weather_code": { + "value": "mostly_cloudy" + }, + "observation_time": { + "value": "2021-03-08T16:00:00.000Z" + } + }, + { + "lat": 38.90694, + "lon": -77.03012, + "temp": { + "value": 42.83, + "units": "F" + }, + "precipitation": { + "value": 0, + "units": "in/hr" + }, + "precipitation_probability": { + "value": 0, + "units": "%" + }, + "wind_speed": { + "value": 5.47, + "units": "mph" + }, + "wind_direction": { + "value": 304.18, + "units": "degrees" + }, + "weather_code": { + "value": "mostly_clear" + }, + "observation_time": { + "value": "2021-03-08T17:00:00.000Z" + } + }, + { + "lat": 38.90694, + "lon": -77.03012, + "temp": { + "value": 45.07, + "units": "F" + }, + "precipitation": { + "value": 0, + "units": "in/hr" + }, + "precipitation_probability": { + "value": 0, + "units": "%" + }, + "wind_speed": { + "value": 4.88, + "units": "mph" + }, + "wind_direction": { + "value": 301.19, + "units": "degrees" + }, + "weather_code": { + "value": "clear" + }, + "observation_time": { + "value": "2021-03-08T18:00:00.000Z" + } + } +] \ No newline at end of file diff --git a/tests/fixtures/climacell/v3_forecast_nowcast.json b/tests/fixtures/climacell/v3_forecast_nowcast.json new file mode 100644 index 0000000000000..23372eae0f942 --- /dev/null +++ b/tests/fixtures/climacell/v3_forecast_nowcast.json @@ -0,0 +1,782 @@ +[ + { + "lat": 38.90694, + "lon": -77.03012, + "temp": { + "value": 44.14, + "units": "F" + }, + "wind_speed": { + "value": 9.58, + "units": "mph" + }, + "precipitation": { + "value": 0, + "units": "in/hr" + }, + "wind_direction": { + "value": 320.22, + "units": "degrees" + }, + "observation_time": { + "value": "2021-03-07T18:54:06.493Z" + }, + "weather_code": { + "value": "clear" + } + }, + { + "lat": 38.90694, + "lon": -77.03012, + "temp": { + "value": 44.17, + "units": "F" + }, + "wind_speed": { + "value": 9.59, + "units": "mph" + }, + "precipitation": { + "value": 0, + "units": "in/hr" + }, + "wind_direction": { + "value": 320.22, + "units": "degrees" + }, + "observation_time": { + "value": "2021-03-07T18:55:06.493Z" + }, + "weather_code": { + "value": "clear" + } + }, + { + "lat": 38.90694, + "lon": -77.03012, + "temp": { + "value": 44.19, + "units": "F" + }, + "wind_speed": { + "value": 9.6, + "units": "mph" + }, + "precipitation": { + "value": 0, + "units": "in/hr" + }, + "wind_direction": { + "value": 320.22, + "units": "degrees" + }, + "observation_time": { + "value": "2021-03-07T18:56:06.493Z" + }, + "weather_code": { + "value": "clear" + } + }, + { + "lat": 38.90694, + "lon": -77.03012, + "temp": { + "value": 44.22, + "units": "F" + }, + "wind_speed": { + "value": 9.61, + "units": "mph" + }, + "precipitation": { + "value": 0, + "units": "in/hr" + }, + "wind_direction": { + "value": 320.22, + "units": "degrees" + }, + "observation_time": { + "value": "2021-03-07T18:57:06.493Z" + }, + "weather_code": { + "value": "clear" + } + }, + { + "lat": 38.90694, + "lon": -77.03012, + "temp": { + "value": 44.24, + "units": "F" + }, + "wind_speed": { + "value": 9.62, + "units": "mph" + }, + "precipitation": { + "value": 0, + "units": "in/hr" + }, + "wind_direction": { + "value": 320.22, + "units": "degrees" + }, + "observation_time": { + "value": "2021-03-07T18:58:06.493Z" + }, + "weather_code": { + "value": "clear" + } + }, + { + "lat": 38.90694, + "lon": -77.03012, + "temp": { + "value": 44.27, + "units": "F" + }, + "wind_speed": { + "value": 9.64, + "units": "mph" + }, + "precipitation": { + "value": 0, + "units": "in/hr" + }, + "wind_direction": { + "value": 320.22, + "units": "degrees" + }, + "observation_time": { + "value": "2021-03-07T18:59:06.493Z" + }, + "weather_code": { + "value": "clear" + } + }, + { + "lat": 38.90694, + "lon": -77.03012, + "temp": { + "value": 44.29, + "units": "F" + }, + "wind_speed": { + "value": 9.65, + "units": "mph" + }, + "precipitation": { + "value": 0, + "units": "in/hr" + }, + "wind_direction": { + "value": 326.14, + "units": "degrees" + }, + "observation_time": { + "value": "2021-03-07T19:00:06.493Z" + }, + "weather_code": { + "value": "clear" + } + }, + { + "lat": 38.90694, + "lon": -77.03012, + "temp": { + "value": 44.31, + "units": "F" + }, + "wind_speed": { + "value": 9.64, + "units": "mph" + }, + "precipitation": { + "value": 0, + "units": "in/hr" + }, + "wind_direction": { + "value": 326.14, + "units": "degrees" + }, + "observation_time": { + "value": "2021-03-07T19:01:06.493Z" + }, + "weather_code": { + "value": "clear" + } + }, + { + "lat": 38.90694, + "lon": -77.03012, + "temp": { + "value": 44.33, + "units": "F" + }, + "wind_speed": { + "value": 9.63, + "units": "mph" + }, + "precipitation": { + "value": 0, + "units": "in/hr" + }, + "wind_direction": { + "value": 326.14, + "units": "degrees" + }, + "observation_time": { + "value": "2021-03-07T19:02:06.493Z" + }, + "weather_code": { + "value": "clear" + } + }, + { + "lat": 38.90694, + "lon": -77.03012, + "temp": { + "value": 44.34, + "units": "F" + }, + "wind_speed": { + "value": 9.63, + "units": "mph" + }, + "precipitation": { + "value": 0, + "units": "in/hr" + }, + "wind_direction": { + "value": 326.14, + "units": "degrees" + }, + "observation_time": { + "value": "2021-03-07T19:03:06.493Z" + }, + "weather_code": { + "value": "clear" + } + }, + { + "lat": 38.90694, + "lon": -77.03012, + "temp": { + "value": 44.36, + "units": "F" + }, + "wind_speed": { + "value": 9.62, + "units": "mph" + }, + "precipitation": { + "value": 0, + "units": "in/hr" + }, + "wind_direction": { + "value": 326.14, + "units": "degrees" + }, + "observation_time": { + "value": "2021-03-07T19:04:06.493Z" + }, + "weather_code": { + "value": "clear" + } + }, + { + "lat": 38.90694, + "lon": -77.03012, + "temp": { + "value": 44.38, + "units": "F" + }, + "wind_speed": { + "value": 9.61, + "units": "mph" + }, + "precipitation": { + "value": 0, + "units": "in/hr" + }, + "wind_direction": { + "value": 326.14, + "units": "degrees" + }, + "observation_time": { + "value": "2021-03-07T19:05:06.493Z" + }, + "weather_code": { + "value": "clear" + } + }, + { + "lat": 38.90694, + "lon": -77.03012, + "temp": { + "value": 44.4, + "units": "F" + }, + "wind_speed": { + "value": 9.61, + "units": "mph" + }, + "precipitation": { + "value": 0, + "units": "in/hr" + }, + "wind_direction": { + "value": 326.14, + "units": "degrees" + }, + "observation_time": { + "value": "2021-03-07T19:06:06.493Z" + }, + "weather_code": { + "value": "clear" + } + }, + { + "lat": 38.90694, + "lon": -77.03012, + "temp": { + "value": 44.41, + "units": "F" + }, + "wind_speed": { + "value": 9.6, + "units": "mph" + }, + "precipitation": { + "value": 0, + "units": "in/hr" + }, + "wind_direction": { + "value": 326.14, + "units": "degrees" + }, + "observation_time": { + "value": "2021-03-07T19:07:06.493Z" + }, + "weather_code": { + "value": "clear" + } + }, + { + "lat": 38.90694, + "lon": -77.03012, + "temp": { + "value": 44.43, + "units": "F" + }, + "wind_speed": { + "value": 9.6, + "units": "mph" + }, + "precipitation": { + "value": 0, + "units": "in/hr" + }, + "wind_direction": { + "value": 326.14, + "units": "degrees" + }, + "observation_time": { + "value": "2021-03-07T19:08:06.493Z" + }, + "weather_code": { + "value": "clear" + } + }, + { + "lat": 38.90694, + "lon": -77.03012, + "temp": { + "value": 44.45, + "units": "F" + }, + "wind_speed": { + "value": 9.59, + "units": "mph" + }, + "precipitation": { + "value": 0, + "units": "in/hr" + }, + "wind_direction": { + "value": 326.14, + "units": "degrees" + }, + "observation_time": { + "value": "2021-03-07T19:09:06.493Z" + }, + "weather_code": { + "value": "clear" + } + }, + { + "lat": 38.90694, + "lon": -77.03012, + "temp": { + "value": 44.46, + "units": "F" + }, + "wind_speed": { + "value": 9.58, + "units": "mph" + }, + "precipitation": { + "value": 0, + "units": "in/hr" + }, + "wind_direction": { + "value": 326.14, + "units": "degrees" + }, + "observation_time": { + "value": "2021-03-07T19:10:06.493Z" + }, + "weather_code": { + "value": "clear" + } + }, + { + "lat": 38.90694, + "lon": -77.03012, + "temp": { + "value": 44.48, + "units": "F" + }, + "wind_speed": { + "value": 9.58, + "units": "mph" + }, + "precipitation": { + "value": 0, + "units": "in/hr" + }, + "wind_direction": { + "value": 326.14, + "units": "degrees" + }, + "observation_time": { + "value": "2021-03-07T19:11:06.493Z" + }, + "weather_code": { + "value": "clear" + } + }, + { + "lat": 38.90694, + "lon": -77.03012, + "temp": { + "value": 44.5, + "units": "F" + }, + "wind_speed": { + "value": 9.57, + "units": "mph" + }, + "precipitation": { + "value": 0, + "units": "in/hr" + }, + "wind_direction": { + "value": 326.14, + "units": "degrees" + }, + "observation_time": { + "value": "2021-03-07T19:12:06.493Z" + }, + "weather_code": { + "value": "clear" + } + }, + { + "lat": 38.90694, + "lon": -77.03012, + "temp": { + "value": 44.51, + "units": "F" + }, + "wind_speed": { + "value": 9.57, + "units": "mph" + }, + "precipitation": { + "value": 0, + "units": "in/hr" + }, + "wind_direction": { + "value": 326.14, + "units": "degrees" + }, + "observation_time": { + "value": "2021-03-07T19:13:06.493Z" + }, + "weather_code": { + "value": "clear" + } + }, + { + "lat": 38.90694, + "lon": -77.03012, + "temp": { + "value": 44.53, + "units": "F" + }, + "wind_speed": { + "value": 9.56, + "units": "mph" + }, + "precipitation": { + "value": 0, + "units": "in/hr" + }, + "wind_direction": { + "value": 326.14, + "units": "degrees" + }, + "observation_time": { + "value": "2021-03-07T19:14:06.493Z" + }, + "weather_code": { + "value": "clear" + } + }, + { + "lat": 38.90694, + "lon": -77.03012, + "temp": { + "value": 44.55, + "units": "F" + }, + "wind_speed": { + "value": 9.55, + "units": "mph" + }, + "precipitation": { + "value": 0, + "units": "in/hr" + }, + "wind_direction": { + "value": 326.14, + "units": "degrees" + }, + "observation_time": { + "value": "2021-03-07T19:15:06.493Z" + }, + "weather_code": { + "value": "clear" + } + }, + { + "lat": 38.90694, + "lon": -77.03012, + "temp": { + "value": 44.56, + "units": "F" + }, + "wind_speed": { + "value": 9.55, + "units": "mph" + }, + "precipitation": { + "value": 0, + "units": "in/hr" + }, + "wind_direction": { + "value": 326.14, + "units": "degrees" + }, + "observation_time": { + "value": "2021-03-07T19:16:06.493Z" + }, + "weather_code": { + "value": "clear" + } + }, + { + "lat": 38.90694, + "lon": -77.03012, + "temp": { + "value": 44.58, + "units": "F" + }, + "wind_speed": { + "value": 9.54, + "units": "mph" + }, + "precipitation": { + "value": 0, + "units": "in/hr" + }, + "wind_direction": { + "value": 326.14, + "units": "degrees" + }, + "observation_time": { + "value": "2021-03-07T19:17:06.493Z" + }, + "weather_code": { + "value": "clear" + } + }, + { + "lat": 38.90694, + "lon": -77.03012, + "temp": { + "value": 44.6, + "units": "F" + }, + "wind_speed": { + "value": 9.54, + "units": "mph" + }, + "precipitation": { + "value": 0, + "units": "in/hr" + }, + "wind_direction": { + "value": 326.14, + "units": "degrees" + }, + "observation_time": { + "value": "2021-03-07T19:18:06.493Z" + }, + "weather_code": { + "value": "clear" + } + }, + { + "lat": 38.90694, + "lon": -77.03012, + "temp": { + "value": 44.61, + "units": "F" + }, + "wind_speed": { + "value": 9.53, + "units": "mph" + }, + "precipitation": { + "value": 0, + "units": "in/hr" + }, + "wind_direction": { + "value": 326.14, + "units": "degrees" + }, + "observation_time": { + "value": "2021-03-07T19:19:06.493Z" + }, + "weather_code": { + "value": "clear" + } + }, + { + "lat": 38.90694, + "lon": -77.03012, + "temp": { + "value": 44.63, + "units": "F" + }, + "wind_speed": { + "value": 9.52, + "units": "mph" + }, + "precipitation": { + "value": 0, + "units": "in/hr" + }, + "wind_direction": { + "value": 326.14, + "units": "degrees" + }, + "observation_time": { + "value": "2021-03-07T19:20:06.493Z" + }, + "weather_code": { + "value": "clear" + } + }, + { + "lat": 38.90694, + "lon": -77.03012, + "temp": { + "value": 44.65, + "units": "F" + }, + "wind_speed": { + "value": 9.52, + "units": "mph" + }, + "precipitation": { + "value": 0, + "units": "in/hr" + }, + "wind_direction": { + "value": 326.14, + "units": "degrees" + }, + "observation_time": { + "value": "2021-03-07T19:21:06.493Z" + }, + "weather_code": { + "value": "clear" + } + }, + { + "lat": 38.90694, + "lon": -77.03012, + "temp": { + "value": 44.66, + "units": "F" + }, + "wind_speed": { + "value": 9.51, + "units": "mph" + }, + "precipitation": { + "value": 0, + "units": "in/hr" + }, + "wind_direction": { + "value": 326.14, + "units": "degrees" + }, + "observation_time": { + "value": "2021-03-07T19:22:06.493Z" + }, + "weather_code": { + "value": "clear" + } + }, + { + "lat": 38.90694, + "lon": -77.03012, + "temp": { + "value": 44.68, + "units": "F" + }, + "wind_speed": { + "value": 9.51, + "units": "mph" + }, + "precipitation": { + "value": 0, + "units": "in/hr" + }, + "wind_direction": { + "value": 326.14, + "units": "degrees" + }, + "observation_time": { + "value": "2021-03-07T19:23:06.493Z" + }, + "weather_code": { + "value": "clear" + } + } +] \ No newline at end of file diff --git a/tests/fixtures/climacell/v3_realtime.json b/tests/fixtures/climacell/v3_realtime.json new file mode 100644 index 0000000000000..8ed05fe538377 --- /dev/null +++ b/tests/fixtures/climacell/v3_realtime.json @@ -0,0 +1,38 @@ +{ + "lat": 38.90694, + "lon": -77.03012, + "temp": { + "value": 43.93, + "units": "F" + }, + "wind_speed": { + "value": 9.09, + "units": "mph" + }, + "baro_pressure": { + "value": 30.3605, + "units": "inHg" + }, + "visibility": { + "value": 6.21, + "units": "mi" + }, + "humidity": { + "value": 24.5, + "units": "%" + }, + "wind_direction": { + "value": 320.31, + "units": "degrees" + }, + "weather_code": { + "value": "clear" + }, + "o3": { + "value": 52.625, + "units": "ppb" + }, + "observation_time": { + "value": "2021-03-07T18:54:06.055Z" + } +} \ No newline at end of file diff --git a/tests/fixtures/climacell/v4.json b/tests/fixtures/climacell/v4.json new file mode 100644 index 0000000000000..d667284a4ad89 --- /dev/null +++ b/tests/fixtures/climacell/v4.json @@ -0,0 +1,2360 @@ +{ + "current": { + "temperature": 44.13, + "humidity": 22.71, + "pressureSeaLevel": 30.35, + "windSpeed": 9.33, + "windDirection": 315.14, + "weatherCode": 1000, + "visibility": 8.15, + "pollutantO3": 46.53 + }, + "forecasts": { + "nowcast": [ + { + "startTime": "2021-03-07T17:48:00Z", + "values": { + "temperatureMin": 44.13, + "temperatureMax": 44.13, + "windSpeed": 9.33, + "windDirection": 315.14, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T17:53:00Z", + "values": { + "temperatureMin": 43.9, + "temperatureMax": 43.9, + "windSpeed": 9.31, + "windDirection": 315.14, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T17:58:00Z", + "values": { + "temperatureMin": 43.68, + "temperatureMax": 43.68, + "windSpeed": 9.28, + "windDirection": 315.14, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T18:03:00Z", + "values": { + "temperatureMin": 43.66, + "temperatureMax": 43.66, + "windSpeed": 9.26, + "windDirection": 315.14, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T18:08:00Z", + "values": { + "temperatureMin": 43.79, + "temperatureMax": 43.79, + "windSpeed": 9.22, + "windDirection": 315.14, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T18:13:00Z", + "values": { + "temperatureMin": 43.92, + "temperatureMax": 43.92, + "windSpeed": 9.17, + "windDirection": 315.14, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T18:18:00Z", + "values": { + "temperatureMin": 44.04, + "temperatureMax": 44.04, + "windSpeed": 9.13, + "windDirection": 315.14, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T18:23:00Z", + "values": { + "temperatureMin": 44.17, + "temperatureMax": 44.17, + "windSpeed": 9.06, + "windDirection": 315.14, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T18:28:00Z", + "values": { + "temperatureMin": 44.31, + "temperatureMax": 44.31, + "windSpeed": 9.02, + "windDirection": 315.14, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T18:33:00Z", + "values": { + "temperatureMin": 44.44, + "temperatureMax": 44.44, + "windSpeed": 8.97, + "windDirection": 321.71, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T18:38:00Z", + "values": { + "temperatureMin": 44.56, + "temperatureMax": 44.56, + "windSpeed": 8.93, + "windDirection": 321.71, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T18:43:00Z", + "values": { + "temperatureMin": 44.69, + "temperatureMax": 44.69, + "windSpeed": 8.88, + "windDirection": 321.71, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T18:48:00Z", + "values": { + "temperatureMin": 44.82, + "temperatureMax": 44.82, + "windSpeed": 8.84, + "windDirection": 321.71, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T18:53:00Z", + "values": { + "temperatureMin": 44.94, + "temperatureMax": 44.94, + "windSpeed": 8.79, + "windDirection": 321.71, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T18:58:00Z", + "values": { + "temperatureMin": 45.07, + "temperatureMax": 45.07, + "windSpeed": 8.75, + "windDirection": 321.71, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T19:03:00Z", + "values": { + "temperatureMin": 45.16, + "temperatureMax": 45.16, + "windSpeed": 8.75, + "windDirection": 321.71, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T19:08:00Z", + "values": { + "temperatureMin": 45.23, + "temperatureMax": 45.23, + "windSpeed": 8.75, + "windDirection": 321.71, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T19:13:00Z", + "values": { + "temperatureMin": 45.28, + "temperatureMax": 45.28, + "windSpeed": 8.77, + "windDirection": 321.71, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T19:18:00Z", + "values": { + "temperatureMin": 45.36, + "temperatureMax": 45.36, + "windSpeed": 8.79, + "windDirection": 321.71, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T19:23:00Z", + "values": { + "temperatureMin": 45.43, + "temperatureMax": 45.43, + "windSpeed": 8.81, + "windDirection": 321.71, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T19:28:00Z", + "values": { + "temperatureMin": 45.5, + "temperatureMax": 45.5, + "windSpeed": 8.81, + "windDirection": 321.71, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T19:33:00Z", + "values": { + "temperatureMin": 45.55, + "temperatureMax": 45.55, + "windSpeed": 8.84, + "windDirection": 323.38, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T19:38:00Z", + "values": { + "temperatureMin": 45.63, + "temperatureMax": 45.63, + "windSpeed": 8.86, + "windDirection": 323.38, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T19:43:00Z", + "values": { + "temperatureMin": 45.7, + "temperatureMax": 45.7, + "windSpeed": 8.88, + "windDirection": 323.38, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T19:48:00Z", + "values": { + "temperatureMin": 45.75, + "temperatureMax": 45.75, + "windSpeed": 8.9, + "windDirection": 323.38, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T19:53:00Z", + "values": { + "temperatureMin": 45.82, + "temperatureMax": 45.82, + "windSpeed": 8.9, + "windDirection": 323.38, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T19:58:00Z", + "values": { + "temperatureMin": 45.9, + "temperatureMax": 45.9, + "windSpeed": 8.93, + "windDirection": 323.38, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T20:03:00Z", + "values": { + "temperatureMin": 45.88, + "temperatureMax": 45.88, + "windSpeed": 8.97, + "windDirection": 323.38, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T20:08:00Z", + "values": { + "temperatureMin": 45.82, + "temperatureMax": 45.82, + "windSpeed": 9.02, + "windDirection": 323.38, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T20:13:00Z", + "values": { + "temperatureMin": 45.75, + "temperatureMax": 45.75, + "windSpeed": 9.06, + "windDirection": 323.38, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T20:18:00Z", + "values": { + "temperatureMin": 45.7, + "temperatureMax": 45.7, + "windSpeed": 9.1, + "windDirection": 323.38, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T20:23:00Z", + "values": { + "temperatureMin": 45.63, + "temperatureMax": 45.63, + "windSpeed": 9.15, + "windDirection": 323.38, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T20:28:00Z", + "values": { + "temperatureMin": 45.57, + "temperatureMax": 45.57, + "windSpeed": 9.19, + "windDirection": 323.38, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T20:33:00Z", + "values": { + "temperatureMin": 45.5, + "temperatureMax": 45.5, + "windSpeed": 9.24, + "windDirection": 318.43, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T20:38:00Z", + "values": { + "temperatureMin": 45.45, + "temperatureMax": 45.45, + "windSpeed": 9.28, + "windDirection": 318.43, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T20:43:00Z", + "values": { + "temperatureMin": 45.39, + "temperatureMax": 45.39, + "windSpeed": 9.33, + "windDirection": 318.43, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T20:48:00Z", + "values": { + "temperatureMin": 45.32, + "temperatureMax": 45.32, + "windSpeed": 9.37, + "windDirection": 318.43, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T20:53:00Z", + "values": { + "temperatureMin": 45.27, + "temperatureMax": 45.27, + "windSpeed": 9.42, + "windDirection": 318.43, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T20:58:00Z", + "values": { + "temperatureMin": 45.19, + "temperatureMax": 45.19, + "windSpeed": 9.46, + "windDirection": 318.43, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T21:03:00Z", + "values": { + "temperatureMin": 45.14, + "temperatureMax": 45.14, + "windSpeed": 9.4, + "windDirection": 318.43, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T21:08:00Z", + "values": { + "temperatureMin": 45.07, + "temperatureMax": 45.07, + "windSpeed": 9.24, + "windDirection": 318.43, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T21:13:00Z", + "values": { + "temperatureMin": 45.01, + "temperatureMax": 45.01, + "windSpeed": 9.08, + "windDirection": 318.43, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T21:18:00Z", + "values": { + "temperatureMin": 44.94, + "temperatureMax": 44.94, + "windSpeed": 8.95, + "windDirection": 318.43, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T21:23:00Z", + "values": { + "temperatureMin": 44.89, + "temperatureMax": 44.89, + "windSpeed": 8.79, + "windDirection": 318.43, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T21:28:00Z", + "values": { + "temperatureMin": 44.82, + "temperatureMax": 44.82, + "windSpeed": 8.63, + "windDirection": 318.43, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T21:33:00Z", + "values": { + "temperatureMin": 44.76, + "temperatureMax": 44.76, + "windSpeed": 8.5, + "windDirection": 320.9, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T21:38:00Z", + "values": { + "temperatureMin": 44.69, + "temperatureMax": 44.69, + "windSpeed": 8.34, + "windDirection": 320.9, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T21:43:00Z", + "values": { + "temperatureMin": 44.64, + "temperatureMax": 44.64, + "windSpeed": 8.19, + "windDirection": 320.9, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T21:48:00Z", + "values": { + "temperatureMin": 44.56, + "temperatureMax": 44.56, + "windSpeed": 8.05, + "windDirection": 320.9, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T21:53:00Z", + "values": { + "temperatureMin": 44.51, + "temperatureMax": 44.51, + "windSpeed": 7.9, + "windDirection": 320.9, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T21:58:00Z", + "values": { + "temperatureMin": 44.44, + "temperatureMax": 44.44, + "windSpeed": 7.74, + "windDirection": 320.9, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T22:03:00Z", + "values": { + "temperatureMin": 44.26, + "temperatureMax": 44.26, + "windSpeed": 7.47, + "windDirection": 320.9, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T22:08:00Z", + "values": { + "temperatureMin": 44.01, + "temperatureMax": 44.01, + "windSpeed": 7.14, + "windDirection": 320.9, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T22:13:00Z", + "values": { + "temperatureMin": 43.74, + "temperatureMax": 43.74, + "windSpeed": 6.78, + "windDirection": 320.9, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T22:18:00Z", + "values": { + "temperatureMin": 43.48, + "temperatureMax": 43.48, + "windSpeed": 6.44, + "windDirection": 320.9, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T22:23:00Z", + "values": { + "temperatureMin": 43.23, + "temperatureMax": 43.23, + "windSpeed": 6.08, + "windDirection": 320.9, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T22:28:00Z", + "values": { + "temperatureMin": 42.98, + "temperatureMax": 42.98, + "windSpeed": 5.75, + "windDirection": 320.9, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T22:33:00Z", + "values": { + "temperatureMin": 42.71, + "temperatureMax": 42.71, + "windSpeed": 5.39, + "windDirection": 322.11, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T22:38:00Z", + "values": { + "temperatureMin": 42.46, + "temperatureMax": 42.46, + "windSpeed": 5.06, + "windDirection": 322.11, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T22:43:00Z", + "values": { + "temperatureMin": 42.21, + "temperatureMax": 42.21, + "windSpeed": 4.7, + "windDirection": 322.11, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T22:48:00Z", + "values": { + "temperatureMin": 41.94, + "temperatureMax": 41.94, + "windSpeed": 4.36, + "windDirection": 322.11, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T22:53:00Z", + "values": { + "temperatureMin": 41.68, + "temperatureMax": 41.68, + "windSpeed": 4, + "windDirection": 322.11, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T22:58:00Z", + "values": { + "temperatureMin": 41.43, + "temperatureMax": 41.43, + "windSpeed": 3.67, + "windDirection": 322.11, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T23:03:00Z", + "values": { + "temperatureMin": 41.16, + "temperatureMax": 41.16, + "windSpeed": 3.6, + "windDirection": 322.11, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T23:08:00Z", + "values": { + "temperatureMin": 40.91, + "temperatureMax": 40.91, + "windSpeed": 3.76, + "windDirection": 322.11, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T23:13:00Z", + "values": { + "temperatureMin": 40.66, + "temperatureMax": 40.66, + "windSpeed": 3.91, + "windDirection": 322.11, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T23:18:00Z", + "values": { + "temperatureMin": 40.41, + "temperatureMax": 40.41, + "windSpeed": 4.05, + "windDirection": 322.11, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T23:23:00Z", + "values": { + "temperatureMin": 40.14, + "temperatureMax": 40.14, + "windSpeed": 4.21, + "windDirection": 322.11, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T23:28:00Z", + "values": { + "temperatureMin": 39.88, + "temperatureMax": 39.88, + "windSpeed": 4.36, + "windDirection": 322.11, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T23:33:00Z", + "values": { + "temperatureMin": 39.63, + "temperatureMax": 39.63, + "windSpeed": 4.5, + "windDirection": 295.94, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T23:38:00Z", + "values": { + "temperatureMin": 39.38, + "temperatureMax": 39.38, + "windSpeed": 4.65, + "windDirection": 295.94, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T23:43:00Z", + "values": { + "temperatureMin": 39.11, + "temperatureMax": 39.11, + "windSpeed": 4.79, + "windDirection": 295.94, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + } + ], + "hourly": [ + { + "startTime": "2021-03-07T17:48:00Z", + "values": { + "temperatureMin": 44.13, + "temperatureMax": 44.13, + "windSpeed": 9.33, + "windDirection": 315.14, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T18:48:00Z", + "values": { + "temperatureMin": 44.82, + "temperatureMax": 44.82, + "windSpeed": 8.84, + "windDirection": 321.71, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T19:48:00Z", + "values": { + "temperatureMin": 45.75, + "temperatureMax": 45.75, + "windSpeed": 8.9, + "windDirection": 323.38, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T20:48:00Z", + "values": { + "temperatureMin": 45.32, + "temperatureMax": 45.32, + "windSpeed": 9.37, + "windDirection": 318.43, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T21:48:00Z", + "values": { + "temperatureMin": 44.56, + "temperatureMax": 44.56, + "windSpeed": 8.05, + "windDirection": 320.9, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T22:48:00Z", + "values": { + "temperatureMin": 41.94, + "temperatureMax": 41.94, + "windSpeed": 4.36, + "windDirection": 322.11, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T23:48:00Z", + "values": { + "temperatureMin": 38.86, + "temperatureMax": 38.86, + "windSpeed": 4.94, + "windDirection": 295.94, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T00:48:00Z", + "values": { + "temperatureMin": 36.18, + "temperatureMax": 36.18, + "windSpeed": 5.59, + "windDirection": 11.94, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T01:48:00Z", + "values": { + "temperatureMin": 34.3, + "temperatureMax": 34.3, + "windSpeed": 5.57, + "windDirection": 13.68, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T02:48:00Z", + "values": { + "temperatureMin": 32.88, + "temperatureMax": 32.88, + "windSpeed": 5.41, + "windDirection": 14.93, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T03:48:00Z", + "values": { + "temperatureMin": 31.91, + "temperatureMax": 31.91, + "windSpeed": 4.61, + "windDirection": 26.07, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T04:48:00Z", + "values": { + "temperatureMin": 29.17, + "temperatureMax": 29.17, + "windSpeed": 2.59, + "windDirection": 51.27, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T05:48:00Z", + "values": { + "temperatureMin": 27.37, + "temperatureMax": 27.37, + "windSpeed": 3.31, + "windDirection": 343.25, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T06:48:00Z", + "values": { + "temperatureMin": 26.73, + "temperatureMax": 26.73, + "windSpeed": 4.27, + "windDirection": 341.46, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T07:48:00Z", + "values": { + "temperatureMin": 26.38, + "temperatureMax": 26.38, + "windSpeed": 3.53, + "windDirection": 322.34, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T08:48:00Z", + "values": { + "temperatureMin": 26.15, + "temperatureMax": 26.15, + "windSpeed": 3.65, + "windDirection": 294.69, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T09:48:00Z", + "values": { + "temperatureMin": 30.07, + "temperatureMax": 30.07, + "windSpeed": 3.2, + "windDirection": 325.32, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T10:48:00Z", + "values": { + "temperatureMin": 31.03, + "temperatureMax": 31.03, + "windSpeed": 2.84, + "windDirection": 322.27, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T11:48:00Z", + "values": { + "temperatureMin": 27.23, + "temperatureMax": 27.23, + "windSpeed": 5.59, + "windDirection": 310.14, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T12:48:00Z", + "values": { + "temperatureMin": 29.21, + "temperatureMax": 29.21, + "windSpeed": 7.05, + "windDirection": 324.8, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T13:48:00Z", + "values": { + "temperatureMin": 33.19, + "temperatureMax": 33.19, + "windSpeed": 6.46, + "windDirection": 335.16, + "weatherCode": 1101, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T14:48:00Z", + "values": { + "temperatureMin": 37.02, + "temperatureMax": 37.02, + "windSpeed": 5.88, + "windDirection": 324.49, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T15:48:00Z", + "values": { + "temperatureMin": 40.01, + "temperatureMax": 40.01, + "windSpeed": 5.55, + "windDirection": 310.68, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T16:48:00Z", + "values": { + "temperatureMin": 42.37, + "temperatureMax": 42.37, + "windSpeed": 5.46, + "windDirection": 304.18, + "weatherCode": 1101, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T17:48:00Z", + "values": { + "temperatureMin": 44.62, + "temperatureMax": 44.62, + "windSpeed": 4.99, + "windDirection": 301.19, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T18:48:00Z", + "values": { + "temperatureMin": 46.78, + "temperatureMax": 46.78, + "windSpeed": 4.72, + "windDirection": 295.05, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T19:48:00Z", + "values": { + "temperatureMin": 48.42, + "temperatureMax": 48.42, + "windSpeed": 4.81, + "windDirection": 287.4, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T20:48:00Z", + "values": { + "temperatureMin": 49.28, + "temperatureMax": 49.28, + "windSpeed": 4.74, + "windDirection": 282.48, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T21:48:00Z", + "values": { + "temperatureMin": 48.72, + "temperatureMax": 48.72, + "windSpeed": 2.51, + "windDirection": 268.74, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T22:48:00Z", + "values": { + "temperatureMin": 44.37, + "temperatureMax": 44.37, + "windSpeed": 3.56, + "windDirection": 180.04, + "weatherCode": 1101, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T23:48:00Z", + "values": { + "temperatureMin": 39.9, + "temperatureMax": 39.9, + "windSpeed": 4.68, + "windDirection": 177.89, + "weatherCode": 1101, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T00:48:00Z", + "values": { + "temperatureMin": 37.87, + "temperatureMax": 37.87, + "windSpeed": 5.21, + "windDirection": 197.47, + "weatherCode": 1101, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T01:48:00Z", + "values": { + "temperatureMin": 36.91, + "temperatureMax": 36.91, + "windSpeed": 5.46, + "windDirection": 209.77, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T02:48:00Z", + "values": { + "temperatureMin": 36.64, + "temperatureMax": 36.64, + "windSpeed": 6.11, + "windDirection": 210.14, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T03:48:00Z", + "values": { + "temperatureMin": 36.63, + "temperatureMax": 36.63, + "windSpeed": 6.4, + "windDirection": 216, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T04:48:00Z", + "values": { + "temperatureMin": 36.23, + "temperatureMax": 36.23, + "windSpeed": 6.22, + "windDirection": 223.92, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T05:48:00Z", + "values": { + "temperatureMin": 35.58, + "temperatureMax": 35.58, + "windSpeed": 5.75, + "windDirection": 229.68, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T06:48:00Z", + "values": { + "temperatureMin": 34.68, + "temperatureMax": 34.68, + "windSpeed": 5.21, + "windDirection": 235.24, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T07:48:00Z", + "values": { + "temperatureMin": 33.69, + "temperatureMax": 33.69, + "windSpeed": 4.81, + "windDirection": 237.24, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T08:48:00Z", + "values": { + "temperatureMin": 32.74, + "temperatureMax": 32.74, + "windSpeed": 4.52, + "windDirection": 239.35, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T09:48:00Z", + "values": { + "temperatureMin": 32.05, + "temperatureMax": 32.05, + "windSpeed": 4.32, + "windDirection": 245.68, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T10:48:00Z", + "values": { + "temperatureMin": 31.57, + "temperatureMax": 31.57, + "windSpeed": 4.14, + "windDirection": 248.11, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T11:48:00Z", + "values": { + "temperatureMin": 32.92, + "temperatureMax": 32.92, + "windSpeed": 4.32, + "windDirection": 249.54, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T12:48:00Z", + "values": { + "temperatureMin": 38.5, + "temperatureMax": 38.5, + "windSpeed": 4.7, + "windDirection": 253.3, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T13:48:00Z", + "values": { + "temperatureMin": 46.08, + "temperatureMax": 46.08, + "windSpeed": 4.41, + "windDirection": 258.49, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T14:48:00Z", + "values": { + "temperatureMin": 53.26, + "temperatureMax": 53.26, + "windSpeed": 4.9, + "windDirection": 260.49, + "weatherCode": 1101, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T15:48:00Z", + "values": { + "temperatureMin": 58.15, + "temperatureMax": 58.15, + "windSpeed": 5.55, + "windDirection": 261.29, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T16:48:00Z", + "values": { + "temperatureMin": 61.56, + "temperatureMax": 61.56, + "windSpeed": 6.35, + "windDirection": 264.3, + "weatherCode": 1101, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T17:48:00Z", + "values": { + "temperatureMin": 64, + "temperatureMax": 64, + "windSpeed": 6.6, + "windDirection": 257.54, + "weatherCode": 1101, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T18:48:00Z", + "values": { + "temperatureMin": 65.79, + "temperatureMax": 65.79, + "windSpeed": 6.96, + "windDirection": 253.12, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T19:48:00Z", + "values": { + "temperatureMin": 66.74, + "temperatureMax": 66.74, + "windSpeed": 6.8, + "windDirection": 259.46, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T20:48:00Z", + "values": { + "temperatureMin": 66.96, + "temperatureMax": 66.96, + "windSpeed": 6.33, + "windDirection": 294.25, + "weatherCode": 1101, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T21:48:00Z", + "values": { + "temperatureMin": 64.35, + "temperatureMax": 64.35, + "windSpeed": 3.91, + "windDirection": 279.37, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T22:48:00Z", + "values": { + "temperatureMin": 61.07, + "temperatureMax": 61.07, + "windSpeed": 3.65, + "windDirection": 218.19, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T23:48:00Z", + "values": { + "temperatureMin": 56.3, + "temperatureMax": 56.3, + "windSpeed": 4.09, + "windDirection": 208.3, + "weatherCode": 1101, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T00:48:00Z", + "values": { + "temperatureMin": 53.19, + "temperatureMax": 53.19, + "windSpeed": 4.21, + "windDirection": 216.42, + "weatherCode": 1102, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T01:48:00Z", + "values": { + "temperatureMin": 51.94, + "temperatureMax": 51.94, + "windSpeed": 3.38, + "windDirection": 257.19, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T02:48:00Z", + "values": { + "temperatureMin": 49.82, + "temperatureMax": 49.82, + "windSpeed": 2.71, + "windDirection": 288.85, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T03:48:00Z", + "values": { + "temperatureMin": 48.24, + "temperatureMax": 48.24, + "windSpeed": 2.8, + "windDirection": 334.41, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T04:48:00Z", + "values": { + "temperatureMin": 47.44, + "temperatureMax": 47.44, + "windSpeed": 2.26, + "windDirection": 342.01, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T05:48:00Z", + "values": { + "temperatureMin": 45.59, + "temperatureMax": 45.59, + "windSpeed": 2.35, + "windDirection": 2.43, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T06:48:00Z", + "values": { + "temperatureMin": 43.43, + "temperatureMax": 43.43, + "windSpeed": 2.3, + "windDirection": 336.56, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T07:48:00Z", + "values": { + "temperatureMin": 41.11, + "temperatureMax": 41.11, + "windSpeed": 2.71, + "windDirection": 4.41, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T08:48:00Z", + "values": { + "temperatureMin": 39.58, + "temperatureMax": 39.58, + "windSpeed": 3.4, + "windDirection": 21.26, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T09:48:00Z", + "values": { + "temperatureMin": 39.85, + "temperatureMax": 39.85, + "windSpeed": 3.31, + "windDirection": 22.76, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T10:48:00Z", + "values": { + "temperatureMin": 37.85, + "temperatureMax": 37.85, + "windSpeed": 4.03, + "windDirection": 29.3, + "weatherCode": 1101, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T11:48:00Z", + "values": { + "temperatureMin": 38.97, + "temperatureMax": 38.97, + "windSpeed": 3.15, + "windDirection": 21.82, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T12:48:00Z", + "values": { + "temperatureMin": 44.31, + "temperatureMax": 44.31, + "windSpeed": 3.53, + "windDirection": 14.25, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T13:48:00Z", + "values": { + "temperatureMin": 50.25, + "temperatureMax": 50.25, + "windSpeed": 2.82, + "windDirection": 42.41, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T14:48:00Z", + "values": { + "temperatureMin": 54.97, + "temperatureMax": 54.97, + "windSpeed": 2.53, + "windDirection": 87.81, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T15:48:00Z", + "values": { + "temperatureMin": 58.46, + "temperatureMax": 58.46, + "windSpeed": 3.09, + "windDirection": 125.82, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T16:48:00Z", + "values": { + "temperatureMin": 61.21, + "temperatureMax": 61.21, + "windSpeed": 4.03, + "windDirection": 157.54, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T17:48:00Z", + "values": { + "temperatureMin": 63.36, + "temperatureMax": 63.36, + "windSpeed": 5.21, + "windDirection": 166.66, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T18:48:00Z", + "values": { + "temperatureMin": 64.83, + "temperatureMax": 64.83, + "windSpeed": 6.93, + "windDirection": 189.24, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T19:48:00Z", + "values": { + "temperatureMin": 65.23, + "temperatureMax": 65.23, + "windSpeed": 8.95, + "windDirection": 194.58, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T20:48:00Z", + "values": { + "temperatureMin": 64.98, + "temperatureMax": 64.98, + "windSpeed": 9.4, + "windDirection": 193.22, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T21:48:00Z", + "values": { + "temperatureMin": 64.06, + "temperatureMax": 64.06, + "windSpeed": 8.55, + "windDirection": 186.39, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T22:48:00Z", + "values": { + "temperatureMin": 61.9, + "temperatureMax": 61.9, + "windSpeed": 7.49, + "windDirection": 171.81, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T23:48:00Z", + "values": { + "temperatureMin": 59.4, + "temperatureMax": 59.4, + "windSpeed": 7.54, + "windDirection": 165.51, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T00:48:00Z", + "values": { + "temperatureMin": 57.63, + "temperatureMax": 57.63, + "windSpeed": 8.12, + "windDirection": 171.94, + "weatherCode": 1102, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T01:48:00Z", + "values": { + "temperatureMin": 56.17, + "temperatureMax": 56.17, + "windSpeed": 8.7, + "windDirection": 176.84, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T02:48:00Z", + "values": { + "temperatureMin": 55.36, + "temperatureMax": 55.36, + "windSpeed": 9.42, + "windDirection": 184.14, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T03:48:00Z", + "values": { + "temperatureMin": 54.88, + "temperatureMax": 54.88, + "windSpeed": 10, + "windDirection": 195.54, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T04:48:00Z", + "values": { + "temperatureMin": 54.14, + "temperatureMax": 54.14, + "windSpeed": 10.4, + "windDirection": 200.56, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T05:48:00Z", + "values": { + "temperatureMin": 53.46, + "temperatureMax": 53.46, + "windSpeed": 10.04, + "windDirection": 198.08, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T06:48:00Z", + "values": { + "temperatureMin": 52.11, + "temperatureMax": 52.11, + "windSpeed": 10.02, + "windDirection": 199.54, + "weatherCode": 1101, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T07:48:00Z", + "values": { + "temperatureMin": 51.64, + "temperatureMax": 51.64, + "windSpeed": 10.51, + "windDirection": 202.73, + "weatherCode": 1102, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T08:48:00Z", + "values": { + "temperatureMin": 50.79, + "temperatureMax": 50.79, + "windSpeed": 10.38, + "windDirection": 203.35, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T09:48:00Z", + "values": { + "temperatureMin": 49.93, + "temperatureMax": 49.93, + "windSpeed": 9.51, + "windDirection": 210.36, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T10:48:00Z", + "values": { + "temperatureMin": 49.1, + "temperatureMax": 49.1, + "windSpeed": 8.61, + "windDirection": 210.6, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T11:48:00Z", + "values": { + "temperatureMin": 48.42, + "temperatureMax": 48.42, + "windSpeed": 9.15, + "windDirection": 211.29, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T12:48:00Z", + "values": { + "temperatureMin": 48.9, + "temperatureMax": 48.9, + "windSpeed": 10.25, + "windDirection": 215.59, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T13:48:00Z", + "values": { + "temperatureMin": 50.54, + "temperatureMax": 50.54, + "windSpeed": 10.18, + "windDirection": 215.48, + "weatherCode": 1102, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T14:48:00Z", + "values": { + "temperatureMin": 53.19, + "temperatureMax": 53.19, + "windSpeed": 9.4, + "windDirection": 208.76, + "weatherCode": 1101, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T15:48:00Z", + "values": { + "temperatureMin": 56.19, + "temperatureMax": 56.19, + "windSpeed": 9.73, + "windDirection": 197.59, + "weatherCode": 1101, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T16:48:00Z", + "values": { + "temperatureMin": 59.34, + "temperatureMax": 59.34, + "windSpeed": 10.69, + "windDirection": 204.29, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T17:48:00Z", + "values": { + "temperatureMin": 62.35, + "temperatureMax": 62.35, + "windSpeed": 11.81, + "windDirection": 204.56, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T18:48:00Z", + "values": { + "temperatureMin": 64.6, + "temperatureMax": 64.6, + "windSpeed": 13.09, + "windDirection": 206.85, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T19:48:00Z", + "values": { + "temperatureMin": 65.91, + "temperatureMax": 65.91, + "windSpeed": 13.82, + "windDirection": 204.82, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T20:48:00Z", + "values": { + "temperatureMin": 66.22, + "temperatureMax": 66.22, + "windSpeed": 14.54, + "windDirection": 208.43, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T21:48:00Z", + "values": { + "temperatureMin": 65.46, + "temperatureMax": 65.46, + "windSpeed": 13.2, + "windDirection": 208.3, + "weatherCode": 1101, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T22:48:00Z", + "values": { + "temperatureMin": 64.35, + "temperatureMax": 64.35, + "windSpeed": 12.35, + "windDirection": 208.58, + "weatherCode": 1101, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T23:48:00Z", + "values": { + "temperatureMin": 62.85, + "temperatureMax": 62.85, + "windSpeed": 12.86, + "windDirection": 205.39, + "weatherCode": 1101, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-12T00:48:00Z", + "values": { + "temperatureMin": 61.75, + "temperatureMax": 61.75, + "windSpeed": 14.7, + "windDirection": 209.51, + "weatherCode": 1102, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-12T01:48:00Z", + "values": { + "temperatureMin": 61.2, + "temperatureMax": 61.2, + "windSpeed": 15.57, + "windDirection": 211.47, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-12T02:48:00Z", + "values": { + "temperatureMin": 60.46, + "temperatureMax": 60.46, + "windSpeed": 14.94, + "windDirection": 211.57, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-12T03:48:00Z", + "values": { + "temperatureMin": 59.94, + "temperatureMax": 59.94, + "windSpeed": 14.29, + "windDirection": 208.93, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-12T04:48:00Z", + "values": { + "temperatureMin": 59.52, + "temperatureMax": 59.52, + "windSpeed": 14.36, + "windDirection": 217.91, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + } + ], + "daily": [ + { + "startTime": "2021-03-07T11:00:00Z", + "values": { + "temperatureMin": 26.11, + "temperatureMax": 45.93, + "windSpeed": 9.49, + "windDirection": 239.6, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T11:00:00Z", + "values": { + "temperatureMin": 26.28, + "temperatureMax": 49.42, + "windSpeed": 7.24, + "windDirection": 262.82, + "weatherCode": 1102, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T11:00:00Z", + "values": { + "temperatureMin": 31.48, + "temperatureMax": 66.98, + "windSpeed": 7.05, + "windDirection": 229.3, + "weatherCode": 1102, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T11:00:00Z", + "values": { + "temperatureMin": 37.32, + "temperatureMax": 65.28, + "windSpeed": 10.64, + "windDirection": 149.91, + "weatherCode": 1102, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T11:00:00Z", + "values": { + "temperatureMin": 48.29, + "temperatureMax": 66.25, + "windSpeed": 15.69, + "windDirection": 210.45, + "weatherCode": 1102, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-12T11:00:00Z", + "values": { + "temperatureMin": 53.83, + "temperatureMax": 67.91, + "windSpeed": 12.3, + "windDirection": 217.98, + "weatherCode": 4000, + "precipitationIntensityAvg": 0.0002, + "precipitationProbability": 25 + } + }, + { + "startTime": "2021-03-13T11:00:00Z", + "values": { + "temperatureMin": 42.91, + "temperatureMax": 54.48, + "windSpeed": 9.72, + "windDirection": 58.79, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 25 + } + }, + { + "startTime": "2021-03-14T10:00:00Z", + "values": { + "temperatureMin": 33.35, + "temperatureMax": 42.91, + "windSpeed": 16.25, + "windDirection": 70.25, + "weatherCode": 5101, + "precipitationIntensityAvg": 0.0393, + "precipitationProbability": 95 + } + }, + { + "startTime": "2021-03-15T10:00:00Z", + "values": { + "temperatureMin": 29.35, + "temperatureMax": 43.67, + "windSpeed": 15.89, + "windDirection": 84.47, + "weatherCode": 5001, + "precipitationIntensityAvg": 0.0024, + "precipitationProbability": 55 + } + }, + { + "startTime": "2021-03-16T10:00:00Z", + "values": { + "temperatureMin": 29.1, + "temperatureMax": 43, + "windSpeed": 6.71, + "windDirection": 103.85, + "weatherCode": 1102, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-17T10:00:00Z", + "values": { + "temperatureMin": 34.32, + "temperatureMax": 52.4, + "windSpeed": 7.27, + "windDirection": 145.41, + "weatherCode": 1102, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-18T10:00:00Z", + "values": { + "temperatureMin": 41.32, + "temperatureMax": 54.07, + "windSpeed": 6.58, + "windDirection": 62.99, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 10 + } + }, + { + "startTime": "2021-03-19T10:00:00Z", + "values": { + "temperatureMin": 39.4, + "temperatureMax": 48.94, + "windSpeed": 13.91, + "windDirection": 68.54, + "weatherCode": 4000, + "precipitationIntensityAvg": 0.0048, + "precipitationProbability": 55 + } + }, + { + "startTime": "2021-03-20T10:00:00Z", + "values": { + "temperatureMin": 35.06, + "temperatureMax": 40.12, + "windSpeed": 17.35, + "windDirection": 56.98, + "weatherCode": 5001, + "precipitationIntensityAvg": 0.002, + "precipitationProbability": 33.3 + } + }, + { + "startTime": "2021-03-21T10:00:00Z", + "values": { + "temperatureMin": 33.66, + "temperatureMax": 66.54, + "windSpeed": 15.93, + "windDirection": 82.57, + "weatherCode": 5001, + "precipitationIntensityAvg": 0.0004, + "precipitationProbability": 45 + } + } + ] + } +} \ No newline at end of file From c28d4e8e0135d2c3edd7472e133d951b80180bb7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 5 Apr 2021 09:50:22 -1000 Subject: [PATCH 0070/1317] Clean and optimize systemmonitor (#48699) - Remove unneeded excinfo to _LOGGER.exception - Use f-strings - Switch last_boot to utc - Cache psutil/os calls used by multiple attributes in the same update cycle --- .../components/systemmonitor/sensor.py | 78 +++++++++++++------ 1 file changed, 56 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index a9cb2edb4c831..8d0680b72c002 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -4,6 +4,7 @@ import asyncio from dataclasses import dataclass import datetime +from functools import lru_cache import logging import os import socket @@ -246,7 +247,7 @@ def _update_sensors() -> None: try: state, value, update_time = _update(type_, data) except Exception as ex: # pylint: disable=broad-except - _LOGGER.exception("Error updating sensor: %s", type_, exc_info=ex) + _LOGGER.exception("Error updating sensor: %s", type_) data.last_exception = ex else: data.state = state @@ -254,6 +255,14 @@ def _update_sensors() -> None: data.update_time = update_time data.last_exception = None + # Only fetch these once per iteration as we use the same + # data source multiple times in _update + _disk_usage.cache_clear() + _swap_memory.cache_clear() + _virtual_memory.cache_clear() + _net_io_counters.cache_clear() + _getloadavg.cache_clear() + async def _async_update_data(*_: Any) -> None: """Update all sensors in one executor jump.""" if _update_lock.locked(): @@ -289,14 +298,14 @@ def __init__( ) -> None: """Initialize the sensor.""" self._type: str = sensor_type - self._name: str = "{} {}".format(self.sensor_type[SENSOR_TYPE_NAME], argument) + self._name: str = f"{self.sensor_type[SENSOR_TYPE_NAME]} {argument}".rstrip() self._unique_id: str = slugify(f"{sensor_type}_{argument}") self._sensor_registry = sensor_registry @property def name(self) -> str: """Return the name of the sensor.""" - return self._name.rstrip() + return self._name @property def unique_id(self) -> str: @@ -362,24 +371,24 @@ def _update( update_time = None if type_ == "disk_use_percent": - state = psutil.disk_usage(data.argument).percent + state = _disk_usage(data.argument).percent elif type_ == "disk_use": - state = round(psutil.disk_usage(data.argument).used / 1024 ** 3, 1) + state = round(_disk_usage(data.argument).used / 1024 ** 3, 1) elif type_ == "disk_free": - state = round(psutil.disk_usage(data.argument).free / 1024 ** 3, 1) + state = round(_disk_usage(data.argument).free / 1024 ** 3, 1) elif type_ == "memory_use_percent": - state = psutil.virtual_memory().percent + state = _virtual_memory().percent elif type_ == "memory_use": - virtual_memory = psutil.virtual_memory() + virtual_memory = _virtual_memory() state = round((virtual_memory.total - virtual_memory.available) / 1024 ** 2, 1) elif type_ == "memory_free": - state = round(psutil.virtual_memory().available / 1024 ** 2, 1) + state = round(_virtual_memory().available / 1024 ** 2, 1) elif type_ == "swap_use_percent": - state = psutil.swap_memory().percent + state = _swap_memory().percent elif type_ == "swap_use": - state = round(psutil.swap_memory().used / 1024 ** 2, 1) + state = round(_swap_memory().used / 1024 ** 2, 1) elif type_ == "swap_free": - state = round(psutil.swap_memory().free / 1024 ** 2, 1) + state = round(_swap_memory().free / 1024 ** 2, 1) elif type_ == "processor_use": state = round(psutil.cpu_percent(interval=None)) elif type_ == "processor_temperature": @@ -398,20 +407,20 @@ def _update( err.name, ) elif type_ in ["network_out", "network_in"]: - counters = psutil.net_io_counters(pernic=True) + counters = _net_io_counters() if data.argument in counters: counter = counters[data.argument][IO_COUNTER[type_]] state = round(counter / 1024 ** 2, 1) else: state = None elif type_ in ["packets_out", "packets_in"]: - counters = psutil.net_io_counters(pernic=True) + counters = _net_io_counters() if data.argument in counters: state = counters[data.argument][IO_COUNTER[type_]] else: state = None elif type_ in ["throughput_network_out", "throughput_network_in"]: - counters = psutil.net_io_counters(pernic=True) + counters = _net_io_counters() if data.argument in counters: counter = counters[data.argument][IO_COUNTER[type_]] now = dt_util.utcnow() @@ -429,7 +438,7 @@ def _update( else: state = None elif type_ in ["ipv4_address", "ipv6_address"]: - addresses = psutil.net_if_addrs() + addresses = _net_io_counters() if data.argument in addresses: for addr in addresses[data.argument]: if addr.family == IF_ADDRS_FAMILY[type_]: @@ -439,21 +448,46 @@ def _update( elif type_ == "last_boot": # Only update on initial setup if data.state is None: - state = dt_util.as_local( - dt_util.utc_from_timestamp(psutil.boot_time()) - ).isoformat() + state = dt_util.utc_from_timestamp(psutil.boot_time()).isoformat() else: state = data.state elif type_ == "load_1m": - state = round(os.getloadavg()[0], 2) + state = round(_getloadavg()[0], 2) elif type_ == "load_5m": - state = round(os.getloadavg()[1], 2) + state = round(_getloadavg()[1], 2) elif type_ == "load_15m": - state = round(os.getloadavg()[2], 2) + state = round(_getloadavg()[2], 2) return state, value, update_time +# When we drop python 3.8 support these can be switched to +# @cache https://docs.python.org/3.9/library/functools.html#functools.cache +@lru_cache(maxsize=None) +def _disk_usage(path: str) -> Any: + return psutil.disk_usage(path) + + +@lru_cache(maxsize=None) +def _swap_memory() -> Any: + return psutil.swap_memory() + + +@lru_cache(maxsize=None) +def _virtual_memory() -> Any: + return psutil.virtual_memory() + + +@lru_cache(maxsize=None) +def _net_io_counters() -> Any: + return psutil.net_io_counters(pernic=True) + + +@lru_cache(maxsize=None) +def _getloadavg() -> tuple[float, float, float]: + return os.getloadavg() + + def _read_cpu_temperature() -> float | None: """Attempt to read CPU / processor temperature.""" temps = psutil.sensors_temperatures() From f3399aa8aafa35278c19d19b9ae3cd6eb0125dcb Mon Sep 17 00:00:00 2001 From: Dylan Gore Date: Mon, 5 Apr 2021 22:23:57 +0100 Subject: [PATCH 0071/1317] =?UTF-8?q?Add=20a=20new=20weather=20integration?= =?UTF-8?q?=20-=20Met=20=C3=89ireann=20(#39429)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added a new weather integration - Met Éireann * Fix codespell error * Update met_eireann to use CoordinatorEntity * Remove deprecated platform setup * Fix merge conflict * Remove unnecessary onboarding/home tracking code * Use common strings for config flow * Remove unnecessary code * Switch to using unique IDs in config flow * Use constants where possible * Fix failing tests * Fix isort errors * Remove unnecessary DataUpdateCoordinator class * Add device info * Explicitly define forecast data * Disable hourly forecast entity by default * Update config flow to reflect requested changes * Cleanup code * Update entity naming to match other similar components * Convert forecast time to UTC * Fix test coverage * Update test coverage * Remove elevation conversion * Update translations for additional clarity * Remove en-GB translation --- .coveragerc | 2 + CODEOWNERS | 1 + .../components/met_eireann/__init__.py | 84 ++++++++ .../components/met_eireann/config_flow.py | 48 +++++ homeassistant/components/met_eireann/const.py | 121 +++++++++++ .../components/met_eireann/manifest.json | 8 + .../components/met_eireann/strings.json | 17 ++ .../met_eireann/translations/en.json | 23 +++ .../components/met_eireann/weather.py | 191 ++++++++++++++++++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/met_eireann/__init__.py | 27 +++ tests/components/met_eireann/conftest.py | 22 ++ .../met_eireann/test_config_flow.py | 95 +++++++++ tests/components/met_eireann/test_init.py | 19 ++ tests/components/met_eireann/test_weather.py | 31 +++ 17 files changed, 696 insertions(+) create mode 100644 homeassistant/components/met_eireann/__init__.py create mode 100644 homeassistant/components/met_eireann/config_flow.py create mode 100644 homeassistant/components/met_eireann/const.py create mode 100644 homeassistant/components/met_eireann/manifest.json create mode 100644 homeassistant/components/met_eireann/strings.json create mode 100644 homeassistant/components/met_eireann/translations/en.json create mode 100644 homeassistant/components/met_eireann/weather.py create mode 100644 tests/components/met_eireann/__init__.py create mode 100644 tests/components/met_eireann/conftest.py create mode 100644 tests/components/met_eireann/test_config_flow.py create mode 100644 tests/components/met_eireann/test_init.py create mode 100644 tests/components/met_eireann/test_weather.py diff --git a/.coveragerc b/.coveragerc index 2dcf43ef6975e..2845a1768a8dc 100644 --- a/.coveragerc +++ b/.coveragerc @@ -577,6 +577,8 @@ omit = homeassistant/components/melcloud/water_heater.py homeassistant/components/message_bird/notify.py homeassistant/components/met/weather.py + homeassistant/components/met_eireann/__init__.py + homeassistant/components/met_eireann/weather.py homeassistant/components/meteo_france/__init__.py homeassistant/components/meteo_france/const.py homeassistant/components/meteo_france/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index a863b469cdf84..51cd7ed43ccb1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -278,6 +278,7 @@ homeassistant/components/mediaroom/* @dgomes homeassistant/components/melcloud/* @vilppuvuorinen homeassistant/components/melissa/* @kennedyshead homeassistant/components/met/* @danielhiversen @thimic +homeassistant/components/met_eireann/* @DylanGore homeassistant/components/meteo_france/* @hacf-fr @oncleben31 @Quentame homeassistant/components/meteoalarm/* @rolfberkenbosch homeassistant/components/metoffice/* @MrHarcombe diff --git a/homeassistant/components/met_eireann/__init__.py b/homeassistant/components/met_eireann/__init__.py new file mode 100644 index 0000000000000..365e4dbafb364 --- /dev/null +++ b/homeassistant/components/met_eireann/__init__.py @@ -0,0 +1,84 @@ +"""The met_eireann component.""" +from datetime import timedelta +import logging + +import meteireann + +from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +import homeassistant.util.dt as dt_util + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +UPDATE_INTERVAL = timedelta(minutes=60) + + +async def async_setup_entry(hass, config_entry): + """Set up Met Éireann as config entry.""" + hass.data.setdefault(DOMAIN, {}) + + raw_weather_data = meteireann.WeatherData( + async_get_clientsession(hass), + latitude=config_entry.data[CONF_LATITUDE], + longitude=config_entry.data[CONF_LONGITUDE], + altitude=config_entry.data[CONF_ELEVATION], + ) + + weather_data = MetEireannWeatherData(hass, config_entry.data, raw_weather_data) + + async def _async_update_data(): + """Fetch data from Met Éireann.""" + try: + return await weather_data.fetch_data() + except Exception as err: + raise UpdateFailed(f"Update failed: {err}") from err + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name=DOMAIN, + update_method=_async_update_data, + update_interval=UPDATE_INTERVAL, + ) + await coordinator.async_refresh() + + hass.data[DOMAIN][config_entry.entry_id] = coordinator + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, "weather") + ) + + return True + + +async def async_unload_entry(hass, config_entry): + """Unload a config entry.""" + await hass.config_entries.async_forward_entry_unload(config_entry, "weather") + hass.data[DOMAIN].pop(config_entry.entry_id) + + return True + + +class MetEireannWeatherData: + """Keep data for Met Éireann weather entities.""" + + def __init__(self, hass, config, weather_data): + """Initialise the weather entity data.""" + self.hass = hass + self._config = config + self._weather_data = weather_data + self.current_weather_data = {} + self.daily_forecast = None + self.hourly_forecast = None + + async def fetch_data(self): + """Fetch data from API - (current weather and forecast).""" + await self._weather_data.fetching_data() + self.current_weather_data = self._weather_data.get_current_weather() + time_zone = dt_util.DEFAULT_TIME_ZONE + self.daily_forecast = self._weather_data.get_forecast(time_zone, False) + self.hourly_forecast = self._weather_data.get_forecast(time_zone, True) + return self diff --git a/homeassistant/components/met_eireann/config_flow.py b/homeassistant/components/met_eireann/config_flow.py new file mode 100644 index 0000000000000..6d736b9061a84 --- /dev/null +++ b/homeassistant/components/met_eireann/config_flow.py @@ -0,0 +1,48 @@ +"""Config flow to configure Met Éireann component.""" + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +import homeassistant.helpers.config_validation as cv + +# pylint:disable=unused-import +from .const import DOMAIN, HOME_LOCATION_NAME + + +class MetEireannFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Config flow for Met Eireann component.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + errors = {} + + if user_input is not None: + # Check if an identical entity is already configured + await self.async_set_unique_id( + f"{user_input.get(CONF_LATITUDE)},{user_input.get(CONF_LONGITUDE)}" + ) + self._abort_if_unique_id_configured() + else: + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_NAME, default=HOME_LOCATION_NAME): str, + vol.Required( + CONF_LATITUDE, default=self.hass.config.latitude + ): cv.latitude, + vol.Required( + CONF_LONGITUDE, default=self.hass.config.longitude + ): cv.longitude, + vol.Required( + CONF_ELEVATION, default=self.hass.config.elevation + ): int, + } + ), + errors=errors, + ) + return self.async_create_entry(title=user_input[CONF_NAME], data=user_input) diff --git a/homeassistant/components/met_eireann/const.py b/homeassistant/components/met_eireann/const.py new file mode 100644 index 0000000000000..98d862183c450 --- /dev/null +++ b/homeassistant/components/met_eireann/const.py @@ -0,0 +1,121 @@ +"""Constants for Met Éireann component.""" +import logging + +from homeassistant.components.weather import ( + ATTR_CONDITION_CLEAR_NIGHT, + ATTR_CONDITION_CLOUDY, + ATTR_CONDITION_FOG, + ATTR_CONDITION_LIGHTNING_RAINY, + ATTR_CONDITION_PARTLYCLOUDY, + ATTR_CONDITION_RAINY, + ATTR_CONDITION_SNOWY, + ATTR_CONDITION_SNOWY_RAINY, + ATTR_CONDITION_SUNNY, + ATTR_FORECAST_CONDITION, + ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_PRESSURE, + ATTR_FORECAST_TEMP, + ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_TIME, + ATTR_FORECAST_WIND_BEARING, + ATTR_FORECAST_WIND_SPEED, + DOMAIN as WEATHER_DOMAIN, +) + +ATTRIBUTION = "Data provided by Met Éireann" + +DEFAULT_NAME = "Met Éireann" + +DOMAIN = "met_eireann" + +HOME_LOCATION_NAME = "Home" + +ENTITY_ID_SENSOR_FORMAT_HOME = f"{WEATHER_DOMAIN}.met_eireann_{HOME_LOCATION_NAME}" + +_LOGGER = logging.getLogger(".") + +FORECAST_MAP = { + ATTR_FORECAST_CONDITION: "condition", + ATTR_FORECAST_PRESSURE: "pressure", + ATTR_FORECAST_PRECIPITATION: "precipitation", + ATTR_FORECAST_TEMP: "temperature", + ATTR_FORECAST_TEMP_LOW: "templow", + ATTR_FORECAST_TIME: "datetime", + ATTR_FORECAST_WIND_BEARING: "wind_bearing", + ATTR_FORECAST_WIND_SPEED: "wind_speed", +} + +CONDITION_MAP = { + ATTR_CONDITION_CLEAR_NIGHT: ["Dark_Sun"], + ATTR_CONDITION_CLOUDY: ["Cloud"], + ATTR_CONDITION_FOG: ["Fog"], + ATTR_CONDITION_LIGHTNING_RAINY: [ + "LightRainThunderSun", + "LightRainThunderSun", + "RainThunder", + "SnowThunder", + "SleetSunThunder", + "Dark_SleetSunThunder", + "SnowSunThunder", + "Dark_SnowSunThunder", + "LightRainThunder", + "SleetThunder", + "DrizzleThunderSun", + "Dark_DrizzleThunderSun", + "RainThunderSun", + "Dark_RainThunderSun", + "LightSleetThunderSun", + "Dark_LightSleetThunderSun", + "HeavySleetThunderSun", + "Dark_HeavySleetThunderSun", + "LightSnowThunderSun", + "Dark_LightSnowThunderSun", + "HeavySnowThunderSun", + "Dark_HeavySnowThunderSun", + "DrizzleThunder", + "LightSleetThunder", + "HeavySleetThunder", + "LightSnowThunder", + "HeavySnowThunder", + ], + ATTR_CONDITION_PARTLYCLOUDY: [ + "LightCloud", + "Dark_LightCloud", + "PartlyCloud", + "Dark_PartlyCloud", + ], + ATTR_CONDITION_RAINY: [ + "LightRainSun", + "Dark_LightRainSun", + "LightRain", + "Rain", + "DrizzleSun", + "Dark_DrizzleSun", + "RainSun", + "Dark_RainSun", + "Drizzle", + ], + ATTR_CONDITION_SNOWY: [ + "SnowSun", + "Dark_SnowSun", + "Snow", + "LightSnowSun", + "Dark_LightSnowSun", + "HeavySnowSun", + "Dark_HeavySnowSun", + "LightSnow", + "HeavySnow", + ], + ATTR_CONDITION_SNOWY_RAINY: [ + "SleetSun", + "Dark_SleetSun", + "Sleet", + "LightSleetSun", + "Dark_LightSleetSun", + "HeavySleetSun", + "Dark_HeavySleetSun", + "LightSleet", + "HeavySleet", + ], + ATTR_CONDITION_SUNNY: "Sun", +} diff --git a/homeassistant/components/met_eireann/manifest.json b/homeassistant/components/met_eireann/manifest.json new file mode 100644 index 0000000000000..5fe6ec5104592 --- /dev/null +++ b/homeassistant/components/met_eireann/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "met_eireann", + "name": "Met Éireann", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/met_eireann", + "requirements": ["pyMetEireann==0.2"], + "codeowners": ["@DylanGore"] +} diff --git a/homeassistant/components/met_eireann/strings.json b/homeassistant/components/met_eireann/strings.json new file mode 100644 index 0000000000000..687631f2cae6d --- /dev/null +++ b/homeassistant/components/met_eireann/strings.json @@ -0,0 +1,17 @@ +{ + "config": { + "step": { + "user": { + "title": "[%key:common::config_flow::data::location%]", + "description": "Enter your location to use weather data from the Met Éireann Public Weather Forecast API", + "data": { + "name": "[%key:common::config_flow::data::name%]", + "latitude": "[%key:common::config_flow::data::latitude%]", + "longitude": "[%key:common::config_flow::data::longitude%]", + "elevation": "[%key:common::config_flow::data::elevation%]" + } + } + }, + "error": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" } + } +} diff --git a/homeassistant/components/met_eireann/translations/en.json b/homeassistant/components/met_eireann/translations/en.json new file mode 100644 index 0000000000000..76b778282e64e --- /dev/null +++ b/homeassistant/components/met_eireann/translations/en.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "title": "Met Éireann", + "description": "Enter your location to use weather data from the Met Éireann Public Weather Forecast API", + "data": { + "name": "Name", + "latitude": "Latitude", + "longitude": "Longitude", + "elevation": "Elevation (in meters)" + } + } + }, + "error": { + "name_exists": "Location already exists" + }, + "abort": { + "already_configured": "Location is already configured", + "unknown": "Unexpected error" + } + } +} diff --git a/homeassistant/components/met_eireann/weather.py b/homeassistant/components/met_eireann/weather.py new file mode 100644 index 0000000000000..190da06f3d957 --- /dev/null +++ b/homeassistant/components/met_eireann/weather.py @@ -0,0 +1,191 @@ +"""Support for Met Éireann weather service.""" +import logging + +from homeassistant.components.weather import ( + ATTR_FORECAST_CONDITION, + ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_TEMP, + ATTR_FORECAST_TIME, + WeatherEntity, +) +from homeassistant.const import ( + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_NAME, + LENGTH_INCHES, + LENGTH_METERS, + LENGTH_MILES, + LENGTH_MILLIMETERS, + PRESSURE_HPA, + PRESSURE_INHG, + TEMP_CELSIUS, +) +from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.util import dt as dt_util +from homeassistant.util.distance import convert as convert_distance +from homeassistant.util.pressure import convert as convert_pressure + +from .const import ATTRIBUTION, CONDITION_MAP, DEFAULT_NAME, DOMAIN, FORECAST_MAP + +_LOGGER = logging.getLogger(__name__) + + +def format_condition(condition: str): + """Map the conditions provided by the weather API to those supported by the frontend.""" + if condition is not None: + for key, value in CONDITION_MAP.items(): + if condition in value: + return key + return condition + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Add a weather entity from a config_entry.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id] + async_add_entities( + [ + MetEireannWeather( + coordinator, config_entry.data, hass.config.units.is_metric, False + ), + MetEireannWeather( + coordinator, config_entry.data, hass.config.units.is_metric, True + ), + ] + ) + + +class MetEireannWeather(CoordinatorEntity, WeatherEntity): + """Implementation of a Met Éireann weather condition.""" + + def __init__(self, coordinator, config, is_metric, hourly): + """Initialise the platform with a data instance and site.""" + super().__init__(coordinator) + self._config = config + self._is_metric = is_metric + self._hourly = hourly + + @property + def unique_id(self): + """Return unique ID.""" + name_appendix = "" + if self._hourly: + name_appendix = "-hourly" + + return f"{self._config[CONF_LATITUDE]}-{self._config[CONF_LONGITUDE]}{name_appendix}" + + @property + def name(self): + """Return the name of the sensor.""" + name = self._config.get(CONF_NAME) + name_appendix = "" + if self._hourly: + name_appendix = " Hourly" + + if name is not None: + return f"{name}{name_appendix}" + + return f"{DEFAULT_NAME}{name_appendix}" + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + return not self._hourly + + @property + def condition(self): + """Return the current condition.""" + return format_condition( + self.coordinator.data.current_weather_data.get("condition") + ) + + @property + def temperature(self): + """Return the temperature.""" + return self.coordinator.data.current_weather_data.get("temperature") + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_CELSIUS + + @property + def pressure(self): + """Return the pressure.""" + pressure_hpa = self.coordinator.data.current_weather_data.get("pressure") + if self._is_metric or pressure_hpa is None: + return pressure_hpa + + return round(convert_pressure(pressure_hpa, PRESSURE_HPA, PRESSURE_INHG), 2) + + @property + def humidity(self): + """Return the humidity.""" + return self.coordinator.data.current_weather_data.get("humidity") + + @property + def wind_speed(self): + """Return the wind speed.""" + speed_m_s = self.coordinator.data.current_weather_data.get("wind_speed") + if self._is_metric or speed_m_s is None: + return speed_m_s + + speed_mi_s = convert_distance(speed_m_s, LENGTH_METERS, LENGTH_MILES) + speed_mi_h = speed_mi_s / 3600.0 + return int(round(speed_mi_h)) + + @property + def wind_bearing(self): + """Return the wind direction.""" + return self.coordinator.data.current_weather_data.get("wind_bearing") + + @property + def attribution(self): + """Return the attribution.""" + return ATTRIBUTION + + @property + def forecast(self): + """Return the forecast array.""" + if self._hourly: + me_forecast = self.coordinator.data.hourly_forecast + else: + me_forecast = self.coordinator.data.daily_forecast + required_keys = {ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME} + + ha_forecast = [] + + for item in me_forecast: + if not set(item).issuperset(required_keys): + continue + ha_item = { + k: item[v] for k, v in FORECAST_MAP.items() if item.get(v) is not None + } + if not self._is_metric and ATTR_FORECAST_PRECIPITATION in ha_item: + precip_inches = convert_distance( + ha_item[ATTR_FORECAST_PRECIPITATION], + LENGTH_MILLIMETERS, + LENGTH_INCHES, + ) + ha_item[ATTR_FORECAST_PRECIPITATION] = round(precip_inches, 2) + if ha_item.get(ATTR_FORECAST_CONDITION): + ha_item[ATTR_FORECAST_CONDITION] = format_condition( + ha_item[ATTR_FORECAST_CONDITION] + ) + # Convert timestamp to UTC + if ha_item.get(ATTR_FORECAST_TIME): + ha_item[ATTR_FORECAST_TIME] = dt_util.as_utc( + ha_item.get(ATTR_FORECAST_TIME) + ).isoformat() + ha_forecast.append(ha_item) + return ha_forecast + + @property + def device_info(self): + """Device info.""" + return { + "identifiers": {(DOMAIN,)}, + "manufacturer": "Met Éireann", + "model": "Forecast", + "default_name": "Forecast", + "entry_type": "service", + } diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 4095993346ec6..26c1b55c923ea 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -139,6 +139,7 @@ "mazda", "melcloud", "met", + "met_eireann", "meteo_france", "metoffice", "mikrotik", diff --git a/requirements_all.txt b/requirements_all.txt index 50e2bfc40bbc2..28a400ecfcec5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1227,6 +1227,9 @@ pyControl4==0.0.6 # homeassistant.components.tplink pyHS100==0.3.5.2 +# homeassistant.components.met_eireann +pyMetEireann==0.2 + # homeassistant.components.met # homeassistant.components.norway_air pyMetno==0.8.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0dce23f3374b5..44090efa0b0b8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -652,6 +652,9 @@ pyControl4==0.0.6 # homeassistant.components.tplink pyHS100==0.3.5.2 +# homeassistant.components.met_eireann +pyMetEireann==0.2 + # homeassistant.components.met # homeassistant.components.norway_air pyMetno==0.8.1 diff --git a/tests/components/met_eireann/__init__.py b/tests/components/met_eireann/__init__.py new file mode 100644 index 0000000000000..3dfadc06f6be7 --- /dev/null +++ b/tests/components/met_eireann/__init__.py @@ -0,0 +1,27 @@ +"""Tests for Met Éireann.""" +from unittest.mock import patch + +from homeassistant.components.met_eireann.const import DOMAIN +from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME + +from tests.common import MockConfigEntry + + +async def init_integration(hass) -> MockConfigEntry: + """Set up the Met Éireann integration in Home Assistant.""" + entry_data = { + CONF_NAME: "test", + CONF_LATITUDE: 0, + CONF_LONGITUDE: 0, + CONF_ELEVATION: 0, + } + entry = MockConfigEntry(domain=DOMAIN, data=entry_data) + with patch( + "homeassistant.components.met_eireann.meteireann.WeatherData.fetching_data", + return_value=True, + ): + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry diff --git a/tests/components/met_eireann/conftest.py b/tests/components/met_eireann/conftest.py new file mode 100644 index 0000000000000..e73d1e41cca9b --- /dev/null +++ b/tests/components/met_eireann/conftest.py @@ -0,0 +1,22 @@ +"""Fixtures for Met Éireann weather testing.""" +from unittest.mock import AsyncMock, patch + +import pytest + + +@pytest.fixture +def mock_weather(): + """Mock weather data.""" + with patch("meteireann.WeatherData") as mock_data: + mock_data = mock_data.return_value + mock_data.fetching_data = AsyncMock(return_value=True) + mock_data.get_current_weather.return_value = { + "condition": "Cloud", + "temperature": 15, + "pressure": 100, + "humidity": 50, + "wind_speed": 10, + "wind_bearing": "NE", + } + mock_data.get_forecast.return_value = {} + yield mock_data diff --git a/tests/components/met_eireann/test_config_flow.py b/tests/components/met_eireann/test_config_flow.py new file mode 100644 index 0000000000000..50060541be540 --- /dev/null +++ b/tests/components/met_eireann/test_config_flow.py @@ -0,0 +1,95 @@ +"""Tests for Met Éireann config flow.""" +from unittest.mock import patch + +import pytest + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.met_eireann.const import DOMAIN, HOME_LOCATION_NAME +from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE + + +@pytest.fixture(name="met_eireann_setup", autouse=True) +def met_setup_fixture(): + """Patch Met Éireann setup entry.""" + with patch( + "homeassistant.components.met_eireann.async_setup_entry", return_value=True + ): + yield + + +async def test_show_config_form(hass): + """Test show configuration form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == config_entries.SOURCE_USER + + +async def test_flow_with_home_location(hass): + """Test config flow. + + Test the flow when a default location is configured. + Then it should return a form with default values. + """ + hass.config.latitude = 1 + hass.config.longitude = 2 + hass.config.elevation = 3 + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == config_entries.SOURCE_USER + + default_data = result["data_schema"]({}) + assert default_data["name"] == HOME_LOCATION_NAME + assert default_data["latitude"] == 1 + assert default_data["longitude"] == 2 + assert default_data["elevation"] == 3 + + +async def test_create_entry(hass): + """Test create entry from user input.""" + test_data = { + "name": "test", + CONF_LONGITUDE: 0, + CONF_LATITUDE: 0, + CONF_ELEVATION: 0, + } + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=test_data + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == test_data.get("name") + assert result["data"] == test_data + + +async def test_flow_entry_already_exists(hass): + """Test user input for config_entry that already exists. + + Test to ensure the config form does not allow duplicate entries. + """ + test_data = { + "name": "test", + CONF_LONGITUDE: 0, + CONF_LATITUDE: 0, + CONF_ELEVATION: 0, + } + + # Create the first entry and assert that it is created successfully + result1 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=test_data + ) + assert result1["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + # Create the second entry and assert that it is aborted + result2 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=test_data + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["reason"] == "already_configured" diff --git a/tests/components/met_eireann/test_init.py b/tests/components/met_eireann/test_init.py new file mode 100644 index 0000000000000..8f95013cd7202 --- /dev/null +++ b/tests/components/met_eireann/test_init.py @@ -0,0 +1,19 @@ +"""Test the Met Éireann integration init.""" +from homeassistant.components.met_eireann.const import DOMAIN +from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED + +from . import init_integration + + +async def test_unload_entry(hass): + """Test successful unload of entry.""" + entry = await init_integration(hass) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert entry.state == ENTRY_STATE_LOADED + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == ENTRY_STATE_NOT_LOADED + assert not hass.data.get(DOMAIN) diff --git a/tests/components/met_eireann/test_weather.py b/tests/components/met_eireann/test_weather.py new file mode 100644 index 0000000000000..e8d01b967a6ba --- /dev/null +++ b/tests/components/met_eireann/test_weather.py @@ -0,0 +1,31 @@ +"""Test Met Éireann weather entity.""" + +from homeassistant.components.met_eireann.const import DOMAIN + +from tests.common import MockConfigEntry + + +async def test_weather(hass, mock_weather): + """Test weather entity.""" + # Create a mock configuration for testing + mock_data = MockConfigEntry( + domain=DOMAIN, + data={"name": "Somewhere", "latitude": 10, "longitude": 20, "elevation": 0}, + ) + mock_data.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_data.entry_id) + await hass.async_block_till_done() + assert len(hass.states.async_entity_ids("weather")) == 1 + assert len(mock_weather.mock_calls) == 4 + + # Test we do not track config + await hass.config.async_update(latitude=10, longitude=20) + await hass.async_block_till_done() + + assert len(mock_weather.mock_calls) == 4 + + entry = hass.config_entries.async_entries()[0] + await hass.config_entries.async_remove(entry.entry_id) + await hass.async_block_till_done() + assert len(hass.states.async_entity_ids("weather")) == 0 From 5305d083ecaf55d3eee3f03238637bff0b01f39b Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 5 Apr 2021 19:25:52 -0400 Subject: [PATCH 0072/1317] Add config flow for Waze Travel Time (#43419) * Add config flow for Waze Travel Time * update translations * setup entry is async * fix update logic during setup * support old config method in the interim * fix requirements * fix requirements * add abort string * changes based on @bdraco review * fix tests * add device identifier * Update homeassistant/components/waze_travel_time/__init__.py Co-authored-by: J. Nick Koston * fix tests * Update homeassistant/components/waze_travel_time/sensor.py Co-authored-by: Martin Hjelmare * log warning for deprecation message * PR feedback * fix tests and bugs * re-add name to config schema to avoid breaking change * handle if we get name from config in entry title * fix name logic * always set up options with defaults * Update homeassistant/components/waze_travel_time/sensor.py Co-authored-by: Martin Hjelmare * Update config_flow.py * Update sensor.py * handle options updates by getting options on every update * patch library instead of sensor * fixes and make sure first update writes the state * validate config entry data during config flow and entry setup * fix input parameters * fix tests * invert if statement * remove unnecessary else * exclude helpers from coverage * remove async_setup because it's no longer needed * fix patch statements Co-authored-by: J. Nick Koston Co-authored-by: Martin Hjelmare --- .coveragerc | 2 + .../components/waze_travel_time/__init__.py | 28 ++ .../waze_travel_time/config_flow.py | 149 ++++++++ .../components/waze_travel_time/const.py | 40 ++ .../components/waze_travel_time/helpers.py | 72 ++++ .../components/waze_travel_time/manifest.json | 7 +- .../components/waze_travel_time/sensor.py | 341 +++++++++--------- .../components/waze_travel_time/strings.json | 38 ++ .../waze_travel_time/translations/en.json | 35 ++ homeassistant/generated/config_flows.py | 1 + requirements_test_all.txt | 3 + tests/components/waze_travel_time/__init__.py | 1 + tests/components/waze_travel_time/conftest.py | 57 +++ .../waze_travel_time/test_config_flow.py | 205 +++++++++++ 14 files changed, 813 insertions(+), 166 deletions(-) create mode 100644 homeassistant/components/waze_travel_time/config_flow.py create mode 100644 homeassistant/components/waze_travel_time/const.py create mode 100644 homeassistant/components/waze_travel_time/helpers.py create mode 100644 homeassistant/components/waze_travel_time/strings.json create mode 100644 homeassistant/components/waze_travel_time/translations/en.json create mode 100644 tests/components/waze_travel_time/__init__.py create mode 100644 tests/components/waze_travel_time/conftest.py create mode 100644 tests/components/waze_travel_time/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 2845a1768a8dc..624037946c362 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1102,6 +1102,8 @@ omit = homeassistant/components/waterfurnace/* homeassistant/components/watson_iot/* homeassistant/components/watson_tts/tts.py + homeassistant/components/waze_travel_time/__init__.py + homeassistant/components/waze_travel_time/helpers.py homeassistant/components/waze_travel_time/sensor.py homeassistant/components/webostv/* homeassistant/components/whois/sensor.py diff --git a/homeassistant/components/waze_travel_time/__init__.py b/homeassistant/components/waze_travel_time/__init__.py index 9674bd9850e5d..20a0c01c64217 100644 --- a/homeassistant/components/waze_travel_time/__init__.py +++ b/homeassistant/components/waze_travel_time/__init__.py @@ -1 +1,29 @@ """The waze_travel_time component.""" +import asyncio + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +PLATFORMS = ["sensor"] + + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Load the saved entities.""" + for platform in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, platform) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(config_entry, platform) + for platform in PLATFORMS + ] + ) + ) diff --git a/homeassistant/components/waze_travel_time/config_flow.py b/homeassistant/components/waze_travel_time/config_flow.py new file mode 100644 index 0000000000000..05dd372f9d9c2 --- /dev/null +++ b/homeassistant/components/waze_travel_time/config_flow.py @@ -0,0 +1,149 @@ +"""Config flow for Waze Travel Time integration.""" +import logging + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_NAME, CONF_REGION +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv +from homeassistant.util import slugify + +from .const import ( + CONF_AVOID_FERRIES, + CONF_AVOID_SUBSCRIPTION_ROADS, + CONF_AVOID_TOLL_ROADS, + CONF_DESTINATION, + CONF_EXCL_FILTER, + CONF_INCL_FILTER, + CONF_ORIGIN, + CONF_REALTIME, + CONF_UNITS, + CONF_VEHICLE_TYPE, + DEFAULT_NAME, + DOMAIN, + REGIONS, + UNITS, + VEHICLE_TYPES, +) +from .helpers import is_valid_config_entry + +_LOGGER = logging.getLogger(__name__) + + +class WazeOptionsFlow(config_entries.OptionsFlow): + """Handle an options flow for Waze Travel Time.""" + + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + """Initialize waze options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Handle the initial step.""" + 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_INCL_FILTER, + default=self.config_entry.options.get(CONF_INCL_FILTER), + ): cv.string, + vol.Optional( + CONF_EXCL_FILTER, + default=self.config_entry.options.get(CONF_EXCL_FILTER), + ): cv.string, + vol.Optional( + CONF_REALTIME, + default=self.config_entry.options[CONF_REALTIME], + ): cv.boolean, + vol.Optional( + CONF_VEHICLE_TYPE, + default=self.config_entry.options[CONF_VEHICLE_TYPE], + ): vol.In(VEHICLE_TYPES), + vol.Optional( + CONF_UNITS, + default=self.config_entry.options[CONF_UNITS], + ): vol.In(UNITS), + vol.Optional( + CONF_AVOID_TOLL_ROADS, + default=self.config_entry.options[CONF_AVOID_TOLL_ROADS], + ): cv.boolean, + vol.Optional( + CONF_AVOID_SUBSCRIPTION_ROADS, + default=self.config_entry.options[ + CONF_AVOID_SUBSCRIPTION_ROADS + ], + ): cv.boolean, + vol.Optional( + CONF_AVOID_FERRIES, + default=self.config_entry.options[CONF_AVOID_FERRIES], + ): cv.boolean, + } + ), + ) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Waze Travel Time.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + @staticmethod + @callback + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> WazeOptionsFlow: + """Get the options flow for this handler.""" + return WazeOptionsFlow(config_entry) + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + if await self.hass.async_add_executor_job( + is_valid_config_entry, + self.hass, + _LOGGER, + user_input[CONF_ORIGIN], + user_input[CONF_DESTINATION], + user_input[CONF_REGION], + ): + await self.async_set_unique_id( + slugify( + f"{DOMAIN}_{user_input[CONF_ORIGIN]}_{user_input[CONF_DESTINATION]}" + ) + ) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=( + user_input.get( + CONF_NAME, + ( + f"{DEFAULT_NAME}: {user_input[CONF_ORIGIN]} -> " + f"{user_input[CONF_DESTINATION]}" + ), + ) + ), + data=user_input, + ) + + # If we get here, it's because we couldn't connect + errors["base"] = "cannot_connect" + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_ORIGIN): cv.string, + vol.Required(CONF_DESTINATION): cv.string, + vol.Required(CONF_REGION): vol.In(REGIONS), + } + ), + errors=errors, + ) + + async_step_import = async_step_user diff --git a/homeassistant/components/waze_travel_time/const.py b/homeassistant/components/waze_travel_time/const.py new file mode 100644 index 0000000000000..1b89fd5e28249 --- /dev/null +++ b/homeassistant/components/waze_travel_time/const.py @@ -0,0 +1,40 @@ +"""Constants for waze_travel_time.""" +from homeassistant.const import CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC + +DOMAIN = "waze_travel_time" + +ATTR_DESTINATION = "destination" +ATTR_DURATION = "duration" +ATTR_DISTANCE = "distance" +ATTR_ORIGIN = "origin" +ATTR_ROUTE = "route" + +ATTRIBUTION = "Powered by Waze" + +CONF_DESTINATION = "destination" +CONF_ORIGIN = "origin" +CONF_INCL_FILTER = "incl_filter" +CONF_EXCL_FILTER = "excl_filter" +CONF_REALTIME = "realtime" +CONF_UNITS = "units" +CONF_VEHICLE_TYPE = "vehicle_type" +CONF_AVOID_TOLL_ROADS = "avoid_toll_roads" +CONF_AVOID_SUBSCRIPTION_ROADS = "avoid_subscription_roads" +CONF_AVOID_FERRIES = "avoid_ferries" + +DEFAULT_NAME = "Waze Travel Time" +DEFAULT_REALTIME = True +DEFAULT_VEHICLE_TYPE = "car" +DEFAULT_AVOID_TOLL_ROADS = False +DEFAULT_AVOID_SUBSCRIPTION_ROADS = False +DEFAULT_AVOID_FERRIES = False + +ICON = "mdi:car" + +UNITS = [CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL] + +REGIONS = ["US", "NA", "EU", "IL", "AU"] +VEHICLE_TYPES = ["car", "taxi", "motorcycle"] + +# Attempt to find entity_id without finding address with period. +ENTITY_ID_PATTERN = "(? None: + """Set up a Waze travel time sensor entry.""" + defaults = { + CONF_REALTIME: DEFAULT_REALTIME, + CONF_VEHICLE_TYPE: DEFAULT_VEHICLE_TYPE, + CONF_UNITS: hass.config.units.name, + CONF_AVOID_FERRIES: DEFAULT_AVOID_FERRIES, + CONF_AVOID_SUBSCRIPTION_ROADS: DEFAULT_AVOID_SUBSCRIPTION_ROADS, + CONF_AVOID_TOLL_ROADS: DEFAULT_AVOID_TOLL_ROADS, + } + name = None + if not config_entry.options: + new_data = config_entry.data.copy() + name = new_data.pop(CONF_NAME, None) + options = {} + for key in [ + CONF_INCL_FILTER, + CONF_EXCL_FILTER, + CONF_REALTIME, + CONF_VEHICLE_TYPE, + CONF_AVOID_TOLL_ROADS, + CONF_AVOID_SUBSCRIPTION_ROADS, + CONF_AVOID_FERRIES, + CONF_UNITS, + ]: + if key in new_data: + options[key] = new_data.pop(key) + elif key in defaults: + options[key] = defaults[key] + + hass.config_entries.async_update_entry( + config_entry, data=new_data, options=options + ) + + destination = config_entry.data[CONF_DESTINATION] + origin = config_entry.data[CONF_ORIGIN] + region = config_entry.data[CONF_REGION] + name = name or f"{DEFAULT_NAME}: {origin} -> {destination}" + + if not await hass.async_add_executor_job( + is_valid_config_entry, hass, _LOGGER, origin, destination, region + ): + raise ConfigEntryNotReady data = WazeTravelTimeData( None, None, region, - incl_filter, - excl_filter, - realtime, - units, - vehicle_type, - avoid_toll_roads, - avoid_subscription_roads, - avoid_ferries, + config_entry, ) - sensor = WazeTravelTime(name, origin, destination, data) + sensor = WazeTravelTime(config_entry.unique_id, name, origin, destination, data) - add_entities([sensor]) - - # Wait until start event is sent to load this component. - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, lambda _: sensor.update()) - - -def _get_location_from_attributes(state): - """Get the lat/long string from an states attributes.""" - attr = state.attributes - return "{},{}".format(attr.get(ATTR_LATITUDE), attr.get(ATTR_LONGITUDE)) + async_add_entities([sensor], False) class WazeTravelTime(SensorEntity): """Representation of a Waze travel time sensor.""" - def __init__(self, name, origin, destination, waze_data): + def __init__(self, unique_id, name, origin, destination, waze_data): """Initialize the Waze travel time sensor.""" - self._name = name + self._unique_id = unique_id self._waze_data = waze_data + self._name = name self._state = None self._origin_entity_id = None self._destination_entity_id = None - - # Attempt to find entity_id without finding address with period. - pattern = "(? None: + """Handle when entity is added.""" + if self.hass.state != CoreState.running: + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_START, self.first_update + ) + else: + await self.first_update() + @property def name(self): """Return the name of the sensor.""" @@ -188,150 +233,118 @@ def extra_state_attributes(self): res[ATTR_DESTINATION] = self._waze_data.destination return res - def _get_location_from_entity(self, entity_id): - """Get the location from the entity_id.""" - state = self.hass.states.get(entity_id) - - if state is None: - _LOGGER.error("Unable to find entity %s", entity_id) - return None - - # Check if the entity has location attributes. - if location.has_location(state): - _LOGGER.debug("Getting %s location", entity_id) - return _get_location_from_attributes(state) - - # Check if device is inside a zone. - zone_state = self.hass.states.get(f"zone.{state.state}") - if location.has_location(zone_state): - _LOGGER.debug( - "%s is in %s, getting zone location", entity_id, zone_state.entity_id - ) - return _get_location_from_attributes(zone_state) - - # If zone was not found in state then use the state as the location. - if entity_id.startswith("sensor."): - return state.state - - # When everything fails just return nothing. - return None - - def _resolve_zone(self, friendly_name): - """Get a lat/long from a zones friendly_name.""" - states = self.hass.states.all() - for state in states: - if state.domain == "zone" and state.name == friendly_name: - return _get_location_from_attributes(state) - - return friendly_name + async def first_update(self, _=None): + """Run first update and write state.""" + await self.hass.async_add_executor_job(self.update) + self.async_write_ha_state() def update(self): """Fetch new state data for the sensor.""" _LOGGER.debug("Fetching Route for %s", self._name) # Get origin latitude and longitude from entity_id. if self._origin_entity_id is not None: - self._waze_data.origin = self._get_location_from_entity( - self._origin_entity_id + self._waze_data.origin = get_location_from_entity( + self.hass, _LOGGER, self._origin_entity_id ) # Get destination latitude and longitude from entity_id. if self._destination_entity_id is not None: - self._waze_data.destination = self._get_location_from_entity( - self._destination_entity_id + self._waze_data.destination = get_location_from_entity( + self.hass, _LOGGER, self._destination_entity_id ) # Get origin from zone name. - self._waze_data.origin = self._resolve_zone(self._waze_data.origin) + self._waze_data.origin = resolve_zone(self.hass, self._waze_data.origin) # Get destination from zone name. - self._waze_data.destination = self._resolve_zone(self._waze_data.destination) + self._waze_data.destination = resolve_zone( + self.hass, self._waze_data.destination + ) self._waze_data.update() + @property + def device_info(self) -> dict[str, Any] | None: + """Return device specific attributes.""" + return { + "name": "Waze", + "identifiers": {(DOMAIN, DOMAIN)}, + "entry_type": "service", + } + + @property + def unique_id(self) -> str: + """Return unique ID of entity.""" + return self._unique_id + class WazeTravelTimeData: """WazeTravelTime Data object.""" - def __init__( - self, - origin, - destination, - region, - include, - exclude, - realtime, - units, - vehicle_type, - avoid_toll_roads, - avoid_subscription_roads, - avoid_ferries, - ): + def __init__(self, origin, destination, region, config_entry): """Set up WazeRouteCalculator.""" - - self._calc = WazeRouteCalculator - self.origin = origin self.destination = destination self.region = region - self.include = include - self.exclude = exclude - self.realtime = realtime - self.units = units + self.config_entry = config_entry self.duration = None self.distance = None self.route = None - self.avoid_toll_roads = avoid_toll_roads - self.avoid_subscription_roads = avoid_subscription_roads - self.avoid_ferries = avoid_ferries - - # Currently WazeRouteCalc only supports PRIVATE, TAXI, MOTORCYCLE. - if vehicle_type.upper() == "CAR": - # Empty means PRIVATE for waze which translates to car. - self.vehicle_type = "" - else: - self.vehicle_type = vehicle_type.upper() def update(self): """Update WazeRouteCalculator Sensor.""" if self.origin is not None and self.destination is not None: + # Grab options on every update + incl_filter = self.config_entry.options.get(CONF_INCL_FILTER) + excl_filter = self.config_entry.options.get(CONF_EXCL_FILTER) + realtime = self.config_entry.options[CONF_REALTIME] + vehicle_type = self.config_entry.options[CONF_VEHICLE_TYPE] + vehicle_type = "" if vehicle_type.upper() == "CAR" else vehicle_type.upper() + avoid_toll_roads = self.config_entry.options[CONF_AVOID_TOLL_ROADS] + avoid_subscription_roads = self.config_entry.options[ + CONF_AVOID_SUBSCRIPTION_ROADS + ] + avoid_ferries = self.config_entry.options[CONF_AVOID_FERRIES] + units = self.config_entry.options[CONF_UNITS] + try: - params = self._calc.WazeRouteCalculator( + params = WazeRouteCalculator( self.origin, self.destination, self.region, - self.vehicle_type, - self.avoid_toll_roads, - self.avoid_subscription_roads, - self.avoid_ferries, + vehicle_type, + avoid_toll_roads, + avoid_subscription_roads, + avoid_ferries, ) - routes = params.calc_all_routes_info(real_time=self.realtime) + routes = params.calc_all_routes_info(real_time=realtime) - if self.include is not None: + if incl_filter is not None: routes = { k: v for k, v in routes.items() - if self.include.lower() in k.lower() + if incl_filter.lower() in k.lower() } - if self.exclude is not None: + if excl_filter is not None: routes = { k: v for k, v in routes.items() - if self.exclude.lower() not in k.lower() + if excl_filter.lower() not in k.lower() } route = list(routes)[0] self.duration, distance = routes[route] - if self.units == CONF_UNIT_SYSTEM_IMPERIAL: + if units == CONF_UNIT_SYSTEM_IMPERIAL: # Convert to miles. self.distance = distance / 1.609 else: self.distance = distance self.route = route - except self._calc.WRCError as exp: + except WRCError as exp: _LOGGER.warning("Error on retrieving data: %s", exp) return except KeyError: diff --git a/homeassistant/components/waze_travel_time/strings.json b/homeassistant/components/waze_travel_time/strings.json new file mode 100644 index 0000000000000..082ee31db7391 --- /dev/null +++ b/homeassistant/components/waze_travel_time/strings.json @@ -0,0 +1,38 @@ +{ + "title": "Waze Travel Time", + "config": { + "step": { + "user": { + "description": "For Origin and Destination, enter the address or the GPS coordinates of the location (GPS coordinates has to be separated by a comma). You can also enter an entity id which provides this information in its state, an entity id with latitude and longitude attributes, or zone friendly name.", + "data": { + "origin": "Origin", + "destination": "Destination", + "region": "Region" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_location%]" + } + }, + "options": { + "step": { + "init": { + "description": "The `substring` inputs will allow you to force the integration to use a particular route or avoid a particular route in its time travel calculation.", + "data": { + "units": "Units", + "vehicle_type": "Vehicle Type", + "incl_filter": "Substring in Description of Selected Route", + "excl_filter": "Substring NOT in Description of Selected Route", + "realtime": "Realtime Travel Time?", + "avoid_toll_roads": "Avoid Toll Roads?", + "avoid_ferries": "Avoid Ferries?", + "avoid_subscription_roads": "Avoid Roads Needing a Vignette / Subscription?" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/waze_travel_time/translations/en.json b/homeassistant/components/waze_travel_time/translations/en.json new file mode 100644 index 0000000000000..4b113302cda4e --- /dev/null +++ b/homeassistant/components/waze_travel_time/translations/en.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "Location is already configured" + }, + "step": { + "user": { + "data": { + "destination": "Destination", + "origin": "Origin", + "region": "Region" + }, + "description": "For Origin and Destination, enter the address or the GPS coordinates of the location (GPS coordinates has to be separated by a comma). You can also enter an entity id which provides this information in its state, an entity id with latitude and longitude attributes, or zone friendly name." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "avoid_ferries": "Avoid Ferries?", + "avoid_subscription_roads": "Avoid Roads Needing a Vignette / Subscription?", + "avoid_toll_roads": "Avoid Toll Roads?", + "excl_filter": "Substring NOT in Description of Selected Route", + "incl_filter": "Substring in Description of Selected Route", + "realtime": "Realtime Travel Time?", + "units": "Units", + "vehicle_type": "Vehicle Type" + }, + "description": "The `substring` inputs will allow you to force the integration to use a particular route or avoid a particular route in its time travel calculation." + } + } + }, + "title": "Waze Travel Time" +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 26c1b55c923ea..e9eece903fc7e 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -258,6 +258,7 @@ "vilfo", "vizio", "volumio", + "waze_travel_time", "wemo", "wiffi", "wilight", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 44090efa0b0b8..64239d0828815 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -38,6 +38,9 @@ RtmAPI==0.7.2 # homeassistant.components.onvif WSDiscovery==2.0.0 +# homeassistant.components.waze_travel_time +WazeRouteCalculator==0.12 + # homeassistant.components.abode abodepy==1.2.0 diff --git a/tests/components/waze_travel_time/__init__.py b/tests/components/waze_travel_time/__init__.py new file mode 100644 index 0000000000000..1df3d9314d07e --- /dev/null +++ b/tests/components/waze_travel_time/__init__.py @@ -0,0 +1 @@ +"""Tests for the Waze Travel Time integration.""" diff --git a/tests/components/waze_travel_time/conftest.py b/tests/components/waze_travel_time/conftest.py new file mode 100644 index 0000000000000..dd5b343cc166a --- /dev/null +++ b/tests/components/waze_travel_time/conftest.py @@ -0,0 +1,57 @@ +"""Fixtures for Waze Travel Time tests.""" +from unittest.mock import patch + +from WazeRouteCalculator import WRCError +import pytest + + +@pytest.fixture(name="skip_notifications", autouse=True) +def skip_notifications_fixture(): + """Skip notification calls.""" + with patch("homeassistant.components.persistent_notification.async_create"), patch( + "homeassistant.components.persistent_notification.async_dismiss" + ): + yield + + +@pytest.fixture(name="validate_config_entry") +def validate_config_entry_fixture(): + """Return valid config entry.""" + with patch( + "homeassistant.components.waze_travel_time.helpers.WazeRouteCalculator" + ) as mock_wrc: + obj = mock_wrc.return_value + obj.calc_all_routes_info.return_value = None + yield + + +@pytest.fixture(name="bypass_setup") +def bypass_setup_fixture(): + """Bypass entry setup.""" + with patch( + "homeassistant.components.waze_travel_time.async_setup_entry", + return_value=True, + ): + yield + + +@pytest.fixture(name="mock_update") +def mock_update_fixture(): + """Mock an update to the sensor.""" + with patch( + "homeassistant.components.waze_travel_time.sensor.WazeRouteCalculator.calc_all_routes_info", + return_value={"My route": (150, 300)}, + ): + yield + + +@pytest.fixture(name="invalidate_config_entry") +def invalidate_config_entry_fixture(): + """Return invalid config entry.""" + with patch( + "homeassistant.components.waze_travel_time.helpers.WazeRouteCalculator" + ) as mock_wrc: + obj = mock_wrc.return_value + obj.calc_all_routes_info.return_value = {} + obj.calc_all_routes_info.side_effect = WRCError("test") + yield diff --git a/tests/components/waze_travel_time/test_config_flow.py b/tests/components/waze_travel_time/test_config_flow.py new file mode 100644 index 0000000000000..f6f1614ca2533 --- /dev/null +++ b/tests/components/waze_travel_time/test_config_flow.py @@ -0,0 +1,205 @@ +"""Test the Waze Travel Time config flow.""" +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.waze_travel_time.const import ( + CONF_AVOID_FERRIES, + CONF_AVOID_SUBSCRIPTION_ROADS, + CONF_AVOID_TOLL_ROADS, + CONF_DESTINATION, + CONF_EXCL_FILTER, + CONF_INCL_FILTER, + CONF_ORIGIN, + CONF_REALTIME, + CONF_UNITS, + CONF_VEHICLE_TYPE, + DEFAULT_NAME, + DOMAIN, +) +from homeassistant.const import CONF_REGION, CONF_UNIT_SYSTEM_IMPERIAL + +from tests.common import MockConfigEntry + + +async def test_minimum_fields(hass, validate_config_entry, bypass_setup): + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + CONF_REGION: "US", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == f"{DEFAULT_NAME}: location1 -> location2" + assert result2["data"] == { + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + CONF_REGION: "US", + } + + +async def test_options(hass, validate_config_entry, mock_update): + """Test options flow.""" + + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + CONF_REGION: "US", + }, + ) + 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, data=None) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_AVOID_FERRIES: True, + CONF_AVOID_SUBSCRIPTION_ROADS: True, + CONF_AVOID_TOLL_ROADS: True, + CONF_EXCL_FILTER: "exclude", + CONF_INCL_FILTER: "include", + CONF_REALTIME: False, + CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL, + CONF_VEHICLE_TYPE: "taxi", + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "" + assert result["data"] == { + CONF_AVOID_FERRIES: True, + CONF_AVOID_SUBSCRIPTION_ROADS: True, + CONF_AVOID_TOLL_ROADS: True, + CONF_EXCL_FILTER: "exclude", + CONF_INCL_FILTER: "include", + CONF_REALTIME: False, + CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL, + CONF_VEHICLE_TYPE: "taxi", + } + + assert entry.options == { + CONF_AVOID_FERRIES: True, + CONF_AVOID_SUBSCRIPTION_ROADS: True, + CONF_AVOID_TOLL_ROADS: True, + CONF_EXCL_FILTER: "exclude", + CONF_INCL_FILTER: "include", + CONF_REALTIME: False, + CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL, + CONF_VEHICLE_TYPE: "taxi", + } + + +async def test_import(hass, validate_config_entry, mock_update): + """Test import for config flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + CONF_REGION: "US", + CONF_AVOID_FERRIES: True, + CONF_AVOID_SUBSCRIPTION_ROADS: True, + CONF_AVOID_TOLL_ROADS: True, + CONF_EXCL_FILTER: "exclude", + CONF_INCL_FILTER: "include", + CONF_REALTIME: False, + CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL, + CONF_VEHICLE_TYPE: "taxi", + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + await hass.async_block_till_done() + entry = hass.config_entries.async_entries(DOMAIN)[0] + assert entry.data == { + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + CONF_REGION: "US", + } + assert entry.options == { + CONF_AVOID_FERRIES: True, + CONF_AVOID_SUBSCRIPTION_ROADS: True, + CONF_AVOID_TOLL_ROADS: True, + CONF_EXCL_FILTER: "exclude", + CONF_INCL_FILTER: "include", + CONF_REALTIME: False, + CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL, + CONF_VEHICLE_TYPE: "taxi", + } + + +async def test_dupe_id(hass, validate_config_entry, bypass_setup): + """Test setting up the same entry twice fails.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + CONF_REGION: "US", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + CONF_REGION: "US", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["reason"] == "already_configured" + + +async def test_invalid_config_entry(hass, invalidate_config_entry): + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {} + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + CONF_REGION: "US", + }, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": "cannot_connect"} From e8cbdea881ef49c78cb7587fa5cf4737828dadf9 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Tue, 6 Apr 2021 00:04:07 +0000 Subject: [PATCH 0073/1317] [ci skip] Translation update --- .../components/climacell/translations/ca.json | 1 + .../components/climacell/translations/en.json | 1 + .../components/climacell/translations/et.json | 1 + .../components/climacell/translations/ru.json | 1 + .../components/deconz/translations/ca.json | 4 ++++ .../components/deconz/translations/en.json | 4 ++++ .../components/deconz/translations/et.json | 4 ++++ .../components/deconz/translations/ko.json | 4 ++++ .../components/deconz/translations/nl.json | 4 ++++ .../components/deconz/translations/ru.json | 4 ++++ .../deconz/translations/zh-Hant.json | 4 ++++ .../components/emonitor/translations/ca.json | 23 +++++++++++++++++++ .../components/emonitor/translations/et.json | 23 +++++++++++++++++++ .../components/emonitor/translations/ko.json | 23 +++++++++++++++++++ .../components/emonitor/translations/ru.json | 23 +++++++++++++++++++ .../emonitor/translations/zh-Hant.json | 23 +++++++++++++++++++ .../enphase_envoy/translations/ca.json | 22 ++++++++++++++++++ .../enphase_envoy/translations/et.json | 22 ++++++++++++++++++ .../enphase_envoy/translations/ko.json | 22 ++++++++++++++++++ .../enphase_envoy/translations/ru.json | 22 ++++++++++++++++++ .../enphase_envoy/translations/zh-Hant.json | 22 ++++++++++++++++++ .../google_travel_time/translations/et.json | 2 +- .../components/harmony/translations/ko.json | 2 +- .../translations/ko.json | 2 +- .../met_eireann/translations/en.json | 22 ++++++++---------- .../opentherm_gw/translations/ko.json | 3 ++- .../components/roomba/translations/ko.json | 3 ++- .../components/songpal/translations/ko.json | 2 +- .../synology_dsm/translations/ko.json | 2 +- .../waze_travel_time/translations/en.json | 3 +++ 30 files changed, 278 insertions(+), 20 deletions(-) create mode 100644 homeassistant/components/emonitor/translations/ca.json create mode 100644 homeassistant/components/emonitor/translations/et.json create mode 100644 homeassistant/components/emonitor/translations/ko.json create mode 100644 homeassistant/components/emonitor/translations/ru.json create mode 100644 homeassistant/components/emonitor/translations/zh-Hant.json create mode 100644 homeassistant/components/enphase_envoy/translations/ca.json create mode 100644 homeassistant/components/enphase_envoy/translations/et.json create mode 100644 homeassistant/components/enphase_envoy/translations/ko.json create mode 100644 homeassistant/components/enphase_envoy/translations/ru.json create mode 100644 homeassistant/components/enphase_envoy/translations/zh-Hant.json diff --git a/homeassistant/components/climacell/translations/ca.json b/homeassistant/components/climacell/translations/ca.json index 23afb6a3d9057..3f215b6323435 100644 --- a/homeassistant/components/climacell/translations/ca.json +++ b/homeassistant/components/climacell/translations/ca.json @@ -10,6 +10,7 @@ "user": { "data": { "api_key": "Clau API", + "api_version": "Versi\u00f3 de l'API", "latitude": "Latitud", "longitude": "Longitud", "name": "Nom" diff --git a/homeassistant/components/climacell/translations/en.json b/homeassistant/components/climacell/translations/en.json index ed3ead421e1e5..c126cf170b131 100644 --- a/homeassistant/components/climacell/translations/en.json +++ b/homeassistant/components/climacell/translations/en.json @@ -10,6 +10,7 @@ "user": { "data": { "api_key": "API Key", + "api_version": "API Version", "latitude": "Latitude", "longitude": "Longitude", "name": "Name" diff --git a/homeassistant/components/climacell/translations/et.json b/homeassistant/components/climacell/translations/et.json index 3722c258afaa4..de44f9d70d1e4 100644 --- a/homeassistant/components/climacell/translations/et.json +++ b/homeassistant/components/climacell/translations/et.json @@ -10,6 +10,7 @@ "user": { "data": { "api_key": "API v\u00f5ti", + "api_version": "API versioon", "latitude": "Laiuskraad", "longitude": "Pikkuskraad", "name": "Nimi" diff --git a/homeassistant/components/climacell/translations/ru.json b/homeassistant/components/climacell/translations/ru.json index 2cce63d95ea03..7e40c61911204 100644 --- a/homeassistant/components/climacell/translations/ru.json +++ b/homeassistant/components/climacell/translations/ru.json @@ -10,6 +10,7 @@ "user": { "data": { "api_key": "\u041a\u043b\u044e\u0447 API", + "api_version": "\u0412\u0435\u0440\u0441\u0438\u044f API", "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" diff --git a/homeassistant/components/deconz/translations/ca.json b/homeassistant/components/deconz/translations/ca.json index d5729f73444b8..60d91a83db8b3 100644 --- a/homeassistant/components/deconz/translations/ca.json +++ b/homeassistant/components/deconz/translations/ca.json @@ -42,6 +42,10 @@ "button_2": "Segon bot\u00f3", "button_3": "Tercer bot\u00f3", "button_4": "Quart bot\u00f3", + "button_5": "Cinqu\u00e8 bot\u00f3", + "button_6": "Sis\u00e8 bot\u00f3", + "button_7": "Set\u00e8 bot\u00f3", + "button_8": "Vuit\u00e8 bot\u00f3", "close": "Tanca", "dim_down": "Atenua la brillantor", "dim_up": "Augmenta la brillantor", diff --git a/homeassistant/components/deconz/translations/en.json b/homeassistant/components/deconz/translations/en.json index 132d8b60feaf0..14ddb6890d429 100644 --- a/homeassistant/components/deconz/translations/en.json +++ b/homeassistant/components/deconz/translations/en.json @@ -42,6 +42,10 @@ "button_2": "Second button", "button_3": "Third button", "button_4": "Fourth button", + "button_5": "Fifth button", + "button_6": "Sixth button", + "button_7": "Seventh button", + "button_8": "Eighth button", "close": "Close", "dim_down": "Dim down", "dim_up": "Dim up", diff --git a/homeassistant/components/deconz/translations/et.json b/homeassistant/components/deconz/translations/et.json index 6a3b6d07592c8..e52b54166a14b 100644 --- a/homeassistant/components/deconz/translations/et.json +++ b/homeassistant/components/deconz/translations/et.json @@ -42,6 +42,10 @@ "button_2": "Teine nupp", "button_3": "Kolmas nupp", "button_4": "Neljas nupp", + "button_5": "Viies nupp", + "button_6": "Kuues nupp", + "button_7": "Seitsmes nupp", + "button_8": "Kaheksas nupp", "close": "Sulge", "dim_down": "H\u00e4marda", "dim_up": "Tee heledamaks", diff --git a/homeassistant/components/deconz/translations/ko.json b/homeassistant/components/deconz/translations/ko.json index 30597cf3af6ff..5158d557106f8 100644 --- a/homeassistant/components/deconz/translations/ko.json +++ b/homeassistant/components/deconz/translations/ko.json @@ -42,6 +42,10 @@ "button_2": "\ub450 \ubc88\uc9f8", "button_3": "\uc138 \ubc88\uc9f8", "button_4": "\ub124 \ubc88\uc9f8", + "button_5": "\ub2e4\uc12f \ubc88\uc9f8 \ubc84\ud2bc", + "button_6": "\uc5ec\uc12f \ubc88\uc9f8 \ubc84\ud2bc", + "button_7": "\uc77c\uacf1 \ubc88\uc9f8 \ubc84\ud2bc", + "button_8": "\uc5ec\ub35f \ubc88\uc9f8 \ubc84\ud2bc", "close": "\ub2eb\uae30", "dim_down": "\uc5b4\ub461\uac8c \ud558\uae30", "dim_up": "\ubc1d\uac8c \ud558\uae30", diff --git a/homeassistant/components/deconz/translations/nl.json b/homeassistant/components/deconz/translations/nl.json index 18fcea974c355..0d0a745bc1b26 100644 --- a/homeassistant/components/deconz/translations/nl.json +++ b/homeassistant/components/deconz/translations/nl.json @@ -42,6 +42,10 @@ "button_2": "Tweede knop", "button_3": "Derde knop", "button_4": "Vierde knop", + "button_5": "Vijfde knop", + "button_6": "Zesde knop", + "button_7": "Zevende knop", + "button_8": "Achtste knop", "close": "Sluiten", "dim_down": "Dim omlaag", "dim_up": "Dim omhoog", diff --git a/homeassistant/components/deconz/translations/ru.json b/homeassistant/components/deconz/translations/ru.json index 7a78c671f5ffd..de97d799381cd 100644 --- a/homeassistant/components/deconz/translations/ru.json +++ b/homeassistant/components/deconz/translations/ru.json @@ -42,6 +42,10 @@ "button_2": "\u0412\u0442\u043e\u0440\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", "button_3": "\u0422\u0440\u0435\u0442\u044c\u044f \u043a\u043d\u043e\u043f\u043a\u0430", "button_4": "\u0427\u0435\u0442\u0432\u0435\u0440\u0442\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button_5": "\u041f\u044f\u0442\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button_6": "\u0428\u0435\u0441\u0442\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button_7": "\u0421\u0435\u0434\u044c\u043c\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button_8": "\u0412\u043e\u0441\u044c\u043c\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", "close": "\u0417\u0430\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", "dim_down": "\u0423\u043c\u0435\u043d\u044c\u0448\u0438\u0442\u044c \u044f\u0440\u043a\u043e\u0441\u0442\u044c", "dim_up": "\u0423\u0432\u0435\u043b\u0438\u0447\u0438\u0442\u044c \u044f\u0440\u043a\u043e\u0441\u0442\u044c", diff --git a/homeassistant/components/deconz/translations/zh-Hant.json b/homeassistant/components/deconz/translations/zh-Hant.json index 70642ace1bf9b..a80afaf46954f 100644 --- a/homeassistant/components/deconz/translations/zh-Hant.json +++ b/homeassistant/components/deconz/translations/zh-Hant.json @@ -42,6 +42,10 @@ "button_2": "\u7b2c\u4e8c\u500b\u6309\u9215", "button_3": "\u7b2c\u4e09\u500b\u6309\u9215", "button_4": "\u7b2c\u56db\u500b\u6309\u9215", + "button_5": "\u7b2c\u4e94\u500b\u6309\u9215", + "button_6": "\u7b2c\u516d\u500b\u6309\u9215", + "button_7": "\u7b2c\u4e03\u500b\u6309\u9215", + "button_8": "\u7b2c\u516b\u500b\u6309\u9215", "close": "\u95dc\u9589", "dim_down": "\u8abf\u6697", "dim_up": "\u8abf\u4eae", diff --git a/homeassistant/components/emonitor/translations/ca.json b/homeassistant/components/emonitor/translations/ca.json new file mode 100644 index 0000000000000..b6fd1f99c849d --- /dev/null +++ b/homeassistant/components/emonitor/translations/ca.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "unknown": "Error inesperat" + }, + "flow_title": "SiteSage {name}", + "step": { + "confirm": { + "description": "Vols configurar {name} ({host})?", + "title": "Configura SiteSage Emonitor" + }, + "user": { + "data": { + "host": "Amfitri\u00f3" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/emonitor/translations/et.json b/homeassistant/components/emonitor/translations/et.json new file mode 100644 index 0000000000000..bea6607a9cad2 --- /dev/null +++ b/homeassistant/components/emonitor/translations/et.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "unknown": "Tundmatu viga" + }, + "flow_title": "SiteSage {name}", + "step": { + "confirm": { + "description": "Kas soovid seadistada {name}({host})?", + "title": "SiteSage Emonitori seadistamine" + }, + "user": { + "data": { + "host": "" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/emonitor/translations/ko.json b/homeassistant/components/emonitor/translations/ko.json new file mode 100644 index 0000000000000..36e9fa7a04c7c --- /dev/null +++ b/homeassistant/components/emonitor/translations/ko.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "flow_title": "SiteSage {name}", + "step": { + "confirm": { + "description": "{name} ({host})\uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "SiteSage eMonitor \uc124\uc815\ud558\uae30" + }, + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/emonitor/translations/ru.json b/homeassistant/components/emonitor/translations/ru.json new file mode 100644 index 0000000000000..e9ae6b12e86ae --- /dev/null +++ b/homeassistant/components/emonitor/translations/ru.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "flow_title": "SiteSage {name}", + "step": { + "confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name} ({host})?", + "title": "SiteSage Emonitor" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/emonitor/translations/zh-Hant.json b/homeassistant/components/emonitor/translations/zh-Hant.json new file mode 100644 index 0000000000000..371cf7575423f --- /dev/null +++ b/homeassistant/components/emonitor/translations/zh-Hant.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "flow_title": "SiteSage {name}", + "step": { + "confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name} ({host})\uff1f", + "title": "\u8a2d\u5b9a SiteSage Emonitor" + }, + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/ca.json b/homeassistant/components/enphase_envoy/translations/ca.json new file mode 100644 index 0000000000000..f388abca5b808 --- /dev/null +++ b/homeassistant/components/enphase_envoy/translations/ca.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "flow_title": "Envoy {serial} ({host})", + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "password": "Contrasenya", + "username": "Nom d'usuari" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/et.json b/homeassistant/components/enphase_envoy/translations/et.json new file mode 100644 index 0000000000000..34f052809df9a --- /dev/null +++ b/homeassistant/components/enphase_envoy/translations/et.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamise viga", + "unknown": "Tundmatu viga" + }, + "flow_title": "Envoy {serial} ({host})", + "step": { + "user": { + "data": { + "host": "Host", + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/ko.json b/homeassistant/components/enphase_envoy/translations/ko.json new file mode 100644 index 0000000000000..74ec68256be47 --- /dev/null +++ b/homeassistant/components/enphase_envoy/translations/ko.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "flow_title": "Envoy {serial} ({host})", + "step": { + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/ru.json b/homeassistant/components/enphase_envoy/translations/ru.json new file mode 100644 index 0000000000000..f10538617398f --- /dev/null +++ b/homeassistant/components/enphase_envoy/translations/ru.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "flow_title": "Envoy {serial} ({host})", + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/zh-Hant.json b/homeassistant/components/enphase_envoy/translations/zh-Hant.json new file mode 100644 index 0000000000000..bf901948b244b --- /dev/null +++ b/homeassistant/components/enphase_envoy/translations/zh-Hant.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "flow_title": "Envoy {serial} ({host})", + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_travel_time/translations/et.json b/homeassistant/components/google_travel_time/translations/et.json index 488a473d14f75..e99472f46a3bc 100644 --- a/homeassistant/components/google_travel_time/translations/et.json +++ b/homeassistant/components/google_travel_time/translations/et.json @@ -27,7 +27,7 @@ "time": "Aeg", "time_type": "Aja t\u00fc\u00fcp", "transit_mode": "Liikumisviis", - "transit_routing_preference": "Marsruudi eelistus", + "transit_routing_preference": "Teekonna eelistused", "units": "\u00dchikud" }, "description": "Soovi korral saad m\u00e4\u00e4rata kas v\u00e4ljumisaja v\u00f5i saabumisaja. V\u00e4ljumisaja m\u00e4\u00e4ramisel saad sisestada \"kohe\", Unix-ajatempli v\u00f5i 24-tunnise ajastringi (nt 08:00:00). Saabumisaja m\u00e4\u00e4ramisel saad kasutada Unix-ajatemplit v\u00f5i 24-tunnist ajastringi nagu '08:00:00'" diff --git a/homeassistant/components/harmony/translations/ko.json b/homeassistant/components/harmony/translations/ko.json index 026e751b788f4..0e9d2a2cf573f 100644 --- a/homeassistant/components/harmony/translations/ko.json +++ b/homeassistant/components/harmony/translations/ko.json @@ -10,7 +10,7 @@ "flow_title": "Logitech Harmony Hub: {name}", "step": { "link": { - "description": "{name} ({host}) \uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "{name} ({host})\uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "Logitech Harmony Hub \uc124\uc815\ud558\uae30" }, "user": { diff --git a/homeassistant/components/hunterdouglas_powerview/translations/ko.json b/homeassistant/components/hunterdouglas_powerview/translations/ko.json index d16945084d00f..5520800c38d64 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/ko.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/ko.json @@ -9,7 +9,7 @@ }, "step": { "link": { - "description": "{name} ({host}) \uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "{name} ({host})\uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "PowerView \ud5c8\ube0c\uc5d0 \uc5f0\uacb0\ud558\uae30" }, "user": { diff --git a/homeassistant/components/met_eireann/translations/en.json b/homeassistant/components/met_eireann/translations/en.json index 76b778282e64e..f01586a15a7e6 100644 --- a/homeassistant/components/met_eireann/translations/en.json +++ b/homeassistant/components/met_eireann/translations/en.json @@ -1,23 +1,19 @@ { "config": { + "error": { + "already_configured": "Service is already configured" + }, "step": { "user": { - "title": "Met Éireann", - "description": "Enter your location to use weather data from the Met Éireann Public Weather Forecast API", "data": { - "name": "Name", + "elevation": "Elevation", "latitude": "Latitude", "longitude": "Longitude", - "elevation": "Elevation (in meters)" - } + "name": "Name" + }, + "description": "Enter your location to use weather data from the Met \u00c9ireann Public Weather Forecast API", + "title": "Location" } - }, - "error": { - "name_exists": "Location already exists" - }, - "abort": { - "already_configured": "Location is already configured", - "unknown": "Unexpected error" } } -} +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/translations/ko.json b/homeassistant/components/opentherm_gw/translations/ko.json index 00f2902a4f3ca..658fd24348e38 100644 --- a/homeassistant/components/opentherm_gw/translations/ko.json +++ b/homeassistant/components/opentherm_gw/translations/ko.json @@ -23,7 +23,8 @@ "floor_temperature": "\uc628\ub3c4 \uc18c\uc218\uc810 \ubc84\ub9bc", "precision": "\uc815\ubc00\ub3c4", "read_precision": "\uc77d\uae30 \uc815\ubc00\ub3c4", - "set_precision": "\uc815\ubc00\ub3c4 \uc124\uc815\ud558\uae30" + "set_precision": "\uc815\ubc00\ub3c4 \uc124\uc815\ud558\uae30", + "temporary_override_mode": "\uc784\uc2dc \uc124\uc815\uac12 \uc7ac\uc815\uc758 \ubaa8\ub4dc" }, "description": "OpenTherm Gateway \uc635\uc158" } diff --git a/homeassistant/components/roomba/translations/ko.json b/homeassistant/components/roomba/translations/ko.json index 4e2db24ba7384..bb33287c9b83e 100644 --- a/homeassistant/components/roomba/translations/ko.json +++ b/homeassistant/components/roomba/translations/ko.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "not_irobot_device": "\ubc1c\uacac\ub41c \uae30\uae30\ub294 \uc544\uc774\ub85c\ubd07 \uae30\uae30\uac00 \uc544\ub2d9\ub2c8\ub2e4" + "not_irobot_device": "\ubc1c\uacac\ub41c \uae30\uae30\ub294 \uc544\uc774\ub85c\ubd07 \uae30\uae30\uac00 \uc544\ub2d9\ub2c8\ub2e4", + "short_blid": "BLID\uac00 \uc798\ub838\uc2b5\ub2c8\ub2e4" }, "error": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" diff --git a/homeassistant/components/songpal/translations/ko.json b/homeassistant/components/songpal/translations/ko.json index abe7f9b384cf2..987f5fa76a3eb 100644 --- a/homeassistant/components/songpal/translations/ko.json +++ b/homeassistant/components/songpal/translations/ko.json @@ -10,7 +10,7 @@ "flow_title": "Sony Songpal: {name} ({host})", "step": { "init": { - "description": "{name} ({host}) \uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" + "description": "{name} ({host})\uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" }, "user": { "data": { diff --git a/homeassistant/components/synology_dsm/translations/ko.json b/homeassistant/components/synology_dsm/translations/ko.json index ab9dc4d445a64..da61e46731e12 100644 --- a/homeassistant/components/synology_dsm/translations/ko.json +++ b/homeassistant/components/synology_dsm/translations/ko.json @@ -26,7 +26,7 @@ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984", "verify_ssl": "SSL \uc778\uc99d\uc11c \ud655\uc778" }, - "description": "{name} ({host}) \uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "{name} ({host})\uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "Synology DSM" }, "user": { diff --git a/homeassistant/components/waze_travel_time/translations/en.json b/homeassistant/components/waze_travel_time/translations/en.json index 4b113302cda4e..31fd8d0793fee 100644 --- a/homeassistant/components/waze_travel_time/translations/en.json +++ b/homeassistant/components/waze_travel_time/translations/en.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "Location is already configured" }, + "error": { + "cannot_connect": "Failed to connect" + }, "step": { "user": { "data": { From b47a90a9d8dad83d992cea8531a920a393c13e03 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Tue, 6 Apr 2021 05:07:22 +0200 Subject: [PATCH 0074/1317] Add AMD Ryzen processor temperatur capability to systemmonitor (#48705) --- homeassistant/components/systemmonitor/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index 8d0680b72c002..2a5f5a7b22b4e 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -179,6 +179,7 @@ def check_required_arg(value: Any) -> Any: "radeon 1", "soc-thermal 1", "soc_thermal 1", + "Tctl", ] From 2a15ae13a73b8bb880ccbb1384f59d7dec73e566 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 5 Apr 2021 17:22:49 -1000 Subject: [PATCH 0075/1317] Small improvements for emonitor (#48700) - Check reason for config abort - Abort if unique id is already configured on user flow - remove unneeded pylint --- .../components/emonitor/config_flow.py | 3 +- tests/components/emonitor/test_config_flow.py | 36 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/emonitor/config_flow.py b/homeassistant/components/emonitor/config_flow.py index bb18f03e3afdc..bd5650d28cd19 100644 --- a/homeassistant/components/emonitor/config_flow.py +++ b/homeassistant/components/emonitor/config_flow.py @@ -12,7 +12,7 @@ from homeassistant.helpers.device_registry import format_mac from . import name_short_mac -from .const import DOMAIN # pylint:disable=unused-import +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -52,6 +52,7 @@ async def async_step_user(self, user_input=None): await self.async_set_unique_id( format_mac(info["mac_address"]), raise_on_progress=False ) + self._abort_if_unique_id_configured() return self.async_create_entry(title=info["title"], data=user_input) return self.async_show_form( diff --git a/tests/components/emonitor/test_config_flow.py b/tests/components/emonitor/test_config_flow.py index 65fc471786f16..1d71275409a64 100644 --- a/tests/components/emonitor/test_config_flow.py +++ b/tests/components/emonitor/test_config_flow.py @@ -185,3 +185,39 @@ async def test_dhcp_already_exists(hass): await hass.async_block_till_done() assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + +async def test_user_unique_id_already_exists(hass): + """Test creating an entry where the unique_id already exists.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "1.2.3.4"}, + unique_id="aa:bb:cc:dd:ee:ff", + ) + 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["errors"] == {} + + with patch( + "homeassistant.components.emonitor.config_flow.Emonitor.async_get_status", + return_value=_mock_emonitor(), + ), patch( + "homeassistant.components.emonitor.async_setup_entry", + return_value=True, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.2.3.4", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "abort" + assert result2["reason"] == "already_configured" From b57d02d786f1431476cf80370dcab6aa1bc87121 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 6 Apr 2021 09:55:47 +0200 Subject: [PATCH 0076/1317] Bump pychromecast to 9.1.2 (#48714) --- homeassistant/components/cast/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index 0c9d0dfc4a583..3f30bc450fdd4 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -3,7 +3,7 @@ "name": "Google Cast", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/cast", - "requirements": ["pychromecast==9.1.1"], + "requirements": ["pychromecast==9.1.2"], "after_dependencies": ["cloud", "http", "media_source", "plex", "tts", "zeroconf"], "zeroconf": ["_googlecast._tcp.local."], "codeowners": ["@emontnemery"] diff --git a/requirements_all.txt b/requirements_all.txt index 28a400ecfcec5..717727b91b232 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1307,7 +1307,7 @@ pycfdns==1.2.1 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==9.1.1 +pychromecast==9.1.2 # homeassistant.components.pocketcasts pycketcasts==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 64239d0828815..a0f173965f237 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -705,7 +705,7 @@ pybotvac==0.0.20 pycfdns==1.2.1 # homeassistant.components.cast -pychromecast==9.1.1 +pychromecast==9.1.2 # homeassistant.components.climacell pyclimacell==0.18.0 From 9f2fb37e17904a699a38349c1a0be4b93d1dbc55 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 6 Apr 2021 12:39:29 +0200 Subject: [PATCH 0077/1317] Flag brightness support for MQTT RGB lights (#48718) --- .../components/mqtt/light/schema_json.py | 4 ++- .../components/mqtt/light/schema_template.py | 2 +- tests/components/mqtt/test_light.py | 29 ++++++++++++++++- tests/components/mqtt/test_light_json.py | 27 ++++++++++++++++ tests/components/mqtt/test_light_template.py | 31 +++++++++++++++++++ 5 files changed, 90 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 4d56435643ac4..8be3708bd614e 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -197,7 +197,9 @@ def _setup_from_config(self, config): self._supported_features |= config[CONF_BRIGHTNESS] and SUPPORT_BRIGHTNESS self._supported_features |= config[CONF_COLOR_TEMP] and SUPPORT_COLOR_TEMP self._supported_features |= config[CONF_HS] and SUPPORT_COLOR - self._supported_features |= config[CONF_RGB] and SUPPORT_COLOR + self._supported_features |= config[CONF_RGB] and ( + SUPPORT_COLOR | SUPPORT_BRIGHTNESS + ) self._supported_features |= config[CONF_WHITE_VALUE] and SUPPORT_WHITE_VALUE self._supported_features |= config[CONF_XY] and SUPPORT_COLOR diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index 118746f2229f6..7c0266265db5c 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -417,7 +417,7 @@ def supported_features(self): and self._templates[CONF_GREEN_TEMPLATE] is not None and self._templates[CONF_BLUE_TEMPLATE] is not None ): - features = features | SUPPORT_COLOR + features = features | SUPPORT_COLOR | SUPPORT_BRIGHTNESS if self._config.get(CONF_EFFECT_LIST) is not None: features = features | SUPPORT_EFFECT if self._templates[CONF_COLOR_TEMP_TEMPLATE] is not None: diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index 00ff8b28b7785..e995b373d0370 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -161,7 +161,13 @@ from homeassistant import config as hass_config from homeassistant.components import light -from homeassistant.const import ATTR_ASSUMED_STATE, SERVICE_RELOAD, STATE_OFF, STATE_ON +from homeassistant.const import ( + ATTR_ASSUMED_STATE, + ATTR_SUPPORTED_FEATURES, + SERVICE_RELOAD, + STATE_OFF, + STATE_ON, +) import homeassistant.core as ha from homeassistant.setup import async_setup_component @@ -206,6 +212,27 @@ async def test_fail_setup_if_no_command_topic(hass, mqtt_mock): assert hass.states.get("light.test") is None +async def test_rgb_light(hass, mqtt_mock): + """Test RGB light flags brightness support.""" + assert await async_setup_component( + hass, + light.DOMAIN, + { + light.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "test_light_rgb/set", + "rgb_command_topic": "test_light_rgb/rgb/set", + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + expected_features = light.SUPPORT_COLOR | light.SUPPORT_BRIGHTNESS + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features + + async def test_no_color_brightness_color_temp_hs_white_xy_if_no_topics(hass, mqtt_mock): """Test if there is no color and brightness if no topic.""" assert await async_setup_component( diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index 7834e1d26784e..7856eb84c07b0 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -188,6 +188,33 @@ async def test_fail_setup_if_color_mode_deprecated(hass, mqtt_mock, deprecated): assert hass.states.get("light.test") is None +async def test_rgb_light(hass, mqtt_mock): + """Test RGB light flags brightness support.""" + assert await async_setup_component( + hass, + light.DOMAIN, + { + light.DOMAIN: { + "platform": "mqtt", + "schema": "json", + "name": "test", + "command_topic": "test_light_rgb/set", + "rgb": True, + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + expected_features = ( + light.SUPPORT_TRANSITION + | light.SUPPORT_COLOR + | light.SUPPORT_FLASH + | light.SUPPORT_BRIGHTNESS + ) + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features + + async def test_no_color_brightness_color_temp_white_val_if_no_topics(hass, mqtt_mock): """Test for no RGB, brightness, color temp, effect, white val or XY.""" assert await async_setup_component( diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py index 3bbf14ca668a1..2e726d40ef112 100644 --- a/tests/components/mqtt/test_light_template.py +++ b/tests/components/mqtt/test_light_template.py @@ -141,6 +141,37 @@ async def test_setup_fails(hass, mqtt_mock): assert hass.states.get("light.test") is None +async def test_rgb_light(hass, mqtt_mock): + """Test RGB light flags brightness support.""" + assert await async_setup_component( + hass, + light.DOMAIN, + { + light.DOMAIN: { + "platform": "mqtt", + "schema": "template", + "name": "test", + "command_topic": "test_light_rgb/set", + "command_on_template": "on", + "command_off_template": "off", + "red_template": '{{ value.split(",")[4].' 'split("-")[0] }}', + "green_template": '{{ value.split(",")[4].' 'split("-")[1] }}', + "blue_template": '{{ value.split(",")[4].' 'split("-")[2] }}', + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + expected_features = ( + light.SUPPORT_TRANSITION + | light.SUPPORT_COLOR + | light.SUPPORT_FLASH + | light.SUPPORT_BRIGHTNESS + ) + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features + + async def test_state_change_via_topic(hass, mqtt_mock): """Test state change via topic.""" with assert_setup_component(1, light.DOMAIN): From 11ed2f4c30997642a647530dc57032173cc11b43 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Apr 2021 12:49:47 +0200 Subject: [PATCH 0078/1317] Bump codecov/codecov-action from v1.3.1 to v1.3.2 (#48716) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from v1.3.1 to v1.3.2. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/master/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v1.3.1...9b0b9bbe2c64e9ed41413180dd7398450dfeee14) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index afee814b43270..49cbb06e71e50 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -739,4 +739,4 @@ jobs: coverage report --fail-under=94 coverage xml - name: Upload coverage to Codecov - uses: codecov/codecov-action@v1.3.1 + uses: codecov/codecov-action@v1.3.2 From 46b673cdc61c62c6c7d52033857d6424df91c97d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 6 Apr 2021 01:32:14 -1000 Subject: [PATCH 0079/1317] Abort discovery for unsupported doorbird accessories (#48710) --- homeassistant/components/doorbird/__init__.py | 9 +- .../components/doorbird/config_flow.py | 47 +++++--- tests/components/doorbird/test_config_flow.py | 113 +++++++++++++----- 3 files changed, 115 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py index 8376d75ccbbe8..3e8e59df203f2 100644 --- a/homeassistant/components/doorbird/__init__.py +++ b/homeassistant/components/doorbird/__init__.py @@ -1,11 +1,10 @@ """Support for DoorBird devices.""" import asyncio import logging -import urllib -from urllib.error import HTTPError from aiohttp import web from doorbirdpy import DoorBird +import requests import voluptuous as vol from homeassistant.components.http import HomeAssistantView @@ -130,8 +129,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): device = DoorBird(device_ip, username, password) try: status, info = await hass.async_add_executor_job(_init_doorbird_device, device) - except urllib.error.HTTPError as err: - if err.code == HTTP_UNAUTHORIZED: + except requests.exceptions.HTTPError as err: + if err.response.status_code == HTTP_UNAUTHORIZED: _LOGGER.error( "Authorization rejected by DoorBird for %s@%s", username, device_ip ) @@ -202,7 +201,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): async def _async_register_events(hass, doorstation): try: await hass.async_add_executor_job(doorstation.register_events, hass) - except HTTPError: + except requests.exceptions.HTTPError: hass.components.persistent_notification.async_create( "Doorbird configuration failed. Please verify that API " "Operator permission is enabled for the Doorbird user. " diff --git a/homeassistant/components/doorbird/config_flow.py b/homeassistant/components/doorbird/config_flow.py index 1b39bb4a8c349..f69b38c7a7a40 100644 --- a/homeassistant/components/doorbird/config_flow.py +++ b/homeassistant/components/doorbird/config_flow.py @@ -1,9 +1,9 @@ """Config flow for DoorBird integration.""" from ipaddress import ip_address import logging -import urllib from doorbirdpy import DoorBird +import requests import voluptuous as vol from homeassistant import config_entries, core, exceptions @@ -34,17 +34,18 @@ def _schema_with_defaults(host=None, name=None): ) -async def validate_input(hass: core.HomeAssistant, data): - """Validate the user input allows us to connect. +def _check_device(device): + """Verify we can connect to the device and return the status.""" + return device.ready(), device.info() + - Data has the keys from DATA_SCHEMA with values provided by the user. - """ +async def validate_input(hass: core.HomeAssistant, data): + """Validate the user input allows us to connect.""" device = DoorBird(data[CONF_HOST], data[CONF_USERNAME], data[CONF_PASSWORD]) try: - status = await hass.async_add_executor_job(device.ready) - info = await hass.async_add_executor_job(device.info) - except urllib.error.HTTPError as err: - if err.code == HTTP_UNAUTHORIZED: + status, info = await hass.async_add_executor_job(_check_device, device) + except requests.exceptions.HTTPError as err: + if err.response.status_code == HTTP_UNAUTHORIZED: raise InvalidAuth from err raise CannotConnect from err except OSError as err: @@ -59,6 +60,19 @@ async def validate_input(hass: core.HomeAssistant, data): return {"title": data[CONF_HOST], "mac_addr": mac_addr} +async def async_verify_supported_device(hass, host): + """Verify the doorbell state endpoint returns a 401.""" + device = DoorBird(host, "", "") + try: + await hass.async_add_executor_job(device.doorbell_state) + except requests.exceptions.HTTPError as err: + if err.response.status_code == HTTP_UNAUTHORIZED: + return True + except OSError: + return False + return False + + class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for DoorBird.""" @@ -85,17 +99,18 @@ async def async_step_user(self, user_input=None): async def async_step_zeroconf(self, discovery_info): """Prepare configuration for a discovered doorbird device.""" macaddress = discovery_info["properties"]["macaddress"] + host = discovery_info[CONF_HOST] if macaddress[:6] != DOORBIRD_OUI: return self.async_abort(reason="not_doorbird_device") - if is_link_local(ip_address(discovery_info[CONF_HOST])): + if is_link_local(ip_address(host)): return self.async_abort(reason="link_local_address") + if not await async_verify_supported_device(self.hass, host): + return self.async_abort(reason="not_doorbird_device") await self.async_set_unique_id(macaddress) - self._abort_if_unique_id_configured( - updates={CONF_HOST: discovery_info[CONF_HOST]} - ) + self._abort_if_unique_id_configured(updates={CONF_HOST: host}) chop_ending = "._axis-video._tcp.local." friendly_hostname = discovery_info["name"] @@ -104,11 +119,9 @@ async def async_step_zeroconf(self, discovery_info): self.context["title_placeholders"] = { CONF_NAME: friendly_hostname, - CONF_HOST: discovery_info[CONF_HOST], + CONF_HOST: host, } - self.discovery_schema = _schema_with_defaults( - host=discovery_info[CONF_HOST], name=friendly_hostname - ) + self.discovery_schema = _schema_with_defaults(host=host, name=friendly_hostname) return await self.async_step_user() diff --git a/tests/components/doorbird/test_config_flow.py b/tests/components/doorbird/test_config_flow.py index e94f73239f13a..d6bbb7412e66c 100644 --- a/tests/components/doorbird/test_config_flow.py +++ b/tests/components/doorbird/test_config_flow.py @@ -1,6 +1,8 @@ """Test the DoorBird config flow.""" -from unittest.mock import MagicMock, patch -import urllib +from unittest.mock import MagicMock, Mock, patch + +import pytest +import requests from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.doorbird import CONF_CUSTOM_URL, CONF_TOKEN @@ -21,7 +23,9 @@ def _get_mock_doorbirdapi_return_values(ready=None, info=None): doorbirdapi_mock = MagicMock() type(doorbirdapi_mock).ready = MagicMock(return_value=ready) type(doorbirdapi_mock).info = MagicMock(return_value=info) - + type(doorbirdapi_mock).doorbell_state = MagicMock( + side_effect=requests.exceptions.HTTPError(response=Mock(status_code=401)) + ) return doorbirdapi_mock @@ -137,17 +141,25 @@ async def test_form_import_with_zeroconf_already_discovered(hass): await setup.async_setup_component(hass, "persistent_notification", {}) + doorbirdapi = _get_mock_doorbirdapi_return_values( + ready=[True], info={"WIFI_MAC_ADDR": "1CCAE3DOORBIRD"} + ) # Running the zeroconf init will make the unique id # in progress - zero_conf = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_ZEROCONF}, - data={ - "properties": {"macaddress": "1CCAE3DOORBIRD"}, - "name": "Doorstation - abc123._axis-video._tcp.local.", - "host": "192.168.1.5", - }, - ) + with patch( + "homeassistant.components.doorbird.config_flow.DoorBird", + return_value=doorbirdapi, + ): + zero_conf = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data={ + "properties": {"macaddress": "1CCAE3DOORBIRD"}, + "name": "Doorstation - abc123._axis-video._tcp.local.", + "host": "192.168.1.5", + }, + ) + await hass.async_block_till_done() assert zero_conf["type"] == data_entry_flow.RESULT_TYPE_FORM assert zero_conf["step_id"] == "user" assert zero_conf["errors"] == {} @@ -159,9 +171,6 @@ async def test_form_import_with_zeroconf_already_discovered(hass): CONF_CUSTOM_URL ] = "http://legacy.custom.url/should/only/come/in/from/yaml" - doorbirdapi = _get_mock_doorbirdapi_return_values( - ready=[True], info={"WIFI_MAC_ADDR": "1CCAE3DOORBIRD"} - ) with patch( "homeassistant.components.doorbird.config_flow.DoorBird", return_value=doorbirdapi, @@ -244,24 +253,29 @@ async def test_form_zeroconf_correct_oui(hass): await hass.async_add_executor_job( init_recorder_component, hass ) # force in memory db - + doorbirdapi = _get_mock_doorbirdapi_return_values( + ready=[True], info={"WIFI_MAC_ADDR": "macaddr"} + ) await setup.async_setup_component(hass, "persistent_notification", {}) - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_ZEROCONF}, - data={ - "properties": {"macaddress": "1CCAE3DOORBIRD"}, - "name": "Doorstation - abc123._axis-video._tcp.local.", - "host": "192.168.1.5", - }, - ) + with patch( + "homeassistant.components.doorbird.config_flow.DoorBird", + return_value=doorbirdapi, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data={ + "properties": {"macaddress": "1CCAE3DOORBIRD"}, + "name": "Doorstation - abc123._axis-video._tcp.local.", + "host": "192.168.1.5", + }, + ) + await hass.async_block_till_done() assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" assert result["errors"] == {} - doorbirdapi = _get_mock_doorbirdapi_return_values( - ready=[True], info={"WIFI_MAC_ADDR": "macaddr"} - ) + with patch( "homeassistant.components.doorbird.config_flow.DoorBird", return_value=doorbirdapi, @@ -288,6 +302,43 @@ async def test_form_zeroconf_correct_oui(hass): assert len(mock_setup_entry.mock_calls) == 1 +@pytest.mark.parametrize( + "doorbell_state_side_effect", + [ + requests.exceptions.HTTPError(response=Mock(status_code=404)), + OSError, + None, + ], +) +async def test_form_zeroconf_correct_oui_wrong_device(hass, doorbell_state_side_effect): + """Test we can setup from zeroconf with the correct OUI source but not a doorstation.""" + await hass.async_add_executor_job( + init_recorder_component, hass + ) # force in memory db + doorbirdapi = _get_mock_doorbirdapi_return_values( + ready=[True], info={"WIFI_MAC_ADDR": "macaddr"} + ) + type(doorbirdapi).doorbell_state = MagicMock(side_effect=doorbell_state_side_effect) + await setup.async_setup_component(hass, "persistent_notification", {}) + + with patch( + "homeassistant.components.doorbird.config_flow.DoorBird", + return_value=doorbirdapi, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data={ + "properties": {"macaddress": "1CCAE3DOORBIRD"}, + "name": "Doorstation - abc123._axis-video._tcp.local.", + "host": "192.168.1.5", + }, + ) + await hass.async_block_till_done() + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "not_doorbird_device" + + async def test_form_user_cannot_connect(hass): """Test we handle cannot connect error.""" await hass.async_add_executor_job( @@ -322,10 +373,8 @@ async def test_form_user_invalid_auth(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - mock_urllib_error = urllib.error.HTTPError( - "http://xyz.tld", 401, "login failed", {}, None - ) - doorbirdapi = _get_mock_doorbirdapi_side_effects(ready=mock_urllib_error) + mock_error = requests.exceptions.HTTPError(response=Mock(status_code=401)) + doorbirdapi = _get_mock_doorbirdapi_side_effects(ready=mock_error) with patch( "homeassistant.components.doorbird.config_flow.DoorBird", return_value=doorbirdapi, From ae67f300b21948c4860125e2d0af84d4b9bc93f5 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 6 Apr 2021 16:50:15 +0200 Subject: [PATCH 0080/1317] Fix sync api use in alarm control panel test (#48725) --- .../custom_components/test/alarm_control_panel.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/testing_config/custom_components/test/alarm_control_panel.py b/tests/testing_config/custom_components/test/alarm_control_panel.py index 6535f5aa1f595..864c99ec5df21 100644 --- a/tests/testing_config/custom_components/test/alarm_control_panel.py +++ b/tests/testing_config/custom_components/test/alarm_control_panel.py @@ -84,25 +84,25 @@ def supported_features(self) -> int: def alarm_arm_away(self, code=None): """Send arm away command.""" self._state = STATE_ALARM_ARMED_AWAY - self.async_write_ha_state() + self.schedule_update_ha_state() def alarm_arm_home(self, code=None): """Send arm home command.""" self._state = STATE_ALARM_ARMED_HOME - self.async_write_ha_state() + self.schedule_update_ha_state() def alarm_arm_night(self, code=None): """Send arm night command.""" self._state = STATE_ALARM_ARMED_NIGHT - self.async_write_ha_state() + self.schedule_update_ha_state() def alarm_disarm(self, code=None): """Send disarm command.""" if code == "1234": self._state = STATE_ALARM_DISARMED - self.async_write_ha_state() + self.schedule_update_ha_state() def alarm_trigger(self, code=None): """Send alarm trigger command.""" self._state = STATE_ALARM_TRIGGERED - self.async_write_ha_state() + self.schedule_update_ha_state() From 42d20395609b1fcbc51b91e8a09125d4f40fae7d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 6 Apr 2021 11:14:54 -0700 Subject: [PATCH 0081/1317] Updated frontend to 20210406.0 (#48734) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 55392323f3d91..b659ec7e7d4c5 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20210402.1" + "home-assistant-frontend==20210406.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7c8c7baf34182..3d8895d0a8270 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ defusedxml==0.6.0 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.42.0 -home-assistant-frontend==20210402.1 +home-assistant-frontend==20210406.0 httpx==0.17.1 jinja2>=2.11.3 netdisco==2.8.2 diff --git a/requirements_all.txt b/requirements_all.txt index 717727b91b232..570f8de1c47e0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -762,7 +762,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210402.1 +home-assistant-frontend==20210406.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a0f173965f237..b7c8a03c79094 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -423,7 +423,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210402.1 +home-assistant-frontend==20210406.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From c4f9489d6126c57bbdfd271bfb743e6c91795188 Mon Sep 17 00:00:00 2001 From: Justin Paupore Date: Tue, 6 Apr 2021 11:39:54 -0700 Subject: [PATCH 0082/1317] Fix infinite recursion in LazyState (#48719) If LazyState cannot parse the attributes of its row as JSON, it prints a message to the logger. Unfortunately, it passes `self` as a format argument to that message, which causes its `__repr__` method to be called, which then tries to retrieve `self.attributes` in order to display them. This leads to an infinite recursion and a crash of the entire core. To fix, send the database row to be printed in the log message, rather than the LazyState object that wraps around it. --- homeassistant/components/history/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 59fdcc7811ba1..09f459b32d6b2 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -715,7 +715,7 @@ def attributes(self): self._attributes = json.loads(self._row.attributes) except ValueError: # When json.loads fails - _LOGGER.exception("Error converting row to state: %s", self) + _LOGGER.exception("Error converting row to state: %s", self._row) self._attributes = {} return self._attributes From 09635678bc6d475ce6c16cd9fa835de8d9d4cfcb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 6 Apr 2021 12:10:39 -0700 Subject: [PATCH 0083/1317] Allow reloading top-level template entities (#48733) --- homeassistant/components/template/__init__.py | 89 ++++++++++++++++--- homeassistant/components/template/config.py | 68 +++++++------- tests/components/template/test_init.py | 40 +++++++-- .../template/sensor_configuration.yaml | 7 ++ 4 files changed, 151 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/template/__init__.py b/homeassistant/components/template/__init__.py index f9b6b3b497577..72a97d6eeab7a 100644 --- a/homeassistant/components/template/__init__.py +++ b/homeassistant/components/template/__init__.py @@ -1,54 +1,112 @@ """The template component.""" +from __future__ import annotations + +import asyncio import logging -from typing import Optional +from typing import Callable +from homeassistant import config as conf_util from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.const import EVENT_HOMEASSISTANT_START -from homeassistant.core import CoreState, callback +from homeassistant.const import EVENT_HOMEASSISTANT_START, SERVICE_RELOAD +from homeassistant.core import CoreState, Event, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import ( discovery, trigger as trigger_helper, update_coordinator, ) -from homeassistant.helpers.reload import async_setup_reload_service +from homeassistant.helpers.reload import async_reload_integration_platforms +from homeassistant.loader import async_get_integration from .const import CONF_TRIGGER, DOMAIN, PLATFORMS +_LOGGER = logging.getLogger(__name__) + async def async_setup(hass, config): """Set up the template integration.""" if DOMAIN in config: - for conf in config[DOMAIN]: - coordinator = TriggerUpdateCoordinator(hass, conf) - await coordinator.async_setup(config) + await _process_config(hass, config) + + async def _reload_config(call: Event) -> None: + """Reload top-level + platforms.""" + try: + unprocessed_conf = await conf_util.async_hass_config_yaml(hass) + except HomeAssistantError as err: + _LOGGER.error(err) + return + + conf = await conf_util.async_process_component_config( + hass, unprocessed_conf, await async_get_integration(hass, DOMAIN) + ) - await async_setup_reload_service(hass, DOMAIN, PLATFORMS) + if conf is None: + return + + await async_reload_integration_platforms(hass, DOMAIN, PLATFORMS) + + if DOMAIN in conf: + await _process_config(hass, conf) + + hass.bus.async_fire(f"event_{DOMAIN}_reloaded", context=call.context) + + hass.helpers.service.async_register_admin_service( + DOMAIN, SERVICE_RELOAD, _reload_config + ) return True +async def _process_config(hass, config): + """Process config.""" + coordinators: list[TriggerUpdateCoordinator] | None = hass.data.get(DOMAIN) + + # Remove old ones + if coordinators: + for coordinator in coordinators: + coordinator.async_remove() + + async def init_coordinator(hass, conf): + coordinator = TriggerUpdateCoordinator(hass, conf) + await coordinator.async_setup(conf) + return coordinator + + hass.data[DOMAIN] = await asyncio.gather( + *[init_coordinator(hass, conf) for conf in config[DOMAIN]] + ) + + class TriggerUpdateCoordinator(update_coordinator.DataUpdateCoordinator): """Class to handle incoming data.""" + REMOVE_TRIGGER = object() + def __init__(self, hass, config): """Instantiate trigger data.""" - super().__init__( - hass, logging.getLogger(__name__), name="Trigger Update Coordinator" - ) + super().__init__(hass, _LOGGER, name="Trigger Update Coordinator") self.config = config - self._unsub_trigger = None + self._unsub_start: Callable[[], None] | None = None + self._unsub_trigger: Callable[[], None] | None = None @property - def unique_id(self) -> Optional[str]: + def unique_id(self) -> str | None: """Return unique ID for the entity.""" return self.config.get("unique_id") + @callback + def async_remove(self): + """Signal that the entities need to remove themselves.""" + if self._unsub_start: + self._unsub_start() + if self._unsub_trigger: + self._unsub_trigger() + async def async_setup(self, hass_config): """Set up the trigger and create entities.""" if self.hass.state == CoreState.running: await self._attach_triggers() else: - self.hass.bus.async_listen_once( + self._unsub_start = self.hass.bus.async_listen_once( EVENT_HOMEASSISTANT_START, self._attach_triggers ) @@ -65,6 +123,9 @@ async def async_setup(self, hass_config): async def _attach_triggers(self, start_event=None) -> None: """Attach the triggers.""" + if start_event is not None: + self._unsub_start = None + self._unsub_trigger = await trigger_helper.async_initialize_triggers( self.hass, self.config[CONF_TRIGGER], diff --git a/homeassistant/components/template/config.py b/homeassistant/components/template/config.py index edef5673f31c9..5d1a66836f3e2 100644 --- a/homeassistant/components/template/config.py +++ b/homeassistant/components/template/config.py @@ -36,7 +36,7 @@ ) from .sensor import SENSOR_SCHEMA as PLATFORM_SENSOR_SCHEMA -CONVERSION_PLATFORM = { +LEGACY_SENSOR = { CONF_ICON_TEMPLATE: CONF_ICON, CONF_ENTITY_PICTURE_TEMPLATE: CONF_PICTURE, CONF_AVAILABILITY_TEMPLATE: CONF_AVAILABILITY, @@ -61,7 +61,7 @@ } ) -TRIGGER_ENTITY_SCHEMA = vol.Schema( +CONFIG_SECTION_SCHEMA = vol.Schema( { vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Required(CONF_TRIGGER): cv.TRIGGER_SCHEMA, @@ -71,16 +71,43 @@ ) +def _rewrite_legacy_to_modern_trigger_conf(cfg: dict): + """Rewrite a legacy to a modern trigger-basd conf.""" + logging.getLogger(__name__).warning( + "The entity definition format under template: differs from the platform configuration format. See https://www.home-assistant.io/integrations/template#configuration-for-trigger-based-template-sensors" + ) + sensor = list(cfg[SENSOR_DOMAIN]) if SENSOR_DOMAIN in cfg else [] + + for device_id, entity_cfg in cfg[CONF_SENSORS].items(): + entity_cfg = {**entity_cfg} + + for from_key, to_key in LEGACY_SENSOR.items(): + if from_key not in entity_cfg or to_key in entity_cfg: + continue + + val = entity_cfg.pop(from_key) + if isinstance(val, str): + val = template.Template(val) + entity_cfg[to_key] = val + + if CONF_NAME not in entity_cfg: + entity_cfg[CONF_NAME] = template.Template(device_id) + + sensor.append(entity_cfg) + + return {**cfg, "sensor": sensor} + + async def async_validate_config(hass, config): """Validate config.""" if DOMAIN not in config: return config - trigger_entity_configs = [] + config_sections = [] for cfg in cv.ensure_list(config[DOMAIN]): try: - cfg = TRIGGER_ENTITY_SCHEMA(cfg) + cfg = CONFIG_SECTION_SCHEMA(cfg) cfg[CONF_TRIGGER] = await async_validate_trigger_config( hass, cfg[CONF_TRIGGER] ) @@ -88,39 +115,14 @@ async def async_validate_config(hass, config): async_log_exception(err, DOMAIN, cfg, hass) continue - if CONF_SENSORS not in cfg: - trigger_entity_configs.append(cfg) - continue - - logging.getLogger(__name__).warning( - "The entity definition format under template: differs from the platform configuration format. See https://www.home-assistant.io/integrations/template#configuration-for-trigger-based-template-sensors" - ) - sensor = list(cfg[SENSOR_DOMAIN]) if SENSOR_DOMAIN in cfg else [] - - for device_id, entity_cfg in cfg[CONF_SENSORS].items(): - entity_cfg = {**entity_cfg} - - for from_key, to_key in CONVERSION_PLATFORM.items(): - if from_key not in entity_cfg or to_key in entity_cfg: - continue - - val = entity_cfg.pop(from_key) - if isinstance(val, str): - val = template.Template(val) - entity_cfg[to_key] = val - - if CONF_NAME not in entity_cfg: - entity_cfg[CONF_NAME] = template.Template(device_id) - - sensor.append(entity_cfg) - - cfg = {**cfg, "sensor": sensor} + if CONF_TRIGGER in cfg and CONF_SENSORS in cfg: + cfg = _rewrite_legacy_to_modern_trigger_conf(cfg) - trigger_entity_configs.append(cfg) + config_sections.append(cfg) # Create a copy of the configuration with all config for current # component removed and add validated config back in. config = config_without_domain(config, DOMAIN) - config[DOMAIN] = trigger_entity_configs + config[DOMAIN] = config_sections return config diff --git a/tests/components/template/test_init.py b/tests/components/template/test_init.py index 107c54c710e86..0f8dff4026fe2 100644 --- a/tests/components/template/test_init.py +++ b/tests/components/template/test_init.py @@ -27,7 +27,14 @@ async def test_reloadable(hass): "value_template": "{{ states.sensor.test_sensor.state }}" }, }, - } + }, + "template": { + "trigger": {"platform": "event", "event_type": "event_1"}, + "sensor": { + "name": "top level", + "state": "{{ trigger.event.data.source }}", + }, + }, }, ) await hass.async_block_till_done() @@ -35,8 +42,12 @@ async def test_reloadable(hass): await hass.async_start() await hass.async_block_till_done() + hass.bus.async_fire("event_1", {"source": "init"}) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 3 assert hass.states.get("sensor.state").state == "mytest" - assert len(hass.states.async_all()) == 2 + assert hass.states.get("sensor.top_level").state == "init" yaml_path = path.join( _get_fixtures_base_path(), @@ -52,11 +63,16 @@ async def test_reloadable(hass): ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 3 + assert len(hass.states.async_all()) == 4 + + hass.bus.async_fire("event_2", {"source": "reload"}) + await hass.async_block_till_done() assert hass.states.get("sensor.state") is None + assert hass.states.get("sensor.top_level") is None assert hass.states.get("sensor.watching_tv_in_master_bedroom").state == "off" assert float(hass.states.get("sensor.combined_sensor_energy_usage").state) == 0 + assert hass.states.get("sensor.top_level_2").state == "reload" async def test_reloadable_can_remove(hass): @@ -74,7 +90,14 @@ async def test_reloadable_can_remove(hass): "value_template": "{{ states.sensor.test_sensor.state }}" }, }, - } + }, + "template": { + "trigger": {"platform": "event", "event_type": "event_1"}, + "sensor": { + "name": "top level", + "state": "{{ trigger.event.data.source }}", + }, + }, }, ) await hass.async_block_till_done() @@ -82,8 +105,12 @@ async def test_reloadable_can_remove(hass): await hass.async_start() await hass.async_block_till_done() + hass.bus.async_fire("event_1", {"source": "init"}) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 3 assert hass.states.get("sensor.state").state == "mytest" - assert len(hass.states.async_all()) == 2 + assert hass.states.get("sensor.top_level").state == "init" yaml_path = path.join( _get_fixtures_base_path(), @@ -251,11 +278,12 @@ async def test_reloadable_multiple_platforms(hass): ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 3 + assert len(hass.states.async_all()) == 4 assert hass.states.get("sensor.state") is None assert hass.states.get("sensor.watching_tv_in_master_bedroom").state == "off" assert float(hass.states.get("sensor.combined_sensor_energy_usage").state) == 0 + assert hass.states.get("sensor.top_level_2") is not None async def test_reload_sensors_that_reference_other_template_sensors(hass): diff --git a/tests/fixtures/template/sensor_configuration.yaml b/tests/fixtures/template/sensor_configuration.yaml index 48ef4cf4304e5..8fb2ae9564fad 100644 --- a/tests/fixtures/template/sensor_configuration.yaml +++ b/tests/fixtures/template/sensor_configuration.yaml @@ -21,3 +21,10 @@ sensor: == "Watch TV" or state_attr("remote.alexander_master_bedroom","current_activity") == "Watch Apple TV" %}on{% else %}off{% endif %}' +template: + trigger: + platform: event + event_type: event_2 + sensor: + name: top level 2 + state: "{{ trigger.event.data.source }}" From 9f5db2ce3fdf8bcea210d8bec7a89a9b881120d9 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 6 Apr 2021 21:11:42 +0200 Subject: [PATCH 0084/1317] Improve warnings on undefined template errors (#48713) --- homeassistant/helpers/template.py | 66 +++++++++++++++++++++++++++---- tests/helpers/test_template.py | 5 ++- 2 files changed, 63 insertions(+), 8 deletions(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 4989c4172aea5..9580da82d65ac 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -6,6 +6,7 @@ import base64 import collections.abc from contextlib import suppress +from contextvars import ContextVar from datetime import datetime, timedelta from functools import partial, wraps import json @@ -79,6 +80,8 @@ ALL_STATES_RATE_LIMIT = timedelta(minutes=1) DOMAIN_STATES_RATE_LIMIT = timedelta(seconds=1) +template_cv: ContextVar[str | None] = ContextVar("template_cv", default=None) + @bind_hass def attach(hass: HomeAssistant, obj: Any) -> None: @@ -299,7 +302,7 @@ def __init__(self, template, hass=None): self.template: str = template.strip() self._compiled_code = None - self._compiled: Template | None = None + self._compiled: jinja2.Template | None = None self.hass = hass self.is_static = not is_template_string(template) self._limited = None @@ -370,7 +373,7 @@ def async_render( kwargs.update(variables) try: - render_result = compiled.render(kwargs) + render_result = _render_with_context(self.template, compiled, **kwargs) except Exception as err: raise TemplateError(err) from err @@ -442,7 +445,7 @@ async def async_render_will_timeout( def _render_template() -> None: try: - compiled.render(kwargs) + _render_with_context(self.template, compiled, **kwargs) except TimeoutError: pass finally: @@ -524,7 +527,9 @@ def async_render_with_possible_json_value( variables["value_json"] = json.loads(value) try: - return self._compiled.render(variables).strip() + return _render_with_context( + self.template, self._compiled, **variables + ).strip() except jinja2.TemplateError as ex: if error_value is _SENTINEL: _LOGGER.error( @@ -535,7 +540,7 @@ def async_render_with_possible_json_value( ) return value if error_value is _SENTINEL else error_value - def _ensure_compiled(self, limited: bool = False) -> Template: + def _ensure_compiled(self, limited: bool = False) -> jinja2.Template: """Bind a template to a specific hass instance.""" self.ensure_valid() @@ -548,7 +553,7 @@ def _ensure_compiled(self, limited: bool = False) -> Template: env = self._env self._compiled = cast( - Template, + jinja2.Template, jinja2.Template.from_code(env, self._compiled_code, env.globals, None), ) @@ -1314,12 +1319,59 @@ def urlencode(value): return urllib_urlencode(value).encode("utf-8") +def _render_with_context( + template_str: str, template: jinja2.Template, **kwargs: Any +) -> str: + """Store template being rendered in a ContextVar to aid error handling.""" + template_cv.set(template_str) + return template.render(**kwargs) + + +class LoggingUndefined(jinja2.Undefined): + """Log on undefined variables.""" + + def _log_message(self): + template = template_cv.get() or "" + _LOGGER.warning( + "Template variable warning: %s when rendering '%s'", + self._undefined_message, + template, + ) + + def _fail_with_undefined_error(self, *args, **kwargs): + try: + return super()._fail_with_undefined_error(*args, **kwargs) + except self._undefined_exception as ex: + template = template_cv.get() or "" + _LOGGER.error( + "Template variable error: %s when rendering '%s'", + self._undefined_message, + template, + ) + raise ex + + def __str__(self): + """Log undefined __str___.""" + self._log_message() + return super().__str__() + + def __iter__(self): + """Log undefined __iter___.""" + self._log_message() + return super().__iter__() + + def __bool__(self): + """Log undefined __bool___.""" + self._log_message() + return super().__bool__() + + class TemplateEnvironment(ImmutableSandboxedEnvironment): """The Home Assistant template environment.""" def __init__(self, hass, limited=False): """Initialise template environment.""" - super().__init__(undefined=jinja2.make_logging_undefined(logger=_LOGGER)) + super().__init__(undefined=LoggingUndefined) self.hass = hass self.template_cache = weakref.WeakValueDictionary() self.filters["round"] = forgiving_round diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index da6a8663cc3ce..a8924f513c6e2 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -2503,4 +2503,7 @@ async def test_undefined_variable(hass, caplog): """Test a warning is logged on undefined variables.""" tpl = template.Template("{{ no_such_variable }}", hass) assert tpl.async_render() == "" - assert "Template variable warning: no_such_variable is undefined" in caplog.text + assert ( + "Template variable warning: 'no_such_variable' is undefined when rendering '{{ no_such_variable }}'" + in caplog.text + ) From fb1444c4144b04e32e0396e9fd74dfda3af3d8a3 Mon Sep 17 00:00:00 2001 From: Pascal Reeb Date: Tue, 6 Apr 2021 21:20:57 +0200 Subject: [PATCH 0085/1317] Add doorsensor + coordinator to nuki (#40933) * implemented coordinator + doorsensor * added async_unload_entry * small fixes + reauth_flow * update function * black * define _data inside __init__ * removed unused property * await on update & coverage for binary_sensor * keep reauth seperate from validate * setting entities unavailable when connection goes down * add unknown error when entity is not present * override extra_state_attributes() * removed unnecessary else * moved to locks & openers variables * removed doorsensorState attribute * changed config entry reload to a task * wait for reload --- .coveragerc | 1 + homeassistant/components/nuki/__init__.py | 149 ++++++++++++++++-- .../components/nuki/binary_sensor.py | 73 +++++++++ homeassistant/components/nuki/config_flow.py | 48 +++++- homeassistant/components/nuki/const.py | 13 ++ homeassistant/components/nuki/lock.py | 75 +++------ homeassistant/components/nuki/manifest.json | 2 +- homeassistant/components/nuki/strings.json | 10 ++ .../components/nuki/translations/en.json | 10 ++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/nuki/test_config_flow.py | 101 ++++++++++++ 12 files changed, 411 insertions(+), 75 deletions(-) create mode 100644 homeassistant/components/nuki/binary_sensor.py diff --git a/.coveragerc b/.coveragerc index 624037946c362..a9ae2313f9f7e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -673,6 +673,7 @@ omit = homeassistant/components/nsw_fuel_station/sensor.py homeassistant/components/nuki/__init__.py homeassistant/components/nuki/const.py + homeassistant/components/nuki/binary_sensor.py homeassistant/components/nuki/lock.py homeassistant/components/nut/sensor.py homeassistant/components/nx584/alarm_control_panel.py diff --git a/homeassistant/components/nuki/__init__.py b/homeassistant/components/nuki/__init__.py index 6aa945a52bf53..a96cda070772b 100644 --- a/homeassistant/components/nuki/__init__.py +++ b/homeassistant/components/nuki/__init__.py @@ -1,28 +1,53 @@ """The nuki component.""" +import asyncio from datetime import timedelta +import logging -import voluptuous as vol +import async_timeout +from pynuki import NukiBridge +from pynuki.bridge import InvalidCredentialsException +from requests.exceptions import RequestException -from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant import exceptions +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) + +from .const import ( + DATA_BRIDGE, + DATA_COORDINATOR, + DATA_LOCKS, + DATA_OPENERS, + DEFAULT_TIMEOUT, + DOMAIN, + ERROR_STATES, +) -from .const import DEFAULT_PORT, DOMAIN +_LOGGER = logging.getLogger(__name__) -PLATFORMS = ["lock"] +PLATFORMS = ["binary_sensor", "lock"] UPDATE_INTERVAL = timedelta(seconds=30) -NUKI_SCHEMA = vol.Schema( - vol.All( - { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Required(CONF_TOKEN): cv.string, - }, - ) -) + +def _get_bridge_devices(bridge): + return bridge.locks, bridge.openers + + +def _update_devices(devices): + for device in devices: + for level in (False, True): + try: + device.update(level) + except RequestException: + continue + + if device.state not in ERROR_STATES: + break async def async_setup(hass, config): @@ -46,8 +71,98 @@ async def async_setup(hass, config): async def async_setup_entry(hass, entry): """Set up the Nuki entry.""" - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, LOCK_DOMAIN) + + hass.data.setdefault(DOMAIN, {}) + + try: + bridge = await hass.async_add_executor_job( + NukiBridge, + entry.data[CONF_HOST], + entry.data[CONF_TOKEN], + entry.data[CONF_PORT], + True, + DEFAULT_TIMEOUT, + ) + + locks, openers = await hass.async_add_executor_job(_get_bridge_devices, bridge) + except InvalidCredentialsException: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_REAUTH}, data=entry.data + ) + ) + return False + except RequestException as err: + raise exceptions.ConfigEntryNotReady from err + + async def async_update_data(): + """Fetch data from Nuki bridge.""" + try: + # Note: asyncio.TimeoutError and aiohttp.ClientError are already + # handled by the data update coordinator. + async with async_timeout.timeout(10): + await hass.async_add_executor_job(_update_devices, locks + openers) + except InvalidCredentialsException as err: + raise UpdateFailed(f"Invalid credentials for Bridge: {err}") from err + except RequestException as err: + raise UpdateFailed(f"Error communicating with Bridge: {err}") from err + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + # Name of the data. For logging purposes. + name="nuki devices", + update_method=async_update_data, + # Polling interval. Will only be polled if there are subscribers. + update_interval=UPDATE_INTERVAL, ) + hass.data[DOMAIN][entry.entry_id] = { + DATA_COORDINATOR: coordinator, + DATA_BRIDGE: bridge, + DATA_LOCKS: locks, + DATA_OPENERS: openers, + } + + # Fetch initial data so we have data when entities subscribe + await coordinator.async_refresh() + + for platform in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, platform) + ) + return True + + +async def async_unload_entry(hass, entry): + """Unload the Nuki entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +class NukiEntity(CoordinatorEntity): + """An entity using CoordinatorEntity. + + The CoordinatorEntity class provides: + should_poll + async_update + async_added_to_hass + available + + """ + + def __init__(self, coordinator, nuki_device): + """Pass coordinator to CoordinatorEntity.""" + super().__init__(coordinator) + self._nuki_device = nuki_device diff --git a/homeassistant/components/nuki/binary_sensor.py b/homeassistant/components/nuki/binary_sensor.py new file mode 100644 index 0000000000000..37641dbf15a98 --- /dev/null +++ b/homeassistant/components/nuki/binary_sensor.py @@ -0,0 +1,73 @@ +"""Doorsensor Support for the Nuki Lock.""" + +import logging + +from pynuki import STATE_DOORSENSOR_OPENED + +from homeassistant.components.binary_sensor import DEVICE_CLASS_DOOR, BinarySensorEntity + +from . import NukiEntity +from .const import ATTR_NUKI_ID, DATA_COORDINATOR, DATA_LOCKS, DOMAIN as NUKI_DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the Nuki lock binary sensor.""" + data = hass.data[NUKI_DOMAIN][entry.entry_id] + coordinator = data[DATA_COORDINATOR] + + entities = [] + + for lock in data[DATA_LOCKS]: + if lock.is_door_sensor_activated: + entities.extend([NukiDoorsensorEntity(coordinator, lock)]) + + async_add_entities(entities) + + +class NukiDoorsensorEntity(NukiEntity, BinarySensorEntity): + """Representation of a Nuki Lock Doorsensor.""" + + @property + def name(self): + """Return the name of the lock.""" + return self._nuki_device.name + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return f"{self._nuki_device.nuki_id}_doorsensor" + + @property + def extra_state_attributes(self): + """Return the device specific state attributes.""" + data = { + ATTR_NUKI_ID: self._nuki_device.nuki_id, + } + return data + + @property + def available(self): + """Return true if door sensor is present and activated.""" + return super().available and self._nuki_device.is_door_sensor_activated + + @property + def door_sensor_state(self): + """Return the state of the door sensor.""" + return self._nuki_device.door_sensor_state + + @property + def door_sensor_state_name(self): + """Return the state name of the door sensor.""" + return self._nuki_device.door_sensor_state_name + + @property + def is_on(self): + """Return true if the door is open.""" + return self.door_sensor_state == STATE_DOORSENSOR_OPENED + + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return DEVICE_CLASS_DOOR diff --git a/homeassistant/components/nuki/config_flow.py b/homeassistant/components/nuki/config_flow.py index 7d7a846aa80d6..7a98ad2f00d03 100644 --- a/homeassistant/components/nuki/config_flow.py +++ b/homeassistant/components/nuki/config_flow.py @@ -22,6 +22,8 @@ } ) +REAUTH_SCHEMA = vol.Schema({vol.Required(CONF_TOKEN): str}) + async def validate_input(hass, data): """Validate the user input allows us to connect. @@ -54,6 +56,7 @@ class NukiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize the Nuki config flow.""" self.discovery_schema = {} + self._data = {} async def async_step_import(self, user_input=None): """Handle a flow initiated by import.""" @@ -79,6 +82,50 @@ async def async_step_dhcp(self, discovery_info: dict): return await self.async_step_validate() + async def async_step_reauth(self, data): + """Perform reauth upon an API authentication error.""" + self._data = data + + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm(self, user_input=None): + """Dialog that inform the user that reauth is required.""" + errors = {} + if user_input is None: + return self.async_show_form( + step_id="reauth_confirm", data_schema=REAUTH_SCHEMA + ) + + conf = { + CONF_HOST: self._data[CONF_HOST], + CONF_PORT: self._data[CONF_PORT], + CONF_TOKEN: user_input[CONF_TOKEN], + } + + try: + info = await validate_input(self.hass, conf) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + if not errors: + existing_entry = await self.async_set_unique_id(info["ids"]["hardwareId"]) + if existing_entry: + self.hass.config_entries.async_update_entry(existing_entry, data=conf) + self.hass.async_create_task( + self.hass.config_entries.async_reload(existing_entry.entry_id) + ) + return self.async_abort(reason="reauth_successful") + errors["base"] = "unknown" + + return self.async_show_form( + step_id="reauth_confirm", data_schema=REAUTH_SCHEMA, errors=errors + ) + async def async_step_validate(self, user_input=None): """Handle init step of a flow.""" @@ -102,7 +149,6 @@ async def async_step_validate(self, user_input=None): ) data_schema = self.discovery_schema or USER_SCHEMA - return self.async_show_form( step_id="user", data_schema=data_schema, errors=errors ) diff --git a/homeassistant/components/nuki/const.py b/homeassistant/components/nuki/const.py index 07ef49ebd8872..da12a3a074ddb 100644 --- a/homeassistant/components/nuki/const.py +++ b/homeassistant/components/nuki/const.py @@ -1,6 +1,19 @@ """Constants for Nuki.""" DOMAIN = "nuki" +# Attributes +ATTR_BATTERY_CRITICAL = "battery_critical" +ATTR_NUKI_ID = "nuki_id" +ATTR_UNLATCH = "unlatch" + +# Data +DATA_BRIDGE = "nuki_bridge_data" +DATA_LOCKS = "nuki_locks_data" +DATA_OPENERS = "nuki_openers_data" +DATA_COORDINATOR = "nuki_coordinator" + # Defaults DEFAULT_PORT = 8080 DEFAULT_TIMEOUT = 20 + +ERROR_STATES = (0, 254, 255) diff --git a/homeassistant/components/nuki/lock.py b/homeassistant/components/nuki/lock.py index 360153d14feab..bd5d58ed42ae1 100644 --- a/homeassistant/components/nuki/lock.py +++ b/homeassistant/components/nuki/lock.py @@ -1,31 +1,28 @@ """Nuki.io lock platform.""" from abc import ABC, abstractmethod -from datetime import timedelta import logging -from pynuki import NukiBridge -from requests.exceptions import RequestException import voluptuous as vol from homeassistant.components.lock import PLATFORM_SCHEMA, SUPPORT_OPEN, LockEntity from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN from homeassistant.helpers import config_validation as cv, entity_platform -from .const import DEFAULT_PORT, DEFAULT_TIMEOUT +from . import NukiEntity +from .const import ( + ATTR_BATTERY_CRITICAL, + ATTR_NUKI_ID, + ATTR_UNLATCH, + DATA_COORDINATOR, + DATA_LOCKS, + DATA_OPENERS, + DEFAULT_PORT, + DOMAIN as NUKI_DOMAIN, + ERROR_STATES, +) _LOGGER = logging.getLogger(__name__) -ATTR_BATTERY_CRITICAL = "battery_critical" -ATTR_NUKI_ID = "nuki_id" -ATTR_UNLATCH = "unlatch" - -MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=5) -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=30) - -NUKI_DATA = "nuki" - -ERROR_STATES = (0, 254, 255) - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, @@ -42,25 +39,15 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry(hass, entry, async_add_entities): """Set up the Nuki lock platform.""" - config = config_entry.data - - def get_entities(): - bridge = NukiBridge( - config[CONF_HOST], - config[CONF_TOKEN], - config[CONF_PORT], - True, - DEFAULT_TIMEOUT, - ) - - entities = [NukiLockEntity(lock) for lock in bridge.locks] - entities.extend([NukiOpenerEntity(opener) for opener in bridge.openers]) - return entities - - entities = await hass.async_add_executor_job(get_entities) + data = hass.data[NUKI_DOMAIN][entry.entry_id] + coordinator = data[DATA_COORDINATOR] + entities = [NukiLockEntity(coordinator, lock) for lock in data[DATA_LOCKS]] + entities.extend( + [NukiOpenerEntity(coordinator, opener) for opener in data[DATA_OPENERS]] + ) async_add_entities(entities) platform = entity_platform.current_platform.get() @@ -75,14 +62,9 @@ def get_entities(): ) -class NukiDeviceEntity(LockEntity, ABC): +class NukiDeviceEntity(NukiEntity, LockEntity, ABC): """Representation of a Nuki device.""" - def __init__(self, nuki_device): - """Initialize the lock.""" - self._nuki_device = nuki_device - self._available = nuki_device.state not in ERROR_STATES - @property def name(self): """Return the name of the lock.""" @@ -115,22 +97,7 @@ def supported_features(self): @property def available(self) -> bool: """Return True if entity is available.""" - return self._available - - def update(self): - """Update the nuki lock properties.""" - for level in (False, True): - try: - self._nuki_device.update(aggressive=level) - except RequestException: - _LOGGER.warning("Network issues detect with %s", self.name) - self._available = False - continue - - # If in error state, we force an update and repoll data - self._available = self._nuki_device.state not in ERROR_STATES - if self._available: - break + return super().available and self._nuki_device.state not in ERROR_STATES @abstractmethod def lock(self, **kwargs): diff --git a/homeassistant/components/nuki/manifest.json b/homeassistant/components/nuki/manifest.json index 7fb9a134c4c55..8500a3c90aa06 100644 --- a/homeassistant/components/nuki/manifest.json +++ b/homeassistant/components/nuki/manifest.json @@ -2,7 +2,7 @@ "domain": "nuki", "name": "Nuki", "documentation": "https://www.home-assistant.io/integrations/nuki", - "requirements": ["pynuki==1.3.8"], + "requirements": ["pynuki==1.4.1"], "codeowners": ["@pschmitt", "@pvizeli", "@pree"], "config_flow": true, "dhcp": [{ "hostname": "nuki_bridge_*" }] diff --git a/homeassistant/components/nuki/strings.json b/homeassistant/components/nuki/strings.json index 9e1e4f5e5ab24..3f6de25122a87 100644 --- a/homeassistant/components/nuki/strings.json +++ b/homeassistant/components/nuki/strings.json @@ -7,12 +7,22 @@ "port": "[%key:common::config_flow::data::port%]", "token": "[%key:common::config_flow::data::access_token%]" } + }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "The Nuki integration needs to re-authenticate with your bridge.", + "data": { + "token": "[%key:common::config_flow::data::access_token%]" + } } }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } } } \ No newline at end of file diff --git a/homeassistant/components/nuki/translations/en.json b/homeassistant/components/nuki/translations/en.json index 135e8de2b2f6d..3d53b85920ca2 100644 --- a/homeassistant/components/nuki/translations/en.json +++ b/homeassistant/components/nuki/translations/en.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "reauth_successful": "Successfully reauthenticated." + }, "error": { "cannot_connect": "Failed to connect", "invalid_auth": "Invalid authentication", @@ -12,6 +15,13 @@ "port": "Port", "token": "Access Token" } + }, + "reauth_confirm": { + "title": "Reauthenticate Integration", + "description": "The Nuki integration needs to re-authenticate with your bridge.", + "data": { + "token": "Access Token" + } } } } diff --git a/requirements_all.txt b/requirements_all.txt index 570f8de1c47e0..dde7b83970988 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1572,7 +1572,7 @@ pynetgear==0.6.1 pynetio==0.1.9.1 # homeassistant.components.nuki -pynuki==1.3.8 +pynuki==1.4.1 # homeassistant.components.nut pynut2==2.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b7c8a03c79094..6ee6a8500a284 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -850,7 +850,7 @@ pymyq==3.0.4 pymysensors==0.21.0 # homeassistant.components.nuki -pynuki==1.3.8 +pynuki==1.4.1 # homeassistant.components.nut pynut2==2.1.2 diff --git a/tests/components/nuki/test_config_flow.py b/tests/components/nuki/test_config_flow.py index 4933ea52b77a3..4039eef598402 100644 --- a/tests/components/nuki/test_config_flow.py +++ b/tests/components/nuki/test_config_flow.py @@ -7,6 +7,7 @@ from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS from homeassistant.components.nuki.const import DOMAIN +from homeassistant.const import CONF_TOKEN from .mock import HOST, MAC, MOCK_INFO, NAME, setup_nuki_integration @@ -227,3 +228,103 @@ async def test_dhcp_flow_already_configured(hass): assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" + + +async def test_reauth_success(hass): + """Test starting a reauthentication flow.""" + entry = await setup_nuki_integration(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=entry.data + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "reauth_confirm" + + with patch( + "homeassistant.components.nuki.config_flow.NukiBridge.info", + return_value=MOCK_INFO, + ), patch("homeassistant.components.nuki.async_setup", return_value=True), patch( + "homeassistant.components.nuki.async_setup_entry", + return_value=True, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_TOKEN: "new-token"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["reason"] == "reauth_successful" + assert entry.data[CONF_TOKEN] == "new-token" + + +async def test_reauth_invalid_auth(hass): + """Test starting a reauthentication flow with invalid auth.""" + entry = await setup_nuki_integration(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=entry.data + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "reauth_confirm" + + with patch( + "homeassistant.components.nuki.config_flow.NukiBridge.info", + side_effect=InvalidCredentialsException, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_TOKEN: "new-token"}, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "reauth_confirm" + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_reauth_cannot_connect(hass): + """Test starting a reauthentication flow with cannot connect.""" + entry = await setup_nuki_integration(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=entry.data + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "reauth_confirm" + + with patch( + "homeassistant.components.nuki.config_flow.NukiBridge.info", + side_effect=RequestException, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_TOKEN: "new-token"}, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "reauth_confirm" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_reauth_unknown_exception(hass): + """Test starting a reauthentication flow with an unknown exception.""" + entry = await setup_nuki_integration(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=entry.data + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "reauth_confirm" + + with patch( + "homeassistant.components.nuki.config_flow.NukiBridge.info", + side_effect=Exception, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_TOKEN: "new-token"}, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "reauth_confirm" + assert result2["errors"] == {"base": "unknown"} From 030e9d314dac71feb7fcda5bd1f10489f4106d10 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Tue, 6 Apr 2021 22:58:35 +0200 Subject: [PATCH 0086/1317] Fix systemmonitor IP address look-up logic (#48740) --- homeassistant/components/systemmonitor/sensor.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index 2a5f5a7b22b4e..dea7d371b4b97 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -262,6 +262,7 @@ def _update_sensors() -> None: _swap_memory.cache_clear() _virtual_memory.cache_clear() _net_io_counters.cache_clear() + _net_if_addrs.cache_clear() _getloadavg.cache_clear() async def _async_update_data(*_: Any) -> None: @@ -439,7 +440,7 @@ def _update( else: state = None elif type_ in ["ipv4_address", "ipv6_address"]: - addresses = _net_io_counters() + addresses = _net_if_addrs() if data.argument in addresses: for addr in addresses[data.argument]: if addr.family == IF_ADDRS_FAMILY[type_]: @@ -484,6 +485,11 @@ def _net_io_counters() -> Any: return psutil.net_io_counters(pernic=True) +@lru_cache(maxsize=None) +def _net_if_addrs() -> Any: + return psutil.net_if_addrs() + + @lru_cache(maxsize=None) def _getloadavg() -> tuple[float, float, float]: return os.getloadavg() From d417dcb8f44c95fc0f99583e456b75585e72c9fa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 6 Apr 2021 12:15:36 -1000 Subject: [PATCH 0087/1317] Bump pysonos to 0.0.42 to fix I/O in event loop (#48743) fixes #48732 Changelog: https://github.com/amelchio/pysonos/compare/v0.0.41...v0.0.42 --- homeassistant/components/sonos/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index cd32a3dab2608..f66e25e3d2718 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -3,7 +3,7 @@ "name": "Sonos", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sonos", - "requirements": ["pysonos==0.0.41"], + "requirements": ["pysonos==0.0.42"], "after_dependencies": ["plex"], "ssdp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index dde7b83970988..421c8d44335fc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1723,7 +1723,7 @@ pysnmp==4.4.12 pysoma==0.0.10 # homeassistant.components.sonos -pysonos==0.0.41 +pysonos==0.0.42 # homeassistant.components.spc pyspcwebgw==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6ee6a8500a284..fc2503c9465d9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -938,7 +938,7 @@ pysmartthings==0.7.6 pysoma==0.0.10 # homeassistant.components.sonos -pysonos==0.0.41 +pysonos==0.0.42 # homeassistant.components.spc pyspcwebgw==0.4.0 From e63e8b6ffe627dce8ee7574c652af99267eb7376 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 7 Apr 2021 00:46:47 +0200 Subject: [PATCH 0088/1317] Rename hassio config entry title to Supervisor (#48748) --- homeassistant/components/hassio/config_flow.py | 2 +- tests/components/hassio/test_config_flow.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hassio/config_flow.py b/homeassistant/components/hassio/config_flow.py index 8b2c68d752d90..acc39f4cf91d2 100644 --- a/homeassistant/components/hassio/config_flow.py +++ b/homeassistant/components/hassio/config_flow.py @@ -19,4 +19,4 @@ async def async_step_system(self, user_input=None): # We only need one Hass.io config entry await self.async_set_unique_id(DOMAIN) self._abort_if_unique_id_configured() - return self.async_create_entry(title=DOMAIN.title(), data={}) + return self.async_create_entry(title="Supervisor", data={}) diff --git a/tests/components/hassio/test_config_flow.py b/tests/components/hassio/test_config_flow.py index c2d306183f080..2b4b8a88914fc 100644 --- a/tests/components/hassio/test_config_flow.py +++ b/tests/components/hassio/test_config_flow.py @@ -18,7 +18,7 @@ async def test_config_flow(hass): DOMAIN, context={"source": "system"} ) assert result["type"] == "create_entry" - assert result["title"] == DOMAIN.title() + assert result["title"] == "Supervisor" assert result["data"] == {} await hass.async_block_till_done() From 82cc5148d7faa8dd50bcd8411ae3b1fb35269a16 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Wed, 7 Apr 2021 00:04:06 +0000 Subject: [PATCH 0089/1317] [ci skip] Translation update --- .../components/adguard/translations/no.json | 4 +- .../components/adguard/translations/pl.json | 4 +- .../components/almond/translations/no.json | 4 +- .../components/almond/translations/pl.json | 4 +- .../components/climacell/translations/hu.json | 1 + .../components/climacell/translations/nl.json | 1 + .../components/climacell/translations/no.json | 1 + .../components/climacell/translations/pl.json | 1 + .../climacell/translations/zh-Hant.json | 1 + .../components/deconz/translations/hu.json | 4 ++ .../components/deconz/translations/no.json | 8 +++- .../components/deconz/translations/pl.json | 8 +++- .../components/emonitor/translations/hu.json | 23 +++++++++++ .../components/emonitor/translations/nl.json | 23 +++++++++++ .../components/emonitor/translations/no.json | 23 +++++++++++ .../components/emonitor/translations/pl.json | 23 +++++++++++ .../enphase_envoy/translations/hu.json | 22 +++++++++++ .../enphase_envoy/translations/nl.json | 22 +++++++++++ .../enphase_envoy/translations/no.json | 22 +++++++++++ .../enphase_envoy/translations/pl.json | 22 +++++++++++ .../google_travel_time/translations/no.json | 38 +++++++++++++++++++ .../google_travel_time/translations/pl.json | 38 +++++++++++++++++++ .../components/homekit/translations/no.json | 2 +- .../components/homekit/translations/pl.json | 2 +- .../components/hyperion/translations/hu.json | 9 +++++ .../met_eireann/translations/ca.json | 19 ++++++++++ .../met_eireann/translations/et.json | 19 ++++++++++ .../met_eireann/translations/hu.json | 18 +++++++++ .../met_eireann/translations/nl.json | 19 ++++++++++ .../met_eireann/translations/no.json | 19 ++++++++++ .../met_eireann/translations/pl.json | 19 ++++++++++ .../met_eireann/translations/ru.json | 19 ++++++++++ .../met_eireann/translations/zh-Hant.json | 19 ++++++++++ .../components/mqtt/translations/no.json | 4 +- .../components/mqtt/translations/pl.json | 4 +- .../components/mysensors/translations/hu.json | 1 + .../components/nuki/translations/ca.json | 10 +++++ .../components/nuki/translations/en.json | 16 ++++---- .../components/nuki/translations/et.json | 10 +++++ .../components/nuki/translations/pl.json | 10 +++++ .../components/roomba/translations/pl.json | 7 ++-- .../waze_travel_time/translations/ca.json | 38 +++++++++++++++++++ .../waze_travel_time/translations/et.json | 38 +++++++++++++++++++ .../waze_travel_time/translations/fa.json | 3 ++ .../waze_travel_time/translations/hu.json | 33 ++++++++++++++++ .../waze_travel_time/translations/nl.json | 33 ++++++++++++++++ .../waze_travel_time/translations/no.json | 38 +++++++++++++++++++ .../waze_travel_time/translations/pl.json | 38 +++++++++++++++++++ .../waze_travel_time/translations/ru.json | 38 +++++++++++++++++++ .../translations/zh-Hant.json | 38 +++++++++++++++++++ .../components/zha/translations/pl.json | 1 + 51 files changed, 792 insertions(+), 29 deletions(-) create mode 100644 homeassistant/components/emonitor/translations/hu.json create mode 100644 homeassistant/components/emonitor/translations/nl.json create mode 100644 homeassistant/components/emonitor/translations/no.json create mode 100644 homeassistant/components/emonitor/translations/pl.json create mode 100644 homeassistant/components/enphase_envoy/translations/hu.json create mode 100644 homeassistant/components/enphase_envoy/translations/nl.json create mode 100644 homeassistant/components/enphase_envoy/translations/no.json create mode 100644 homeassistant/components/enphase_envoy/translations/pl.json create mode 100644 homeassistant/components/google_travel_time/translations/no.json create mode 100644 homeassistant/components/google_travel_time/translations/pl.json create mode 100644 homeassistant/components/met_eireann/translations/ca.json create mode 100644 homeassistant/components/met_eireann/translations/et.json create mode 100644 homeassistant/components/met_eireann/translations/hu.json create mode 100644 homeassistant/components/met_eireann/translations/nl.json create mode 100644 homeassistant/components/met_eireann/translations/no.json create mode 100644 homeassistant/components/met_eireann/translations/pl.json create mode 100644 homeassistant/components/met_eireann/translations/ru.json create mode 100644 homeassistant/components/met_eireann/translations/zh-Hant.json create mode 100644 homeassistant/components/waze_travel_time/translations/ca.json create mode 100644 homeassistant/components/waze_travel_time/translations/et.json create mode 100644 homeassistant/components/waze_travel_time/translations/fa.json create mode 100644 homeassistant/components/waze_travel_time/translations/hu.json create mode 100644 homeassistant/components/waze_travel_time/translations/nl.json create mode 100644 homeassistant/components/waze_travel_time/translations/no.json create mode 100644 homeassistant/components/waze_travel_time/translations/pl.json create mode 100644 homeassistant/components/waze_travel_time/translations/ru.json create mode 100644 homeassistant/components/waze_travel_time/translations/zh-Hant.json diff --git a/homeassistant/components/adguard/translations/no.json b/homeassistant/components/adguard/translations/no.json index 11c35c5895ef2..a35bfb181d624 100644 --- a/homeassistant/components/adguard/translations/no.json +++ b/homeassistant/components/adguard/translations/no.json @@ -9,8 +9,8 @@ }, "step": { "hassio_confirm": { - "description": "Vil du konfigurere Home Assistant for \u00e5 koble til AdGuard Home levert av Hass.io-tillegget: {addon} ?", - "title": "AdGuard Home via Hass.io-tillegg" + "description": "Vil du konfigurere Home Assistant til \u00e5 koble til AdGuard Home levert av tillegget: {addon} ?", + "title": "AdGuard Home via Home Assistant-tillegg" }, "user": { "data": { diff --git a/homeassistant/components/adguard/translations/pl.json b/homeassistant/components/adguard/translations/pl.json index f5c433a0bf49a..50f442d793718 100644 --- a/homeassistant/components/adguard/translations/pl.json +++ b/homeassistant/components/adguard/translations/pl.json @@ -9,8 +9,8 @@ }, "step": { "hassio_confirm": { - "description": "Czy chcesz skonfigurowa\u0107 Home Assistanta, aby po\u0142\u0105czy\u0142 si\u0119 z AdGuard Home przez dodatek Hass.io: {addon}?", - "title": "AdGuard Home przez dodatek Hass.io" + "description": "Czy chcesz skonfigurowa\u0107 Home Assistanta, aby po\u0142\u0105czy\u0142 si\u0119 z AdGuard Home przez dodatek {addon}?", + "title": "AdGuard Home przez dodatek Home Assistant" }, "user": { "data": { diff --git a/homeassistant/components/almond/translations/no.json b/homeassistant/components/almond/translations/no.json index 84a57a42ff71a..098184ff7af40 100644 --- a/homeassistant/components/almond/translations/no.json +++ b/homeassistant/components/almond/translations/no.json @@ -8,8 +8,8 @@ }, "step": { "hassio_confirm": { - "description": "Vil du konfigurere Home Assistant til \u00e5 koble til Almond levert av Supervisor-tillegg: {addon}?", - "title": "Almond via Hass.io-tillegg" + "description": "Vil du konfigurere Home Assistant for \u00e5 koble til Almond levert av tillegget: {addon} ?", + "title": "Almond via Home Assistant-tillegg" }, "pick_implementation": { "title": "Velg godkjenningsmetode" diff --git a/homeassistant/components/almond/translations/pl.json b/homeassistant/components/almond/translations/pl.json index 110ab5a6a397f..88fd6cda01c8d 100644 --- a/homeassistant/components/almond/translations/pl.json +++ b/homeassistant/components/almond/translations/pl.json @@ -8,8 +8,8 @@ }, "step": { "hassio_confirm": { - "description": "Czy chcesz skonfigurowa\u0107 Home Assistant, aby \u0142\u0105czy\u0142 si\u0119 z Almond dostarczonym przez dodatek Hass.io: {addon}?", - "title": "Almond poprzez dodatek Hass.io" + "description": "Czy chcesz skonfigurowa\u0107 Home Assistant, aby \u0142\u0105czy\u0142 si\u0119 z Almond dostarczonym przez dodatek {addon}?", + "title": "Almond poprzez dodatek Home Assistant" }, "pick_implementation": { "title": "Wybierz metod\u0119 uwierzytelniania" diff --git a/homeassistant/components/climacell/translations/hu.json b/homeassistant/components/climacell/translations/hu.json index fa0aa2ec0c730..6d97a51b530d6 100644 --- a/homeassistant/components/climacell/translations/hu.json +++ b/homeassistant/components/climacell/translations/hu.json @@ -9,6 +9,7 @@ "user": { "data": { "api_key": "API kulcs", + "api_version": "API Verzi\u00f3", "latitude": "Sz\u00e9less\u00e9g", "longitude": "Hossz\u00fas\u00e1g", "name": "N\u00e9v" diff --git a/homeassistant/components/climacell/translations/nl.json b/homeassistant/components/climacell/translations/nl.json index f267be3447887..925300c089dfc 100644 --- a/homeassistant/components/climacell/translations/nl.json +++ b/homeassistant/components/climacell/translations/nl.json @@ -10,6 +10,7 @@ "user": { "data": { "api_key": "API-sleutel", + "api_version": "API-versie", "latitude": "Breedtegraad", "longitude": "Lengtegraad", "name": "Naam" diff --git a/homeassistant/components/climacell/translations/no.json b/homeassistant/components/climacell/translations/no.json index d59f55905182f..2aad790060717 100644 --- a/homeassistant/components/climacell/translations/no.json +++ b/homeassistant/components/climacell/translations/no.json @@ -10,6 +10,7 @@ "user": { "data": { "api_key": "API-n\u00f8kkel", + "api_version": "API-versjon", "latitude": "Breddegrad", "longitude": "Lengdegrad", "name": "Navn" diff --git a/homeassistant/components/climacell/translations/pl.json b/homeassistant/components/climacell/translations/pl.json index 6fc13aadc96c4..6c8bad0f57a40 100644 --- a/homeassistant/components/climacell/translations/pl.json +++ b/homeassistant/components/climacell/translations/pl.json @@ -10,6 +10,7 @@ "user": { "data": { "api_key": "Klucz API", + "api_version": "Wersja API", "latitude": "Szeroko\u015b\u0107 geograficzna", "longitude": "D\u0142ugo\u015b\u0107 geograficzna", "name": "Nazwa" diff --git a/homeassistant/components/climacell/translations/zh-Hant.json b/homeassistant/components/climacell/translations/zh-Hant.json index 76eaf50b93253..710759b954cbc 100644 --- a/homeassistant/components/climacell/translations/zh-Hant.json +++ b/homeassistant/components/climacell/translations/zh-Hant.json @@ -10,6 +10,7 @@ "user": { "data": { "api_key": "API \u5bc6\u9470", + "api_version": "API \u7248\u672c", "latitude": "\u7def\u5ea6", "longitude": "\u7d93\u5ea6", "name": "\u540d\u7a31" diff --git a/homeassistant/components/deconz/translations/hu.json b/homeassistant/components/deconz/translations/hu.json index 61322087cbf67..0463463c0b3f3 100644 --- a/homeassistant/components/deconz/translations/hu.json +++ b/homeassistant/components/deconz/translations/hu.json @@ -35,6 +35,10 @@ "button_2": "M\u00e1sodik gomb", "button_3": "Harmadik gomb", "button_4": "Negyedik gomb", + "button_5": "\u00d6t\u00f6dik gomb", + "button_6": "Hatodik gomb", + "button_7": "Hetedik gomb", + "button_8": "Nyolcadik gomb", "close": "Bez\u00e1r\u00e1s", "dim_down": "S\u00f6t\u00e9t\u00edt", "dim_up": "Vil\u00e1gos\u00edt", diff --git a/homeassistant/components/deconz/translations/no.json b/homeassistant/components/deconz/translations/no.json index 4dcd693b5f46c..f27e7235f4068 100644 --- a/homeassistant/components/deconz/translations/no.json +++ b/homeassistant/components/deconz/translations/no.json @@ -14,8 +14,8 @@ "flow_title": "", "step": { "hassio_confirm": { - "description": "Vil du konfigurere Home Assistant til \u00e5 koble til deCONZ gateway levert av Hass.io-tillegget {addon} ?", - "title": "deCONZ Zigbee gateway via Hass.io-tillegg" + "description": "Vil du konfigurere Home Assistant til \u00e5 koble til deCONZ gateway levert av tillegget {addon} ?", + "title": "deCONZ Zigbee-gateway via Home Assistant-tillegget" }, "link": { "description": "L\u00e5s opp deCONZ-gatewayen din for \u00e5 registrere deg med Home Assistant. \n\n 1. G\u00e5 til deCONZ-systeminnstillinger -> Gateway -> Avansert \n 2. Trykk p\u00e5 \"Autentiser app\" knappen", @@ -42,6 +42,10 @@ "button_2": "Andre knapp", "button_3": "Tredje knapp", "button_4": "Fjerde knapp", + "button_5": "Femte knapp", + "button_6": "Sjette knapp", + "button_7": "Syvende knapp", + "button_8": "\u00c5ttende knapp", "close": "Lukk", "dim_down": "Dimm ned", "dim_up": "Dimm opp", diff --git a/homeassistant/components/deconz/translations/pl.json b/homeassistant/components/deconz/translations/pl.json index 1b4eba97096ae..d2352bdb9731b 100644 --- a/homeassistant/components/deconz/translations/pl.json +++ b/homeassistant/components/deconz/translations/pl.json @@ -14,8 +14,8 @@ "flow_title": "Bramka deCONZ Zigbee ({host})", "step": { "hassio_confirm": { - "description": "Czy chcesz skonfigurowa\u0107 Home Assistanta, aby po\u0142\u0105czy\u0142 si\u0119 z bramk\u0105 deCONZ dostarczon\u0105 przez dodatek Hass.io {addon}?", - "title": "Bramka deCONZ Zigbee przez dodatek Hass.io" + "description": "Czy chcesz skonfigurowa\u0107 Home Assistanta, aby po\u0142\u0105czy\u0142 si\u0119 z bramk\u0105 deCONZ dostarczon\u0105 przez dodatek {addon}?", + "title": "Bramka deCONZ Zigbee przez dodatek Home Assistant" }, "link": { "description": "Odblokuj bramk\u0119 deCONZ, aby zarejestrowa\u0107 j\u0105 w Home Assistantem. \n\n 1. Przejd\u017a do ustawienia deCONZ > bramka > Zaawansowane\n 2. Naci\u015bnij przycisk \"Uwierzytelnij aplikacj\u0119\"", @@ -42,6 +42,10 @@ "button_2": "drugi", "button_3": "trzeci", "button_4": "czwarty", + "button_5": "pi\u0105ty", + "button_6": "sz\u00f3sty", + "button_7": "si\u00f3dmy", + "button_8": "\u00f3smy", "close": "zamknij", "dim_down": "zmniejszenie jasno\u015bci", "dim_up": "zwi\u0119kszenie jasno\u015bci", diff --git a/homeassistant/components/emonitor/translations/hu.json b/homeassistant/components/emonitor/translations/hu.json new file mode 100644 index 0000000000000..2d7d4218e7de8 --- /dev/null +++ b/homeassistant/components/emonitor/translations/hu.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "flow_title": "SiteSage {name}", + "step": { + "confirm": { + "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a {name}({host})-t?", + "title": "A SiteSage Emonitor be\u00e1ll\u00edt\u00e1sa" + }, + "user": { + "data": { + "host": "Hoszt" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/emonitor/translations/nl.json b/homeassistant/components/emonitor/translations/nl.json new file mode 100644 index 0000000000000..742656c8e9259 --- /dev/null +++ b/homeassistant/components/emonitor/translations/nl.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "unknown": "Onverwachte fout" + }, + "flow_title": "SiteSage {name}", + "step": { + "confirm": { + "description": "Wilt u {name} ( {host} ) instellen?", + "title": "SiteSage Emonitor instellen" + }, + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/emonitor/translations/no.json b/homeassistant/components/emonitor/translations/no.json new file mode 100644 index 0000000000000..866602d854b30 --- /dev/null +++ b/homeassistant/components/emonitor/translations/no.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "unknown": "Uventet feil" + }, + "flow_title": "SiteSage {name}", + "step": { + "confirm": { + "description": "Vil du konfigurere {name} ({host})?", + "title": "Konfigurer SiteSage Emonitor" + }, + "user": { + "data": { + "host": "Vert" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/emonitor/translations/pl.json b/homeassistant/components/emonitor/translations/pl.json new file mode 100644 index 0000000000000..a5b250c3f4d77 --- /dev/null +++ b/homeassistant/components/emonitor/translations/pl.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "flow_title": "SiteSage {name}", + "step": { + "confirm": { + "description": "Czy chcesz skonfigurowa\u0107 {name} ({host})?", + "title": "Konfiguracja SiteSage Emonitor" + }, + "user": { + "data": { + "host": "Nazwa hosta lub adres IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/hu.json b/homeassistant/components/enphase_envoy/translations/hu.json new file mode 100644 index 0000000000000..caef6a32c8646 --- /dev/null +++ b/homeassistant/components/enphase_envoy/translations/hu.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "flow_title": "Envoy {serial} ({host})", + "step": { + "user": { + "data": { + "host": "Hoszt", + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/nl.json b/homeassistant/components/enphase_envoy/translations/nl.json new file mode 100644 index 0000000000000..1679e5ce0f417 --- /dev/null +++ b/homeassistant/components/enphase_envoy/translations/nl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "flow_title": "Envoy {serial} ({host})", + "step": { + "user": { + "data": { + "host": "Host", + "password": "Wachtwoord", + "username": "Gebruikersnaam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/no.json b/homeassistant/components/enphase_envoy/translations/no.json new file mode 100644 index 0000000000000..b059bbf6be032 --- /dev/null +++ b/homeassistant/components/enphase_envoy/translations/no.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "flow_title": "Utsending {serial} ({host})", + "step": { + "user": { + "data": { + "host": "Vert", + "password": "Passord", + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/pl.json b/homeassistant/components/enphase_envoy/translations/pl.json new file mode 100644 index 0000000000000..de961875c56c6 --- /dev/null +++ b/homeassistant/components/enphase_envoy/translations/pl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "flow_title": "Envoy {serial} ({host})", + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_travel_time/translations/no.json b/homeassistant/components/google_travel_time/translations/no.json new file mode 100644 index 0000000000000..5dfe345af0183 --- /dev/null +++ b/homeassistant/components/google_travel_time/translations/no.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "Plasseringen er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes" + }, + "step": { + "user": { + "data": { + "api_key": "API-n\u00f8kkel", + "destination": "Destinasjon", + "origin": "Opprinnelse" + }, + "description": "N\u00e5r du spesifiserer opprinnelse og destinasjon, kan du oppgi en eller flere steder atskilt med r\u00f8rtegnet, i form av en adresse, breddegrad / lengdegradskoordinat eller en Google-sted-ID. N\u00e5r du spesifiserer stedet ved hjelp av en Google-sted-ID, m\u00e5 ID-en v\u00e6re foran \"place_id:`." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "avoid": "Unng\u00e5", + "language": "Spr\u00e5k", + "mode": "Reisemodus", + "time": "Tid", + "time_type": "Tidstype", + "transit_mode": "Transittmodus", + "transit_routing_preference": "Ruteinnstillinger for kollektivtransport", + "units": "Enheter" + }, + "description": "Du kan eventuelt angi enten avgangstid eller ankomsttid. Hvis du spesifiserer en avgangstid, kan du angi \"n\u00e5\", et Unix-tidsstempel eller en 24-timers tidsstreng som \"08: 00: 00\". Hvis du spesifiserer en ankomsttid, kan du bruke et Unix-tidsstempel eller en 24-timers tidsstreng som '08: 00: 00'" + } + } + }, + "title": "Google Maps reisetid" +} \ No newline at end of file diff --git a/homeassistant/components/google_travel_time/translations/pl.json b/homeassistant/components/google_travel_time/translations/pl.json new file mode 100644 index 0000000000000..c420e65912f0e --- /dev/null +++ b/homeassistant/components/google_travel_time/translations/pl.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "Lokalizacja jest ju\u017c skonfigurowana" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + }, + "step": { + "user": { + "data": { + "api_key": "Klucz API", + "destination": "Punkt docelowy", + "origin": "Punkt pocz\u0105tkowy" + }, + "description": "Okre\u015blaj\u0105c punkt pocz\u0105tkowy i docelowy, mo\u017cesz poda\u0107 jedn\u0105 lub wi\u0119cej lokalizacji oddzielonych pionow\u0105 kresk\u0105, w postaci adresu, wsp\u00f3\u0142rz\u0119dnych szeroko\u015bci / d\u0142ugo\u015bci geograficznej lub identyfikatora miejsca Google. Okre\u015blaj\u0105c lokalizacj\u0119 za pomoc\u0105 identyfikatora miejsca Google, identyfikator musi by\u0107 poprzedzony przedrostkiem \u201eplace_id:\u201d." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "avoid": "Unikaj", + "language": "J\u0119zyk", + "mode": "Tryb podr\u00f3\u017cy", + "time": "Czas", + "time_type": "Typ czasu", + "transit_mode": "Tryb tranzytu", + "transit_routing_preference": "Preferencje trasy tranzytowej", + "units": "Jednostki" + }, + "description": "Opcjonalnie mo\u017cesz okre\u015bli\u0107 godzin\u0119 wyjazdu lub przyjazdu. Je\u015bli okre\u015blasz czas wyjazdu, mo\u017cesz wprowadzi\u0107 \u201eteraz\u201d, uniksowy znacznik czasu lub ci\u0105g 24-godzinny, taki jak \u201e08:00:00\u201d. Je\u015bli okre\u015blasz czas przyjazdu, mo\u017cesz u\u017cy\u0107 uniksowego znacznika czasu lub ci\u0105gu 24-godzinnego, takiego jak \u201e08:00:00\u201d." + } + } + }, + "title": "Czas podr\u00f3\u017cy w Mapach Google" +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/no.json b/homeassistant/components/homekit/translations/no.json index 6e13907d05743..e18f9224c6816 100644 --- a/homeassistant/components/homekit/translations/no.json +++ b/homeassistant/components/homekit/translations/no.json @@ -55,7 +55,7 @@ "entities": "Entiteter", "mode": "Modus" }, - "description": "Velg enhetene som skal inkluderes. I tilbeh\u00f8rsmodus er bare en enkelt enhet inkludert. I bridge-inkluderingsmodus vil alle enheter i domenet bli inkludert, med mindre spesifikke enheter er valgt. I bridge-ekskluderingsmodus vil alle enheter i domenet bli inkludert, bortsett fra de ekskluderte enhetene. For best ytelse opprettes et eget HomeKit-tilbeh\u00f8r for hver tv-mediaspiller og kamera.", + "description": "Velg enhetene som skal inkluderes. I tilbeh\u00f8rsmodus er bare en enkelt enhet inkludert. I bridge-inkluderingsmodus vil alle enheter i domenet bli inkludert, med mindre spesifikke enheter er valgt. I bridge-ekskluderingsmodus vil alle enheter i domenet bli inkludert, bortsett fra de ekskluderte enhetene. For best ytelse opprettes et eget HomeKit-tilbeh\u00f8r for hver tv-mediaspiller, aktivitetsbasert fjernkontroll, l\u00e5s og kamera.", "title": "Velg enheter som skal inkluderes" }, "init": { diff --git a/homeassistant/components/homekit/translations/pl.json b/homeassistant/components/homekit/translations/pl.json index ef35ff667c410..bcd088762ca96 100644 --- a/homeassistant/components/homekit/translations/pl.json +++ b/homeassistant/components/homekit/translations/pl.json @@ -55,7 +55,7 @@ "entities": "Encje", "mode": "Tryb" }, - "description": "Wybierz encje, kt\u00f3re maj\u0105 by\u0107 uwzgl\u0119dnione. W trybie \"Akcesorium\" tylko jedna encja jest uwzgl\u0119dniona. W trybie \"Uwzgl\u0119dnij mostek\", wszystkie encje w danej domenie b\u0119d\u0105 uwzgl\u0119dnione, chyba \u017ce wybrane s\u0105 tylko konkretne encje. W trybie \"Wyklucz mostek\", wszystkie encje b\u0119d\u0105 uwzgl\u0119dnione, z wyj\u0105tkiem tych wybranych. Dla najlepszej wydajno\u015bci, zostanie utworzone oddzielne akcesorium HomeKit dla ka\u017cdego tv media playera oraz kamery.", + "description": "Wybierz encje, kt\u00f3re maj\u0105 by\u0107 uwzgl\u0119dnione. W trybie \"Akcesorium\" tylko jedna encja jest uwzgl\u0119dniona. W trybie \"Uwzgl\u0119dnij mostek\", wszystkie encje w danej domenie b\u0119d\u0105 uwzgl\u0119dnione, chyba \u017ce wybrane s\u0105 tylko konkretne encje. W trybie \"Wyklucz mostek\", wszystkie encje b\u0119d\u0105 uwzgl\u0119dnione, z wyj\u0105tkiem tych wybranych. Dla najlepszej wydajno\u015bci, zostanie utworzone oddzielne akcesorium HomeKit dla ka\u017cdego tv media playera, aktywno\u015bci pilota, zamka oraz kamery.", "title": "Wybierz encje, kt\u00f3re maj\u0105 by\u0107 uwzgl\u0119dnione" }, "init": { diff --git a/homeassistant/components/hyperion/translations/hu.json b/homeassistant/components/hyperion/translations/hu.json index cfe649d9d5ee1..5096423c14367 100644 --- a/homeassistant/components/hyperion/translations/hu.json +++ b/homeassistant/components/hyperion/translations/hu.json @@ -23,5 +23,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "effect_show_list": "Megjelen\u00edtend\u0151 Hyperion effektusok" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/met_eireann/translations/ca.json b/homeassistant/components/met_eireann/translations/ca.json new file mode 100644 index 0000000000000..6a694a73c6710 --- /dev/null +++ b/homeassistant/components/met_eireann/translations/ca.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "already_configured": "El servei ja est\u00e0 configurat" + }, + "step": { + "user": { + "data": { + "elevation": "Altitud", + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Nom" + }, + "description": "Introdueix la treva ubicaci\u00f3 per utilitzar les dades meteorol\u00f2giques de l'API p\u00fablica de previsi\u00f3 meteorol\u00f2gica de Met \u00c9ireann", + "title": "Ubicaci\u00f3" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/met_eireann/translations/et.json b/homeassistant/components/met_eireann/translations/et.json new file mode 100644 index 0000000000000..48646b03049d5 --- /dev/null +++ b/homeassistant/components/met_eireann/translations/et.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "already_configured": "Teenus on juba seadistatud" + }, + "step": { + "user": { + "data": { + "elevation": "K\u00f5rgus merepinnast", + "latitude": "Laiuskraad", + "longitude": "Pikkuskraad", + "name": "Nimi" + }, + "description": "Met \u00c9ireanni avaliku ilmaprognoosi API ilmaandmete kasutamiseks sisesta oma asukoht", + "title": "Asukoht" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/met_eireann/translations/hu.json b/homeassistant/components/met_eireann/translations/hu.json new file mode 100644 index 0000000000000..65108e183a94a --- /dev/null +++ b/homeassistant/components/met_eireann/translations/hu.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" + }, + "step": { + "user": { + "data": { + "elevation": "Magass\u00e1g", + "latitude": "Sz\u00e9less\u00e9g", + "longitude": "Hossz\u00fas\u00e1g", + "name": "N\u00e9v" + }, + "title": "Elhelyezked\u00e9s" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/met_eireann/translations/nl.json b/homeassistant/components/met_eireann/translations/nl.json new file mode 100644 index 0000000000000..b67c167ca8d39 --- /dev/null +++ b/homeassistant/components/met_eireann/translations/nl.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "already_configured": "Service is al geconfigureerd" + }, + "step": { + "user": { + "data": { + "elevation": "Hoogte", + "latitude": "Breedtegraad", + "longitude": "Lengtegraad", + "name": "Naam" + }, + "description": "Voer uw locatie in om weergegevens van de Met \u00c9ireann Public Weather Forecast API te gebruiken", + "title": "Locatie" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/met_eireann/translations/no.json b/homeassistant/components/met_eireann/translations/no.json new file mode 100644 index 0000000000000..307efb3a1b0b2 --- /dev/null +++ b/homeassistant/components/met_eireann/translations/no.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "already_configured": "Tjenesten er allerede konfigurert" + }, + "step": { + "user": { + "data": { + "elevation": "Elevasjon", + "latitude": "Breddegrad", + "longitude": "Lengdegrad", + "name": "Navn" + }, + "description": "Skriv inn posisjonen din for \u00e5 bruke v\u00e6rdata fra Met \u00c9ireann Public Weather Forecast API", + "title": "Plassering" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/met_eireann/translations/pl.json b/homeassistant/components/met_eireann/translations/pl.json new file mode 100644 index 0000000000000..888017b790bb6 --- /dev/null +++ b/homeassistant/components/met_eireann/translations/pl.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana" + }, + "step": { + "user": { + "data": { + "elevation": "Wysoko\u015b\u0107", + "latitude": "Szeroko\u015b\u0107 geograficzna", + "longitude": "D\u0142ugo\u015b\u0107 geograficzna", + "name": "Nazwa" + }, + "description": "Wprowad\u017a swoj\u0105 lokalizacj\u0119, aby korzysta\u0107 z danych pogodowych z API Met \u00c9ireann Public Weather Forecast", + "title": "Lokalizacja" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/met_eireann/translations/ru.json b/homeassistant/components/met_eireann/translations/ru.json new file mode 100644 index 0000000000000..de121b259668f --- /dev/null +++ b/homeassistant/components/met_eireann/translations/ru.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." + }, + "step": { + "user": { + "data": { + "elevation": "\u0412\u044b\u0441\u043e\u0442\u0430", + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435, \u0447\u0442\u043e\u0431\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0434\u0430\u043d\u043d\u044b\u0435 \u043e \u043f\u043e\u0433\u043e\u0434\u0435 \u0438\u0437 \u043f\u0443\u0431\u043b\u0438\u0447\u043d\u043e\u0433\u043e API Met \u00c9ireann.", + "title": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/met_eireann/translations/zh-Hant.json b/homeassistant/components/met_eireann/translations/zh-Hant.json new file mode 100644 index 0000000000000..5e7bc04e24c9e --- /dev/null +++ b/homeassistant/components/met_eireann/translations/zh-Hant.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "step": { + "user": { + "data": { + "elevation": "\u6d77\u62d4", + "latitude": "\u7def\u5ea6", + "longitude": "\u7d93\u5ea6", + "name": "\u540d\u7a31" + }, + "description": "\u8f38\u5165\u5ea7\u6a19\u4ee5\u4f7f\u7528 Met \u00c9ireann Public Weather Forecast API \u5929\u6c23\u8cc7\u6599", + "title": "\u5ea7\u6a19" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/no.json b/homeassistant/components/mqtt/translations/no.json index 586c62dac6a36..44792075813de 100644 --- a/homeassistant/components/mqtt/translations/no.json +++ b/homeassistant/components/mqtt/translations/no.json @@ -21,8 +21,8 @@ "data": { "discovery": "Aktiver oppdagelse" }, - "description": "Vil du konfigurere Home Assistant til \u00e5 koble til MQTT-megleren levert av Hass.io-tillegget {addon} ?", - "title": "MQTT Megler via Hass.io-tillegg" + "description": "Vil du konfigurere Home Assistant for \u00e5 koble til MQTT-megleren levert av tillegget {addon} ?", + "title": "MQTT Broker via Home Assistant-tillegg" } } }, diff --git a/homeassistant/components/mqtt/translations/pl.json b/homeassistant/components/mqtt/translations/pl.json index 287f0165d9655..17ea7407f3c2f 100644 --- a/homeassistant/components/mqtt/translations/pl.json +++ b/homeassistant/components/mqtt/translations/pl.json @@ -21,8 +21,8 @@ "data": { "discovery": "W\u0142\u0105cz wykrywanie" }, - "description": "Czy chcesz skonfigurowa\u0107 Home Assistanta, aby po\u0142\u0105czy\u0142 si\u0119 z po\u015brednikiem MQTT dostarczonym przez dodatek Hass.io {addon}?", - "title": "Po\u015brednik MQTT przez dodatek Hass.io" + "description": "Czy chcesz skonfigurowa\u0107 Home Assistanta, aby po\u0142\u0105czy\u0142 si\u0119 z po\u015brednikiem MQTT dostarczonym przez dodatek {addon}?", + "title": "Po\u015brednik MQTT przez dodatek Home Assistant" } } }, diff --git a/homeassistant/components/mysensors/translations/hu.json b/homeassistant/components/mysensors/translations/hu.json index 7d4df1f12da2c..fefe3fd4b6c29 100644 --- a/homeassistant/components/mysensors/translations/hu.json +++ b/homeassistant/components/mysensors/translations/hu.json @@ -5,6 +5,7 @@ "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "invalid_device": "\u00c9rv\u00e9nytelen eszk\u00f6z", + "not_a_number": "Adj meg egy sz\u00e1mot.", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "error": { diff --git a/homeassistant/components/nuki/translations/ca.json b/homeassistant/components/nuki/translations/ca.json index a08308e78977f..e7b149349db98 100644 --- a/homeassistant/components/nuki/translations/ca.json +++ b/homeassistant/components/nuki/translations/ca.json @@ -1,11 +1,21 @@ { "config": { + "abort": { + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" + }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "unknown": "Error inesperat" }, "step": { + "reauth_confirm": { + "data": { + "token": "Token d'acc\u00e9s" + }, + "description": "La integraci\u00f3 Nuki ha de tornar a autenticar-se amb la passarel\u00b7la d'enlla\u00e7.", + "title": "Reautenticaci\u00f3 de la integraci\u00f3" + }, "user": { "data": { "host": "Amfitri\u00f3", diff --git a/homeassistant/components/nuki/translations/en.json b/homeassistant/components/nuki/translations/en.json index 3d53b85920ca2..99c43859eb0ae 100644 --- a/homeassistant/components/nuki/translations/en.json +++ b/homeassistant/components/nuki/translations/en.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "reauth_successful": "Successfully reauthenticated." + "reauth_successful": "Re-authentication was successful" }, "error": { "cannot_connect": "Failed to connect", @@ -9,17 +9,17 @@ "unknown": "Unexpected error" }, "step": { - "user": { + "reauth_confirm": { "data": { - "host": "Host", - "port": "Port", "token": "Access Token" - } - }, - "reauth_confirm": { - "title": "Reauthenticate Integration", + }, "description": "The Nuki integration needs to re-authenticate with your bridge.", + "title": "Reauthenticate Integration" + }, + "user": { "data": { + "host": "Host", + "port": "Port", "token": "Access Token" } } diff --git a/homeassistant/components/nuki/translations/et.json b/homeassistant/components/nuki/translations/et.json index 750afff003c24..e587458bbf0a8 100644 --- a/homeassistant/components/nuki/translations/et.json +++ b/homeassistant/components/nuki/translations/et.json @@ -1,11 +1,21 @@ { "config": { + "abort": { + "reauth_successful": "Taastuvastamine \u00f5nnestus" + }, "error": { "cannot_connect": "\u00dchendamine nurjus", "invalid_auth": "Vigane autentimine", "unknown": "Ootamatu t\u00f5rge" }, "step": { + "reauth_confirm": { + "data": { + "token": "Juurdep\u00e4\u00e4sut\u00f5end" + }, + "description": "Nuki sidumise peab sillaga uuesti autentima.", + "title": "Taastuvasta sidumine" + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/nuki/translations/pl.json b/homeassistant/components/nuki/translations/pl.json index 77a7c31ee34e0..c51a431cfe7e7 100644 --- a/homeassistant/components/nuki/translations/pl.json +++ b/homeassistant/components/nuki/translations/pl.json @@ -1,11 +1,21 @@ { "config": { + "abort": { + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie", "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { + "reauth_confirm": { + "data": { + "token": "Token dost\u0119pu" + }, + "description": "Integracja Nuki wymaga ponownego uwierzytelnienia z Twoim mostkiem.", + "title": "Ponownie uwierzytelnij integracj\u0119" + }, "user": { "data": { "host": "Nazwa hosta lub adres IP", diff --git a/homeassistant/components/roomba/translations/pl.json b/homeassistant/components/roomba/translations/pl.json index e4951a366ddd2..863023f321b66 100644 --- a/homeassistant/components/roomba/translations/pl.json +++ b/homeassistant/components/roomba/translations/pl.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", - "not_irobot_device": "Wykryte urz\u0105dzenie nie jest urz\u0105dzeniem iRobot" + "not_irobot_device": "Wykryte urz\u0105dzenie nie jest urz\u0105dzeniem iRobot", + "short_blid": "BLID zosta\u0142 obci\u0119ty" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" @@ -18,7 +19,7 @@ "title": "Po\u0142\u0105cz si\u0119 automatycznie z urz\u0105dzeniem" }, "link": { - "description": "Naci\u015bnij i przytrzymaj przycisk Home na {name} a\u017c urz\u0105dzenie wygeneruje d\u017awi\u0119k (oko\u0142o dwie sekundy).", + "description": "Naci\u015bnij i przytrzymaj przycisk Home na {name} a\u017c urz\u0105dzenie wygeneruje d\u017awi\u0119k (oko\u0142o dwie sekundy), a nast\u0119pnie prze\u015blij w ci\u0105gu 30 sekund.", "title": "Odzyskiwanie has\u0142a" }, "link_manual": { @@ -33,7 +34,7 @@ "blid": "BLID", "host": "Nazwa hosta lub adres IP" }, - "description": "W Twojej sieci nie wykryto urz\u0105dzenia Roomba ani Braava. BLID to cz\u0119\u015b\u0107 nazwy hosta urz\u0105dzenia po `iRobot-`. Post\u0119puj zgodnie z instrukcjami podanymi w dokumentacji pod adresem: {auth_help_url}", + "description": "W Twojej sieci nie wykryto urz\u0105dzenia Roomba ani Braava. BLID to cz\u0119\u015b\u0107 nazwy hosta urz\u0105dzenia po `iRobot-` lub 'Roomba-'. Post\u0119puj zgodnie z instrukcjami podanymi w dokumentacji pod adresem: {auth_help_url}", "title": "R\u0119czne po\u0142\u0105czenie z urz\u0105dzeniem" }, "user": { diff --git a/homeassistant/components/waze_travel_time/translations/ca.json b/homeassistant/components/waze_travel_time/translations/ca.json new file mode 100644 index 0000000000000..f8f1db711c0ad --- /dev/null +++ b/homeassistant/components/waze_travel_time/translations/ca.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "La ubicaci\u00f3 ja est\u00e0 configurada" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "step": { + "user": { + "data": { + "destination": "Destinaci\u00f3", + "origin": "Origen", + "region": "Regi\u00f3" + }, + "description": "A origen i destinaci\u00f3, introdueix l'adre\u00e7a o les coordenades GPS de la ubicaci\u00f3 (les coordenades GPS han d'estar separades per una coma). Tamb\u00e9 pots introduir un ID d'entitat (que contingui aquesta informaci\u00f3 en el seu estat), un ID d'entitat amb atributs de latitud i longitud o un sobrenom de zona." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "avoid_ferries": "Evita ferris?", + "avoid_subscription_roads": "Evita carreteres que necessiten tiquet/subscripci\u00f3?", + "avoid_toll_roads": "Evita peatges?", + "excl_filter": "Subcadena NO present a la descripci\u00f3 de la ruta seleccionada", + "incl_filter": "Subcadena a la descripci\u00f3 de la ruta seleccionada", + "realtime": "Temps de viatge en temps real?", + "units": "Unitats", + "vehicle_type": "Tipus de vehicle" + }, + "description": "Les entrades de 'subcadena' et permeten for\u00e7ar que la integraci\u00f3 utilitzi o eviti una ruta espec\u00edfica durant el c\u00e0lcul del temps de viatge." + } + } + }, + "title": "Temps de viatge de Waze" +} \ No newline at end of file diff --git a/homeassistant/components/waze_travel_time/translations/et.json b/homeassistant/components/waze_travel_time/translations/et.json new file mode 100644 index 0000000000000..a2ff51e1bb942 --- /dev/null +++ b/homeassistant/components/waze_travel_time/translations/et.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "See asukoht on juba seadistatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus" + }, + "step": { + "user": { + "data": { + "destination": "Sihtkoht", + "origin": "L\u00e4htekoht", + "region": "Piirkond" + }, + "description": "L\u00e4htekoha ja sihtkoha jaoks sisesta asukoha aadress v\u00f5i GPS-koordinaadid (GPS-koordinaadid tuleb eraldada komaga). Samuti saaf sisestada \u00fcksuse ID, mis annab selle teabe olekus, \u00fcksuse ID laius- ja pikkuskraadi atribuutidega v\u00f5i tsoonis\u00f5braliku nime." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "avoid_ferries": "V\u00e4ltida parvlaevu?", + "avoid_subscription_roads": "V\u00e4ltida teid mis vajavad vinjetti / ettemaksu?", + "avoid_toll_roads": "V\u00e4ltida tasulisi teid?", + "excl_filter": "Substring EI ole valitud teekonna kirjelduses", + "incl_filter": "Alamstring valitud teekonna kirjelduses", + "realtime": "Travel Time reaalajas?", + "units": "\u00dchikud", + "vehicle_type": "S\u00f5iduki t\u00fc\u00fcp" + }, + "description": "\"Alamstringi\" sisendid v\u00f5imaldavad sundida sidumist kasutama kindlat teekondai v\u00f5i v\u00e4ltima kindlat teekonda aja arvutamisel." + } + } + }, + "title": "" +} \ No newline at end of file diff --git a/homeassistant/components/waze_travel_time/translations/fa.json b/homeassistant/components/waze_travel_time/translations/fa.json new file mode 100644 index 0000000000000..b6abec0d8578b --- /dev/null +++ b/homeassistant/components/waze_travel_time/translations/fa.json @@ -0,0 +1,3 @@ +{ + "title": "\u0632\u0645\u0627\u0646 \u0633\u0641\u0631 \u0628\u0627 Waze" +} \ No newline at end of file diff --git a/homeassistant/components/waze_travel_time/translations/hu.json b/homeassistant/components/waze_travel_time/translations/hu.json new file mode 100644 index 0000000000000..94e5f96814e8d --- /dev/null +++ b/homeassistant/components/waze_travel_time/translations/hu.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "A hely m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "step": { + "user": { + "data": { + "destination": "\u00c9rkez\u00e9s helye", + "origin": "Indul\u00e1s helye", + "region": "R\u00e9gi\u00f3" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "avoid_ferries": "Ker\u00fclje kompokat?", + "avoid_subscription_roads": "Ker\u00fclje el az utakat, amelyekre matrica / el\u0151fizet\u00e9s sz\u00fcks\u00e9ges?", + "avoid_toll_roads": "Ker\u00fclje a fizet\u0151s utakat?", + "realtime": "Val\u00f3s idej\u0171 utaz\u00e1si id\u0151?", + "vehicle_type": "J\u00e1rm\u0171 t\u00edpus" + } + } + } + }, + "title": "Waze Travel Time" +} \ No newline at end of file diff --git a/homeassistant/components/waze_travel_time/translations/nl.json b/homeassistant/components/waze_travel_time/translations/nl.json new file mode 100644 index 0000000000000..ecf7db3a13ee2 --- /dev/null +++ b/homeassistant/components/waze_travel_time/translations/nl.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "Locatie is al geconfigureerd." + }, + "error": { + "cannot_connect": "Kan geen verbinding maken" + }, + "step": { + "user": { + "data": { + "destination": "Bestemming" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "avoid_toll_roads": "Tolwegen vermijden?", + "excl_filter": "Substring NIET in beschrijving van geselecteerde route", + "incl_filter": "Substring in beschrijving van geselecteerde route", + "realtime": "Realtime reistijd?", + "units": "Eenheden", + "vehicle_type": "Voertuigtype" + }, + "description": "Met de 'substring'-invoer kunt u de integratie forceren om een bepaalde route te gebruiken of een bepaalde route te vermijden in de tijdreisberekening." + } + } + }, + "title": "Waze reistijd" +} \ No newline at end of file diff --git a/homeassistant/components/waze_travel_time/translations/no.json b/homeassistant/components/waze_travel_time/translations/no.json new file mode 100644 index 0000000000000..7ae2bf8d41812 --- /dev/null +++ b/homeassistant/components/waze_travel_time/translations/no.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "Plasseringen er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes" + }, + "step": { + "user": { + "data": { + "destination": "Destinasjon", + "origin": "Opprinnelse", + "region": "Region" + }, + "description": "For opprinnelse og destinasjon, skriv inn adressen eller GPS-koordinatene til stedet (GPS-koordinatene m\u00e5 v\u00e6re atskilt med komma). Du kan ogs\u00e5 angi en enhets-ID som gir denne informasjonen i sin tilstand, en enhets-id med breddegrad og lengdegrad eller attributt navn." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "avoid_ferries": "Unng\u00e5 ferger?", + "avoid_subscription_roads": "Unng\u00e5 veier trenger en vignett / abonnement?", + "avoid_toll_roads": "Unng\u00e5 bomveier?", + "excl_filter": "Delstreng IKKE i beskrivelse av valgt rute", + "incl_filter": "Delstreng i Beskrivelse av valgt rute", + "realtime": "Reisetid i sanntid?", + "units": "Enheter", + "vehicle_type": "Kj\u00f8ret\u00f8y Type" + }, + "description": "`Substring`-inngangene lar deg tvinge integrasjonen til \u00e5 bruke en bestemt rute eller unng\u00e5 en bestemt rute i beregningen av tidsreiser." + } + } + }, + "title": "Waze reisetid" +} \ No newline at end of file diff --git a/homeassistant/components/waze_travel_time/translations/pl.json b/homeassistant/components/waze_travel_time/translations/pl.json new file mode 100644 index 0000000000000..e46469f59d7b6 --- /dev/null +++ b/homeassistant/components/waze_travel_time/translations/pl.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "Lokalizacja jest ju\u017c skonfigurowana" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + }, + "step": { + "user": { + "data": { + "destination": "Punkt docelowy", + "origin": "Punkt pocz\u0105tkowy", + "region": "Region" + }, + "description": "W polu Punkt pocz\u0105tkowy i Punkt docelowy, wprowad\u017a adres lub wsp\u00f3\u0142rz\u0119dne GPS (wsp\u00f3\u0142rz\u0119dne GPS musz\u0105 by\u0107 oddzielone przecinkiem). Mo\u017cesz r\u00f3wnie\u017c wprowadzi\u0107 identyfikator encji (entity_id), kt\u00f3ry dostarcza te informacje w swoim stanie, identyfikator jednostki z atrybutami szeroko\u015bci i d\u0142ugo\u015bci geograficznej lub przyjazn\u0105 nazw\u0119 strefy." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "avoid_ferries": "Unikaj prom\u00f3w?", + "avoid_subscription_roads": "Unikaj dr\u00f3g wymagaj\u0105cych winiety / abonamentu?", + "avoid_toll_roads": "Unikaj dr\u00f3g p\u0142atnych?", + "excl_filter": "NIE MA podci\u0105gu w opisie wybranej trasy", + "incl_filter": "Podci\u0105g w opisie wybranej trasy", + "realtime": "Czas podr\u00f3\u017cy w czasie rzeczywistym?", + "units": "Jednostki", + "vehicle_type": "Typ pojazdu" + }, + "description": "Dane wej\u015bciowe \u201epodci\u0105gu\u201d pozwol\u0105 Ci wymusi\u0107 na integracji u\u017cycie okre\u015blonej trasy lub omini\u0119cie okre\u015blonej trasy w obliczeniach dotycz\u0105cych czasu podr\u00f3\u017cy." + } + } + }, + "title": "Czas podr\u00f3\u017cy Waze" +} \ No newline at end of file diff --git a/homeassistant/components/waze_travel_time/translations/ru.json b/homeassistant/components/waze_travel_time/translations/ru.json new file mode 100644 index 0000000000000..5d0c0990c28b2 --- /dev/null +++ b/homeassistant/components/waze_travel_time/translations/ru.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." + }, + "step": { + "user": { + "data": { + "destination": "\u041f\u0443\u043d\u043a\u0442 \u043d\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f", + "origin": "\u041f\u0443\u043d\u043a\u0442 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f", + "region": "\u0420\u0435\u0433\u0438\u043e\u043d" + }, + "description": "\u041f\u0443\u043d\u043a\u0442 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u0438 \u043f\u0443\u043d\u043a\u0442 \u043d\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u043c\u043e\u0436\u043d\u043e \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0432 \u0432\u0438\u0434\u0435 \u0430\u0434\u0440\u0435\u0441\u0430 \u0438\u043b\u0438 GPS-\u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442 (\u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u0431\u044b\u0442\u044c \u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d\u044b \u0437\u0430\u043f\u044f\u0442\u043e\u0439). \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c ID \u043e\u0431\u044a\u0435\u043a\u0442\u0430, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u044d\u0442\u0443 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u0432 \u0441\u0432\u043e\u0435\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438 \u0438\u043b\u0438 \u0432 \u0430\u0442\u0440\u0438\u0431\u0443\u0442\u0430\u0445, \u0430 \u0442\u0430\u043a\u0436\u0435 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u044f \u0437\u043e\u043d \u0438\u0437 Home Assistant." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "avoid_ferries": "\u0418\u0437\u0431\u0435\u0433\u0430\u0442\u044c \u043f\u0430\u0440\u043e\u043c\u043e\u0432?", + "avoid_subscription_roads": "\u0418\u0437\u0431\u0435\u0433\u0430\u0439\u0442\u0435 \u0434\u043e\u0440\u043e\u0433, \u0442\u0440\u0435\u0431\u0443\u044e\u0449\u0438\u0445 \u0432\u0438\u043d\u044c\u0435\u0442\u043a\u0438/\u043f\u043e\u0434\u043f\u0438\u0441\u043a\u0438?", + "avoid_toll_roads": "\u0418\u0437\u0431\u0435\u0433\u0430\u0442\u044c \u043f\u043b\u0430\u0442\u043d\u044b\u0445 \u0434\u043e\u0440\u043e\u0433?", + "excl_filter": "\u041f\u043e\u0434\u0441\u0442\u0440\u043e\u043a\u0430 \u041d\u0415 \u0432 \u043e\u043f\u0438\u0441\u0430\u043d\u0438\u0438 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0433\u043e \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0430", + "incl_filter": "\u041f\u043e\u0434\u0441\u0442\u0440\u043e\u043a\u0430 \u0432 \u043e\u043f\u0438\u0441\u0430\u043d\u0438\u0438 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0433\u043e \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0430", + "realtime": "\u0412\u0440\u0435\u043c\u044f \u0432 \u043f\u0443\u0442\u0438 \u0432 \u0440\u0435\u0436\u0438\u043c\u0435 \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u0432\u0440\u0435\u043c\u0435\u043d\u0438?", + "units": "\u0415\u0434\u0438\u043d\u0438\u0446\u044b \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f", + "vehicle_type": "\u0422\u0438\u043f \u0442\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u043d\u043e\u0433\u043e \u0441\u0440\u0435\u0434\u0441\u0442\u0432\u0430" + }, + "description": "\u0412\u0445\u043e\u0434\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 `\u043f\u043e\u0434\u0441\u0442\u0440\u043e\u043a\u0430` \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u044e\u0442 \u043f\u0440\u0438\u043d\u0443\u0434\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u0439 \u043c\u0430\u0440\u0448\u0440\u0443\u0442 \u0438\u043b\u0438 \u0438\u0437\u0431\u0435\u0433\u0430\u0442\u044c \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u043e\u0433\u043e \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0430 \u043f\u0440\u0438 \u0440\u0430\u0441\u0447\u0435\u0442\u0435 \u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u0432 \u043f\u0443\u0442\u0438." + } + } + }, + "title": "Waze Travel Time" +} \ No newline at end of file diff --git a/homeassistant/components/waze_travel_time/translations/zh-Hant.json b/homeassistant/components/waze_travel_time/translations/zh-Hant.json new file mode 100644 index 0000000000000..a0f71b51ae2da --- /dev/null +++ b/homeassistant/components/waze_travel_time/translations/zh-Hant.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "\u5ea7\u6a19\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "step": { + "user": { + "data": { + "destination": "\u76ee\u7684\u5730", + "origin": "\u51fa\u767c\u5730", + "region": "\u5340\u57df" + }, + "description": "\u65bc\u51fa\u767c\u5730\u8207\u76ee\u7684\u5730\u3001\u8f38\u5165\u5730\u5740\u6216 GPS \u5ea7\u6a19\uff08GPS \u5ea7\u6a19\u4ee5\u9017\u865f\u5206\u9694\uff09\u3002\u540c\u6642\u4e5f\u53ef\u4ee5\u8f38\u5165\u5305\u542b\u72c0\u614b\u7684\u5be6\u9ad4 ID\u3001\u5305\u542b\u7d93\u7def\u5ea6\u5c6c\u6027\u7684\u5be6\u9ad4\u3001\u6216\u5340\u57df\u7684\u540d\u7a31\u3002" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "avoid_ferries": "\u907f\u958b\u6e21\u8f2a\uff1f", + "avoid_subscription_roads": "\u907f\u958b\u9700\u8981\u5716\u5b9a\u6a19\u793a / \u8a02\u95b1\u8def\u7dda\uff1f", + "avoid_toll_roads": "\u907f\u958b\u6536\u8cbb\u9053\u8def\uff1f", + "excl_filter": "\u6240\u9078\u64c7\u8def\u7dda\u63cf\u8ff0\u4e0d\u5305\u542b Substring", + "incl_filter": "\u6240\u9078\u64c7\u8def\u7dda\u63cf\u8ff0\u5305\u542b Substring", + "realtime": "\u5373\u6642\u65c5\u7a0b\u6642\u9593\uff1f", + "units": "\u55ae\u4f4d", + "vehicle_type": "\u8eca\u8f1b\u985e\u578b" + }, + "description": "`substring` \u8f38\u5165\u53ef\u4f9b\u5f37\u5236\u6574\u5408\u3001\u65bc\u8a08\u7b97\u65c5\u7a0b\u6642\u9593\u6642\uff0c\u4f7f\u7528\u7279\u5b9a\u8def\u7dda\u6216\u907f\u958b\u4f7f\u7528\u7279\u5b9a\u8def\u7dda\u3002" + } + } + }, + "title": "Waze \u65c5\u7a0b\u6642\u9593" +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/pl.json b/homeassistant/components/zha/translations/pl.json index 4cdada49f5013..f9b34d1be8297 100644 --- a/homeassistant/components/zha/translations/pl.json +++ b/homeassistant/components/zha/translations/pl.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, + "flow_title": "ZHA: {name}", "step": { "pick_radio": { "data": { From 89f2f458d29b02d57a2d0ef52c96cf1875c59566 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Wed, 7 Apr 2021 02:34:49 +0200 Subject: [PATCH 0090/1317] Generate a seperate UUID for the analytics integration (#48742) --- .../components/analytics/__init__.py | 5 +- .../components/analytics/analytics.py | 18 +++-- homeassistant/components/analytics/const.py | 2 +- tests/components/analytics/test_analytics.py | 67 ++++++++++++------- tests/components/analytics/test_init.py | 10 +-- 5 files changed, 60 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/analytics/__init__.py b/homeassistant/components/analytics/__init__.py index 3a06c56add585..c1187af7f1730 100644 --- a/homeassistant/components/analytics/__init__.py +++ b/homeassistant/components/analytics/__init__.py @@ -7,7 +7,7 @@ from homeassistant.helpers.event import async_call_later, async_track_time_interval from .analytics import Analytics -from .const import ATTR_HUUID, ATTR_PREFERENCES, DOMAIN, INTERVAL, PREFERENCE_SCHEMA +from .const import ATTR_PREFERENCES, DOMAIN, INTERVAL, PREFERENCE_SCHEMA async def async_setup(hass: HomeAssistant, _): @@ -44,10 +44,9 @@ async def websocket_analytics( ) -> None: """Return analytics preferences.""" analytics: Analytics = hass.data[DOMAIN] - huuid = await hass.helpers.instance_id.async_get() connection.send_result( msg["id"], - {ATTR_PREFERENCES: analytics.preferences, ATTR_HUUID: huuid}, + {ATTR_PREFERENCES: analytics.preferences}, ) diff --git a/homeassistant/components/analytics/analytics.py b/homeassistant/components/analytics/analytics.py index ef7c2fbde6e63..d7764a052c853 100644 --- a/homeassistant/components/analytics/analytics.py +++ b/homeassistant/components/analytics/analytics.py @@ -1,5 +1,6 @@ """Analytics helper class for the analytics integration.""" import asyncio +import uuid import aiohttp import async_timeout @@ -24,7 +25,6 @@ ATTR_BASE, ATTR_DIAGNOSTICS, ATTR_HEALTHY, - ATTR_HUUID, ATTR_INTEGRATION_COUNT, ATTR_INTEGRATIONS, ATTR_ONBOARDED, @@ -37,6 +37,7 @@ ATTR_SUPPORTED, ATTR_USAGE, ATTR_USER_COUNT, + ATTR_UUID, ATTR_VERSION, LOGGER, PREFERENCE_SCHEMA, @@ -52,7 +53,7 @@ def __init__(self, hass: HomeAssistant) -> None: """Initialize the Analytics class.""" self.hass: HomeAssistant = hass self.session = async_get_clientsession(hass) - self._data = {ATTR_PREFERENCES: {}, ATTR_ONBOARDED: False} + self._data = {ATTR_PREFERENCES: {}, ATTR_ONBOARDED: False, ATTR_UUID: None} self._store: Store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) @property @@ -71,6 +72,11 @@ def onboarded(self) -> bool: """Return bool if the user has made a choice.""" return self._data[ATTR_ONBOARDED] + @property + def uuid(self) -> bool: + """Return the uuid for the analytics integration.""" + return self._data[ATTR_UUID] + @property def supervisor(self) -> bool: """Return bool if a supervisor is present.""" @@ -81,6 +87,7 @@ async def load(self) -> None: stored = await self._store.async_load() if stored: self._data = stored + if self.supervisor: supervisor_info = hassio.get_supervisor_info(self.hass) if not self.onboarded: @@ -99,6 +106,7 @@ async def save_preferences(self, preferences: dict) -> None: preferences = PREFERENCE_SCHEMA(preferences) self._data[ATTR_PREFERENCES].update(preferences) self._data[ATTR_ONBOARDED] = True + await self._store.async_save(self._data) if self.supervisor: @@ -114,7 +122,9 @@ async def send_analytics(self, _=None) -> None: LOGGER.debug("Nothing to submit") return - huuid = await self.hass.helpers.instance_id.async_get() + if self._data.get(ATTR_UUID) is None: + self._data[ATTR_UUID] = uuid.uuid4().hex + await self._store.async_save(self._data) if self.supervisor: supervisor_info = hassio.get_supervisor_info(self.hass) @@ -123,7 +133,7 @@ async def send_analytics(self, _=None) -> None: integrations = [] addons = [] payload: dict = { - ATTR_HUUID: huuid, + ATTR_UUID: self.uuid, ATTR_VERSION: HA_VERSION, ATTR_INSTALLATION_TYPE: system_info[ATTR_INSTALLATION_TYPE], } diff --git a/homeassistant/components/analytics/const.py b/homeassistant/components/analytics/const.py index ba56ba265a715..998dac9cf80c8 100644 --- a/homeassistant/components/analytics/const.py +++ b/homeassistant/components/analytics/const.py @@ -20,7 +20,6 @@ ATTR_BASE = "base" ATTR_DIAGNOSTICS = "diagnostics" ATTR_HEALTHY = "healthy" -ATTR_HUUID = "huuid" ATTR_INSTALLATION_TYPE = "installation_type" ATTR_INTEGRATION_COUNT = "integration_count" ATTR_INTEGRATIONS = "integrations" @@ -34,6 +33,7 @@ ATTR_SUPPORTED = "supported" ATTR_USAGE = "usage" ATTR_USER_COUNT = "user_count" +ATTR_UUID = "uuid" ATTR_VERSION = "version" diff --git a/tests/components/analytics/test_analytics.py b/tests/components/analytics/test_analytics.py index 1a636d165987d..f7f55c510c105 100644 --- a/tests/components/analytics/test_analytics.py +++ b/tests/components/analytics/test_analytics.py @@ -1,5 +1,5 @@ """The tests for the analytics .""" -from unittest.mock import AsyncMock, Mock, patch +from unittest.mock import AsyncMock, Mock, PropertyMock, patch import aiohttp import pytest @@ -13,10 +13,11 @@ ATTR_STATISTICS, ATTR_USAGE, ) +from homeassistant.components.api import ATTR_UUID from homeassistant.const import __version__ as HA_VERSION from homeassistant.loader import IntegrationNotFound -MOCK_HUUID = "abcdefg" +MOCK_UUID = "abcdefg" async def test_no_send(hass, caplog, aioclient_mock): @@ -26,8 +27,7 @@ async def test_no_send(hass, caplog, aioclient_mock): with patch( "homeassistant.components.hassio.is_hassio", side_effect=Mock(return_value=False), - ), patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID): - await analytics.load() + ): assert not analytics.preferences[ATTR_BASE] await analytics.send_analytics() @@ -76,9 +76,7 @@ async def test_failed_to_send(hass, caplog, aioclient_mock): analytics = Analytics(hass) await analytics.save_preferences({ATTR_BASE: True}) assert analytics.preferences[ATTR_BASE] - - with patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID): - await analytics.send_analytics() + await analytics.send_analytics() assert "Sending analytics failed with statuscode 400" in caplog.text @@ -88,9 +86,7 @@ async def test_failed_to_send_raises(hass, caplog, aioclient_mock): analytics = Analytics(hass) await analytics.save_preferences({ATTR_BASE: True}) assert analytics.preferences[ATTR_BASE] - - with patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID): - await analytics.send_analytics() + await analytics.send_analytics() assert "Error sending analytics" in caplog.text @@ -98,12 +94,15 @@ async def test_send_base(hass, caplog, aioclient_mock): """Test send base prefrences are defined.""" aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) analytics = Analytics(hass) + await analytics.save_preferences({ATTR_BASE: True}) assert analytics.preferences[ATTR_BASE] - with patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID): + with patch("uuid.UUID.hex", new_callable=PropertyMock) as hex: + hex.return_value = MOCK_UUID await analytics.send_analytics() - assert f"'huuid': '{MOCK_HUUID}'" in caplog.text + + assert f"'uuid': '{MOCK_UUID}'" in caplog.text assert f"'version': '{HA_VERSION}'" in caplog.text assert "'installation_type':" in caplog.text assert "'integration_count':" not in caplog.text @@ -131,10 +130,14 @@ async def test_send_base_with_supervisor(hass, caplog, aioclient_mock): "homeassistant.components.hassio.is_hassio", side_effect=Mock(return_value=True), ), patch( - "homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID - ): + "uuid.UUID.hex", new_callable=PropertyMock + ) as hex: + hex.return_value = MOCK_UUID + await analytics.load() + await analytics.send_analytics() - assert f"'huuid': '{MOCK_HUUID}'" in caplog.text + + assert f"'uuid': '{MOCK_UUID}'" in caplog.text assert f"'version': '{HA_VERSION}'" in caplog.text assert "'supervisor': {'healthy': True, 'supported': True}}" in caplog.text assert "'installation_type':" in caplog.text @@ -147,12 +150,13 @@ async def test_send_usage(hass, caplog, aioclient_mock): aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) analytics = Analytics(hass) await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True}) + assert analytics.preferences[ATTR_BASE] assert analytics.preferences[ATTR_USAGE] hass.config.components = ["default_config"] - with patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID): - await analytics.send_analytics() + await analytics.send_analytics() + assert "'integrations': ['default_config']" in caplog.text assert "'integration_count':" not in caplog.text @@ -195,8 +199,6 @@ async def test_send_usage_with_supervisor(hass, caplog, aioclient_mock): ), patch( "homeassistant.components.hassio.is_hassio", side_effect=Mock(return_value=True), - ), patch( - "homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID ): await analytics.send_analytics() assert ( @@ -215,8 +217,7 @@ async def test_send_statistics(hass, caplog, aioclient_mock): assert analytics.preferences[ATTR_STATISTICS] hass.config.components = ["default_config"] - with patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID): - await analytics.send_analytics() + await analytics.send_analytics() assert ( "'state_count': 0, 'automation_count': 0, 'integration_count': 1, 'user_count': 0" in caplog.text @@ -236,11 +237,11 @@ async def test_send_statistics_one_integration_fails(hass, caplog, aioclient_moc with patch( "homeassistant.components.analytics.analytics.async_get_integration", side_effect=IntegrationNotFound("any"), - ), patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID): + ): await analytics.send_analytics() post_call = aioclient_mock.mock_calls[0] - assert "huuid" in post_call[2] + assert "uuid" in post_call[2] assert post_call[2]["integration_count"] == 0 @@ -258,7 +259,7 @@ async def test_send_statistics_async_get_integration_unknown_exception( with pytest.raises(ValueError), patch( "homeassistant.components.analytics.analytics.async_get_integration", side_effect=ValueError, - ), patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID): + ): await analytics.send_analytics() @@ -298,9 +299,23 @@ async def test_send_statistics_with_supervisor(hass, caplog, aioclient_mock): ), patch( "homeassistant.components.hassio.is_hassio", side_effect=Mock(return_value=True), - ), patch( - "homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID ): await analytics.send_analytics() assert "'addon_count': 1" in caplog.text assert "'integrations':" not in caplog.text + + +async def test_reusing_uuid(hass, aioclient_mock): + """Test reusing the stored UUID.""" + aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) + analytics = Analytics(hass) + analytics._data[ATTR_UUID] = "NOT_MOCK_UUID" + + await analytics.save_preferences({ATTR_BASE: True}) + + with patch("uuid.UUID.hex", new_callable=PropertyMock) as hex: + # This is not actually called but that in itself prove the test + hex.return_value = MOCK_UUID + await analytics.send_analytics() + + assert analytics.uuid == "NOT_MOCK_UUID" diff --git a/tests/components/analytics/test_init.py b/tests/components/analytics/test_init.py index 4f8c95bc6b413..af10592692644 100644 --- a/tests/components/analytics/test_init.py +++ b/tests/components/analytics/test_init.py @@ -1,6 +1,4 @@ """The tests for the analytics .""" -from unittest.mock import patch - from homeassistant.components.analytics.const import ANALYTICS_ENDPOINT_URL, DOMAIN from homeassistant.setup import async_setup_component @@ -22,11 +20,9 @@ async def test_websocket(hass, hass_ws_client, aioclient_mock): ws_client = await hass_ws_client(hass) await ws_client.send_json({"id": 1, "type": "analytics"}) - with patch("homeassistant.helpers.instance_id.async_get", return_value="abcdef"): - response = await ws_client.receive_json() + response = await ws_client.receive_json() assert response["success"] - assert response["result"]["huuid"] == "abcdef" await ws_client.send_json( {"id": 2, "type": "analytics/preferences", "preferences": {"base": True}} @@ -36,7 +32,5 @@ async def test_websocket(hass, hass_ws_client, aioclient_mock): assert response["result"]["preferences"]["base"] await ws_client.send_json({"id": 3, "type": "analytics"}) - with patch("homeassistant.helpers.instance_id.async_get", return_value="abcdef"): - response = await ws_client.receive_json() + response = await ws_client.receive_json() assert response["result"]["preferences"]["base"] - assert response["result"]["huuid"] == "abcdef" From 191c01a6113aa2136100bb261ddde2ee9ed0d506 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Wed, 7 Apr 2021 04:33:08 +0200 Subject: [PATCH 0091/1317] Add custom integrations to analytics (#48753) --- homeassistant/components/analytics/analytics.py | 16 ++++++++++++++-- homeassistant/components/analytics/const.py | 1 + tests/components/analytics/test_analytics.py | 16 +++++++++++++++- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/analytics/analytics.py b/homeassistant/components/analytics/analytics.py index d7764a052c853..e6e8678cc1094 100644 --- a/homeassistant/components/analytics/analytics.py +++ b/homeassistant/components/analytics/analytics.py @@ -8,7 +8,7 @@ from homeassistant.components import hassio from homeassistant.components.api import ATTR_INSTALLATION_TYPE from homeassistant.components.automation.const import DOMAIN as AUTOMATION_DOMAIN -from homeassistant.const import __version__ as HA_VERSION +from homeassistant.const import ATTR_DOMAIN, __version__ as HA_VERSION from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.storage import Store @@ -23,6 +23,7 @@ ATTR_AUTO_UPDATE, ATTR_AUTOMATION_COUNT, ATTR_BASE, + ATTR_CUSTOM_INTEGRATIONS, ATTR_DIAGNOSTICS, ATTR_HEALTHY, ATTR_INTEGRATION_COUNT, @@ -131,6 +132,7 @@ async def send_analytics(self, _=None) -> None: system_info = await async_get_system_info(self.hass) integrations = [] + custom_integrations = [] addons = [] payload: dict = { ATTR_UUID: self.uuid, @@ -162,7 +164,16 @@ async def send_analytics(self, _=None) -> None: if isinstance(integration, BaseException): raise integration - if integration.disabled or not integration.is_built_in: + if integration.disabled: + continue + + if not integration.is_built_in: + custom_integrations.append( + { + ATTR_DOMAIN: integration.domain, + ATTR_VERSION: integration.version, + } + ) continue integrations.append(integration.domain) @@ -186,6 +197,7 @@ async def send_analytics(self, _=None) -> None: if self.preferences.get(ATTR_USAGE, False): payload[ATTR_INTEGRATIONS] = integrations + payload[ATTR_CUSTOM_INTEGRATIONS] = custom_integrations if supervisor_info is not None: payload[ATTR_ADDONS] = addons diff --git a/homeassistant/components/analytics/const.py b/homeassistant/components/analytics/const.py index 998dac9cf80c8..a6fe91b5a44b1 100644 --- a/homeassistant/components/analytics/const.py +++ b/homeassistant/components/analytics/const.py @@ -18,6 +18,7 @@ ATTR_AUTO_UPDATE = "auto_update" ATTR_AUTOMATION_COUNT = "automation_count" ATTR_BASE = "base" +ATTR_CUSTOM_INTEGRATIONS = "custom_integrations" ATTR_DIAGNOSTICS = "diagnostics" ATTR_HEALTHY = "healthy" ATTR_INSTALLATION_TYPE = "installation_type" diff --git a/tests/components/analytics/test_analytics.py b/tests/components/analytics/test_analytics.py index f7f55c510c105..e1716df9cdb9d 100644 --- a/tests/components/analytics/test_analytics.py +++ b/tests/components/analytics/test_analytics.py @@ -14,8 +14,9 @@ ATTR_USAGE, ) from homeassistant.components.api import ATTR_UUID -from homeassistant.const import __version__ as HA_VERSION +from homeassistant.const import ATTR_DOMAIN, __version__ as HA_VERSION from homeassistant.loader import IntegrationNotFound +from homeassistant.setup import async_setup_component MOCK_UUID = "abcdefg" @@ -319,3 +320,16 @@ async def test_reusing_uuid(hass, aioclient_mock): await analytics.send_analytics() assert analytics.uuid == "NOT_MOCK_UUID" + + +async def test_custom_integrations(hass, aioclient_mock): + """Test sending custom integrations.""" + aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) + analytics = Analytics(hass) + assert await async_setup_component(hass, "test_package", {"test_package": {}}) + await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True}) + + await analytics.send_analytics() + + payload = aioclient_mock.mock_calls[0][2] + assert payload["custom_integrations"][0][ATTR_DOMAIN] == "test_package" From 815db999dad5130f1f4e614ec5945634b1873ebb Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Wed, 7 Apr 2021 09:13:55 +0200 Subject: [PATCH 0092/1317] Use microsecond precision for datetime values on MariaDB/MySQL (#48749) Co-authored-by: Paulus Schoutsen --- homeassistant/components/recorder/migration.py | 14 ++++++++++++++ homeassistant/components/recorder/models.py | 16 ++++++++++------ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index e730b1af239f2..5ab2d9091727a 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -363,6 +363,20 @@ def _apply_update(engine, new_version, old_version): if engine.dialect.name == "mysql": _modify_columns(engine, "events", ["event_data LONGTEXT"]) _modify_columns(engine, "states", ["attributes LONGTEXT"]) + elif new_version == 13: + if engine.dialect.name == "mysql": + _modify_columns( + engine, "events", ["time_fired DATETIME(6)", "created DATETIME(6)"] + ) + _modify_columns( + engine, + "states", + [ + "last_changed DATETIME(6)", + "last_updated DATETIME(6)", + "created DATETIME(6)", + ], + ) else: raise ValueError(f"No schema migration defined for version {new_version}") diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index ef7181c9c03ed..a547f3151338b 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -26,7 +26,7 @@ # pylint: disable=invalid-name Base = declarative_base() -SCHEMA_VERSION = 12 +SCHEMA_VERSION = 13 _LOGGER = logging.getLogger(__name__) @@ -39,6 +39,10 @@ ALL_TABLES = [TABLE_STATES, TABLE_EVENTS, TABLE_RECORDER_RUNS, TABLE_SCHEMA_CHANGES] +DATETIME_TYPE = DateTime(timezone=True).with_variant( + mysql.DATETIME(timezone=True, fsp=6), "mysql" +) + class Events(Base): # type: ignore """Event history data.""" @@ -52,8 +56,8 @@ class Events(Base): # type: ignore event_type = Column(String(32)) event_data = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) origin = Column(String(32)) - time_fired = Column(DateTime(timezone=True), index=True) - created = Column(DateTime(timezone=True), default=dt_util.utcnow) + time_fired = Column(DATETIME_TYPE, index=True) + created = Column(DATETIME_TYPE, default=dt_util.utcnow) context_id = Column(String(36), index=True) context_user_id = Column(String(36), index=True) context_parent_id = Column(String(36), index=True) @@ -123,9 +127,9 @@ class States(Base): # type: ignore event_id = Column( Integer, ForeignKey("events.event_id", ondelete="CASCADE"), index=True ) - last_changed = Column(DateTime(timezone=True), default=dt_util.utcnow) - last_updated = Column(DateTime(timezone=True), default=dt_util.utcnow, index=True) - created = Column(DateTime(timezone=True), default=dt_util.utcnow) + last_changed = Column(DATETIME_TYPE, default=dt_util.utcnow) + last_updated = Column(DATETIME_TYPE, default=dt_util.utcnow, index=True) + created = Column(DATETIME_TYPE, default=dt_util.utcnow) old_state_id = Column( Integer, ForeignKey("states.state_id", ondelete="NO ACTION"), index=True ) From 589f2240b16b93e64518c0e821c3989f4ed45c31 Mon Sep 17 00:00:00 2001 From: stegm Date: Wed, 7 Apr 2021 09:18:07 +0200 Subject: [PATCH 0093/1317] New integration for Kostal Plenticore solar inverters (#43404) * New integration for Kostal Plenticore solar inverters. * Fix errors from github pipeline. * Fixed test for py37. * Add more test for test coverage check. * Try to fix test coverage check. * Fix import sort order. * Try fix test code coverage . * Mock api client for tests. * Fix typo. * Fix order of rebased code from dev. * Add new data point for home power. * Modifications to review. Remove service for write access (for first pull request). Refactor update coordinator to not use the entity API. * Fixed mock imports. * Ignore new python module on coverage. * Changes after review. * Fixed unit test because of config title. * Fixes from review. * Changes from review (unique id and mocking of tests) * Use async update method. Change unique id. Remove _dict * Remove _data field. * Removed login flag from PlenticoreUpdateCoordinator. * Removed Dynamic SoC sensor because it should be a binary sensor. * Remove more sensors because they are binary sensors. --- .coveragerc | 4 + CODEOWNERS | 1 + .../components/kostal_plenticore/__init__.py | 60 ++ .../kostal_plenticore/config_flow.py | 78 +++ .../components/kostal_plenticore/const.py | 521 ++++++++++++++++++ .../components/kostal_plenticore/helper.py | 259 +++++++++ .../kostal_plenticore/manifest.json | 10 + .../components/kostal_plenticore/sensor.py | 193 +++++++ .../components/kostal_plenticore/strings.json | 21 + .../kostal_plenticore/translations/en.json | 22 + homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + .../components/kostal_plenticore/__init__.py | 1 + .../kostal_plenticore/test_config_flow.py | 206 +++++++ 15 files changed, 1383 insertions(+) create mode 100644 homeassistant/components/kostal_plenticore/__init__.py create mode 100644 homeassistant/components/kostal_plenticore/config_flow.py create mode 100644 homeassistant/components/kostal_plenticore/const.py create mode 100644 homeassistant/components/kostal_plenticore/helper.py create mode 100644 homeassistant/components/kostal_plenticore/manifest.json create mode 100644 homeassistant/components/kostal_plenticore/sensor.py create mode 100644 homeassistant/components/kostal_plenticore/strings.json create mode 100644 homeassistant/components/kostal_plenticore/translations/en.json create mode 100644 tests/components/kostal_plenticore/__init__.py create mode 100644 tests/components/kostal_plenticore/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index a9ae2313f9f7e..0292a6c14416b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -506,6 +506,10 @@ omit = homeassistant/components/kodi/media_player.py homeassistant/components/kodi/notify.py homeassistant/components/konnected/* + homeassistant/components/kostal_plenticore/__init__.py + homeassistant/components/kostal_plenticore/const.py + homeassistant/components/kostal_plenticore/helper.py + homeassistant/components/kostal_plenticore/sensor.py homeassistant/components/kwb/sensor.py homeassistant/components/lacrosse/sensor.py homeassistant/components/lametric/* diff --git a/CODEOWNERS b/CODEOWNERS index 51cd7ed43ccb1..eaad0a975e4bd 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -250,6 +250,7 @@ homeassistant/components/kmtronic/* @dgomes homeassistant/components/knx/* @Julius2342 @farmio @marvin-w homeassistant/components/kodi/* @OnFreund @cgtobi homeassistant/components/konnected/* @heythisisnate @kit-klein +homeassistant/components/kostal_plenticore/* @stegm homeassistant/components/kulersky/* @emlove homeassistant/components/lametric/* @robbiet480 homeassistant/components/launch_library/* @ludeeus diff --git a/homeassistant/components/kostal_plenticore/__init__.py b/homeassistant/components/kostal_plenticore/__init__.py new file mode 100644 index 0000000000000..f06657fdaa162 --- /dev/null +++ b/homeassistant/components/kostal_plenticore/__init__.py @@ -0,0 +1,60 @@ +"""The Kostal Plenticore Solar Inverter integration.""" +import asyncio +import logging + +from kostal.plenticore import PlenticoreApiException + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DOMAIN +from .helper import Plenticore + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = ["sensor"] + + +async def async_setup(hass: HomeAssistant, config: dict) -> bool: + """Set up the Kostal Plenticore Solar Inverter component.""" + hass.data.setdefault(DOMAIN, {}) + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Kostal Plenticore Solar Inverter from a config entry.""" + + plenticore = Plenticore(hass, entry) + + if not await plenticore.async_setup(): + return False + + hass.data[DOMAIN][entry.entry_id] = plenticore + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + if unload_ok: + # remove API object + plenticore = hass.data[DOMAIN].pop(entry.entry_id) + try: + await plenticore.async_unload() + except PlenticoreApiException as err: + _LOGGER.error("Error logging out from inverter: %s", err) + + return unload_ok diff --git a/homeassistant/components/kostal_plenticore/config_flow.py b/homeassistant/components/kostal_plenticore/config_flow.py new file mode 100644 index 0000000000000..d70115a499f8a --- /dev/null +++ b/homeassistant/components/kostal_plenticore/config_flow.py @@ -0,0 +1,78 @@ +"""Config flow for Kostal Plenticore Solar Inverter integration.""" +import asyncio +import logging + +from aiohttp.client_exceptions import ClientError +from kostal.plenticore import PlenticoreApiClient, PlenticoreAuthenticationException +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_BASE, CONF_HOST, CONF_PASSWORD +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Required(CONF_PASSWORD): str, + } +) + + +@callback +def configured_instances(hass): + """Return a set of configured Kostal Plenticore HOSTS.""" + return { + entry.data[CONF_HOST] for entry in hass.config_entries.async_entries(DOMAIN) + } + + +async def test_connection(hass: HomeAssistant, data) -> str: + """Test the connection to the inverter. + + Data has the keys from DATA_SCHEMA with values provided by the user. + """ + + session = async_get_clientsession(hass) + async with PlenticoreApiClient(session, data["host"]) as client: + await client.login(data["password"]) + values = await client.get_setting_values("scb:network", "Hostname") + + return values["scb:network"]["Hostname"] + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Kostal Plenticore Solar Inverter.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + hostname = None + + if user_input is not None: + if user_input[CONF_HOST] in configured_instances(self.hass): + return self.async_abort(reason="already_configured") + try: + hostname = await test_connection(self.hass, user_input) + except PlenticoreAuthenticationException as ex: + errors[CONF_PASSWORD] = "invalid_auth" + _LOGGER.error("Error response: %s", ex) + except (ClientError, asyncio.TimeoutError): + errors[CONF_HOST] = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors[CONF_BASE] = "unknown" + + if not errors: + return self.async_create_entry(title=hostname, data=user_input) + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) diff --git a/homeassistant/components/kostal_plenticore/const.py b/homeassistant/components/kostal_plenticore/const.py new file mode 100644 index 0000000000000..8342ff74adaf1 --- /dev/null +++ b/homeassistant/components/kostal_plenticore/const.py @@ -0,0 +1,521 @@ +"""Constants for the Kostal Plenticore Solar Inverter integration.""" + +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ICON, + ATTR_UNIT_OF_MEASUREMENT, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + ENERGY_KILO_WATT_HOUR, + PERCENTAGE, + POWER_WATT, +) + +DOMAIN = "kostal_plenticore" + +ATTR_ENABLED_DEFAULT = "entity_registry_enabled_default" + +# Defines all entities for process data. +# +# Each entry is defined with a tuple of these values: +# - module id (str) +# - process data id (str) +# - entity name suffix (str) +# - sensor properties (dict) +# - value formatter (str) +SENSOR_PROCESS_DATA = [ + ( + "devices:local", + "Inverter:State", + "Inverter State", + {ATTR_ICON: "mdi:state-machine"}, + "format_inverter_state", + ), + ( + "devices:local", + "Dc_P", + "Solar Power", + { + ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, + ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, + ATTR_ENABLED_DEFAULT: True, + }, + "format_round", + ), + ( + "devices:local", + "Grid_P", + "Grid Power", + { + ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, + ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, + ATTR_ENABLED_DEFAULT: True, + }, + "format_round", + ), + ( + "devices:local", + "HomeBat_P", + "Home Power from Battery", + {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER}, + "format_round", + ), + ( + "devices:local", + "HomeGrid_P", + "Home Power from Grid", + {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER}, + "format_round", + ), + ( + "devices:local", + "HomeOwn_P", + "Home Power from Own", + {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER}, + "format_round", + ), + ( + "devices:local", + "HomePv_P", + "Home Power from PV", + {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER}, + "format_round", + ), + ( + "devices:local", + "Home_P", + "Home Power", + {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER}, + "format_round", + ), + ( + "devices:local:ac", + "P", + "AC Power", + { + ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, + ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, + ATTR_ENABLED_DEFAULT: True, + }, + "format_round", + ), + ( + "devices:local:pv1", + "P", + "DC1 Power", + {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER}, + "format_round", + ), + ( + "devices:local:pv2", + "P", + "DC2 Power", + {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER}, + "format_round", + ), + ( + "devices:local", + "PV2Bat_P", + "PV to Battery Power", + {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER}, + "format_round", + ), + ( + "devices:local", + "EM_State", + "Energy Manager State", + {ATTR_ICON: "mdi:state-machine"}, + "format_em_manager_state", + ), + ( + "devices:local:battery", + "Cycles", + "Battery Cycles", + {ATTR_ICON: "mdi:recycle"}, + "format_round", + ), + ( + "devices:local:battery", + "P", + "Battery Power", + {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER}, + "format_round", + ), + ( + "devices:local:battery", + "SoC", + "Battery SoC", + {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY}, + "format_round", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:Autarky:Day", + "Autarky Day", + {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut"}, + "format_round", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:Autarky:Month", + "Autarky Month", + {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut"}, + "format_round", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:Autarky:Total", + "Autarky Total", + {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut"}, + "format_round", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:Autarky:Year", + "Autarky Year", + {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut"}, + "format_round", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:OwnConsumptionRate:Day", + "Own Consumption Rate Day", + {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut"}, + "format_round", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:OwnConsumptionRate:Month", + "Own Consumption Rate Month", + {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut"}, + "format_round", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:OwnConsumptionRate:Total", + "Own Consumption Rate Total", + {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut"}, + "format_round", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:OwnConsumptionRate:Year", + "Own Consumption Rate Year", + {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut"}, + "format_round", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyHome:Day", + "Home Consumption Day", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyHome:Month", + "Home Consumption Month", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyHome:Year", + "Home Consumption Year", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyHome:Total", + "Home Consumption Total", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyHomeBat:Day", + "Home Consumption from Battery Day", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyHomeBat:Month", + "Home Consumption from Battery Month", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyHomeBat:Year", + "Home Consumption from Battery Year", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyHomeBat:Total", + "Home Consumption from Battery Total", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyHomeGrid:Day", + "Home Consumption from Grid Day", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyHomeGrid:Month", + "Home Consumption from Grid Month", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyHomeGrid:Year", + "Home Consumption from Grid Year", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyHomeGrid:Total", + "Home Consumption from Grid Total", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyHomePv:Day", + "Home Consumption from PV Day", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyHomePv:Month", + "Home Consumption from PV Month", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyHomePv:Year", + "Home Consumption from PV Year", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyHomePv:Total", + "Home Consumption from PV Total", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyPv1:Day", + "Energy PV1 Day", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyPv1:Month", + "Energy PV1 Month", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyPv1:Year", + "Energy PV1 Year", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyPv1:Total", + "Energy PV1 Total", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyPv2:Day", + "Energy PV2 Day", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyPv2:Month", + "Energy PV2 Month", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyPv2:Year", + "Energy PV2 Year", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyPv2:Total", + "Energy PV2 Total", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:Yield:Day", + "Energy Yield Day", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_ENABLED_DEFAULT: True, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:Yield:Month", + "Energy Yield Month", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:Yield:Year", + "Energy Yield Year", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:Yield:Total", + "Energy Yield Total", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), +] + +# Defines all entities for settings. +# +# Each entry is defined with a tuple of these values: +# - module id (str) +# - process data id (str) +# - entity name suffix (str) +# - sensor properties (dict) +# - value formatter (str) +SENSOR_SETTINGS_DATA = [ + ( + "devices:local", + "Battery:MinHomeComsumption", + "Battery min Home Consumption", + {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER}, + "format_round", + ), + ( + "devices:local", + "Battery:MinSoc", + "Battery min Soc", + {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:battery-negative"}, + "format_round", + ), + ( + "devices:local", + "Battery:Strategy", + "Battery Strategy", + {}, + "format_round", + ), +] diff --git a/homeassistant/components/kostal_plenticore/helper.py b/homeassistant/components/kostal_plenticore/helper.py new file mode 100644 index 0000000000000..6f9cc4f5ee0a7 --- /dev/null +++ b/homeassistant/components/kostal_plenticore/helper.py @@ -0,0 +1,259 @@ +"""Code to handle the Plenticore API.""" +import asyncio +from collections import defaultdict +from datetime import datetime, timedelta +import logging +from typing import Dict, Union + +from aiohttp.client_exceptions import ClientError +from kostal.plenticore import PlenticoreApiClient, PlenticoreAuthenticationException + +from homeassistant.const import CONF_HOST, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class Plenticore: + """Manages the Plenticore API.""" + + def __init__(self, hass, config_entry): + """Create a new plenticore manager instance.""" + self.hass = hass + self.config_entry = config_entry + + self._client = None + self._shutdown_remove_listener = None + + self.device_info = {} + + @property + def host(self) -> str: + """Return the host of the Plenticore inverter.""" + return self.config_entry.data[CONF_HOST] + + @property + def client(self) -> PlenticoreApiClient: + """Return the Plenticore API client.""" + return self._client + + async def async_setup(self) -> bool: + """Set up Plenticore API client.""" + self._client = PlenticoreApiClient( + async_get_clientsession(self.hass), host=self.host + ) + try: + await self._client.login(self.config_entry.data[CONF_PASSWORD]) + except PlenticoreAuthenticationException as err: + _LOGGER.error( + "Authentication exception connecting to %s: %s", self.host, err + ) + return False + except (ClientError, asyncio.TimeoutError) as err: + _LOGGER.error("Error connecting to %s", self.host) + raise ConfigEntryNotReady from err + else: + _LOGGER.debug("Log-in successfully to %s", self.host) + + self._shutdown_remove_listener = self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, self._async_shutdown + ) + + # get some device meta data + settings = await self._client.get_setting_values( + { + "devices:local": [ + "Properties:SerialNo", + "Branding:ProductName1", + "Branding:ProductName2", + "Properties:VersionIOC", + "Properties:VersionMC", + ], + "scb:network": ["Hostname"], + } + ) + + device_local = settings["devices:local"] + prod1 = device_local["Branding:ProductName1"] + prod2 = device_local["Branding:ProductName2"] + + self.device_info = { + "identifiers": {(DOMAIN, device_local["Properties:SerialNo"])}, + "manufacturer": "Kostal", + "model": f"{prod1} {prod2}", + "name": settings["scb:network"]["Hostname"], + "sw_version": f'IOC: {device_local["Properties:VersionIOC"]}' + + f' MC: {device_local["Properties:VersionMC"]}', + } + + return True + + async def _async_shutdown(self, event): + """Call from Homeassistant shutdown event.""" + # unset remove listener otherwise calling it would raise an exception + self._shutdown_remove_listener = None + await self.async_unload() + + async def async_unload(self) -> None: + """Unload the Plenticore API client.""" + if self._shutdown_remove_listener: + self._shutdown_remove_listener() + + await self._client.logout() + self._client = None + _LOGGER.debug("Logged out from %s", self.host) + + +class PlenticoreUpdateCoordinator(DataUpdateCoordinator): + """Base implementation of DataUpdateCoordinator for Plenticore data.""" + + def __init__( + self, + hass: HomeAssistant, + logger: logging.Logger, + name: str, + update_inverval: timedelta, + plenticore: Plenticore, + ): + """Create a new update coordinator for plenticore data.""" + super().__init__( + hass=hass, + logger=logger, + name=name, + update_interval=update_inverval, + ) + # data ids to poll + self._fetch = defaultdict(list) + self._plenticore = plenticore + + def start_fetch_data(self, module_id: str, data_id: str) -> None: + """Start fetching the given data (module-id and data-id).""" + self._fetch[module_id].append(data_id) + + # Force an update of all data. Multiple refresh calls + # are ignored by the debouncer. + async def force_refresh(event_time: datetime) -> None: + await self.async_request_refresh() + + async_call_later(self.hass, 2, force_refresh) + + def stop_fetch_data(self, module_id: str, data_id: str) -> None: + """Stop fetching the given data (module-id and data-id).""" + self._fetch[module_id].remove(data_id) + + +class ProcessDataUpdateCoordinator(PlenticoreUpdateCoordinator): + """Implementation of PlenticoreUpdateCoordinator for process data.""" + + async def _async_update_data(self) -> Dict[str, Dict[str, str]]: + client = self._plenticore.client + + if not self._fetch or client is None: + return {} + + _LOGGER.debug("Fetching %s for %s", self.name, self._fetch) + + fetched_data = await client.get_process_data_values(self._fetch) + return { + module_id: { + process_data.id: process_data.value + for process_data in fetched_data[module_id] + } + for module_id in fetched_data + } + + +class SettingDataUpdateCoordinator(PlenticoreUpdateCoordinator): + """Implementation of PlenticoreUpdateCoordinator for settings data.""" + + async def _async_update_data(self) -> Dict[str, Dict[str, str]]: + client = self._plenticore.client + + if not self._fetch or client is None: + return {} + + _LOGGER.debug("Fetching %s for %s", self.name, self._fetch) + + fetched_data = await client.get_setting_values(self._fetch) + + return fetched_data + + +class PlenticoreDataFormatter: + """Provides method to format values of process or settings data.""" + + INVERTER_STATES = { + 0: "Off", + 1: "Init", + 2: "IsoMEas", + 3: "GridCheck", + 4: "StartUp", + 6: "FeedIn", + 7: "Throttled", + 8: "ExtSwitchOff", + 9: "Update", + 10: "Standby", + 11: "GridSync", + 12: "GridPreCheck", + 13: "GridSwitchOff", + 14: "Overheating", + 15: "Shutdown", + 16: "ImproperDcVoltage", + 17: "ESB", + } + + EM_STATES = { + 0: "Idle", + 1: "n/a", + 2: "Emergency Battery Charge", + 4: "n/a", + 8: "Winter Mode Step 1", + 16: "Winter Mode Step 2", + } + + @classmethod + def get_method(cls, name: str) -> callable: + """Return a callable formatter of the given name.""" + return getattr(cls, name) + + @staticmethod + def format_round(state: str) -> Union[int, str]: + """Return the given state value as rounded integer.""" + try: + return round(float(state)) + except (TypeError, ValueError): + return state + + @staticmethod + def format_energy(state: str) -> Union[float, str]: + """Return the given state value as energy value, scaled to kWh.""" + try: + return round(float(state) / 1000, 1) + except (TypeError, ValueError): + return state + + @staticmethod + def format_inverter_state(state: str) -> str: + """Return a readable string of the inverter state.""" + try: + value = int(state) + except (TypeError, ValueError): + return state + + return PlenticoreDataFormatter.INVERTER_STATES.get(value) + + @staticmethod + def format_em_manager_state(state: str) -> str: + """Return a readable state of the energy manager.""" + try: + value = int(state) + except (TypeError, ValueError): + return state + + return PlenticoreDataFormatter.EM_STATES.get(value) diff --git a/homeassistant/components/kostal_plenticore/manifest.json b/homeassistant/components/kostal_plenticore/manifest.json new file mode 100644 index 0000000000000..427c730833cf4 --- /dev/null +++ b/homeassistant/components/kostal_plenticore/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "kostal_plenticore", + "name": "Kostal Plenticore Solar Inverter", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/kostal_plenticore", + "requirements": ["kostal_plenticore==0.2.0"], + "codeowners": [ + "@stegm" + ] +} \ No newline at end of file diff --git a/homeassistant/components/kostal_plenticore/sensor.py b/homeassistant/components/kostal_plenticore/sensor.py new file mode 100644 index 0000000000000..82b06c96a7731 --- /dev/null +++ b/homeassistant/components/kostal_plenticore/sensor.py @@ -0,0 +1,193 @@ +"""Platform for Kostal Plenticore sensors.""" +from datetime import timedelta +import logging +from typing import Any, Callable, Dict, Optional + +from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import ( + ATTR_ENABLED_DEFAULT, + DOMAIN, + SENSOR_PROCESS_DATA, + SENSOR_SETTINGS_DATA, +) +from .helper import ( + PlenticoreDataFormatter, + ProcessDataUpdateCoordinator, + SettingDataUpdateCoordinator, +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities +): + """Add kostal plenticore Sensors.""" + plenticore = hass.data[DOMAIN][entry.entry_id] + + entities = [] + + available_process_data = await plenticore.client.get_process_data() + process_data_update_coordinator = ProcessDataUpdateCoordinator( + hass, + _LOGGER, + "Process Data", + timedelta(seconds=10), + plenticore, + ) + for module_id, data_id, name, sensor_data, fmt in SENSOR_PROCESS_DATA: + if ( + module_id not in available_process_data + or data_id not in available_process_data[module_id] + ): + _LOGGER.debug( + "Skipping non existing process data %s/%s", module_id, data_id + ) + continue + + entities.append( + PlenticoreDataSensor( + process_data_update_coordinator, + entry.entry_id, + entry.title, + module_id, + data_id, + name, + sensor_data, + PlenticoreDataFormatter.get_method(fmt), + plenticore.device_info, + ) + ) + + available_settings_data = await plenticore.client.get_settings() + settings_data_update_coordinator = SettingDataUpdateCoordinator( + hass, + _LOGGER, + "Settings Data", + timedelta(seconds=300), + plenticore, + ) + for module_id, data_id, name, sensor_data, fmt in SENSOR_SETTINGS_DATA: + if module_id not in available_settings_data or data_id not in ( + setting.id for setting in available_settings_data[module_id] + ): + _LOGGER.debug( + "Skipping non existing setting data %s/%s", module_id, data_id + ) + continue + + entities.append( + PlenticoreDataSensor( + settings_data_update_coordinator, + entry.entry_id, + entry.title, + module_id, + data_id, + name, + sensor_data, + PlenticoreDataFormatter.get_method(fmt), + plenticore.device_info, + ) + ) + + async_add_entities(entities) + + +class PlenticoreDataSensor(CoordinatorEntity, SensorEntity): + """Representation of a Plenticore data Sensor.""" + + def __init__( + self, + coordinator, + entry_id: str, + platform_name: str, + module_id: str, + data_id: str, + sensor_name: str, + sensor_data: Dict[str, Any], + formatter: Callable[[str], Any], + device_info: Dict[str, Any], + ): + """Create a new Sensor Entity for Plenticore process data.""" + super().__init__(coordinator) + self.entry_id = entry_id + self.platform_name = platform_name + self.module_id = module_id + self.data_id = data_id + + self._sensor_name = sensor_name + self._sensor_data = sensor_data + self._formatter = formatter + + self._device_info = device_info + + @property + def available(self) -> bool: + """Return if entity is available.""" + return ( + super().available + and self.coordinator.data is not None + and self.module_id in self.coordinator.data + and self.data_id in self.coordinator.data[self.module_id] + ) + + async def async_added_to_hass(self) -> None: + """Register this entity on the Update Coordinator.""" + await super().async_added_to_hass() + self.coordinator.start_fetch_data(self.module_id, self.data_id) + + async def async_will_remove_from_hass(self) -> None: + """Unregister this entity from the Update Coordinator.""" + self.coordinator.stop_fetch_data(self.module_id, self.data_id) + await super().async_will_remove_from_hass() + + @property + def device_info(self) -> Dict[str, Any]: + """Return the device info.""" + return self._device_info + + @property + def unique_id(self) -> str: + """Return the unique id of this Sensor Entity.""" + return f"{self.entry_id}_{self.module_id}_{self.data_id}" + + @property + def name(self) -> str: + """Return the name of this Sensor Entity.""" + return f"{self.platform_name} {self._sensor_name}" + + @property + def unit_of_measurement(self) -> Optional[str]: + """Return the unit of this Sensor Entity or None.""" + return self._sensor_data.get(ATTR_UNIT_OF_MEASUREMENT) + + @property + def icon(self) -> Optional[str]: + """Return the icon name of this Sensor Entity or None.""" + return self._sensor_data.get(ATTR_ICON) + + @property + def device_class(self) -> Optional[str]: + """Return the class of this device, from component DEVICE_CLASSES.""" + return self._sensor_data.get(ATTR_DEVICE_CLASS) + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + return self._sensor_data.get(ATTR_ENABLED_DEFAULT, False) + + @property + def state(self) -> Optional[Any]: + """Return the state of the sensor.""" + if self.coordinator.data is None: + # None is translated to STATE_UNKNOWN + return None + + raw_value = self.coordinator.data[self.module_id][self.data_id] + + return self._formatter(raw_value) if self._formatter else raw_value diff --git a/homeassistant/components/kostal_plenticore/strings.json b/homeassistant/components/kostal_plenticore/strings.json new file mode 100644 index 0000000000000..771c3ada744bf --- /dev/null +++ b/homeassistant/components/kostal_plenticore/strings.json @@ -0,0 +1,21 @@ +{ + "title": "Kostal Plenticore Solar Inverter", + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kostal_plenticore/translations/en.json b/homeassistant/components/kostal_plenticore/translations/en.json new file mode 100644 index 0000000000000..f9aafb90c270d --- /dev/null +++ b/homeassistant/components/kostal_plenticore/translations/en.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "timeout": "Timeout/No answer", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Password" + } + } + } + }, + "title": "Kostal Plenticore Solar Inverter" +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index e9eece903fc7e..293e39764f978 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -124,6 +124,7 @@ "kmtronic", "kodi", "konnected", + "kostal_plenticore", "kulersky", "life360", "lifx", diff --git a/requirements_all.txt b/requirements_all.txt index 421c8d44335fc..edeaa0d3e10e0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -848,6 +848,9 @@ kiwiki-client==0.1.1 # homeassistant.components.konnected konnected==1.2.0 +# homeassistant.components.kostal_plenticore +kostal_plenticore==0.2.0 + # homeassistant.components.eufy lakeside==0.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fc2503c9465d9..ab36db6f5a070 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -468,6 +468,9 @@ jsonpath==0.82 # homeassistant.components.konnected konnected==1.2.0 +# homeassistant.components.kostal_plenticore +kostal_plenticore==0.2.0 + # homeassistant.components.dyson libpurecool==0.6.4 diff --git a/tests/components/kostal_plenticore/__init__.py b/tests/components/kostal_plenticore/__init__.py new file mode 100644 index 0000000000000..bba546eea1179 --- /dev/null +++ b/tests/components/kostal_plenticore/__init__.py @@ -0,0 +1 @@ +"""Tests for the Kostal Plenticore Solar Inverter integration.""" diff --git a/tests/components/kostal_plenticore/test_config_flow.py b/tests/components/kostal_plenticore/test_config_flow.py new file mode 100644 index 0000000000000..04a69892b4381 --- /dev/null +++ b/tests/components/kostal_plenticore/test_config_flow.py @@ -0,0 +1,206 @@ +"""Test the Kostal Plenticore Solar Inverter config flow.""" +import asyncio +from unittest.mock import ANY, AsyncMock, MagicMock, patch + +from kostal.plenticore import PlenticoreAuthenticationException + +from homeassistant import config_entries, setup +from homeassistant.components.kostal_plenticore import config_flow +from homeassistant.components.kostal_plenticore.const import DOMAIN + +from tests.common import MockConfigEntry + + +async def test_formx(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.kostal_plenticore.config_flow.PlenticoreApiClient" + ) as mock_api_class, patch( + "homeassistant.components.kostal_plenticore.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.kostal_plenticore.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + # mock of the context manager instance + mock_api_ctx = MagicMock() + mock_api_ctx.login = AsyncMock() + mock_api_ctx.get_setting_values = AsyncMock( + return_value={"scb:network": {"Hostname": "scb"}} + ) + + # mock of the return instance of PlenticoreApiClient + mock_api = MagicMock() + mock_api.__aenter__.return_value = mock_api_ctx + mock_api.__aexit__ = AsyncMock() + + mock_api_class.return_value = mock_api + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "password": "test-password", + }, + ) + + mock_api_class.assert_called_once_with(ANY, "1.1.1.1") + mock_api.__aenter__.assert_called_once() + mock_api.__aexit__.assert_called_once() + mock_api_ctx.login.assert_called_once_with("test-password") + mock_api_ctx.get_setting_values.assert_called_once() + + assert result2["type"] == "create_entry" + assert result2["title"] == "scb" + assert result2["data"] == { + "host": "1.1.1.1", + "password": "test-password", + } + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_invalid_auth(hass): + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.kostal_plenticore.config_flow.PlenticoreApiClient" + ) as mock_api_class: + # mock of the context manager instance + mock_api_ctx = MagicMock() + mock_api_ctx.login = AsyncMock( + side_effect=PlenticoreAuthenticationException(404, "invalid user"), + ) + + # mock of the return instance of PlenticoreApiClient + mock_api = MagicMock() + mock_api.__aenter__.return_value = mock_api_ctx + mock_api.__aexit__.return_value = None + + mock_api_class.return_value = mock_api + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "password": "test-password", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"password": "invalid_auth"} + + +async def test_form_cannot_connect(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.kostal_plenticore.config_flow.PlenticoreApiClient" + ) as mock_api_class: + # mock of the context manager instance + mock_api_ctx = MagicMock() + mock_api_ctx.login = AsyncMock( + side_effect=asyncio.TimeoutError(), + ) + + # mock of the return instance of PlenticoreApiClient + mock_api = MagicMock() + mock_api.__aenter__.return_value = mock_api_ctx + mock_api.__aexit__.return_value = None + + mock_api_class.return_value = mock_api + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "password": "test-password", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"host": "cannot_connect"} + + +async def test_form_unexpected_error(hass): + """Test we handle unexpected error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.kostal_plenticore.config_flow.PlenticoreApiClient" + ) as mock_api_class: + # mock of the context manager instance + mock_api_ctx = MagicMock() + mock_api_ctx.login = AsyncMock( + side_effect=Exception(), + ) + + # mock of the return instance of PlenticoreApiClient + mock_api = MagicMock() + mock_api.__aenter__.return_value = mock_api_ctx + mock_api.__aexit__.return_value = None + + mock_api_class.return_value = mock_api + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "password": "test-password", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "unknown"} + + +async def test_already_configured(hass): + """Test we handle already configured error.""" + MockConfigEntry( + domain="kostal_plenticore", + data={"host": "1.1.1.1", "password": "foobar"}, + unique_id="112233445566", + ).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "password": "test-password", + }, + ) + + assert result2["type"] == "abort" + assert result2["reason"] == "already_configured" + + +def test_configured_instances(hass): + """Test configured_instances returns all configured hosts.""" + MockConfigEntry( + domain="kostal_plenticore", + data={"host": "2.2.2.2", "password": "foobar"}, + unique_id="112233445566", + ).add_to_hass(hass) + + result = config_flow.configured_instances(hass) + + assert result == {"2.2.2.2"} From b558f20ad2524321270213237461a2a1d47fd5b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Wed, 7 Apr 2021 09:27:58 +0200 Subject: [PATCH 0094/1317] Met.no - only update data if coordinates changed (#48756) --- homeassistant/components/met/__init__.py | 15 ++++++++++----- tests/components/met/test_weather.py | 5 +++++ 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/met/__init__.py b/homeassistant/components/met/__init__.py index 4367ca9853603..d89ab3242d8fc 100644 --- a/homeassistant/components/met/__init__.py +++ b/homeassistant/components/met/__init__.py @@ -68,7 +68,7 @@ def __init__(self, hass, config_entry): self.weather = MetWeatherData( hass, config_entry.data, hass.config.units.is_metric ) - self.weather.init_data() + self.weather.set_coordinates() update_interval = timedelta(minutes=randrange(55, 65)) @@ -88,8 +88,8 @@ def track_home(self): async def _async_update_weather_data(_event=None): """Update weather data.""" - self.weather.init_data() - await self.async_refresh() + if self.weather.set_coordinates(): + await self.async_refresh() self._unsub_track_home = self.hass.bus.async_listen( EVENT_CORE_CONFIG_UPDATE, _async_update_weather_data @@ -114,9 +114,10 @@ def __init__(self, hass, config, is_metric): self.current_weather_data = {} self.daily_forecast = None self.hourly_forecast = None + self._coordinates = None - def init_data(self): - """Weather data inialization - get the coordinates.""" + def set_coordinates(self): + """Weather data inialization - set the coordinates.""" if self._config.get(CONF_TRACK_HOME, False): latitude = self.hass.config.latitude longitude = self.hass.config.longitude @@ -136,10 +137,14 @@ def init_data(self): "lon": str(longitude), "msl": str(elevation), } + if coordinates == self._coordinates: + return False + self._coordinates = coordinates self._weather_data = metno.MetWeatherData( coordinates, async_get_clientsession(self.hass), api_url=URL ) + return True async def fetch_data(self): """Fetch data from API - (current weather and forecast).""" diff --git a/tests/components/met/test_weather.py b/tests/components/met/test_weather.py index 89c1dc6261234..92e9b67466814 100644 --- a/tests/components/met/test_weather.py +++ b/tests/components/met/test_weather.py @@ -29,6 +29,11 @@ async def test_tracking_home(hass, mock_weather): assert len(mock_weather.mock_calls) == 8 + # Same coordinates again should not trigger any new requests to met.no + await hass.config.async_update(latitude=10, longitude=20) + await hass.async_block_till_done() + assert len(mock_weather.mock_calls) == 8 + entry = hass.config_entries.async_entries()[0] await hass.config_entries.async_remove(entry.entry_id) await hass.async_block_till_done() From 5f8fcca5adfb5ba3a710ac3b1f76b42a4c7f2103 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 6 Apr 2021 21:39:04 -1000 Subject: [PATCH 0095/1317] Solve cast delaying startup when discovered devices are slow to setup (#48755) * Solve cast delaying startup when devices are slow to setup * Update homeassistant/components/cast/media_player.py Co-authored-by: Erik Montnemery Co-authored-by: Erik Montnemery --- homeassistant/components/cast/media_player.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index 7c2a696027fae..25b2674821d04 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -1,6 +1,7 @@ """Provide functionality to interact with Cast devices on the network.""" from __future__ import annotations +import asyncio from contextlib import suppress from datetime import timedelta import functools as ft @@ -185,7 +186,9 @@ async def async_added_to_hass(self): ) self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._async_stop) self.async_set_cast_info(self._cast_info) - self.hass.async_create_task( + # asyncio.create_task is used to avoid delaying startup wrapup if the device + # is discovered already during startup but then fails to respond + asyncio.create_task( async_create_catching_coro(self.async_connect_to_chromecast()) ) From 6ec8e17e7b09449cd2a25f80c21732e8753b1e71 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 7 Apr 2021 09:39:39 +0200 Subject: [PATCH 0096/1317] Do not activate Met.no without setting a Home coordinates (#48741) --- homeassistant/components/met/__init__.py | 21 ++++++++++++- homeassistant/components/met/config_flow.py | 16 +++++++++- homeassistant/components/met/const.py | 3 ++ homeassistant/components/met/strings.json | 7 ++++- tests/components/met/__init__.py | 12 +++++--- tests/components/met/test_config_flow.py | 20 ++++++++++++ tests/components/met/test_init.py | 34 +++++++++++++++++++-- 7 files changed, 104 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/met/__init__.py b/homeassistant/components/met/__init__.py index d89ab3242d8fc..47d946b92e7a6 100644 --- a/homeassistant/components/met/__init__.py +++ b/homeassistant/components/met/__init__.py @@ -19,7 +19,12 @@ from homeassistant.util.distance import convert as convert_distance import homeassistant.util.dt as dt_util -from .const import CONF_TRACK_HOME, DOMAIN +from .const import ( + CONF_TRACK_HOME, + DEFAULT_HOME_LATITUDE, + DEFAULT_HOME_LONGITUDE, + DOMAIN, +) URL = "https://aa015h6buqvih86i1.api.met.no/weatherapi/locationforecast/2.0/complete" @@ -35,6 +40,20 @@ async def async_setup(hass: HomeAssistant, config: Config) -> bool: async def async_setup_entry(hass, config_entry): """Set up Met as config entry.""" + # Don't setup if tracking home location and latitude or longitude isn't set. + # Also, filters out our onboarding default location. + if config_entry.data.get(CONF_TRACK_HOME, False) and ( + (not hass.config.latitude and not hass.config.longitude) + or ( + hass.config.latitude == DEFAULT_HOME_LATITUDE + and hass.config.longitude == DEFAULT_HOME_LONGITUDE + ) + ): + _LOGGER.warning( + "Skip setting up met.no integration; No Home location has been set" + ) + return False + coordinator = MetDataUpdateCoordinator(hass, config_entry) await coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/met/config_flow.py b/homeassistant/components/met/config_flow.py index b9d50ba59a58e..5cfd71ea80121 100644 --- a/homeassistant/components/met/config_flow.py +++ b/homeassistant/components/met/config_flow.py @@ -10,7 +10,13 @@ from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from .const import CONF_TRACK_HOME, DOMAIN, HOME_LOCATION_NAME +from .const import ( + CONF_TRACK_HOME, + DEFAULT_HOME_LATITUDE, + DEFAULT_HOME_LONGITUDE, + DOMAIN, + HOME_LOCATION_NAME, +) @callback @@ -81,6 +87,14 @@ async def async_step_import(self, user_input: dict | None = None) -> dict[str, A async def async_step_onboarding(self, data=None): """Handle a flow initialized by onboarding.""" + # Don't create entry if latitude or longitude isn't set. + # Also, filters out our onboarding default location. + if (not self.hass.config.latitude and not self.hass.config.longitude) or ( + self.hass.config.latitude == DEFAULT_HOME_LATITUDE + and self.hass.config.longitude == DEFAULT_HOME_LONGITUDE + ): + return self.async_abort(reason="no_home") + return self.async_create_entry( title=HOME_LOCATION_NAME, data={CONF_TRACK_HOME: True} ) diff --git a/homeassistant/components/met/const.py b/homeassistant/components/met/const.py index b78c412393d3f..0f4c22dbba334 100644 --- a/homeassistant/components/met/const.py +++ b/homeassistant/components/met/const.py @@ -34,6 +34,9 @@ CONF_TRACK_HOME = "track_home" +DEFAULT_HOME_LATITUDE = 52.3731339 +DEFAULT_HOME_LONGITUDE = 4.8903147 + ENTITY_ID_SENSOR_FORMAT_HOME = f"{WEATHER_DOMAIN}.met_{HOME_LOCATION_NAME}" CONDITIONS_MAP = { diff --git a/homeassistant/components/met/strings.json b/homeassistant/components/met/strings.json index b9e94aba8658e..b9d251e21d890 100644 --- a/homeassistant/components/met/strings.json +++ b/homeassistant/components/met/strings.json @@ -12,6 +12,11 @@ } } }, - "error": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" } + "error": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + }, + "abort": { + "no_home": "No home coordinates are set in the Home Assistant configuration" + } } } diff --git a/tests/components/met/__init__.py b/tests/components/met/__init__.py index 13b186f3b4725..0a17b415965dd 100644 --- a/tests/components/met/__init__.py +++ b/tests/components/met/__init__.py @@ -1,20 +1,24 @@ """Tests for Met.no.""" from unittest.mock import patch -from homeassistant.components.met.const import DOMAIN +from homeassistant.components.met.const import CONF_TRACK_HOME, DOMAIN from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from tests.common import MockConfigEntry -async def init_integration(hass) -> MockConfigEntry: +async def init_integration(hass, track_home=False) -> MockConfigEntry: """Set up the Met integration in Home Assistant.""" entry_data = { CONF_NAME: "test", CONF_LATITUDE: 0, - CONF_LONGITUDE: 0, - CONF_ELEVATION: 0, + CONF_LONGITUDE: 1.0, + CONF_ELEVATION: 1.0, } + + if track_home: + entry_data = {CONF_TRACK_HOME: True} + entry = MockConfigEntry(domain=DOMAIN, data=entry_data) with patch( "homeassistant.components.met.metno.MetWeatherData.fetching_data", diff --git a/tests/components/met/test_config_flow.py b/tests/components/met/test_config_flow.py index 622475e8376db..25e123f67e828 100644 --- a/tests/components/met/test_config_flow.py +++ b/tests/components/met/test_config_flow.py @@ -4,6 +4,7 @@ import pytest from homeassistant.components.met.const import DOMAIN, HOME_LOCATION_NAME +from homeassistant.config import async_process_ha_core_config from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE from tests.common import MockConfigEntry @@ -106,6 +107,25 @@ async def test_onboarding_step(hass): assert result["data"] == {"track_home": True} +@pytest.mark.parametrize("latitude,longitude", [(52.3731339, 4.8903147), (0.0, 0.0)]) +async def test_onboarding_step_abort_no_home(hass, latitude, longitude): + """Test entry not created when default step fails.""" + await async_process_ha_core_config( + hass, + {"latitude": latitude, "longitude": longitude}, + ) + + assert hass.config.latitude == latitude + assert hass.config.longitude == longitude + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "onboarding"}, data={} + ) + + assert result["type"] == "abort" + assert result["reason"] == "no_home" + + async def test_import_step(hass): """Test initializing via import step.""" test_data = { diff --git a/tests/components/met/test_init.py b/tests/components/met/test_init.py index a3323f0156515..074293249c8af 100644 --- a/tests/components/met/test_init.py +++ b/tests/components/met/test_init.py @@ -1,6 +1,15 @@ """Test the Met integration init.""" -from homeassistant.components.met.const import DOMAIN -from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED +from homeassistant.components.met.const import ( + DEFAULT_HOME_LATITUDE, + DEFAULT_HOME_LONGITUDE, + DOMAIN, +) +from homeassistant.config import async_process_ha_core_config +from homeassistant.config_entries import ( + ENTRY_STATE_LOADED, + ENTRY_STATE_NOT_LOADED, + ENTRY_STATE_SETUP_ERROR, +) from . import init_integration @@ -17,3 +26,24 @@ async def test_unload_entry(hass): assert entry.state == ENTRY_STATE_NOT_LOADED assert not hass.data.get(DOMAIN) + + +async def test_fail_default_home_entry(hass, caplog): + """Test abort setup of default home location.""" + await async_process_ha_core_config( + hass, + {"latitude": 52.3731339, "longitude": 4.8903147}, + ) + + assert hass.config.latitude == DEFAULT_HOME_LATITUDE + assert hass.config.longitude == DEFAULT_HOME_LONGITUDE + + entry = await init_integration(hass, track_home=True) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert entry.state == ENTRY_STATE_SETUP_ERROR + + assert ( + "Skip setting up met.no integration; No Home location has been set" + in caplog.text + ) From 06381f56196ea509f3bb988517e4ee24ada027ca Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 7 Apr 2021 10:40:53 +0200 Subject: [PATCH 0097/1317] Upgrade pre-commit to 2.12.0 (#48731) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 81c8819d44933..1d4ada0afcb00 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -9,7 +9,7 @@ coverage==5.5 jsonpickle==1.4.1 mock-open==1.4.0 mypy==0.812 -pre-commit==2.11.1 +pre-commit==2.12.0 pylint==2.7.4 astroid==2.5.2 pipdeptree==1.0.0 From 46371a9e875e360ca19cecbd8091b244faff3625 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 7 Apr 2021 11:12:31 +0200 Subject: [PATCH 0098/1317] Fix whitespace error in cast (#48763) --- homeassistant/components/cast/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index 25b2674821d04..b6ca8dd07286b 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -186,7 +186,7 @@ async def async_added_to_hass(self): ) self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._async_stop) self.async_set_cast_info(self._cast_info) - # asyncio.create_task is used to avoid delaying startup wrapup if the device + # asyncio.create_task is used to avoid delaying startup wrapup if the device # is discovered already during startup but then fails to respond asyncio.create_task( async_create_catching_coro(self.async_connect_to_chromecast()) From ab190f36ac6aed0b7473688033f317ac6418de82 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 7 Apr 2021 11:42:12 +0200 Subject: [PATCH 0099/1317] Update frontend to 20210407.0 (#48765) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index b659ec7e7d4c5..79bf552a5e258 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20210406.0" + "home-assistant-frontend==20210407.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3d8895d0a8270..65f9e9074eff4 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ defusedxml==0.6.0 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.42.0 -home-assistant-frontend==20210406.0 +home-assistant-frontend==20210407.0 httpx==0.17.1 jinja2>=2.11.3 netdisco==2.8.2 diff --git a/requirements_all.txt b/requirements_all.txt index edeaa0d3e10e0..5c81918801746 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -762,7 +762,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210406.0 +home-assistant-frontend==20210407.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ab36db6f5a070..9f48f5c892155 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -423,7 +423,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210406.0 +home-assistant-frontend==20210407.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 5be1eacde9873863605c383589644d935f685a17 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 7 Apr 2021 12:08:22 +0200 Subject: [PATCH 0100/1317] Set AsusWRT mac_address and ip_address properties (#48764) --- .../components/asuswrt/device_tracker.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/asuswrt/device_tracker.py b/homeassistant/components/asuswrt/device_tracker.py index bd86dd21edd8c..0db5dba0b17a5 100644 --- a/homeassistant/components/asuswrt/device_tracker.py +++ b/homeassistant/components/asuswrt/device_tracker.py @@ -81,16 +81,23 @@ def source_type(self) -> str: @property def extra_state_attributes(self) -> dict[str, any]: """Return the attributes.""" - attrs = { - "mac": self._device.mac, - "ip_address": self._device.ip_address, - } + attrs = {} if self._device.last_activity: attrs["last_time_reachable"] = self._device.last_activity.isoformat( timespec="seconds" ) return attrs + @property + def ip_address(self) -> str: + """Return the primary ip address of the device.""" + return self._device.ip_address + + @property + def mac_address(self) -> str: + """Return the mac address of the device.""" + return self._device.mac + @property def device_info(self) -> dict[str, any]: """Return the device information.""" From 2555b10d49371f1b408fc3701acfcf106e0dfe73 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 7 Apr 2021 12:15:56 +0200 Subject: [PATCH 0101/1317] Remove login details before logging SQL errors (#48758) --- homeassistant/components/sql/sensor.py | 24 ++++++++++++++-- tests/components/sql/test_sensor.py | 40 ++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sql/sensor.py b/homeassistant/components/sql/sensor.py index a537b160d0b36..b90ce2f8e594b 100644 --- a/homeassistant/components/sql/sensor.py +++ b/homeassistant/components/sql/sensor.py @@ -2,6 +2,7 @@ import datetime import decimal import logging +import re import sqlalchemy from sqlalchemy.orm import scoped_session, sessionmaker @@ -18,6 +19,13 @@ CONF_QUERIES = "queries" CONF_QUERY = "query" +DB_URL_RE = re.compile("//.*:.*@") + + +def redact_credentials(data): + """Redact credentials from string data.""" + return DB_URL_RE.sub("//****:****@", data) + def validate_sql_select(value): """Validate that value is a SQL SELECT query.""" @@ -47,6 +55,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if not db_url: db_url = DEFAULT_URL.format(hass_config_path=hass.config.path(DEFAULT_DB_FILE)) + sess = None try: engine = sqlalchemy.create_engine(db_url) sessmaker = scoped_session(sessionmaker(bind=engine)) @@ -56,10 +65,15 @@ def setup_platform(hass, config, add_entities, discovery_info=None): sess.execute("SELECT 1;") except sqlalchemy.exc.SQLAlchemyError as err: - _LOGGER.error("Couldn't connect using %s DB_URL: %s", db_url, err) + _LOGGER.error( + "Couldn't connect using %s DB_URL: %s", + redact_credentials(db_url), + redact_credentials(str(err)), + ) return finally: - sess.close() + if sess: + sess.close() queries = [] @@ -147,7 +161,11 @@ def update(self): value = str(value) self._attributes[key] = value except sqlalchemy.exc.SQLAlchemyError as err: - _LOGGER.error("Error executing query %s: %s", self._query, err) + _LOGGER.error( + "Error executing query %s: %s", + self._query, + redact_credentials(str(err)), + ) return finally: sess.close() diff --git a/tests/components/sql/test_sensor.py b/tests/components/sql/test_sensor.py index ddab7b1ba3615..11f59444c2c5d 100644 --- a/tests/components/sql/test_sensor.py +++ b/tests/components/sql/test_sensor.py @@ -55,3 +55,43 @@ async def test_invalid_query(hass): state = hass.states.get("sensor.count_tables") assert state.state == STATE_UNKNOWN + + +@pytest.mark.parametrize( + "url,expected_patterns,not_expected_patterns", + [ + ( + "sqlite://homeassistant:hunter2@homeassistant.local", + ["sqlite://****:****@homeassistant.local"], + ["sqlite://homeassistant:hunter2@homeassistant.local"], + ), + ( + "sqlite://homeassistant.local", + ["sqlite://homeassistant.local"], + [], + ), + ], +) +async def test_invalid_url(hass, caplog, url, expected_patterns, not_expected_patterns): + """Test credentials in url is not logged.""" + config = { + "sensor": { + "platform": "sql", + "db_url": url, + "queries": [ + { + "name": "count_tables", + "query": "SELECT 5 as value", + "column": "value", + } + ], + } + } + + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + for pattern in not_expected_patterns: + assert pattern not in caplog.text + for pattern in expected_patterns: + assert pattern in caplog.text From caaa62a7f956093556779f042e7e013939c0303b Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 7 Apr 2021 06:39:27 -0400 Subject: [PATCH 0102/1317] Clean up google travel time code (#48708) --- homeassistant/components/google_travel_time/__init__.py | 5 ----- tests/components/google_travel_time/conftest.py | 2 -- 2 files changed, 7 deletions(-) diff --git a/homeassistant/components/google_travel_time/__init__.py b/homeassistant/components/google_travel_time/__init__.py index d9afaf46deee0..ef53db9c815c8 100644 --- a/homeassistant/components/google_travel_time/__init__.py +++ b/homeassistant/components/google_travel_time/__init__.py @@ -7,11 +7,6 @@ PLATFORMS = ["sensor"] -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Google Maps Travel Time component.""" - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Google Maps Travel Time from a config entry.""" for component in PLATFORMS: diff --git a/tests/components/google_travel_time/conftest.py b/tests/components/google_travel_time/conftest.py index 3c8d897aadd17..18e16a79e27c1 100644 --- a/tests/components/google_travel_time/conftest.py +++ b/tests/components/google_travel_time/conftest.py @@ -31,8 +31,6 @@ def validate_config_entry_fixture(): def bypass_setup_fixture(): """Bypass entry setup.""" with patch( - "homeassistant.components.google_travel_time.async_setup", return_value=True - ), patch( "homeassistant.components.google_travel_time.async_setup_entry", return_value=True, ): From fa8436889af75469b71752db6c11c4828c77d4e9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 7 Apr 2021 13:20:00 +0200 Subject: [PATCH 0103/1317] Bump actions/upload-artifact from v2.2.2 to v2.2.3 (#48761) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from v2.2.2 to v2.2.3. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v2.2.2...ee69f02b3dfdecd58bb31b4d133da38ba6fe3700) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 49cbb06e71e50..5d43b124c4953 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -699,7 +699,7 @@ jobs: -p no:sugar \ tests - name: Upload coverage artifact - uses: actions/upload-artifact@v2.2.2 + uses: actions/upload-artifact@v2.2.3 with: name: coverage-${{ matrix.python-version }}-group${{ matrix.group }} path: .coverage From c732749640e5952cfaebd21a5795748f830d78db Mon Sep 17 00:00:00 2001 From: Daniel Sack Date: Wed, 7 Apr 2021 15:51:35 +0200 Subject: [PATCH 0104/1317] Update __init__.py (#48659) This change solves that HMIP-RCV-1 is not found when used in a service call to invoke a virtual key (case-sensitivity problem). - https://community.home-assistant.io/t/homematic-hmip-rcv-50-not-working-with-virtual-key-any-more/249000 - https://github.com/danielperna84/pyhomematic/issues/368 --- homeassistant/components/homematic/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 6df738037bfe5..46f3ac6caf204 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -611,6 +611,8 @@ def _device_from_servicecall(hass, service): interface = service.data.get(ATTR_INTERFACE) if address == "BIDCOS-RF": address = "BidCoS-RF" + if address == "HMIP-RCV-1": + address = "HmIP-RCV-1" if interface: return hass.data[DATA_HOMEMATIC].devices[interface].get(address) From f2ef9e7505b9ea4c062ac0b77b5a3c369dfa5fba Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 7 Apr 2021 17:57:45 +0200 Subject: [PATCH 0105/1317] Update frontend to 20210407.1 (#48778) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 79bf552a5e258..b910c0acc468f 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20210407.0" + "home-assistant-frontend==20210407.1" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 65f9e9074eff4..d2e0aa84118ef 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ defusedxml==0.6.0 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.42.0 -home-assistant-frontend==20210407.0 +home-assistant-frontend==20210407.1 httpx==0.17.1 jinja2>=2.11.3 netdisco==2.8.2 diff --git a/requirements_all.txt b/requirements_all.txt index 5c81918801746..c8b86784c529e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -762,7 +762,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210407.0 +home-assistant-frontend==20210407.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9f48f5c892155..33043e18f06c5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -423,7 +423,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210407.0 +home-assistant-frontend==20210407.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 61b38baf2e1ae8a4ee6bf8bd8398c17700321bfa Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 7 Apr 2021 18:00:42 +0200 Subject: [PATCH 0106/1317] Reject nan, inf from generic_thermostat sensor (#48771) --- .../components/generic_thermostat/climate.py | 6 +++++- tests/components/generic_thermostat/test_climate.py | 11 ++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/generic_thermostat/climate.py b/homeassistant/components/generic_thermostat/climate.py index ef3cf11fa1c3c..e83852d122fa0 100644 --- a/homeassistant/components/generic_thermostat/climate.py +++ b/homeassistant/components/generic_thermostat/climate.py @@ -1,6 +1,7 @@ """Adds support for generic thermostat units.""" import asyncio import logging +import math import voluptuous as vol @@ -419,7 +420,10 @@ def _async_switch_changed(self, event): def _async_update_temp(self, state): """Update thermostat with latest state from sensor.""" try: - self._cur_temp = float(state.state) + cur_temp = float(state.state) + if math.isnan(cur_temp) or math.isinf(cur_temp): + raise ValueError(f"Sensor has illegal state {state.state}") + self._cur_temp = cur_temp except ValueError as ex: _LOGGER.error("Unable to update from sensor: %s", ex) diff --git a/tests/components/generic_thermostat/test_climate.py b/tests/components/generic_thermostat/test_climate.py index c2c1435464e34..f5a27ac8b9721 100644 --- a/tests/components/generic_thermostat/test_climate.py +++ b/tests/components/generic_thermostat/test_climate.py @@ -331,9 +331,18 @@ async def test_sensor_bad_value(hass, setup_comp_2): _setup_sensor(hass, None) await hass.async_block_till_done() + state = hass.states.get(ENTITY) + assert state.attributes.get("current_temperature") == temp + _setup_sensor(hass, "inf") + await hass.async_block_till_done() + state = hass.states.get(ENTITY) + assert state.attributes.get("current_temperature") == temp + + _setup_sensor(hass, "nan") + await hass.async_block_till_done() state = hass.states.get(ENTITY) - assert temp == state.attributes.get("current_temperature") + assert state.attributes.get("current_temperature") == temp async def test_sensor_unknown(hass): From cdb151e8c9ffaccaa50e6b51a5496ebf31fb1cba Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 7 Apr 2021 09:27:47 -1000 Subject: [PATCH 0107/1317] Remove doorbird recorder test workaround (#48781) Apparently this is no longer needed --- tests/components/doorbird/test_config_flow.py | 36 +------------------ 1 file changed, 1 insertion(+), 35 deletions(-) diff --git a/tests/components/doorbird/test_config_flow.py b/tests/components/doorbird/test_config_flow.py index d6bbb7412e66c..9fa9752dc659b 100644 --- a/tests/components/doorbird/test_config_flow.py +++ b/tests/components/doorbird/test_config_flow.py @@ -9,7 +9,7 @@ from homeassistant.components.doorbird.const import CONF_EVENTS, DOMAIN from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME -from tests.common import MockConfigEntry, init_recorder_component +from tests.common import MockConfigEntry VALID_CONFIG = { CONF_HOST: "1.2.3.4", @@ -39,10 +39,6 @@ def _get_mock_doorbirdapi_side_effects(ready=None, info=None): async def test_user_form(hass): """Test we get the user form.""" - await hass.async_add_executor_job( - init_recorder_component, hass - ) # force in memory db - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -82,10 +78,6 @@ async def test_user_form(hass): async def test_form_import(hass): """Test we get the form with import source.""" - await hass.async_add_executor_job( - init_recorder_component, hass - ) # force in memory db - await setup.async_setup_component(hass, "persistent_notification", {}) import_config = VALID_CONFIG.copy() @@ -135,10 +127,6 @@ async def test_form_import(hass): async def test_form_import_with_zeroconf_already_discovered(hass): """Test we get the form with import source.""" - await hass.async_add_executor_job( - init_recorder_component, hass - ) # force in memory db - await setup.async_setup_component(hass, "persistent_notification", {}) doorbirdapi = _get_mock_doorbirdapi_return_values( @@ -208,10 +196,6 @@ async def test_form_import_with_zeroconf_already_discovered(hass): async def test_form_zeroconf_wrong_oui(hass): """Test we abort when we get the wrong OUI via zeroconf.""" - await hass.async_add_executor_job( - init_recorder_component, hass - ) # force in memory db - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( @@ -229,10 +213,6 @@ async def test_form_zeroconf_wrong_oui(hass): async def test_form_zeroconf_link_local_ignored(hass): """Test we abort when we get a link local address via zeroconf.""" - await hass.async_add_executor_job( - init_recorder_component, hass - ) # force in memory db - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( @@ -250,9 +230,6 @@ async def test_form_zeroconf_link_local_ignored(hass): async def test_form_zeroconf_correct_oui(hass): """Test we can setup from zeroconf with the correct OUI source.""" - await hass.async_add_executor_job( - init_recorder_component, hass - ) # force in memory db doorbirdapi = _get_mock_doorbirdapi_return_values( ready=[True], info={"WIFI_MAC_ADDR": "macaddr"} ) @@ -312,9 +289,6 @@ async def test_form_zeroconf_correct_oui(hass): ) async def test_form_zeroconf_correct_oui_wrong_device(hass, doorbell_state_side_effect): """Test we can setup from zeroconf with the correct OUI source but not a doorstation.""" - await hass.async_add_executor_job( - init_recorder_component, hass - ) # force in memory db doorbirdapi = _get_mock_doorbirdapi_return_values( ready=[True], info={"WIFI_MAC_ADDR": "macaddr"} ) @@ -341,10 +315,6 @@ async def test_form_zeroconf_correct_oui_wrong_device(hass, doorbell_state_side_ async def test_form_user_cannot_connect(hass): """Test we handle cannot connect error.""" - await hass.async_add_executor_job( - init_recorder_component, hass - ) # force in memory db - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -365,10 +335,6 @@ async def test_form_user_cannot_connect(hass): async def test_form_user_invalid_auth(hass): """Test we handle cannot invalid auth error.""" - await hass.async_add_executor_job( - init_recorder_component, hass - ) # force in memory db - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) From 8e6238ff611b8f57b45db3ccdbbd144e5e2dc66c Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 8 Apr 2021 00:03:23 +0000 Subject: [PATCH 0108/1317] [ci skip] Translation update --- .../components/broadlink/translations/nl.json | 4 +- .../components/climacell/translations/de.json | 1 + .../components/climacell/translations/it.json | 1 + .../components/cover/translations/de.json | 3 +- .../components/deconz/translations/it.json | 4 ++ .../components/emonitor/translations/de.json | 18 +++++++++ .../components/emonitor/translations/it.json | 23 +++++++++++ .../enphase_envoy/translations/de.json | 22 +++++++++++ .../enphase_envoy/translations/it.json | 22 +++++++++++ .../google_travel_time/translations/de.json | 28 ++++++++++++++ .../components/icloud/translations/ru.json | 2 +- .../kostal_plenticore/translations/ca.json | 21 ++++++++++ .../kostal_plenticore/translations/de.json | 20 ++++++++++ .../kostal_plenticore/translations/en.json | 1 - .../kostal_plenticore/translations/et.json | 21 ++++++++++ .../kostal_plenticore/translations/it.json | 21 ++++++++++ .../kostal_plenticore/translations/nl.json | 21 ++++++++++ .../kostal_plenticore/translations/no.json | 21 ++++++++++ .../kostal_plenticore/translations/pl.json | 21 ++++++++++ .../kostal_plenticore/translations/ru.json | 21 ++++++++++ .../translations/zh-Hant.json | 21 ++++++++++ .../components/met/translations/ca.json | 3 ++ .../components/met/translations/en.json | 3 ++ .../components/met/translations/et.json | 3 ++ .../components/met/translations/it.json | 3 ++ .../components/met/translations/nl.json | 3 ++ .../components/met/translations/no.json | 3 ++ .../components/met/translations/pl.json | 3 ++ .../components/met/translations/ru.json | 3 ++ .../components/met/translations/zh-Hant.json | 3 ++ .../met_eireann/translations/de.json | 18 +++++++++ .../met_eireann/translations/it.json | 19 ++++++++++ .../components/nest/translations/ru.json | 2 +- .../components/nuki/translations/de.json | 9 +++++ .../components/nuki/translations/it.json | 10 +++++ .../components/nuki/translations/nl.json | 10 +++++ .../components/nuki/translations/no.json | 10 +++++ .../components/nuki/translations/ru.json | 10 +++++ .../components/nuki/translations/zh-Hant.json | 10 +++++ .../simplisafe/translations/ru.json | 2 +- .../components/sonarr/translations/ru.json | 2 +- .../components/spotify/translations/ru.json | 2 +- .../totalconnect/translations/ru.json | 2 +- .../waze_travel_time/translations/de.json | 19 ++++++++++ .../waze_travel_time/translations/it.json | 38 +++++++++++++++++++ .../waze_travel_time/translations/nl.json | 9 ++++- .../components/withings/translations/nl.json | 2 +- .../components/withings/translations/ru.json | 2 +- .../wolflink/translations/sensor.nl.json | 4 +- 49 files changed, 508 insertions(+), 16 deletions(-) create mode 100644 homeassistant/components/emonitor/translations/de.json create mode 100644 homeassistant/components/emonitor/translations/it.json create mode 100644 homeassistant/components/enphase_envoy/translations/de.json create mode 100644 homeassistant/components/enphase_envoy/translations/it.json create mode 100644 homeassistant/components/google_travel_time/translations/de.json create mode 100644 homeassistant/components/kostal_plenticore/translations/ca.json create mode 100644 homeassistant/components/kostal_plenticore/translations/de.json create mode 100644 homeassistant/components/kostal_plenticore/translations/et.json create mode 100644 homeassistant/components/kostal_plenticore/translations/it.json create mode 100644 homeassistant/components/kostal_plenticore/translations/nl.json create mode 100644 homeassistant/components/kostal_plenticore/translations/no.json create mode 100644 homeassistant/components/kostal_plenticore/translations/pl.json create mode 100644 homeassistant/components/kostal_plenticore/translations/ru.json create mode 100644 homeassistant/components/kostal_plenticore/translations/zh-Hant.json create mode 100644 homeassistant/components/met_eireann/translations/de.json create mode 100644 homeassistant/components/met_eireann/translations/it.json create mode 100644 homeassistant/components/waze_travel_time/translations/de.json create mode 100644 homeassistant/components/waze_travel_time/translations/it.json diff --git a/homeassistant/components/broadlink/translations/nl.json b/homeassistant/components/broadlink/translations/nl.json index 06c26235d0a0c..da75118d5b1cc 100644 --- a/homeassistant/components/broadlink/translations/nl.json +++ b/homeassistant/components/broadlink/translations/nl.json @@ -25,14 +25,14 @@ "title": "Kies een naam voor het apparaat" }, "reset": { - "description": "{name} ( {model} op {host} ) is vergrendeld. U moet het apparaat ontgrendelen om te verifi\u00ebren en de configuratie te voltooien. Instructies:\n 1. Open de Broadlink-app.\n 2. Klik op het apparaat.\n 3. Klik op '...' in de rechterbovenhoek.\n 4. Scrol naar de onderkant van de pagina.\n 5. Schakel het slot uit.", + "description": "{name} ({model} op {host}) is vergrendeld. U moet het apparaat ontgrendelen om te verifi\u00ebren en de configuratie te voltooien. Instructies:\n 1. Open de Broadlink-app.\n 2. Klik op het apparaat.\n 3. Klik op '...' in de rechterbovenhoek.\n 4. Scrol naar de onderkant van de pagina.\n 5. Schakel het slot uit.", "title": "Ontgrendel het apparaat" }, "unlock": { "data": { "unlock": "Ja, doe het." }, - "description": "{name} ( {model} op {host} ) is vergrendeld. Dit kan leiden tot authenticatieproblemen in Home Assistant. Wilt u deze ontgrendelen?", + "description": "{name} ({model} op {host}) is vergrendeld. Dit kan leiden tot authenticatieproblemen in Home Assistant. Wilt u deze ontgrendelen?", "title": "Ontgrendel het apparaat (optioneel)" }, "user": { diff --git a/homeassistant/components/climacell/translations/de.json b/homeassistant/components/climacell/translations/de.json index 7ec41d017337c..e53b96d8e731c 100644 --- a/homeassistant/components/climacell/translations/de.json +++ b/homeassistant/components/climacell/translations/de.json @@ -10,6 +10,7 @@ "user": { "data": { "api_key": "API-Schl\u00fcssel", + "api_version": "API Version", "latitude": "Breitengrad", "longitude": "L\u00e4ngengrad", "name": "Name" diff --git a/homeassistant/components/climacell/translations/it.json b/homeassistant/components/climacell/translations/it.json index cc7df4f8ab380..bbd8e33d30502 100644 --- a/homeassistant/components/climacell/translations/it.json +++ b/homeassistant/components/climacell/translations/it.json @@ -10,6 +10,7 @@ "user": { "data": { "api_key": "Chiave API", + "api_version": "Versione API", "latitude": "Latitudine", "longitude": "Logitudine", "name": "Nome" diff --git a/homeassistant/components/cover/translations/de.json b/homeassistant/components/cover/translations/de.json index a90ec822adc36..bf320e07f9e5a 100644 --- a/homeassistant/components/cover/translations/de.json +++ b/homeassistant/components/cover/translations/de.json @@ -6,7 +6,8 @@ "open": "\u00d6ffne {entity_name}", "open_tilt": "{entity_name} gekippt \u00f6ffnen", "set_position": "Position von {entity_name} setzen", - "set_tilt_position": "Neigeposition von {entity_name} einstellen" + "set_tilt_position": "Neigeposition von {entity_name} einstellen", + "stop": "Stoppen {entity_name}" }, "condition_type": { "is_closed": "{entity_name} ist geschlossen", diff --git a/homeassistant/components/deconz/translations/it.json b/homeassistant/components/deconz/translations/it.json index 1c5d02de09078..cb445ac4f764b 100644 --- a/homeassistant/components/deconz/translations/it.json +++ b/homeassistant/components/deconz/translations/it.json @@ -42,6 +42,10 @@ "button_2": "Secondo pulsante", "button_3": "Terzo pulsante", "button_4": "Quarto pulsante", + "button_5": "Quinto pulsante", + "button_6": "Sesto pulsante", + "button_7": "Settimo pulsante", + "button_8": "Ottavo pulsante", "close": "Chiudere", "dim_down": "Diminuire luminosit\u00e0", "dim_up": "Aumentare luminosit\u00e0", diff --git a/homeassistant/components/emonitor/translations/de.json b/homeassistant/components/emonitor/translations/de.json new file mode 100644 index 0000000000000..6abbe1b2b276a --- /dev/null +++ b/homeassistant/components/emonitor/translations/de.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/emonitor/translations/it.json b/homeassistant/components/emonitor/translations/it.json new file mode 100644 index 0000000000000..7a194a301a54b --- /dev/null +++ b/homeassistant/components/emonitor/translations/it.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "unknown": "Errore imprevisto" + }, + "flow_title": "SiteSage {name}", + "step": { + "confirm": { + "description": "Vuoi impostare {name} ({host})?", + "title": "Imposta SiteSage Emonitor" + }, + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/de.json b/homeassistant/components/enphase_envoy/translations/de.json new file mode 100644 index 0000000000000..c3c916f31f709 --- /dev/null +++ b/homeassistant/components/enphase_envoy/translations/de.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "flow_title": "Envoy {serial} ({host})", + "step": { + "user": { + "data": { + "host": "Host", + "password": "Passwort", + "username": "Benutzername" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/it.json b/homeassistant/components/enphase_envoy/translations/it.json new file mode 100644 index 0000000000000..18eab778b340f --- /dev/null +++ b/homeassistant/components/enphase_envoy/translations/it.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "flow_title": "Envoy {serial} ({host})", + "step": { + "user": { + "data": { + "host": "Host", + "password": "Password", + "username": "Nome utente" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_travel_time/translations/de.json b/homeassistant/components/google_travel_time/translations/de.json new file mode 100644 index 0000000000000..c2a95e49afbd8 --- /dev/null +++ b/homeassistant/components/google_travel_time/translations/de.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Standort ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "step": { + "user": { + "data": { + "api_key": "API-Schl\u00fcssel", + "destination": "Zielort", + "origin": "Startort" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "language": "Sprache" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/translations/ru.json b/homeassistant/components/icloud/translations/ru.json index bdd6fe776ad42..f3f856302157e 100644 --- a/homeassistant/components/icloud/translations/ru.json +++ b/homeassistant/components/icloud/translations/ru.json @@ -16,7 +16,7 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u044c" }, "description": "\u0420\u0430\u043d\u0435\u0435 \u0432\u0432\u0435\u0434\u0435\u043d\u043d\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f {username} \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442. \u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0439\u0442\u0435\u0441\u044c, \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u044c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u044d\u0442\u0443 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044e.", - "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0444\u0438\u043b\u044f" + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" }, "trusted_device": { "data": { diff --git a/homeassistant/components/kostal_plenticore/translations/ca.json b/homeassistant/components/kostal_plenticore/translations/ca.json new file mode 100644 index 0000000000000..2ce39d904a6ab --- /dev/null +++ b/homeassistant/components/kostal_plenticore/translations/ca.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "password": "Contrasenya" + } + } + } + }, + "title": "Inversor solar Kostal Plenticore" +} \ No newline at end of file diff --git a/homeassistant/components/kostal_plenticore/translations/de.json b/homeassistant/components/kostal_plenticore/translations/de.json new file mode 100644 index 0000000000000..095487fff3f32 --- /dev/null +++ b/homeassistant/components/kostal_plenticore/translations/de.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Passwort" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kostal_plenticore/translations/en.json b/homeassistant/components/kostal_plenticore/translations/en.json index f9aafb90c270d..a058336b077f1 100644 --- a/homeassistant/components/kostal_plenticore/translations/en.json +++ b/homeassistant/components/kostal_plenticore/translations/en.json @@ -5,7 +5,6 @@ }, "error": { "cannot_connect": "Failed to connect", - "timeout": "Timeout/No answer", "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, diff --git a/homeassistant/components/kostal_plenticore/translations/et.json b/homeassistant/components/kostal_plenticore/translations/et.json new file mode 100644 index 0000000000000..c96935d5db86e --- /dev/null +++ b/homeassistant/components/kostal_plenticore/translations/et.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamise viga", + "unknown": "Tundmatu viga" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Salas\u00f5na" + } + } + } + }, + "title": "" +} \ No newline at end of file diff --git a/homeassistant/components/kostal_plenticore/translations/it.json b/homeassistant/components/kostal_plenticore/translations/it.json new file mode 100644 index 0000000000000..8e46b765fe041 --- /dev/null +++ b/homeassistant/components/kostal_plenticore/translations/it.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Password" + } + } + } + }, + "title": "Inverter solare Kostal Plenticore" +} \ No newline at end of file diff --git a/homeassistant/components/kostal_plenticore/translations/nl.json b/homeassistant/components/kostal_plenticore/translations/nl.json new file mode 100644 index 0000000000000..83a77fb6e0d80 --- /dev/null +++ b/homeassistant/components/kostal_plenticore/translations/nl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Wachtwoord" + } + } + } + }, + "title": "Kostal Plenticore omvormer voor zonne-energie" +} \ No newline at end of file diff --git a/homeassistant/components/kostal_plenticore/translations/no.json b/homeassistant/components/kostal_plenticore/translations/no.json new file mode 100644 index 0000000000000..0f0d77a83e665 --- /dev/null +++ b/homeassistant/components/kostal_plenticore/translations/no.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "host": "Vert", + "password": "Passord" + } + } + } + }, + "title": "Kostal Plenticore Solar Inverter" +} \ No newline at end of file diff --git a/homeassistant/components/kostal_plenticore/translations/pl.json b/homeassistant/components/kostal_plenticore/translations/pl.json new file mode 100644 index 0000000000000..781bddfc979ae --- /dev/null +++ b/homeassistant/components/kostal_plenticore/translations/pl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "password": "Has\u0142o" + } + } + } + }, + "title": "Inwerter solarny Kostal Plenticore" +} \ No newline at end of file diff --git a/homeassistant/components/kostal_plenticore/translations/ru.json b/homeassistant/components/kostal_plenticore/translations/ru.json new file mode 100644 index 0000000000000..d272fd0f304d2 --- /dev/null +++ b/homeassistant/components/kostal_plenticore/translations/ru.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + } + } + } + }, + "title": "Kostal Plenticore Solar Inverter" +} \ No newline at end of file diff --git a/homeassistant/components/kostal_plenticore/translations/zh-Hant.json b/homeassistant/components/kostal_plenticore/translations/zh-Hant.json new file mode 100644 index 0000000000000..b1fef7a714316 --- /dev/null +++ b/homeassistant/components/kostal_plenticore/translations/zh-Hant.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "password": "\u5bc6\u78bc" + } + } + } + }, + "title": "Kostal Plenticore \u592a\u967d\u80fd\u63db\u6d41\u5668" +} \ No newline at end of file diff --git a/homeassistant/components/met/translations/ca.json b/homeassistant/components/met/translations/ca.json index 11815222df5ce..7b227fd8df04a 100644 --- a/homeassistant/components/met/translations/ca.json +++ b/homeassistant/components/met/translations/ca.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_home": "No s'han configurat coordenades d'ubicaci\u00f3 principal en la configuraci\u00f3 de Home Assistant" + }, "error": { "already_configured": "El servei ja est\u00e0 configurat" }, diff --git a/homeassistant/components/met/translations/en.json b/homeassistant/components/met/translations/en.json index 590bf48e635fb..498c23aa3282d 100644 --- a/homeassistant/components/met/translations/en.json +++ b/homeassistant/components/met/translations/en.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_home": "No home coordinates are set in the Home Assistant configuration" + }, "error": { "already_configured": "Service is already configured" }, diff --git a/homeassistant/components/met/translations/et.json b/homeassistant/components/met/translations/et.json index d25ca8df0a51b..81155c80d549e 100644 --- a/homeassistant/components/met/translations/et.json +++ b/homeassistant/components/met/translations/et.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_home": "Home Assistanti s\u00e4tetes pole kodu koordinaate m\u00e4\u00e4ratud" + }, "error": { "already_configured": "Teenus on juba h\u00e4\u00e4lestatud" }, diff --git a/homeassistant/components/met/translations/it.json b/homeassistant/components/met/translations/it.json index 2a00b31eedb33..9ff994a2aeabc 100644 --- a/homeassistant/components/met/translations/it.json +++ b/homeassistant/components/met/translations/it.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_home": "Nessuna coordinata di casa \u00e8 impostata nella configurazione di Home Assistant" + }, "error": { "already_configured": "Il servizio \u00e8 gi\u00e0 configurato" }, diff --git a/homeassistant/components/met/translations/nl.json b/homeassistant/components/met/translations/nl.json index 108c2a44f6631..7c3d03fdb1ff3 100644 --- a/homeassistant/components/met/translations/nl.json +++ b/homeassistant/components/met/translations/nl.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_home": "Er zijn geen thuisco\u00f6rdinaten ingesteld in de Home Assistant-configuratie" + }, "error": { "already_configured": "Service is al geconfigureerd" }, diff --git a/homeassistant/components/met/translations/no.json b/homeassistant/components/met/translations/no.json index 05ba5b0c9d914..b2fabd10a1c99 100644 --- a/homeassistant/components/met/translations/no.json +++ b/homeassistant/components/met/translations/no.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_home": "Ingen hjemmekoordinater er angitt i Home Assistant-konfigurasjonen" + }, "error": { "already_configured": "Tjenesten er allerede konfigurert" }, diff --git a/homeassistant/components/met/translations/pl.json b/homeassistant/components/met/translations/pl.json index 7b357f6b7eb37..1e7cf1ac67e7e 100644 --- a/homeassistant/components/met/translations/pl.json +++ b/homeassistant/components/met/translations/pl.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_home": "Nie ustawiono wsp\u00f3\u0142rz\u0119dnych domu w konfiguracji Home Assistant" + }, "error": { "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana" }, diff --git a/homeassistant/components/met/translations/ru.json b/homeassistant/components/met/translations/ru.json index f28ce7f28131f..6dc9a667a8b42 100644 --- a/homeassistant/components/met/translations/ru.json +++ b/homeassistant/components/met/translations/ru.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_home": "\u0412 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 Home Assistant \u043d\u0435 \u0443\u043a\u0430\u0437\u0430\u043d\u044b \u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u044b \u0440\u0430\u0441\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0434\u043e\u043c\u0430." + }, "error": { "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." }, diff --git a/homeassistant/components/met/translations/zh-Hant.json b/homeassistant/components/met/translations/zh-Hant.json index d5cba312536a0..e4b2c65e7015f 100644 --- a/homeassistant/components/met/translations/zh-Hant.json +++ b/homeassistant/components/met/translations/zh-Hant.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_home": "Home Assistant \u672a\u8a2d\u5b9a\u4f4f\u5bb6\u5ea7\u6a19" + }, "error": { "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, diff --git a/homeassistant/components/met_eireann/translations/de.json b/homeassistant/components/met_eireann/translations/de.json new file mode 100644 index 0000000000000..0d979ed800b95 --- /dev/null +++ b/homeassistant/components/met_eireann/translations/de.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "already_configured": "Der Dienst ist bereits konfiguriert" + }, + "step": { + "user": { + "data": { + "elevation": "H\u00f6he", + "latitude": "Breitengrad", + "longitude": "L\u00e4ngengrad", + "name": "Name" + }, + "title": "Standort" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/met_eireann/translations/it.json b/homeassistant/components/met_eireann/translations/it.json new file mode 100644 index 0000000000000..2d89c6983af5d --- /dev/null +++ b/homeassistant/components/met_eireann/translations/it.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato" + }, + "step": { + "user": { + "data": { + "elevation": "Altitudine", + "latitude": "Latitudine", + "longitude": "Logitudine", + "name": "Nome" + }, + "description": "Inserisci la tua posizione per utilizzare i dati meteorologici dall'API Met \u00c9ireann Public Weather Forecast", + "title": "Posizione" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/translations/ru.json b/homeassistant/components/nest/translations/ru.json index 07ac5246cbfa1..0763b68a1becf 100644 --- a/homeassistant/components/nest/translations/ru.json +++ b/homeassistant/components/nest/translations/ru.json @@ -38,7 +38,7 @@ }, "reauth_confirm": { "description": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Nest", - "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0444\u0438\u043b\u044f" + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" } } }, diff --git a/homeassistant/components/nuki/translations/de.json b/homeassistant/components/nuki/translations/de.json index 30d7e6865cdc8..ae1322d7641be 100644 --- a/homeassistant/components/nuki/translations/de.json +++ b/homeassistant/components/nuki/translations/de.json @@ -1,11 +1,20 @@ { "config": { + "abort": { + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, "step": { + "reauth_confirm": { + "data": { + "token": "Zugangstoken" + }, + "title": "Integration erneut authentifizieren" + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/nuki/translations/it.json b/homeassistant/components/nuki/translations/it.json index 899093e1f4182..eaf0a8e52e4e2 100644 --- a/homeassistant/components/nuki/translations/it.json +++ b/homeassistant/components/nuki/translations/it.json @@ -1,11 +1,21 @@ { "config": { + "abort": { + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" + }, "error": { "cannot_connect": "Impossibile connettersi", "invalid_auth": "Autenticazione non valida", "unknown": "Errore imprevisto" }, "step": { + "reauth_confirm": { + "data": { + "token": "Token di accesso" + }, + "description": "L'integrazione Nuki deve essere nuovamente autenticata con il tuo bridge.", + "title": "Autenticare nuovamente l'integrazione" + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/nuki/translations/nl.json b/homeassistant/components/nuki/translations/nl.json index 4e220dbe78d79..3bfa8f60b7021 100644 --- a/homeassistant/components/nuki/translations/nl.json +++ b/homeassistant/components/nuki/translations/nl.json @@ -1,11 +1,21 @@ { "config": { + "abort": { + "reauth_successful": "Herauthenticatie was succesvol" + }, "error": { "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" }, "step": { + "reauth_confirm": { + "data": { + "token": "Toegangstoken" + }, + "description": "De Nuki integratie moet opnieuw authenticeren met je bridge.", + "title": "Verifieer de integratie opnieuw" + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/nuki/translations/no.json b/homeassistant/components/nuki/translations/no.json index 8cdbac230d735..1ae4eb0362442 100644 --- a/homeassistant/components/nuki/translations/no.json +++ b/homeassistant/components/nuki/translations/no.json @@ -1,11 +1,21 @@ { "config": { + "abort": { + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + }, "error": { "cannot_connect": "Tilkobling mislyktes", "invalid_auth": "Ugyldig godkjenning", "unknown": "Uventet feil" }, "step": { + "reauth_confirm": { + "data": { + "token": "Tilgangstoken" + }, + "description": "Nuki-integrasjonen m\u00e5 godkjennes p\u00e5 nytt med broen din.", + "title": "Godkjenne integrering p\u00e5 nytt" + }, "user": { "data": { "host": "Vert", diff --git a/homeassistant/components/nuki/translations/ru.json b/homeassistant/components/nuki/translations/ru.json index a7fe1c61f5b62..a39f1429e140c 100644 --- a/homeassistant/components/nuki/translations/ru.json +++ b/homeassistant/components/nuki/translations/ru.json @@ -1,11 +1,21 @@ { "config": { + "abort": { + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { + "reauth_confirm": { + "data": { + "token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430" + }, + "description": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0434\u043b\u044f \u0448\u043b\u044e\u0437\u0430 Nuki.", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442", diff --git a/homeassistant/components/nuki/translations/zh-Hant.json b/homeassistant/components/nuki/translations/zh-Hant.json index 4bf21552952d2..fb486faced1a6 100644 --- a/homeassistant/components/nuki/translations/zh-Hant.json +++ b/homeassistant/components/nuki/translations/zh-Hant.json @@ -1,11 +1,21 @@ { "config": { + "abort": { + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" + }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { + "reauth_confirm": { + "data": { + "token": "\u5b58\u53d6\u6b0a\u6756" + }, + "description": "Nuki \u6574\u5408\u9700\u8981\u91cd\u65b0\u8a8d\u8b49 Bridge\u3002", + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" + }, "user": { "data": { "host": "\u4e3b\u6a5f\u7aef", diff --git a/homeassistant/components/simplisafe/translations/ru.json b/homeassistant/components/simplisafe/translations/ru.json index abe0542c926b1..bcfffc575336f 100644 --- a/homeassistant/components/simplisafe/translations/ru.json +++ b/homeassistant/components/simplisafe/translations/ru.json @@ -20,7 +20,7 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u044c" }, "description": "\u0412\u0430\u0448 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0438\u0441\u0442\u0435\u043a \u0438\u043b\u0438 \u0431\u044b\u043b \u0430\u043d\u043d\u0443\u043b\u0438\u0440\u043e\u0432\u0430\u043d. \u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u044c, \u0447\u0442\u043e\u0431\u044b \u0437\u0430\u043d\u043e\u0432\u043e \u043f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c.", - "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0444\u0438\u043b\u044f" + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" }, "user": { "data": { diff --git a/homeassistant/components/sonarr/translations/ru.json b/homeassistant/components/sonarr/translations/ru.json index 75d23cd3ec0ed..6bbb204b69e3f 100644 --- a/homeassistant/components/sonarr/translations/ru.json +++ b/homeassistant/components/sonarr/translations/ru.json @@ -13,7 +13,7 @@ "step": { "reauth_confirm": { "description": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e API Sonarr \u043f\u043e \u0430\u0434\u0440\u0435\u0441\u0443: {host}", - "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0444\u0438\u043b\u044f" + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" }, "user": { "data": { diff --git a/homeassistant/components/spotify/translations/ru.json b/homeassistant/components/spotify/translations/ru.json index 722cb12516922..bac888937a5b9 100644 --- a/homeassistant/components/spotify/translations/ru.json +++ b/homeassistant/components/spotify/translations/ru.json @@ -15,7 +15,7 @@ }, "reauth_confirm": { "description": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0432 Spotify \u0434\u043b\u044f \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438: {account}", - "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0444\u0438\u043b\u044f" + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" } } }, diff --git a/homeassistant/components/totalconnect/translations/ru.json b/homeassistant/components/totalconnect/translations/ru.json index a32f92b7b580d..a4e48ca01d4f0 100644 --- a/homeassistant/components/totalconnect/translations/ru.json +++ b/homeassistant/components/totalconnect/translations/ru.json @@ -18,7 +18,7 @@ }, "reauth_confirm": { "description": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Total Connect", - "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0444\u0438\u043b\u044f" + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" }, "user": { "data": { diff --git a/homeassistant/components/waze_travel_time/translations/de.json b/homeassistant/components/waze_travel_time/translations/de.json new file mode 100644 index 0000000000000..f5586b3d80d95 --- /dev/null +++ b/homeassistant/components/waze_travel_time/translations/de.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Standort ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "step": { + "user": { + "data": { + "destination": "Zielort", + "origin": "Startort", + "region": "Region" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/waze_travel_time/translations/it.json b/homeassistant/components/waze_travel_time/translations/it.json new file mode 100644 index 0000000000000..ce109b3751cf0 --- /dev/null +++ b/homeassistant/components/waze_travel_time/translations/it.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "La posizione \u00e8 gi\u00e0 configurata" + }, + "error": { + "cannot_connect": "Impossibile connettersi" + }, + "step": { + "user": { + "data": { + "destination": "Destinazione", + "origin": "Origine", + "region": "Area geografica" + }, + "description": "Per Origine e Destinazione, inserisci l'indirizzo o le coordinate GPS della posizione (le coordinate GPS devono essere separate da una virgola). Puoi anche inserire un ID entit\u00e0 che fornisce queste informazioni nel suo stato, un ID entit\u00e0 con attributi di latitudine e longitudine o il nome assegnato alla zona." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "avoid_ferries": "Evitare i traghetti?", + "avoid_subscription_roads": "Evitare le strade che richiedono una vignetta/abbonamento?", + "avoid_toll_roads": "Evitare le strade a pedaggio?", + "excl_filter": "Sottostringa NON nella descrizione del percorso selezionato", + "incl_filter": "Sottostringa nella descrizione del percorso selezionato", + "realtime": "Tempo di viaggio in tempo reale?", + "units": "Unit\u00e0", + "vehicle_type": "Tipo di veicolo" + }, + "description": "Gli input 'sottostringa' consentono di forzare l'integrazione a utilizzare o evitare un percorso particolare nel calcolo del tempo di viaggio." + } + } + }, + "title": "Tempo di viaggio di Waze" +} \ No newline at end of file diff --git a/homeassistant/components/waze_travel_time/translations/nl.json b/homeassistant/components/waze_travel_time/translations/nl.json index ecf7db3a13ee2..4d7bee4ad4db3 100644 --- a/homeassistant/components/waze_travel_time/translations/nl.json +++ b/homeassistant/components/waze_travel_time/translations/nl.json @@ -9,8 +9,11 @@ "step": { "user": { "data": { - "destination": "Bestemming" - } + "destination": "Bestemming", + "origin": "Vertrekpunt", + "region": "Regio" + }, + "description": "Voor Vertrekpunt en Bestemming voert u het adres of de GPS-co\u00f6rdinaten van de locatie in (GPS-co\u00f6rdinaten moeten worden gescheiden door een komma). U kunt ook een entiteits-id invoeren die deze informatie in zijn status geeft, een entiteits-id met lengte- en breedtegraadattributen, of een zone-vriendelijke naam." } } }, @@ -18,6 +21,8 @@ "step": { "init": { "data": { + "avoid_ferries": "Veerboten vermijden?", + "avoid_subscription_roads": "Vermijd wegen die een vignet / abonnement nodig hebben?", "avoid_toll_roads": "Tolwegen vermijden?", "excl_filter": "Substring NIET in beschrijving van geselecteerde route", "incl_filter": "Substring in beschrijving van geselecteerde route", diff --git a/homeassistant/components/withings/translations/nl.json b/homeassistant/components/withings/translations/nl.json index b20323347e4aa..23e110a1d60bd 100644 --- a/homeassistant/components/withings/translations/nl.json +++ b/homeassistant/components/withings/translations/nl.json @@ -25,7 +25,7 @@ "title": "Gebruikersprofiel." }, "reauth": { - "description": "Het \"{profile}\" profiel moet opnieuw worden geverifieerd om Withings gegevens te kunnen blijven ontvangen.", + "description": "Het {profile} \" moet opnieuw worden geverifieerd om Withings-gegevens te blijven ontvangen.", "title": "Verifieer de integratie opnieuw" } } diff --git a/homeassistant/components/withings/translations/ru.json b/homeassistant/components/withings/translations/ru.json index f493fa4594fd4..d8cfd6c0b3b3b 100644 --- a/homeassistant/components/withings/translations/ru.json +++ b/homeassistant/components/withings/translations/ru.json @@ -26,7 +26,7 @@ }, "reauth": { "description": "\u041f\u0440\u043e\u0444\u0438\u043b\u044c \"{profile}\" \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u0446\u0438\u0440\u043e\u0432\u0430\u043d \u0434\u043b\u044f \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0435\u043d\u0438\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0434\u0430\u043d\u043d\u044b\u0445 Withings.", - "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0444\u0438\u043b\u044f" + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" } } } diff --git a/homeassistant/components/wolflink/translations/sensor.nl.json b/homeassistant/components/wolflink/translations/sensor.nl.json index f050fe4f62963..304a4fd6f2799 100644 --- a/homeassistant/components/wolflink/translations/sensor.nl.json +++ b/homeassistant/components/wolflink/translations/sensor.nl.json @@ -16,7 +16,7 @@ "automatik_aus": "Automatisch UIT", "automatik_ein": "Automatisch AAN", "bereit_keine_ladung": "Klaar, niet laden", - "betrieb_ohne_brenner": "Werken zonder brander", + "betrieb_ohne_brenner": "Werkend zonder brander", "cooling": "Koelen", "deaktiviert": "Inactief", "dhw_prior": "DHWPrior", @@ -25,7 +25,7 @@ "estrichtrocknung": "Dekvloer drogen", "externe_deaktivierung": "Externe uitschakeling", "fernschalter_ein": "Op afstand bedienen ingeschakeld", - "frost_heizkreis": "Verwarmengscircuit ontdooien", + "frost_heizkreis": "Verwarmingscircuit ontdooien", "frost_warmwasser": "DHW vorst", "frostschutz": "Vorstbescherming", "gasdruck": "Gasdruk", From e70d7327f95bae584bb7c6bf2b89101ac012ff44 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 7 Apr 2021 23:34:47 -0600 Subject: [PATCH 0109/1317] Store Recollect Waste pickup dates in UTC (#48690) * Store Recollect Waste pickup dates in UTC * Code review * Code review --- .../components/recollect_waste/manifest.json | 2 +- .../components/recollect_waste/sensor.py | 24 +++++++++++-------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/recollect_waste/manifest.json b/homeassistant/components/recollect_waste/manifest.json index dc8a85ce2aacd..4e7568a3fff30 100644 --- a/homeassistant/components/recollect_waste/manifest.json +++ b/homeassistant/components/recollect_waste/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/recollect_waste", "requirements": [ - "aiorecollect==1.0.1" + "aiorecollect==1.0.4" ], "codeowners": [ "@bachya" diff --git a/homeassistant/components/recollect_waste/sensor.py b/homeassistant/components/recollect_waste/sensor.py index 1c3dabc2c87ce..b95f1d6e8fa21 100644 --- a/homeassistant/components/recollect_waste/sensor.py +++ b/homeassistant/components/recollect_waste/sensor.py @@ -8,13 +8,19 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import ATTR_ATTRIBUTION, CONF_FRIENDLY_NAME, CONF_NAME +from homeassistant.const import ( + ATTR_ATTRIBUTION, + CONF_FRIENDLY_NAME, + CONF_NAME, + DEVICE_CLASS_TIMESTAMP, +) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, ) +from homeassistant.util.dt import as_utc from .const import CONF_PLACE_ID, CONF_SERVICE_ID, DATA_COORDINATOR, DOMAIN, LOGGER @@ -25,7 +31,6 @@ DEFAULT_ATTRIBUTION = "Pickup data provided by ReCollect Waste" DEFAULT_NAME = "recollect_waste" -DEFAULT_ICON = "mdi:trash-can-outline" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -87,16 +92,16 @@ def __init__(self, coordinator: DataUpdateCoordinator, entry: ConfigEntry) -> No self._entry = entry self._state = None + @property + def device_class(self) -> dict: + """Return the device class.""" + return DEVICE_CLASS_TIMESTAMP + @property def extra_state_attributes(self) -> dict: """Return the state attributes.""" return self._attributes - @property - def icon(self) -> str: - """Icon to use in the frontend.""" - return DEFAULT_ICON - @property def name(self) -> str: """Return the name of the sensor.""" @@ -128,9 +133,8 @@ def update_from_latest_data(self) -> None: """Update the state.""" pickup_event = self.coordinator.data[0] next_pickup_event = self.coordinator.data[1] - next_date = str(next_pickup_event.date) - self._state = pickup_event.date + self._state = as_utc(pickup_event.date).isoformat() self._attributes.update( { ATTR_PICKUP_TYPES: async_get_pickup_type_names( @@ -140,6 +144,6 @@ def update_from_latest_data(self) -> None: ATTR_NEXT_PICKUP_TYPES: async_get_pickup_type_names( self._entry, next_pickup_event.pickup_types ), - ATTR_NEXT_PICKUP_DATE: next_date, + ATTR_NEXT_PICKUP_DATE: as_utc(next_pickup_event.date).isoformat(), } ) diff --git a/requirements_all.txt b/requirements_all.txt index c8b86784c529e..e0ef50debbbda 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -224,7 +224,7 @@ aiopvpc==2.0.2 aiopylgtv==0.4.0 # homeassistant.components.recollect_waste -aiorecollect==1.0.1 +aiorecollect==1.0.4 # homeassistant.components.shelly aioshelly==0.6.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 33043e18f06c5..4520ee3893a99 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -146,7 +146,7 @@ aiopvpc==2.0.2 aiopylgtv==0.4.0 # homeassistant.components.recollect_waste -aiorecollect==1.0.1 +aiorecollect==1.0.4 # homeassistant.components.shelly aioshelly==0.6.2 From 2765256b61375ae7da1f067c40dfa87417317e97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hans=20Kr=C3=B6ner?= Date: Thu, 8 Apr 2021 13:39:53 +0200 Subject: [PATCH 0110/1317] Account for openweathermap 'dew_point' not always being present (#48826) --- .../openweathermap/weather_update_coordinator.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/openweathermap/weather_update_coordinator.py b/homeassistant/components/openweathermap/weather_update_coordinator.py index 51e475eb75455..20cc71da72573 100644 --- a/homeassistant/components/openweathermap/weather_update_coordinator.py +++ b/homeassistant/components/openweathermap/weather_update_coordinator.py @@ -122,7 +122,7 @@ def _convert_weather_response(self, weather_response): ATTR_API_FEELS_LIKE_TEMPERATURE: current_weather.temperature("celsius").get( "feels_like" ), - ATTR_API_DEW_POINT: (round(current_weather.dewpoint / 100, 1)), + ATTR_API_DEW_POINT: self._fmt_dewpoint(current_weather.dewpoint), ATTR_API_PRESSURE: current_weather.pressure.get("press"), ATTR_API_HUMIDITY: current_weather.humidity, ATTR_API_WIND_BEARING: current_weather.wind().get("deg"), @@ -178,6 +178,12 @@ def _convert_forecast(self, entry): return forecast + @staticmethod + def _fmt_dewpoint(dewpoint): + if dewpoint is not None: + return round(dewpoint / 100, 1) + return None + @staticmethod def _get_rain(rain): """Get rain data from weather data.""" From 78dabc83ecf393c49a362e1719246d92a107ce9b Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 8 Apr 2021 13:40:29 +0200 Subject: [PATCH 0111/1317] Add Xiaomi Miio zhimi.airpurifier.mc2 (#48840) * add zhimi.airpurifier.mc2 * fix issort --- homeassistant/components/xiaomi_miio/const.py | 2 ++ homeassistant/components/xiaomi_miio/fan.py | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index 9b33bab08f7fc..35c4d4a166207 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -25,6 +25,7 @@ MODEL_AIRPURIFIER_SA1 = "zhimi.airpurifier.sa1" MODEL_AIRPURIFIER_SA2 = "zhimi.airpurifier.sa2" MODEL_AIRPURIFIER_2S = "zhimi.airpurifier.mc1" +MODEL_AIRPURIFIER_2H = "zhimi.airpurifier.mc2" MODEL_AIRPURIFIER_3 = "zhimi.airpurifier.ma4" MODEL_AIRPURIFIER_3H = "zhimi.airpurifier.mb3" MODEL_AIRPURIFIER_PROH = "zhimi.airpurifier.va1" @@ -56,6 +57,7 @@ MODEL_AIRPURIFIER_SA1, MODEL_AIRPURIFIER_SA2, MODEL_AIRPURIFIER_2S, + MODEL_AIRPURIFIER_2H, MODEL_AIRHUMIDIFIER_V1, MODEL_AIRHUMIDIFIER_CA1, MODEL_AIRHUMIDIFIER_CB1, diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index e20b1429bc6f0..6d18131cdeb0b 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -61,6 +61,7 @@ MODEL_AIRHUMIDIFIER_CA1, MODEL_AIRHUMIDIFIER_CA4, MODEL_AIRHUMIDIFIER_CB1, + MODEL_AIRPURIFIER_2H, MODEL_AIRPURIFIER_2S, MODEL_AIRPURIFIER_PRO, MODEL_AIRPURIFIER_PRO_V7, @@ -780,7 +781,7 @@ def __init__(self, name, device, entry, unique_id): self._device_features = FEATURE_FLAGS_AIRPURIFIER_PRO_V7 self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO_V7 self._speed_list = OPERATION_MODES_AIRPURIFIER_PRO_V7 - elif self._model == MODEL_AIRPURIFIER_2S: + elif self._model in [MODEL_AIRPURIFIER_2S, MODEL_AIRPURIFIER_2H]: self._device_features = FEATURE_FLAGS_AIRPURIFIER_2S self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_2S self._speed_list = OPERATION_MODES_AIRPURIFIER_2S From 9377a45d8aa66246cbf323347c3526ce587b51a7 Mon Sep 17 00:00:00 2001 From: Niccolo Zapponi Date: Thu, 8 Apr 2021 12:50:46 +0100 Subject: [PATCH 0112/1317] Fix iCloud extra attributes (#48815) --- homeassistant/components/icloud/account.py | 2 +- homeassistant/components/icloud/device_tracker.py | 2 +- homeassistant/components/icloud/sensor.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/icloud/account.py b/homeassistant/components/icloud/account.py index a357df39e42ee..5c3bd2bf51968 100644 --- a/homeassistant/components/icloud/account.py +++ b/homeassistant/components/icloud/account.py @@ -501,6 +501,6 @@ def location(self) -> dict[str, any]: return self._location @property - def exta_state_attributes(self) -> dict[str, any]: + def extra_state_attributes(self) -> dict[str, any]: """Return the attributes.""" return self._attrs diff --git a/homeassistant/components/icloud/device_tracker.py b/homeassistant/components/icloud/device_tracker.py index 3dbc10bcf1b30..502c2b00f8bb0 100644 --- a/homeassistant/components/icloud/device_tracker.py +++ b/homeassistant/components/icloud/device_tracker.py @@ -110,7 +110,7 @@ def icon(self) -> str: @property def extra_state_attributes(self) -> dict[str, any]: """Return the device state attributes.""" - return self._device.state_attributes + return self._device.extra_state_attributes @property def device_info(self) -> dict[str, any]: diff --git a/homeassistant/components/icloud/sensor.py b/homeassistant/components/icloud/sensor.py index ddd3d54c556aa..f889495af25e5 100644 --- a/homeassistant/components/icloud/sensor.py +++ b/homeassistant/components/icloud/sensor.py @@ -93,7 +93,7 @@ def icon(self) -> str: @property def extra_state_attributes(self) -> dict[str, any]: """Return default attributes for the iCloud device entity.""" - return self._device.state_attributes + return self._device.extra_state_attributes @property def device_info(self) -> dict[str, any]: From 91837f08ce87b635dbc55e533c40da86dc64c4ef Mon Sep 17 00:00:00 2001 From: Marvin Wichmann Date: Thu, 8 Apr 2021 14:54:43 +0200 Subject: [PATCH 0113/1317] Update xknx to version 0.18.0 (#48799) --- homeassistant/components/knx/__init__.py | 15 +-------------- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 11ed7fc3c7c88..0fe3e133b6eb1 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -56,8 +56,6 @@ _LOGGER = logging.getLogger(__name__) -CONF_KNX_CONFIG = "config_file" - CONF_KNX_ROUTING = "routing" CONF_KNX_TUNNELING = "tunneling" CONF_KNX_FIRE_EVENT = "fire_event" @@ -81,13 +79,12 @@ { DOMAIN: vol.All( # deprecated since 2021.4 - cv.deprecated(CONF_KNX_CONFIG), + cv.deprecated("config_file"), # deprecated since 2021.2 cv.deprecated(CONF_KNX_FIRE_EVENT), cv.deprecated("fire_event_filter", replacement_key=CONF_KNX_EVENT_FILTER), vol.Schema( { - vol.Optional(CONF_KNX_CONFIG): cv.string, vol.Exclusive( CONF_KNX_ROUTING, "connection_type" ): ConnectionSchema.ROUTING_SCHEMA, @@ -313,7 +310,6 @@ def __init__(self, hass: HomeAssistant, config: ConfigType) -> None: def init_xknx(self) -> None: """Initialize XKNX object.""" self.xknx = XKNX( - config=self.config_file(), own_address=self.config[DOMAIN][CONF_KNX_INDIVIDUAL_ADDRESS], rate_limit=self.config[DOMAIN][CONF_KNX_RATE_LIMIT], multicast_group=self.config[DOMAIN][CONF_KNX_MCAST_GRP], @@ -332,15 +328,6 @@ async def stop(self, event: Event) -> None: """Stop XKNX object. Disconnect from tunneling or Routing device.""" await self.xknx.stop() - def config_file(self) -> str | None: - """Resolve and return the full path of xknx.yaml if configured.""" - config_file = self.config[DOMAIN].get(CONF_KNX_CONFIG) - if not config_file: - return None - if not config_file.startswith("/"): - return self.hass.config.path(config_file) - return config_file # type: ignore - def connection_config(self) -> ConnectionConfig: """Return the connection_config.""" if CONF_KNX_TUNNELING in self.config[DOMAIN]: diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index f15e909755cfb..abb7fff37e027 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -2,7 +2,7 @@ "domain": "knx", "name": "KNX", "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==0.17.5"], + "requirements": ["xknx==0.18.0"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "silver" } diff --git a/requirements_all.txt b/requirements_all.txt index e0ef50debbbda..2c9db42da3b8d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2338,7 +2338,7 @@ xbox-webapi==2.0.8 xboxapi==2.0.1 # homeassistant.components.knx -xknx==0.17.5 +xknx==0.18.0 # homeassistant.components.bluesound # homeassistant.components.rest diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4520ee3893a99..e369689e492cb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1226,7 +1226,7 @@ wolf_smartset==0.1.8 xbox-webapi==2.0.8 # homeassistant.components.knx -xknx==0.17.5 +xknx==0.18.0 # homeassistant.components.bluesound # homeassistant.components.rest From f1c4072d3c8502932c60e546ad03ae3c528c417e Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Thu, 8 Apr 2021 16:51:59 +0200 Subject: [PATCH 0114/1317] Return TP-Link sensor & light attributes as `float` rather than `string` (#48828) --- homeassistant/components/tplink/light.py | 12 ++++++------ homeassistant/components/tplink/switch.py | 20 ++++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index 88c24d7cf3056..8880373955f05 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -382,8 +382,8 @@ def _update_emeter(self): or self._last_current_power_update + CURRENT_POWER_UPDATE_INTERVAL < now ): self._last_current_power_update = now - self._emeter_params[ATTR_CURRENT_POWER_W] = "{:.1f}".format( - self.smartbulb.current_consumption() + self._emeter_params[ATTR_CURRENT_POWER_W] = round( + float(self.smartbulb.current_consumption()), 1 ) if ( @@ -395,11 +395,11 @@ def _update_emeter(self): daily_statistics = self.smartbulb.get_emeter_daily() monthly_statistics = self.smartbulb.get_emeter_monthly() try: - self._emeter_params[ATTR_DAILY_ENERGY_KWH] = "{:.3f}".format( - daily_statistics[int(time.strftime("%d"))] + self._emeter_params[ATTR_DAILY_ENERGY_KWH] = round( + float(daily_statistics[int(time.strftime("%d"))]), 3 ) - self._emeter_params[ATTR_MONTHLY_ENERGY_KWH] = "{:.3f}".format( - monthly_statistics[int(time.strftime("%m"))] + self._emeter_params[ATTR_MONTHLY_ENERGY_KWH] = round( + float(monthly_statistics[int(time.strftime("%m"))]), 3 ) except KeyError: # device returned no daily/monthly history diff --git a/homeassistant/components/tplink/switch.py b/homeassistant/components/tplink/switch.py index 4d7dce37447e1..11b86d6254f0c 100644 --- a/homeassistant/components/tplink/switch.py +++ b/homeassistant/components/tplink/switch.py @@ -138,23 +138,23 @@ def attempt_update(self, update_attempt): if self.smartplug.has_emeter: emeter_readings = self.smartplug.get_emeter_realtime() - self._emeter_params[ATTR_CURRENT_POWER_W] = "{:.2f}".format( - emeter_readings["power"] + self._emeter_params[ATTR_CURRENT_POWER_W] = round( + float(emeter_readings["power"]), 2 ) - self._emeter_params[ATTR_TOTAL_ENERGY_KWH] = "{:.3f}".format( - emeter_readings["total"] + self._emeter_params[ATTR_TOTAL_ENERGY_KWH] = round( + float(emeter_readings["total"]), 3 ) - self._emeter_params[ATTR_VOLTAGE] = "{:.1f}".format( - emeter_readings["voltage"] + self._emeter_params[ATTR_VOLTAGE] = round( + float(emeter_readings["voltage"]), 1 ) - self._emeter_params[ATTR_CURRENT_A] = "{:.2f}".format( - emeter_readings["current"] + self._emeter_params[ATTR_CURRENT_A] = round( + float(emeter_readings["current"]), 2 ) emeter_statics = self.smartplug.get_emeter_daily() with suppress(KeyError): # Device returned no daily history - self._emeter_params[ATTR_TODAY_ENERGY_KWH] = "{:.3f}".format( - emeter_statics[int(time.strftime("%e"))] + self._emeter_params[ATTR_TODAY_ENERGY_KWH] = round( + float(emeter_statics[int(time.strftime("%e"))]), 3 ) return True except (SmartDeviceException, OSError) as ex: From 2768f202b6263101660d35f631ec0f53fe78e913 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 8 Apr 2021 10:53:20 -0400 Subject: [PATCH 0115/1317] Check all endpoints for zwave_js.climate fan mode and operating state (#48800) * Check all endpoints for zwave_js.climate fan mode and operating state * fix test --- homeassistant/components/zwave_js/climate.py | 2 + tests/components/zwave_js/test_climate.py | 4 +- tests/components/zwave_js/test_services.py | 13 +- ..._ct100_plus_different_endpoints_state.json | 1807 ++++++++++------- 4 files changed, 1096 insertions(+), 730 deletions(-) diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py index b814aef2a9dbd..c64a5ef788fc9 100644 --- a/homeassistant/components/zwave_js/climate.py +++ b/homeassistant/components/zwave_js/climate.py @@ -169,11 +169,13 @@ def __init__( THERMOSTAT_MODE_PROPERTY, CommandClass.THERMOSTAT_FAN_MODE, add_to_watched_value_ids=True, + check_all_endpoints=True, ) self._fan_state = self.get_zwave_value( THERMOSTAT_OPERATING_STATE_PROPERTY, CommandClass.THERMOSTAT_FAN_STATE, add_to_watched_value_ids=True, + check_all_endpoints=True, ) self._set_modes_and_presets() self._supported_features = 0 diff --git a/tests/components/zwave_js/test_climate.py b/tests/components/zwave_js/test_climate.py index 5631798fc15c3..83a607f3add39 100644 --- a/tests/components/zwave_js/test_climate.py +++ b/tests/components/zwave_js/test_climate.py @@ -348,7 +348,9 @@ async def test_thermostat_different_endpoints( """Test an entity with values on a different endpoint from the primary value.""" state = hass.states.get(CLIMATE_RADIO_THERMOSTAT_ENTITY) - assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 22.5 + assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 22.8 + assert state.attributes[ATTR_FAN_MODE] == "Auto low" + assert state.attributes[ATTR_FAN_STATE] == "Idle / off" async def test_setpoint_thermostat(hass, client, climate_danfoss_lc_13, integration): diff --git a/tests/components/zwave_js/test_services.py b/tests/components/zwave_js/test_services.py index b5a5f8f48f004..7bdba7894d2f5 100644 --- a/tests/components/zwave_js/test_services.py +++ b/tests/components/zwave_js/test_services.py @@ -495,7 +495,7 @@ async def test_poll_value( assert args["valueId"] == { "commandClassName": "Thermostat Mode", "commandClass": 64, - "endpoint": 0, + "endpoint": 1, "property": "mode", "propertyName": "mode", "metadata": { @@ -503,19 +503,16 @@ async def test_poll_value( "readable": True, "writeable": True, "min": 0, - "max": 31, + "max": 255, "label": "Thermostat mode", "states": { "0": "Off", "1": "Heat", "2": "Cool", - "3": "Auto", - "11": "Energy heat", - "12": "Energy cool", }, }, - "value": 1, - "ccVersion": 2, + "value": 2, + "ccVersion": 0, } client.async_send_command.reset_mock() @@ -531,7 +528,7 @@ async def test_poll_value( }, blocking=True, ) - assert len(client.async_send_command.call_args_list) == 8 + assert len(client.async_send_command.call_args_list) == 7 # Test polling against an invalid entity raises ValueError with pytest.raises(ValueError): diff --git a/tests/fixtures/zwave_js/climate_radio_thermostat_ct100_plus_different_endpoints_state.json b/tests/fixtures/zwave_js/climate_radio_thermostat_ct100_plus_different_endpoints_state.json index fcdd57e981b7a..f940dd210aa08 100644 --- a/tests/fixtures/zwave_js/climate_radio_thermostat_ct100_plus_different_endpoints_state.json +++ b/tests/fixtures/zwave_js/climate_radio_thermostat_ct100_plus_different_endpoints_state.json @@ -1,722 +1,1087 @@ { - "nodeId": 26, - "index": 0, - "installerIcon": 4608, - "userIcon": 4608, - "status": 4, - "ready": true, - "deviceClass": { - "basic": {"key": 4, "label":"Routing Slave"}, - "generic": {"key": 8, "label":"Thermostat"}, - "specific": {"key": 6, "label":"Thermostat General V2"}, - "mandatorySupportedCCs": [], - "mandatoryControlCCs": [] - }, - "isListening": true, - "isFrequentListening": false, - "isRouting": true, - "maxBaudRate": 40000, - "isSecure": false, - "version": 4, - "isBeaming": true, - "manufacturerId": 152, - "productId": 256, - "productType": 25602, - "firmwareVersion": "10.7", - "zwavePlusVersion": 1, - "nodeType": 0, - "roleType": 5, - "deviceConfig": { - "manufacturerId": 152, - "manufacturer": "Radio Thermostat Company of America (RTC)", - "label": "CT100 Plus", - "description": "Z-Wave Thermostat", - "devices": [{ "productType": "0x6402", "productId": "0x0100" }], - "firmwareVersion": { "min": "0.0", "max": "255.255" }, - "paramInformation": { "_map": {} } - }, - "label": "CT100 Plus", - "neighbors": [1, 2, 3, 4, 23], - "endpointCountIsDynamic": false, - "endpointsHaveIdenticalCapabilities": false, - "individualEndpointCount": 2, - "aggregatedEndpointCount": 0, - "interviewAttempts": 1, - "endpoints": [ - { - "nodeId": 26, - "index": 0, - "installerIcon": 4608, - "userIcon": 4608 - }, - { "nodeId": 26, "index": 1 }, - { - "nodeId": 26, - "index": 2, - "installerIcon": 3328, - "userIcon": 3333 - } - ], - "commandClasses": [], - "values": [ - { - "commandClassName": "Manufacturer Specific", - "commandClass": 114, - "endpoint": 0, - "property": "manufacturerId", - "propertyName": "manufacturerId", - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "min": 0, - "max": 65535, - "label": "Manufacturer ID" - }, - "value": 152, - "ccVersion": 2 - }, - { - "commandClassName": "Manufacturer Specific", - "commandClass": 114, - "endpoint": 0, - "property": "productType", - "propertyName": "productType", - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "min": 0, - "max": 65535, - "label": "Product type" - }, - "value": 25602, - "ccVersion": 2 - }, - { - "commandClassName": "Manufacturer Specific", - "commandClass": 114, - "endpoint": 0, - "property": "productId", - "propertyName": "productId", - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "min": 0, - "max": 65535, - "label": "Product ID" - }, - "value": 256, - "ccVersion": 2 - }, - { - "commandClassName": "Thermostat Mode", - "commandClass": 64, - "endpoint": 0, - "property": "mode", - "propertyName": "mode", - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "min": 0, - "max": 31, - "label": "Thermostat mode", - "states": { - "0": "Off", - "1": "Heat", - "2": "Cool", - "3": "Auto", - "11": "Energy heat", - "12": "Energy cool" - } - }, - "value": 1, - "ccVersion": 2 - }, - { - "commandClassName": "Thermostat Mode", - "commandClass": 64, - "endpoint": 0, - "property": "manufacturerData", - "propertyName": "manufacturerData", - "metadata": { - "type": "any", - "readable": true, - "writeable": true - }, - "ccVersion": 2 - }, - { - "commandClassName": "Thermostat Setpoint", - "commandClass": 67, - "endpoint": 0, - "property": "setpoint", - "propertyKey": 1, - "propertyName": "setpoint", - "propertyKeyName": "Heating", - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "unit": "\u00b0F", - "ccSpecific": { "setpointType": 1 } - }, - "value": 72, - "ccVersion": 2 - }, - { - "commandClassName": "Thermostat Setpoint", - "commandClass": 67, - "endpoint": 0, - "property": "setpoint", - "propertyKey": 2, - "propertyName": "setpoint", - "propertyKeyName": "Cooling", - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "unit": "\u00b0F", - "ccSpecific": { "setpointType": 2 } - }, - "value": 73, - "ccVersion": 2 - }, - { - "commandClassName": "Thermostat Setpoint", - "commandClass": 67, - "endpoint": 0, - "property": "setpoint", - "propertyKey": 11, - "propertyName": "setpoint", - "propertyKeyName": "Energy Save Heating", - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "unit": "\u00b0F", - "ccSpecific": { "setpointType": 11 } - }, - "value": 62, - "ccVersion": 2 - }, - { - "commandClassName": "Thermostat Setpoint", - "commandClass": 67, - "endpoint": 0, - "property": "setpoint", - "propertyKey": 12, - "propertyName": "setpoint", - "propertyKeyName": "Energy Save Cooling", - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "unit": "\u00b0F", - "ccSpecific": { "setpointType": 12 } - }, - "value": 85, - "ccVersion": 2 - }, - { - "commandClassName": "Version", - "commandClass": 134, - "endpoint": 0, - "property": "libraryType", - "propertyName": "libraryType", - "metadata": { - "type": "any", - "readable": true, - "writeable": false, - "label": "Library type" - }, - "value": 3, - "ccVersion": 2 - }, - { - "commandClassName": "Version", - "commandClass": 134, - "endpoint": 0, - "property": "protocolVersion", - "propertyName": "protocolVersion", - "metadata": { - "type": "any", - "readable": true, - "writeable": false, - "label": "Z-Wave protocol version" - }, - "value": "4.24", - "ccVersion": 2 - }, - { - "commandClassName": "Version", - "commandClass": 134, - "endpoint": 0, - "property": "firmwareVersions", - "propertyName": "firmwareVersions", - "metadata": { - "type": "any", - "readable": true, - "writeable": false, - "label": "Z-Wave chip firmware versions" - }, - "value": ["10.7"], - "ccVersion": 2 - }, - { - "commandClassName": "Version", - "commandClass": 134, - "endpoint": 0, - "property": "hardwareVersion", - "propertyName": "hardwareVersion", - "metadata": { - "type": "any", - "readable": true, - "writeable": false, - "label": "Z-Wave chip hardware version" - }, - "ccVersion": 2 - }, - { - "commandClassName": "Indicator", - "commandClass": 135, - "endpoint": 0, - "property": "value", - "propertyName": "value", - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "min": 0, - "max": 255, - "label": "Indicator value", - "ccSpecific": { "indicatorId": 0 } - }, - "value": 0, - "ccVersion": 1 - }, - { - "commandClassName": "Thermostat Operating State", - "commandClass": 66, - "endpoint": 0, - "property": "state", - "propertyName": "state", - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "min": 0, - "max": 255, - "label": "Operating state", - "states": { - "0": "Idle", - "1": "Heating", - "2": "Cooling", - "3": "Fan Only", - "4": "Pending Heat", - "5": "Pending Cool", - "6": "Vent/Economizer", - "7": "Aux Heating", - "8": "2nd Stage Heating", - "9": "2nd Stage Cooling", - "10": "2nd Stage Aux Heat", - "11": "3rd Stage Aux Heat" - } - }, - "value": 0, - "ccVersion": 2 - }, - { - "commandClassName": "Configuration", - "commandClass": 112, - "endpoint": 0, - "property": 1, - "propertyName": "Temperature Reporting Threshold", - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "valueSize": 1, - "min": 0, - "max": 4, - "default": 2, - "format": 0, - "allowManualEntry": false, - "states": { - "0": "Disabled", - "1": "0.5\u00b0 F", - "2": "1.0\u00b0 F", - "3": "1.5\u00b0 F", - "4": "2.0\u00b0 F" - }, - "label": "Temperature Reporting Threshold", - "description": "Reporting threshold for changes in the ambient temperature", - "isFromConfig": true - }, - "value": 2, - "ccVersion": 1 - }, - { - "commandClassName": "Configuration", - "commandClass": 112, - "endpoint": 0, - "property": 2, - "propertyName": "HVAC Settings", - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "valueSize": 4, - "min": 0, - "max": 0, - "default": 0, - "format": 0, - "allowManualEntry": true, - "label": "HVAC Settings", - "description": "Configured HVAC settings", - "isFromConfig": true - }, - "value": 17891329, - "ccVersion": 1 - }, - { - "commandClassName": "Configuration", - "commandClass": 112, - "endpoint": 0, - "property": 4, - "propertyName": "Power Status", - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "valueSize": 1, - "min": 0, - "max": 0, - "default": 0, - "format": 0, - "allowManualEntry": true, - "label": "Power Status", - "description": "C-Wire / Battery Status", - "isFromConfig": true - }, - "value": 1, - "ccVersion": 1 - }, - { - "commandClassName": "Configuration", - "commandClass": 112, - "endpoint": 0, - "property": 5, - "propertyName": "Humidity Reporting Threshold", - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "valueSize": 1, - "min": 0, - "max": 255, - "default": 0, - "format": 1, - "allowManualEntry": false, - "states": { - "0": "Disabled", - "1": "3% RH", - "2": "5% RH", - "3": "10% RH" - }, - "label": "Humidity Reporting Threshold", - "description": "Reporting threshold for changes in the relative humidity", - "isFromConfig": true - }, - "value": 2, - "ccVersion": 1 - }, - { - "commandClassName": "Configuration", - "commandClass": 112, - "endpoint": 0, - "property": 6, - "propertyName": "Auxiliary/Emergency", - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "valueSize": 1, - "min": 0, - "max": 255, - "default": 0, - "format": 1, - "allowManualEntry": false, - "states": { - "0": "Auxiliary/Emergency heat disabled", - "1": "Auxiliary/Emergency heat enabled" - }, - "label": "Auxiliary/Emergency", - "description": "Enables or disables auxiliary / emergency heating", - "isFromConfig": true - }, - "value": 0, - "ccVersion": 1 - }, - { - "commandClassName": "Configuration", - "commandClass": 112, - "endpoint": 0, - "property": 7, - "propertyName": "Thermostat Swing Temperature", - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "valueSize": 1, - "min": 1, - "max": 8, - "default": 2, - "format": 0, - "allowManualEntry": false, - "states": { - "1": "0.5\u00b0 F", - "2": "1.0\u00b0 F", - "3": "1.5\u00b0 F", - "4": "2.0\u00b0 F", - "5": "2.5\u00b0 F", - "6": "3.0\u00b0 F", - "7": "3.5\u00b0 F", - "8": "4.0\u00b0 F" - }, - "label": "Thermostat Swing Temperature", - "description": "Variance allowed from setpoint to engage HVAC", - "isFromConfig": true - }, - "value": 2, - "ccVersion": 1 - }, - { - "commandClassName": "Configuration", - "commandClass": 112, - "endpoint": 0, - "property": 8, - "propertyName": "Thermostat Diff Temperature", - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "valueSize": 1, - "min": 4, - "max": 12, - "default": 4, - "format": 0, - "allowManualEntry": false, - "states": { - "4": "2.0\u00b0 F", - "8": "4.0\u00b0 F", - "12": "6.0\u00b0 F" - }, - "label": "Thermostat Diff Temperature", - "description": "Configures additional stages", - "isFromConfig": true - }, - "value": 1028, - "ccVersion": 1 - }, - { - "commandClassName": "Configuration", - "commandClass": 112, - "endpoint": 0, - "property": 9, - "propertyName": "Thermostat Recovery Mode", - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "valueSize": 1, - "min": 1, - "max": 2, - "default": 2, - "format": 0, - "allowManualEntry": false, - "states": { - "1": "Fast recovery mode", - "2": "Economy recovery mode" - }, - "label": "Thermostat Recovery Mode", - "description": "Fast or Economy recovery mode", - "isFromConfig": true - }, - "value": 2, - "ccVersion": 1 - }, - { - "commandClassName": "Configuration", - "commandClass": 112, - "endpoint": 0, - "property": 10, - "propertyName": "Temperature Reporting Filter", - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "valueSize": 4, - "min": 0, - "max": 124, - "default": 124, - "format": 0, - "allowManualEntry": true, - "label": "Temperature Reporting Filter", - "description": "Upper/Lower bounds for thermostat temperature reporting", - "isFromConfig": true - }, - "value": 32000, - "ccVersion": 1 - }, - { - "commandClassName": "Configuration", - "commandClass": 112, - "endpoint": 0, - "property": 11, - "propertyName": "Simple UI Mode", - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "valueSize": 1, - "min": 0, - "max": 1, - "default": 1, - "format": 0, - "allowManualEntry": false, - "states": { - "0": "Normal mode enabled", - "1": "Simple mode enabled" - }, - "label": "Simple UI Mode", - "description": "Simple mode enable/disable", - "isFromConfig": true - }, - "value": 1, - "ccVersion": 1 - }, - { - "commandClassName": "Configuration", - "commandClass": 112, - "endpoint": 0, - "property": 12, - "propertyName": "Multicast", - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "valueSize": 1, - "min": 0, - "max": 1, - "default": 0, - "format": 0, - "allowManualEntry": false, - "states": { - "0": "Multicast disabled", - "1": "Multicast enabled" - }, - "label": "Multicast", - "description": "Enable or disables Multicast", - "isFromConfig": true - }, - "value": 0, - "ccVersion": 1 - }, - { - "commandClassName": "Configuration", - "commandClass": 112, - "endpoint": 0, - "property": 3, - "propertyName": "Utility Lock Enable/Disable", - "metadata": { - "type": "number", - "readable": false, - "writeable": true, - "valueSize": 1, - "min": 0, - "max": 255, - "default": 0, - "format": 1, - "allowManualEntry": false, - "states": { - "0": "Utility lock disabled", - "1": "Utility lock enabled" - }, - "label": "Utility Lock Enable/Disable", - "description": "Prevents setpoint changes at thermostat", - "isFromConfig": true - }, - "ccVersion": 1 - }, - { - "commandClassName": "Battery", - "commandClass": 128, - "endpoint": 0, - "property": "level", - "propertyName": "level", - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "min": 0, - "max": 100, - "unit": "%", - "label": "Battery level" - }, - "value": 100, - "ccVersion": 1 - }, - { - "commandClassName": "Battery", - "commandClass": 128, - "endpoint": 0, - "property": "isLow", - "propertyName": "isLow", - "metadata": { - "type": "boolean", - "readable": true, - "writeable": false, - "label": "Low battery level" - }, - "value": false, - "ccVersion": 1 - }, - { - "commandClassName": "Multilevel Sensor", - "commandClass": 49, - "endpoint": 2, - "property": "Air temperature", - "propertyName": "Air temperature", - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "unit": "\u00b0F", - "label": "Air temperature", - "ccSpecific": { "sensorType": 1, "scale": 1 } - }, - "value": 72.5, - "ccVersion": 5 - }, - { - "commandClassName": "Multilevel Sensor", - "commandClass": 49, - "endpoint": 2, - "property": "Humidity", - "propertyName": "Humidity", - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "unit": "%", - "label": "Humidity", - "ccSpecific": { "sensorType": 5, "scale": 0 } - }, - "value": 20, - "ccVersion": 5 - } - ] -} \ No newline at end of file + "nodeId": 26, + "index": 0, + "installerIcon": 4608, + "userIcon": 4608, + "status": 4, + "ready": true, + "isListening": true, + "isRouting": true, + "isSecure": false, + "manufacturerId": 152, + "productId": 256, + "productType": 25602, + "firmwareVersion": "10.7", + "zwavePlusVersion": 1, + "deviceConfig": { + "filename": "/opt/node_modules/@zwave-js/config/config/devices/0x0098/ct100_plus.json", + "manufacturer": "Radio Thermostat Company of America (RTC)", + "manufacturerId": 152, + "label": "CT100 Plus", + "description": "Z-Wave Thermostat", + "devices": [ + { + "productType": 25602, + "productId": 256 + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "paramInformation": { + "_map": {} + } + }, + "label": "CT100 Plus", + "neighbors": [1, 2, 29, 3, 4, 5, 6], + "endpointCountIsDynamic": false, + "endpointsHaveIdenticalCapabilities": false, + "individualEndpointCount": 2, + "aggregatedEndpointCount": 0, + "interviewAttempts": 0, + "interviewStage": 6, + "endpoints": [ + { + "nodeId": 26, + "index": 0, + "installerIcon": 4608, + "userIcon": 4608, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 8, + "label": "Thermostat" + }, + "specific": { + "key": 6, + "label": "General Thermostat V2" + }, + "mandatorySupportedCCs": [32, 114, 64, 67, 134], + "mandatoryControlledCCs": [] + } + }, + { + "nodeId": 26, + "index": 1, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 8, + "label": "Thermostat" + }, + "specific": { + "key": 6, + "label": "General Thermostat V2" + }, + "mandatorySupportedCCs": [32, 114, 64, 67, 134], + "mandatoryControlledCCs": [] + } + }, + { + "nodeId": 26, + "index": 2, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 33, + "label": "Multilevel Sensor" + }, + "specific": { + "key": 0, + "label": "Unused" + }, + "mandatorySupportedCCs": [32, 49], + "mandatoryControlledCCs": [] + } + } + ], + "values": [ + { + "endpoint": 0, + "commandClass": 49, + "commandClassName": "Multilevel Sensor", + "property": "Air temperature", + "propertyName": "Air temperature", + "ccVersion": 5, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Air temperature", + "ccSpecific": { + "sensorType": 1, + "scale": 1 + }, + "unit": "\u00b0F" + }, + "value": 73 + }, + { + "endpoint": 0, + "commandClass": 49, + "commandClassName": "Multilevel Sensor", + "property": "Humidity", + "propertyName": "Humidity", + "ccVersion": 5, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Humidity", + "ccSpecific": { + "sensorType": 5, + "scale": 0 + }, + "unit": "%" + }, + "value": 36 + }, + { + "endpoint": 0, + "commandClass": 66, + "commandClassName": "Thermostat Operating State", + "property": "state", + "propertyName": "state", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Operating state", + "min": 0, + "max": 255, + "states": { + "0": "Idle", + "1": "Heating", + "2": "Cooling", + "3": "Fan Only", + "4": "Pending Heat", + "5": "Pending Cool", + "6": "Vent/Economizer", + "7": "Aux Heating", + "8": "2nd Stage Heating", + "9": "2nd Stage Cooling", + "10": "2nd Stage Aux Heat", + "11": "3rd Stage Aux Heat" + } + }, + "value": 2 + }, + { + "endpoint": 0, + "commandClass": 68, + "commandClassName": "Thermostat Fan Mode", + "property": "mode", + "propertyName": "mode", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Thermostat fan mode", + "min": 0, + "max": 255, + "states": { + "0": "Auto low", + "1": "Low" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 69, + "commandClassName": "Thermostat Fan State", + "property": "state", + "propertyName": "state", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Thermostat fan state", + "min": 0, + "max": 255, + "states": { + "0": "Idle / off", + "1": "Running / running low", + "2": "Running high", + "3": "Running medium", + "4": "Circulation mode", + "5": "Humidity circulation mode", + "6": "Right - left circulation mode", + "7": "Up - down circulation mode", + "8": "Quiet circulation mode" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 1, + "propertyName": "Temperature Reporting Threshold", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Reporting threshold for changes in the ambient temperature", + "label": "Temperature Reporting Threshold", + "default": 2, + "min": 0, + "max": 4, + "states": { + "0": "Disabled", + "1": "0.5\u00b0 F", + "2": "1.0\u00b0 F", + "3": "1.5\u00b0 F", + "4": "2.0\u00b0 F" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 2, + "propertyName": "HVAC Settings", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "description": "Configured HVAC settings", + "label": "HVAC Settings", + "default": 0, + "min": 0, + "max": 0, + "valueSize": 4, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 17891329 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 4, + "propertyName": "Power Status", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "description": "C-Wire / Battery Status", + "label": "Power Status", + "default": 0, + "min": 0, + "max": 0, + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 7, + "propertyName": "Thermostat Swing Temperature", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Variance allowed from setpoint to engage HVAC", + "label": "Thermostat Swing Temperature", + "default": 2, + "min": 1, + "max": 8, + "states": { + "1": "0.5\u00b0 F", + "2": "1.0\u00b0 F", + "3": "1.5\u00b0 F", + "4": "2.0\u00b0 F", + "5": "2.5\u00b0 F", + "6": "3.0\u00b0 F", + "7": "3.5\u00b0 F", + "8": "4.0\u00b0 F" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 2 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 8, + "propertyName": "Thermostat Diff Temperature", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Configures additional stages", + "label": "Thermostat Diff Temperature", + "default": 4, + "min": 4, + "max": 12, + "states": { + "4": "2.0\u00b0 F", + "8": "4.0\u00b0 F", + "12": "6.0\u00b0 F" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1028 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 9, + "propertyName": "Thermostat Recovery Mode", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Fast or Economy recovery mode", + "label": "Thermostat Recovery Mode", + "default": 2, + "min": 1, + "max": 2, + "states": { + "1": "Fast recovery mode", + "2": "Economy recovery mode" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 2 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 10, + "propertyName": "Temperature Reporting Filter", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Upper/Lower bounds for thermostat temperature reporting", + "label": "Temperature Reporting Filter", + "default": 124, + "min": 0, + "max": 124, + "valueSize": 4, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 11, + "propertyName": "Simple UI Mode", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Simple mode enable/disable", + "label": "Simple UI Mode", + "default": 1, + "min": 0, + "max": 1, + "states": { + "0": "Normal mode enabled", + "1": "Simple mode enabled" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 12, + "propertyName": "Multicast", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Enable or disables Multicast", + "label": "Multicast", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "Multicast disabled", + "1": "Multicast enabled" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 3, + "propertyName": "Utility Lock Enable/Disable", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": false, + "writeable": true, + "description": "Prevents setpoint changes at thermostat", + "label": "Utility Lock Enable/Disable", + "default": 0, + "min": 0, + "max": 255, + "states": { + "0": "Utility lock disabled", + "1": "Utility lock enabled" + }, + "valueSize": 1, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 5, + "propertyName": "Humidity Reporting Threshold", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Reporting threshold for changes in the relative humidity", + "label": "Humidity Reporting Threshold", + "default": 0, + "min": 0, + "max": 255, + "states": { + "0": "Disabled", + "1": "3% RH", + "2": "5% RH", + "3": "10% RH" + }, + "valueSize": 1, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 6, + "propertyName": "Auxiliary/Emergency", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Enables or disables auxiliary / emergency heating", + "label": "Auxiliary/Emergency", + "default": 0, + "min": 0, + "max": 255, + "states": { + "0": "Auxiliary/Emergency heat disabled", + "1": "Auxiliary/Emergency heat enabled" + }, + "valueSize": 1, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Manufacturer ID", + "min": 0, + "max": 65535 + }, + "value": 152 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product type", + "min": 0, + "max": 65535 + }, + "value": 25602 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product ID", + "min": 0, + "max": 65535 + }, + "value": 256 + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "level", + "propertyName": "level", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Battery level", + "min": 0, + "max": 100, + "unit": "%" + }, + "value": 100 + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "isLow", + "propertyName": "isLow", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Low battery level" + }, + "value": false + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Library type" + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "4.24" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": ["10.7"] + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version" + } + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": "value", + "propertyName": "value", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Indicator value", + "ccSpecific": { + "indicatorId": 0 + }, + "min": 0, + "max": 255 + }, + "value": 0 + }, + { + "endpoint": 1, + "commandClass": 32, + "commandClassName": "Basic", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current value", + "min": 0, + "max": 99 + } + }, + { + "endpoint": 1, + "commandClass": 32, + "commandClassName": "Basic", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Target value", + "min": 0, + "max": 99 + } + }, + { + "endpoint": 1, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Manufacturer ID", + "min": 0, + "max": 65535 + }, + "value": 152 + }, + { + "endpoint": 1, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product type", + "min": 0, + "max": 65535 + }, + "value": 25602 + }, + { + "endpoint": 1, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product ID", + "min": 0, + "max": 65535 + }, + "value": 256 + }, + { + "endpoint": 1, + "commandClass": 64, + "commandClassName": "Thermostat Mode", + "property": "mode", + "propertyName": "mode", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Thermostat mode", + "min": 0, + "max": 255, + "states": { + "0": "Off", + "1": "Heat", + "2": "Cool" + } + }, + "value": 2 + }, + { + "endpoint": 1, + "commandClass": 64, + "commandClassName": "Thermostat Mode", + "property": "manufacturerData", + "propertyName": "manufacturerData", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + } + }, + { + "endpoint": 1, + "commandClass": 67, + "commandClassName": "Thermostat Setpoint", + "property": "setpoint", + "propertyKey": 1, + "propertyName": "setpoint", + "propertyKeyName": "Heating", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "ccSpecific": { + "setpointType": 1 + }, + "unit": "\u00b0F" + }, + "value": 72 + }, + { + "endpoint": 1, + "commandClass": 67, + "commandClassName": "Thermostat Setpoint", + "property": "setpoint", + "propertyKey": 2, + "propertyName": "setpoint", + "propertyKeyName": "Cooling", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "ccSpecific": { + "setpointType": 2 + }, + "unit": "\u00b0F" + }, + "value": 73 + }, + { + "endpoint": 1, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Library type" + } + }, + { + "endpoint": 1, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + } + }, + { + "endpoint": 1, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + } + }, + { + "endpoint": 2, + "commandClass": 32, + "commandClassName": "Basic", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current value", + "min": 0, + "max": 99 + } + }, + { + "endpoint": 2, + "commandClass": 32, + "commandClassName": "Basic", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Target value", + "min": 0, + "max": 99 + } + } + ], + "isFrequentListening": false, + "maxDataRate": 100000, + "supportedDataRates": [40000, 100000], + "protocolVersion": 3, + "supportsBeaming": true, + "supportsSecurity": false, + "nodeType": 1, + "zwavePlusNodeType": 0, + "zwavePlusRoleType": 5, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 8, + "label": "Thermostat" + }, + "specific": { + "key": 6, + "label": "General Thermostat V2" + }, + "mandatorySupportedCCs": [32, 114, 64, 67, 134], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 49, + "name": "Multilevel Sensor", + "version": 5, + "isSecure": false + }, + { + "id": 64, + "name": "Thermostat Mode", + "version": 2, + "isSecure": false + }, + { + "id": 66, + "name": "Thermostat Operating State", + "version": 2, + "isSecure": false + }, + { + "id": 67, + "name": "Thermostat Setpoint", + "version": 2, + "isSecure": false + }, + { + "id": 68, + "name": "Thermostat Fan Mode", + "version": 1, + "isSecure": false + }, + { + "id": 69, + "name": "Thermostat Fan State", + "version": 1, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 96, + "name": "Multi Channel", + "version": 4, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 3, + "isSecure": false + }, + { + "id": 128, + "name": "Battery", + "version": 1, + "isSecure": false + }, + { + "id": 129, + "name": "Clock", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": false + }, + { + "id": 135, + "name": "Indicator", + "version": 1, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + } + ] +} From e70111b93c29944ff974fd199aa92dc40111b689 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Nenz=C3=A9n?= Date: Thu, 8 Apr 2021 17:00:49 +0200 Subject: [PATCH 0116/1317] Add missing super call in Verisure Camera entity (#48812) --- homeassistant/components/verisure/camera.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/verisure/camera.py b/homeassistant/components/verisure/camera.py index cb159027c16b0..e667829bb10e0 100644 --- a/homeassistant/components/verisure/camera.py +++ b/homeassistant/components/verisure/camera.py @@ -54,6 +54,7 @@ def __init__( ): """Initialize Verisure File Camera component.""" super().__init__(coordinator) + Camera.__init__(self) self.serial_number = serial_number self._directory_path = directory_path From 50bc037819f729b5da7c1f6c3fc53b9364f16c3e Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Thu, 8 Apr 2021 17:35:02 +0200 Subject: [PATCH 0117/1317] Bump speedtest-cli to 2.1.3 (#48861) --- homeassistant/components/speedtestdotnet/manifest.json | 4 +++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/speedtestdotnet/manifest.json b/homeassistant/components/speedtestdotnet/manifest.json index d230f03f954d2..f2e2a2196c98b 100644 --- a/homeassistant/components/speedtestdotnet/manifest.json +++ b/homeassistant/components/speedtestdotnet/manifest.json @@ -3,6 +3,8 @@ "name": "Speedtest.net", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/speedtestdotnet", - "requirements": ["speedtest-cli==2.1.2"], + "requirements": [ + "speedtest-cli==2.1.3" + ], "codeowners": ["@rohankapoorcom", "@engrbm87"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2c9db42da3b8d..d2b817a9ae60c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2111,7 +2111,7 @@ sonarr==0.3.0 speak2mary==1.4.0 # homeassistant.components.speedtestdotnet -speedtest-cli==2.1.2 +speedtest-cli==2.1.3 # homeassistant.components.spider spiderpy==1.4.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e369689e492cb..f993915af3bb5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1113,7 +1113,7 @@ sonarr==0.3.0 speak2mary==1.4.0 # homeassistant.components.speedtestdotnet -speedtest-cli==2.1.2 +speedtest-cli==2.1.3 # homeassistant.components.spider spiderpy==1.4.2 From 94fc7b8aed4f376643d0a3407e37c3cfce0588a1 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 8 Apr 2021 17:54:13 +0200 Subject: [PATCH 0118/1317] Correct wrong x in frontend manifest (#48865) --- homeassistant/components/frontend/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 20369503f5ccd..0529fd6dbb2f0 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -62,7 +62,7 @@ "screenshots": [ { "src": "/static/images/screenshots/screenshot-1.png", - "sizes": "413×792", + "sizes": "413x792", "type": "image/png", } ], From e475b6b9c37aa8e6aa78371bd27b347f4b911f90 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 8 Apr 2021 18:02:29 +0200 Subject: [PATCH 0119/1317] Fix optional data payload in Prowl messaging service (#48868) --- homeassistant/components/prowl/notify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/prowl/notify.py b/homeassistant/components/prowl/notify.py index 725c3b9de303a..802679ab03d11 100644 --- a/homeassistant/components/prowl/notify.py +++ b/homeassistant/components/prowl/notify.py @@ -48,7 +48,7 @@ async def async_send_message(self, message, **kwargs): "description": message, "priority": data["priority"] if data and "priority" in data else 0, } - if data.get("url"): + if data and data.get("url"): payload["url"] = data["url"] _LOGGER.debug("Attempting call Prowl service at %s", url) From 1dafea705dca74b0b0a0770812d5e0cccaf558c7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 8 Apr 2021 19:03:11 +0200 Subject: [PATCH 0120/1317] Fix possibly missing changed_by in Verisure Alarm (#48867) --- homeassistant/components/verisure/alarm_control_panel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/verisure/alarm_control_panel.py b/homeassistant/components/verisure/alarm_control_panel.py index 34a60b9cae4da..1cefd6af27293 100644 --- a/homeassistant/components/verisure/alarm_control_panel.py +++ b/homeassistant/components/verisure/alarm_control_panel.py @@ -112,7 +112,7 @@ def _handle_coordinator_update(self) -> None: self._state = ALARM_STATE_TO_HA.get( self.coordinator.data["alarm"]["statusType"] ) - self._changed_by = self.coordinator.data["alarm"]["name"] + self._changed_by = self.coordinator.data["alarm"].get("name") super()._handle_coordinator_update() async def async_added_to_hass(self) -> None: From c7e4857d2c0a8bfc7fdf98c44592391fc27cfcf0 Mon Sep 17 00:00:00 2001 From: Laszlo Magyar Date: Thu, 8 Apr 2021 19:08:49 +0200 Subject: [PATCH 0121/1317] Let recorder deal with event names longer than 32 chars (#47748) --- .../components/recorder/migration.py | 25 ++++++++++++++++++- homeassistant/components/recorder/models.py | 4 +-- tests/components/recorder/test_migrate.py | 20 +++++++++++++++ 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 5ab2d9091727a..fa93f6155617c 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -206,6 +206,16 @@ def _add_columns(engine, table_name, columns_def): def _modify_columns(engine, table_name, columns_def): """Modify columns in a table.""" + if engine.dialect.name == "sqlite": + _LOGGER.debug( + "Skipping to modify columns %s in table %s; " + "Modifying column length in SQLite is unnecessary, " + "it does not impose any length restrictions", + ", ".join(column.split(" ")[0] for column in columns_def), + table_name, + ) + return + _LOGGER.warning( "Modifying columns %s in table %s. Note: this can take several " "minutes on large databases and slow computers. Please " @@ -213,7 +223,18 @@ def _modify_columns(engine, table_name, columns_def): ", ".join(column.split(" ")[0] for column in columns_def), table_name, ) - columns_def = [f"MODIFY {col_def}" for col_def in columns_def] + + if engine.dialect.name == "postgresql": + columns_def = [ + "ALTER {column} TYPE {type}".format( + **dict(zip(["column", "type"], col_def.split(" ", 1))) + ) + for col_def in columns_def + ] + elif engine.dialect.name == "mssql": + columns_def = [f"ALTER COLUMN {col_def}" for col_def in columns_def] + else: + columns_def = [f"MODIFY {col_def}" for col_def in columns_def] try: engine.execute( @@ -377,6 +398,8 @@ def _apply_update(engine, new_version, old_version): "created DATETIME(6)", ], ) + elif new_version == 14: + _modify_columns(engine, "events", ["event_type VARCHAR(64)"]) else: raise ValueError(f"No schema migration defined for version {new_version}") diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index a547f3151338b..b26c523ce4031 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -26,7 +26,7 @@ # pylint: disable=invalid-name Base = declarative_base() -SCHEMA_VERSION = 13 +SCHEMA_VERSION = 14 _LOGGER = logging.getLogger(__name__) @@ -53,7 +53,7 @@ class Events(Base): # type: ignore } __tablename__ = TABLE_EVENTS event_id = Column(Integer, primary_key=True) - event_type = Column(String(32)) + event_type = Column(String(64)) event_data = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) origin = Column(String(32)) time_fired = Column(DATETIME_TYPE, index=True) diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index 3bde17ab8ef10..c4e0d32adcf06 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -76,6 +76,26 @@ def test_invalid_update(): migration._apply_update(None, -1, 0) +@pytest.mark.parametrize( + ["engine_type", "substr"], + [ + ("postgresql", "ALTER event_type TYPE VARCHAR(64)"), + ("mssql", "ALTER COLUMN event_type VARCHAR(64)"), + ("mysql", "MODIFY event_type VARCHAR(64)"), + ("sqlite", None), + ], +) +def test_modify_column(engine_type, substr): + """Test that modify column generates the expected query.""" + engine = Mock() + engine.dialect.name = engine_type + migration._modify_columns(engine, "events", ["event_type VARCHAR(64)"]) + if substr: + assert substr in engine.execute.call_args[0][0].text + else: + assert not engine.execute.called + + def test_forgiving_add_column(): """Test that add column will continue if column exists.""" engine = create_engine("sqlite://", poolclass=StaticPool) From 1f80c756abfd018bf20db0a01b941eb3ea7872c5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 8 Apr 2021 07:30:33 -1000 Subject: [PATCH 0122/1317] Fix subscribe_bootstrap_integrations to send events (#48754) --- homeassistant/bootstrap.py | 6 ++++-- homeassistant/components/websocket_api/commands.py | 2 +- tests/components/websocket_api/test_commands.py | 5 ++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index b43e789005b5e..fc12ec065a9bd 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -440,7 +440,7 @@ async def _async_set_up_integrations( hass.data[DATA_SETUP_STARTED] = {} setup_time = hass.data[DATA_SETUP_TIME] = {} - log_task = asyncio.create_task(_async_watch_pending_setups(hass)) + watch_task = asyncio.create_task(_async_watch_pending_setups(hass)) domains_to_setup = _get_domains(hass, config) @@ -555,7 +555,9 @@ async def _async_set_up_integrations( except asyncio.TimeoutError: _LOGGER.warning("Setup timed out for stage 2 - moving forward") - log_task.cancel() + watch_task.cancel() + async_dispatcher_send(hass, SIGNAL_BOOTSTRAP_INTEGRATONS, {}) + _LOGGER.debug( "Integration setup times: %s", { diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index f7961046043a6..33a3370366821 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -111,7 +111,7 @@ def handle_subscribe_bootstrap_integrations(hass, connection, msg): @callback def forward_bootstrap_integrations(message): """Forward bootstrap integrations to websocket.""" - connection.send_message(messages.result_message(msg["id"], message)) + connection.send_message(messages.event_message(msg["id"], message)) connection.subscriptions[msg["id"]] = async_dispatcher_connect( hass, SIGNAL_BOOTSTRAP_INTEGRATONS, forward_bootstrap_integrations diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 09123db457997..3b01e6ecd8acb 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -1147,9 +1147,8 @@ async def test_subscribe_unsubscribe_bootstrap_integrations( async_dispatcher_send(hass, SIGNAL_BOOTSTRAP_INTEGRATONS, message) msg = await websocket_client.receive_json() assert msg["id"] == 7 - assert msg["success"] is True - assert msg["type"] == "result" - assert msg["result"] == message + assert msg["type"] == "event" + assert msg["event"] == message async def test_integration_setup_info(hass, websocket_client, hass_admin_user): From 3ca69f55683c4d7090b9c4551087a930b2eba57b Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 8 Apr 2021 14:46:28 -0400 Subject: [PATCH 0123/1317] Raise an exception when event_type exceeds the max length (#48115) * raise an exception when event_type exceeds the max length that the recorder supports * add test * use max length constant in recorder * update config entry reloaded service name * remove exception string function because it's not needed * increase limit to 64 and revert event name change * fix test * assert exception args * fix test * add comment about migration --- homeassistant/components/recorder/models.py | 3 ++- homeassistant/const.py | 4 ++++ homeassistant/core.py | 5 +++++ homeassistant/exceptions.py | 17 +++++++++++++++++ tests/test_core.py | 16 ++++++++++++++++ 5 files changed, 44 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index b26c523ce4031..3459da309eebf 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -18,6 +18,7 @@ from sqlalchemy.orm import relationship from sqlalchemy.orm.session import Session +from homeassistant.const import MAX_LENGTH_EVENT_TYPE from homeassistant.core import Context, Event, EventOrigin, State, split_entity_id from homeassistant.helpers.json import JSONEncoder import homeassistant.util.dt as dt_util @@ -53,7 +54,7 @@ class Events(Base): # type: ignore } __tablename__ = TABLE_EVENTS event_id = Column(Integer, primary_key=True) - event_type = Column(String(64)) + event_type = Column(String(MAX_LENGTH_EVENT_TYPE)) event_data = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) origin = Column(String(32)) time_fired = Column(DATETIME_TYPE, index=True) diff --git a/homeassistant/const.py b/homeassistant/const.py index ea86400d963f1..7d05a7c03f476 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -22,6 +22,10 @@ # If no name is specified DEVICE_DEFAULT_NAME = "Unnamed Device" +# Max characters for an event_type (changing this requires a recorder +# database migration) +MAX_LENGTH_EVENT_TYPE = 64 + # Sun events SUN_EVENT_SUNSET = "sunset" SUN_EVENT_SUNRISE = "sunrise" diff --git a/homeassistant/core.py b/homeassistant/core.py index fdf2a0939283c..6ad722e0d18b4 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -58,12 +58,14 @@ EVENT_TIMER_OUT_OF_SYNC, LENGTH_METERS, MATCH_ALL, + MAX_LENGTH_EVENT_TYPE, __version__, ) from homeassistant.exceptions import ( HomeAssistantError, InvalidEntityFormatError, InvalidStateError, + MaxLengthExceeded, ServiceNotFound, Unauthorized, ) @@ -697,6 +699,9 @@ def async_fire( This method must be run in the event loop. """ + if len(event_type) > MAX_LENGTH_EVENT_TYPE: + raise MaxLengthExceeded(event_type, "event_type", MAX_LENGTH_EVENT_TYPE) + listeners = self._listeners.get(event_type, []) # EVENT_HOMEASSISTANT_CLOSE should go only to his listeners diff --git a/homeassistant/exceptions.py b/homeassistant/exceptions.py index 375db78961840..b40aa99520d1d 100644 --- a/homeassistant/exceptions.py +++ b/homeassistant/exceptions.py @@ -154,3 +154,20 @@ def __init__(self, domain: str, service: str) -> None: def __str__(self) -> str: """Return string representation.""" return f"Unable to find service {self.domain}.{self.service}" + + +class MaxLengthExceeded(HomeAssistantError): + """Raised when a property value has exceeded the max character length.""" + + def __init__(self, value: str, property_name: str, max_length: int) -> None: + """Initialize error.""" + super().__init__( + self, + ( + f"Value {value} for property {property_name} has a max length of " + f"{max_length} characters" + ), + ) + self.value = value + self.property_name = property_name + self.max_length = max_length diff --git a/tests/test_core.py b/tests/test_core.py index 88b4e1d58f6c6..d3283c14b84a0 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -36,6 +36,7 @@ from homeassistant.exceptions import ( InvalidEntityFormatError, InvalidStateError, + MaxLengthExceeded, ServiceNotFound, ) import homeassistant.util.dt as dt_util @@ -524,6 +525,21 @@ async def coroutine_listener(event): assert len(coroutine_calls) == 1 +async def test_eventbus_max_length_exceeded(hass): + """Test that an exception is raised when the max character length is exceeded.""" + + long_evt_name = ( + "this_event_exceeds_the_max_character_length_even_with_the_new_limit" + ) + + with pytest.raises(MaxLengthExceeded) as exc_info: + hass.bus.async_fire(long_evt_name) + + assert exc_info.value.property_name == "event_type" + assert exc_info.value.max_length == 64 + assert exc_info.value.value == long_evt_name + + def test_state_init(): """Test state.init.""" with pytest.raises(InvalidEntityFormatError): From c2d98f190569e262ca5eccd566e8f829b844709d Mon Sep 17 00:00:00 2001 From: Khole Date: Thu, 8 Apr 2021 19:51:00 +0100 Subject: [PATCH 0124/1317] Add hive boost off functionality (#48701) * Add boost off functionality * Added backwards compatibility * Update homeassistant/components/hive/services.yaml Co-authored-by: Martin Hjelmare * Update homeassistant/components/hive/climate.py Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- homeassistant/components/hive/climate.py | 44 +++++++++++++++++++-- homeassistant/components/hive/const.py | 3 +- homeassistant/components/hive/services.yaml | 34 +++++++++++++++- 3 files changed, 76 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/hive/climate.py b/homeassistant/components/hive/climate.py index e6da78d921cb9..d5b60fa4b952f 100644 --- a/homeassistant/components/hive/climate.py +++ b/homeassistant/components/hive/climate.py @@ -1,5 +1,6 @@ """Support for the Hive climate devices.""" from datetime import timedelta +import logging import voluptuous as vol @@ -20,7 +21,12 @@ from homeassistant.helpers import config_validation as cv, entity_platform from . import HiveEntity, refresh_system -from .const import ATTR_TIME_PERIOD, DOMAIN, SERVICE_BOOST_HEATING +from .const import ( + ATTR_TIME_PERIOD, + DOMAIN, + SERVICE_BOOST_HEATING_OFF, + SERVICE_BOOST_HEATING_ON, +) HIVE_TO_HASS_STATE = { "SCHEDULE": HVAC_MODE_AUTO, @@ -47,6 +53,7 @@ SUPPORT_PRESET = [PRESET_NONE, PRESET_BOOST] PARALLEL_UPDATES = 0 SCAN_INTERVAL = timedelta(seconds=15) +_LOGGER = logging.getLogger() async def async_setup_entry(hass, entry, async_add_entities): @@ -63,7 +70,7 @@ async def async_setup_entry(hass, entry, async_add_entities): platform = entity_platform.current_platform.get() platform.async_register_entity_service( - SERVICE_BOOST_HEATING, + "boost_heating", { vol.Required(ATTR_TIME_PERIOD): vol.All( cv.time_period, @@ -75,6 +82,25 @@ async def async_setup_entry(hass, entry, async_add_entities): "async_heating_boost", ) + platform.async_register_entity_service( + SERVICE_BOOST_HEATING_ON, + { + vol.Required(ATTR_TIME_PERIOD): vol.All( + cv.time_period, + cv.positive_timedelta, + lambda td: td.total_seconds() // 60, + ), + vol.Optional(ATTR_TEMPERATURE, default="25.0"): vol.Coerce(float), + }, + "async_heating_boost_on", + ) + + platform.async_register_entity_service( + SERVICE_BOOST_HEATING_OFF, + {}, + "async_heating_boost_off", + ) + class HiveClimateEntity(HiveEntity, ClimateEntity): """Hive Climate Device.""" @@ -198,11 +224,23 @@ async def async_set_preset_mode(self, preset_mode): temperature = curtemp + 0.5 await self.hive.heating.setBoostOn(self.device, 30, temperature) - @refresh_system async def async_heating_boost(self, time_period, temperature): + """Handle boost heating service call.""" + _LOGGER.warning( + "Hive Service heating_boost will be removed in 2021.7.0, please update to heating_boost_on" + ) + await self.async_heating_boost_on(time_period, temperature) + + @refresh_system + async def async_heating_boost_on(self, time_period, temperature): """Handle boost heating service call.""" await self.hive.heating.setBoostOn(self.device, time_period, temperature) + @refresh_system + async def async_heating_boost_off(self): + """Handle boost heating service call.""" + await self.hive.heating.setBoostOff(self.device) + async def async_update(self): """Update all Node data from Hive.""" await self.hive.session.updateData(self.device) diff --git a/homeassistant/components/hive/const.py b/homeassistant/components/hive/const.py index ea416fbfe32e0..9e1d7fc1f8034 100644 --- a/homeassistant/components/hive/const.py +++ b/homeassistant/components/hive/const.py @@ -16,5 +16,6 @@ "water_heater": "water_heater", } SERVICE_BOOST_HOT_WATER = "boost_hot_water" -SERVICE_BOOST_HEATING = "boost_heating" +SERVICE_BOOST_HEATING_ON = "boost_heating_on" +SERVICE_BOOST_HEATING_OFF = "boost_heating_off" WATER_HEATER_MODES = ["on", "off"] diff --git a/homeassistant/components/hive/services.yaml b/homeassistant/components/hive/services.yaml index f029af7b0b518..de1439eead456 100644 --- a/homeassistant/components/hive/services.yaml +++ b/homeassistant/components/hive/services.yaml @@ -1,5 +1,24 @@ boost_heating: - name: Boost Heating + name: Boost Heating (To be deprecated) + description: To be deprecated please use boost_heating_on. + fields: + entity_id: + name: Entity ID + description: Select entity_id to boost. + required: true + example: climate.heating + time_period: + name: Time Period + description: Set the time period for the boost. + required: true + example: 01:30:00 + temperature: + name: Temperature + description: Set the target temperature for the boost period. + required: true + example: 20.5 +boost_heating_on: + name: Boost Heating On description: Set the boost mode ON defining the period of time and the desired target temperature for the boost. fields: entity_id: @@ -30,6 +49,19 @@ boost_heating: step: 0.5 unit_of_measurement: degrees mode: slider +boost_heating_off: + name: Boost Heating Off + description: Set the boost mode OFF. + fields: + entity_id: + name: Entity ID + description: Select entity_id to turn boost off. + required: true + example: climate.heating + selector: + entity: + integration: hive + domain: climate boost_hot_water: name: Boost Hotwater description: Set the boost mode ON or OFF defining the period of time for the boost. From 493bd4cdca4b62789ddde8d54bd77b037dd9807c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 8 Apr 2021 09:03:10 -1000 Subject: [PATCH 0125/1317] Add manufacturer matching support to zeroconf (#48810) We plan on matching with _airplay which means we need to able to limit to specific manufacturers to avoid generating flows for integrations with the wrong manufacturer --- homeassistant/components/zeroconf/__init__.py | 15 ++++ tests/components/zeroconf/test_init.py | 85 ++++++++++++++++++- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 38544798b9bca..d2eaa6ca76622 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -279,6 +279,13 @@ def service_update( else: uppercase_mac = None + if "manufacturer" in info["properties"]: + lowercase_manufacturer: str | None = info["properties"][ + "manufacturer" + ].lower() + else: + lowercase_manufacturer = None + # Not all homekit types are currently used for discovery # so not all service type exist in zeroconf_types for entry in zeroconf_types.get(service_type, []): @@ -295,6 +302,14 @@ def service_update( and not fnmatch.fnmatch(lowercase_name, entry["name"]) ): continue + if ( + lowercase_manufacturer is not None + and "manufacturer" in entry + and not fnmatch.fnmatch( + lowercase_manufacturer, entry["manufacturer"] + ) + ): + continue hass.add_job( hass.config_entries.flow.async_init( diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index 7bd670fb4c927..e7a30abc73f0b 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -104,6 +104,24 @@ def mock_zc_info(service_type, name): return mock_zc_info +def get_zeroconf_info_mock_manufacturer(manufacturer): + """Return info for get_service_info for an zeroconf device.""" + + def mock_zc_info(service_type, name): + return ServiceInfo( + service_type, + name, + addresses=[b"\n\x00\x00\x14"], + port=80, + weight=0, + priority=0, + server="name.local.", + properties={b"manufacturer": manufacturer.encode()}, + ) + + return mock_zc_info + + async def test_setup(hass, mock_zeroconf): """Test configured options for a device are loaded via config entry.""" with patch.object( @@ -237,7 +255,7 @@ async def test_service_with_invalid_name(hass, mock_zeroconf, caplog): assert "Failed to get info for device name" in caplog.text -async def test_zeroconf_match(hass, mock_zeroconf): +async def test_zeroconf_match_macaddress(hass, mock_zeroconf): """Test configured options for a device are loaded via config entry.""" def http_only_service_update_mock(zeroconf, services, handlers): @@ -274,6 +292,39 @@ def http_only_service_update_mock(zeroconf, services, handlers): assert mock_config_flow.mock_calls[0][1][0] == "shelly" +async def test_zeroconf_match_manufacturer(hass, mock_zeroconf): + """Test configured options for a device are loaded via config entry.""" + + def http_only_service_update_mock(zeroconf, services, handlers): + """Call service update handler.""" + handlers[0]( + zeroconf, + "_airplay._tcp.local.", + "s1000._airplay._tcp.local.", + ServiceStateChange.Added, + ) + + with patch.dict( + zc_gen.ZEROCONF, + {"_airplay._tcp.local.": [{"domain": "samsungtv", "manufacturer": "samsung*"}]}, + clear=True, + ), patch.object( + hass.config_entries.flow, "async_init" + ) as mock_config_flow, patch.object( + zeroconf, "HaServiceBrowser", side_effect=http_only_service_update_mock + ) as mock_service_browser: + mock_zeroconf.get_service_info.side_effect = ( + get_zeroconf_info_mock_manufacturer("Samsung Electronics") + ) + assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert len(mock_service_browser.mock_calls) == 1 + assert len(mock_config_flow.mock_calls) == 1 + assert mock_config_flow.mock_calls[0][1][0] == "samsungtv" + + async def test_zeroconf_no_match(hass, mock_zeroconf): """Test configured options for a device are loaded via config entry.""" @@ -306,6 +357,38 @@ def http_only_service_update_mock(zeroconf, services, handlers): assert len(mock_config_flow.mock_calls) == 0 +async def test_zeroconf_no_match_manufacturer(hass, mock_zeroconf): + """Test configured options for a device are loaded via config entry.""" + + def http_only_service_update_mock(zeroconf, services, handlers): + """Call service update handler.""" + handlers[0]( + zeroconf, + "_airplay._tcp.local.", + "s1000._airplay._tcp.local.", + ServiceStateChange.Added, + ) + + with patch.dict( + zc_gen.ZEROCONF, + {"_airplay._tcp.local.": [{"domain": "samsungtv", "manufacturer": "samsung*"}]}, + clear=True, + ), patch.object( + hass.config_entries.flow, "async_init" + ) as mock_config_flow, patch.object( + zeroconf, "HaServiceBrowser", side_effect=http_only_service_update_mock + ) as mock_service_browser: + mock_zeroconf.get_service_info.side_effect = ( + get_zeroconf_info_mock_manufacturer("Not Samsung Electronics") + ) + assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert len(mock_service_browser.mock_calls) == 1 + assert len(mock_config_flow.mock_calls) == 0 + + async def test_homekit_match_partial_space(hass, mock_zeroconf): """Test configured options for a device are loaded via config entry.""" with patch.dict( From e988062034935eac7cad36c75e2e72cab3af25aa Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 8 Apr 2021 21:39:03 +0200 Subject: [PATCH 0126/1317] Fix mysensor cover closed state (#48833) --- homeassistant/components/mysensors/cover.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mysensors/cover.py b/homeassistant/components/mysensors/cover.py index 0e3478a57bf37..33393f08defab 100644 --- a/homeassistant/components/mysensors/cover.py +++ b/homeassistant/components/mysensors/cover.py @@ -70,12 +70,12 @@ def get_cover_state(self): else: amount = 100 if self._values.get(set_req.V_LIGHT) == STATE_ON else 0 + if amount == 0: + return CoverState.CLOSED if v_up and not v_down and not v_stop: return CoverState.OPENING if not v_up and v_down and not v_stop: return CoverState.CLOSING - if not v_up and not v_down and v_stop and amount == 0: - return CoverState.CLOSED return CoverState.OPEN @property From 5e8559e3cc0ef7b34ff37b5e6a2f565f01e66b1a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 8 Apr 2021 21:40:48 +0200 Subject: [PATCH 0127/1317] Validate supported_color_modes for MQTT JSON light (#48836) --- homeassistant/components/light/__init__.py | 14 ++++++++++ .../components/mqtt/light/schema_json.py | 6 ++++- tests/components/mqtt/test_light_json.py | 27 +++++++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 4fae5caab0092..fe9a38d12b4f7 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -73,6 +73,20 @@ COLOR_MODES_BRIGHTNESS = VALID_COLOR_MODES - {COLOR_MODE_ONOFF} COLOR_MODES_COLOR = {COLOR_MODE_HS, COLOR_MODE_RGB, COLOR_MODE_XY} + +def valid_supported_color_modes(color_modes): + """Validate the given color modes.""" + color_modes = set(color_modes) + if ( + not color_modes + or COLOR_MODE_UNKNOWN in color_modes + or (COLOR_MODE_BRIGHTNESS in color_modes and len(color_modes) > 1) + or (COLOR_MODE_ONOFF in color_modes and len(color_modes) > 1) + ): + raise vol.Error(f"Invalid supported_color_modes {sorted(color_modes)}") + return color_modes + + # Float that represents transition time in seconds to make change. ATTR_TRANSITION = "transition" diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 8be3708bd614e..aaf12f3362f42 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -35,6 +35,7 @@ SUPPORT_WHITE_VALUE, VALID_COLOR_MODES, LightEntity, + valid_supported_color_modes, ) from homeassistant.const import ( CONF_BRIGHTNESS, @@ -130,7 +131,10 @@ def valid_color_configuration(config): vol.Optional(CONF_RGB, default=DEFAULT_RGB): cv.boolean, vol.Optional(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Inclusive(CONF_SUPPORTED_COLOR_MODES, "color_mode"): vol.All( - cv.ensure_list, [vol.In(VALID_COLOR_MODES)], vol.Unique() + cv.ensure_list, + [vol.In(VALID_COLOR_MODES)], + vol.Unique(), + valid_supported_color_modes, ), vol.Optional(CONF_WHITE_VALUE, default=DEFAULT_WHITE_VALUE): cv.boolean, vol.Optional(CONF_XY, default=DEFAULT_XY): cv.boolean, diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index 7856eb84c07b0..6c9c7ae903a80 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -188,6 +188,33 @@ async def test_fail_setup_if_color_mode_deprecated(hass, mqtt_mock, deprecated): assert hass.states.get("light.test") is None +@pytest.mark.parametrize( + "supported_color_modes", [["onoff", "rgb"], ["brightness", "rgb"], ["unknown"]] +) +async def test_fail_setup_if_color_modes_invalid( + hass, mqtt_mock, supported_color_modes +): + """Test if setup fails if supported color modes is invalid.""" + config = { + light.DOMAIN: { + "brightness": True, + "color_mode": True, + "command_topic": "test_light_rgb/set", + "name": "test", + "platform": "mqtt", + "schema": "json", + "supported_color_modes": supported_color_modes, + } + } + assert await async_setup_component( + hass, + light.DOMAIN, + config, + ) + await hass.async_block_till_done() + assert hass.states.get("light.test") is None + + async def test_rgb_light(hass, mqtt_mock): """Test RGB light flags brightness support.""" assert await async_setup_component( From aaa9367554fb3783a4254f6e59a2ddb3f94c37ef Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 8 Apr 2021 21:41:40 +0200 Subject: [PATCH 0128/1317] Update frontend to 20210407.2 (#48888) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index b910c0acc468f..98ae51341af4f 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20210407.1" + "home-assistant-frontend==20210407.2" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d2e0aa84118ef..2a9df6eebe4f5 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ defusedxml==0.6.0 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.42.0 -home-assistant-frontend==20210407.1 +home-assistant-frontend==20210407.2 httpx==0.17.1 jinja2>=2.11.3 netdisco==2.8.2 diff --git a/requirements_all.txt b/requirements_all.txt index d2b817a9ae60c..dd7faca5a6100 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -762,7 +762,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210407.1 +home-assistant-frontend==20210407.2 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f993915af3bb5..3d7ce0f2684ec 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -423,7 +423,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210407.1 +home-assistant-frontend==20210407.2 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 2dc46d4516868caedd3f8e5b2c3fa17d33729b57 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 8 Apr 2021 21:42:56 +0200 Subject: [PATCH 0129/1317] Fix motion_blinds gateway signal strength sensor (#48866) Co-authored-by: Martin Hjelmare --- homeassistant/components/motion_blinds/sensor.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/motion_blinds/sensor.py b/homeassistant/components/motion_blinds/sensor.py index d7f40337cec9d..0da38795f7b1a 100644 --- a/homeassistant/components/motion_blinds/sensor.py +++ b/homeassistant/components/motion_blinds/sensor.py @@ -183,10 +183,14 @@ def available(self): if self.coordinator.data is None: return False - if not self.coordinator.data[KEY_GATEWAY][ATTR_AVAILABLE]: - return False + gateway_available = self.coordinator.data[KEY_GATEWAY][ATTR_AVAILABLE] + if self._device_type == TYPE_GATEWAY: + return gateway_available - return self.coordinator.data[self._device.mac][ATTR_AVAILABLE] + return ( + gateway_available + and self.coordinator.data[self._device.mac][ATTR_AVAILABLE] + ) @property def unit_of_measurement(self): From b0aa64d59c2c8d4c93cf0a5fc3864b38912c579c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 8 Apr 2021 21:44:17 +0200 Subject: [PATCH 0130/1317] Replace redacted stream recorder credentials with '****' (#48832) --- homeassistant/components/stream/__init__.py | 15 ++++++++------- homeassistant/components/stream/worker.py | 6 ++---- tests/components/stream/test_recorder.py | 2 +- tests/components/stream/test_worker.py | 2 +- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 0226bb82f6db1..0d91b63844e75 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -39,7 +39,12 @@ _LOGGER = logging.getLogger(__name__) -STREAM_SOURCE_RE = re.compile("//(.*):(.*)@") +STREAM_SOURCE_RE = re.compile("//.*:.*@") + + +def redact_credentials(data): + """Redact credentials from string data.""" + return STREAM_SOURCE_RE.sub("//****:****@", data) def create_stream(hass, stream_source, options=None): @@ -176,9 +181,7 @@ def start(self): target=self._run_worker, ) self._thread.start() - _LOGGER.info( - "Started stream: %s", STREAM_SOURCE_RE.sub("//", str(self.source)) - ) + _LOGGER.info("Started stream: %s", redact_credentials(str(self.source))) def update_source(self, new_source): """Restart the stream with a new stream source.""" @@ -244,9 +247,7 @@ def _stop(self): self._thread_quit.set() self._thread.join() self._thread = None - _LOGGER.info( - "Stopped stream: %s", STREAM_SOURCE_RE.sub("//", str(self.source)) - ) + _LOGGER.info("Stopped stream: %s", redact_credentials(str(self.source))) async def async_record(self, video_path, duration=30, lookback=5): """Make a .mp4 recording from a provided stream.""" diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index 5a12935698337..cd4528b3088e2 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -5,7 +5,7 @@ import av -from . import STREAM_SOURCE_RE +from . import redact_credentials from .const import ( AUDIO_CODECS, MAX_MISSING_DTS, @@ -128,9 +128,7 @@ def stream_worker(source, options, segment_buffer, quit_event): try: container = av.open(source, options=options, timeout=STREAM_TIMEOUT) except av.AVError: - _LOGGER.error( - "Error opening stream %s", STREAM_SOURCE_RE.sub("//", str(source)) - ) + _LOGGER.error("Error opening stream %s", redact_credentials(str(source))) return try: video_stream = container.streams.video[0] diff --git a/tests/components/stream/test_recorder.py b/tests/components/stream/test_recorder.py index 564da4b108e11..5ee055754b987 100644 --- a/tests/components/stream/test_recorder.py +++ b/tests/components/stream/test_recorder.py @@ -266,4 +266,4 @@ async def test_recorder_log(hass, caplog): with patch.object(hass.config, "is_allowed_path", return_value=True): await stream.async_record("/example/path") assert "https://abcd:efgh@foo.bar" not in caplog.text - assert "https://foo.bar" in caplog.text + assert "https://****:****@foo.bar" in caplog.text diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index cf72a90168b6f..d5527105a704c 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -588,4 +588,4 @@ async def test_worker_log(hass, caplog): ) await hass.async_block_till_done() assert "https://abcd:efgh@foo.bar" not in caplog.text - assert "https://foo.bar" in caplog.text + assert "https://****:****@foo.bar" in caplog.text From a59460a23336627d0bc12b1eefffdaa516e55e87 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 8 Apr 2021 13:04:39 -0700 Subject: [PATCH 0131/1317] Test that we do not initialize bad configuration (#48872) * Test that we do not initialize bad configuration * Simplify test as we are not calling a service --- tests/components/automation/test_init.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 71727258fcc45..5997be22644ce 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -1357,6 +1357,26 @@ async def test_blueprint_automation(hass, calls): ] +async def test_blueprint_automation_bad_config(hass, caplog): + """Test blueprint automation with bad inputs.""" + assert await async_setup_component( + hass, + "automation", + { + "automation": { + "use_blueprint": { + "path": "test_event_service.yaml", + "input": { + "trigger_event": "blueprint_event", + "service_to_call": {"dict": "not allowed"}, + }, + } + } + }, + ) + assert "generated invalid automation" in caplog.text + + async def test_trigger_service(hass, calls): """Test the automation trigger service.""" assert await async_setup_component( From bdbc38c9378fbe526b07d0a3cf43e2d10df26e41 Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Fri, 9 Apr 2021 01:43:41 +0200 Subject: [PATCH 0132/1317] Catch expected errors and log them in rituals perfume genie (#48870) * Add update error logging * Move try available to else * Remove TimeoutError --- .../components/rituals_perfume_genie/switch.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/rituals_perfume_genie/switch.py b/homeassistant/components/rituals_perfume_genie/switch.py index 471be52b054af..bc8e2b5e17599 100644 --- a/homeassistant/components/rituals_perfume_genie/switch.py +++ b/homeassistant/components/rituals_perfume_genie/switch.py @@ -1,10 +1,15 @@ """Support for Rituals Perfume Genie switches.""" from datetime import timedelta +import logging + +import aiohttp from homeassistant.components.switch import SwitchEntity from .const import DOMAIN +_LOGGER = logging.getLogger(__name__) + SCAN_INTERVAL = timedelta(seconds=30) ON_STATE = "1" @@ -33,6 +38,7 @@ class DiffuserSwitch(SwitchEntity): def __init__(self, diffuser): """Initialize the switch.""" self._diffuser = diffuser + self._available = True @property def device_info(self): @@ -53,7 +59,7 @@ def unique_id(self): @property def available(self): """Return if the device is available.""" - return self._diffuser.data["hub"]["status"] == AVAILABLE_STATE + return self._available @property def name(self): @@ -89,4 +95,10 @@ async def async_turn_off(self, **kwargs): async def async_update(self): """Update the data of the device.""" - await self._diffuser.update_data() + try: + await self._diffuser.update_data() + except aiohttp.ClientError: + self._available = False + _LOGGER.error("Unable to retrieve data from rituals.sense-company.com") + else: + self._available = self._diffuser.data["hub"]["status"] == AVAILABLE_STATE From 23dd57a5629c88c1022528bca6197d0b87060ab5 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Fri, 9 Apr 2021 00:03:15 +0000 Subject: [PATCH 0133/1317] [ci skip] Translation update --- .../components/atag/translations/de.json | 3 ++- .../components/bsblan/translations/de.json | 2 ++ .../components/climacell/translations/ko.json | 1 + .../components/climate/translations/cs.json | 2 +- .../components/deconz/translations/cs.json | 4 ++-- .../components/denonavr/translations/de.json | 4 +++- .../components/dunehd/translations/de.json | 1 + .../forked_daapd/translations/de.json | 12 ++++++++--- .../components/gogogate2/translations/de.json | 3 ++- .../components/guardian/translations/de.json | 6 +++++- .../components/homekit/translations/de.json | 1 + .../hvv_departures/translations/nl.json | 2 +- .../components/isy994/translations/de.json | 5 ++++- .../kostal_plenticore/translations/ko.json | 20 +++++++++++++++++++ .../lutron_caseta/translations/de.json | 1 + .../met_eireann/translations/ko.json | 19 ++++++++++++++++++ .../components/nuki/translations/ko.json | 9 +++++++++ .../components/plugwise/translations/de.json | 3 ++- .../components/sonarr/translations/de.json | 10 ++++++++++ .../components/tuya/translations/de.json | 1 + .../waze_travel_time/translations/ko.json | 20 +++++++++++++++++++ .../components/wiffi/translations/de.json | 3 ++- .../wolflink/translations/sensor.cs.json | 2 +- 23 files changed, 119 insertions(+), 15 deletions(-) create mode 100644 homeassistant/components/kostal_plenticore/translations/ko.json create mode 100644 homeassistant/components/met_eireann/translations/ko.json create mode 100644 homeassistant/components/waze_travel_time/translations/ko.json diff --git a/homeassistant/components/atag/translations/de.json b/homeassistant/components/atag/translations/de.json index b94103d898bca..8b2b7ce4dff99 100644 --- a/homeassistant/components/atag/translations/de.json +++ b/homeassistant/components/atag/translations/de.json @@ -4,7 +4,8 @@ "already_configured": "Dieses Ger\u00e4t wurde bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen" + "cannot_connect": "Verbindung fehlgeschlagen", + "unauthorized": "Pairing verweigert, Ger\u00e4t auf Authentifizierungsanforderung pr\u00fcfen" }, "step": { "user": { diff --git a/homeassistant/components/bsblan/translations/de.json b/homeassistant/components/bsblan/translations/de.json index 77a810844149a..d1400529b0b84 100644 --- a/homeassistant/components/bsblan/translations/de.json +++ b/homeassistant/components/bsblan/translations/de.json @@ -6,10 +6,12 @@ "error": { "cannot_connect": "Verbindung fehlgeschlagen" }, + "flow_title": "BSB-Lan: {name}", "step": { "user": { "data": { "host": "Host", + "passkey": "Passkey String", "password": "Passwort", "port": "Port Nummer", "username": "Benutzername" diff --git a/homeassistant/components/climacell/translations/ko.json b/homeassistant/components/climacell/translations/ko.json index 6fc5a6d7e8bee..901fd429b1a04 100644 --- a/homeassistant/components/climacell/translations/ko.json +++ b/homeassistant/components/climacell/translations/ko.json @@ -10,6 +10,7 @@ "user": { "data": { "api_key": "API \ud0a4", + "api_version": "API \ubc84\uc804", "latitude": "\uc704\ub3c4", "longitude": "\uacbd\ub3c4", "name": "\uc774\ub984" diff --git a/homeassistant/components/climate/translations/cs.json b/homeassistant/components/climate/translations/cs.json index caeb255264e35..3740a7b423eec 100644 --- a/homeassistant/components/climate/translations/cs.json +++ b/homeassistant/components/climate/translations/cs.json @@ -16,7 +16,7 @@ }, "state": { "_": { - "auto": "Automatika", + "auto": "Auto", "cool": "Chlazen\u00ed", "dry": "Vysou\u0161en\u00ed", "fan_only": "Pouze ventil\u00e1tor", diff --git a/homeassistant/components/deconz/translations/cs.json b/homeassistant/components/deconz/translations/cs.json index 7e08a89ec317a..c198068e07edf 100644 --- a/homeassistant/components/deconz/translations/cs.json +++ b/homeassistant/components/deconz/translations/cs.json @@ -14,8 +14,8 @@ "flow_title": "Br\u00e1na deCONZ ZigBee ({host})", "step": { "hassio_confirm": { - "description": "Chcete nakonfigurovat slu\u017ebu Home Assistant pro p\u0159ipojen\u00ed k deCONZ br\u00e1n\u011b pomoc\u00ed Supervisor {addon}?", - "title": "deCONZ Zigbee br\u00e1na prost\u0159ednictv\u00edm dopl\u0148ku Supervisor" + "description": "Chcete nakonfigurovat slu\u017ebu Home Assistant pro p\u0159ipojen\u00ed k deCONZ br\u00e1n\u011b pomoc\u00ed dopl\u0148ku {addon}?", + "title": "deCONZ Zigbee br\u00e1na prost\u0159ednictv\u00edm Home Assistant dopl\u0148ku" }, "link": { "description": "Odemkn\u011bte br\u00e1nu deCONZ pro registraci v Home Assistant.\n\n 1. P\u0159ejd\u011bte na Nastaven\u00ed deCONZ - > Br\u00e1na - > Pokro\u010dil\u00e9\n 2. Stiskn\u011bte tla\u010d\u00edtko \"Ov\u011b\u0159it aplikaci\"", diff --git a/homeassistant/components/denonavr/translations/de.json b/homeassistant/components/denonavr/translations/de.json index e1330024c5383..6bd9f1613dc5f 100644 --- a/homeassistant/components/denonavr/translations/de.json +++ b/homeassistant/components/denonavr/translations/de.json @@ -3,7 +3,9 @@ "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", - "cannot_connect": "Verbindung fehlgeschlagen. Bitte versuchen Sie es noch einmal. Trennen Sie ggf. Strom- und Ethernetkabel und verbinden Sie diese erneut." + "cannot_connect": "Verbindung fehlgeschlagen. Bitte versuchen Sie es noch einmal. Trennen Sie ggf. Strom- und Ethernetkabel und verbinden Sie diese erneut.", + "not_denonavr_manufacturer": "Kein Denon AVR-Netzwerkempf\u00e4nger, entdeckter Hersteller stimmte nicht \u00fcberein", + "not_denonavr_missing": "Kein Denon AVR-Netzwerk-Receiver, Erkennungsinformationen nicht vollst\u00e4ndig" }, "error": { "discovery_error": "Denon AVR-Netzwerk-Receiver konnte nicht gefunden werden" diff --git a/homeassistant/components/dunehd/translations/de.json b/homeassistant/components/dunehd/translations/de.json index 57856b68421a7..aa87de530b811 100644 --- a/homeassistant/components/dunehd/translations/de.json +++ b/homeassistant/components/dunehd/translations/de.json @@ -13,6 +13,7 @@ "data": { "host": "Host" }, + "description": "Richte die Dune HD-Integration ein. Wenn du Probleme mit der Konfiguration hast, gehe zu: https://www.home-assistant.io/integrations/dunehd \n\nStelle sicher, dass dein Player eingeschaltet ist.", "title": "Dune HD" } } diff --git a/homeassistant/components/forked_daapd/translations/de.json b/homeassistant/components/forked_daapd/translations/de.json index be581502398e9..559db72d42cd0 100644 --- a/homeassistant/components/forked_daapd/translations/de.json +++ b/homeassistant/components/forked_daapd/translations/de.json @@ -1,22 +1,26 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "not_forked_daapd": "Das Ger\u00e4t ist kein Forked-Daapd-Server." }, "error": { "forbidden": "Verbindung kann nicht hergestellt werden. Bitte \u00fcberpr\u00fcfen Sie Ihre forked-daapd-Netzwerkberechtigungen.", "unknown_error": "Unbekannter Fehler", + "websocket_not_enabled": "Forked-Daapd-Server-Websocket nicht aktiviert.", "wrong_host_or_port": "Verbindung konnte nicht hergestellt werden. Bitte Host und Port pr\u00fcfen.", "wrong_password": "Ung\u00fcltiges Passwort", "wrong_server_type": "F\u00fcr die forked-daapd Integration ist ein forked-daapd Server mit der Version > = 27.0 erforderlich." }, + "flow_title": "Forked-Daapd-Server: {name} ({host})", "step": { "user": { "data": { "host": "Host", "password": "API-Passwort (leer lassen, wenn kein Passwort vorhanden ist)", "port": "API Port" - } + }, + "title": "Forked-Daapd-Ger\u00e4t einrichten" } } }, @@ -27,7 +31,9 @@ "max_playlists": "Maximale Anzahl der als Quellen verwendeten Wiedergabelisten", "tts_pause_time": "Sekunden bis zur Pause vor und nach der TTS", "tts_volume": "TTS-Lautst\u00e4rke (Float im Bereich [0,1])" - } + }, + "description": "Lege verschiedene Optionen f\u00fcr die Forked-Daapd-Integration fest.", + "title": "Konfigurieren der Forked-Daapd-Optionen" } } } diff --git a/homeassistant/components/gogogate2/translations/de.json b/homeassistant/components/gogogate2/translations/de.json index 119d198615c2d..30a1ff67b6534 100644 --- a/homeassistant/components/gogogate2/translations/de.json +++ b/homeassistant/components/gogogate2/translations/de.json @@ -13,7 +13,8 @@ "ip_address": "IP-Adresse", "password": "Passwort", "username": "Benutzername" - } + }, + "title": "GogoGate2 oder iSmartGate einrichten" } } } diff --git a/homeassistant/components/guardian/translations/de.json b/homeassistant/components/guardian/translations/de.json index d1218cb23729a..432afe8df27ee 100644 --- a/homeassistant/components/guardian/translations/de.json +++ b/homeassistant/components/guardian/translations/de.json @@ -10,7 +10,11 @@ "data": { "ip_address": "IP-Adresse", "port": "Port" - } + }, + "description": "Konfiguriere ein lokales Elexa Guardian Ger\u00e4t." + }, + "zeroconf_confirm": { + "description": "M\u00f6chtest du dieses Guardian-Ger\u00e4t einrichten?" } } } diff --git a/homeassistant/components/homekit/translations/de.json b/homeassistant/components/homekit/translations/de.json index 18fde7a7f91e5..55df691b58b94 100644 --- a/homeassistant/components/homekit/translations/de.json +++ b/homeassistant/components/homekit/translations/de.json @@ -27,6 +27,7 @@ "include_domains": "Einzubeziehende Domains", "mode": "Modus" }, + "description": "W\u00e4hlen Sie die Domains aus, die aufgenommen werden sollen. Alle unterst\u00fctzten Entit\u00e4ten in der Domain werden aufgenommen. F\u00fcr jeden TV-Mediaplayer und jede Kamera wird eine separate HomeKit-Instanz im Zubeh\u00f6rmodus erstellt.", "title": "HomeKit aktivieren" } } diff --git a/homeassistant/components/hvv_departures/translations/nl.json b/homeassistant/components/hvv_departures/translations/nl.json index 09c8b5b60e761..8782499ee0596 100644 --- a/homeassistant/components/hvv_departures/translations/nl.json +++ b/homeassistant/components/hvv_departures/translations/nl.json @@ -36,7 +36,7 @@ "init": { "data": { "filter": "Selecteer lijnen", - "offset": "Offset (minuten)", + "offset": "Afwijking (minuten)", "real_time": "Gebruik realtime gegevens" }, "description": "Wijzig opties voor deze vertreksensor", diff --git a/homeassistant/components/isy994/translations/de.json b/homeassistant/components/isy994/translations/de.json index ef13c4318b330..0a4758e1156de 100644 --- a/homeassistant/components/isy994/translations/de.json +++ b/homeassistant/components/isy994/translations/de.json @@ -9,6 +9,7 @@ "invalid_host": "Der Hosteintrag hatte nicht das vollst\u00e4ndige URL-Format, z. B. http://192.168.10.100:80", "unknown": "Unerwarteter Fehler" }, + "flow_title": "Universalger\u00e4te ISY994 {name} ({host})", "step": { "user": { "data": { @@ -27,7 +28,9 @@ "init": { "data": { "ignore_string": "Zeichenfolge ignorieren", - "restore_light_state": "Lichthelligkeit wiederherstellen" + "restore_light_state": "Lichthelligkeit wiederherstellen", + "sensor_string": "Knoten Sensor String", + "variable_sensor_string": "Variabler Sensor String" }, "description": "Stelle die Optionen f\u00fcr die ISY-Integration ein: \n - Node Sensor String: Jedes Ger\u00e4t oder jeder Ordner, der 'Node Sensor String' im Namen enth\u00e4lt, wird als Sensor oder bin\u00e4rer Sensor behandelt. \n - String ignorieren: Jedes Ger\u00e4t mit 'Ignore String' im Namen wird ignoriert. \n - Variable Sensor Zeichenfolge: Jede Variable, die 'Variable Sensor String' im Namen enth\u00e4lt, wird als Sensor hinzugef\u00fcgt. \n - Lichthelligkeit wiederherstellen: Wenn diese Option aktiviert ist, wird beim Einschalten eines Lichts die vorherige Helligkeit wiederhergestellt und nicht der integrierte Ein-Pegel des Ger\u00e4ts.", "title": "ISY994 Optionen" diff --git a/homeassistant/components/kostal_plenticore/translations/ko.json b/homeassistant/components/kostal_plenticore/translations/ko.json new file mode 100644 index 0000000000000..98a520d9444cb --- /dev/null +++ b/homeassistant/components/kostal_plenticore/translations/ko.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "password": "\ube44\ubc00\ubc88\ud638" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/de.json b/homeassistant/components/lutron_caseta/translations/de.json index 84a3ade5ffc26..75cdac794828a 100644 --- a/homeassistant/components/lutron_caseta/translations/de.json +++ b/homeassistant/components/lutron_caseta/translations/de.json @@ -11,6 +11,7 @@ "flow_title": "Lutron Cas\u00e9ta {name} ({host})", "step": { "import_failed": { + "description": "Konnte die aus configuration.yaml importierte Bridge (Host: {host}) nicht einrichten.", "title": "Import der Cas\u00e9ta-Bridge-Konfiguration fehlgeschlagen." }, "link": { diff --git a/homeassistant/components/met_eireann/translations/ko.json b/homeassistant/components/met_eireann/translations/ko.json new file mode 100644 index 0000000000000..d0adc5f4addb0 --- /dev/null +++ b/homeassistant/components/met_eireann/translations/ko.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "elevation": "\uace0\ub3c4", + "latitude": "\uc704\ub3c4", + "longitude": "\uacbd\ub3c4", + "name": "\uc774\ub984" + }, + "description": "Met \u00c9ireann \uacf5\uacf5 \uae30\uc0c1\uc608\ubcf4 API\uc5d0\uc11c \ub0a0\uc528 \ub370\uc774\ud130\ub97c \uc0ac\uc6a9\ud560 \uc704\uce58\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "title": "\uc704\uce58" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuki/translations/ko.json b/homeassistant/components/nuki/translations/ko.json index 68f43847d6c78..3015596e7d467 100644 --- a/homeassistant/components/nuki/translations/ko.json +++ b/homeassistant/components/nuki/translations/ko.json @@ -1,11 +1,20 @@ { "config": { + "abort": { + "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4" + }, "error": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "step": { + "reauth_confirm": { + "data": { + "token": "\uc561\uc138\uc2a4 \ud1a0\ud070" + }, + "title": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc7ac\uc778\uc99d\ud558\uae30" + }, "user": { "data": { "host": "\ud638\uc2a4\ud2b8", diff --git a/homeassistant/components/plugwise/translations/de.json b/homeassistant/components/plugwise/translations/de.json index 685cd6fb9ae9f..4d01be82b6a5d 100644 --- a/homeassistant/components/plugwise/translations/de.json +++ b/homeassistant/components/plugwise/translations/de.json @@ -14,7 +14,8 @@ "data": { "flow_type": "Verbindungstyp" }, - "description": "Details" + "description": "Details", + "title": "Plugwise Typ" }, "user_gateway": { "data": { diff --git a/homeassistant/components/sonarr/translations/de.json b/homeassistant/components/sonarr/translations/de.json index 939c5eed1c82f..b4ceeeb43b767 100644 --- a/homeassistant/components/sonarr/translations/de.json +++ b/homeassistant/components/sonarr/translations/de.json @@ -26,5 +26,15 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "Anzahl der anzuzeigenden Tage", + "wanted_max_items": "Maximale Anzahl der anzuzeigenden gesuchten Elemente" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/de.json b/homeassistant/components/tuya/translations/de.json index 6650dc754b306..c16a945200ec5 100644 --- a/homeassistant/components/tuya/translations/de.json +++ b/homeassistant/components/tuya/translations/de.json @@ -14,6 +14,7 @@ "data": { "country_code": "L\u00e4ndercode Ihres Kontos (z. B. 1 f\u00fcr USA oder 86 f\u00fcr China)", "password": "Passwort", + "platform": "Die App, in der Ihr Konto registriert ist", "username": "Benutzername" }, "description": "Gib deine Tuya-Anmeldeinformationen ein.", diff --git a/homeassistant/components/waze_travel_time/translations/ko.json b/homeassistant/components/waze_travel_time/translations/ko.json new file mode 100644 index 0000000000000..3596754ca0407 --- /dev/null +++ b/homeassistant/components/waze_travel_time/translations/ko.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "destination": "\ubaa9\uc801\uc9c0", + "origin": "\ucd9c\ubc1c\uc9c0", + "region": "\uc9c0\uc5ed" + }, + "description": "\ucd9c\ubc1c\uc9c0 \ubc0f \ub3c4\ucc29\uc9c0\uc758 \uacbd\uc6b0 \uc704\uce58\uc758 \uc8fc\uc18c \ub610\ub294 GPS \uc88c\ud45c\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694. (GPS \uc88c\ud45c\ub294 \uc27c\ud45c\ub85c \uad6c\ubd84\ud574\uc57c \ud569\ub2c8\ub2e4) \ub610\ub294 \uc774\ub7ec\ud55c \uc815\ubcf4\ub97c \ud574\ub2f9 \uc0c1\ud0dc\ub85c \uc81c\uacf5\ud558\ub294 \uad6c\uc131\uc694\uc18c ID\ub098 \uc704\ub3c4 \ubc0f \uacbd\ub3c4 \uc18d\uc131\uc744 \uac00\uc9c4 \uad6c\uc131\uc694\uc18c ID \ub610\ub294 \uc9c0\uc5ed \uc774\ub984\uc744 \uc785\ub825\ud560 \uc218\ub3c4 \uc788\uc2b5\ub2c8\ub2e4." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wiffi/translations/de.json b/homeassistant/components/wiffi/translations/de.json index 79bf8168a14a6..4084cda8f9f21 100644 --- a/homeassistant/components/wiffi/translations/de.json +++ b/homeassistant/components/wiffi/translations/de.json @@ -8,7 +8,8 @@ "user": { "data": { "port": "Server Port" - } + }, + "title": "TCP-Server f\u00fcr WIFFI-Ger\u00e4te einrichten" } } }, diff --git a/homeassistant/components/wolflink/translations/sensor.cs.json b/homeassistant/components/wolflink/translations/sensor.cs.json index 046fc4e6ed9a0..fff383f8b8d4d 100644 --- a/homeassistant/components/wolflink/translations/sensor.cs.json +++ b/homeassistant/components/wolflink/translations/sensor.cs.json @@ -3,7 +3,7 @@ "wolflink__state": { "aktiviert": "Aktivov\u00e1no", "aus": "Zak\u00e1z\u00e1no", - "auto": "Automatika", + "auto": "Auto", "automatik_aus": "Automatick\u00e9 vypnut\u00ed", "automatik_ein": "Automatick\u00e9 zapnut\u00ed", "cooling": "Chlazen\u00ed", From 19e047e80106b1c7986845e92a392728b6fcc7e4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 8 Apr 2021 19:24:35 -1000 Subject: [PATCH 0134/1317] Fix logic reversal in sonos update_media_radio (#48900) --- homeassistant/components/sonos/media_player.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 6e0fe6c7293b4..3ee458ec9db42 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -718,8 +718,11 @@ def update_media_radio(self, variables: dict) -> None: ) and ( self.state != STATE_PLAYING or self.soco.music_source_from_uri(self._media_title) == MUSIC_SRC_RADIO - and self._uri is not None - and self._media_title in self._uri # type: ignore[operator] + or ( + isinstance(self._media_title, str) + and isinstance(self._uri, str) + and self._media_title in self._uri + ) ): self._media_title = uri_meta_data.title except (TypeError, KeyError, AttributeError): From d1df6e6fbabdd1137016c28160a8a129a4caa1c8 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 9 Apr 2021 07:26:09 +0200 Subject: [PATCH 0135/1317] Don't get code_context when calling inspect.stack (#48849) * Don't get code_context when calling inspect.stack * Update homeassistant/helpers/config_validation.py --- homeassistant/helpers/config_validation.py | 2 +- homeassistant/helpers/deprecation.py | 2 +- homeassistant/util/logging.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 9b56bb068655c..21d04f1155179 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -711,7 +711,7 @@ def deprecated( - No warning if neither key nor replacement_key are provided - Adds replacement_key with default value in this case """ - module = inspect.getmodule(inspect.stack()[1][0]) + module = inspect.getmodule(inspect.stack(context=0)[1].frame) if module is not None: module_name = module.__name__ else: diff --git a/homeassistant/helpers/deprecation.py b/homeassistant/helpers/deprecation.py index 38b1dfca437d5..06f09327dc91f 100644 --- a/homeassistant/helpers/deprecation.py +++ b/homeassistant/helpers/deprecation.py @@ -59,7 +59,7 @@ def get_deprecated( and a warning is issued to the user. """ if old_name in config: - module = inspect.getmodule(inspect.stack()[1][0]) + module = inspect.getmodule(inspect.stack(context=0)[1].frame) if module is not None: module_name = module.__name__ else: diff --git a/homeassistant/util/logging.py b/homeassistant/util/logging.py index ba846c0e8b429..816af95718d6b 100644 --- a/homeassistant/util/logging.py +++ b/homeassistant/util/logging.py @@ -85,7 +85,7 @@ def _async_stop_queue_handler(_: Any) -> None: def log_exception(format_err: Callable[..., Any], *args: Any) -> None: """Log an exception with additional context.""" - module = inspect.getmodule(inspect.stack()[1][0]) + module = inspect.getmodule(inspect.stack(context=0)[1].frame) if module is not None: module_name = module.__name__ else: From e7e53b879e62224d5d61baac15b82a20799f1d5d Mon Sep 17 00:00:00 2001 From: Phil Hollenback Date: Fri, 9 Apr 2021 01:25:03 -0700 Subject: [PATCH 0136/1317] Fix cpu temperature reporting for Armbian on Odroid (#48903) Some systems expose cpu temperatures differently in psutil. Specifically, running armbian on the Odroid xu4 sbc gives the following temerature output: >>> pp.pprint(psutil.sensors_temperatures()) { 'cpu0-thermal': [ shwtemp(label='', current=54.0, high=115.0, critical=115.0)], 'cpu1-thermal': [ shwtemp(label='', current=56.0, high=115.0, critical=115.0)], 'cpu2-thermal': [ shwtemp(label='', current=58.0, high=115.0, critical=115.0)], 'cpu3-thermal': [ shwtemp(label='', current=56.0, high=115.0, critical=115.0)], } Since the cpu number is embedded inside the name, the current code can't find it. To fix this, check both the name and the constructed label for matches against CPU_SENSOR_PREFIXES, and add the appropriate label cpu0-thermal in the prefix list. While this is slightly less efficient that just generating the label and checking it, it results in easier to understand code. --- homeassistant/components/systemmonitor/sensor.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index dea7d371b4b97..94f747014a46d 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -180,6 +180,7 @@ def check_required_arg(value: Any) -> Any: "soc-thermal 1", "soc_thermal 1", "Tctl", + "cpu0-thermal", ] @@ -504,7 +505,9 @@ def _read_cpu_temperature() -> float | None: # In case the label is empty (e.g. on Raspberry PI 4), # construct it ourself here based on the sensor key name. _label = f"{name} {i}" if not entry.label else entry.label - if _label in CPU_SENSOR_PREFIXES: + # check both name and label because some systems embed cpu# in the + # name, which makes label not match because label adds cpu# at end. + if _label in CPU_SENSOR_PREFIXES or name in CPU_SENSOR_PREFIXES: return cast(float, round(entry.current, 1)) return None From 31ae121645f6d2fa4475ac3d5b03c0900979c176 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 9 Apr 2021 10:56:53 +0200 Subject: [PATCH 0137/1317] Add fixtures for Axis rtsp client and adapt tests to use them (#47901) * Add a fixture for rtsp client and adapt tests to use it * Better fixtures for RTSP events and signals --- homeassistant/components/axis/__init__.py | 4 +- homeassistant/components/axis/device.py | 4 +- tests/components/axis/conftest.py | 112 +++++++++++++++++++- tests/components/axis/test_binary_sensor.py | 51 ++++----- tests/components/axis/test_device.py | 40 +++++-- tests/components/axis/test_light.py | 56 +++++----- tests/components/axis/test_switch.py | 55 ++++++---- 7 files changed, 231 insertions(+), 91 deletions(-) diff --git a/homeassistant/components/axis/__init__.py b/homeassistant/components/axis/__init__.py index 378d02bcccdfa..acbdc2ca78254 100644 --- a/homeassistant/components/axis/__init__.py +++ b/homeassistant/components/axis/__init__.py @@ -26,7 +26,9 @@ async def async_setup_entry(hass, config_entry): await device.async_update_device_registry() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, device.shutdown) + device.listeners.append( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, device.shutdown) + ) return True diff --git a/homeassistant/components/axis/device.py b/homeassistant/components/axis/device.py index f732ad2fb5d7e..93b63b6412241 100644 --- a/homeassistant/components/axis/device.py +++ b/homeassistant/components/axis/device.py @@ -263,9 +263,7 @@ async def start_platforms(): def disconnect_from_stream(self): """Stop stream.""" if self.api.stream.state != STATE_STOPPED: - self.api.stream.connection_status_callback.remove( - self.async_connection_status_callback - ) + self.api.stream.connection_status_callback.clear() self.api.stream.stop() async def shutdown(self, event): diff --git a/tests/components/axis/conftest.py b/tests/components/axis/conftest.py index b3964663767f6..be4483593665b 100644 --- a/tests/components/axis/conftest.py +++ b/tests/components/axis/conftest.py @@ -1,2 +1,112 @@ -"""axis conftest.""" +"""Axis conftest.""" + +from typing import Optional +from unittest.mock import patch + +from axis.rtsp import ( + SIGNAL_DATA, + SIGNAL_FAILED, + SIGNAL_PLAYING, + STATE_PLAYING, + STATE_STOPPED, +) +import pytest + from tests.components.light.conftest import mock_light_profiles # noqa: F401 + + +@pytest.fixture(autouse=True) +def mock_axis_rtspclient(): + """No real RTSP communication allowed.""" + with patch("axis.streammanager.RTSPClient") as rtsp_client_mock: + + rtsp_client_mock.return_value.session.state = STATE_STOPPED + + async def start_stream(): + """Set state to playing when calling RTSPClient.start.""" + rtsp_client_mock.return_value.session.state = STATE_PLAYING + + rtsp_client_mock.return_value.start = start_stream + + def stop_stream(): + """Set state to stopped when calling RTSPClient.stop.""" + rtsp_client_mock.return_value.session.state = STATE_STOPPED + + rtsp_client_mock.return_value.stop = stop_stream + + def make_rtsp_call(data: Optional[dict] = None, state: str = ""): + """Generate a RTSP call.""" + axis_streammanager_session_callback = rtsp_client_mock.call_args[0][4] + + if data: + rtsp_client_mock.return_value.rtp.data = data + axis_streammanager_session_callback(signal=SIGNAL_DATA) + elif state: + axis_streammanager_session_callback(signal=state) + else: + raise NotImplementedError + + yield make_rtsp_call + + +@pytest.fixture(autouse=True) +def mock_rtsp_event(mock_axis_rtspclient): + """Fixture to allow mocking received RTSP events.""" + + def send_event( + topic: str, + data_type: str, + data_value: str, + operation: str = "Initialized", + source_name: str = "", + source_idx: str = "", + ) -> None: + source = "" + if source_name != "" and source_idx != "": + source = f'' + + event = f""" + + + + + {topic} + + + + uri://bf32a3b9-e5e7-4d57-a48d-1b5be9ae7b16/ProducerReference + + + + + {source} + + + + + + + + + +""" + + mock_axis_rtspclient(data=event.encode("utf-8")) + + yield send_event + + +@pytest.fixture(autouse=True) +def mock_rtsp_signal_state(mock_axis_rtspclient): + """Fixture to allow mocking RTSP state signalling.""" + + def send_signal(connected: bool) -> None: + """Signal state change of RTSP connection.""" + signal = SIGNAL_PLAYING if connected else SIGNAL_FAILED + mock_axis_rtspclient(state=signal) + + yield send_signal diff --git a/tests/components/axis/test_binary_sensor.py b/tests/components/axis/test_binary_sensor.py index 98ef55282c3aa..2429ec618554a 100644 --- a/tests/components/axis/test_binary_sensor.py +++ b/tests/components/axis/test_binary_sensor.py @@ -10,31 +10,6 @@ from .test_device import NAME, setup_axis_integration -EVENTS = [ - { - "operation": "Initialized", - "topic": "tns1:Device/tnsaxis:Sensor/PIR", - "source": "sensor", - "source_idx": "0", - "type": "state", - "value": "0", - }, - { - "operation": "Initialized", - "topic": "tns1:PTZController/tnsaxis:PTZPresets/Channel_1", - "source": "PresetToken", - "source_idx": "0", - "type": "on_preset", - "value": "1", - }, - { - "operation": "Initialized", - "topic": "tnsaxis:CameraApplicationPlatform/VMD/Camera1Profile1", - "type": "active", - "value": "1", - }, -] - async def test_platform_manually_configured(hass): """Test that nothing happens when platform is manually configured.""" @@ -57,12 +32,30 @@ async def test_no_binary_sensors(hass): assert not hass.states.async_entity_ids(BINARY_SENSOR_DOMAIN) -async def test_binary_sensors(hass): +async def test_binary_sensors(hass, mock_rtsp_event): """Test that sensors are loaded properly.""" - config_entry = await setup_axis_integration(hass) - device = hass.data[AXIS_DOMAIN][config_entry.unique_id] + await setup_axis_integration(hass) - device.api.event.update(EVENTS) + mock_rtsp_event( + topic="tns1:Device/tnsaxis:Sensor/PIR", + data_type="state", + data_value="0", + source_name="sensor", + source_idx="0", + ) + mock_rtsp_event( + topic="tnsaxis:CameraApplicationPlatform/VMD/Camera1Profile1", + data_type="active", + data_value="1", + ) + # Unsupported event + mock_rtsp_event( + topic="tns1:PTZController/tnsaxis:PTZPresets/Channel_1", + data_type="on_preset", + data_value="1", + source_name="PresetToken", + source_idx="0", + ) await hass.async_block_till_done() assert len(hass.states.async_entity_ids(BINARY_SENSOR_DOMAIN)) == 2 diff --git a/tests/components/axis/test_device.py b/tests/components/axis/test_device.py index a537139563854..cb6e5b1a12be0 100644 --- a/tests/components/axis/test_device.py +++ b/tests/components/axis/test_device.py @@ -23,7 +23,9 @@ CONF_PASSWORD, CONF_PORT, CONF_USERNAME, + STATE_OFF, STATE_ON, + STATE_UNAVAILABLE, ) from tests.common import MockConfigEntry, async_fire_mqtt_message @@ -288,7 +290,7 @@ async def setup_axis_integration(hass, config=ENTRY_CONFIG, options=ENTRY_OPTION ) config_entry.add_to_hass(hass) - with patch("axis.rtsp.RTSPClient.start", return_value=True), respx.mock: + with respx.mock: mock_default_vapix_requests(respx) await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -389,12 +391,38 @@ async def test_update_address(hass): assert len(mock_setup_entry.mock_calls) == 1 -async def test_device_unavailable(hass): +async def test_device_unavailable(hass, mock_rtsp_event, mock_rtsp_signal_state): """Successful setup.""" - config_entry = await setup_axis_integration(hass) - device = hass.data[AXIS_DOMAIN][config_entry.unique_id] - device.async_connection_status_callback(status=False) - assert not device.available + await setup_axis_integration(hass) + + # Provide an entity that can be used to verify connection state on + mock_rtsp_event( + topic="tns1:AudioSource/tnsaxis:TriggerLevel", + data_type="triggered", + data_value="10", + source_name="channel", + source_idx="1", + ) + await hass.async_block_till_done() + + assert hass.states.get(f"{BINARY_SENSOR_DOMAIN}.{NAME}_sound_1").state == STATE_OFF + + # Connection to device has failed + + mock_rtsp_signal_state(connected=False) + await hass.async_block_till_done() + + assert ( + hass.states.get(f"{BINARY_SENSOR_DOMAIN}.{NAME}_sound_1").state + == STATE_UNAVAILABLE + ) + + # Connection to device has been restored + + mock_rtsp_signal_state(connected=True) + await hass.async_block_till_done() + + assert hass.states.get(f"{BINARY_SENSOR_DOMAIN}.{NAME}_sound_1").state == STATE_OFF async def test_device_reset(hass): diff --git a/tests/components/axis/test_light.py b/tests/components/axis/test_light.py index db4ba86ceae43..db7ca6921fb88 100644 --- a/tests/components/axis/test_light.py +++ b/tests/components/axis/test_light.py @@ -27,24 +27,6 @@ "name": "Light Control", } -EVENT_ON = { - "operation": "Initialized", - "topic": "tns1:Device/tnsaxis:Light/Status", - "source": "id", - "source_idx": "0", - "type": "state", - "value": "ON", -} - -EVENT_OFF = { - "operation": "Initialized", - "topic": "tns1:Device/tnsaxis:Light/Status", - "source": "id", - "source_idx": "0", - "type": "state", - "value": "OFF", -} - async def test_platform_manually_configured(hass): """Test that nothing happens when platform is manually configured.""" @@ -62,7 +44,9 @@ async def test_no_lights(hass): assert not hass.states.async_entity_ids(LIGHT_DOMAIN) -async def test_no_light_entity_without_light_control_representation(hass): +async def test_no_light_entity_without_light_control_representation( + hass, mock_rtsp_event +): """Verify no lights entities get created without light control representation.""" api_discovery = deepcopy(API_DISCOVERY_RESPONSE) api_discovery["data"]["apiList"].append(API_DISCOVERY_LIGHT_CONTROL) @@ -73,23 +57,27 @@ async def test_no_light_entity_without_light_control_representation(hass): with patch.dict(API_DISCOVERY_RESPONSE, api_discovery), patch.dict( LIGHT_CONTROL_RESPONSE, light_control ): - config_entry = await setup_axis_integration(hass) - device = hass.data[AXIS_DOMAIN][config_entry.unique_id] - - device.api.event.update([EVENT_ON]) + await setup_axis_integration(hass) + + mock_rtsp_event( + topic="tns1:Device/tnsaxis:Light/Status", + data_type="state", + data_value="ON", + source_name="id", + source_idx="0", + ) await hass.async_block_till_done() assert not hass.states.async_entity_ids(LIGHT_DOMAIN) -async def test_lights(hass): +async def test_lights(hass, mock_rtsp_event): """Test that lights are loaded properly.""" api_discovery = deepcopy(API_DISCOVERY_RESPONSE) api_discovery["data"]["apiList"].append(API_DISCOVERY_LIGHT_CONTROL) with patch.dict(API_DISCOVERY_RESPONSE, api_discovery): - config_entry = await setup_axis_integration(hass) - device = hass.data[AXIS_DOMAIN][config_entry.unique_id] + await setup_axis_integration(hass) # Add light with patch( @@ -99,7 +87,13 @@ async def test_lights(hass): "axis.light_control.LightControl.get_valid_intensity", return_value={"data": {"ranges": [{"high": 150}]}}, ): - device.api.event.update([EVENT_ON]) + mock_rtsp_event( + topic="tns1:Device/tnsaxis:Light/Status", + data_type="state", + data_value="ON", + source_name="id", + source_idx="0", + ) await hass.async_block_till_done() assert len(hass.states.async_entity_ids(LIGHT_DOMAIN)) == 1 @@ -144,7 +138,13 @@ async def test_lights(hass): mock_deactivate.assert_called_once() # Event turn off light - device.api.event.update([EVENT_OFF]) + mock_rtsp_event( + topic="tns1:Device/tnsaxis:Light/Status", + data_type="state", + data_value="OFF", + source_name="id", + source_idx="0", + ) await hass.async_block_till_done() light_0 = hass.states.get(entity_id) diff --git a/tests/components/axis/test_switch.py b/tests/components/axis/test_switch.py index dcbe285cb54ce..541c377d3ffbb 100644 --- a/tests/components/axis/test_switch.py +++ b/tests/components/axis/test_switch.py @@ -21,25 +21,6 @@ setup_axis_integration, ) -EVENTS = [ - { - "operation": "Initialized", - "topic": "tns1:Device/Trigger/Relay", - "source": "RelayToken", - "source_idx": "0", - "type": "LogicalState", - "value": "inactive", - }, - { - "operation": "Initialized", - "topic": "tns1:Device/Trigger/Relay", - "source": "RelayToken", - "source_idx": "1", - "type": "LogicalState", - "value": "active", - }, -] - async def test_platform_manually_configured(hass): """Test that nothing happens when platform is manually configured.""" @@ -57,7 +38,7 @@ async def test_no_switches(hass): assert not hass.states.async_entity_ids(SWITCH_DOMAIN) -async def test_switches_with_port_cgi(hass): +async def test_switches_with_port_cgi(hass, mock_rtsp_event): """Test that switches are loaded properly using port.cgi.""" config_entry = await setup_axis_integration(hass) device = hass.data[AXIS_DOMAIN][config_entry.unique_id] @@ -68,7 +49,20 @@ async def test_switches_with_port_cgi(hass): device.api.vapix.ports["0"].close = AsyncMock() device.api.vapix.ports["1"].name = "" - device.api.event.update(EVENTS) + mock_rtsp_event( + topic="tns1:Device/Trigger/Relay", + data_type="LogicalState", + data_value="inactive", + source_name="RelayToken", + source_idx="0", + ) + mock_rtsp_event( + topic="tns1:Device/Trigger/Relay", + data_type="LogicalState", + data_value="active", + source_name="RelayToken", + source_idx="1", + ) await hass.async_block_till_done() assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2 @@ -100,7 +94,9 @@ async def test_switches_with_port_cgi(hass): device.api.vapix.ports["0"].open.assert_called_once() -async def test_switches_with_port_management(hass): +async def test_switches_with_port_management( + hass, mock_axis_rtspclient, mock_rtsp_event +): """Test that switches are loaded properly using port management.""" api_discovery = deepcopy(API_DISCOVERY_RESPONSE) api_discovery["data"]["apiList"].append(API_DISCOVERY_PORT_MANAGEMENT) @@ -115,7 +111,20 @@ async def test_switches_with_port_management(hass): device.api.vapix.ports["0"].close = AsyncMock() device.api.vapix.ports["1"].name = "" - device.api.event.update(EVENTS) + mock_rtsp_event( + topic="tns1:Device/Trigger/Relay", + data_type="LogicalState", + data_value="inactive", + source_name="RelayToken", + source_idx="0", + ) + mock_rtsp_event( + topic="tns1:Device/Trigger/Relay", + data_type="LogicalState", + data_value="active", + source_name="RelayToken", + source_idx="1", + ) await hass.async_block_till_done() assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2 From f396804f54c48f51a902e02fce1a882215489537 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 9 Apr 2021 11:27:43 +0200 Subject: [PATCH 0138/1317] Bump pykodi to 0.2.4 (#48913) --- homeassistant/components/kodi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/kodi/manifest.json b/homeassistant/components/kodi/manifest.json index 63282ed1a9a77..58d46aea8ba71 100644 --- a/homeassistant/components/kodi/manifest.json +++ b/homeassistant/components/kodi/manifest.json @@ -3,7 +3,7 @@ "name": "Kodi", "documentation": "https://www.home-assistant.io/integrations/kodi", "requirements": [ - "pykodi==0.2.3" + "pykodi==0.2.4" ], "codeowners": [ "@OnFreund", diff --git a/requirements_all.txt b/requirements_all.txt index dd7faca5a6100..9eb01b9360c48 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1482,7 +1482,7 @@ pykira==0.1.1 pykmtronic==0.0.3 # homeassistant.components.kodi -pykodi==0.2.3 +pykodi==0.2.4 # homeassistant.components.kulersky pykulersky==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3d7ce0f2684ec..f6b2714d5f839 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -802,7 +802,7 @@ pykira==0.1.1 pykmtronic==0.0.3 # homeassistant.components.kodi -pykodi==0.2.3 +pykodi==0.2.4 # homeassistant.components.kulersky pykulersky==0.5.2 From 52e8c7166b724126b39e22cbb8b52c27e8b8757c Mon Sep 17 00:00:00 2001 From: Brandon Rothweiler Date: Fri, 9 Apr 2021 05:36:02 -0400 Subject: [PATCH 0139/1317] Allow template covers to have opening and closing states (#47925) --- homeassistant/components/template/cover.py | 15 +++++++++++++++ tests/components/template/test_cover.py | 16 +++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index cd552a33e5d76..d985473792edc 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -219,6 +219,8 @@ def __init__( self._optimistic = optimistic or (not state_template and not position_template) self._tilt_optimistic = tilt_optimistic or not tilt_template self._position = None + self._is_opening = False + self._is_closing = False self._tilt_value = None self._unique_id = unique_id @@ -260,6 +262,9 @@ def _update_state(self, result): self._position = 100 else: self._position = 0 + + self._is_opening = state == STATE_OPENING + self._is_closing = state == STATE_CLOSING else: _LOGGER.error( "Received invalid cover is_on state: %s. Expected: %s", @@ -319,6 +324,16 @@ def is_closed(self): """Return if the cover is closed.""" return self._position == 0 + @property + def is_opening(self): + """Return if the cover is currently opening.""" + return self._is_opening + + @property + def is_closing(self): + """Return if the cover is currently closing.""" + return self._is_closing + @property def current_cover_position(self): """Return current position of cover. diff --git a/tests/components/template/test_cover.py b/tests/components/template/test_cover.py index 08c789633fc39..c1309a16e67b6 100644 --- a/tests/components/template/test_cover.py +++ b/tests/components/template/test_cover.py @@ -1,4 +1,4 @@ -"""The tests the cover command line platform.""" +"""The tests for the Template cover platform.""" import pytest from homeassistant import setup @@ -15,9 +15,11 @@ SERVICE_TOGGLE, SERVICE_TOGGLE_COVER_TILT, STATE_CLOSED, + STATE_CLOSING, STATE_OFF, STATE_ON, STATE_OPEN, + STATE_OPENING, STATE_UNAVAILABLE, ) @@ -74,6 +76,18 @@ async def test_template_state_text(hass, calls): state = hass.states.get("cover.test_template_cover") assert state.state == STATE_CLOSED + state = hass.states.async_set("cover.test_state", STATE_OPENING) + await hass.async_block_till_done() + + state = hass.states.get("cover.test_template_cover") + assert state.state == STATE_OPENING + + state = hass.states.async_set("cover.test_state", STATE_CLOSING) + await hass.async_block_till_done() + + state = hass.states.get("cover.test_template_cover") + assert state.state == STATE_CLOSING + async def test_template_state_boolean(hass, calls): """Test the value_template attribute.""" From 2391134d26a3eb152a227b1ec953c5b347422fb5 Mon Sep 17 00:00:00 2001 From: amitfin Date: Fri, 9 Apr 2021 13:26:55 +0300 Subject: [PATCH 0140/1317] Update "issur_melacha_in_effect" via time tracking (#42485) --- .../jewish_calendar/binary_sensor.py | 48 +++- .../jewish_calendar/test_binary_sensor.py | 213 ++++++++++++++++-- 2 files changed, 236 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/jewish_calendar/binary_sensor.py b/homeassistant/components/jewish_calendar/binary_sensor.py index 6edcc7b27c3ce..bda2bd5a1173d 100644 --- a/homeassistant/components/jewish_calendar/binary_sensor.py +++ b/homeassistant/components/jewish_calendar/binary_sensor.py @@ -1,7 +1,11 @@ """Support for Jewish Calendar binary sensors.""" +import datetime as dt + import hdate from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.core import callback +from homeassistant.helpers import event import homeassistant.util.dt as dt_util from . import DOMAIN, SENSOR_TYPES @@ -32,8 +36,8 @@ def __init__(self, data, sensor, sensor_info): self._hebrew = data["language"] == "hebrew" self._candle_lighting_offset = data["candle_lighting_offset"] self._havdalah_offset = data["havdalah_offset"] - self._state = False self._prefix = data["prefix"] + self._update_unsub = None @property def icon(self): @@ -53,11 +57,16 @@ def name(self): @property def is_on(self): """Return true if sensor is on.""" - return self._state + return self._get_zmanim().issur_melacha_in_effect - async def async_update(self): - """Update the state of the sensor.""" - zmanim = hdate.Zmanim( + @property + def should_poll(self): + """No polling needed.""" + return False + + def _get_zmanim(self): + """Return the Zmanim object for now().""" + return hdate.Zmanim( date=dt_util.now(), location=self._location, candle_lighting_offset=self._candle_lighting_offset, @@ -65,4 +74,31 @@ async def async_update(self): hebrew=self._hebrew, ) - self._state = zmanim.issur_melacha_in_effect + async def async_added_to_hass(self): + """Run when entity about to be added to hass.""" + await super().async_added_to_hass() + self._schedule_update() + + @callback + def _update(self, now=None): + """Update the state of the sensor.""" + self._update_unsub = None + self._schedule_update() + self.async_write_ha_state() + + def _schedule_update(self): + """Schedule the next update of the sensor.""" + now = dt_util.now() + zmanim = self._get_zmanim() + update = zmanim.zmanim["sunrise"] + dt.timedelta(days=1) + candle_lighting = zmanim.candle_lighting + if candle_lighting is not None and now < candle_lighting < update: + update = candle_lighting + havdalah = zmanim.havdalah + if havdalah is not None and now < havdalah < update: + update = havdalah + if self._update_unsub: + self._update_unsub() + self._update_unsub = event.async_track_point_in_time( + self.hass, self._update, update + ) diff --git a/tests/components/jewish_calendar/test_binary_sensor.py b/tests/components/jewish_calendar/test_binary_sensor.py index ca31381f164d7..c21211962263b 100644 --- a/tests/components/jewish_calendar/test_binary_sensor.py +++ b/tests/components/jewish_calendar/test_binary_sensor.py @@ -19,19 +19,118 @@ from tests.common import async_fire_time_changed MELACHA_PARAMS = [ - make_nyc_test_params(dt(2018, 9, 1, 16, 0), STATE_ON), - make_nyc_test_params(dt(2018, 9, 1, 20, 21), STATE_OFF), - make_nyc_test_params(dt(2018, 9, 7, 13, 1), STATE_OFF), - make_nyc_test_params(dt(2018, 9, 8, 21, 25), STATE_OFF), - make_nyc_test_params(dt(2018, 9, 9, 21, 25), STATE_ON), - make_nyc_test_params(dt(2018, 9, 10, 21, 25), STATE_ON), - make_nyc_test_params(dt(2018, 9, 28, 21, 25), STATE_ON), - make_nyc_test_params(dt(2018, 9, 29, 21, 25), STATE_OFF), - make_nyc_test_params(dt(2018, 9, 30, 21, 25), STATE_ON), - make_nyc_test_params(dt(2018, 10, 1, 21, 25), STATE_ON), - make_jerusalem_test_params(dt(2018, 9, 29, 21, 25), STATE_OFF), - make_jerusalem_test_params(dt(2018, 9, 30, 21, 25), STATE_ON), - make_jerusalem_test_params(dt(2018, 10, 1, 21, 25), STATE_OFF), + make_nyc_test_params( + dt(2018, 9, 1, 16, 0), + { + "state": STATE_ON, + "update": dt(2018, 9, 1, 20, 14), + "new_state": STATE_OFF, + }, + ), + make_nyc_test_params( + dt(2018, 9, 1, 20, 21), + { + "state": STATE_OFF, + "update": dt(2018, 9, 2, 6, 21), + "new_state": STATE_OFF, + }, + ), + make_nyc_test_params( + dt(2018, 9, 7, 13, 1), + { + "state": STATE_OFF, + "update": dt(2018, 9, 7, 19, 4), + "new_state": STATE_ON, + }, + ), + make_nyc_test_params( + dt(2018, 9, 8, 21, 25), + { + "state": STATE_OFF, + "update": dt(2018, 9, 9, 6, 27), + "new_state": STATE_OFF, + }, + ), + make_nyc_test_params( + dt(2018, 9, 9, 21, 25), + { + "state": STATE_ON, + "update": dt(2018, 9, 10, 6, 28), + "new_state": STATE_ON, + }, + ), + make_nyc_test_params( + dt(2018, 9, 10, 21, 25), + { + "state": STATE_ON, + "update": dt(2018, 9, 11, 6, 29), + "new_state": STATE_ON, + }, + ), + make_nyc_test_params( + dt(2018, 9, 11, 11, 25), + { + "state": STATE_ON, + "update": dt(2018, 9, 11, 19, 57), + "new_state": STATE_OFF, + }, + ), + make_nyc_test_params( + dt(2018, 9, 29, 16, 25), + { + "state": STATE_ON, + "update": dt(2018, 9, 29, 19, 25), + "new_state": STATE_OFF, + }, + ), + make_nyc_test_params( + dt(2018, 9, 29, 21, 25), + { + "state": STATE_OFF, + "update": dt(2018, 9, 30, 6, 48), + "new_state": STATE_OFF, + }, + ), + make_nyc_test_params( + dt(2018, 9, 30, 21, 25), + { + "state": STATE_ON, + "update": dt(2018, 10, 1, 6, 49), + "new_state": STATE_ON, + }, + ), + make_nyc_test_params( + dt(2018, 10, 1, 21, 25), + { + "state": STATE_ON, + "update": dt(2018, 10, 2, 6, 50), + "new_state": STATE_ON, + }, + ), + make_jerusalem_test_params( + dt(2018, 9, 29, 21, 25), + { + "state": STATE_OFF, + "update": dt(2018, 9, 30, 6, 29), + "new_state": STATE_OFF, + }, + ), + make_jerusalem_test_params( + dt(2018, 10, 1, 11, 25), + { + "state": STATE_ON, + "update": dt(2018, 10, 1, 19, 2), + "new_state": STATE_OFF, + }, + ), + make_jerusalem_test_params( + dt(2018, 10, 1, 21, 25), + { + "state": STATE_OFF, + "update": dt(2018, 10, 2, 6, 31), + "new_state": STATE_OFF, + }, + ), ] MELACHA_TEST_IDS = [ @@ -40,7 +139,8 @@ "friday_upcoming_shabbat", "upcoming_rosh_hashana", "currently_rosh_hashana", - "second_day_rosh_hashana", + "second_day_rosh_hashana_night", + "second_day_rosh_hashana_day", "currently_shabbat_chol_hamoed", "upcoming_two_day_yomtov_in_diaspora", "currently_first_day_of_two_day_yomtov_in_diaspora", @@ -103,13 +203,9 @@ async def test_issur_melacha_sensor( ) await hass.async_block_till_done() - future = dt_util.utcnow() + timedelta(seconds=30) - async_fire_time_changed(hass, future) - await hass.async_block_till_done() - assert ( hass.states.get("binary_sensor.test_issur_melacha_in_effect").state - == result + == result["state"] ) entity = registry.async_get("binary_sensor.test_issur_melacha_in_effect") target_uid = "_".join( @@ -129,3 +225,82 @@ async def test_issur_melacha_sensor( ) ) assert entity.unique_id == target_uid + + with alter_time(result["update"]): + async_fire_time_changed(hass, result["update"]) + await hass.async_block_till_done() + assert ( + hass.states.get("binary_sensor.test_issur_melacha_in_effect").state + == result["new_state"] + ) + + +@pytest.mark.parametrize( + [ + "now", + "candle_lighting", + "havdalah", + "diaspora", + "tzname", + "latitude", + "longitude", + "result", + ], + [ + make_nyc_test_params( + dt(2020, 10, 23, 17, 46, 59, 999999), [STATE_OFF, STATE_ON] + ), + make_nyc_test_params( + dt(2020, 10, 24, 18, 44, 59, 999999), [STATE_ON, STATE_OFF] + ), + ], + ids=["before_candle_lighting", "before_havdalah"], +) +async def test_issur_melacha_sensor_update( + hass, + legacy_patchable_time, + now, + candle_lighting, + havdalah, + diaspora, + tzname, + latitude, + longitude, + result, +): + """Test Issur Melacha sensor output.""" + time_zone = dt_util.get_time_zone(tzname) + test_time = time_zone.localize(now) + + hass.config.time_zone = time_zone + hass.config.latitude = latitude + hass.config.longitude = longitude + + with alter_time(test_time): + assert await async_setup_component( + hass, + jewish_calendar.DOMAIN, + { + "jewish_calendar": { + "name": "test", + "language": "english", + "diaspora": diaspora, + "candle_lighting_minutes_before_sunset": candle_lighting, + "havdalah_minutes_after_sunset": havdalah, + } + }, + ) + await hass.async_block_till_done() + assert ( + hass.states.get("binary_sensor.test_issur_melacha_in_effect").state + == result[0] + ) + + test_time += timedelta(microseconds=1) + with alter_time(test_time): + async_fire_time_changed(hass, test_time) + await hass.async_block_till_done() + assert ( + hass.states.get("binary_sensor.test_issur_melacha_in_effect").state + == result[1] + ) From e30cf8845993126a006792270453adcca06bef25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Fri, 9 Apr 2021 12:37:46 +0200 Subject: [PATCH 0141/1317] AEMET town timestamp should be UTC (#48916) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AEMET OpenData doesn't clarify if the hourly data timestamp is UTC or not, but after correctly formatting the town timestamp in ISO format, it is clear that the timestamp is provided as UTC value. Therefore, the only values not provided as UTC are the ones related to the specific daily and hourly forecast values. Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/aemet/weather_update_coordinator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/aemet/weather_update_coordinator.py b/homeassistant/components/aemet/weather_update_coordinator.py index a7ca0a1242247..7aab23488b56d 100644 --- a/homeassistant/components/aemet/weather_update_coordinator.py +++ b/homeassistant/components/aemet/weather_update_coordinator.py @@ -239,7 +239,7 @@ def _convert_weather_response(self, weather_response): return None elaborated = dt_util.parse_datetime( - weather_response.hourly[ATTR_DATA][0][AEMET_ATTR_ELABORATED] + weather_response.hourly[ATTR_DATA][0][AEMET_ATTR_ELABORATED] + "Z" ) now = dt_util.now() now_utc = dt_util.utcnow() From 155322584d2bc2203b0e658ae075a5e930af73b3 Mon Sep 17 00:00:00 2001 From: RenierM26 <66512715+RenierM26@users.noreply.github.com> Date: Fri, 9 Apr 2021 12:39:19 +0200 Subject: [PATCH 0142/1317] Update Ezviz Component (#45722) * Update Ezviz Component * Update Ezviz for pylint test * Update Ezviz component pylint tests * Update Ezviz component tests * Update Ezviz Component tests * Update Ezviz component pylint error * Fix ezviz component config flow tests * Update ezviz component * Update Ezviz component * Add sensor platforms * issue with requirements file * Update binary_sensor to include switches * Updates to Ezviz sensors * Removed enum private method. * Fix switch args * Update homeassistant/components/ezviz/switch.py Co-authored-by: Martin Hjelmare * config flow checks login info * Config_flow now imports ezviz from camera platform * Update test * Updated config_flow with unique_id and remove period from logging * Added two camera services and clarified service descryptions in services.yaml * Fixed variable name mistake with new service * Added french integration translation * Config_flow add camera rtsp credentials as seperate entities, with user step and import step * rerun hassfest after rebase * Removed region from legacy config schema, removed logging in camera platform setup that could contain credentials, removed unused constant. * Regenerate requirements * Fix tests and add config_flow import config test * Added addition test to config_flow to test successfull camera entity create. * Add to tests method to end in create entry, config_flow cleanup, use entry instead of entry.data * Removed all services, sorted platforms in init file. * Changed RTSP logging to debug from warning. (Forgot to change this before commit) * Cleanup typing, change platform order, bump pyezviz version * Added types to entries, allow creation of main entry if deleted by validating existance of type * Config_flow doesn't store serial under entry data, camera rtsp read from entry and not stored in hass, removed duplicate abort if unique id from config flow * Fix test of config_flow * Update tests/components/ezviz/test_config_flow.py Co-authored-by: Martin Hjelmare * Update tests/components/ezviz/test_config_flow.py Co-authored-by: Martin Hjelmare * Update tests/components/ezviz/test_config_flow.py Co-authored-by: Martin Hjelmare * Bumped pyezviz api version, added api pyezvizerror exception raised in api (on HTTPError), cleanup unused imports. * rebase * cleanup coordinator, bump pyezviz api version, move async_setup_entry to add entry options to camera entries. (order change) * Added discovery step in config_flow if cameras detected without rtsp config entry * Reload main integration after addition or completion of camera rtsp config entry * Add tests for discovery config_flow, added a few other output asserts * Camera platform call discover flow with hass.async_create_task. Fixes to config_flow for discovery step * Fix config_flow discovery, add check to legacy yaml camera platform import, move camera private method to camera import step * Remove not needed check from config_flow import step. * Cleanup config_flow * Added config_flow description for discovered camera * Reordered description in config_flow confim step. * Added serial to flow_step description for discovered camera, readded camera attributes for rtsp stream url (allows user to check RTSP cred), added local ip and firmware upgade available. * Bumped pyezviz version and changed region code to region url. (Russia uses a completly different url). PyEzviz adds a Local IP sensor, removed camera entity attributes. * Add RSTP describe auth check from API to config_flow * url as vol.in options in Config_flow * Config_flow changes to discovery step, added exceptions, fixed tests, added rtsp config validate module mock to test disovery confirm step * Add test for config_flow step user_camera * Added tests for abort flow * Extend tests on custom url flow step * Fix exceptions in config_flow, fix test for discovery import exception test * Bump pyezviz api version * Bump api version, added config_flow function to wake hybernating camera before testing credentials, removed "user camera" step from config flow not needed as cameras are discovered. * Create pyezviz Api instance for config_flow wake hybernating camera, fixed tests and added fixture to mock method * Added alarm_control_panel with support to arm/disarm all cameras, fixed camera is available attribute (returns 2 if unavailable, 1 if available) * Skip ignored entities when setup up camera RTSP stream * Remove alarm_control_panel, add additional config_flow tests * Cleanup tests, add tests for discovery_step. * Add test for config_flow rtsp test step1 exceptions * Removed redundant except from second step in test RTSP method * All tests to CREATE or ABORT, added step exception for general HTTP error so user can retry in case of trasient network condition * Ammended tests with output checks for step_id, error, data, create entry method calls. * bumped ezviz api now rases library exceptions. Config_flow, coordiantor and init raises library exceptions. Updated test sideeffect for library exceptions * Bump api version, Create mock ezviz cloud account on discovery tests first to allow more complete testing of step. * Add abort to rtsp verification method if cloud account was deleted and add tests * Update tests/components/ezviz/__init__.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/ezviz/const.py Co-authored-by: Martin Hjelmare * Update tests/components/ezviz/__init__.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/ezviz/camera.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/ezviz/camera.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/ezviz/camera.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/ezviz/camera.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/ezviz/camera.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/ezviz/camera.py Co-authored-by: Martin Hjelmare * Undo config import change to password key for yaml, move hass.data.setdefault to async_setup_entry and remove async_setup * Fixed tests by removing _patch_async_setup as this was removed from init. * Update homeassistant/components/ezviz/camera.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/ezviz/camera.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/ezviz/camera.py Co-authored-by: Martin Hjelmare * Changed L67 on camera config to complete suggestion for cleanup Co-authored-by: Martin Hjelmare --- .coveragerc | 8 +- CODEOWNERS | 2 +- homeassistant/components/ezviz/__init__.py | 130 ++++- .../components/ezviz/binary_sensor.py | 77 +++ homeassistant/components/ezviz/camera.py | 340 ++++++----- homeassistant/components/ezviz/config_flow.py | 374 ++++++++++++ homeassistant/components/ezviz/const.py | 42 ++ homeassistant/components/ezviz/coordinator.py | 38 ++ homeassistant/components/ezviz/manifest.json | 7 +- homeassistant/components/ezviz/sensor.py | 75 +++ homeassistant/components/ezviz/strings.json | 52 ++ homeassistant/components/ezviz/switch.py | 90 +++ .../components/ezviz/translations/en.json | 52 ++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/ezviz/__init__.py | 118 ++++ tests/components/ezviz/conftest.py | 48 ++ tests/components/ezviz/test_config_flow.py | 547 ++++++++++++++++++ 19 files changed, 1848 insertions(+), 159 deletions(-) create mode 100644 homeassistant/components/ezviz/binary_sensor.py create mode 100644 homeassistant/components/ezviz/config_flow.py create mode 100644 homeassistant/components/ezviz/const.py create mode 100644 homeassistant/components/ezviz/coordinator.py create mode 100644 homeassistant/components/ezviz/sensor.py create mode 100644 homeassistant/components/ezviz/strings.json create mode 100644 homeassistant/components/ezviz/switch.py create mode 100644 homeassistant/components/ezviz/translations/en.json create mode 100644 tests/components/ezviz/__init__.py create mode 100644 tests/components/ezviz/conftest.py create mode 100644 tests/components/ezviz/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 0292a6c14416b..6e3db6555efb6 100644 --- a/.coveragerc +++ b/.coveragerc @@ -274,7 +274,13 @@ omit = homeassistant/components/eufy/* homeassistant/components/everlights/light.py homeassistant/components/evohome/* - homeassistant/components/ezviz/* + homeassistant/components/ezviz/__init__.py + homeassistant/components/ezviz/camera.py + homeassistant/components/ezviz/coordinator.py + homeassistant/components/ezviz/const.py + homeassistant/components/ezviz/binary_sensor.py + homeassistant/components/ezviz/sensor.py + homeassistant/components/ezviz/switch.py homeassistant/components/familyhub/camera.py homeassistant/components/faa_delays/__init__.py homeassistant/components/faa_delays/binary_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index eaad0a975e4bd..5f2fd6588a627 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -148,7 +148,7 @@ homeassistant/components/eq3btsmart/* @rytilahti homeassistant/components/esphome/* @OttoWinter homeassistant/components/essent/* @TheLastProject homeassistant/components/evohome/* @zxdavb -homeassistant/components/ezviz/* @baqs +homeassistant/components/ezviz/* @RenierM26 @baqs homeassistant/components/faa_delays/* @ntilley905 homeassistant/components/fastdotcom/* @rohankapoorcom homeassistant/components/file/* @fabaff diff --git a/homeassistant/components/ezviz/__init__.py b/homeassistant/components/ezviz/__init__.py index 96891e8b291e4..7619d83e27bc8 100644 --- a/homeassistant/components/ezviz/__init__.py +++ b/homeassistant/components/ezviz/__init__.py @@ -1 +1,129 @@ -"""Support for Ezviz devices via Ezviz Cloud API.""" +"""Support for Ezviz camera.""" +import asyncio +from datetime import timedelta +import logging + +from pyezviz.client import EzvizClient, HTTPError, InvalidURL, PyEzvizError + +from homeassistant.const import ( + CONF_PASSWORD, + CONF_TIMEOUT, + CONF_TYPE, + CONF_URL, + CONF_USERNAME, +) +from homeassistant.exceptions import ConfigEntryNotReady + +from .const import ( + ATTR_TYPE_CAMERA, + ATTR_TYPE_CLOUD, + CONF_FFMPEG_ARGUMENTS, + DATA_COORDINATOR, + DATA_UNDO_UPDATE_LISTENER, + DEFAULT_FFMPEG_ARGUMENTS, + DEFAULT_TIMEOUT, + DOMAIN, +) +from .coordinator import EzvizDataUpdateCoordinator + +_LOGGER = logging.getLogger(__name__) + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) + +PLATFORMS = [ + "binary_sensor", + "camera", + "sensor", + "switch", +] + + +async def async_setup_entry(hass, entry): + """Set up Ezviz from a config entry.""" + hass.data.setdefault(DOMAIN, {}) + + if not entry.options: + options = { + CONF_FFMPEG_ARGUMENTS: entry.data.get( + CONF_FFMPEG_ARGUMENTS, DEFAULT_FFMPEG_ARGUMENTS + ), + CONF_TIMEOUT: entry.data.get(CONF_TIMEOUT, DEFAULT_TIMEOUT), + } + hass.config_entries.async_update_entry(entry, options=options) + + if entry.data.get(CONF_TYPE) == ATTR_TYPE_CAMERA: + if hass.data.get(DOMAIN): + # Should only execute on addition of new camera entry. + # Fetch Entry id of main account and reload it. + for item in hass.config_entries.async_entries(): + if item.data.get(CONF_TYPE) == ATTR_TYPE_CLOUD: + _LOGGER.info("Reload Ezviz integration with new camera rtsp entry") + await hass.config_entries.async_reload(item.entry_id) + + return True + + try: + ezviz_client = await hass.async_add_executor_job( + _get_ezviz_client_instance, entry + ) + except (InvalidURL, HTTPError, PyEzvizError) as error: + _LOGGER.error("Unable to connect to Ezviz service: %s", str(error)) + raise ConfigEntryNotReady from error + + coordinator = EzvizDataUpdateCoordinator(hass, api=ezviz_client) + await coordinator.async_refresh() + + if not coordinator.last_update_success: + raise ConfigEntryNotReady + + undo_listener = entry.add_update_listener(_async_update_listener) + + hass.data[DOMAIN][entry.entry_id] = { + DATA_COORDINATOR: coordinator, + DATA_UNDO_UPDATE_LISTENER: undo_listener, + } + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + + if entry.data.get(CONF_TYPE) == ATTR_TYPE_CAMERA: + return True + + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + + if unload_ok: + hass.data[DOMAIN][entry.entry_id][DATA_UNDO_UPDATE_LISTENER]() + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +async def _async_update_listener(hass, entry): + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) + + +def _get_ezviz_client_instance(entry): + """Initialize a new instance of EzvizClientApi.""" + ezviz_client = EzvizClient( + entry.data[CONF_USERNAME], + entry.data[CONF_PASSWORD], + entry.data[CONF_URL], + entry.options.get(CONF_TIMEOUT, DEFAULT_TIMEOUT), + ) + ezviz_client.login() + return ezviz_client diff --git a/homeassistant/components/ezviz/binary_sensor.py b/homeassistant/components/ezviz/binary_sensor.py new file mode 100644 index 0000000000000..9d8db7fbb30ee --- /dev/null +++ b/homeassistant/components/ezviz/binary_sensor.py @@ -0,0 +1,77 @@ +"""Support for Ezviz binary sensors.""" +import logging + +from pyezviz.constants import BinarySensorType + +from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DATA_COORDINATOR, DOMAIN, MANUFACTURER + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up Ezviz sensors based on a config entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] + sensors = [] + sensor_type_name = "None" + + for idx, camera in enumerate(coordinator.data): + for name in camera: + # Only add sensor with value. + if camera.get(name) is None: + continue + + if name in BinarySensorType.__members__: + sensor_type_name = getattr(BinarySensorType, name).value + sensors.append( + EzvizBinarySensor(coordinator, idx, name, sensor_type_name) + ) + + async_add_entities(sensors) + + +class EzvizBinarySensor(CoordinatorEntity, BinarySensorEntity): + """Representation of a Ezviz sensor.""" + + def __init__(self, coordinator, idx, name, sensor_type_name): + """Initialize the sensor.""" + super().__init__(coordinator) + self._idx = idx + self._camera_name = self.coordinator.data[self._idx]["name"] + self._name = name + self._sensor_name = f"{self._camera_name}.{self._name}" + self.sensor_type_name = sensor_type_name + self._serial = self.coordinator.data[self._idx]["serial"] + + @property + def name(self): + """Return the name of the Ezviz sensor.""" + return self._sensor_name + + @property + def is_on(self): + """Return the state of the sensor.""" + return self.coordinator.data[self._idx][self._name] + + @property + def unique_id(self): + """Return the unique ID of this sensor.""" + return f"{self._serial}_{self._sensor_name}" + + @property + def device_info(self): + """Return the device_info of the device.""" + return { + "identifiers": {(DOMAIN, self._serial)}, + "name": self.coordinator.data[self._idx]["name"], + "model": self.coordinator.data[self._idx]["device_sub_category"], + "manufacturer": MANUFACTURER, + "sw_version": self.coordinator.data[self._idx]["version"], + } + + @property + def device_class(self): + """Device class for the sensor.""" + return self.sensor_type_name diff --git a/homeassistant/components/ezviz/camera.py b/homeassistant/components/ezviz/camera.py index 4cce0e6865465..919ff5039b216 100644 --- a/homeassistant/components/ezviz/camera.py +++ b/homeassistant/components/ezviz/camera.py @@ -1,28 +1,30 @@ -"""This component provides basic support for Ezviz IP cameras.""" +"""Support ezviz camera devices.""" import asyncio +from datetime import timedelta import logging -# pylint: disable=import-error from haffmpeg.tools import IMAGE_JPEG, ImageFrame -from pyezviz.camera import EzvizCamera -from pyezviz.client import EzvizClient, PyEzvizError import voluptuous as vol from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_STREAM, Camera -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.components.ffmpeg import DATA_FFMPEG +from homeassistant.config_entries import SOURCE_DISCOVERY, SOURCE_IGNORE, SOURCE_IMPORT +from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers import config_validation as cv - -_LOGGER = logging.getLogger(__name__) - -CONF_CAMERAS = "cameras" - -DEFAULT_CAMERA_USERNAME = "admin" -DEFAULT_RTSP_PORT = "554" - -DATA_FFMPEG = "ffmpeg" - -EZVIZ_DATA = "ezviz" -ENTITIES = "entities" +from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import ( + ATTR_SERIAL, + CONF_CAMERAS, + CONF_FFMPEG_ARGUMENTS, + DATA_COORDINATOR, + DEFAULT_CAMERA_USERNAME, + DEFAULT_FFMPEG_ARGUMENTS, + DEFAULT_RTSP_PORT, + DOMAIN, + MANUFACTURER, +) CAMERA_SCHEMA = vol.Schema( {vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string} @@ -36,162 +38,162 @@ } ) +_LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Ezviz IP Cameras.""" - - conf_cameras = config[CONF_CAMERAS] +MIN_TIME_BETWEEN_SESSION_RENEW = timedelta(seconds=90) - account = config[CONF_USERNAME] - password = config[CONF_PASSWORD] - try: - ezviz_client = EzvizClient(account, password) - ezviz_client.login() - cameras = ezviz_client.load_cameras() +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up a Ezviz IP Camera from platform config.""" + _LOGGER.warning( + "Loading ezviz via platform config is deprecated, it will be automatically imported. Please remove it afterwards" + ) - except PyEzvizError as exp: - _LOGGER.error(exp) + # Check if entry config exists and skips import if it does. + if hass.config_entries.async_entries(DOMAIN): return - # now, let's build the HASS devices - camera_entities = [] + # Check if importing camera account. + if CONF_CAMERAS in config: + cameras_conf = config[CONF_CAMERAS] + for serial, camera in cameras_conf.items(): + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + ATTR_SERIAL: serial, + CONF_USERNAME: camera[CONF_USERNAME], + CONF_PASSWORD: camera[CONF_PASSWORD], + }, + ) + ) + + # Check if importing main ezviz cloud account. + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, + ) + ) + - # Add the cameras as devices in HASS - for camera in cameras: +async def async_setup_entry(hass, entry, async_add_entities): + """Set up Ezviz cameras based on a config entry.""" - camera_username = DEFAULT_CAMERA_USERNAME - camera_password = "" - camera_rtsp_stream = "" - camera_serial = camera["serial"] + coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] + camera_config_entries = hass.config_entries.async_entries(DOMAIN) + + camera_entities = [] + + for idx, camera in enumerate(coordinator.data): # There seem to be a bug related to localRtspPort in Ezviz API... local_rtsp_port = DEFAULT_RTSP_PORT - if camera["local_rtsp_port"] and camera["local_rtsp_port"] != 0: + + camera_rtsp_entry = [ + item + for item in camera_config_entries + if item.unique_id == camera[ATTR_SERIAL] + ] + + if camera["local_rtsp_port"] != 0: local_rtsp_port = camera["local_rtsp_port"] - if camera_serial in conf_cameras: - camera_username = conf_cameras[camera_serial][CONF_USERNAME] - camera_password = conf_cameras[camera_serial][CONF_PASSWORD] - camera_rtsp_stream = f"rtsp://{camera_username}:{camera_password}@{camera['local_ip']}:{local_rtsp_port}" + if camera_rtsp_entry: + conf_cameras = camera_rtsp_entry[0] + + # Skip ignored entities. + if conf_cameras.source == SOURCE_IGNORE: + continue + + ffmpeg_arguments = conf_cameras.options.get( + CONF_FFMPEG_ARGUMENTS, DEFAULT_FFMPEG_ARGUMENTS + ) + + camera_username = conf_cameras.data[CONF_USERNAME] + camera_password = conf_cameras.data[CONF_PASSWORD] + + camera_rtsp_stream = f"rtsp://{camera_username}:{camera_password}@{camera['local_ip']}:{local_rtsp_port}{ffmpeg_arguments}" _LOGGER.debug( - "Camera %s source stream: %s", camera["serial"], camera_rtsp_stream + "Camera %s source stream: %s", camera[ATTR_SERIAL], camera_rtsp_stream ) else: - _LOGGER.info( - "Found camera with serial %s without configuration. Add it to configuration.yaml to see the camera stream", - camera_serial, - ) - camera["username"] = camera_username - camera["password"] = camera_password - camera["rtsp_stream"] = camera_rtsp_stream - - camera["ezviz_camera"] = EzvizCamera(ezviz_client, camera_serial) - - camera_entities.append(HassEzvizCamera(**camera)) - - add_entities(camera_entities) - - -class HassEzvizCamera(Camera): - """An implementation of a Foscam IP camera.""" - - def __init__(self, **data): - """Initialize an Ezviz camera.""" - super().__init__() - - self._username = data["username"] - self._password = data["password"] - self._rtsp_stream = data["rtsp_stream"] - - self._ezviz_camera = data["ezviz_camera"] - self._serial = data["serial"] - self._name = data["name"] - self._status = data["status"] - self._privacy = data["privacy"] - self._audio = data["audio"] - self._ir_led = data["ir_led"] - self._state_led = data["state_led"] - self._follow_move = data["follow_move"] - self._alarm_notify = data["alarm_notify"] - self._alarm_sound_mod = data["alarm_sound_mod"] - self._encrypted = data["encrypted"] - self._local_ip = data["local_ip"] - self._detection_sensibility = data["detection_sensibility"] - self._device_sub_category = data["device_sub_category"] - self._local_rtsp_port = data["local_rtsp_port"] - - self._ffmpeg = None - - def update(self): - """Update the camera states.""" - - data = self._ezviz_camera.status() - - self._name = data["name"] - self._status = data["status"] - self._privacy = data["privacy"] - self._audio = data["audio"] - self._ir_led = data["ir_led"] - self._state_led = data["state_led"] - self._follow_move = data["follow_move"] - self._alarm_notify = data["alarm_notify"] - self._alarm_sound_mod = data["alarm_sound_mod"] - self._encrypted = data["encrypted"] - self._local_ip = data["local_ip"] - self._detection_sensibility = data["detection_sensibility"] - self._device_sub_category = data["device_sub_category"] - self._local_rtsp_port = data["local_rtsp_port"] - - async def async_added_to_hass(self): - """Subscribe to ffmpeg and add camera to list.""" - self._ffmpeg = self.hass.data[DATA_FFMPEG] + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DISCOVERY}, + data={ + ATTR_SERIAL: camera[ATTR_SERIAL], + CONF_IP_ADDRESS: camera["local_ip"], + }, + ) + ) - @property - def should_poll(self) -> bool: - """Return True if entity has to be polled for state. + camera_username = DEFAULT_CAMERA_USERNAME + camera_password = "" + camera_rtsp_stream = "" + ffmpeg_arguments = DEFAULT_FFMPEG_ARGUMENTS + _LOGGER.warning( + "Found camera with serial %s without configuration. Please go to integration to complete setup", + camera[ATTR_SERIAL], + ) - False if entity pushes its state to HA. - """ - return True + camera_entities.append( + EzvizCamera( + hass, + coordinator, + idx, + camera_username, + camera_password, + camera_rtsp_stream, + local_rtsp_port, + ffmpeg_arguments, + ) + ) - @property - def extra_state_attributes(self): - """Return the Ezviz-specific camera state attributes.""" - return { - # if privacy == true, the device closed the lid or did a 180° tilt - "privacy": self._privacy, - # is the camera listening ? - "audio": self._audio, - # infrared led on ? - "ir_led": self._ir_led, - # state led on ? - "state_led": self._state_led, - # if true, the camera will move automatically to follow movements - "follow_move": self._follow_move, - # if true, if some movement is detected, the app is notified - "alarm_notify": self._alarm_notify, - # if true, if some movement is detected, the camera makes some sound - "alarm_sound_mod": self._alarm_sound_mod, - # are the camera's stored videos/images encrypted? - "encrypted": self._encrypted, - # camera's local ip on local network - "local_ip": self._local_ip, - # from 1 to 9, the higher is the sensibility, the more it will detect small movements - "detection_sensibility": self._detection_sensibility, - } + async_add_entities(camera_entities) + + +class EzvizCamera(CoordinatorEntity, Camera, RestoreEntity): + """An implementation of a Ezviz security camera.""" + + def __init__( + self, + hass, + coordinator, + idx, + camera_username, + camera_password, + camera_rtsp_stream, + local_rtsp_port, + ffmpeg_arguments, + ): + """Initialize a Ezviz security camera.""" + super().__init__(coordinator) + Camera.__init__(self) + self._username = camera_username + self._password = camera_password + self._rtsp_stream = camera_rtsp_stream + self._idx = idx + self._ffmpeg = hass.data[DATA_FFMPEG] + self._local_rtsp_port = local_rtsp_port + self._ffmpeg_arguments = ffmpeg_arguments + + self._serial = self.coordinator.data[self._idx]["serial"] + self._name = self.coordinator.data[self._idx]["name"] + self._local_ip = self.coordinator.data[self._idx]["local_ip"] @property def available(self): """Return True if entity is available.""" - return self._status + if self.coordinator.data[self._idx]["status"] == 2: + return False - @property - def brand(self): - """Return the camera brand.""" - return "Ezviz" + return True @property def supported_features(self): @@ -200,20 +202,40 @@ def supported_features(self): return SUPPORT_STREAM return 0 + @property + def name(self): + """Return the name of this device.""" + return self._name + @property def model(self): - """Return the camera model.""" - return self._device_sub_category + """Return the model of this device.""" + return self.coordinator.data[self._idx]["device_sub_category"] + + @property + def brand(self): + """Return the manufacturer of this device.""" + return MANUFACTURER @property def is_on(self): """Return true if on.""" - return self._status + return bool(self.coordinator.data[self._idx]["status"]) @property - def name(self): + def is_recording(self): + """Return true if the device is recording.""" + return self.coordinator.data[self._idx]["alarm_notify"] + + @property + def motion_detection_enabled(self): + """Camera Motion Detection Status.""" + return self.coordinator.data[self._idx]["alarm_notify"] + + @property + def unique_id(self): """Return the name of this camera.""" - return self._name + return self._serial async def async_camera_image(self): """Return a frame from the camera stream.""" @@ -224,12 +246,24 @@ async def async_camera_image(self): ) return image + @property + def device_info(self): + """Return the device_info of the device.""" + return { + "identifiers": {(DOMAIN, self._serial)}, + "name": self.coordinator.data[self._idx]["name"], + "model": self.coordinator.data[self._idx]["device_sub_category"], + "manufacturer": MANUFACTURER, + "sw_version": self.coordinator.data[self._idx]["version"], + } + async def stream_source(self): """Return the stream source.""" + local_ip = self.coordinator.data[self._idx]["local_ip"] if self._local_rtsp_port: rtsp_stream_source = ( f"rtsp://{self._username}:{self._password}@" - f"{self._local_ip}:{self._local_rtsp_port}" + f"{local_ip}:{self._local_rtsp_port}{self._ffmpeg_arguments}" ) _LOGGER.debug( "Camera %s source stream: %s", self._serial, rtsp_stream_source diff --git a/homeassistant/components/ezviz/config_flow.py b/homeassistant/components/ezviz/config_flow.py new file mode 100644 index 0000000000000..ba514879703f0 --- /dev/null +++ b/homeassistant/components/ezviz/config_flow.py @@ -0,0 +1,374 @@ +"""Config flow for ezviz.""" +import logging + +from pyezviz.client import EzvizClient, HTTPError, InvalidURL, PyEzvizError +from pyezviz.test_cam_rtsp import AuthTestResultFailed, InvalidHost, TestRTSPAuth +import voluptuous as vol + +from homeassistant.config_entries import CONN_CLASS_CLOUD_POLL, ConfigFlow, OptionsFlow +from homeassistant.const import ( + CONF_CUSTOMIZE, + CONF_IP_ADDRESS, + CONF_PASSWORD, + CONF_TIMEOUT, + CONF_TYPE, + CONF_URL, + CONF_USERNAME, +) +from homeassistant.core import callback + +from .const import ( # pylint: disable=unused-import + ATTR_SERIAL, + ATTR_TYPE_CAMERA, + ATTR_TYPE_CLOUD, + CONF_FFMPEG_ARGUMENTS, + DEFAULT_CAMERA_USERNAME, + DEFAULT_FFMPEG_ARGUMENTS, + DEFAULT_TIMEOUT, + DOMAIN, + EU_URL, + RUSSIA_URL, +) + +_LOGGER = logging.getLogger(__name__) + + +def _get_ezviz_client_instance(data): + """Initialize a new instance of EzvizClientApi.""" + + ezviz_client = EzvizClient( + data[CONF_USERNAME], + data[CONF_PASSWORD], + data.get(CONF_URL, EU_URL), + data.get(CONF_TIMEOUT, DEFAULT_TIMEOUT), + ) + + ezviz_client.login() + return ezviz_client + + +def _test_camera_rtsp_creds(data): + """Try DESCRIBE on RTSP camera with credentials.""" + + test_rtsp = TestRTSPAuth( + data[CONF_IP_ADDRESS], data[CONF_USERNAME], data[CONF_PASSWORD] + ) + + test_rtsp.main() + + +class EzvizConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Ezviz.""" + + VERSION = 1 + CONNECTION_CLASS = CONN_CLASS_CLOUD_POLL + + async def _validate_and_create_auth(self, data): + """Try to login to ezviz cloud account and create entry if successful.""" + await self.async_set_unique_id(data[CONF_USERNAME]) + self._abort_if_unique_id_configured() + + # Verify cloud credentials by attempting a login request. + try: + await self.hass.async_add_executor_job(_get_ezviz_client_instance, data) + + except InvalidURL as err: + raise InvalidURL from err + + except HTTPError as err: + raise InvalidHost from err + + except PyEzvizError as err: + raise PyEzvizError from err + + auth_data = { + CONF_USERNAME: data[CONF_USERNAME], + CONF_PASSWORD: data[CONF_PASSWORD], + CONF_URL: data.get(CONF_URL, EU_URL), + CONF_TYPE: ATTR_TYPE_CLOUD, + } + + return self.async_create_entry(title=data[CONF_USERNAME], data=auth_data) + + async def _validate_and_create_camera_rtsp(self, data): + """Try DESCRIBE on RTSP camera with credentials.""" + + # Get Ezviz cloud credentials from config entry + ezviz_client_creds = { + CONF_USERNAME: None, + CONF_PASSWORD: None, + CONF_URL: None, + } + + for item in self._async_current_entries(): + if item.data.get(CONF_TYPE) == ATTR_TYPE_CLOUD: + ezviz_client_creds = { + CONF_USERNAME: item.data.get(CONF_USERNAME), + CONF_PASSWORD: item.data.get(CONF_PASSWORD), + CONF_URL: item.data.get(CONF_URL), + } + + # Abort flow if user removed cloud account before adding camera. + if ezviz_client_creds[CONF_USERNAME] is None: + return self.async_abort(reason="ezviz_cloud_account_missing") + + # We need to wake hibernating cameras. + # First create EZVIZ API instance. + try: + ezviz_client = await self.hass.async_add_executor_job( + _get_ezviz_client_instance, ezviz_client_creds + ) + + except InvalidURL as err: + raise InvalidURL from err + + except HTTPError as err: + raise InvalidHost from err + + except PyEzvizError as err: + raise PyEzvizError from err + + # Secondly try to wake hybernating camera. + try: + await self.hass.async_add_executor_job( + ezviz_client.get_detection_sensibility, data[ATTR_SERIAL] + ) + + except HTTPError as err: + raise InvalidHost from err + + # Thirdly attempts an authenticated RTSP DESCRIBE request. + try: + await self.hass.async_add_executor_job(_test_camera_rtsp_creds, data) + + except InvalidHost as err: + raise InvalidHost from err + + except AuthTestResultFailed as err: + raise AuthTestResultFailed from err + + return self.async_create_entry( + title=data[ATTR_SERIAL], + data={ + CONF_USERNAME: data[CONF_USERNAME], + CONF_PASSWORD: data[CONF_PASSWORD], + CONF_TYPE: ATTR_TYPE_CAMERA, + }, + ) + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return EzvizOptionsFlowHandler(config_entry) + + async def async_step_user(self, user_input=None): + """Handle a flow initiated by the user.""" + + # Check if ezviz cloud account is present in entry config, + # abort if already configured. + for item in self._async_current_entries(): + if item.data.get(CONF_TYPE) == ATTR_TYPE_CLOUD: + return self.async_abort(reason="already_configured_account") + + errors = {} + + if user_input is not None: + + if user_input[CONF_URL] == CONF_CUSTOMIZE: + self.context["data"] = { + CONF_USERNAME: user_input[CONF_USERNAME], + CONF_PASSWORD: user_input[CONF_PASSWORD], + } + return await self.async_step_user_custom_url() + + if CONF_TIMEOUT not in user_input: + user_input[CONF_TIMEOUT] = DEFAULT_TIMEOUT + + try: + return await self._validate_and_create_auth(user_input) + + except InvalidURL: + errors["base"] = "invalid_host" + + except InvalidHost: + errors["base"] = "cannot_connect" + + except PyEzvizError: + errors["base"] = "invalid_auth" + + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + return self.async_abort(reason="unknown") + + data_schema = vol.Schema( + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_URL, default=EU_URL): vol.In( + [EU_URL, RUSSIA_URL, CONF_CUSTOMIZE] + ), + } + ) + + return self.async_show_form( + step_id="user", data_schema=data_schema, errors=errors + ) + + async def async_step_user_custom_url(self, user_input=None): + """Handle a flow initiated by the user for custom region url.""" + + errors = {} + + if user_input is not None: + user_input[CONF_USERNAME] = self.context["data"][CONF_USERNAME] + user_input[CONF_PASSWORD] = self.context["data"][CONF_PASSWORD] + + if CONF_TIMEOUT not in user_input: + user_input[CONF_TIMEOUT] = DEFAULT_TIMEOUT + + try: + return await self._validate_and_create_auth(user_input) + + except InvalidURL: + errors["base"] = "invalid_host" + + except InvalidHost: + errors["base"] = "cannot_connect" + + except PyEzvizError: + errors["base"] = "invalid_auth" + + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + return self.async_abort(reason="unknown") + + data_schema_custom_url = vol.Schema( + { + vol.Required(CONF_URL, default=EU_URL): str, + } + ) + + return self.async_show_form( + step_id="user_custom_url", data_schema=data_schema_custom_url, errors=errors + ) + + async def async_step_discovery(self, discovery_info): + """Handle a flow for discovered camera without rtsp config entry.""" + + await self.async_set_unique_id(discovery_info[ATTR_SERIAL]) + self._abort_if_unique_id_configured() + + self.context["title_placeholders"] = {"serial": self.unique_id} + self.context["data"] = {CONF_IP_ADDRESS: discovery_info[CONF_IP_ADDRESS]} + + return await self.async_step_confirm() + + async def async_step_confirm(self, user_input=None): + """Confirm and create entry from discovery step.""" + errors = {} + + if user_input is not None: + user_input[ATTR_SERIAL] = self.unique_id + user_input[CONF_IP_ADDRESS] = self.context["data"][CONF_IP_ADDRESS] + try: + return await self._validate_and_create_camera_rtsp(user_input) + + except (InvalidHost, InvalidURL): + errors["base"] = "invalid_host" + + except (PyEzvizError, AuthTestResultFailed): + errors["base"] = "invalid_auth" + + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + return self.async_abort(reason="unknown") + + discovered_camera_schema = vol.Schema( + { + vol.Required(CONF_USERNAME, default=DEFAULT_CAMERA_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } + ) + + return self.async_show_form( + step_id="confirm", + data_schema=discovered_camera_schema, + errors=errors, + description_placeholders={ + "serial": self.unique_id, + CONF_IP_ADDRESS: self.context["data"][CONF_IP_ADDRESS], + }, + ) + + async def async_step_import(self, import_config): + """Handle config import from yaml.""" + _LOGGER.debug("import config: %s", import_config) + + # Check importing camera. + if ATTR_SERIAL in import_config: + return await self.async_step_import_camera(import_config) + + # Validate and setup of main ezviz cloud account. + try: + return await self._validate_and_create_auth(import_config) + + except InvalidURL: + _LOGGER.error("Error importing Ezviz platform config: invalid host") + return self.async_abort(reason="invalid_host") + + except InvalidHost: + _LOGGER.error("Error importing Ezviz platform config: cannot connect") + return self.async_abort(reason="cannot_connect") + + except (AuthTestResultFailed, PyEzvizError): + _LOGGER.error("Error importing Ezviz platform config: invalid auth") + return self.async_abort(reason="invalid_auth") + + except Exception: # pylint: disable=broad-except + _LOGGER.exception( + "Error importing ezviz platform config: unexpected exception" + ) + + return self.async_abort(reason="unknown") + + async def async_step_import_camera(self, data): + """Create RTSP auth entry per camera in config.""" + + await self.async_set_unique_id(data[ATTR_SERIAL]) + self._abort_if_unique_id_configured() + + _LOGGER.debug("Create camera with: %s", data) + + cam_serial = data.pop(ATTR_SERIAL) + data[CONF_TYPE] = ATTR_TYPE_CAMERA + + return self.async_create_entry(title=cam_serial, data=data) + + +class EzvizOptionsFlowHandler(OptionsFlow): + """Handle Ezviz client options.""" + + def __init__(self, config_entry): + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Manage Ezviz options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + options = { + vol.Optional( + CONF_TIMEOUT, + default=self.config_entry.options.get(CONF_TIMEOUT, DEFAULT_TIMEOUT), + ): int, + vol.Optional( + CONF_FFMPEG_ARGUMENTS, + default=self.config_entry.options.get( + CONF_FFMPEG_ARGUMENTS, DEFAULT_FFMPEG_ARGUMENTS + ), + ): str, + } + + return self.async_show_form(step_id="init", data_schema=vol.Schema(options)) diff --git a/homeassistant/components/ezviz/const.py b/homeassistant/components/ezviz/const.py new file mode 100644 index 0000000000000..c307f0693f6b5 --- /dev/null +++ b/homeassistant/components/ezviz/const.py @@ -0,0 +1,42 @@ +"""Constants for the ezviz integration.""" + +DOMAIN = "ezviz" +MANUFACTURER = "Ezviz" + +# Configuration +ATTR_SERIAL = "serial" +CONF_CAMERAS = "cameras" +ATTR_SWITCH = "switch" +ATTR_ENABLE = "enable" +ATTR_DIRECTION = "direction" +ATTR_SPEED = "speed" +ATTR_LEVEL = "level" +ATTR_TYPE = "type_value" +DIR_UP = "up" +DIR_DOWN = "down" +DIR_LEFT = "left" +DIR_RIGHT = "right" +ATTR_LIGHT = "LIGHT" +ATTR_SOUND = "SOUND" +ATTR_INFRARED_LIGHT = "INFRARED_LIGHT" +ATTR_PRIVACY = "PRIVACY" +ATTR_SLEEP = "SLEEP" +ATTR_MOBILE_TRACKING = "MOBILE_TRACKING" +ATTR_TRACKING = "TRACKING" +CONF_FFMPEG_ARGUMENTS = "ffmpeg_arguments" +ATTR_HOME = "HOME_MODE" +ATTR_AWAY = "AWAY_MODE" +ATTR_TYPE_CLOUD = "EZVIZ_CLOUD_ACCOUNT" +ATTR_TYPE_CAMERA = "CAMERA_ACCOUNT" + +# Defaults +EU_URL = "apiieu.ezvizlife.com" +RUSSIA_URL = "apirus.ezvizru.com" +DEFAULT_CAMERA_USERNAME = "admin" +DEFAULT_RTSP_PORT = "554" +DEFAULT_TIMEOUT = 25 +DEFAULT_FFMPEG_ARGUMENTS = "" + +# Data +DATA_COORDINATOR = "coordinator" +DATA_UNDO_UPDATE_LISTENER = "undo_update_listener" diff --git a/homeassistant/components/ezviz/coordinator.py b/homeassistant/components/ezviz/coordinator.py new file mode 100644 index 0000000000000..2fc9f6c9f825b --- /dev/null +++ b/homeassistant/components/ezviz/coordinator.py @@ -0,0 +1,38 @@ +"""Provides the ezviz DataUpdateCoordinator.""" +from datetime import timedelta +import logging + +from async_timeout import timeout +from pyezviz.client import HTTPError, InvalidURL, PyEzvizError + +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class EzvizDataUpdateCoordinator(DataUpdateCoordinator): + """Class to manage fetching Ezviz data.""" + + def __init__(self, hass, *, api): + """Initialize global Ezviz data updater.""" + self.ezviz_client = api + update_interval = timedelta(seconds=30) + + super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) + + def _update_data(self): + """Fetch data from Ezviz via camera load function.""" + cameras = self.ezviz_client.load_cameras() + + return cameras + + async def _async_update_data(self): + """Fetch data from Ezviz.""" + try: + async with timeout(35): + return await self.hass.async_add_executor_job(self._update_data) + + except (InvalidURL, HTTPError, PyEzvizError) as error: + raise UpdateFailed(f"Invalid response from API: {error}") from error diff --git a/homeassistant/components/ezviz/manifest.json b/homeassistant/components/ezviz/manifest.json index 03bdfc5217c8b..32742de203578 100644 --- a/homeassistant/components/ezviz/manifest.json +++ b/homeassistant/components/ezviz/manifest.json @@ -1,8 +1,9 @@ { - "disabled": "Dependency contains code that breaks Home Assistant.", "domain": "ezviz", "name": "Ezviz", "documentation": "https://www.home-assistant.io/integrations/ezviz", - "codeowners": ["@baqs"], - "requirements": ["pyezviz==0.1.5"] + "dependencies": ["ffmpeg"], + "codeowners": ["@RenierM26", "@baqs"], + "requirements": ["pyezviz==0.1.8.7"], + "config_flow": true } diff --git a/homeassistant/components/ezviz/sensor.py b/homeassistant/components/ezviz/sensor.py new file mode 100644 index 0000000000000..f4f9f6588f01e --- /dev/null +++ b/homeassistant/components/ezviz/sensor.py @@ -0,0 +1,75 @@ +"""Support for Ezviz sensors.""" +import logging + +from pyezviz.constants import SensorType + +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DATA_COORDINATOR, DOMAIN, MANUFACTURER + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up Ezviz sensors based on a config entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] + sensors = [] + sensor_type_name = "None" + + for idx, camera in enumerate(coordinator.data): + for name in camera: + # Only add sensor with value. + if camera.get(name) is None: + continue + + if name in SensorType.__members__: + sensor_type_name = getattr(SensorType, name).value + sensors.append(EzvizSensor(coordinator, idx, name, sensor_type_name)) + + async_add_entities(sensors) + + +class EzvizSensor(CoordinatorEntity, Entity): + """Representation of a Ezviz sensor.""" + + def __init__(self, coordinator, idx, name, sensor_type_name): + """Initialize the sensor.""" + super().__init__(coordinator) + self._idx = idx + self._camera_name = self.coordinator.data[self._idx]["name"] + self._name = name + self._sensor_name = f"{self._camera_name}.{self._name}" + self.sensor_type_name = sensor_type_name + self._serial = self.coordinator.data[self._idx]["serial"] + + @property + def name(self): + """Return the name of the Ezviz sensor.""" + return self._sensor_name + + @property + def state(self): + """Return the state of the sensor.""" + return self.coordinator.data[self._idx][self._name] + + @property + def unique_id(self): + """Return the unique ID of this sensor.""" + return f"{self._serial}_{self._sensor_name}" + + @property + def device_info(self): + """Return the device_info of the device.""" + return { + "identifiers": {(DOMAIN, self._serial)}, + "name": self.coordinator.data[self._idx]["name"], + "model": self.coordinator.data[self._idx]["device_sub_category"], + "manufacturer": MANUFACTURER, + "sw_version": self.coordinator.data[self._idx]["version"], + } + + @property + def device_class(self): + """Device class for the sensor.""" + return self.sensor_type_name diff --git a/homeassistant/components/ezviz/strings.json b/homeassistant/components/ezviz/strings.json new file mode 100644 index 0000000000000..a8831d2ae3435 --- /dev/null +++ b/homeassistant/components/ezviz/strings.json @@ -0,0 +1,52 @@ +{ + "config": { + "flow_title": "{serial}", + "step": { + "user": { + "title": "Connect to Ezviz Cloud", + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "url": "[%key:common::config_flow::data::url%]" + } + }, + "user_custom_url": { + "title": "Connect to custom Ezviz URL", + "description": "Manually specify your region URL", + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "url": "[%key:common::config_flow::data::url%]" + } + }, + "confirm": { + "title": "Discovered Ezviz Camera", + "description": "Enter RTSP credentials for Ezviz camera {serial} with IP {ip_address}", + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "invalid_host": "[%key:common::config_flow::error::invalid_host%]" + }, + "abort": { + "already_configured_account": "[%key:common::config_flow::abort::already_configured_account%]", + "unknown": "[%key:common::config_flow::error::unknown%]", + "ezviz_cloud_account_missing": "Ezviz cloud account missing. Please reconfigure Ezviz cloud account" + } + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "Request Timeout (seconds)", + "ffmpeg_arguments": "Arguments passed to ffmpeg for cameras" + } + } + } + } +} diff --git a/homeassistant/components/ezviz/switch.py b/homeassistant/components/ezviz/switch.py new file mode 100644 index 0000000000000..00230a3ac2d50 --- /dev/null +++ b/homeassistant/components/ezviz/switch.py @@ -0,0 +1,90 @@ +"""Support for Ezviz Switch sensors.""" +import logging + +from pyezviz.constants import DeviceSwitchType + +from homeassistant.components.switch import DEVICE_CLASS_SWITCH, SwitchEntity +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DATA_COORDINATOR, DOMAIN, MANUFACTURER + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up Ezviz switch based on a config entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] + switch_entities = [] + supported_switches = [] + + for switches in DeviceSwitchType: + supported_switches.append(switches.value) + + supported_switches = set(supported_switches) + + for idx, camera in enumerate(coordinator.data): + if not camera.get("switches"): + continue + for switch in camera["switches"]: + if switch not in supported_switches: + continue + switch_entities.append(EzvizSwitch(coordinator, idx, switch)) + + async_add_entities(switch_entities) + + +class EzvizSwitch(CoordinatorEntity, SwitchEntity): + """Representation of a Ezviz sensor.""" + + def __init__(self, coordinator, idx, switch): + """Initialize the switch.""" + super().__init__(coordinator) + self._idx = idx + self._camera_name = self.coordinator.data[self._idx]["name"] + self._name = switch + self._sensor_name = f"{self._camera_name}.{DeviceSwitchType(self._name).name}" + self._serial = self.coordinator.data[self._idx]["serial"] + self._device_class = DEVICE_CLASS_SWITCH + + @property + def name(self): + """Return the name of the Ezviz switch.""" + return f"{self._camera_name}.{DeviceSwitchType(self._name).name}" + + @property + def is_on(self): + """Return the state of the switch.""" + return self.coordinator.data[self._idx]["switches"][self._name] + + @property + def unique_id(self): + """Return the unique ID of this switch.""" + return f"{self._serial}_{self._sensor_name}" + + def turn_on(self, **kwargs): + """Change a device switch on the camera.""" + _LOGGER.debug("Set EZVIZ Switch '%s' to on", self._name) + + self.coordinator.ezviz_client.switch_status(self._serial, self._name, 1) + + def turn_off(self, **kwargs): + """Change a device switch on the camera.""" + _LOGGER.debug("Set EZVIZ Switch '%s' to off", self._name) + + self.coordinator.ezviz_client.switch_status(self._serial, self._name, 0) + + @property + def device_info(self): + """Return the device_info of the device.""" + return { + "identifiers": {(DOMAIN, self._serial)}, + "name": self.coordinator.data[self._idx]["name"], + "model": self.coordinator.data[self._idx]["device_sub_category"], + "manufacturer": MANUFACTURER, + "sw_version": self.coordinator.data[self._idx]["version"], + } + + @property + def device_class(self): + """Device class for the sensor.""" + return self._device_class diff --git a/homeassistant/components/ezviz/translations/en.json b/homeassistant/components/ezviz/translations/en.json new file mode 100644 index 0000000000000..e5103f07973eb --- /dev/null +++ b/homeassistant/components/ezviz/translations/en.json @@ -0,0 +1,52 @@ +{ + "config": { + "abort": { + "already_configured_account": "Account is already configured.", + "unknown": "Unexpected error", + "ezviz_cloud_account_missing": "Ezviz cloud account missing. Please reconfigure Ezviz cloud account" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "invalid_host": "Invalid IP or URL" + }, + "flow_title": "{serial}", + "step": { + "user": { + "data": { + "username": "Username", + "password": "Password", + "url": "URL" + }, + "title": "Connect to Ezviz Cloud" + }, + "user_custom_url": { + "data": { + "username": "Username", + "password": "Password", + "url": "URL" + }, + "title": "Connect to custom Ezviz URL", + "description": "Manually specify your region URL" + }, + "confirm": { + "data": { + "username": "Username", + "password": "Password" + }, + "title": "Discovered Ezviz Camera", + "description": "Enter RTSP credentials for Ezviz camera {serial} with IP as {ip_address}" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "Request Timeout (seconds)", + "ffmpeg_arguments": "Arguments passed to ffmpeg for cameras" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 293e39764f978..808f18c319d50 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -66,6 +66,7 @@ "enphase_envoy", "epson", "esphome", + "ezviz", "faa_delays", "fireservicerota", "flick_electric", diff --git a/requirements_all.txt b/requirements_all.txt index 9eb01b9360c48..db66c39447f44 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1384,6 +1384,9 @@ pyephember==0.3.1 # homeassistant.components.everlights pyeverlights==0.1.0 +# homeassistant.components.ezviz +pyezviz==0.1.8.7 + # homeassistant.components.fido pyfido==2.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f6b2714d5f839..e57032be86e7f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -737,6 +737,9 @@ pyeconet==0.1.13 # homeassistant.components.everlights pyeverlights==0.1.0 +# homeassistant.components.ezviz +pyezviz==0.1.8.7 + # homeassistant.components.fido pyfido==2.1.1 diff --git a/tests/components/ezviz/__init__.py b/tests/components/ezviz/__init__.py new file mode 100644 index 0000000000000..9a133a6f50bd3 --- /dev/null +++ b/tests/components/ezviz/__init__.py @@ -0,0 +1,118 @@ +"""Tests for the Ezviz integration.""" +from unittest.mock import patch + +from homeassistant.components.ezviz.const import ( + ATTR_SERIAL, + ATTR_TYPE_CAMERA, + ATTR_TYPE_CLOUD, + CONF_CAMERAS, + CONF_FFMPEG_ARGUMENTS, + DEFAULT_FFMPEG_ARGUMENTS, + DEFAULT_TIMEOUT, + DOMAIN, +) +from homeassistant.const import ( + CONF_IP_ADDRESS, + CONF_PASSWORD, + CONF_TIMEOUT, + CONF_TYPE, + CONF_URL, + CONF_USERNAME, +) +from homeassistant.helpers.typing import HomeAssistantType + +from tests.common import MockConfigEntry + +ENTRY_CONFIG = { + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_URL: "apiieu.ezvizlife.com", + CONF_TYPE: ATTR_TYPE_CLOUD, +} + +ENTRY_OPTIONS = { + CONF_FFMPEG_ARGUMENTS: DEFAULT_FFMPEG_ARGUMENTS, + CONF_TIMEOUT: DEFAULT_TIMEOUT, +} + +USER_INPUT_VALIDATE = { + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_URL: "apiieu.ezvizlife.com", +} + +USER_INPUT = { + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_URL: "apiieu.ezvizlife.com", + CONF_TYPE: ATTR_TYPE_CLOUD, +} + +USER_INPUT_CAMERA_VALIDATE = { + ATTR_SERIAL: "C666666", + CONF_PASSWORD: "test-password", + CONF_USERNAME: "test-username", +} + +USER_INPUT_CAMERA = { + CONF_PASSWORD: "test-password", + CONF_USERNAME: "test-username", + CONF_TYPE: ATTR_TYPE_CAMERA, +} + +YAML_CONFIG = { + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_URL: "apiieu.ezvizlife.com", + CONF_CAMERAS: { + "C666666": {CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"} + }, +} + +YAML_INVALID = { + "C666666": {CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"} +} + +YAML_CONFIG_CAMERA = { + ATTR_SERIAL: "C666666", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", +} + +DISCOVERY_INFO = { + ATTR_SERIAL: "C666666", + CONF_USERNAME: None, + CONF_PASSWORD: None, + CONF_IP_ADDRESS: "127.0.0.1", +} + +TEST = { + CONF_USERNAME: None, + CONF_PASSWORD: None, + CONF_IP_ADDRESS: "127.0.0.1", +} + + +def _patch_async_setup_entry(return_value=True): + return patch( + "homeassistant.components.ezviz.async_setup_entry", + return_value=return_value, + ) + + +async def init_integration( + hass: HomeAssistantType, + *, + data: dict = ENTRY_CONFIG, + options: dict = ENTRY_OPTIONS, + skip_entry_setup: bool = False, +) -> MockConfigEntry: + """Set up the Ezviz integration in Home Assistant.""" + entry = MockConfigEntry(domain=DOMAIN, data=data, options=options) + entry.add_to_hass(hass) + + if not skip_entry_setup: + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry diff --git a/tests/components/ezviz/conftest.py b/tests/components/ezviz/conftest.py new file mode 100644 index 0000000000000..64d9981a9806d --- /dev/null +++ b/tests/components/ezviz/conftest.py @@ -0,0 +1,48 @@ +"""Define fixtures available for all tests.""" +from unittest.mock import MagicMock, patch + +from pyezviz import EzvizClient +from pyezviz.test_cam_rtsp import TestRTSPAuth +from pytest import fixture + + +@fixture(autouse=True) +def mock_ffmpeg(hass): + """Mock ffmpeg is loaded.""" + hass.config.components.add("ffmpeg") + + +@fixture +def ezviz_test_rtsp_config_flow(hass): + """Mock the EzvizApi for easier testing.""" + with patch.object(TestRTSPAuth, "main", return_value=True), patch( + "homeassistant.components.ezviz.config_flow.TestRTSPAuth" + ) as mock_ezviz_test_rtsp: + instance = mock_ezviz_test_rtsp.return_value = TestRTSPAuth( + "test-ip", + "test-username", + "test-password", + ) + + instance.main = MagicMock(return_value=True) + + yield mock_ezviz_test_rtsp + + +@fixture +def ezviz_config_flow(hass): + """Mock the EzvizAPI for easier config flow testing.""" + with patch.object(EzvizClient, "login", return_value=True), patch( + "homeassistant.components.ezviz.config_flow.EzvizClient" + ) as mock_ezviz: + instance = mock_ezviz.return_value = EzvizClient( + "test-username", + "test-password", + "local.host", + "1", + ) + + instance.login = MagicMock(return_value=True) + instance.get_detection_sensibility = MagicMock(return_value=True) + + yield mock_ezviz diff --git a/tests/components/ezviz/test_config_flow.py b/tests/components/ezviz/test_config_flow.py new file mode 100644 index 0000000000000..b762f10447ff5 --- /dev/null +++ b/tests/components/ezviz/test_config_flow.py @@ -0,0 +1,547 @@ +"""Test the Ezviz config flow.""" + +from unittest.mock import patch + +from pyezviz.client import HTTPError, InvalidURL, PyEzvizError +from pyezviz.test_cam_rtsp import AuthTestResultFailed, InvalidHost + +from homeassistant.components.ezviz.const import ( + ATTR_SERIAL, + ATTR_TYPE_CAMERA, + ATTR_TYPE_CLOUD, + CONF_FFMPEG_ARGUMENTS, + DEFAULT_FFMPEG_ARGUMENTS, + DEFAULT_TIMEOUT, + DOMAIN, +) +from homeassistant.config_entries import SOURCE_DISCOVERY, SOURCE_IMPORT, SOURCE_USER +from homeassistant.const import ( + CONF_CUSTOMIZE, + CONF_IP_ADDRESS, + CONF_PASSWORD, + CONF_TIMEOUT, + CONF_TYPE, + CONF_URL, + CONF_USERNAME, +) +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) +from homeassistant.setup import async_setup_component + +from . import ( + DISCOVERY_INFO, + USER_INPUT, + USER_INPUT_CAMERA, + USER_INPUT_CAMERA_VALIDATE, + USER_INPUT_VALIDATE, + YAML_CONFIG, + YAML_CONFIG_CAMERA, + YAML_INVALID, + _patch_async_setup_entry, + init_integration, +) + + +async def test_user_form(hass, ezviz_config_flow): + """Test the user initiated form.""" + await async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + with _patch_async_setup_entry() as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + USER_INPUT_VALIDATE, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "test-username" + assert result["data"] == {**USER_INPUT} + + assert len(mock_setup_entry.mock_calls) == 1 + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured_account" + + +async def test_user_custom_url(hass, ezviz_config_flow): + """Test custom url step.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_USERNAME: "test-user", CONF_PASSWORD: "test-pass", CONF_URL: "customize"}, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user_custom_url" + assert result["errors"] == {} + + with _patch_async_setup_entry() as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_URL: "test-user"}, + ) + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["data"] == { + CONF_PASSWORD: "test-pass", + CONF_TYPE: ATTR_TYPE_CLOUD, + CONF_URL: "test-user", + CONF_USERNAME: "test-user", + } + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_async_step_import(hass, ezviz_config_flow): + """Test the config import flow.""" + await async_setup_component(hass, "persistent_notification", {}) + + with _patch_async_setup_entry() as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=YAML_CONFIG + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["data"] == USER_INPUT + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_async_step_import_camera(hass, ezviz_config_flow): + """Test the config import camera flow.""" + await async_setup_component(hass, "persistent_notification", {}) + + with _patch_async_setup_entry() as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=YAML_CONFIG_CAMERA + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["data"] == USER_INPUT_CAMERA + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_async_step_import_2nd_form_returns_camera(hass, ezviz_config_flow): + """Test we get the user initiated form.""" + await async_setup_component(hass, "persistent_notification", {}) + + with _patch_async_setup_entry() as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=YAML_CONFIG + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["data"] == USER_INPUT + + assert len(mock_setup_entry.mock_calls) == 1 + + with _patch_async_setup_entry() as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=USER_INPUT_CAMERA_VALIDATE + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["data"] == USER_INPUT_CAMERA + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_async_step_import_abort(hass, ezviz_config_flow): + """Test the config import flow with invalid data.""" + await async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=YAML_INVALID + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "unknown" + + +async def test_step_discovery_abort_if_cloud_account_missing(hass): + """Test discovery and confirm step, abort if cloud account was removed.""" + await async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_DISCOVERY}, data=DISCOVERY_INFO + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "confirm" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "test-user", + CONF_PASSWORD: "test-pass", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "ezviz_cloud_account_missing" + + +async def test_async_step_discovery( + hass, ezviz_config_flow, ezviz_test_rtsp_config_flow +): + """Test discovery and confirm step.""" + with patch("homeassistant.components.ezviz.PLATFORMS", []): + await init_integration(hass) + await async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_DISCOVERY}, data=DISCOVERY_INFO + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "confirm" + assert result["errors"] == {} + + with _patch_async_setup_entry() as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "test-user", + CONF_PASSWORD: "test-pass", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["data"] == { + CONF_PASSWORD: "test-pass", + CONF_TYPE: ATTR_TYPE_CAMERA, + CONF_USERNAME: "test-user", + } + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_options_flow(hass): + """Test updating options.""" + with patch("homeassistant.components.ezviz.PLATFORMS", []): + entry = await init_integration(hass) + + assert entry.options[CONF_FFMPEG_ARGUMENTS] == DEFAULT_FFMPEG_ARGUMENTS + assert entry.options[CONF_TIMEOUT] == DEFAULT_TIMEOUT + + result = await hass.config_entries.options.async_init(entry.entry_id) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "init" + assert result["errors"] is None + + with _patch_async_setup_entry() as mock_setup_entry: + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={CONF_FFMPEG_ARGUMENTS: "/H.264", CONF_TIMEOUT: 25}, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["data"][CONF_FFMPEG_ARGUMENTS] == "/H.264" + assert result["data"][CONF_TIMEOUT] == 25 + + assert len(mock_setup_entry.mock_calls) == 0 + + +async def test_user_form_exception(hass, ezviz_config_flow): + """Test we handle exception on user form.""" + ezviz_config_flow.side_effect = PyEzvizError + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + USER_INPUT_VALIDATE, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "invalid_auth"} + + ezviz_config_flow.side_effect = InvalidURL + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + USER_INPUT_VALIDATE, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "invalid_host"} + + ezviz_config_flow.side_effect = HTTPError + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + USER_INPUT_VALIDATE, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "cannot_connect"} + + ezviz_config_flow.side_effect = Exception + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + USER_INPUT_VALIDATE, + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "unknown" + + +async def test_import_exception(hass, ezviz_config_flow): + """Test we handle unexpected exception on import.""" + ezviz_config_flow.side_effect = PyEzvizError + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=YAML_CONFIG + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "invalid_auth" + + ezviz_config_flow.side_effect = InvalidURL + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=YAML_CONFIG + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "invalid_host" + + ezviz_config_flow.side_effect = HTTPError + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=YAML_CONFIG + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "cannot_connect" + + ezviz_config_flow.side_effect = Exception + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=YAML_CONFIG + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "unknown" + + +async def test_discover_exception_step1( + hass, + ezviz_config_flow, +): + """Test we handle unexpected exception on discovery.""" + with patch("homeassistant.components.ezviz.PLATFORMS", []): + await init_integration(hass) + + await async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DISCOVERY}, + data={ATTR_SERIAL: "C66666", CONF_IP_ADDRESS: "test-ip"}, + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "confirm" + assert result["errors"] == {} + + # Test Step 1 + ezviz_config_flow.side_effect = PyEzvizError + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "test-user", + CONF_PASSWORD: "test-pass", + }, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "confirm" + assert result["errors"] == {"base": "invalid_auth"} + + ezviz_config_flow.side_effect = InvalidURL + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "test-user", + CONF_PASSWORD: "test-pass", + }, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "confirm" + assert result["errors"] == {"base": "invalid_host"} + + ezviz_config_flow.side_effect = HTTPError + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "test-user", + CONF_PASSWORD: "test-pass", + }, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "confirm" + assert result["errors"] == {"base": "invalid_host"} + + ezviz_config_flow.side_effect = Exception + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "test-user", + CONF_PASSWORD: "test-pass", + }, + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "unknown" + + +async def test_discover_exception_step3( + hass, + ezviz_config_flow, + ezviz_test_rtsp_config_flow, +): + """Test we handle unexpected exception on discovery.""" + with patch("homeassistant.components.ezviz.PLATFORMS", []): + await init_integration(hass) + await async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DISCOVERY}, + data={ATTR_SERIAL: "C66666", CONF_IP_ADDRESS: "test-ip"}, + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "confirm" + assert result["errors"] == {} + + # Test Step 3 + ezviz_test_rtsp_config_flow.side_effect = AuthTestResultFailed + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "test-user", + CONF_PASSWORD: "test-pass", + }, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "confirm" + assert result["errors"] == {"base": "invalid_auth"} + + ezviz_test_rtsp_config_flow.side_effect = InvalidHost + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "test-user", + CONF_PASSWORD: "test-pass", + }, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "confirm" + assert result["errors"] == {"base": "invalid_host"} + + ezviz_test_rtsp_config_flow.side_effect = Exception + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "test-user", + CONF_PASSWORD: "test-pass", + }, + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "unknown" + + +async def test_user_custom_url_exception(hass, ezviz_config_flow): + """Test we handle unexpected exception.""" + ezviz_config_flow.side_effect = PyEzvizError() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "test-user", + CONF_PASSWORD: "test-pass", + CONF_URL: CONF_CUSTOMIZE, + }, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user_custom_url" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_URL: "test-user"}, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user_custom_url" + assert result["errors"] == {"base": "invalid_auth"} + + ezviz_config_flow.side_effect = InvalidURL + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_URL: "test-user"}, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user_custom_url" + assert result["errors"] == {"base": "invalid_host"} + + ezviz_config_flow.side_effect = HTTPError + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_URL: "test-user"}, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user_custom_url" + assert result["errors"] == {"base": "cannot_connect"} + + ezviz_config_flow.side_effect = Exception + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_URL: "test-user"}, + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "unknown" From 65c39bbd9204c34b29a7f2da04d8df91a75863cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Fri, 9 Apr 2021 16:44:02 +0200 Subject: [PATCH 0143/1317] Change discovery timeout from 10 to 60 (#48924) --- homeassistant/components/hassio/handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index 9007726118512..301d353faf053 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -148,7 +148,7 @@ def retrieve_discovery_messages(self): This method return a coroutine. """ - return self.send_command("/discovery", method="get") + return self.send_command("/discovery", method="get", timeout=60) @api_data def get_discovery_message(self, uuid): From 346af58f27f988c75e0865d966cab2f73623f7d3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 9 Apr 2021 17:19:23 +0200 Subject: [PATCH 0144/1317] Extend media source URL expiry to 24h (#48912) --- homeassistant/components/media_source/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/media_source/__init__.py b/homeassistant/components/media_source/__init__.py index 6aa01403a5f1f..0ef5d460580ff 100644 --- a/homeassistant/components/media_source/__init__.py +++ b/homeassistant/components/media_source/__init__.py @@ -19,6 +19,8 @@ from .const import DOMAIN, URI_SCHEME, URI_SCHEME_REGEX from .error import Unresolvable +DEFAULT_EXPIRY_TIME = 3600 * 24 + def is_media_source_id(media_content_id: str): """Test if identifier is a media source.""" @@ -105,7 +107,7 @@ async def websocket_browse_media(hass, connection, msg): { vol.Required("type"): "media_source/resolve_media", vol.Required(ATTR_MEDIA_CONTENT_ID): str, - vol.Optional("expires", default=30): int, + vol.Optional("expires", default=DEFAULT_EXPIRY_TIME): int, } ) @websocket_api.async_response From b66c4a9dca4c5281429800d72db554921106e434 Mon Sep 17 00:00:00 2001 From: Ph-Wagner <55734069+Ph-Wagner@users.noreply.github.com> Date: Fri, 9 Apr 2021 18:02:06 +0200 Subject: [PATCH 0145/1317] Extend Google Cast media source URL expiry to 24h (#48937) * Extend media source URL expiry to 12h closes #46280 After checking out https://github.com/home-assistant/core/pull/48912 I just think why not. * Update homeassistant/components/cast/media_player.py Co-authored-by: Erik Montnemery --- homeassistant/components/cast/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index b6ca8dd07286b..016d5162d2380 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -473,7 +473,7 @@ async def async_play_media(self, media_type, media_id, **kwargs): self.hass, refresh_token.id, media_id, - timedelta(minutes=5), + timedelta(seconds=media_source.DEFAULT_EXPIRY_TIME), ) # prepend external URL From f2f03313095a158c87d6dc1130577f68bb715525 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Fri, 9 Apr 2021 12:08:56 -0400 Subject: [PATCH 0146/1317] Bump ZHA quirks library (#48931) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 5825bdcda0f7c..5cd57e2627413 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -7,7 +7,7 @@ "bellows==0.23.1", "pyserial==3.5", "pyserial-asyncio==0.5", - "zha-quirks==0.0.55", + "zha-quirks==0.0.56", "zigpy-cc==0.5.2", "zigpy-deconz==0.12.0", "zigpy==0.33.0", diff --git a/requirements_all.txt b/requirements_all.txt index db66c39447f44..aa13a58e7f21e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2378,7 +2378,7 @@ zengge==0.2 zeroconf==0.29.0 # homeassistant.components.zha -zha-quirks==0.0.55 +zha-quirks==0.0.56 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e57032be86e7f..9f482a0b1e0d5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1251,7 +1251,7 @@ zeep[async]==4.0.0 zeroconf==0.29.0 # homeassistant.components.zha -zha-quirks==0.0.55 +zha-quirks==0.0.56 # homeassistant.components.zha zigpy-cc==0.5.2 From af3b18c40a683168fb196f064f712327e367ddfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Fri, 9 Apr 2021 18:54:24 +0200 Subject: [PATCH 0147/1317] Handle exceptions when looking for new version (#48922) --- homeassistant/components/version/sensor.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/version/sensor.py b/homeassistant/components/version/sensor.py index 9d558f4ba7c22..d438f39133486 100644 --- a/homeassistant/components/version/sensor.py +++ b/homeassistant/components/version/sensor.py @@ -1,7 +1,9 @@ """Sensor that can display the current Home Assistant versions.""" from datetime import timedelta +import logging from pyhaversion import HaVersion, HaVersionChannel, HaVersionSource +from pyhaversion.exceptions import HaVersionFetchException, HaVersionParseException import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity @@ -59,6 +61,8 @@ } ) +_LOGGER: logging.Logger = logging.getLogger(__name__) + async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Version sensor platform.""" @@ -114,7 +118,14 @@ def __init__(self, api: HaVersion): @Throttle(TIME_BETWEEN_UPDATES) async def async_update(self): """Get the latest version information.""" - await self.api.get_version() + try: + await self.api.get_version() + except HaVersionFetchException as exception: + _LOGGER.warning(exception) + except HaVersionParseException as exception: + _LOGGER.warning( + "Could not parse data received for %s - %s", self.api.source, exception + ) class VersionSensor(SensorEntity): From ee0c87df1cb6fd2efda81ae97ee46023bbfb9185 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 9 Apr 2021 18:54:39 +0200 Subject: [PATCH 0148/1317] Bump pykodi to 0.2.5 (#48930) --- homeassistant/components/kodi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/kodi/manifest.json b/homeassistant/components/kodi/manifest.json index 58d46aea8ba71..9ab510507048a 100644 --- a/homeassistant/components/kodi/manifest.json +++ b/homeassistant/components/kodi/manifest.json @@ -3,7 +3,7 @@ "name": "Kodi", "documentation": "https://www.home-assistant.io/integrations/kodi", "requirements": [ - "pykodi==0.2.4" + "pykodi==0.2.5" ], "codeowners": [ "@OnFreund", diff --git a/requirements_all.txt b/requirements_all.txt index aa13a58e7f21e..db48351aa5217 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1485,7 +1485,7 @@ pykira==0.1.1 pykmtronic==0.0.3 # homeassistant.components.kodi -pykodi==0.2.4 +pykodi==0.2.5 # homeassistant.components.kulersky pykulersky==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9f482a0b1e0d5..02f9ae3f4b163 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -805,7 +805,7 @@ pykira==0.1.1 pykmtronic==0.0.3 # homeassistant.components.kodi -pykodi==0.2.4 +pykodi==0.2.5 # homeassistant.components.kulersky pykulersky==0.5.2 From 8e2b5b36b5ab27c40b9d8c1c8705d529ef25b344 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 9 Apr 2021 18:58:27 +0200 Subject: [PATCH 0149/1317] Bump pyupgrade to 2.12.0 (#48943) --- .pre-commit-config.yaml | 2 +- homeassistant/components/config/zwave.py | 2 +- homeassistant/components/conversation/util.py | 4 +- homeassistant/components/dyson/climate.py | 2 +- homeassistant/components/fail2ban/sensor.py | 2 +- homeassistant/components/rachio/switch.py | 2 +- homeassistant/components/reddit/sensor.py | 2 +- homeassistant/components/repetier/__init__.py | 2 +- .../components/ring/binary_sensor.py | 2 +- homeassistant/components/ring/sensor.py | 4 +- homeassistant/components/scene/__init__.py | 6 +- homeassistant/components/serial_pm/sensor.py | 2 +- .../seven_segments/image_processing.py | 2 +- .../components/seventeentrack/sensor.py | 2 +- homeassistant/components/sht31/sensor.py | 2 +- homeassistant/components/skybell/sensor.py | 2 +- .../components/smartthings/sensor.py | 4 +- homeassistant/components/solaredge/sensor.py | 2 +- homeassistant/components/stream/hls.py | 6 +- .../components/synology_chat/notify.py | 2 +- homeassistant/components/ted5000/sensor.py | 2 +- .../components/telegram_bot/__init__.py | 4 +- .../components/tensorflow/image_processing.py | 2 +- .../components/thinkingcleaner/sensor.py | 2 +- homeassistant/components/todoist/calendar.py | 2 +- homeassistant/components/travisci/sensor.py | 2 +- homeassistant/components/vesync/common.py | 2 +- .../components/volkszaehler/sensor.py | 2 +- .../components/volvooncall/device_tracker.py | 2 +- homeassistant/components/waqi/sensor.py | 2 +- .../components/waterfurnace/sensor.py | 2 +- .../components/waze_travel_time/helpers.py | 2 +- .../components/zha/core/channels/base.py | 2 +- homeassistant/components/zha/core/gateway.py | 4 +- homeassistant/helpers/icon.py | 4 +- homeassistant/helpers/location.py | 2 +- homeassistant/util/color.py | 2 +- requirements_test_pre_commit.txt | 2 +- script/hassfest/__main__.py | 2 +- tests/components/hassio/test_ingress.py | 79 ++++++++++--------- tests/components/history/test_init.py | 4 +- tests/components/mobile_app/test_http_api.py | 2 +- tests/components/ps4/test_init.py | 2 +- tests/components/ps4/test_media_player.py | 2 +- tests/helpers/test_icon.py | 2 +- 45 files changed, 94 insertions(+), 95 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2f4aea74ae9c3..97093bc8dbed3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/asottile/pyupgrade - rev: v2.11.0 + rev: v2.12.0 hooks: - id: pyupgrade args: [--py38-plus] diff --git a/homeassistant/components/config/zwave.py b/homeassistant/components/config/zwave.py index ca1a99c6bb760..dd1bf1f08e20f 100644 --- a/homeassistant/components/config/zwave.py +++ b/homeassistant/components/config/zwave.py @@ -226,7 +226,7 @@ def _fetch_protection(): return self.json(protection_options) protections = node.get_protections() protection_options = { - "value_id": "{:d}".format(list(protections)[0]), + "value_id": f"{list(protections)[0]:d}", "selected": node.get_protection_item(list(protections)[0]), "options": node.get_protection_items(list(protections)[0]), } diff --git a/homeassistant/components/conversation/util.py b/homeassistant/components/conversation/util.py index 4904cb9f990da..b21b75be9b580 100644 --- a/homeassistant/components/conversation/util.py +++ b/homeassistant/components/conversation/util.py @@ -24,11 +24,11 @@ def create_matcher(utterance): # Group part if group_match is not None: - pattern.append(r"(?P<{}>[\w ]+?)\s*".format(group_match.groups()[0])) + pattern.append(fr"(?P<{group_match.groups()[0]}>[\w ]+?)\s*") # Optional part elif optional_match is not None: - pattern.append(r"(?:{} *)?".format(optional_match.groups()[0])) + pattern.append(fr"(?:{optional_match.groups()[0]} *)?") pattern.append("$") return re.compile("".join(pattern), re.I) diff --git a/homeassistant/components/dyson/climate.py b/homeassistant/components/dyson/climate.py index e7b8f42f1b146..4f4c4d7cbbadf 100644 --- a/homeassistant/components/dyson/climate.py +++ b/homeassistant/components/dyson/climate.py @@ -109,7 +109,7 @@ def current_temperature(self): and self._device.environmental_state.temperature ): temperature_kelvin = self._device.environmental_state.temperature - return float("{:.1f}".format(temperature_kelvin - 273)) + return float(f"{temperature_kelvin - 273:.1f}") return None @property diff --git a/homeassistant/components/fail2ban/sensor.py b/homeassistant/components/fail2ban/sensor.py index 29ac5c3d0b5f9..908ab5d77c0c2 100644 --- a/homeassistant/components/fail2ban/sensor.py +++ b/homeassistant/components/fail2ban/sensor.py @@ -55,7 +55,7 @@ def __init__(self, name, jail, log_parser): self.last_ban = None self.log_parser = log_parser self.log_parser.ip_regex[self.jail] = re.compile( - r"\[{}\]\s*(Ban|Unban) (.*)".format(re.escape(self.jail)) + fr"\[{re.escape(self.jail)}\]\s*(Ban|Unban) (.*)" ) _LOGGER.debug("Setting up jail %s", self.jail) diff --git a/homeassistant/components/rachio/switch.py b/homeassistant/components/rachio/switch.py index 8d87b688aa47e..30146cb44f689 100644 --- a/homeassistant/components/rachio/switch.py +++ b/homeassistant/components/rachio/switch.py @@ -356,7 +356,7 @@ def __init__(self, person, controller, data, current_schedule): def __str__(self): """Display the zone as a string.""" - return 'Rachio Zone "{}" on {}'.format(self.name, str(self._controller)) + return f'Rachio Zone "{self.name}" on {str(self._controller)}' @property def zone_id(self) -> str: diff --git a/homeassistant/components/reddit/sensor.py b/homeassistant/components/reddit/sensor.py index a88de916009e0..1e755b950bf66 100644 --- a/homeassistant/components/reddit/sensor.py +++ b/homeassistant/components/reddit/sensor.py @@ -56,7 +56,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Reddit sensor platform.""" subreddits = config[CONF_SUBREDDITS] - user_agent = "{}_home_assistant_sensor".format(config[CONF_USERNAME]) + user_agent = f"{config[CONF_USERNAME]}_home_assistant_sensor" limit = config[CONF_MAXIMUM] sort_by = config[CONF_SORT_BY] diff --git a/homeassistant/components/repetier/__init__.py b/homeassistant/components/repetier/__init__.py index a680fd777612e..c104fc447e2ab 100644 --- a/homeassistant/components/repetier/__init__.py +++ b/homeassistant/components/repetier/__init__.py @@ -167,7 +167,7 @@ def setup(hass, config): for repetier in config[DOMAIN]: _LOGGER.debug("Repetier server config %s", repetier[CONF_HOST]) - url = "http://{}".format(repetier[CONF_HOST]) + url = f"http://{repetier[CONF_HOST]}" port = repetier[CONF_PORT] api_key = repetier[CONF_API_KEY] diff --git a/homeassistant/components/ring/binary_sensor.py b/homeassistant/components/ring/binary_sensor.py index 18ce87e722e0d..28d686df06a69 100644 --- a/homeassistant/components/ring/binary_sensor.py +++ b/homeassistant/components/ring/binary_sensor.py @@ -52,7 +52,7 @@ def __init__(self, config_entry_id, ring, device, sensor_type): super().__init__(config_entry_id, device) self._ring = ring self._sensor_type = sensor_type - self._name = "{} {}".format(self._device.name, SENSOR_TYPES.get(sensor_type)[0]) + self._name = f"{self._device.name} {SENSOR_TYPES.get(sensor_type)[0]}" self._device_class = SENSOR_TYPES.get(sensor_type)[2] self._state = None self._unique_id = f"{device.id}-{sensor_type}" diff --git a/homeassistant/components/ring/sensor.py b/homeassistant/components/ring/sensor.py index a2b9e2300dc75..fb1c38fcbdeb3 100644 --- a/homeassistant/components/ring/sensor.py +++ b/homeassistant/components/ring/sensor.py @@ -44,9 +44,9 @@ def __init__(self, config_entry_id, device, sensor_type): super().__init__(config_entry_id, device) self._sensor_type = sensor_type self._extra = None - self._icon = "mdi:{}".format(SENSOR_TYPES.get(sensor_type)[3]) + self._icon = f"mdi:{SENSOR_TYPES.get(sensor_type)[3]}" self._kind = SENSOR_TYPES.get(sensor_type)[4] - self._name = "{} {}".format(self._device.name, SENSOR_TYPES.get(sensor_type)[0]) + self._name = f"{self._device.name} {SENSOR_TYPES.get(sensor_type)[0]}" self._unique_id = f"{device.id}-{sensor_type}" @property diff --git a/homeassistant/components/scene/__init__.py b/homeassistant/components/scene/__init__.py index e11934c61c380..ced56fe59056f 100644 --- a/homeassistant/components/scene/__init__.py +++ b/homeassistant/components/scene/__init__.py @@ -32,13 +32,11 @@ def _hass_domain_validator(config): def _platform_validator(config): """Validate it is a valid platform.""" try: - platform = importlib.import_module( - ".{}".format(config[CONF_PLATFORM]), __name__ - ) + platform = importlib.import_module(f".{config[CONF_PLATFORM]}", __name__) except ImportError: try: platform = importlib.import_module( - "homeassistant.components.{}.scene".format(config[CONF_PLATFORM]) + f"homeassistant.components.{config[CONF_PLATFORM]}.scene" ) except ImportError: raise vol.Invalid("Invalid platform specified") from None diff --git a/homeassistant/components/serial_pm/sensor.py b/homeassistant/components/serial_pm/sensor.py index b81c60e0a19d2..fd017661de2e1 100644 --- a/homeassistant/components/serial_pm/sensor.py +++ b/homeassistant/components/serial_pm/sensor.py @@ -47,7 +47,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for pmname in coll.supported_values(): if config.get(CONF_NAME) is not None: - name = "{} PM{}".format(config.get(CONF_NAME), pmname) + name = f"{config.get(CONF_NAME)} PM{pmname}" else: name = f"PM{pmname}" dev.append(ParticulateMatterSensor(coll, name, pmname)) diff --git a/homeassistant/components/seven_segments/image_processing.py b/homeassistant/components/seven_segments/image_processing.py index 4515ec7441bdf..6ff6b63746a22 100644 --- a/homeassistant/components/seven_segments/image_processing.py +++ b/homeassistant/components/seven_segments/image_processing.py @@ -69,7 +69,7 @@ def __init__(self, hass, camera_entity, config, name): if name: self._name = name else: - self._name = "SevenSegment OCR {}".format(split_entity_id(camera_entity)[1]) + self._name = f"SevenSegment OCR {split_entity_id(camera_entity)[1]}" self._state = None self.filepath = os.path.join( diff --git a/homeassistant/components/seventeentrack/sensor.py b/homeassistant/components/seventeentrack/sensor.py index e856f71b008db..ab0f077965631 100644 --- a/homeassistant/components/seventeentrack/sensor.py +++ b/homeassistant/components/seventeentrack/sensor.py @@ -132,7 +132,7 @@ def state(self): @property def unique_id(self): """Return a unique, Home Assistant friendly identifier for this entity.""" - return "summary_{}_{}".format(self._data.account_id, slugify(self._status)) + return f"summary_{self._data.account_id}_{slugify(self._status)}" @property def unit_of_measurement(self): diff --git a/homeassistant/components/sht31/sensor.py b/homeassistant/components/sht31/sensor.py index fd5506ee513f3..65ebbf0d882c2 100644 --- a/homeassistant/components/sht31/sensor.py +++ b/homeassistant/components/sht31/sensor.py @@ -66,7 +66,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): devs = [] for sensor_type, sensor_class in sensor_classes.items(): - name = "{} {}".format(config.get(CONF_NAME), sensor_type.capitalize()) + name = f"{config.get(CONF_NAME)} {sensor_type.capitalize()}" devs.append(sensor_class(sensor_client, name)) add_entities(devs) diff --git a/homeassistant/components/skybell/sensor.py b/homeassistant/components/skybell/sensor.py index 8dc13814c678d..de99a22f4c992 100644 --- a/homeassistant/components/skybell/sensor.py +++ b/homeassistant/components/skybell/sensor.py @@ -45,7 +45,7 @@ def __init__(self, device, sensor_type): """Initialize a sensor for a Skybell device.""" super().__init__(device) self._sensor_type = sensor_type - self._icon = "mdi:{}".format(SENSOR_TYPES[self._sensor_type][1]) + self._icon = f"mdi:{SENSOR_TYPES[self._sensor_type][1]}" self._name = "{} {}".format( self._device.name, SENSOR_TYPES[self._sensor_type][0] ) diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py index 86377e32e2394..533d8f6476e4d 100644 --- a/homeassistant/components/smartthings/sensor.py +++ b/homeassistant/components/smartthings/sensor.py @@ -358,12 +358,12 @@ def __init__(self, device, index): @property def name(self) -> str: """Return the name of the binary sensor.""" - return "{} {}".format(self._device.label, THREE_AXIS_NAMES[self._index]) + return f"{self._device.label} {THREE_AXIS_NAMES[self._index]}" @property def unique_id(self) -> str: """Return a unique ID.""" - return "{}.{}".format(self._device.device_id, THREE_AXIS_NAMES[self._index]) + return f"{self._device.device_id}.{THREE_AXIS_NAMES[self._index]}" @property def state(self): diff --git a/homeassistant/components/solaredge/sensor.py b/homeassistant/components/solaredge/sensor.py index b93a84a77fb61..d827990ac55b5 100644 --- a/homeassistant/components/solaredge/sensor.py +++ b/homeassistant/components/solaredge/sensor.py @@ -153,7 +153,7 @@ def unit_of_measurement(self) -> str | None: @property def name(self) -> str: """Return the name.""" - return "{} ({})".format(self.platform_name, SENSOR_TYPES[self.sensor_key][1]) + return f"{self.platform_name} ({SENSOR_TYPES[self.sensor_key][1]})" @property def icon(self) -> str | None: diff --git a/homeassistant/components/stream/hls.py b/homeassistant/components/stream/hls.py index 4909bbf95a345..ffeae4dbffd3b 100644 --- a/homeassistant/components/stream/hls.py +++ b/homeassistant/components/stream/hls.py @@ -81,8 +81,8 @@ def render_playlist(track): return [] playlist = [ - "#EXT-X-MEDIA-SEQUENCE:{}".format(segments[0].sequence), - "#EXT-X-DISCONTINUITY-SEQUENCE:{}".format(segments[0].stream_id), + f"#EXT-X-MEDIA-SEQUENCE:{segments[0].sequence}", + f"#EXT-X-DISCONTINUITY-SEQUENCE:{segments[0].stream_id}", ] last_stream_id = segments[0].stream_id @@ -91,7 +91,7 @@ def render_playlist(track): playlist.append("#EXT-X-DISCONTINUITY") playlist.extend( [ - "#EXTINF:{:.04f},".format(float(segment.duration)), + f"#EXTINF:{float(segment.duration):.04f},", f"./segment/{segment.sequence}.m4s", ] ) diff --git a/homeassistant/components/synology_chat/notify.py b/homeassistant/components/synology_chat/notify.py index df43c5668f388..f73fd65ba3f4f 100644 --- a/homeassistant/components/synology_chat/notify.py +++ b/homeassistant/components/synology_chat/notify.py @@ -51,7 +51,7 @@ def send_message(self, message="", **kwargs): if file_url: data["file_url"] = file_url - to_send = "payload={}".format(json.dumps(data)) + to_send = f"payload={json.dumps(data)}" response = requests.post( self._resource, data=to_send, timeout=10, verify=self._verify_ssl diff --git a/homeassistant/components/ted5000/sensor.py b/homeassistant/components/ted5000/sensor.py index d618ca9c2cf6b..62cdd5066adbe 100644 --- a/homeassistant/components/ted5000/sensor.py +++ b/homeassistant/components/ted5000/sensor.py @@ -56,7 +56,7 @@ def __init__(self, gateway, name, mtu, unit): """Initialize the sensor.""" units = {POWER_WATT: "power", VOLT: "voltage"} self._gateway = gateway - self._name = "{} mtu{} {}".format(name, mtu, units[unit]) + self._name = f"{name} mtu{mtu} {units[unit]}" self._mtu = mtu self._unit = unit self.update() diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index 86bf4c2440776..589d85bd20ec6 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -303,9 +303,7 @@ async def async_setup(hass, config): p_type = p_config.get(CONF_PLATFORM) - platform = importlib.import_module( - ".{}".format(p_config[CONF_PLATFORM]), __name__ - ) + platform = importlib.import_module(f".{p_config[CONF_PLATFORM]}", __name__) _LOGGER.info("Setting up %s.%s", DOMAIN, p_type) try: diff --git a/homeassistant/components/tensorflow/image_processing.py b/homeassistant/components/tensorflow/image_processing.py index dad83005512b9..f4e90a8315421 100644 --- a/homeassistant/components/tensorflow/image_processing.py +++ b/homeassistant/components/tensorflow/image_processing.py @@ -218,7 +218,7 @@ def __init__( if name: self._name = name else: - self._name = "TensorFlow {}".format(split_entity_id(camera_entity)[1]) + self._name = f"TensorFlow {split_entity_id(camera_entity)[1]}" self._category_index = category_index self._min_confidence = config.get(CONF_CONFIDENCE) self._file_out = config.get(CONF_FILE_OUT) diff --git a/homeassistant/components/thinkingcleaner/sensor.py b/homeassistant/components/thinkingcleaner/sensor.py index 56cf272f7d15a..fa1dfd5988cb2 100644 --- a/homeassistant/components/thinkingcleaner/sensor.py +++ b/homeassistant/components/thinkingcleaner/sensor.py @@ -87,7 +87,7 @@ def __init__(self, tc_object, sensor_type, update_devices): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self._tc_object.name, SENSOR_TYPES[self.type][0]) + return f"{self._tc_object.name} {SENSOR_TYPES[self.type][0]}" @property def icon(self): diff --git a/homeassistant/components/todoist/calendar.py b/homeassistant/components/todoist/calendar.py index 976462c95faaa..8b9379d218636 100644 --- a/homeassistant/components/todoist/calendar.py +++ b/homeassistant/components/todoist/calendar.py @@ -385,7 +385,7 @@ def create_todoist_task(self, data): task[SUMMARY] = data[CONTENT] task[COMPLETED] = data[CHECKED] == 1 task[PRIORITY] = data[PRIORITY] - task[DESCRIPTION] = "https://todoist.com/showTask?id={}".format(data[ID]) + task[DESCRIPTION] = f"https://todoist.com/showTask?id={data[ID]}" # All task Labels (optional parameter). task[LABELS] = [ diff --git a/homeassistant/components/travisci/sensor.py b/homeassistant/components/travisci/sensor.py index 94a6ba3a48f07..82b158aa0ec0a 100644 --- a/homeassistant/components/travisci/sensor.py +++ b/homeassistant/components/travisci/sensor.py @@ -105,7 +105,7 @@ def __init__(self, data, repo_name, user, branch, sensor_type): self._user = user self._branch = branch self._state = None - self._name = "{} {}".format(self._repo_name, SENSOR_TYPES[self._sensor_type][0]) + self._name = f"{self._repo_name} {SENSOR_TYPES[self._sensor_type][0]}" @property def name(self): diff --git a/homeassistant/components/vesync/common.py b/homeassistant/components/vesync/common.py index 240a5e4828737..fcab5bb5a6333 100644 --- a/homeassistant/components/vesync/common.py +++ b/homeassistant/components/vesync/common.py @@ -47,7 +47,7 @@ def __init__(self, device): def unique_id(self): """Return the ID of this device.""" if isinstance(self.device.sub_device_no, int): - return "{}{}".format(self.device.cid, str(self.device.sub_device_no)) + return f"{self.device.cid}{str(self.device.sub_device_no)}" return self.device.cid @property diff --git a/homeassistant/components/volkszaehler/sensor.py b/homeassistant/components/volkszaehler/sensor.py index 61fcf1d296988..4eb2f512f3134 100644 --- a/homeassistant/components/volkszaehler/sensor.py +++ b/homeassistant/components/volkszaehler/sensor.py @@ -89,7 +89,7 @@ def __init__(self, vz_api, name, sensor_type): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self._name, SENSOR_TYPES[self.type][0]) + return f"{self._name} {SENSOR_TYPES[self.type][0]}" @property def icon(self): diff --git a/homeassistant/components/volvooncall/device_tracker.py b/homeassistant/components/volvooncall/device_tracker.py index eab42156c9aa0..ebc4990db550d 100644 --- a/homeassistant/components/volvooncall/device_tracker.py +++ b/homeassistant/components/volvooncall/device_tracker.py @@ -18,7 +18,7 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None): async def see_vehicle(): """Handle the reporting of the vehicle position.""" host_name = instrument.vehicle_name - dev_id = "volvo_{}".format(slugify(host_name)) + dev_id = f"volvo_{slugify(host_name)}" await async_see( dev_id=dev_id, host_name=host_name, diff --git a/homeassistant/components/waqi/sensor.py b/homeassistant/components/waqi/sensor.py index ef01c057a9e7d..084ec17bb2827 100644 --- a/homeassistant/components/waqi/sensor.py +++ b/homeassistant/components/waqi/sensor.py @@ -121,7 +121,7 @@ def name(self): """Return the name of the sensor.""" if self.station_name: return f"WAQI {self.station_name}" - return "WAQI {}".format(self.url if self.url else self.uid) + return f"WAQI {self.url if self.url else self.uid}" @property def icon(self): diff --git a/homeassistant/components/waterfurnace/sensor.py b/homeassistant/components/waterfurnace/sensor.py index 91e455d03d69a..fe7c94ed6345b 100644 --- a/homeassistant/components/waterfurnace/sensor.py +++ b/homeassistant/components/waterfurnace/sensor.py @@ -74,7 +74,7 @@ def __init__(self, client, config): # This ensures that the sensors are isolated per waterfurnace unit self.entity_id = ENTITY_ID_FORMAT.format( - "wf_{}_{}".format(slugify(self.client.unit), slugify(self._attr)) + f"wf_{slugify(self.client.unit)}_{slugify(self._attr)}" ) @property diff --git a/homeassistant/components/waze_travel_time/helpers.py b/homeassistant/components/waze_travel_time/helpers.py index 8c8c89f28e67e..326a6018c9662 100644 --- a/homeassistant/components/waze_travel_time/helpers.py +++ b/homeassistant/components/waze_travel_time/helpers.py @@ -69,4 +69,4 @@ def resolve_zone(hass, friendly_name): def _get_location_from_attributes(state): """Get the lat/long string from an states attributes.""" attr = state.attributes - return "{},{}".format(attr.get(ATTR_LATITUDE), attr.get(ATTR_LONGITUDE)) + return f"{attr.get(ATTR_LATITUDE)},{attr.get(ATTR_LONGITUDE)}" diff --git a/homeassistant/components/zha/core/channels/base.py b/homeassistant/components/zha/core/channels/base.py index bc93459dbad41..4238707656df4 100644 --- a/homeassistant/components/zha/core/channels/base.py +++ b/homeassistant/components/zha/core/channels/base.py @@ -315,7 +315,7 @@ def __init__(self, cluster, device): self._cluster = cluster self._zha_device = device self._status = ChannelStatus.CREATED - self._unique_id = "{}:{}_ZDO".format(str(device.ieee), device.name) + self._unique_id = f"{str(device.ieee)}:{device.name}_ZDO" self._cluster.add_listener(self) @property diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index de65ed6695e15..96e4a7c3eb818 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -360,9 +360,7 @@ def device_removed(self, device): if zha_device is not None: device_info = zha_device.zha_device_info zha_device.async_cleanup_handles() - async_dispatcher_send( - self._hass, "{}_{}".format(SIGNAL_REMOVE, str(zha_device.ieee)) - ) + async_dispatcher_send(self._hass, f"{SIGNAL_REMOVE}_{str(zha_device.ieee)}") asyncio.ensure_future(self._async_remove_device(zha_device, entity_refs)) if device_info is not None: async_dispatcher_send( diff --git a/homeassistant/helpers/icon.py b/homeassistant/helpers/icon.py index 628dc9d341e39..a289ab4a8749f 100644 --- a/homeassistant/helpers/icon.py +++ b/homeassistant/helpers/icon.py @@ -10,13 +10,13 @@ def icon_for_battery_level( if battery_level is None: return f"{icon}-unknown" if charging and battery_level > 10: - icon += "-charging-{}".format(int(round(battery_level / 20 - 0.01)) * 20) + icon += f"-charging-{int(round(battery_level / 20 - 0.01)) * 20}" elif charging: icon += "-outline" elif battery_level <= 5: icon += "-alert" elif 5 < battery_level < 95: - icon += "-{}".format(int(round(battery_level / 10 - 0.01)) * 10) + icon += f"-{int(round(battery_level / 10 - 0.01)) * 10}" return icon diff --git a/homeassistant/helpers/location.py b/homeassistant/helpers/location.py index a613220ef0f67..ff27c580d2327 100644 --- a/homeassistant/helpers/location.py +++ b/homeassistant/helpers/location.py @@ -105,4 +105,4 @@ def find_coordinates( def _get_location_from_attributes(entity_state: State) -> str: """Get the lat/long string from an entities attributes.""" attr = entity_state.attributes - return "{},{}".format(attr.get(ATTR_LATITUDE), attr.get(ATTR_LONGITUDE)) + return f"{attr.get(ATTR_LATITUDE)},{attr.get(ATTR_LONGITUDE)}" diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py index 2a34fe82c5933..4b5c7a11cbc69 100644 --- a/homeassistant/util/color.py +++ b/homeassistant/util/color.py @@ -427,7 +427,7 @@ def color_rgbw_to_rgb(r: int, g: int, b: int, w: int) -> tuple[int, int, int]: def color_rgb_to_hex(r: int, g: int, b: int) -> str: """Return a RGB color from a hex color string.""" - return "{:02x}{:02x}{:02x}".format(round(r), round(g), round(b)) + return f"{round(r):02x}{round(g):02x}{round(b):02x}" def rgb_hex_to_rgb_list(hex_string: str) -> list[int]: diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 06e87a5c51c51..0bdcde4080816 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -11,5 +11,5 @@ isort==5.7.0 pycodestyle==2.7.0 pydocstyle==6.0.0 pyflakes==2.3.1 -pyupgrade==2.11.0 +pyupgrade==2.12.0 yamllint==1.24.2 diff --git a/script/hassfest/__main__.py b/script/hassfest/__main__.py index 6901622bfb386..8edc3ec6eb6c0 100644 --- a/script/hassfest/__main__.py +++ b/script/hassfest/__main__.py @@ -121,7 +121,7 @@ def main(): if plugin is requirements and not config.specific_integrations: print() plugin.validate(integrations, config) - print(" done in {:.2f}s".format(monotonic() - start)) + print(f" done in {monotonic() - start:.2f}s") except RuntimeError as err: print() print() diff --git a/tests/components/hassio/test_ingress.py b/tests/components/hassio/test_ingress.py index 72500d0be204e..e75de8741bf24 100644 --- a/tests/components/hassio/test_ingress.py +++ b/tests/components/hassio/test_ingress.py @@ -17,12 +17,12 @@ async def test_ingress_request_get(hassio_client, build_type, aioclient_mock): """Test no auth needed for .""" aioclient_mock.get( - "http://127.0.0.1/ingress/{}/{}".format(build_type[0], build_type[1]), + f"http://127.0.0.1/ingress/{build_type[0]}/{build_type[1]}", text="test", ) resp = await hassio_client.get( - "/api/hassio_ingress/{}/{}".format(build_type[0], build_type[1]), + f"/api/hassio_ingress/{build_type[0]}/{build_type[1]}", headers={"X-Test-Header": "beer"}, ) @@ -34,9 +34,10 @@ async def test_ingress_request_get(hassio_client, build_type, aioclient_mock): # Check we forwarded command assert len(aioclient_mock.mock_calls) == 1 assert aioclient_mock.mock_calls[-1][3]["X-Hassio-Key"] == "123456" - assert aioclient_mock.mock_calls[-1][3][ - "X-Ingress-Path" - ] == "/api/hassio_ingress/{}".format(build_type[0]) + assert ( + aioclient_mock.mock_calls[-1][3]["X-Ingress-Path"] + == f"/api/hassio_ingress/{build_type[0]}" + ) assert aioclient_mock.mock_calls[-1][3]["X-Test-Header"] == "beer" assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_FOR] assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_HOST] @@ -56,12 +57,12 @@ async def test_ingress_request_get(hassio_client, build_type, aioclient_mock): async def test_ingress_request_post(hassio_client, build_type, aioclient_mock): """Test no auth needed for .""" aioclient_mock.post( - "http://127.0.0.1/ingress/{}/{}".format(build_type[0], build_type[1]), + f"http://127.0.0.1/ingress/{build_type[0]}/{build_type[1]}", text="test", ) resp = await hassio_client.post( - "/api/hassio_ingress/{}/{}".format(build_type[0], build_type[1]), + f"/api/hassio_ingress/{build_type[0]}/{build_type[1]}", headers={"X-Test-Header": "beer"}, ) @@ -73,9 +74,10 @@ async def test_ingress_request_post(hassio_client, build_type, aioclient_mock): # Check we forwarded command assert len(aioclient_mock.mock_calls) == 1 assert aioclient_mock.mock_calls[-1][3]["X-Hassio-Key"] == "123456" - assert aioclient_mock.mock_calls[-1][3][ - "X-Ingress-Path" - ] == "/api/hassio_ingress/{}".format(build_type[0]) + assert ( + aioclient_mock.mock_calls[-1][3]["X-Ingress-Path"] + == f"/api/hassio_ingress/{build_type[0]}" + ) assert aioclient_mock.mock_calls[-1][3]["X-Test-Header"] == "beer" assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_FOR] assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_HOST] @@ -95,12 +97,12 @@ async def test_ingress_request_post(hassio_client, build_type, aioclient_mock): async def test_ingress_request_put(hassio_client, build_type, aioclient_mock): """Test no auth needed for .""" aioclient_mock.put( - "http://127.0.0.1/ingress/{}/{}".format(build_type[0], build_type[1]), + f"http://127.0.0.1/ingress/{build_type[0]}/{build_type[1]}", text="test", ) resp = await hassio_client.put( - "/api/hassio_ingress/{}/{}".format(build_type[0], build_type[1]), + f"/api/hassio_ingress/{build_type[0]}/{build_type[1]}", headers={"X-Test-Header": "beer"}, ) @@ -112,9 +114,10 @@ async def test_ingress_request_put(hassio_client, build_type, aioclient_mock): # Check we forwarded command assert len(aioclient_mock.mock_calls) == 1 assert aioclient_mock.mock_calls[-1][3]["X-Hassio-Key"] == "123456" - assert aioclient_mock.mock_calls[-1][3][ - "X-Ingress-Path" - ] == "/api/hassio_ingress/{}".format(build_type[0]) + assert ( + aioclient_mock.mock_calls[-1][3]["X-Ingress-Path"] + == f"/api/hassio_ingress/{build_type[0]}" + ) assert aioclient_mock.mock_calls[-1][3]["X-Test-Header"] == "beer" assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_FOR] assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_HOST] @@ -134,12 +137,12 @@ async def test_ingress_request_put(hassio_client, build_type, aioclient_mock): async def test_ingress_request_delete(hassio_client, build_type, aioclient_mock): """Test no auth needed for .""" aioclient_mock.delete( - "http://127.0.0.1/ingress/{}/{}".format(build_type[0], build_type[1]), + f"http://127.0.0.1/ingress/{build_type[0]}/{build_type[1]}", text="test", ) resp = await hassio_client.delete( - "/api/hassio_ingress/{}/{}".format(build_type[0], build_type[1]), + f"/api/hassio_ingress/{build_type[0]}/{build_type[1]}", headers={"X-Test-Header": "beer"}, ) @@ -151,9 +154,10 @@ async def test_ingress_request_delete(hassio_client, build_type, aioclient_mock) # Check we forwarded command assert len(aioclient_mock.mock_calls) == 1 assert aioclient_mock.mock_calls[-1][3]["X-Hassio-Key"] == "123456" - assert aioclient_mock.mock_calls[-1][3][ - "X-Ingress-Path" - ] == "/api/hassio_ingress/{}".format(build_type[0]) + assert ( + aioclient_mock.mock_calls[-1][3]["X-Ingress-Path"] + == f"/api/hassio_ingress/{build_type[0]}" + ) assert aioclient_mock.mock_calls[-1][3]["X-Test-Header"] == "beer" assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_FOR] assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_HOST] @@ -173,12 +177,12 @@ async def test_ingress_request_delete(hassio_client, build_type, aioclient_mock) async def test_ingress_request_patch(hassio_client, build_type, aioclient_mock): """Test no auth needed for .""" aioclient_mock.patch( - "http://127.0.0.1/ingress/{}/{}".format(build_type[0], build_type[1]), + f"http://127.0.0.1/ingress/{build_type[0]}/{build_type[1]}", text="test", ) resp = await hassio_client.patch( - "/api/hassio_ingress/{}/{}".format(build_type[0], build_type[1]), + f"/api/hassio_ingress/{build_type[0]}/{build_type[1]}", headers={"X-Test-Header": "beer"}, ) @@ -190,9 +194,10 @@ async def test_ingress_request_patch(hassio_client, build_type, aioclient_mock): # Check we forwarded command assert len(aioclient_mock.mock_calls) == 1 assert aioclient_mock.mock_calls[-1][3]["X-Hassio-Key"] == "123456" - assert aioclient_mock.mock_calls[-1][3][ - "X-Ingress-Path" - ] == "/api/hassio_ingress/{}".format(build_type[0]) + assert ( + aioclient_mock.mock_calls[-1][3]["X-Ingress-Path"] + == f"/api/hassio_ingress/{build_type[0]}" + ) assert aioclient_mock.mock_calls[-1][3]["X-Test-Header"] == "beer" assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_FOR] assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_HOST] @@ -212,12 +217,12 @@ async def test_ingress_request_patch(hassio_client, build_type, aioclient_mock): async def test_ingress_request_options(hassio_client, build_type, aioclient_mock): """Test no auth needed for .""" aioclient_mock.options( - "http://127.0.0.1/ingress/{}/{}".format(build_type[0], build_type[1]), + f"http://127.0.0.1/ingress/{build_type[0]}/{build_type[1]}", text="test", ) resp = await hassio_client.options( - "/api/hassio_ingress/{}/{}".format(build_type[0], build_type[1]), + f"/api/hassio_ingress/{build_type[0]}/{build_type[1]}", headers={"X-Test-Header": "beer"}, ) @@ -229,9 +234,10 @@ async def test_ingress_request_options(hassio_client, build_type, aioclient_mock # Check we forwarded command assert len(aioclient_mock.mock_calls) == 1 assert aioclient_mock.mock_calls[-1][3]["X-Hassio-Key"] == "123456" - assert aioclient_mock.mock_calls[-1][3][ - "X-Ingress-Path" - ] == "/api/hassio_ingress/{}".format(build_type[0]) + assert ( + aioclient_mock.mock_calls[-1][3]["X-Ingress-Path"] + == f"/api/hassio_ingress/{build_type[0]}" + ) assert aioclient_mock.mock_calls[-1][3]["X-Test-Header"] == "beer" assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_FOR] assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_HOST] @@ -250,22 +256,21 @@ async def test_ingress_request_options(hassio_client, build_type, aioclient_mock ) async def test_ingress_websocket(hassio_client, build_type, aioclient_mock): """Test no auth needed for .""" - aioclient_mock.get( - "http://127.0.0.1/ingress/{}/{}".format(build_type[0], build_type[1]) - ) + aioclient_mock.get(f"http://127.0.0.1/ingress/{build_type[0]}/{build_type[1]}") # Ignore error because we can setup a full IO infrastructure await hassio_client.ws_connect( - "/api/hassio_ingress/{}/{}".format(build_type[0], build_type[1]), + f"/api/hassio_ingress/{build_type[0]}/{build_type[1]}", headers={"X-Test-Header": "beer"}, ) # Check we forwarded command assert len(aioclient_mock.mock_calls) == 1 assert aioclient_mock.mock_calls[-1][3]["X-Hassio-Key"] == "123456" - assert aioclient_mock.mock_calls[-1][3][ - "X-Ingress-Path" - ] == "/api/hassio_ingress/{}".format(build_type[0]) + assert ( + aioclient_mock.mock_calls[-1][3]["X-Ingress-Path"] + == f"/api/hassio_ingress/{build_type[0]}" + ) assert aioclient_mock.mock_calls[-1][3]["X-Test-Header"] == "beer" assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_FOR] assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_HOST] diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index f4d1c81785873..497f296437f50 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -34,7 +34,7 @@ def test_get_states(hass_history): with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=now): for i in range(5): state = ha.State( - "test.point_in_time_{}".format(i % 5), + f"test.point_in_time_{i % 5}", f"State {i}", {"attribute_test": i}, ) @@ -49,7 +49,7 @@ def test_get_states(hass_history): with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=future): for i in range(5): state = ha.State( - "test.point_in_time_{}".format(i % 5), + f"test.point_in_time_{i % 5}", f"State {i}", {"attribute_test": i}, ) diff --git a/tests/components/mobile_app/test_http_api.py b/tests/components/mobile_app/test_http_api.py index c5b363c25b05c..456f3fab26130 100644 --- a/tests/components/mobile_app/test_http_api.py +++ b/tests/components/mobile_app/test_http_api.py @@ -86,7 +86,7 @@ async def test_registration_encryption(hass, hass_client): container = {"type": "render_template", "encrypted": True, "encrypted_data": data} resp = await api_client.post( - "/api/webhook/{}".format(register_json[CONF_WEBHOOK_ID]), json=container + f"/api/webhook/{register_json[CONF_WEBHOOK_ID]}", json=container ) assert resp.status == 200 diff --git a/tests/components/ps4/test_init.py b/tests/components/ps4/test_init.py index da24102293813..cfe2f4b8e8723 100644 --- a/tests/components/ps4/test_init.py +++ b/tests/components/ps4/test_init.py @@ -170,7 +170,7 @@ async def test_config_flow_entry_migrate(hass): assert mock_entity.device_id == MOCK_DEVICE_ID # Test that last four of credentials is appended to the unique_id. - assert mock_entity.unique_id == "{}_{}".format(MOCK_UNIQUE_ID, MOCK_CREDS[-4:]) + assert mock_entity.unique_id == f"{MOCK_UNIQUE_ID}_{MOCK_CREDS[-4:]}" # Test that config entry is at the current version. assert mock_entry.version == VERSION diff --git a/tests/components/ps4/test_media_player.py b/tests/components/ps4/test_media_player.py index d402cbb01ae03..3f71f5f9d521d 100644 --- a/tests/components/ps4/test_media_player.py +++ b/tests/components/ps4/test_media_player.py @@ -299,7 +299,7 @@ async def test_device_info_is_set_from_status_correctly(hass, patch_get_status): # Reformat mock status-sw_version for assertion. mock_version = MOCK_STATUS_STANDBY["system-version"] mock_version = mock_version[1:4] - mock_version = "{}.{}".format(mock_version[0], mock_version[1:]) + mock_version = f"{mock_version[0]}.{mock_version[1:]}" mock_state = hass.states.get(mock_entity_id).state diff --git a/tests/helpers/test_icon.py b/tests/helpers/test_icon.py index 4f1d4cb223fe5..033a6cd6b6967 100644 --- a/tests/helpers/test_icon.py +++ b/tests/helpers/test_icon.py @@ -37,7 +37,7 @@ def test_battery_icon(): else: postfix_charging = "-charging-100" if 5 < level < 95: - postfix = "-{}".format(int(round(level / 10 - 0.01)) * 10) + postfix = f"-{int(round(level / 10 - 0.01)) * 10}" elif level <= 5: postfix = "-alert" else: From 40450b9cfdfeaa177b1580327526302b996babc0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 9 Apr 2021 07:14:33 -1000 Subject: [PATCH 0150/1317] Detach aiohttp.ClientSession created by config entry setup on unload (#48908) --- homeassistant/config_entries.py | 29 ++++++++ homeassistant/helpers/aiohttp_client.py | 79 ++++++++++++++++---- tests/test_config_entries.py | 99 ++++++++++++++++++++++++- tests/test_util/aiohttp.py | 2 +- 4 files changed, 192 insertions(+), 17 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 23758cf88f2ad..6ef14afb6a6be 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from contextvars import ContextVar import functools import logging from types import MappingProxyType, MethodType @@ -133,6 +134,7 @@ class ConfigEntry: "_setup_lock", "update_listeners", "_async_cancel_retry_setup", + "_on_unload", ) def __init__( @@ -198,6 +200,9 @@ def __init__( # Function to cancel a scheduled retry self._async_cancel_retry_setup: Callable[[], Any] | None = None + # Hold list for functions to call on unload. + self._on_unload: list[CALLBACK_TYPE] | None = None + async def async_setup( self, hass: HomeAssistant, @@ -206,6 +211,7 @@ async def async_setup( tries: int = 0, ) -> None: """Set up an entry.""" + current_entry.set(self) if self.source == SOURCE_IGNORE or self.disabled_by: return @@ -290,6 +296,8 @@ async def setup_again(*_: Any) -> None: self._async_cancel_retry_setup = hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STARTED, setup_again ) + + self._async_process_on_unload() return except Exception: # pylint: disable=broad-except _LOGGER.exception( @@ -358,6 +366,8 @@ async def async_unload( if result and integration.domain == self.domain: self.state = ENTRY_STATE_NOT_LOADED + self._async_process_on_unload() + return result except Exception: # pylint: disable=broad-except _LOGGER.exception( @@ -470,6 +480,25 @@ def as_dict(self) -> dict[str, Any]: "disabled_by": self.disabled_by, } + @callback + def async_on_unload(self, func: CALLBACK_TYPE) -> None: + """Add a function to call when config entry is unloaded.""" + if self._on_unload is None: + self._on_unload = [] + self._on_unload.append(func) + + @callback + def _async_process_on_unload(self) -> None: + """Process the on_unload callbacks.""" + if self._on_unload is not None: + while self._on_unload: + self._on_unload.pop()() + + +current_entry: ContextVar[ConfigEntry | None] = ContextVar( + "current_entry", default=None +) + class ConfigEntriesFlowManager(data_entry_flow.FlowManager): """Manage all the config entry flows that are in progress.""" diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index f3ded75062e2b..53b906efd353b 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -5,7 +5,7 @@ from contextlib import suppress from ssl import SSLContext import sys -from typing import Any, Awaitable, cast +from typing import Any, Awaitable, Callable, cast import aiohttp from aiohttp import web @@ -13,6 +13,7 @@ from aiohttp.web_exceptions import HTTPBadGateway, HTTPGatewayTimeout import async_timeout +from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE, __version__ from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers.frame import warn_use @@ -27,6 +28,8 @@ __version__, aiohttp.__version__, sys.version_info ) +WARN_CLOSE_MSG = "closes the Home Assistant aiohttp session" + @callback @bind_hass @@ -37,12 +40,14 @@ def async_get_clientsession( This method must be run in the event loop. """ - key = DATA_CLIENTSESSION_NOTVERIFY - if verify_ssl: - key = DATA_CLIENTSESSION + key = DATA_CLIENTSESSION if verify_ssl else DATA_CLIENTSESSION_NOTVERIFY if key not in hass.data: - hass.data[key] = async_create_clientsession(hass, verify_ssl) + hass.data[key] = _async_create_clientsession( + hass, + verify_ssl, + auto_cleanup_method=_async_register_default_clientsession_shutdown, + ) return cast(aiohttp.ClientSession, hass.data[key]) @@ -59,24 +64,44 @@ def async_create_clientsession( If auto_cleanup is False, you need to call detach() after the session returned is no longer used. Default is True, the session will be - automatically detached on homeassistant_stop. + automatically detached on homeassistant_stop or when being created + in config entry setup, the config entry is unloaded. This method must be run in the event loop. """ - connector = _async_get_connector(hass, verify_ssl) + auto_cleanup_method = None + if auto_cleanup: + auto_cleanup_method = _async_register_clientsession_shutdown + + clientsession = _async_create_clientsession( + hass, + verify_ssl, + auto_cleanup_method=auto_cleanup_method, + **kwargs, + ) + + return clientsession + +@callback +def _async_create_clientsession( + hass: HomeAssistant, + verify_ssl: bool = True, + auto_cleanup_method: Callable[[HomeAssistant, aiohttp.ClientSession], None] + | None = None, + **kwargs: Any, +) -> aiohttp.ClientSession: + """Create a new ClientSession with kwargs, i.e. for cookies.""" clientsession = aiohttp.ClientSession( - connector=connector, + connector=_async_get_connector(hass, verify_ssl), headers={USER_AGENT: SERVER_SOFTWARE}, **kwargs, ) - clientsession.close = warn_use( # type: ignore - clientsession.close, "closes the Home Assistant aiohttp session" - ) + clientsession.close = warn_use(clientsession.close, WARN_CLOSE_MSG) # type: ignore - if auto_cleanup: - _async_register_clientsession_shutdown(hass, clientsession) + if auto_cleanup_method: + auto_cleanup_method(hass, clientsession) return clientsession @@ -146,7 +171,33 @@ async def async_aiohttp_proxy_stream( def _async_register_clientsession_shutdown( hass: HomeAssistant, clientsession: aiohttp.ClientSession ) -> None: - """Register ClientSession close on Home Assistant shutdown. + """Register ClientSession close on Home Assistant shutdown or config entry unload. + + This method must be run in the event loop. + """ + + @callback + def _async_close_websession(*_: Any) -> None: + """Close websession.""" + clientsession.detach() + + unsub = hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_CLOSE, _async_close_websession + ) + + config_entry = config_entries.current_entry.get() + if not config_entry: + return + + config_entry.async_on_unload(unsub) + config_entry.async_on_unload(_async_close_websession) + + +@callback +def _async_register_default_clientsession_shutdown( + hass: HomeAssistant, clientsession: aiohttp.ClientSession +) -> None: + """Register default ClientSession close on Home Assistant shutdown. This method must be run in the event loop. """ diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index c35ba61a7670a..24d635d52a354 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -1,15 +1,16 @@ """Test the config manager.""" import asyncio from datetime import timedelta -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, Mock, patch import pytest from homeassistant import config_entries, data_entry_flow, loader -from homeassistant.const import EVENT_HOMEASSISTANT_STARTED +from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE, EVENT_HOMEASSISTANT_STARTED from homeassistant.core import CoreState, callback from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.aiohttp_client import async_create_clientsession from homeassistant.setup import async_setup_component from homeassistant.util import dt @@ -2489,3 +2490,97 @@ async def test_updating_entry_with_and_without_changes(manager): assert manager.async_update_entry(entry, title="newtitle") is True assert manager.async_update_entry(entry, unique_id="abc123") is False assert manager.async_update_entry(entry, unique_id="abc1234") is True + + +async def test_entry_reload_calls_on_unload_listeners(hass, manager): + """Test reload calls the on unload listeners.""" + entry = MockConfigEntry(domain="comp", state=config_entries.ENTRY_STATE_LOADED) + entry.add_to_hass(hass) + + async_setup = AsyncMock(return_value=True) + mock_setup_entry = AsyncMock(return_value=True) + async_unload_entry = AsyncMock(return_value=True) + + mock_integration( + hass, + MockModule( + "comp", + async_setup=async_setup, + async_setup_entry=mock_setup_entry, + async_unload_entry=async_unload_entry, + ), + ) + mock_entity_platform(hass, "config_flow.comp", None) + + mock_unload_callback = Mock() + + entry.async_on_unload(mock_unload_callback) + + assert await manager.async_reload(entry.entry_id) + assert len(async_unload_entry.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_unload_callback.mock_calls) == 1 + assert entry.state == config_entries.ENTRY_STATE_LOADED + + assert await manager.async_reload(entry.entry_id) + assert len(async_unload_entry.mock_calls) == 2 + assert len(mock_setup_entry.mock_calls) == 2 + # Since we did not register another async_on_unload it should + # have only been called once + assert len(mock_unload_callback.mock_calls) == 1 + assert entry.state == config_entries.ENTRY_STATE_LOADED + + +async def test_entry_reload_cleans_up_aiohttp_session(hass, manager): + """Test reload cleans up aiohttp sessions their close listener created by the config entry.""" + entry = MockConfigEntry(domain="comp", state=config_entries.ENTRY_STATE_LOADED) + entry.add_to_hass(hass) + async_setup_calls = 0 + + async def async_setup_entry(hass, _): + """Mock setup entry.""" + nonlocal async_setup_calls + async_setup_calls += 1 + async_create_clientsession(hass) + return True + + async_setup = AsyncMock(return_value=True) + async_unload_entry = AsyncMock(return_value=True) + + mock_integration( + hass, + MockModule( + "comp", + async_setup=async_setup, + async_setup_entry=async_setup_entry, + async_unload_entry=async_unload_entry, + ), + ) + mock_entity_platform(hass, "config_flow.comp", None) + + assert await manager.async_reload(entry.entry_id) + assert len(async_unload_entry.mock_calls) == 1 + assert async_setup_calls == 1 + assert entry.state == config_entries.ENTRY_STATE_LOADED + + original_close_listeners = hass.bus.async_listeners()[EVENT_HOMEASSISTANT_CLOSE] + + assert await manager.async_reload(entry.entry_id) + assert len(async_unload_entry.mock_calls) == 2 + assert async_setup_calls == 2 + assert entry.state == config_entries.ENTRY_STATE_LOADED + + assert ( + hass.bus.async_listeners()[EVENT_HOMEASSISTANT_CLOSE] + == original_close_listeners + ) + + assert await manager.async_reload(entry.entry_id) + assert len(async_unload_entry.mock_calls) == 3 + assert async_setup_calls == 3 + assert entry.state == config_entries.ENTRY_STATE_LOADED + + assert ( + hass.bus.async_listeners()[EVENT_HOMEASSISTANT_CLOSE] + == original_close_listeners + ) diff --git a/tests/test_util/aiohttp.py b/tests/test_util/aiohttp.py index 5219212f1cf19..58e4c6a227594 100644 --- a/tests/test_util/aiohttp.py +++ b/tests/test_util/aiohttp.py @@ -288,7 +288,7 @@ async def close_session(event): return session with mock.patch( - "homeassistant.helpers.aiohttp_client.async_create_clientsession", + "homeassistant.helpers.aiohttp_client._async_create_clientsession", side_effect=create_session, ): yield mocker From ac02f7c88a4d8ea471a59bdf1f3766d30ff93944 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 9 Apr 2021 07:16:59 -1000 Subject: [PATCH 0151/1317] Bump boto3 to 1.16.52 (#47772) --- homeassistant/components/amazon_polly/manifest.json | 2 +- homeassistant/components/aws/manifest.json | 2 +- homeassistant/components/aws/notify.py | 5 +---- homeassistant/components/route53/manifest.json | 2 +- requirements_all.txt | 4 ++-- requirements_test_all.txt | 2 +- 6 files changed, 7 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/amazon_polly/manifest.json b/homeassistant/components/amazon_polly/manifest.json index bdf049011559a..6b8a1894f5028 100644 --- a/homeassistant/components/amazon_polly/manifest.json +++ b/homeassistant/components/amazon_polly/manifest.json @@ -2,6 +2,6 @@ "domain": "amazon_polly", "name": "Amazon Polly", "documentation": "https://www.home-assistant.io/integrations/amazon_polly", - "requirements": ["boto3==1.9.252"], + "requirements": ["boto3==1.16.52"], "codeowners": [] } diff --git a/homeassistant/components/aws/manifest.json b/homeassistant/components/aws/manifest.json index bd9c76cc39714..a1a307dda9428 100644 --- a/homeassistant/components/aws/manifest.json +++ b/homeassistant/components/aws/manifest.json @@ -2,6 +2,6 @@ "domain": "aws", "name": "Amazon Web Services (AWS)", "documentation": "https://www.home-assistant.io/integrations/aws", - "requirements": ["aiobotocore==0.11.1"], + "requirements": ["aiobotocore==1.2.2"], "codeowners": [] } diff --git a/homeassistant/components/aws/notify.py b/homeassistant/components/aws/notify.py index f487bc7aab3f3..c9d6ca2faa725 100644 --- a/homeassistant/components/aws/notify.py +++ b/homeassistant/components/aws/notify.py @@ -28,10 +28,7 @@ async def get_available_regions(hass, service): """Get available regions for a service.""" session = aiobotocore.get_session() - # get_available_regions is not a coroutine since it does not perform - # network I/O. But it still perform file I/O heavily, so put it into - # an executor thread to unblock event loop - return await hass.async_add_executor_job(session.get_available_regions, service) + return await session.get_available_regions(service) async def async_get_service(hass, config, discovery_info=None): diff --git a/homeassistant/components/route53/manifest.json b/homeassistant/components/route53/manifest.json index 4879f12a3be82..61fb7d34ced05 100644 --- a/homeassistant/components/route53/manifest.json +++ b/homeassistant/components/route53/manifest.json @@ -2,6 +2,6 @@ "domain": "route53", "name": "AWS Route53", "documentation": "https://www.home-assistant.io/integrations/route53", - "requirements": ["boto3==1.9.252"], + "requirements": ["boto3==1.16.52"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index db48351aa5217..5fdfc887c2e1b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -144,7 +144,7 @@ aioasuswrt==1.3.1 aioazuredevops==1.3.5 # homeassistant.components.aws -aiobotocore==0.11.1 +aiobotocore==1.2.2 # homeassistant.components.dhcp aiodiscover==1.3.3 @@ -381,7 +381,7 @@ bond-api==0.1.12 # homeassistant.components.amazon_polly # homeassistant.components.route53 -boto3==1.9.252 +boto3==1.16.52 # homeassistant.components.braviatv bravia-tv==1.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 02f9ae3f4b163..2961a4e344a16 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -84,7 +84,7 @@ aioasuswrt==1.3.1 aioazuredevops==1.3.5 # homeassistant.components.aws -aiobotocore==0.11.1 +aiobotocore==1.2.2 # homeassistant.components.dhcp aiodiscover==1.3.3 From 7e2c8a27374dbc6feebc286e0f9dd0fbe6da2622 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Fri, 9 Apr 2021 19:36:13 +0200 Subject: [PATCH 0152/1317] Fix "notify.events" trim() issue + add initial tests (#48928) Co-authored-by: Paulus Schoutsen --- .../components/notify_events/notify.py | 5 +-- requirements_test_all.txt | 3 ++ tests/components/notify_events/__init__.py | 1 + tests/components/notify_events/test_init.py | 12 ++++++ tests/components/notify_events/test_notify.py | 38 +++++++++++++++++++ 5 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 tests/components/notify_events/__init__.py create mode 100644 tests/components/notify_events/test_init.py create mode 100644 tests/components/notify_events/test_notify.py diff --git a/homeassistant/components/notify_events/notify.py b/homeassistant/components/notify_events/notify.py index ce7c353badb3a..51705453edf74 100644 --- a/homeassistant/components/notify_events/notify.py +++ b/homeassistant/components/notify_events/notify.py @@ -116,12 +116,9 @@ def prepare_message(self, message, data) -> Message: def send_message(self, message, **kwargs): """Send a message.""" - token = self.token data = kwargs.get(ATTR_DATA) or {} + token = data.get(ATTR_TOKEN, self.token) msg = self.prepare_message(message, data) - if data.get(ATTR_TOKEN, "").trim(): - token = data[ATTR_TOKEN] - msg.send(token) diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2961a4e344a16..4223b1ce19c83 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -532,6 +532,9 @@ netdisco==2.8.2 # homeassistant.components.nexia nexia==0.9.5 +# homeassistant.components.notify_events +notify-events==1.0.4 + # homeassistant.components.nsw_fuel_station nsw-fuel-api-client==1.0.10 diff --git a/tests/components/notify_events/__init__.py b/tests/components/notify_events/__init__.py new file mode 100644 index 0000000000000..5e2f9c2eaf1c0 --- /dev/null +++ b/tests/components/notify_events/__init__.py @@ -0,0 +1 @@ +"""Tests for the notify_events integration.""" diff --git a/tests/components/notify_events/test_init.py b/tests/components/notify_events/test_init.py new file mode 100644 index 0000000000000..861be83a9ccb4 --- /dev/null +++ b/tests/components/notify_events/test_init.py @@ -0,0 +1,12 @@ +"""The tests for notify_events.""" +from homeassistant.components.notify_events.const import DOMAIN +from homeassistant.setup import async_setup_component + + +async def test_setup(hass): + """Test setup of the integration.""" + config = {"notify_events": {"token": "ABC"}} + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + + assert DOMAIN in hass.data diff --git a/tests/components/notify_events/test_notify.py b/tests/components/notify_events/test_notify.py new file mode 100644 index 0000000000000..55cf62750442e --- /dev/null +++ b/tests/components/notify_events/test_notify.py @@ -0,0 +1,38 @@ +"""The tests for notify_events.""" +from homeassistant.components.notify import ATTR_DATA, ATTR_MESSAGE, DOMAIN +from homeassistant.components.notify_events.notify import ( + ATTR_LEVEL, + ATTR_PRIORITY, + ATTR_TOKEN, +) + +from tests.common import async_mock_service + + +async def test_send_msg(hass): + """Test notify.events service.""" + notify_calls = async_mock_service(hass, DOMAIN, "events") + + await hass.services.async_call( + DOMAIN, + "events", + { + ATTR_MESSAGE: "message content", + ATTR_DATA: { + ATTR_TOKEN: "XYZ", + ATTR_LEVEL: "warning", + ATTR_PRIORITY: "high", + }, + }, + blocking=True, + ) + + assert len(notify_calls) == 1 + call = notify_calls[-1] + + assert call.domain == DOMAIN + assert call.service == "events" + assert call.data.get(ATTR_MESSAGE) == "message content" + assert call.data.get(ATTR_DATA).get(ATTR_TOKEN) == "XYZ" + assert call.data.get(ATTR_DATA).get(ATTR_LEVEL) == "warning" + assert call.data.get(ATTR_DATA).get(ATTR_PRIORITY) == "high" From 2e3258974170e84691390eb9e17888485bdd4681 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Fri, 9 Apr 2021 12:38:01 -0500 Subject: [PATCH 0153/1317] Fix Plex live TV handling (#48953) --- homeassistant/components/plex/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/plex/models.py b/homeassistant/components/plex/models.py index 731d5bbc7dbca..af1343095f0c0 100644 --- a/homeassistant/components/plex/models.py +++ b/homeassistant/components/plex/models.py @@ -7,7 +7,7 @@ ) from homeassistant.util import dt as dt_util -LIVE_TV_SECTION = "-4" +LIVE_TV_SECTION = -4 class PlexSession: From 43335953a2a3fc2ff58063f348929f4c84cf0a40 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 9 Apr 2021 20:53:20 +0200 Subject: [PATCH 0154/1317] Update frontend to 20210407.3 (#48957) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 98ae51341af4f..b69ee769d6689 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20210407.2" + "home-assistant-frontend==20210407.3" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 2a9df6eebe4f5..449c0602df213 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ defusedxml==0.6.0 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.42.0 -home-assistant-frontend==20210407.2 +home-assistant-frontend==20210407.3 httpx==0.17.1 jinja2>=2.11.3 netdisco==2.8.2 diff --git a/requirements_all.txt b/requirements_all.txt index 5fdfc887c2e1b..53d7ffd11f5ac 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -762,7 +762,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210407.2 +home-assistant-frontend==20210407.3 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4223b1ce19c83..46c2f2f25f3a9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -423,7 +423,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210407.2 +home-assistant-frontend==20210407.3 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 16196e2e16ea62b819f596a17acd12cd99cfd178 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 9 Apr 2021 21:10:02 +0200 Subject: [PATCH 0155/1317] Don't log template errors from developer tool (#48933) --- .../components/websocket_api/commands.py | 6 ++- homeassistant/helpers/event.py | 11 ++-- homeassistant/helpers/template.py | 52 +++++++++++++++---- .../components/websocket_api/test_commands.py | 31 +++++++++-- tests/helpers/test_template.py | 10 ++-- 5 files changed, 89 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 33a3370366821..301f106edcc33 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -290,6 +290,7 @@ def handle_ping(hass, connection, msg): vol.Optional("entity_ids"): cv.entity_ids, vol.Optional("variables"): dict, vol.Optional("timeout"): vol.Coerce(float), + vol.Optional("strict", default=False): bool, } ) @decorators.async_response @@ -303,7 +304,9 @@ async def handle_render_template(hass, connection, msg): if timeout: try: - timed_out = await template_obj.async_render_will_timeout(timeout) + timed_out = await template_obj.async_render_will_timeout( + timeout, strict=msg["strict"] + ) except TemplateError as ex: connection.send_error(msg["id"], const.ERR_TEMPLATE_ERROR, str(ex)) return @@ -337,6 +340,7 @@ def _template_listener(event, updates): [TrackTemplate(template_obj, variables)], _template_listener, raise_on_template_error=True, + strict=msg["strict"], ) except TemplateError as ex: connection.send_error(msg["id"], const.ERR_TEMPLATE_ERROR, str(ex)) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 2a3ee75ce7561..d52ebdb551f46 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -790,12 +790,14 @@ def __init__( self._track_state_changes: _TrackStateChangeFiltered | None = None self._time_listeners: dict[Template, Callable] = {} - def async_setup(self, raise_on_template_error: bool) -> None: + def async_setup(self, raise_on_template_error: bool, strict: bool = False) -> None: """Activation of template tracking.""" for track_template_ in self._track_templates: template = track_template_.template variables = track_template_.variables - self._info[template] = info = template.async_render_to_info(variables) + self._info[template] = info = template.async_render_to_info( + variables, strict=strict + ) if info.exception: if raise_on_template_error: @@ -1022,6 +1024,7 @@ def async_track_template_result( track_templates: Iterable[TrackTemplate], action: TrackTemplateResultListener, raise_on_template_error: bool = False, + strict: bool = False, ) -> _TrackTemplateResultInfo: """Add a listener that fires when the result of a template changes. @@ -1050,6 +1053,8 @@ def async_track_template_result( processing the template during setup, the system will raise the exception instead of setting up tracking. + strict + When set to True, raise on undefined variables. Returns ------- @@ -1057,7 +1062,7 @@ def async_track_template_result( """ tracker = _TrackTemplateResultInfo(hass, track_templates, action) - tracker.async_setup(raise_on_template_error) + tracker.async_setup(raise_on_template_error, strict=strict) return tracker diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 9580da82d65ac..ea338e22b8441 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -15,6 +15,7 @@ from operator import attrgetter import random import re +import sys from typing import Any, Generator, Iterable, cast from urllib.parse import urlencode as urllib_urlencode import weakref @@ -57,6 +58,7 @@ _RENDER_INFO = "template.render_info" _ENVIRONMENT = "template.environment" _ENVIRONMENT_LIMITED = "template.environment_limited" +_ENVIRONMENT_STRICT = "template.environment_strict" _RE_JINJA_DELIMITERS = re.compile(r"\{%|\{\{|\{#") # Match "simple" ints and floats. -1.0, 1, +5, 5.0 @@ -292,7 +294,9 @@ class Template: "is_static", "_compiled_code", "_compiled", + "_exc_info", "_limited", + "_strict", ) def __init__(self, template, hass=None): @@ -305,16 +309,23 @@ def __init__(self, template, hass=None): self._compiled: jinja2.Template | None = None self.hass = hass self.is_static = not is_template_string(template) + self._exc_info = None self._limited = None + self._strict = None @property def _env(self) -> TemplateEnvironment: if self.hass is None: return _NO_HASS_ENV - wanted_env = _ENVIRONMENT_LIMITED if self._limited else _ENVIRONMENT + if self._limited: + wanted_env = _ENVIRONMENT_LIMITED + elif self._strict: + wanted_env = _ENVIRONMENT_STRICT + else: + wanted_env = _ENVIRONMENT ret: TemplateEnvironment | None = self.hass.data.get(wanted_env) if ret is None: - ret = self.hass.data[wanted_env] = TemplateEnvironment(self.hass, self._limited) # type: ignore[no-untyped-call] + ret = self.hass.data[wanted_env] = TemplateEnvironment(self.hass, self._limited, self._strict) # type: ignore[no-untyped-call] return ret def ensure_valid(self) -> None: @@ -354,6 +365,7 @@ def async_render( variables: TemplateVarsType = None, parse_result: bool = True, limited: bool = False, + strict: bool = False, **kwargs: Any, ) -> Any: """Render given template. @@ -367,7 +379,7 @@ def async_render( return self.template return self._parse_result(self.template) - compiled = self._compiled or self._ensure_compiled(limited) + compiled = self._compiled or self._ensure_compiled(limited, strict) if variables is not None: kwargs.update(variables) @@ -418,7 +430,11 @@ def _parse_result(self, render_result: str) -> Any: # pylint: disable=no-self-u return render_result async def async_render_will_timeout( - self, timeout: float, variables: TemplateVarsType = None, **kwargs: Any + self, + timeout: float, + variables: TemplateVarsType = None, + strict: bool = False, + **kwargs: Any, ) -> bool: """Check to see if rendering a template will timeout during render. @@ -436,11 +452,12 @@ async def async_render_will_timeout( if self.is_static: return False - compiled = self._compiled or self._ensure_compiled() + compiled = self._compiled or self._ensure_compiled(strict=strict) if variables is not None: kwargs.update(variables) + self._exc_info = None finish_event = asyncio.Event() def _render_template() -> None: @@ -448,6 +465,8 @@ def _render_template() -> None: _render_with_context(self.template, compiled, **kwargs) except TimeoutError: pass + except Exception: # pylint: disable=broad-except + self._exc_info = sys.exc_info() finally: run_callback_threadsafe(self.hass.loop, finish_event.set) @@ -455,6 +474,8 @@ def _render_template() -> None: template_render_thread = ThreadWithException(target=_render_template) template_render_thread.start() await asyncio.wait_for(finish_event.wait(), timeout=timeout) + if self._exc_info: + raise TemplateError(self._exc_info[1].with_traceback(self._exc_info[2])) except asyncio.TimeoutError: template_render_thread.raise_exc(TimeoutError) return True @@ -465,7 +486,7 @@ def _render_template() -> None: @callback def async_render_to_info( - self, variables: TemplateVarsType = None, **kwargs: Any + self, variables: TemplateVarsType = None, strict: bool = False, **kwargs: Any ) -> RenderInfo: """Render the template and collect an entity filter.""" assert self.hass and _RENDER_INFO not in self.hass.data @@ -480,7 +501,7 @@ def async_render_to_info( self.hass.data[_RENDER_INFO] = render_info try: - render_info._result = self.async_render(variables, **kwargs) + render_info._result = self.async_render(variables, strict=strict, **kwargs) except TemplateError as ex: render_info.exception = ex finally: @@ -540,7 +561,9 @@ def async_render_with_possible_json_value( ) return value if error_value is _SENTINEL else error_value - def _ensure_compiled(self, limited: bool = False) -> jinja2.Template: + def _ensure_compiled( + self, limited: bool = False, strict: bool = False + ) -> jinja2.Template: """Bind a template to a specific hass instance.""" self.ensure_valid() @@ -548,8 +571,13 @@ def _ensure_compiled(self, limited: bool = False) -> jinja2.Template: assert ( self._limited is None or self._limited == limited ), "can't change between limited and non limited template" + assert ( + self._strict is None or self._strict == strict + ), "can't change between strict and non strict template" + assert not (strict and limited), "can't combine strict and limited template" self._limited = limited + self._strict = strict env = self._env self._compiled = cast( @@ -1369,9 +1397,13 @@ def __bool__(self): class TemplateEnvironment(ImmutableSandboxedEnvironment): """The Home Assistant template environment.""" - def __init__(self, hass, limited=False): + def __init__(self, hass, limited=False, strict=False): """Initialise template environment.""" - super().__init__(undefined=LoggingUndefined) + if not strict: + undefined = LoggingUndefined + else: + undefined = jinja2.StrictUndefined + super().__init__(undefined=undefined) self.hass = hass self.template_cache = weakref.WeakValueDictionary() self.filters["round"] = forgiving_round diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 3b01e6ecd8acb..67abb7b2b53cd 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -697,10 +697,19 @@ async def test_render_template_manual_entity_ids_no_longer_needed( } -async def test_render_template_with_error(hass, websocket_client, caplog): +@pytest.mark.parametrize( + "template", + [ + "{{ my_unknown_func() + 1 }}", + "{{ my_unknown_var }}", + "{{ my_unknown_var + 1 }}", + "{{ now() | unknown_filter }}", + ], +) +async def test_render_template_with_error(hass, websocket_client, caplog, template): """Test a template with an error.""" await websocket_client.send_json( - {"id": 5, "type": "render_template", "template": "{{ my_unknown_var() + 1 }}"} + {"id": 5, "type": "render_template", "template": template, "strict": True} ) msg = await websocket_client.receive_json() @@ -709,17 +718,30 @@ async def test_render_template_with_error(hass, websocket_client, caplog): assert not msg["success"] assert msg["error"]["code"] == const.ERR_TEMPLATE_ERROR + assert "Template variable error" not in caplog.text assert "TemplateError" not in caplog.text -async def test_render_template_with_timeout_and_error(hass, websocket_client, caplog): +@pytest.mark.parametrize( + "template", + [ + "{{ my_unknown_func() + 1 }}", + "{{ my_unknown_var }}", + "{{ my_unknown_var + 1 }}", + "{{ now() | unknown_filter }}", + ], +) +async def test_render_template_with_timeout_and_error( + hass, websocket_client, caplog, template +): """Test a template with an error with a timeout.""" await websocket_client.send_json( { "id": 5, "type": "render_template", - "template": "{{ now() | rando }}", + "template": template, "timeout": 5, + "strict": True, } ) @@ -729,6 +751,7 @@ async def test_render_template_with_timeout_and_error(hass, websocket_client, ca assert not msg["success"] assert msg["error"]["code"] == const.ERR_TEMPLATE_ERROR + assert "Template variable error" not in caplog.text assert "TemplateError" not in caplog.text diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index a8924f513c6e2..06b313218ca8e 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -2267,9 +2267,6 @@ async def test_template_timeout(hass): tmp = template.Template("{{ states | count }}", hass) assert await tmp.async_render_will_timeout(3) is False - tmp2 = template.Template("{{ error_invalid + 1 }}", hass) - assert await tmp2.async_render_will_timeout(3) is False - tmp3 = template.Template("static", hass) assert await tmp3.async_render_will_timeout(3) is False @@ -2287,6 +2284,13 @@ async def test_template_timeout(hass): assert await tmp5.async_render_will_timeout(0.000001) is True +async def test_template_timeout_raise(hass): + """Test we can raise from.""" + tmp2 = template.Template("{{ error_invalid + 1 }}", hass) + with pytest.raises(TemplateError): + assert await tmp2.async_render_will_timeout(3) is False + + async def test_lights(hass): """Test we can sort lights.""" From 9f06639ecc11f792d50a9ad4bd62aa62af4ae248 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 9 Apr 2021 13:43:38 -0700 Subject: [PATCH 0156/1317] Bump hass-nabucasa 0.43 (#48964) --- homeassistant/components/cloud/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index b854cb4578dcd..08bccf5eb6557 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -2,7 +2,7 @@ "domain": "cloud", "name": "Home Assistant Cloud", "documentation": "https://www.home-assistant.io/integrations/cloud", - "requirements": ["hass-nabucasa==0.42.0"], + "requirements": ["hass-nabucasa==0.43.0"], "dependencies": ["http", "webhook", "alexa"], "after_dependencies": ["google_assistant"], "codeowners": ["@home-assistant/cloud"] diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 449c0602df213..3b29c32ff18fb 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ cryptography==3.3.2 defusedxml==0.6.0 distro==1.5.0 emoji==1.2.0 -hass-nabucasa==0.42.0 +hass-nabucasa==0.43.0 home-assistant-frontend==20210407.3 httpx==0.17.1 jinja2>=2.11.3 diff --git a/requirements_all.txt b/requirements_all.txt index 53d7ffd11f5ac..95122b824bf4f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -729,7 +729,7 @@ habitipy==0.2.0 hangups==0.4.11 # homeassistant.components.cloud -hass-nabucasa==0.42.0 +hass-nabucasa==0.43.0 # homeassistant.components.splunk hass_splunk==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 46c2f2f25f3a9..b16fee985db21 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -402,7 +402,7 @@ habitipy==0.2.0 hangups==0.4.11 # homeassistant.components.cloud -hass-nabucasa==0.42.0 +hass-nabucasa==0.43.0 # homeassistant.components.tasmota hatasmota==0.2.9 From 6d5c34f2dc089721a539ca086a2c1a77e4eec12e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 9 Apr 2021 16:12:40 -0700 Subject: [PATCH 0157/1317] Fix config forwarding (#48967) --- homeassistant/components/template/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/template/__init__.py b/homeassistant/components/template/__init__.py index 72a97d6eeab7a..3b10e708e518b 100644 --- a/homeassistant/components/template/__init__.py +++ b/homeassistant/components/template/__init__.py @@ -68,7 +68,7 @@ async def _process_config(hass, config): async def init_coordinator(hass, conf): coordinator = TriggerUpdateCoordinator(hass, conf) - await coordinator.async_setup(conf) + await coordinator.async_setup(config) return coordinator hass.data[DOMAIN] = await asyncio.gather( From eef7faa1e44f281a738d8d997ffe47cbe73b24ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sat, 10 Apr 2021 01:13:07 +0200 Subject: [PATCH 0158/1317] Add TTS engines in config.components (#48939) Co-authored-by: Paulus Schoutsen --- homeassistant/components/tts/__init__.py | 5 +++++ homeassistant/components/tts/manifest.json | 1 + homeassistant/setup.py | 1 + tests/components/tts/test_init.py | 1 + 4 files changed, 8 insertions(+) diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index e0f59c51e5a36..5922392f17d97 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -31,6 +31,7 @@ CONF_PLATFORM, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, + PLATFORM_FORMAT, ) from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError @@ -316,6 +317,10 @@ def async_register_engine(self, engine, provider, config): provider.name = engine self.providers[engine] = provider + self.hass.config.components.add( + PLATFORM_FORMAT.format(domain=engine, platform=DOMAIN) + ) + async def async_get_url_path( self, engine, message, cache=None, language=None, options=None ): diff --git a/homeassistant/components/tts/manifest.json b/homeassistant/components/tts/manifest.json index 3db130d01bcd3..07cee3b867b61 100644 --- a/homeassistant/components/tts/manifest.json +++ b/homeassistant/components/tts/manifest.json @@ -5,5 +5,6 @@ "requirements": ["mutagen==1.45.1"], "dependencies": ["http"], "after_dependencies": ["media_player"], + "quality_scale": "internal", "codeowners": ["@pvizeli"] } diff --git a/homeassistant/setup.py b/homeassistant/setup.py index c65e428e03a45..6af20e21905f5 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -37,6 +37,7 @@ "scene", "sensor", "switch", + "tts", "vacuum", "water_heater", } diff --git a/tests/components/tts/test_init.py b/tests/components/tts/test_init.py index 77fbd3f7170df..8cd1641caa0ed 100644 --- a/tests/components/tts/test_init.py +++ b/tests/components/tts/test_init.py @@ -102,6 +102,7 @@ async def test_setup_component_demo(hass): assert hass.services.has_service(tts.DOMAIN, "demo_say") assert hass.services.has_service(tts.DOMAIN, "clear_cache") + assert f"{tts.DOMAIN}.demo" in hass.config.components async def test_setup_component_demo_no_access_cache_folder(hass, mock_init_cache_dir): From 28ad5b5514b94f1f46c6f670db6ab0eef5f2f004 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sat, 10 Apr 2021 01:14:48 +0200 Subject: [PATCH 0159/1317] Implement percentage_step and preset_mode is not not speed fix for MQTT fan (#48951) --- homeassistant/components/mqtt/fan.py | 88 ++---- tests/components/mqtt/test_fan.py | 410 ++++++++++----------------- 2 files changed, 168 insertions(+), 330 deletions(-) diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 395480a041d9f..6009b941c5c3d 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -32,6 +32,7 @@ from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.util.percentage import ( + int_states_in_range, ordered_list_item_to_percentage, percentage_to_ordered_list_item, percentage_to_ranged_value, @@ -224,6 +225,9 @@ def __init__(self, hass, config, config_entry, discovery_data): self._optimistic_preset_mode = None self._optimistic_speed = None + self._legacy_speeds_list = [] + self._legacy_speeds_list_no_off = [] + MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @staticmethod @@ -284,28 +288,18 @@ def _setup_from_config(self, config): self._legacy_speeds_list_no_off = speed_list_without_preset_modes( self._legacy_speeds_list ) - else: - self._legacy_speeds_list = [] self._feature_percentage = CONF_PERCENTAGE_COMMAND_TOPIC in config self._feature_preset_mode = CONF_PRESET_MODE_COMMAND_TOPIC in config if self._feature_preset_mode: - self._speeds_list = speed_list_without_preset_modes( - self._legacy_speeds_list + config[CONF_PRESET_MODES_LIST] - ) - self._preset_modes = ( - self._legacy_speeds_list + config[CONF_PRESET_MODES_LIST] - ) + self._preset_modes = config[CONF_PRESET_MODES_LIST] else: - self._speeds_list = speed_list_without_preset_modes( - self._legacy_speeds_list - ) self._preset_modes = [] - if not self._speeds_list or self._feature_percentage: - self._speed_count = 100 + if self._feature_percentage: + self._speed_count = min(int_states_in_range(self._speed_range), 100) else: - self._speed_count = len(self._speeds_list) + self._speed_count = len(self._legacy_speeds_list_no_off) or 100 optimistic = config[CONF_OPTIMISTIC] self._optimistic = optimistic or self._topic[CONF_STATE_TOPIC] is None @@ -327,11 +321,7 @@ def _setup_from_config(self, config): self._topic[CONF_OSCILLATION_COMMAND_TOPIC] is not None and SUPPORT_OSCILLATE ) - if self._feature_preset_mode and self._speeds_list: - self._supported_features |= SUPPORT_SET_SPEED - if self._feature_percentage: - self._supported_features |= SUPPORT_SET_SPEED - if self._feature_legacy_speeds: + if self._feature_percentage or self._feature_legacy_speeds: self._supported_features |= SUPPORT_SET_SPEED if self._feature_preset_mode: self._supported_features |= SUPPORT_PRESET_MODE @@ -414,10 +404,6 @@ def preset_mode_received(msg): return self._preset_mode = preset_mode - if not self._implemented_percentage and (preset_mode in self.speed_list): - self._percentage = ordered_list_item_to_percentage( - self.speed_list, preset_mode - ) self.async_write_ha_state() if self._topic[CONF_PRESET_MODE_STATE_TOPIC] is not None: @@ -455,10 +441,10 @@ def speed_received(msg): ) return - if not self._implemented_percentage: - if speed in self._speeds_list: + if not self._feature_percentage: + if speed in self._legacy_speeds_list_no_off: self._percentage = ordered_list_item_to_percentage( - self._speeds_list, speed + self._legacy_speeds_list_no_off, speed ) elif speed == SPEED_OFF: self._percentage = 0 @@ -506,19 +492,9 @@ def is_on(self): """Return true if device is on.""" return self._state - @property - def _implemented_percentage(self): - """Return true if percentage has been implemented.""" - return self._feature_percentage - - @property - def _implemented_preset_mode(self): - """Return true if preset_mode has been implemented.""" - return self._feature_preset_mode - # The use of legacy speeds is deprecated in the schema, support will be removed after a quarter (2021.7) @property - def _implemented_speed(self): + def _implemented_speed(self) -> bool: """Return true if speed has been implemented.""" return self._feature_legacy_speeds @@ -541,7 +517,7 @@ def preset_modes(self) -> list: @property def speed_list(self) -> list: """Get the list of available speeds.""" - return self._speeds_list + return self._legacy_speeds_list_no_off @property def supported_features(self) -> int: @@ -555,7 +531,7 @@ def speed(self): @property def speed_count(self) -> int: - """Return the number of speeds the fan supports or 100 if percentage is supported.""" + """Return the number of speeds the fan supports.""" return self._speed_count @property @@ -620,20 +596,8 @@ async def async_set_percentage(self, percentage: int) -> None: percentage_to_ranged_value(self._speed_range, percentage) ) mqtt_payload = self._command_templates[ATTR_PERCENTAGE](percentage_payload) - if self._implemented_preset_mode: - if percentage: - await self.async_set_preset_mode( - preset_mode=percentage_to_ordered_list_item( - self.speed_list, percentage - ) - ) - # Legacy are deprecated in the schema, support will be removed after a quarter (2021.7) - elif self._feature_legacy_speeds and ( - SPEED_OFF in self._legacy_speeds_list - ): - await self.async_set_preset_mode(SPEED_OFF) # Legacy are deprecated in the schema, support will be removed after a quarter (2021.7) - elif self._feature_legacy_speeds: + if self._feature_legacy_speeds: if percentage: await self.async_set_speed( percentage_to_ordered_list_item( @@ -644,7 +608,7 @@ async def async_set_percentage(self, percentage: int) -> None: elif SPEED_OFF in self._legacy_speeds_list: await self.async_set_speed(SPEED_OFF) - if self._implemented_percentage: + if self._feature_percentage: mqtt.async_publish( self.hass, self._topic[CONF_PERCENTAGE_COMMAND_TOPIC], @@ -665,13 +629,7 @@ async def async_set_preset_mode(self, preset_mode: str) -> None: if preset_mode not in self.preset_modes: _LOGGER.warning("'%s'is not a valid preset mode", preset_mode) return - # Legacy are deprecated in the schema, support will be removed after a quarter (2021.7) - if preset_mode in self._legacy_speeds_list: - await self.async_set_speed(speed=preset_mode) - if not self._implemented_percentage and preset_mode in self.speed_list: - self._percentage = ordered_list_item_to_percentage( - self.speed_list, preset_mode - ) + mqtt_payload = self._command_templates[ATTR_PRESET_MODE](preset_mode) mqtt.async_publish( @@ -693,18 +651,18 @@ async def async_set_speed(self, speed: str) -> None: This method is a coroutine. """ speed_payload = None - if self._feature_legacy_speeds: + if speed in self._legacy_speeds_list: if speed == SPEED_LOW: speed_payload = self._payload["SPEED_LOW"] elif speed == SPEED_MEDIUM: speed_payload = self._payload["SPEED_MEDIUM"] elif speed == SPEED_HIGH: speed_payload = self._payload["SPEED_HIGH"] - elif speed == SPEED_OFF: - speed_payload = self._payload["SPEED_OFF"] else: - _LOGGER.warning("'%s'is not a valid speed", speed) - return + speed_payload = self._payload["SPEED_OFF"] + else: + _LOGGER.warning("'%s' is not a valid speed", speed) + return if speed_payload: mqtt.async_publish( diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index be32540b04df6..5caec9b7473f7 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -85,11 +85,11 @@ async def test_controlling_state_via_topic(hass, mqtt_mock, caplog): "preset_mode_state_topic": "preset-mode-state-topic", "preset_mode_command_topic": "preset-mode-command-topic", "preset_modes": [ - "medium", - "medium-high", - "high", - "very-high", - "freaking-high", + "auto", + "smart", + "whoosh", + "eco", + "breeze", "silent", ], "speed_range_min": 1, @@ -126,6 +126,8 @@ async def test_controlling_state_via_topic(hass, mqtt_mock, caplog): state = hass.states.get("fan.test") assert state.attributes.get("oscillating") is False + assert state.attributes.get("percentage_step") == 1.0 + async_fire_mqtt_message(hass, "percentage-state-topic", "0") state = hass.states.get("fan.test") assert state.attributes.get(fan.ATTR_PERCENTAGE) == 0 @@ -151,16 +153,16 @@ async def test_controlling_state_via_topic(hass, mqtt_mock, caplog): caplog.clear() async_fire_mqtt_message(hass, "preset-mode-state-topic", "low") - state = hass.states.get("fan.test") - assert state.attributes.get("preset_mode") == "low" + assert "not a valid preset mode" in caplog.text + caplog.clear() - async_fire_mqtt_message(hass, "preset-mode-state-topic", "medium") + async_fire_mqtt_message(hass, "preset-mode-state-topic", "auto") state = hass.states.get("fan.test") - assert state.attributes.get("preset_mode") == "medium" + assert state.attributes.get("preset_mode") == "auto" - async_fire_mqtt_message(hass, "preset-mode-state-topic", "very-high") + async_fire_mqtt_message(hass, "preset-mode-state-topic", "eco") state = hass.states.get("fan.test") - assert state.attributes.get("preset_mode") == "very-high" + assert state.attributes.get("preset_mode") == "eco" async_fire_mqtt_message(hass, "preset-mode-state-topic", "silent") state = hass.states.get("fan.test") @@ -256,7 +258,9 @@ async def test_controlling_state_via_topic_with_different_speed_range( caplog.clear() -async def test_controlling_state_via_topic_no_percentage_topics(hass, mqtt_mock): +async def test_controlling_state_via_topic_no_percentage_topics( + hass, mqtt_mock, caplog +): """Test the controlling state via topic without percentage topics.""" assert await async_setup_component( hass, @@ -273,9 +277,11 @@ async def test_controlling_state_via_topic_no_percentage_topics(hass, mqtt_mock) "preset_mode_state_topic": "preset-mode-state-topic", "preset_mode_command_topic": "preset-mode-command-topic", "preset_modes": [ - "high", - "freaking-high", - "silent", + "auto", + "smart", + "whoosh", + "eco", + "breeze", ], # use of speeds is deprecated, support will be removed after a quarter (2021.7) "speeds": ["off", "low", "medium"], @@ -288,57 +294,51 @@ async def test_controlling_state_via_topic_no_percentage_topics(hass, mqtt_mock) assert state.state == STATE_OFF assert not state.attributes.get(ATTR_ASSUMED_STATE) - async_fire_mqtt_message(hass, "preset-mode-state-topic", "freaking-high") + async_fire_mqtt_message(hass, "preset-mode-state-topic", "smart") state = hass.states.get("fan.test") - assert state.attributes.get("preset_mode") == "freaking-high" - assert state.attributes.get(fan.ATTR_PERCENTAGE) == 100 + assert state.attributes.get("preset_mode") == "smart" + assert state.attributes.get(fan.ATTR_PERCENTAGE) is None # use of speeds is deprecated, support will be removed after a quarter (2021.7) assert state.attributes.get("speed") == fan.SPEED_OFF - async_fire_mqtt_message(hass, "preset-mode-state-topic", "high") + async_fire_mqtt_message(hass, "preset-mode-state-topic", "auto") state = hass.states.get("fan.test") - assert state.attributes.get("preset_mode") == "high" - assert state.attributes.get(fan.ATTR_PERCENTAGE) == 75 + assert state.attributes.get("preset_mode") == "auto" + assert state.attributes.get(fan.ATTR_PERCENTAGE) is None # use of speeds is deprecated, support will be removed after a quarter (2021.7) assert state.attributes.get("speed") == fan.SPEED_OFF - async_fire_mqtt_message(hass, "preset-mode-state-topic", "silent") + async_fire_mqtt_message(hass, "preset-mode-state-topic", "whoosh") state = hass.states.get("fan.test") - assert state.attributes.get("preset_mode") == "silent" - assert state.attributes.get(fan.ATTR_PERCENTAGE) == 75 + assert state.attributes.get("preset_mode") == "whoosh" + assert state.attributes.get(fan.ATTR_PERCENTAGE) is None # use of speeds is deprecated, support will be removed after a quarter (2021.7) assert state.attributes.get("speed") == fan.SPEED_OFF async_fire_mqtt_message(hass, "preset-mode-state-topic", "medium") - state = hass.states.get("fan.test") - assert state.attributes.get("preset_mode") == "medium" - assert state.attributes.get(fan.ATTR_PERCENTAGE) == 50 - # use of speeds is deprecated, support will be removed after a quarter (2021.7) - assert state.attributes.get("speed") == fan.SPEED_OFF + assert "not a valid preset mode" in caplog.text + caplog.clear() async_fire_mqtt_message(hass, "preset-mode-state-topic", "low") - state = hass.states.get("fan.test") - assert state.attributes.get("preset_mode") == "low" - assert state.attributes.get(fan.ATTR_PERCENTAGE) == 25 - # use of speeds is deprecated, support will be removed after a quarter (2021.7) - assert state.attributes.get("speed") == fan.SPEED_OFF + assert "not a valid preset mode" in caplog.text + caplog.clear() # use of speeds is deprecated, support will be removed after a quarter (2021.7) async_fire_mqtt_message(hass, "speed-state-topic", "medium") state = hass.states.get("fan.test") - assert state.attributes.get("preset_mode") == "low" - assert state.attributes.get(fan.ATTR_PERCENTAGE) == 50 + assert state.attributes.get("preset_mode") == "whoosh" + assert state.attributes.get(fan.ATTR_PERCENTAGE) == 100 assert state.attributes.get("speed") == fan.SPEED_MEDIUM async_fire_mqtt_message(hass, "speed-state-topic", "low") state = hass.states.get("fan.test") - assert state.attributes.get("preset_mode") == "low" - assert state.attributes.get(fan.ATTR_PERCENTAGE) == 25 + assert state.attributes.get("preset_mode") == "whoosh" + assert state.attributes.get(fan.ATTR_PERCENTAGE) == 50 assert state.attributes.get("speed") == fan.SPEED_LOW async_fire_mqtt_message(hass, "speed-state-topic", "off") state = hass.states.get("fan.test") - assert state.attributes.get("preset_mode") == "low" + assert state.attributes.get("preset_mode") == "whoosh" assert state.attributes.get(fan.ATTR_PERCENTAGE) == 0 assert state.attributes.get("speed") == fan.SPEED_OFF @@ -361,11 +361,11 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock, cap "preset_mode_state_topic": "preset-mode-state-topic", "preset_mode_command_topic": "preset-mode-command-topic", "preset_modes": [ - "medium", - "medium-high", - "high", - "very-high", - "freaking-high", + "auto", + "smart", + "whoosh", + "eco", + "breeze", "silent", ], "state_value_template": "{{ value_json.val }}", @@ -412,20 +412,20 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock, cap assert "not a valid preset mode" in caplog.text caplog.clear() - async_fire_mqtt_message(hass, "preset-mode-state-topic", '{"val": "medium"}') + async_fire_mqtt_message(hass, "preset-mode-state-topic", '{"val": "auto"}') state = hass.states.get("fan.test") - assert state.attributes.get("preset_mode") == "medium" + assert state.attributes.get("preset_mode") == "auto" - async_fire_mqtt_message(hass, "preset-mode-state-topic", '{"val": "freaking-high"}') + async_fire_mqtt_message(hass, "preset-mode-state-topic", '{"val": "breeze"}') state = hass.states.get("fan.test") - assert state.attributes.get("preset_mode") == "freaking-high" + assert state.attributes.get("preset_mode") == "breeze" async_fire_mqtt_message(hass, "preset-mode-state-topic", '{"val": "silent"}') state = hass.states.get("fan.test") assert state.attributes.get("preset_mode") == "silent" -async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): +async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock, caplog): """Test optimistic mode without state topic.""" assert await async_setup_component( hass, @@ -447,8 +447,8 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): # use of speeds is deprecated, support will be removed after a quarter (2021.7) "speeds": ["off", "low", "medium"], "preset_modes": [ - "high", - "freaking-high", + "whoosh", + "breeze", "silent", ], # use of speeds is deprecated, support will be removed after a quarter (2021.7) @@ -510,7 +510,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): assert mqtt_mock.async_publish.call_count == 2 mqtt_mock.async_publish.assert_any_call("percentage-command-topic", "100", 0, False) mqtt_mock.async_publish.assert_any_call( - "preset-mode-command-topic", "freaking-high", 0, False + "speed-command-topic", "speed_mEdium", 0, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("fan.test") @@ -518,11 +518,8 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): assert state.attributes.get(ATTR_ASSUMED_STATE) await common.async_set_percentage(hass, "fan.test", 0) - assert mqtt_mock.async_publish.call_count == 3 + assert mqtt_mock.async_publish.call_count == 2 mqtt_mock.async_publish.assert_any_call("percentage-command-topic", "0", 0, False) - mqtt_mock.async_publish.assert_any_call( - "preset-mode-command-topic", "off", 0, False - ) # use of speeds is deprecated, support will be removed after a quarter (2021.7) mqtt_mock.async_publish.assert_any_call( "speed-command-topic", "speed_OfF", 0, False @@ -534,54 +531,32 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): assert state.attributes.get(fan.ATTR_SPEED) == fan.SPEED_OFF assert state.attributes.get(ATTR_ASSUMED_STATE) - await common.async_set_preset_mode(hass, "fan.test", "low") - assert mqtt_mock.async_publish.call_count == 2 - # use of speeds is deprecated, support will be removed after a quarter (2021.7) - mqtt_mock.async_publish.assert_any_call( - "speed-command-topic", "speed_lOw", 0, False - ) - mqtt_mock.async_publish.assert_any_call( - "preset-mode-command-topic", "low", 0, False - ) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get("fan.test") - assert state.attributes.get(fan.ATTR_PRESET_MODE) == "low" # use of speeds is deprecated, support will be removed after a quarter (2021.7) - assert state.attributes.get(fan.ATTR_SPEED) == fan.SPEED_LOW - assert state.attributes.get(ATTR_ASSUMED_STATE) + await common.async_set_preset_mode(hass, "fan.test", "low") + assert "not a valid preset mode" in caplog.text + caplog.clear() - await common.async_set_preset_mode(hass, "fan.test", "medium") - assert mqtt_mock.async_publish.call_count == 2 # use of speeds is deprecated, support will be removed after a quarter (2021.7) - mqtt_mock.async_publish.assert_any_call( - "speed-command-topic", "speed_mEdium", 0, False - ) - mqtt_mock.async_publish.assert_any_call( - "preset-mode-command-topic", "medium", 0, False - ) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get("fan.test") - assert state.attributes.get(fan.ATTR_PRESET_MODE) == "medium" - # use of speeds is deprecated, support will be removed after a quarter (2021.7) - assert state.attributes.get(fan.ATTR_SPEED) == fan.SPEED_MEDIUM - assert state.attributes.get(ATTR_ASSUMED_STATE) + await common.async_set_preset_mode(hass, "fan.test", "medium") + assert "not a valid preset mode" in caplog.text + caplog.clear() - await common.async_set_preset_mode(hass, "fan.test", "high") + await common.async_set_preset_mode(hass, "fan.test", "whoosh") mqtt_mock.async_publish.assert_called_once_with( - "preset-mode-command-topic", "high", 0, False + "preset-mode-command-topic", "whoosh", 0, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("fan.test") - assert state.attributes.get(fan.ATTR_PRESET_MODE) == "high" + assert state.attributes.get(fan.ATTR_PRESET_MODE) == "whoosh" assert state.attributes.get(ATTR_ASSUMED_STATE) - await common.async_set_preset_mode(hass, "fan.test", "freaking-high") + await common.async_set_preset_mode(hass, "fan.test", "breeze") mqtt_mock.async_publish.assert_called_once_with( - "preset-mode-command-topic", "freaking-high", 0, False + "preset-mode-command-topic", "breeze", 0, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("fan.test") - assert state.attributes.get(fan.ATTR_PRESET_MODE) == "freaking-high" + assert state.attributes.get(fan.ATTR_PRESET_MODE) == "breeze" assert state.attributes.get(ATTR_ASSUMED_STATE) await common.async_set_preset_mode(hass, "fan.test", "silent") @@ -615,13 +590,8 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): # use of speeds is deprecated, support will be removed after a quarter (2021.7) await common.async_set_speed(hass, "fan.test", fan.SPEED_HIGH) - mqtt_mock.async_publish.assert_called_once_with( - "speed-command-topic", "speed_High", 0, False - ) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get("fan.test") - assert state.state == STATE_OFF - assert state.attributes.get(ATTR_ASSUMED_STATE) + assert "not a valid speed" in caplog.text + caplog.clear() # use of speeds is deprecated, support will be removed after a quarter (2021.7) await common.async_set_speed(hass, "fan.test", fan.SPEED_OFF) @@ -735,8 +705,8 @@ async def test_sending_mqtt_commands_and_optimistic_no_legacy(hass, mqtt_mock, c "percentage_command_topic": "percentage-command-topic", "preset_mode_command_topic": "preset-mode-command-topic", "preset_modes": [ - "high", - "freaking-high", + "whoosh", + "breeze", "silent", ], } @@ -769,14 +739,12 @@ async def test_sending_mqtt_commands_and_optimistic_no_legacy(hass, mqtt_mock, c await common.async_set_percentage(hass, "fan.test", 101) await common.async_set_percentage(hass, "fan.test", 100) - mqtt_mock.async_publish.assert_any_call("percentage-command-topic", "100", 0, False) - mqtt_mock.async_publish.assert_any_call( - "preset-mode-command-topic", "freaking-high", 0, False + mqtt_mock.async_publish.assert_called_once_with( + "percentage-command-topic", "100", 0, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("fan.test") assert state.attributes.get(fan.ATTR_PERCENTAGE) == 100 - assert state.attributes.get(fan.ATTR_PRESET_MODE) == "freaking-high" assert state.attributes.get(ATTR_ASSUMED_STATE) await common.async_set_percentage(hass, "fan.test", 0) @@ -793,26 +761,26 @@ async def test_sending_mqtt_commands_and_optimistic_no_legacy(hass, mqtt_mock, c assert "not a valid preset mode" in caplog.text caplog.clear() - await common.async_set_preset_mode(hass, "fan.test", "medium") + await common.async_set_preset_mode(hass, "fan.test", "auto") assert "not a valid preset mode" in caplog.text caplog.clear() - await common.async_set_preset_mode(hass, "fan.test", "high") + await common.async_set_preset_mode(hass, "fan.test", "whoosh") mqtt_mock.async_publish.assert_called_once_with( - "preset-mode-command-topic", "high", 0, False + "preset-mode-command-topic", "whoosh", 0, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("fan.test") - assert state.attributes.get(fan.ATTR_PRESET_MODE) == "high" + assert state.attributes.get(fan.ATTR_PRESET_MODE) == "whoosh" assert state.attributes.get(ATTR_ASSUMED_STATE) - await common.async_set_preset_mode(hass, "fan.test", "freaking-high") + await common.async_set_preset_mode(hass, "fan.test", "breeze") mqtt_mock.async_publish.assert_called_once_with( - "preset-mode-command-topic", "freaking-high", 0, False + "preset-mode-command-topic", "breeze", 0, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("fan.test") - assert state.attributes.get(fan.ATTR_PRESET_MODE) == "freaking-high" + assert state.attributes.get(fan.ATTR_PRESET_MODE) == "breeze" assert state.attributes.get(ATTR_ASSUMED_STATE) await common.async_set_preset_mode(hass, "fan.test", "silent") @@ -825,12 +793,9 @@ async def test_sending_mqtt_commands_and_optimistic_no_legacy(hass, mqtt_mock, c assert state.attributes.get(ATTR_ASSUMED_STATE) await common.async_turn_on(hass, "fan.test", percentage=25) - assert mqtt_mock.async_publish.call_count == 3 + assert mqtt_mock.async_publish.call_count == 2 mqtt_mock.async_publish.assert_any_call("command-topic", "ON", 0, False) mqtt_mock.async_publish.assert_any_call("percentage-command-topic", "25", 0, False) - mqtt_mock.async_publish.assert_any_call( - "preset-mode-command-topic", "high", 0, False - ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("fan.test") assert state.state == STATE_ON @@ -843,11 +808,11 @@ async def test_sending_mqtt_commands_and_optimistic_no_legacy(hass, mqtt_mock, c assert state.state == STATE_OFF assert state.attributes.get(ATTR_ASSUMED_STATE) - await common.async_turn_on(hass, "fan.test", preset_mode="high") + await common.async_turn_on(hass, "fan.test", preset_mode="whoosh") assert mqtt_mock.async_publish.call_count == 2 mqtt_mock.async_publish.assert_any_call("command-topic", "ON", 0, False) mqtt_mock.async_publish.assert_any_call( - "preset-mode-command-topic", "high", 0, False + "preset-mode-command-topic", "whoosh", 0, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("fan.test") @@ -855,7 +820,7 @@ async def test_sending_mqtt_commands_and_optimistic_no_legacy(hass, mqtt_mock, c assert state.attributes.get(ATTR_ASSUMED_STATE) with pytest.raises(NotValidPresetModeError): - await common.async_turn_on(hass, "fan.test", preset_mode="low") + await common.async_turn_on(hass, "fan.test", preset_mode="freaking-high") async def test_sending_mqtt_command_templates_(hass, mqtt_mock, caplog): @@ -876,8 +841,8 @@ async def test_sending_mqtt_command_templates_(hass, mqtt_mock, caplog): "preset_mode_command_topic": "preset-mode-command-topic", "preset_mode_command_template": "preset_mode: {{ value }}", "preset_modes": [ - "high", - "freaking-high", + "whoosh", + "breeze", "silent", ], } @@ -914,16 +879,12 @@ async def test_sending_mqtt_command_templates_(hass, mqtt_mock, caplog): await common.async_set_percentage(hass, "fan.test", 101) await common.async_set_percentage(hass, "fan.test", 100) - mqtt_mock.async_publish.assert_any_call( + mqtt_mock.async_publish.assert_called_once_with( "percentage-command-topic", "percentage: 100", 0, False ) - mqtt_mock.async_publish.assert_any_call( - "preset-mode-command-topic", "preset_mode: freaking-high", 0, False - ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("fan.test") assert state.attributes.get(fan.ATTR_PERCENTAGE) == 100 - assert state.attributes.get(fan.ATTR_PRESET_MODE) == "freaking-high" assert state.attributes.get(ATTR_ASSUMED_STATE) await common.async_set_percentage(hass, "fan.test", 0) @@ -944,22 +905,22 @@ async def test_sending_mqtt_command_templates_(hass, mqtt_mock, caplog): assert "not a valid preset mode" in caplog.text caplog.clear() - await common.async_set_preset_mode(hass, "fan.test", "high") + await common.async_set_preset_mode(hass, "fan.test", "whoosh") mqtt_mock.async_publish.assert_called_once_with( - "preset-mode-command-topic", "preset_mode: high", 0, False + "preset-mode-command-topic", "preset_mode: whoosh", 0, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("fan.test") - assert state.attributes.get(fan.ATTR_PRESET_MODE) == "high" + assert state.attributes.get(fan.ATTR_PRESET_MODE) == "whoosh" assert state.attributes.get(ATTR_ASSUMED_STATE) - await common.async_set_preset_mode(hass, "fan.test", "freaking-high") + await common.async_set_preset_mode(hass, "fan.test", "breeze") mqtt_mock.async_publish.assert_called_once_with( - "preset-mode-command-topic", "preset_mode: freaking-high", 0, False + "preset-mode-command-topic", "preset_mode: breeze", 0, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("fan.test") - assert state.attributes.get(fan.ATTR_PRESET_MODE) == "freaking-high" + assert state.attributes.get(fan.ATTR_PRESET_MODE) == "breeze" assert state.attributes.get(ATTR_ASSUMED_STATE) await common.async_set_preset_mode(hass, "fan.test", "silent") @@ -972,14 +933,11 @@ async def test_sending_mqtt_command_templates_(hass, mqtt_mock, caplog): assert state.attributes.get(ATTR_ASSUMED_STATE) await common.async_turn_on(hass, "fan.test", percentage=25) - assert mqtt_mock.async_publish.call_count == 3 + assert mqtt_mock.async_publish.call_count == 2 mqtt_mock.async_publish.assert_any_call("command-topic", "state: ON", 0, False) mqtt_mock.async_publish.assert_any_call( "percentage-command-topic", "percentage: 25", 0, False ) - mqtt_mock.async_publish.assert_any_call( - "preset-mode-command-topic", "preset_mode: high", 0, False - ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("fan.test") assert state.state == STATE_ON @@ -992,11 +950,11 @@ async def test_sending_mqtt_command_templates_(hass, mqtt_mock, caplog): assert state.state == STATE_OFF assert state.attributes.get(ATTR_ASSUMED_STATE) - await common.async_turn_on(hass, "fan.test", preset_mode="high") + await common.async_turn_on(hass, "fan.test", preset_mode="whoosh") assert mqtt_mock.async_publish.call_count == 2 mqtt_mock.async_publish.assert_any_call("command-topic", "state: ON", 0, False) mqtt_mock.async_publish.assert_any_call( - "preset-mode-command-topic", "preset_mode: high", 0, False + "preset-mode-command-topic", "preset_mode: whoosh", 0, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("fan.test") @@ -1008,7 +966,7 @@ async def test_sending_mqtt_command_templates_(hass, mqtt_mock, caplog): async def test_sending_mqtt_commands_and_optimistic_no_percentage_topic( - hass, mqtt_mock + hass, mqtt_mock, caplog ): """Test optimistic mode without state topic without percentage command topic.""" assert await async_setup_component( @@ -1027,9 +985,10 @@ async def test_sending_mqtt_commands_and_optimistic_no_percentage_topic( # use of speeds is deprecated, support will be removed after a quarter (2021.7) "speeds": ["off", "low", "medium"], "preset_modes": [ - "high", - "freaking-high", + "whoosh", + "breeze", "silent", + "high", ], } }, @@ -1047,9 +1006,7 @@ async def test_sending_mqtt_commands_and_optimistic_no_percentage_topic( await common.async_set_percentage(hass, "fan.test", 101) await common.async_set_percentage(hass, "fan.test", 100) - mqtt_mock.async_publish.assert_any_call( - "preset-mode-command-topic", "freaking-high", 0, False - ) + mqtt_mock.async_publish.assert_any_call("speed-command-topic", "medium", 0, False) mqtt_mock.async_publish.reset_mock() state = hass.states.get("fan.test") assert state.attributes.get(fan.ATTR_PERCENTAGE) == 100 @@ -1063,41 +1020,27 @@ async def test_sending_mqtt_commands_and_optimistic_no_percentage_topic( assert state.attributes.get(fan.ATTR_PERCENTAGE) == 0 assert state.attributes.get(ATTR_ASSUMED_STATE) - await common.async_set_preset_mode(hass, "fan.test", "low") - assert mqtt_mock.async_publish.call_count == 2 # use of speeds is deprecated, support will be removed after a quarter (2021.7) - mqtt_mock.async_publish.assert_any_call("speed-command-topic", "low", 0, False) - mqtt_mock.async_publish.assert_any_call( - "preset-mode-command-topic", "low", 0, False - ) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get("fan.test") - assert state.attributes.get(fan.ATTR_PRESET_MODE) is None - assert state.attributes.get(ATTR_ASSUMED_STATE) + await common.async_set_preset_mode(hass, "fan.test", "low") + assert "not a valid preset mode" in caplog.text + caplog.clear() await common.async_set_preset_mode(hass, "fan.test", "medium") - assert mqtt_mock.async_publish.call_count == 2 - mqtt_mock.async_publish.assert_any_call("speed-command-topic", "medium", 0, False) - mqtt_mock.async_publish.assert_any_call( - "preset-mode-command-topic", "medium", 0, False - ) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get("fan.test") - assert state.attributes.get(fan.ATTR_PRESET_MODE) is None - assert state.attributes.get(ATTR_ASSUMED_STATE) + assert "not a valid preset mode" in caplog.text + caplog.clear() - await common.async_set_preset_mode(hass, "fan.test", "high") + await common.async_set_preset_mode(hass, "fan.test", "whoosh") mqtt_mock.async_publish.assert_called_once_with( - "preset-mode-command-topic", "high", 0, False + "preset-mode-command-topic", "whoosh", 0, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("fan.test") assert state.attributes.get(fan.ATTR_PRESET_MODE) is None assert state.attributes.get(ATTR_ASSUMED_STATE) - await common.async_set_preset_mode(hass, "fan.test", "freaking-high") + await common.async_set_preset_mode(hass, "fan.test", "breeze") mqtt_mock.async_publish.assert_called_once_with( - "preset-mode-command-topic", "freaking-high", 0, False + "preset-mode-command-topic", "breeze", 0, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("fan.test") @@ -1133,14 +1076,8 @@ async def test_sending_mqtt_commands_and_optimistic_no_percentage_topic( assert state.attributes.get(ATTR_ASSUMED_STATE) await common.async_set_speed(hass, "fan.test", fan.SPEED_HIGH) - mqtt_mock.async_publish.assert_called_once_with( - "speed-command-topic", "high", 0, False - ) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get("fan.test") - assert state.state == STATE_OFF - assert state.attributes.get(ATTR_ASSUMED_STATE) - + assert "not a valid speed" in caplog.text + caplog.clear() await common.async_set_speed(hass, "fan.test", fan.SPEED_OFF) mqtt_mock.async_publish.assert_any_call("speed-command-topic", "off", 0, False) @@ -1150,13 +1087,10 @@ async def test_sending_mqtt_commands_and_optimistic_no_percentage_topic( assert state.attributes.get(ATTR_ASSUMED_STATE) await common.async_turn_on(hass, "fan.test", speed="medium") - assert mqtt_mock.async_publish.call_count == 3 + assert mqtt_mock.async_publish.call_count == 2 mqtt_mock.async_publish.assert_any_call("command-topic", "ON", 0, False) # use of speeds is deprecated, support will be removed after a quarter (2021.7) mqtt_mock.async_publish.assert_any_call("speed-command-topic", "medium", 0, False) - mqtt_mock.async_publish.assert_any_call( - "preset-mode-command-topic", "medium", 0, False - ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("fan.test") assert state.state == STATE_ON @@ -1325,8 +1259,8 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock, ca # use of speeds is deprecated, support will be removed after a quarter (2021.7) "speeds": ["off", "low", "medium"], "preset_modes": [ - "high", - "freaking-high", + "whoosh", + "breeze", "silent", ], "optimistic": True, @@ -1358,9 +1292,7 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock, ca mqtt_mock.async_publish.assert_any_call("command-topic", "ON", 0, False) # use of speeds is deprecated, support will be removed after a quarter (2021.7) mqtt_mock.async_publish.assert_any_call("speed-command-topic", "medium", 0, False) - mqtt_mock.async_publish.assert_any_call( - "preset-mode-command-topic", "medium", 0, False - ) + mqtt_mock.async_publish.assert_any_call("percentage-command-topic", "100", 0, False) mqtt_mock.async_publish.reset_mock() state = hass.states.get("fan.test") assert state.state == STATE_ON @@ -1374,11 +1306,8 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock, ca assert state.attributes.get(ATTR_ASSUMED_STATE) await common.async_turn_on(hass, "fan.test", percentage=25) - assert mqtt_mock.async_publish.call_count == 4 + assert mqtt_mock.async_publish.call_count == 3 mqtt_mock.async_publish.assert_any_call("command-topic", "ON", 0, False) - mqtt_mock.async_publish.assert_any_call( - "preset-mode-command-topic", "low", 0, False - ) mqtt_mock.async_publish.assert_any_call("percentage-command-topic", "25", 0, False) # use of speeds is deprecated, support will be removed after a quarter (2021.7) mqtt_mock.async_publish.assert_any_call("speed-command-topic", "low", 0, False) @@ -1394,24 +1323,15 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock, ca assert state.state == STATE_OFF assert state.attributes.get(ATTR_ASSUMED_STATE) - await common.async_turn_on(hass, "fan.test", preset_mode="medium") - assert mqtt_mock.async_publish.call_count == 3 - mqtt_mock.async_publish.assert_any_call("command-topic", "ON", 0, False) - mqtt_mock.async_publish.assert_any_call( - "preset-mode-command-topic", "medium", 0, False - ) # use of speeds is deprecated, support will be removed after a quarter (2021.7) - mqtt_mock.async_publish.assert_any_call("speed-command-topic", "medium", 0, False) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get("fan.test") - assert state.state == STATE_ON - assert state.attributes.get(ATTR_ASSUMED_STATE) + with pytest.raises(NotValidPresetModeError): + await common.async_turn_on(hass, "fan.test", preset_mode="auto") - await common.async_turn_on(hass, "fan.test", preset_mode="high") + await common.async_turn_on(hass, "fan.test", preset_mode="whoosh") assert mqtt_mock.async_publish.call_count == 2 mqtt_mock.async_publish.assert_any_call("command-topic", "ON", 0, False) mqtt_mock.async_publish.assert_any_call( - "preset-mode-command-topic", "high", 0, False + "preset-mode-command-topic", "whoosh", 0, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("fan.test") @@ -1471,14 +1391,11 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock, ca assert state.attributes.get(ATTR_ASSUMED_STATE) await common.async_turn_on(hass, "fan.test", percentage=50) - assert mqtt_mock.async_publish.call_count == 4 + assert mqtt_mock.async_publish.call_count == 3 mqtt_mock.async_publish.assert_any_call("command-topic", "ON", 0, False) mqtt_mock.async_publish.assert_any_call("percentage-command-topic", "50", 0, False) - mqtt_mock.async_publish.assert_any_call( - "preset-mode-command-topic", "medium", 0, False - ) # use of speeds is deprecated, support will be removed after a quarter (2021.7) - mqtt_mock.async_publish.assert_any_call("speed-command-topic", "medium", 0, False) + mqtt_mock.async_publish.assert_any_call("speed-command-topic", "low", 0, False) mqtt_mock.async_publish.reset_mock() state = hass.states.get("fan.test") assert state.state == STATE_ON @@ -1501,26 +1418,20 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock, ca assert state.attributes.get(ATTR_ASSUMED_STATE) await common.async_set_percentage(hass, "fan.test", 33) - assert mqtt_mock.async_publish.call_count == 3 + assert mqtt_mock.async_publish.call_count == 2 mqtt_mock.async_publish.assert_any_call("percentage-command-topic", "33", 0, False) # use of speeds is deprecated, support will be removed after a quarter (2021.7) - mqtt_mock.async_publish.assert_any_call("speed-command-topic", "medium", 0, False) - mqtt_mock.async_publish.assert_any_call( - "preset-mode-command-topic", "medium", 0, False - ) + mqtt_mock.async_publish.assert_any_call("speed-command-topic", "low", 0, False) mqtt_mock.async_publish.reset_mock() state = hass.states.get("fan.test") assert state.state == STATE_OFF assert state.attributes.get(ATTR_ASSUMED_STATE) await common.async_set_percentage(hass, "fan.test", 50) - assert mqtt_mock.async_publish.call_count == 3 + assert mqtt_mock.async_publish.call_count == 2 mqtt_mock.async_publish.assert_any_call("percentage-command-topic", "50", 0, False) # use of speeds is deprecated, support will be removed after a quarter (2021.7) - mqtt_mock.async_publish.assert_any_call("speed-command-topic", "medium", 0, False) - mqtt_mock.async_publish.assert_any_call( - "preset-mode-command-topic", "medium", 0, False - ) + mqtt_mock.async_publish.assert_any_call("speed-command-topic", "low", 0, False) mqtt_mock.async_publish.reset_mock() state = hass.states.get("fan.test") assert state.state == STATE_OFF @@ -1529,22 +1440,18 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock, ca await common.async_set_percentage(hass, "fan.test", 100) assert mqtt_mock.async_publish.call_count == 2 mqtt_mock.async_publish.assert_any_call("percentage-command-topic", "100", 0, False) - mqtt_mock.async_publish.assert_any_call( - "preset-mode-command-topic", "freaking-high", 0, False - ) + # use of speeds is deprecated, support will be removed after a quarter (2021.7) + mqtt_mock.async_publish.assert_any_call("speed-command-topic", "medium", 0, False) mqtt_mock.async_publish.reset_mock() state = hass.states.get("fan.test") assert state.state == STATE_OFF assert state.attributes.get(ATTR_ASSUMED_STATE) await common.async_set_percentage(hass, "fan.test", 0) - assert mqtt_mock.async_publish.call_count == 3 + assert mqtt_mock.async_publish.call_count == 2 mqtt_mock.async_publish.assert_any_call("percentage-command-topic", "0", 0, False) # use of speeds is deprecated, support will be removed after a quarter (2021.7) mqtt_mock.async_publish.assert_any_call("speed-command-topic", "off", 0, False) - mqtt_mock.async_publish.assert_any_call( - "preset-mode-command-topic", "off", 0, False - ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("fan.test") assert state.state == STATE_OFF @@ -1554,32 +1461,16 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock, ca await common.async_set_percentage(hass, "fan.test", 101) await common.async_set_preset_mode(hass, "fan.test", "low") - assert mqtt_mock.async_publish.call_count == 2 - # use of speeds is deprecated, support will be removed after a quarter (2021.7) - mqtt_mock.async_publish.assert_any_call("speed-command-topic", "low", 0, False) - mqtt_mock.async_publish.assert_any_call( - "preset-mode-command-topic", "low", 0, False - ) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get("fan.test") - assert state.state == STATE_OFF - assert state.attributes.get(ATTR_ASSUMED_STATE) + assert "not a valid preset mode" in caplog.text + caplog.clear() await common.async_set_preset_mode(hass, "fan.test", "medium") - assert mqtt_mock.async_publish.call_count == 2 - # use of speeds is deprecated, support will be removed after a quarter (2021.7) - mqtt_mock.async_publish.assert_any_call("speed-command-topic", "medium", 0, False) - mqtt_mock.async_publish.assert_any_call( - "preset-mode-command-topic", "medium", 0, False - ) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get("fan.test") - assert state.state == STATE_OFF - assert state.attributes.get(ATTR_ASSUMED_STATE) + assert "not a valid preset mode" in caplog.text + caplog.clear() - await common.async_set_preset_mode(hass, "fan.test", "high") + await common.async_set_preset_mode(hass, "fan.test", "whoosh") mqtt_mock.async_publish.assert_called_once_with( - "preset-mode-command-topic", "high", 0, False + "preset-mode-command-topic", "whoosh", 0, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("fan.test") @@ -1595,7 +1486,7 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock, ca assert state.state == STATE_OFF assert state.attributes.get(ATTR_ASSUMED_STATE) - await common.async_set_preset_mode(hass, "fan.test", "ModeX") + await common.async_set_preset_mode(hass, "fan.test", "freaking-high") assert "not a valid preset mode" in caplog.text caplog.clear() @@ -1615,13 +1506,8 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock, ca assert state.attributes.get(ATTR_ASSUMED_STATE) await common.async_set_speed(hass, "fan.test", fan.SPEED_HIGH) - mqtt_mock.async_publish.assert_called_once_with( - "speed-command-topic", "high", 0, False - ) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get("fan.test") - assert state.state == STATE_OFF - assert state.attributes.get(ATTR_ASSUMED_STATE) + assert "not a valid speed" in caplog.text + caplog.clear() await common.async_set_speed(hass, "fan.test", fan.SPEED_OFF) mqtt_mock.async_publish.assert_called_once_with( @@ -1653,7 +1539,7 @@ async def test_attributes(hass, mqtt_mock, caplog): "preset_mode_command_topic": "preset-mode-command-topic", "percentage_command_topic": "percentage-command-topic", "preset_modes": [ - "freaking-high", + "breeze", "silent", ], } @@ -1667,7 +1553,6 @@ async def test_attributes(hass, mqtt_mock, caplog): "low", "medium", "high", - "freaking-high", ] await common.async_turn_on(hass, "fan.test") @@ -1821,14 +1706,14 @@ async def test_supported_features(hass, mqtt_mock): "name": "test3c2", "command_topic": "command-topic", "preset_mode_command_topic": "preset-mode-command-topic", - "preset_modes": ["very-fast", "auto"], + "preset_modes": ["eco", "auto"], }, { "platform": "mqtt", "name": "test3c3", "command_topic": "command-topic", "preset_mode_command_topic": "preset-mode-command-topic", - "preset_modes": ["off", "on", "auto"], + "preset_modes": ["eco", "smart", "auto"], }, { "platform": "mqtt", @@ -1863,7 +1748,7 @@ async def test_supported_features(hass, mqtt_mock): "name": "test5pr_mb", "command_topic": "command-topic", "preset_mode_command_topic": "preset-mode-command-topic", - "preset_modes": ["off", "on", "auto"], + "preset_modes": ["whoosh", "silent", "auto"], }, { "platform": "mqtt", @@ -1927,10 +1812,7 @@ async def test_supported_features(hass, mqtt_mock): assert state is None state = hass.states.get("fan.test3c2") - assert ( - state.attributes.get(ATTR_SUPPORTED_FEATURES) - == fan.SUPPORT_PRESET_MODE | fan.SUPPORT_SET_SPEED - ) + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == fan.SUPPORT_PRESET_MODE state = hass.states.get("fan.test3c3") assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == fan.SUPPORT_PRESET_MODE @@ -1949,21 +1831,19 @@ async def test_supported_features(hass, mqtt_mock): ) state = hass.states.get("fan.test5pr_ma") - assert ( - state.attributes.get(ATTR_SUPPORTED_FEATURES) - == fan.SUPPORT_SET_SPEED | fan.SUPPORT_PRESET_MODE - ) + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == fan.SUPPORT_PRESET_MODE state = hass.states.get("fan.test5pr_mb") assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == fan.SUPPORT_PRESET_MODE state = hass.states.get("fan.test5pr_mc") assert ( state.attributes.get(ATTR_SUPPORTED_FEATURES) - == fan.SUPPORT_OSCILLATE | fan.SUPPORT_SET_SPEED | fan.SUPPORT_PRESET_MODE + == fan.SUPPORT_OSCILLATE | fan.SUPPORT_PRESET_MODE ) state = hass.states.get("fan.test6spd_range_a") assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == fan.SUPPORT_SET_SPEED + assert state.attributes.get("percentage_step") == 2.5 state = hass.states.get("fan.test6spd_range_b") assert state is None state = hass.states.get("fan.test6spd_range_c") From 9b0b2d91685a3a102d2a093376f00b026164d2d6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 9 Apr 2021 13:56:15 -1000 Subject: [PATCH 0160/1317] Prevent ping id allocation conflict with device_tracker (#48969) * Prevent ping id allocation conflict with device_tracker - Solves id conflict resulting unexpected home state * Update homeassistant/components/ping/device_tracker.py Co-authored-by: Paulus Schoutsen --- homeassistant/components/ping/__init__.py | 22 ++++++++------- .../components/ping/device_tracker.py | 2 +- tests/components/ping/test_init.py | 27 +++++++++++++++++++ 3 files changed, 40 insertions(+), 11 deletions(-) create mode 100644 tests/components/ping/test_init.py diff --git a/homeassistant/components/ping/__init__.py b/homeassistant/components/ping/__init__.py index 726bb212574db..b9a9f6460db1f 100644 --- a/homeassistant/components/ping/__init__.py +++ b/homeassistant/components/ping/__init__.py @@ -24,20 +24,22 @@ async def async_setup(hass, config): @callback -def async_get_next_ping_id(hass): +def async_get_next_ping_id(hass, count=1): """Find the next id to use in the outbound ping. + When using multiping, we increment the id + by the number of ids that multiping + will use. + Must be called in async """ - current_id = hass.data[DOMAIN][PING_ID] - if current_id == MAX_PING_ID: - next_id = DEFAULT_START_ID - else: - next_id = current_id + 1 - - hass.data[DOMAIN][PING_ID] = next_id - - return next_id + allocated_id = hass.data[DOMAIN][PING_ID] + 1 + if allocated_id > MAX_PING_ID: + allocated_id -= MAX_PING_ID - DEFAULT_START_ID + hass.data[DOMAIN][PING_ID] += count + if hass.data[DOMAIN][PING_ID] > MAX_PING_ID: + hass.data[DOMAIN][PING_ID] -= MAX_PING_ID - DEFAULT_START_ID + return allocated_id def _can_use_icmp_lib_with_privilege() -> None | bool: diff --git a/homeassistant/components/ping/device_tracker.py b/homeassistant/components/ping/device_tracker.py index a6b75a9245b76..256023263bab6 100644 --- a/homeassistant/components/ping/device_tracker.py +++ b/homeassistant/components/ping/device_tracker.py @@ -125,7 +125,7 @@ async def async_update(now): count=PING_ATTEMPTS_COUNT, timeout=ICMP_TIMEOUT, privileged=privileged, - id=async_get_next_ping_id(hass), + id=async_get_next_ping_id(hass, len(ip_to_dev_id)), ) ) _LOGGER.debug("Multiping responses: %s", responses) diff --git a/tests/components/ping/test_init.py b/tests/components/ping/test_init.py new file mode 100644 index 0000000000000..3dfe193c4d57c --- /dev/null +++ b/tests/components/ping/test_init.py @@ -0,0 +1,27 @@ +"""Test ping id allocation.""" + +from homeassistant.components.ping import async_get_next_ping_id +from homeassistant.components.ping.const import ( + DEFAULT_START_ID, + DOMAIN, + MAX_PING_ID, + PING_ID, +) + + +async def test_async_get_next_ping_id(hass): + """Verify we allocate ping ids as expected.""" + hass.data[DOMAIN] = {PING_ID: DEFAULT_START_ID} + + assert async_get_next_ping_id(hass) == DEFAULT_START_ID + 1 + assert async_get_next_ping_id(hass) == DEFAULT_START_ID + 2 + assert async_get_next_ping_id(hass, 2) == DEFAULT_START_ID + 3 + assert async_get_next_ping_id(hass) == DEFAULT_START_ID + 5 + + hass.data[DOMAIN][PING_ID] = MAX_PING_ID + assert async_get_next_ping_id(hass) == DEFAULT_START_ID + 1 + assert async_get_next_ping_id(hass) == DEFAULT_START_ID + 2 + + hass.data[DOMAIN][PING_ID] = MAX_PING_ID + assert async_get_next_ping_id(hass, 2) == DEFAULT_START_ID + 1 + assert async_get_next_ping_id(hass) == DEFAULT_START_ID + 3 From 98396e13af0ad9fc9609d2b65f14f73cd845051b Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sat, 10 Apr 2021 02:58:44 +0300 Subject: [PATCH 0161/1317] Fix Shelly button device triggers (#48974) --- .../components/shelly/device_trigger.py | 16 ++++- .../components/shelly/test_device_trigger.py | 70 +++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/device_trigger.py b/homeassistant/components/shelly/device_trigger.py index b7cf11209490d..9793804054370 100644 --- a/homeassistant/components/shelly/device_trigger.py +++ b/homeassistant/components/shelly/device_trigger.py @@ -27,6 +27,7 @@ DOMAIN, EVENT_SHELLY_CLICK, INPUTS_EVENTS_SUBTYPES, + SHBTN_1_INPUTS_EVENTS_TYPES, SUPPORTED_INPUTS_EVENTS_TYPES, ) from .utils import get_device_wrapper, get_input_triggers @@ -45,7 +46,7 @@ async def async_validate_trigger_config(hass, config): # if device is available verify parameters against device capabilities wrapper = get_device_wrapper(hass, config[CONF_DEVICE_ID]) - if not wrapper: + if not wrapper or not wrapper.device.initialized: return config trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) @@ -68,6 +69,19 @@ async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]: if not wrapper: raise InvalidDeviceAutomationConfig(f"Device not found: {device_id}") + if wrapper.model in ("SHBTN-1", "SHBTN-2"): + for trigger in SHBTN_1_INPUTS_EVENTS_TYPES: + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_TYPE: trigger, + CONF_SUBTYPE: "button", + } + ) + return triggers + for block in wrapper.device.blocks: input_triggers = get_input_triggers(wrapper.device, block) diff --git a/tests/components/shelly/test_device_trigger.py b/tests/components/shelly/test_device_trigger.py index a725f5a1f3076..bedf4abc0f23e 100644 --- a/tests/components/shelly/test_device_trigger.py +++ b/tests/components/shelly/test_device_trigger.py @@ -1,4 +1,6 @@ """The tests for Shelly device triggers.""" +from unittest.mock import AsyncMock, Mock + import pytest from homeassistant import setup @@ -6,10 +8,13 @@ from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) +from homeassistant.components.shelly import ShellyDeviceWrapper from homeassistant.components.shelly.const import ( ATTR_CHANNEL, ATTR_CLICK_TYPE, + COAP, CONF_SUBTYPE, + DATA_CONFIG_ENTRY, DOMAIN, EVENT_SHELLY_CLICK, ) @@ -52,6 +57,71 @@ async def test_get_triggers(hass, coap_wrapper): assert_lists_same(triggers, expected_triggers) +async def test_get_triggers_button(hass): + """Test we get the expected triggers from a shelly button.""" + await async_setup_component(hass, "shelly", {}) + + config_entry = MockConfigEntry( + domain=DOMAIN, + data={"sleep_period": 43200, "model": "SHBTN-1"}, + unique_id="12345678", + ) + config_entry.add_to_hass(hass) + + device = Mock( + blocks=None, + settings=None, + shelly=None, + update=AsyncMock(), + initialized=False, + ) + + hass.data[DOMAIN] = {DATA_CONFIG_ENTRY: {}} + hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id] = {} + coap_wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][ + COAP + ] = ShellyDeviceWrapper(hass, config_entry, device) + + await coap_wrapper.async_setup() + + expected_triggers = [ + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: coap_wrapper.device_id, + CONF_DOMAIN: DOMAIN, + CONF_TYPE: "single", + CONF_SUBTYPE: "button", + }, + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: coap_wrapper.device_id, + CONF_DOMAIN: DOMAIN, + CONF_TYPE: "double", + CONF_SUBTYPE: "button", + }, + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: coap_wrapper.device_id, + CONF_DOMAIN: DOMAIN, + CONF_TYPE: "triple", + CONF_SUBTYPE: "button", + }, + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: coap_wrapper.device_id, + CONF_DOMAIN: DOMAIN, + CONF_TYPE: "long", + CONF_SUBTYPE: "button", + }, + ] + + triggers = await async_get_device_automations( + hass, "trigger", coap_wrapper.device_id + ) + + assert_lists_same(triggers, expected_triggers) + + async def test_get_triggers_for_invalid_device_id(hass, device_reg, coap_wrapper): """Test error raised for invalid shelly device_id.""" assert coap_wrapper From a36712509b96783a084c6183ad5b4c240f96b683 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sat, 10 Apr 2021 00:03:44 +0000 Subject: [PATCH 0162/1317] [ci skip] Translation update --- .../components/asuswrt/translations/ca.json | 2 +- .../components/braviatv/translations/ca.json | 2 +- .../components/broadlink/translations/ca.json | 4 +- .../components/climacell/translations/et.json | 2 +- .../components/dunehd/translations/ca.json | 2 +- .../components/ezviz/translations/ca.json | 52 +++++++++++++++++++ .../components/ezviz/translations/en.json | 40 +++++++------- .../components/ezviz/translations/et.json | 52 +++++++++++++++++++ .../components/ezviz/translations/it.json | 52 +++++++++++++++++++ .../components/ezviz/translations/ru.json | 7 +++ .../components/goalzero/translations/ca.json | 2 +- .../xiaomi_aqara/translations/ca.json | 2 +- 12 files changed, 191 insertions(+), 28 deletions(-) create mode 100644 homeassistant/components/ezviz/translations/ca.json create mode 100644 homeassistant/components/ezviz/translations/et.json create mode 100644 homeassistant/components/ezviz/translations/it.json create mode 100644 homeassistant/components/ezviz/translations/ru.json diff --git a/homeassistant/components/asuswrt/translations/ca.json b/homeassistant/components/asuswrt/translations/ca.json index 2b15199a092b7..446b08ecdfefd 100644 --- a/homeassistant/components/asuswrt/translations/ca.json +++ b/homeassistant/components/asuswrt/translations/ca.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", - "invalid_host": "Nom de l'amfitri\u00f3 o l'adre\u00e7a IP inv\u00e0lids", + "invalid_host": "Nom de l'amfitri\u00f3 o adre\u00e7a IP inv\u00e0lids", "pwd_and_ssh": "Proporciona, nom\u00e9s, la contrasenya o el fitxer de claus SSH", "pwd_or_ssh": "Proporciona la contrasenya o el fitxer de claus SSH", "ssh_not_file": "No s'ha trobat el fitxer de claus SSH", diff --git a/homeassistant/components/braviatv/translations/ca.json b/homeassistant/components/braviatv/translations/ca.json index 5aba974f5d277..94fe36dcddc91 100644 --- a/homeassistant/components/braviatv/translations/ca.json +++ b/homeassistant/components/braviatv/translations/ca.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", - "invalid_host": "Nom de l'amfitri\u00f3 o l'adre\u00e7a IP inv\u00e0lids", + "invalid_host": "Nom de l'amfitri\u00f3 o adre\u00e7a IP inv\u00e0lids", "unsupported_model": "Aquest model de televisor no \u00e9s compatible." }, "step": { diff --git a/homeassistant/components/broadlink/translations/ca.json b/homeassistant/components/broadlink/translations/ca.json index 9ea559dcf93af..d36520e4e44ad 100644 --- a/homeassistant/components/broadlink/translations/ca.json +++ b/homeassistant/components/broadlink/translations/ca.json @@ -4,13 +4,13 @@ "already_configured": "El dispositiu ja est\u00e0 configurat", "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", "cannot_connect": "Ha fallat la connexi\u00f3", - "invalid_host": "Nom de l'amfitri\u00f3 o l'adre\u00e7a IP inv\u00e0lids", + "invalid_host": "Nom de l'amfitri\u00f3 o adre\u00e7a IP inv\u00e0lids", "not_supported": "Dispositiu no compatible", "unknown": "Error inesperat" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", - "invalid_host": "Nom de l'amfitri\u00f3 o l'adre\u00e7a IP inv\u00e0lids", + "invalid_host": "Nom de l'amfitri\u00f3 o adre\u00e7a IP inv\u00e0lids", "unknown": "Error inesperat" }, "flow_title": "{name} ({model} a {host})", diff --git a/homeassistant/components/climacell/translations/et.json b/homeassistant/components/climacell/translations/et.json index de44f9d70d1e4..4e9cec722ef07 100644 --- a/homeassistant/components/climacell/translations/et.json +++ b/homeassistant/components/climacell/translations/et.json @@ -4,7 +4,7 @@ "cannot_connect": "\u00dchendamine nurjus", "invalid_api_key": "Vale API v\u00f5ti", "rate_limited": "Hetkel on p\u00e4ringud piiratud, proovi hiljem uuesti.", - "unknown": "Tundmatu t\u00f5rge" + "unknown": "Ootamatu t\u00f5rge" }, "step": { "user": { diff --git a/homeassistant/components/dunehd/translations/ca.json b/homeassistant/components/dunehd/translations/ca.json index b0da4a8080ad5..12f139afe60e8 100644 --- a/homeassistant/components/dunehd/translations/ca.json +++ b/homeassistant/components/dunehd/translations/ca.json @@ -6,7 +6,7 @@ "error": { "already_configured": "El dispositiu ja est\u00e0 configurat", "cannot_connect": "Ha fallat la connexi\u00f3", - "invalid_host": "Nom de l'amfitri\u00f3 o l'adre\u00e7a IP inv\u00e0lids" + "invalid_host": "Nom de l'amfitri\u00f3 o adre\u00e7a IP inv\u00e0lids" }, "step": { "user": { diff --git a/homeassistant/components/ezviz/translations/ca.json b/homeassistant/components/ezviz/translations/ca.json new file mode 100644 index 0000000000000..c7c71e0712213 --- /dev/null +++ b/homeassistant/components/ezviz/translations/ca.json @@ -0,0 +1,52 @@ +{ + "config": { + "abort": { + "already_configured_account": "El compte ja ha estat configurat", + "ezviz_cloud_account_missing": "Falta el compte d'Ezviz cloud. Torna'l a configurar", + "unknown": "Error inesperat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "invalid_host": "Nom de l'amfitri\u00f3 o adre\u00e7a IP inv\u00e0lids" + }, + "flow_title": "{serial}", + "step": { + "confirm": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + }, + "description": "Introdueix les credencials RTSP per a la c\u00e0mera Ezviz {serial} amb IP {ip_address}", + "title": "S'ha descobert c\u00e0mera Ezviz" + }, + "user": { + "data": { + "password": "Contrasenya", + "url": "URL", + "username": "Nom d'usuari" + }, + "title": "Connexi\u00f3 amb Ezviz Cloud" + }, + "user_custom_url": { + "data": { + "password": "Contrasenya", + "url": "URL", + "username": "Nom d'usuari" + }, + "description": "Especifica manualment l'URL de teva regi\u00f3", + "title": "Connexi\u00f3 amb URL de Ezviz personalitzat" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ffmpeg_arguments": "Par\u00e0metres passats a ffmpeg per a les c\u00e0meres", + "timeout": "Temps d'espera de la sol\u00b7licitud (segons)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ezviz/translations/en.json b/homeassistant/components/ezviz/translations/en.json index e5103f07973eb..9b5e273b0ad0d 100644 --- a/homeassistant/components/ezviz/translations/en.json +++ b/homeassistant/components/ezviz/translations/en.json @@ -1,41 +1,41 @@ { "config": { "abort": { - "already_configured_account": "Account is already configured.", - "unknown": "Unexpected error", - "ezviz_cloud_account_missing": "Ezviz cloud account missing. Please reconfigure Ezviz cloud account" + "already_configured_account": "Account is already configured", + "ezviz_cloud_account_missing": "Ezviz cloud account missing. Please reconfigure Ezviz cloud account", + "unknown": "Unexpected error" }, "error": { "cannot_connect": "Failed to connect", "invalid_auth": "Invalid authentication", - "invalid_host": "Invalid IP or URL" + "invalid_host": "Invalid hostname or IP address" }, "flow_title": "{serial}", "step": { - "user": { + "confirm": { "data": { - "username": "Username", "password": "Password", - "url": "URL" + "username": "Username" }, - "title": "Connect to Ezviz Cloud" + "description": "Enter RTSP credentials for Ezviz camera {serial} with IP {ip_address}", + "title": "Discovered Ezviz Camera" }, - "user_custom_url": { + "user": { "data": { - "username": "Username", "password": "Password", - "url": "URL" + "url": "URL", + "username": "Username" }, - "title": "Connect to custom Ezviz URL", - "description": "Manually specify your region URL" + "title": "Connect to Ezviz Cloud" }, - "confirm": { + "user_custom_url": { "data": { - "username": "Username", - "password": "Password" + "password": "Password", + "url": "URL", + "username": "Username" }, - "title": "Discovered Ezviz Camera", - "description": "Enter RTSP credentials for Ezviz camera {serial} with IP as {ip_address}" + "description": "Manually specify your region URL", + "title": "Connect to custom Ezviz URL" } } }, @@ -43,8 +43,8 @@ "step": { "init": { "data": { - "timeout": "Request Timeout (seconds)", - "ffmpeg_arguments": "Arguments passed to ffmpeg for cameras" + "ffmpeg_arguments": "Arguments passed to ffmpeg for cameras", + "timeout": "Request Timeout (seconds)" } } } diff --git a/homeassistant/components/ezviz/translations/et.json b/homeassistant/components/ezviz/translations/et.json new file mode 100644 index 0000000000000..55a6e6784c112 --- /dev/null +++ b/homeassistant/components/ezviz/translations/et.json @@ -0,0 +1,52 @@ +{ + "config": { + "abort": { + "already_configured_account": "Kasutaja on juba seadistatud", + "ezviz_cloud_account_missing": "Ezvizi pilvekonto puudub. Seadista Ezvizi pilvekonto uuesti", + "unknown": "Ootamatu t\u00f5rge" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Vigane autentimine", + "invalid_host": "Sobimatu hostinimi v\u00f5i IP-aadress" + }, + "flow_title": "{serial}", + "step": { + "confirm": { + "data": { + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + }, + "description": "Sisesta Ezviz kaamera {serial} IP-ga {ip_address} RTSP mandaat", + "title": "Avastati Ezvizi kaamera" + }, + "user": { + "data": { + "password": "Salas\u00f5na", + "url": "URL", + "username": "Kasutajanimi" + }, + "title": "Loo \u00fchendus Ezvizi pilvega" + }, + "user_custom_url": { + "data": { + "password": "Salas\u00f5na", + "url": "URL", + "username": "Kasutajanimi" + }, + "description": "M\u00e4\u00e4ra oma piirkonna URL k\u00e4sitsi", + "title": "\u00dchenduse loomine kohandatud Ezvizi URL-iga" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ffmpeg_arguments": "Kaamerate jaoks edastavad argumendid (ffmpeg)", + "timeout": "P\u00e4ringu ajal\u00f5pp (sekundites)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ezviz/translations/it.json b/homeassistant/components/ezviz/translations/it.json new file mode 100644 index 0000000000000..84e7811a4a558 --- /dev/null +++ b/homeassistant/components/ezviz/translations/it.json @@ -0,0 +1,52 @@ +{ + "config": { + "abort": { + "already_configured_account": "L'account \u00e8 gi\u00e0 configurato", + "ezviz_cloud_account_missing": "Ezviz cloud account mancante. Si prega di riconfigurare l'account Ezviz cloud", + "unknown": "Errore imprevisto" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "invalid_host": "Nome host o indirizzo IP non valido" + }, + "flow_title": "{serial}", + "step": { + "confirm": { + "data": { + "password": "Password", + "username": "Nome utente" + }, + "description": "Inserisci le credenziali RTSP per la videocamera Ezviz {serial} con IP {ip_address}", + "title": "Rilevata videocamera Ezviz" + }, + "user": { + "data": { + "password": "Password", + "url": "URL", + "username": "Nome utente" + }, + "title": "Connettiti a Ezviz Cloud" + }, + "user_custom_url": { + "data": { + "password": "Password", + "url": "URL", + "username": "Nome utente" + }, + "description": "Specificare manualmente l'URL dell'area geografica", + "title": "Connettiti all'URL personalizzato di Ezviz" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ffmpeg_arguments": "Argomenti passati a ffmpeg per le fotocamere", + "timeout": "Richiesta Timeout (secondi)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ezviz/translations/ru.json b/homeassistant/components/ezviz/translations/ru.json new file mode 100644 index 0000000000000..f047b071be4c6 --- /dev/null +++ b/homeassistant/components/ezviz/translations/ru.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured_account": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/goalzero/translations/ca.json b/homeassistant/components/goalzero/translations/ca.json index ac4c2a696e23e..22e229d1c7ee2 100644 --- a/homeassistant/components/goalzero/translations/ca.json +++ b/homeassistant/components/goalzero/translations/ca.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", - "invalid_host": "Nom de l'amfitri\u00f3 o l'adre\u00e7a IP inv\u00e0lids", + "invalid_host": "Nom de l'amfitri\u00f3 o adre\u00e7a IP inv\u00e0lids", "unknown": "Error inesperat" }, "step": { diff --git a/homeassistant/components/xiaomi_aqara/translations/ca.json b/homeassistant/components/xiaomi_aqara/translations/ca.json index 23502d50d9ceb..6c43f026e2a94 100644 --- a/homeassistant/components/xiaomi_aqara/translations/ca.json +++ b/homeassistant/components/xiaomi_aqara/translations/ca.json @@ -7,7 +7,7 @@ }, "error": { "discovery_error": "No s'ha pogut descobrir cap passarel\u00b7la Xiaomi Aqara, prova d'utilitzar la IP del dispositiu que executa Home Assistant com a interf\u00edcie", - "invalid_host": "Nom de l'amfitri\u00f3 o l'adre\u00e7a IP inv\u00e0lids, consulta https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem", + "invalid_host": "Nom de l'amfitri\u00f3 o adre\u00e7a IP inv\u00e0lids, consulta https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem", "invalid_interface": "Interf\u00edcie de xarxa no v\u00e0lida", "invalid_key": "Clau de la passarel\u00b7la no v\u00e0lida", "invalid_mac": "Adre\u00e7a MAC no v\u00e0lida" From 441c304f115674d386130cb032e9d7679f04bb74 Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Sat, 10 Apr 2021 02:07:04 +0200 Subject: [PATCH 0163/1317] Bump devolo Home Control to support old websocket-client versions again (#48960) --- homeassistant/components/devolo_home_control/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/devolo_home_control/manifest.json b/homeassistant/components/devolo_home_control/manifest.json index 93cf4be5d350d..e53e715ffb168 100644 --- a/homeassistant/components/devolo_home_control/manifest.json +++ b/homeassistant/components/devolo_home_control/manifest.json @@ -2,7 +2,7 @@ "domain": "devolo_home_control", "name": "devolo Home Control", "documentation": "https://www.home-assistant.io/integrations/devolo_home_control", - "requirements": ["devolo-home-control-api==0.17.1"], + "requirements": ["devolo-home-control-api==0.17.3"], "after_dependencies": ["zeroconf"], "config_flow": true, "codeowners": ["@2Fake", "@Shutgun"], diff --git a/requirements_all.txt b/requirements_all.txt index 95122b824bf4f..2f08df723d4b3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -482,7 +482,7 @@ deluge-client==1.7.1 denonavr==0.10.5 # homeassistant.components.devolo_home_control -devolo-home-control-api==0.17.1 +devolo-home-control-api==0.17.3 # homeassistant.components.directv directv==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b16fee985db21..9ca7ad00176e0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -267,7 +267,7 @@ defusedxml==0.6.0 denonavr==0.10.5 # homeassistant.components.devolo_home_control -devolo-home-control-api==0.17.1 +devolo-home-control-api==0.17.3 # homeassistant.components.directv directv==0.4.0 From 4149cc9662380ed41e82519642d55743ef5899fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 10 Apr 2021 03:08:13 +0300 Subject: [PATCH 0164/1317] Huawei LTE cleanups (#48959) --- .../components/huawei_lte/__init__.py | 35 +--------------- .../components/huawei_lte/config_flow.py | 40 +++++++------------ homeassistant/components/huawei_lte/const.py | 1 - .../components/huawei_lte/device_tracker.py | 1 + homeassistant/components/huawei_lte/sensor.py | 11 ++--- .../components/huawei_lte/strings.json | 3 +- 6 files changed, 22 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index 67170aaf86647..25df0f620fa50 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -48,11 +48,7 @@ device_registry as dr, discovery, ) -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, - dispatcher_send, -) +from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType, HomeAssistantType @@ -82,7 +78,6 @@ SERVICE_REBOOT, SERVICE_RESUME_INTEGRATION, SERVICE_SUSPEND_INTEGRATION, - UPDATE_OPTIONS_SIGNAL, UPDATE_SIGNAL, ) @@ -436,11 +431,6 @@ def signal_update() -> None: hass.data[DOMAIN].hass_config, ) - # Add config entry options update listener - router.unload_handlers.append( - config_entry.add_update_listener(async_signal_options_update) - ) - def _update_router(*_: Any) -> None: """ Update router data. @@ -492,9 +482,8 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: def service_handler(service: ServiceCall) -> None: """Apply a service.""" - url = service.data.get(CONF_URL) routers = hass.data[DOMAIN].routers - if url: + if url := service.data.get(CONF_URL): router = routers.get(url) elif not routers: _LOGGER.error("%s: no routers configured", service.service) @@ -559,13 +548,6 @@ def service_handler(service: ServiceCall) -> None: return True -async def async_signal_options_update( - hass: HomeAssistantType, config_entry: ConfigEntry -) -> None: - """Handle config entry options update.""" - async_dispatcher_send(hass, UPDATE_OPTIONS_SIGNAL, config_entry) - - async def async_migrate_entry( hass: HomeAssistantType, config_entry: ConfigEntry ) -> bool: @@ -631,30 +613,17 @@ async def async_update(self) -> None: """Update state.""" raise NotImplementedError - async def async_update_options(self, config_entry: ConfigEntry) -> None: - """Update config entry options.""" - async def async_added_to_hass(self) -> None: """Connect to update signals.""" self._unsub_handlers.append( async_dispatcher_connect(self.hass, UPDATE_SIGNAL, self._async_maybe_update) ) - self._unsub_handlers.append( - async_dispatcher_connect( - self.hass, UPDATE_OPTIONS_SIGNAL, self._async_maybe_update_options - ) - ) async def _async_maybe_update(self, url: str) -> None: """Update state if the update signal comes from our router.""" if url == self.router.url: self.async_schedule_update_ha_state(True) - async def _async_maybe_update_options(self, config_entry: ConfigEntry) -> None: - """Update options if the update signal comes from our router.""" - if config_entry.data[CONF_URL] == self.router.url: - await self.async_update_options(config_entry) - async def async_will_remove_from_hass(self) -> None: """Invoke unsubscription handlers.""" for unsub in self._unsub_handlers: diff --git a/homeassistant/components/huawei_lte/config_flow.py b/homeassistant/components/huawei_lte/config_flow.py index 415e2ea2bc3bd..fc455f865fd30 100644 --- a/homeassistant/components/huawei_lte/config_flow.py +++ b/homeassistant/components/huawei_lte/config_flow.py @@ -1,7 +1,6 @@ """Config flow for the Huawei LTE platform.""" from __future__ import annotations -from collections import OrderedDict import logging from typing import Any from urllib.parse import urlparse @@ -65,32 +64,21 @@ async def _async_show_user_form( return self.async_show_form( step_id="user", data_schema=vol.Schema( - OrderedDict( - ( - ( - vol.Required( - CONF_URL, - default=user_input.get( - CONF_URL, - self.context.get(CONF_URL, ""), - ), - ), - str, + { + vol.Required( + CONF_URL, + default=user_input.get( + CONF_URL, + self.context.get(CONF_URL, ""), ), - ( - vol.Optional( - CONF_USERNAME, default=user_input.get(CONF_USERNAME, "") - ), - str, - ), - ( - vol.Optional( - CONF_PASSWORD, default=user_input.get(CONF_PASSWORD, "") - ), - str, - ), - ) - ) + ): str, + vol.Optional( + CONF_USERNAME, default=user_input.get(CONF_USERNAME, "") + ): str, + vol.Optional( + CONF_PASSWORD, default=user_input.get(CONF_PASSWORD, "") + ): str, + } ), errors=errors or {}, ) diff --git a/homeassistant/components/huawei_lte/const.py b/homeassistant/components/huawei_lte/const.py index 039bab10fb95e..519da09caee07 100644 --- a/homeassistant/components/huawei_lte/const.py +++ b/homeassistant/components/huawei_lte/const.py @@ -6,7 +6,6 @@ DEFAULT_NOTIFY_SERVICE_NAME = DOMAIN UPDATE_SIGNAL = f"{DOMAIN}_update" -UPDATE_OPTIONS_SIGNAL = f"{DOMAIN}_options_update" CONNECTION_TIMEOUT = 10 NOTIFY_SUPPRESS_TIMEOUT = 30 diff --git a/homeassistant/components/huawei_lte/device_tracker.py b/homeassistant/components/huawei_lte/device_tracker.py index b042c0c2912ef..595221a3d8410 100644 --- a/homeassistant/components/huawei_lte/device_tracker.py +++ b/homeassistant/components/huawei_lte/device_tracker.py @@ -105,6 +105,7 @@ def async_add_new_entities( def _better_snakecase(text: str) -> str: + # Awaiting https://github.com/okunishinishi/python-stringcase/pull/18 if text == text.upper(): # All uppercase to all lowercase to get http for HTTP, not h_t_t_p text = text.lower() diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index c6cb93f0e678a..da21894745795 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -337,11 +337,9 @@ async def async_setup_entry( router = hass.data[DOMAIN].routers[config_entry.data[CONF_URL]] sensors: list[Entity] = [] for key in SENSOR_KEYS: - items = router.data.get(key) - if not items: + if not (items := router.data.get(key)): continue - key_meta = SENSOR_META.get(key) - if key_meta: + if key_meta := SENSOR_META.get(key): if key_meta.include: items = filter(key_meta.include.search, items) if key_meta.exclude: @@ -361,10 +359,9 @@ def format_default(value: StateType) -> tuple[StateType, str | None]: unit = None if value is not None: # Clean up value and infer unit, e.g. -71dBm, 15 dB - match = re.match( + if match := re.match( r"([>=<]*)(?P.+?)\s*(?P[a-zA-Z]+)\s*$", str(value) - ) - if match: + ): try: value = float(match.group("value")) unit = match.group("unit") diff --git a/homeassistant/components/huawei_lte/strings.json b/homeassistant/components/huawei_lte/strings.json index 00994f8b0a0ab..4aa0278faf4d9 100644 --- a/homeassistant/components/huawei_lte/strings.json +++ b/homeassistant/components/huawei_lte/strings.json @@ -33,8 +33,7 @@ "init": { "data": { "name": "Notification service name (change requires restart)", - "recipient": "SMS notification recipients", - "track_new_devices": "Track new devices" + "recipient": "SMS notification recipients" } } } From 5c7408cdcecc772ad3a6ba0a3d360df4e4e13cee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Sat, 10 Apr 2021 02:30:32 +0100 Subject: [PATCH 0165/1317] Remove uneeded check in ZHA battery voltage attrib (#48968) --- homeassistant/components/zha/sensor.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 41dce816e86f7..aa7a1649b14a7 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -192,9 +192,7 @@ def extra_state_attributes(self) -> dict[str, Any]: state_attrs["battery_quantity"] = battery_quantity battery_voltage = self._channel.cluster.get("battery_voltage") if battery_voltage is not None: - v_10mv = round(battery_voltage / 10, 2) - v_100mv = round(battery_voltage / 10, 1) - state_attrs["battery_voltage"] = v_100mv if v_100mv == v_10mv else v_10mv + state_attrs["battery_voltage"] = round(battery_voltage / 10, 2) return state_attrs From 324dd12db80472155df8d3329b682469303a503c Mon Sep 17 00:00:00 2001 From: Matt Zimmerman Date: Fri, 9 Apr 2021 20:36:57 -0700 Subject: [PATCH 0166/1317] Update python-smarttub to 0.0.23 (#48978) --- homeassistant/components/smarttub/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/smarttub/manifest.json b/homeassistant/components/smarttub/manifest.json index 2425268e05c75..5505ba69a6d33 100644 --- a/homeassistant/components/smarttub/manifest.json +++ b/homeassistant/components/smarttub/manifest.json @@ -6,7 +6,7 @@ "dependencies": [], "codeowners": ["@mdz"], "requirements": [ - "python-smarttub==0.0.19" + "python-smarttub==0.0.23" ], "quality_scale": "platinum" } diff --git a/requirements_all.txt b/requirements_all.txt index 2f08df723d4b3..ccad87480c7b2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1831,7 +1831,7 @@ python-qbittorrent==0.4.2 python-ripple-api==0.0.3 # homeassistant.components.smarttub -python-smarttub==0.0.19 +python-smarttub==0.0.23 # homeassistant.components.sochain python-sochain-api==0.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9ca7ad00176e0..becdbf700d341 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -980,7 +980,7 @@ python-nest==4.1.0 python-openzwave-mqtt[mqtt-client]==1.4.0 # homeassistant.components.smarttub -python-smarttub==0.0.19 +python-smarttub==0.0.23 # homeassistant.components.songpal python-songpal==0.12 From 7cc857a2988d069a3e4b71e7d3690c8c4c87698f Mon Sep 17 00:00:00 2001 From: Jason <37859597+zachowj@users.noreply.github.com> Date: Fri, 9 Apr 2021 20:47:10 -0700 Subject: [PATCH 0167/1317] Add custom JSONEncoder for subscribe_trigger WS endpoint (#48664) --- homeassistant/components/trace/utils.py | 20 --------- .../components/trace/websocket_api.py | 6 ++- .../components/websocket_api/commands.py | 9 ++-- homeassistant/helpers/json.py | 18 +++++++- tests/components/trace/test_utils.py | 42 ------------------- tests/helpers/test_json.py | 40 +++++++++++++++++- 6 files changed, 66 insertions(+), 69 deletions(-) delete mode 100644 tests/components/trace/test_utils.py diff --git a/homeassistant/components/trace/utils.py b/homeassistant/components/trace/utils.py index 7e804724c5564..50d1590e4fda2 100644 --- a/homeassistant/components/trace/utils.py +++ b/homeassistant/components/trace/utils.py @@ -1,9 +1,5 @@ """Helpers for script and automation tracing and debugging.""" from collections import OrderedDict -from datetime import timedelta -from typing import Any - -from homeassistant.helpers.json import JSONEncoder as HAJSONEncoder class LimitedSizeDict(OrderedDict): @@ -25,19 +21,3 @@ def _check_size_limit(self): if self.size_limit is not None: while len(self) > self.size_limit: self.popitem(last=False) - - -class TraceJSONEncoder(HAJSONEncoder): - """JSONEncoder that supports Home Assistant objects and falls back to repr(o).""" - - def default(self, o: Any) -> Any: - """Convert certain objects. - - Fall back to repr(o). - """ - if isinstance(o, timedelta): - return {"__type": str(type(o)), "total_seconds": o.total_seconds()} - try: - return super().default(o) - except TypeError: - return {"__type": str(type(o)), "repr": repr(o)} diff --git a/homeassistant/components/trace/websocket_api.py b/homeassistant/components/trace/websocket_api.py index 17f3dc7860dae..8f59660e74dab 100644 --- a/homeassistant/components/trace/websocket_api.py +++ b/homeassistant/components/trace/websocket_api.py @@ -11,6 +11,7 @@ async_dispatcher_connect, async_dispatcher_send, ) +from homeassistant.helpers.json import ExtendedJSONEncoder from homeassistant.helpers.script import ( SCRIPT_BREAKPOINT_HIT, SCRIPT_DEBUG_CONTINUE_ALL, @@ -24,7 +25,6 @@ ) from .const import DATA_TRACE -from .utils import TraceJSONEncoder # mypy: allow-untyped-calls, allow-untyped-defs @@ -71,7 +71,9 @@ def websocket_trace_get(hass, connection, msg): message = websocket_api.messages.result_message(msg["id"], trace) - connection.send_message(json.dumps(message, cls=TraceJSONEncoder, allow_nan=False)) + connection.send_message( + json.dumps(message, cls=ExtendedJSONEncoder, allow_nan=False) + ) def get_debug_traces(hass, key): diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 301f106edcc33..4045477f75e7f 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -1,5 +1,6 @@ """Commands part of Websocket API.""" import asyncio +import json import voluptuous as vol @@ -17,6 +18,7 @@ from homeassistant.helpers import config_validation as cv, entity, template from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.event import TrackTemplate, async_track_template_result +from homeassistant.helpers.json import ExtendedJSONEncoder from homeassistant.helpers.service import async_get_all_descriptions from homeassistant.loader import IntegrationNotFound, async_get_integration from homeassistant.setup import DATA_SETUP_TIME, async_get_loaded_integrations @@ -417,10 +419,11 @@ async def handle_subscribe_trigger(hass, connection, msg): @callback def forward_triggers(variables, context=None): """Forward events to websocket.""" + message = messages.event_message( + msg["id"], {"variables": variables, "context": context} + ) connection.send_message( - messages.event_message( - msg["id"], {"variables": variables, "context": context} - ) + json.dumps(message, cls=ExtendedJSONEncoder, allow_nan=False) ) connection.subscriptions[msg["id"]] = ( diff --git a/homeassistant/helpers/json.py b/homeassistant/helpers/json.py index 3168310dc5935..738f744194fc4 100644 --- a/homeassistant/helpers/json.py +++ b/homeassistant/helpers/json.py @@ -1,5 +1,5 @@ """Helpers to help with encoding Home Assistant objects in JSON.""" -from datetime import datetime +from datetime import datetime, timedelta import json from typing import Any @@ -20,3 +20,19 @@ def default(self, o: Any) -> Any: return o.as_dict() return json.JSONEncoder.default(self, o) + + +class ExtendedJSONEncoder(JSONEncoder): + """JSONEncoder that supports Home Assistant objects and falls back to repr(o).""" + + def default(self, o: Any) -> Any: + """Convert certain objects. + + Fall back to repr(o). + """ + if isinstance(o, timedelta): + return {"__type": str(type(o)), "total_seconds": o.total_seconds()} + try: + return super().default(o) + except TypeError: + return {"__type": str(type(o)), "repr": repr(o)} diff --git a/tests/components/trace/test_utils.py b/tests/components/trace/test_utils.py deleted file mode 100644 index ce0f09bfdd8b0..0000000000000 --- a/tests/components/trace/test_utils.py +++ /dev/null @@ -1,42 +0,0 @@ -"""Test trace helpers.""" -from datetime import timedelta - -from homeassistant import core -from homeassistant.components import trace -from homeassistant.util import dt as dt_util - - -def test_json_encoder(hass): - """Test the Trace JSON Encoder.""" - ha_json_enc = trace.utils.TraceJSONEncoder() - state = core.State("test.test", "hello") - - # Test serializing a datetime - now = dt_util.utcnow() - assert ha_json_enc.default(now) == now.isoformat() - - # Test serializing a timedelta - data = timedelta( - days=50, - seconds=27, - microseconds=10, - milliseconds=29000, - minutes=5, - hours=8, - weeks=2, - ) - assert ha_json_enc.default(data) == { - "__type": str(type(data)), - "total_seconds": data.total_seconds(), - } - - # Test serializing a set() - data = {"milk", "beer"} - assert sorted(ha_json_enc.default(data)) == sorted(data) - - # Test serializong object which implements as_dict - assert ha_json_enc.default(state) == state.as_dict() - - # Default method falls back to repr(o) - o = object() - assert ha_json_enc.default(o) == {"__type": str(type(o)), "repr": repr(o)} diff --git a/tests/helpers/test_json.py b/tests/helpers/test_json.py index 1a68f2b8da59d..076af218676b0 100644 --- a/tests/helpers/test_json.py +++ b/tests/helpers/test_json.py @@ -1,8 +1,10 @@ """Test Home Assistant remote methods and classes.""" +from datetime import timedelta + import pytest from homeassistant import core -from homeassistant.helpers.json import JSONEncoder +from homeassistant.helpers.json import ExtendedJSONEncoder, JSONEncoder from homeassistant.util import dt as dt_util @@ -25,3 +27,39 @@ def test_json_encoder(hass): # Default method raises TypeError if non HA object with pytest.raises(TypeError): ha_json_enc.default(1) + + +def test_trace_json_encoder(hass): + """Test the Trace JSON Encoder.""" + ha_json_enc = ExtendedJSONEncoder() + state = core.State("test.test", "hello") + + # Test serializing a datetime + now = dt_util.utcnow() + assert ha_json_enc.default(now) == now.isoformat() + + # Test serializing a timedelta + data = timedelta( + days=50, + seconds=27, + microseconds=10, + milliseconds=29000, + minutes=5, + hours=8, + weeks=2, + ) + assert ha_json_enc.default(data) == { + "__type": str(type(data)), + "total_seconds": data.total_seconds(), + } + + # Test serializing a set() + data = {"milk", "beer"} + assert sorted(ha_json_enc.default(data)) == sorted(data) + + # Test serializong object which implements as_dict + assert ha_json_enc.default(state) == state.as_dict() + + # Default method falls back to repr(o) + o = object() + assert ha_json_enc.default(o) == {"__type": str(type(o)), "repr": repr(o)} From 4cd7f9bd8b5315ad66246c7d048f8221fee4468f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 9 Apr 2021 19:41:29 -1000 Subject: [PATCH 0168/1317] Raise ConfigEntryAuthFailed during setup or coordinator update to start reauth (#48962) --- .coveragerc | 1 + homeassistant/components/abode/__init__.py | 15 +- .../components/airvisual/__init__.py | 25 +-- homeassistant/components/august/__init__.py | 34 +--- homeassistant/components/awair/__init__.py | 24 +-- homeassistant/components/awair/config_flow.py | 10 +- homeassistant/components/axis/device.py | 14 +- .../components/azure_devops/__init__.py | 14 +- .../components/azure_devops/config_flow.py | 21 +-- homeassistant/components/deconz/gateway.py | 14 +- .../components/fireservicerota/__init__.py | 20 +- .../components/fireservicerota/config_flow.py | 9 +- homeassistant/components/fritzbox/__init__.py | 14 +- .../components/fritzbox/config_flow.py | 10 +- homeassistant/components/hive/__init__.py | 16 +- homeassistant/components/hyperion/__init__.py | 23 +-- .../components/icloud/config_flow.py | 9 +- homeassistant/components/neato/__init__.py | 20 +- homeassistant/components/nest/__init__.py | 13 +- homeassistant/components/nuki/__init__.py | 11 +- .../components/ovo_energy/__init__.py | 18 +- .../components/ovo_energy/config_flow.py | 21 +-- homeassistant/components/plex/__init__.py | 28 +-- .../components/powerwall/__init__.py | 29 +-- .../components/sharkiq/config_flow.py | 9 +- .../components/sharkiq/update_coordinator.py | 28 +-- .../components/simplisafe/__init__.py | 27 +-- homeassistant/components/sonarr/__init__.py | 23 +-- .../components/sonarr/config_flow.py | 3 +- homeassistant/components/spotify/__init__.py | 12 +- homeassistant/components/tesla/__init__.py | 23 +-- .../components/totalconnect/__init__.py | 26 +-- homeassistant/components/unifi/config_flow.py | 5 +- homeassistant/components/unifi/controller.py | 14 +- homeassistant/components/verisure/__init__.py | 10 +- .../components/verisure/config_flow.py | 2 +- homeassistant/config_entries.py | 44 ++++- homeassistant/exceptions.py | 16 +- homeassistant/helpers/entity_platform.py | 2 - homeassistant/helpers/update_coordinator.py | 29 ++- tests/components/abode/test_init.py | 21 ++- tests/components/august/test_init.py | 26 +++ .../fireservicerota/test_config_flow.py | 39 ++++ tests/components/fritzbox/test_config_flow.py | 12 +- tests/components/fritzbox/test_init.py | 30 ++- tests/components/hyperion/test_light.py | 12 +- tests/components/sonarr/test_config_flow.py | 8 +- tests/components/sonarr/test_init.py | 8 +- tests/components/unifi/test_config_flow.py | 8 +- tests/components/verisure/test_config_flow.py | 24 ++- tests/test_config_entries.py | 173 +++++++++++++----- 51 files changed, 532 insertions(+), 515 deletions(-) diff --git a/.coveragerc b/.coveragerc index 6e3db6555efb6..2a5e6ecc502ad 100644 --- a/.coveragerc +++ b/.coveragerc @@ -776,6 +776,7 @@ omit = homeassistant/components/poolsense/__init__.py homeassistant/components/poolsense/sensor.py homeassistant/components/poolsense/binary_sensor.py + homeassistant/components/powerwall/__init__.py homeassistant/components/proliphix/climate.py homeassistant/components/progettihwsw/__init__.py homeassistant/components/progettihwsw/binary_sensor.py diff --git a/homeassistant/components/abode/__init__.py b/homeassistant/components/abode/__init__.py index c1c89951c3f8f..329a0a679bca3 100644 --- a/homeassistant/components/abode/__init__.py +++ b/homeassistant/components/abode/__init__.py @@ -9,7 +9,7 @@ from requests.exceptions import ConnectTimeout, HTTPError import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_DATE, @@ -20,7 +20,7 @@ CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.entity import Entity @@ -124,17 +124,10 @@ async def async_setup_entry(hass, config_entry): ) except AbodeAuthenticationException as ex: - LOGGER.error("Invalid credentials: %s", ex) - await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_REAUTH}, - data=config_entry.data, - ) - return False + raise ConfigEntryAuthFailed(f"Invalid credentials: {ex}") from ex except (AbodeException, ConnectTimeout, HTTPError) as ex: - LOGGER.error("Unable to connect to Abode: %s", ex) - raise ConfigEntryNotReady from ex + raise ConfigEntryNotReady(f"Unable to connect to Abode: {ex}") from ex hass.data[DOMAIN] = AbodeSystem(abode, polling) diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py index f02020d25b4de..8447e62a15b29 100644 --- a/homeassistant/components/airvisual/__init__.py +++ b/homeassistant/components/airvisual/__init__.py @@ -11,7 +11,6 @@ NodeProError, ) -from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_API_KEY, @@ -23,6 +22,7 @@ CONF_STATE, ) from homeassistant.core import callback +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -206,27 +206,8 @@ async def async_update_data(): try: return await api_coro - except (InvalidKeyError, KeyExpiredError): - matching_flows = [ - flow - for flow in hass.config_entries.flow.async_progress() - if flow["context"]["source"] == SOURCE_REAUTH - and flow["context"]["unique_id"] == config_entry.unique_id - ] - - if not matching_flows: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={ - "source": SOURCE_REAUTH, - "unique_id": config_entry.unique_id, - }, - data=config_entry.data, - ) - ) - - return {} + except (InvalidKeyError, KeyExpiredError) as ex: + raise ConfigEntryAuthFailed from ex except AirVisualError as err: raise UpdateFailed(f"Error while retrieving data: {err}") from err diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index 46acd1132d9f1..041f24cc44fe7 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -8,10 +8,14 @@ from yalexs.pubnub_activity import activities_from_pubnub_message from yalexs.pubnub_async import AugustPubNub, async_create_pubnub -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry -from homeassistant.const import CONF_PASSWORD, HTTP_UNAUTHORIZED +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError +from homeassistant.exceptions import ( + ConfigEntryAuthFailed, + ConfigEntryNotReady, + HomeAssistantError, +) from .activity import ActivityStream from .const import DATA_AUGUST, DOMAIN, MIN_TIME_BETWEEN_DETAIL_UPDATES, PLATFORMS @@ -43,28 +47,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): try: await august_gateway.async_setup(entry.data) return await async_setup_august(hass, entry, august_gateway) - except ClientResponseError as err: - if err.status == HTTP_UNAUTHORIZED: - _async_start_reauth(hass, entry) - return False - + except (RequireValidation, InvalidAuth) as err: + raise ConfigEntryAuthFailed from err + except (ClientResponseError, CannotConnect, asyncio.TimeoutError) as err: raise ConfigEntryNotReady from err - except (RequireValidation, InvalidAuth): - _async_start_reauth(hass, entry) - return False - except (CannotConnect, asyncio.TimeoutError) as err: - raise ConfigEntryNotReady from err - - -def _async_start_reauth(hass: HomeAssistant, entry: ConfigEntry): - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_REAUTH}, - data=entry.data, - ) - ) - _LOGGER.error("Password is no longer valid. Please reauthenticate") async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): diff --git a/homeassistant/components/awair/__init__.py b/homeassistant/components/awair/__init__.py index bfb95fd91fccb..5b59e4d83aca5 100644 --- a/homeassistant/components/awair/__init__.py +++ b/homeassistant/components/awair/__init__.py @@ -8,8 +8,8 @@ from python_awair import Awair from python_awair.exceptions import AuthError -from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -74,27 +74,7 @@ async def _async_update_data(self) -> Any | None: ) return {result.device.uuid: result for result in results} except AuthError as err: - flow_context = { - "source": SOURCE_REAUTH, - "unique_id": self._config_entry.unique_id, - } - - matching_flows = [ - flow - for flow in self.hass.config_entries.flow.async_progress() - if flow["context"] == flow_context - ] - - if not matching_flows: - self.hass.async_create_task( - self.hass.config_entries.flow.async_init( - DOMAIN, - context=flow_context, - data=self._config_entry.data, - ) - ) - - raise UpdateFailed(err) from err + raise ConfigEntryAuthFailed from err except Exception as err: raise UpdateFailed(err) from err diff --git a/homeassistant/components/awair/config_flow.py b/homeassistant/components/awair/config_flow.py index 76c7cbca3a9b7..466d45999f571 100644 --- a/homeassistant/components/awair/config_flow.py +++ b/homeassistant/components/awair/config_flow.py @@ -69,13 +69,9 @@ async def async_step_reauth(self, user_input: dict | None = None): _, error = await self._check_connection(access_token) if error is None: - for entry in self._async_current_entries(): - if entry.unique_id == self.unique_id: - self.hass.config_entries.async_update_entry( - entry, data=user_input - ) - - return self.async_abort(reason="reauth_successful") + entry = await self.async_set_unique_id(self.unique_id) + self.hass.config_entries.async_update_entry(entry, data=user_input) + return self.async_abort(reason="reauth_successful") if error != "invalid_access_token": return self.async_abort(reason=error) diff --git a/homeassistant/components/axis/device.py b/homeassistant/components/axis/device.py index 93b63b6412241..b2af9e0efc6c1 100644 --- a/homeassistant/components/axis/device.py +++ b/homeassistant/components/axis/device.py @@ -13,7 +13,6 @@ from homeassistant.components import mqtt from homeassistant.components.mqtt import DOMAIN as MQTT_DOMAIN from homeassistant.components.mqtt.models import Message -from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -23,7 +22,7 @@ CONF_USERNAME, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.httpx_client import get_async_client @@ -221,15 +220,8 @@ async def async_setup(self): except CannotConnect as err: raise ConfigEntryNotReady from err - except AuthenticationRequired: - self.hass.async_create_task( - self.hass.config_entries.flow.async_init( - AXIS_DOMAIN, - context={"source": SOURCE_REAUTH}, - data=self.config_entry.data, - ) - ) - return False + except AuthenticationRequired as err: + raise ConfigEntryAuthFailed from err self.fw_version = self.api.vapix.firmware_version self.product_type = self.api.vapix.product_type diff --git a/homeassistant/components/azure_devops/__init__.py b/homeassistant/components/azure_devops/__init__.py index 3db74679d9abb..a971c06826ca3 100644 --- a/homeassistant/components/azure_devops/__init__.py +++ b/homeassistant/components/azure_devops/__init__.py @@ -14,8 +14,8 @@ DATA_AZURE_DEVOPS_CLIENT, DOMAIN, ) -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.config_entries import ConfigEntry +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType, HomeAssistantType @@ -30,17 +30,9 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool if entry.data[CONF_PAT] is not None: await client.authorize(entry.data[CONF_PAT], entry.data[CONF_ORG]) if not client.authorized: - _LOGGER.warning( + raise ConfigEntryAuthFailed( "Could not authorize with Azure DevOps. You may need to update your token" ) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_REAUTH}, - data=entry.data, - ) - ) - return False await client.get_project(entry.data[CONF_ORG], entry.data[CONF_PROJECT]) except aiohttp.ClientError as exception: _LOGGER.warning(exception) diff --git a/homeassistant/components/azure_devops/config_flow.py b/homeassistant/components/azure_devops/config_flow.py index 138ea67e788ea..8ca32193e6359 100644 --- a/homeassistant/components/azure_devops/config_flow.py +++ b/homeassistant/components/azure_devops/config_flow.py @@ -105,17 +105,16 @@ async def async_step_reauth(self, user_input): if errors is not None: return await self._show_reauth_form(errors) - for entry in self._async_current_entries(): - if entry.unique_id == self.unique_id: - self.hass.config_entries.async_update_entry( - entry, - data={ - CONF_ORG: self._organization, - CONF_PROJECT: self._project, - CONF_PAT: self._pat, - }, - ) - return self.async_abort(reason="reauth_successful") + entry = await self.async_set_unique_id(self.unique_id) + self.hass.config_entries.async_update_entry( + entry, + data={ + CONF_ORG: self._organization, + CONF_PROJECT: self._project, + CONF_PAT: self._pat, + }, + ) + return self.async_abort(reason="reauth_successful") def _async_create_entry(self): """Handle create entry.""" diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index 2b38f6956beb7..93a0befa93704 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -4,10 +4,9 @@ import async_timeout from pydeconz import DeconzSession, errors -from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT from homeassistant.core import callback -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import aiohttp_client from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -174,15 +173,8 @@ async def async_setup(self) -> bool: except CannotConnect as err: raise ConfigEntryNotReady from err - except AuthenticationRequired: - self.hass.async_create_task( - self.hass.config_entries.flow.async_init( - DECONZ_DOMAIN, - context={"source": SOURCE_REAUTH}, - data=self.config_entry.data, - ) - ) - return False + except AuthenticationRequired as err: + raise ConfigEntryAuthFailed from err for platform in PLATFORMS: self.hass.async_create_task( diff --git a/homeassistant/components/fireservicerota/__init__.py b/homeassistant/components/fireservicerota/__init__.py index 593109b4f52ee..0a4936b6ed6df 100644 --- a/homeassistant/components/fireservicerota/__init__.py +++ b/homeassistant/components/fireservicerota/__init__.py @@ -14,9 +14,10 @@ from homeassistant.components.binary_sensor import DOMAIN as BINARYSENSOR_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -109,19 +110,10 @@ async def async_refresh_tokens(self) -> bool: self._fsr.refresh_tokens ) - except (InvalidAuthError, InvalidTokenError): - _LOGGER.error("Error refreshing tokens, triggered reauth workflow") - self._hass.async_create_task( - self._hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_REAUTH}, - data={ - **self._entry.data, - }, - ) - ) - - return False + except (InvalidAuthError, InvalidTokenError) as err: + raise ConfigEntryAuthFailed( + "Error refreshing tokens, triggered reauth workflow" + ) from err _LOGGER.debug("Saving new tokens in config entry") self._hass.config_entries.async_update_entry( diff --git a/homeassistant/components/fireservicerota/config_flow.py b/homeassistant/components/fireservicerota/config_flow.py index be986744d6c44..6d16c8513d84a 100644 --- a/homeassistant/components/fireservicerota/config_flow.py +++ b/homeassistant/components/fireservicerota/config_flow.py @@ -82,11 +82,10 @@ async def _validate_and_create_entry(self, user_input, step_id): if step_id == "user": return self.async_create_entry(title=self._username, data=data) - for entry in self.hass.config_entries.async_entries(DOMAIN): - if entry.unique_id == self.unique_id: - self.hass.config_entries.async_update_entry(entry, data=data) - await self.hass.config_entries.async_reload(entry.entry_id) - return self.async_abort(reason="reauth_successful") + entry = await self.async_set_unique_id(self.unique_id) + self.hass.config_entries.async_update_entry(entry, data=data) + await self.hass.config_entries.async_reload(entry.entry_id) + return self.async_abort(reason="reauth_successful") def _show_setup_form(self, user_input=None, errors=None, step_id="user"): """Show the setup form to the user.""" diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index 34f56ddc6f9f7..ff417b25daf19 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -5,7 +5,7 @@ from pyfritzhome import Fritzhome, LoginError import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( CONF_DEVICES, CONF_HOST, @@ -13,6 +13,7 @@ CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, ) +from homeassistant.exceptions import ConfigEntryAuthFailed import homeassistant.helpers.config_validation as cv from .const import CONF_CONNECTIONS, DEFAULT_HOST, DEFAULT_USERNAME, DOMAIN, PLATFORMS @@ -80,15 +81,8 @@ async def async_setup_entry(hass, entry): try: await hass.async_add_executor_job(fritz.login) - except LoginError: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_REAUTH}, - data=entry, - ) - ) - return False + except LoginError as err: + raise ConfigEntryAuthFailed from err hass.data.setdefault(DOMAIN, {CONF_CONNECTIONS: {}, CONF_DEVICES: set()}) hass.data[DOMAIN][CONF_CONNECTIONS][entry.entry_id] = fritz diff --git a/homeassistant/components/fritzbox/config_flow.py b/homeassistant/components/fritzbox/config_flow.py index a462f885484ea..6a200ff22e48e 100644 --- a/homeassistant/components/fritzbox/config_flow.py +++ b/homeassistant/components/fritzbox/config_flow.py @@ -170,12 +170,12 @@ async def async_step_confirm(self, user_input=None): errors=errors, ) - async def async_step_reauth(self, entry): + async def async_step_reauth(self, data): """Trigger a reauthentication flow.""" - self._entry = entry - self._host = entry.data[CONF_HOST] - self._name = entry.data[CONF_HOST] - self._username = entry.data[CONF_USERNAME] + self._entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + self._host = data[CONF_HOST] + self._name = data[CONF_HOST] + self._username = data[CONF_USERNAME] return await self.async_step_reauth_confirm() diff --git a/homeassistant/components/hive/__init__.py b/homeassistant/components/hive/__init__.py index 040ef7b467439..cc20b49b67abb 100644 --- a/homeassistant/components/hive/__init__.py +++ b/homeassistant/components/hive/__init__.py @@ -10,7 +10,7 @@ from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, @@ -77,18 +77,8 @@ async def async_setup_entry(hass, entry): except HTTPException as error: _LOGGER.error("Could not connect to the internet: %s", error) raise ConfigEntryNotReady() from error - except HiveReauthRequired: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={ - "source": config_entries.SOURCE_REAUTH, - "unique_id": entry.unique_id, - }, - data=entry.data, - ) - ) - return False + except HiveReauthRequired as err: + raise ConfigEntryAuthFailed from err for ha_type, hive_type in PLATFORM_LOOKUP.items(): device_list = devices.get(hive_type) diff --git a/homeassistant/components/hyperion/__init__.py b/homeassistant/components/hyperion/__init__.py index 03b892ce83b70..93f3c35f5145e 100644 --- a/homeassistant/components/hyperion/__init__.py +++ b/homeassistant/components/hyperion/__init__.py @@ -11,10 +11,10 @@ from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry -from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SOURCE, CONF_TOKEN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -109,17 +109,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -async def _create_reauth_flow( - hass: HomeAssistant, - config_entry: ConfigEntry, -) -> None: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_REAUTH}, data=config_entry.data - ) - ) - - @callback def listen_for_instance_updates( hass: HomeAssistant, @@ -181,14 +170,12 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b and token is None ): await hyperion_client.async_client_disconnect() - await _create_reauth_flow(hass, config_entry) - return False + raise ConfigEntryAuthFailed # Client login doesn't work? => Reauth. if not await hyperion_client.async_client_login(): await hyperion_client.async_client_disconnect() - await _create_reauth_flow(hass, config_entry) - return False + raise ConfigEntryAuthFailed # Cannot switch instance or cannot load state? => Not ready. if ( diff --git a/homeassistant/components/icloud/config_flow.py b/homeassistant/components/icloud/config_flow.py index 28570f3d93c6b..c26fb43e8b29b 100644 --- a/homeassistant/components/icloud/config_flow.py +++ b/homeassistant/components/icloud/config_flow.py @@ -154,11 +154,10 @@ async def _validate_and_create_entry(self, user_input, step_id): if step_id == "user": return self.async_create_entry(title=self._username, data=data) - for entry in self.hass.config_entries.async_entries(DOMAIN): - if entry.unique_id == self.unique_id: - self.hass.config_entries.async_update_entry(entry, data=data) - await self.hass.config_entries.async_reload(entry.entry_id) - return self.async_abort(reason="reauth_successful") + entry = await self.async_set_unique_id(self.unique_id) + self.hass.config_entries.async_update_entry(entry, data=data) + await self.hass.config_entries.async_reload(entry.entry_id) + return self.async_abort(reason="reauth_successful") async def async_step_user(self, user_input=None): """Handle a flow initiated by the user.""" diff --git a/homeassistant/components/neato/__init__.py b/homeassistant/components/neato/__init__.py index bb0db8ebd8559..9413ff77236fe 100644 --- a/homeassistant/components/neato/__init__.py +++ b/homeassistant/components/neato/__init__.py @@ -7,14 +7,9 @@ from pybotvac.exceptions import NeatoException import voluptuous as vol -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry -from homeassistant.const import ( - CONF_CLIENT_ID, - CONF_CLIENT_SECRET, - CONF_SOURCE, - CONF_TOKEN, -) -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_TOKEN +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.util import Throttle @@ -74,14 +69,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: """Set up config entry.""" if CONF_TOKEN not in entry.data: - # Init reauth flow - hass.async_create_task( - hass.config_entries.flow.async_init( - NEATO_DOMAIN, - context={CONF_SOURCE: SOURCE_REAUTH}, - ) - ) - return False + raise ConfigEntryAuthFailed implementation = ( await config_entry_oauth2_flow.async_get_config_entry_implementation( diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index cd3f6ed9ed336..42b167ee851bc 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -12,7 +12,7 @@ from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber import voluptuous as vol -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_BINARY_SENSORS, CONF_CLIENT_ID, @@ -22,7 +22,7 @@ CONF_STRUCTURE, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import ( aiohttp_client, config_entry_oauth2_flow, @@ -167,14 +167,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): await subscriber.start_async() except AuthException as err: _LOGGER.debug("Subscriber authentication error: %s", err) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_REAUTH}, - data=entry.data, - ) - ) - return False + raise ConfigEntryAuthFailed from err except ConfigurationException as err: _LOGGER.error("Configuration error: %s", err) subscriber.stop_async() diff --git a/homeassistant/components/nuki/__init__.py b/homeassistant/components/nuki/__init__.py index a96cda070772b..173beca0c4a6d 100644 --- a/homeassistant/components/nuki/__init__.py +++ b/homeassistant/components/nuki/__init__.py @@ -10,7 +10,7 @@ from requests.exceptions import RequestException from homeassistant import exceptions -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -85,13 +85,8 @@ async def async_setup_entry(hass, entry): ) locks, openers = await hass.async_add_executor_job(_get_bridge_devices, bridge) - except InvalidCredentialsException: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_REAUTH}, data=entry.data - ) - ) - return False + except InvalidCredentialsException as err: + raise exceptions.ConfigEntryAuthFailed from err except RequestException as err: raise exceptions.ConfigEntryNotReady from err diff --git a/homeassistant/components/ovo_energy/__init__.py b/homeassistant/components/ovo_energy/__init__.py index 98ed42ea10e42..77fafef05ca8f 100644 --- a/homeassistant/components/ovo_energy/__init__.py +++ b/homeassistant/components/ovo_energy/__init__.py @@ -10,9 +10,9 @@ from ovoenergy import OVODailyUsage from ovoenergy.ovoenergy import OVOEnergy -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -44,12 +44,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool raise ConfigEntryNotReady from exception if not authenticated: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_REAUTH}, data=entry.data - ) - ) - return False + raise ConfigEntryAuthFailed async def async_update_data() -> OVODailyUsage: """Fetch data from OVO Energy.""" @@ -61,12 +56,7 @@ async def async_update_data() -> OVODailyUsage: except aiohttp.ClientError as exception: raise UpdateFailed(exception) from exception if not authenticated: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_REAUTH}, data=entry.data - ) - ) - raise UpdateFailed("Not authenticated with OVO Energy") + raise ConfigEntryAuthFailed("Not authenticated with OVO Energy") return await client.get_daily_usage(datetime.utcnow().strftime("%Y-%m")) coordinator = DataUpdateCoordinator( diff --git a/homeassistant/components/ovo_energy/config_flow.py b/homeassistant/components/ovo_energy/config_flow.py index f65b8007ecb3e..25d66d9310210 100644 --- a/homeassistant/components/ovo_energy/config_flow.py +++ b/homeassistant/components/ovo_energy/config_flow.py @@ -74,18 +74,15 @@ async def async_step_reauth(self, user_input): errors["base"] = "connection_error" else: if authenticated: - await self.async_set_unique_id(self.username) - - for entry in self._async_current_entries(): - if entry.unique_id == self.unique_id: - self.hass.config_entries.async_update_entry( - entry, - data={ - CONF_USERNAME: self.username, - CONF_PASSWORD: user_input[CONF_PASSWORD], - }, - ) - return self.async_abort(reason="reauth_successful") + entry = await self.async_set_unique_id(self.username) + self.hass.config_entries.async_update_entry( + entry, + data={ + CONF_USERNAME: self.username, + CONF_PASSWORD: user_input[CONF_PASSWORD], + }, + ) + return self.async_abort(reason="reauth_successful") errors["base"] = "authorization_error" diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index 137c0524bac59..ec2c6480776c9 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -15,15 +15,10 @@ import requests.exceptions from homeassistant.components.media_player import DOMAIN as MP_DOMAIN -from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY, SOURCE_REAUTH -from homeassistant.const import ( - CONF_SOURCE, - CONF_URL, - CONF_VERIFY_SSL, - EVENT_HOMEASSISTANT_STOP, -) +from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY +from homeassistant.const import CONF_URL, CONF_VERIFY_SSL, EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import device_registry as dev_reg, entity_registry as ent_reg from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.debounce import Debouncer @@ -120,19 +115,10 @@ async def async_setup_entry(hass, entry): error, ) raise ConfigEntryNotReady from error - except plexapi.exceptions.Unauthorized: - hass.async_create_task( - hass.config_entries.flow.async_init( - PLEX_DOMAIN, - context={CONF_SOURCE: SOURCE_REAUTH}, - data=entry.data, - ) - ) - _LOGGER.error( - "Token not accepted, please reauthenticate Plex server '%s'", - entry.data[CONF_SERVER], - ) - return False + except plexapi.exceptions.Unauthorized as ex: + raise ConfigEntryAuthFailed( + f"Token not accepted, please reauthenticate Plex server '{entry.data[CONF_SERVER]}'" + ) from ex except ( plexapi.exceptions.BadRequest, plexapi.exceptions.NotFound, diff --git a/homeassistant/components/powerwall/__init__.py b/homeassistant/components/powerwall/__init__.py index ceec56aa05a94..6d61db659c8d1 100644 --- a/homeassistant/components/powerwall/__init__.py +++ b/homeassistant/components/powerwall/__init__.py @@ -11,10 +11,10 @@ PowerwallUnreachableError, ) -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -115,8 +115,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): except AccessDeniedError as err: _LOGGER.debug("Authentication failed", exc_info=err) http_session.close() - _async_start_reauth(hass, entry) - return False + raise ConfigEntryAuthFailed from err await _migrate_old_unique_ids(hass, entry_id, powerwall_data) @@ -130,13 +129,16 @@ async def async_update_data(): _LOGGER.debug("Updating data") try: return await _async_update_powerwall_data(hass, entry, power_wall) - except AccessDeniedError: + except AccessDeniedError as err: if password is None: - raise + raise ConfigEntryAuthFailed from err # If the session expired, relogin, and try again - await hass.async_add_executor_job(power_wall.login, "", password) - return await _async_update_powerwall_data(hass, entry, power_wall) + try: + await hass.async_add_executor_job(power_wall.login, "", password) + return await _async_update_powerwall_data(hass, entry, power_wall) + except AccessDeniedError as ex: + raise ConfigEntryAuthFailed from ex coordinator = DataUpdateCoordinator( hass, @@ -181,17 +183,6 @@ async def _async_update_powerwall_data( return hass.data[DOMAIN][entry.entry_id][POWERWALL_COORDINATOR].data -def _async_start_reauth(hass: HomeAssistant, entry: ConfigEntry): - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_REAUTH}, - data=entry.data, - ) - ) - _LOGGER.error("Password is no longer valid. Please reauthenticate") - - def _login_and_fetch_base_info(power_wall: Powerwall, password: str): """Login to the powerwall and fetch the base info.""" if password is not None: diff --git a/homeassistant/components/sharkiq/config_flow.py b/homeassistant/components/sharkiq/config_flow.py index 046aaee7df511..962d29d77755c 100644 --- a/homeassistant/components/sharkiq/config_flow.py +++ b/homeassistant/components/sharkiq/config_flow.py @@ -84,13 +84,10 @@ async def async_step_reauth(self, user_input: dict | None = None): _, errors = await self._async_validate_input(user_input) if not errors: - for entry in self._async_current_entries(): - if entry.unique_id == self.unique_id: - self.hass.config_entries.async_update_entry( - entry, data=user_input - ) + entry = await self.async_set_unique_id(self.unique_id) + self.hass.config_entries.async_update_entry(entry, data=user_input) - return self.async_abort(reason="reauth_successful") + return self.async_abort(reason="reauth_successful") if errors["base"] != "invalid_auth": return self.async_abort(reason=errors["base"]) diff --git a/homeassistant/components/sharkiq/update_coordinator.py b/homeassistant/components/sharkiq/update_coordinator.py index 73f4093739a49..01490c392975f 100644 --- a/homeassistant/components/sharkiq/update_coordinator.py +++ b/homeassistant/components/sharkiq/update_coordinator.py @@ -12,8 +12,9 @@ SharkIqVacuum, ) -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import _LOGGER, API_TIMEOUT, DOMAIN, UPDATE_INTERVAL @@ -75,30 +76,7 @@ async def _async_update_data(self) -> bool: SharkIqAuthExpiringError, ) as err: _LOGGER.debug("Bad auth state. Attempting re-auth", exc_info=err) - flow_context = { - "source": SOURCE_REAUTH, - "unique_id": self._config_entry.unique_id, - } - - matching_flows = [ - flow - for flow in self.hass.config_entries.flow.async_progress() - if flow["context"] == flow_context - ] - - if not matching_flows: - _LOGGER.debug("Re-initializing flows. Attempting re-auth") - self.hass.async_create_task( - self.hass.config_entries.flow.async_init( - DOMAIN, - context=flow_context, - data=self._config_entry.data, - ) - ) - else: - _LOGGER.debug("Matching flow found") - - raise UpdateFailed(err) from err + raise ConfigEntryAuthFailed from err except Exception as err: _LOGGER.exception("Unexpected error updating SharkIQ") raise UpdateFailed(err) from err diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 485284b32937b..723c04caea027 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -17,7 +17,6 @@ ) import voluptuous as vol -from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.const import ( ATTR_CODE, CONF_CODE, @@ -26,7 +25,7 @@ EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import CoreState, callback -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import ( aiohttp_client, config_validation as cv, @@ -514,27 +513,9 @@ async def async_update_system(system): for result in results: if isinstance(result, InvalidCredentialsError): if self._emergency_refresh_token_used: - matching_flows = [ - flow - for flow in self._hass.config_entries.flow.async_progress() - if flow["context"].get("source") == SOURCE_REAUTH - and flow["context"].get("unique_id") - == self.config_entry.unique_id - ] - - if not matching_flows: - self._hass.async_create_task( - self._hass.config_entries.flow.async_init( - DOMAIN, - context={ - "source": SOURCE_REAUTH, - "unique_id": self.config_entry.unique_id, - }, - data=self.config_entry.data, - ) - ) - - raise UpdateFailed("Update failed with stored refresh token") + raise ConfigEntryAuthFailed( + "Update failed with stored refresh token" + ) LOGGER.warning("SimpliSafe cloud error; trying stored refresh token") self._emergency_refresh_token_used = True diff --git a/homeassistant/components/sonarr/__init__.py b/homeassistant/components/sonarr/__init__.py index 946d9b1e04760..810539220344a 100644 --- a/homeassistant/components/sonarr/__init__.py +++ b/homeassistant/components/sonarr/__init__.py @@ -8,17 +8,16 @@ from sonarr import Sonarr, SonarrAccessRestricted, SonarrError -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_NAME, CONF_API_KEY, CONF_HOST, CONF_PORT, - CONF_SOURCE, CONF_SSL, CONF_VERIFY_SSL, ) -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType @@ -73,9 +72,10 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool try: await sonarr.update() - except SonarrAccessRestricted: - _async_start_reauth(hass, entry) - return False + except SonarrAccessRestricted as err: + raise ConfigEntryAuthFailed( + "API Key is no longer valid. Please reauthenticate" + ) from err except SonarrError as err: raise ConfigEntryNotReady from err @@ -113,17 +113,6 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> boo return unload_ok -def _async_start_reauth(hass: HomeAssistantType, entry: ConfigEntry): - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={CONF_SOURCE: SOURCE_REAUTH}, - data={"config_entry_id": entry.entry_id, **entry.data}, - ) - ) - _LOGGER.error("API Key is no longer valid. Please reauthenticate") - - async def _async_update_listener(hass: HomeAssistantType, entry: ConfigEntry) -> None: """Handle options update.""" await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/sonarr/config_flow.py b/homeassistant/components/sonarr/config_flow.py index fd7315585dcba..fe4cdd13454fd 100644 --- a/homeassistant/components/sonarr/config_flow.py +++ b/homeassistant/components/sonarr/config_flow.py @@ -79,7 +79,8 @@ async def async_step_reauth(self, data: ConfigType | None = None) -> dict[str, A """Handle configuration by re-auth.""" self._reauth = True self._entry_data = dict(data) - self._entry_id = self._entry_data.pop("config_entry_id") + entry = await self.async_set_unique_id(self.unique_id) + self._entry_id = entry.entry_id return await self.async_step_reauth_confirm() diff --git a/homeassistant/components/spotify/__init__.py b/homeassistant/components/spotify/__init__.py index e36491670f5f6..c4b8e30a8ba61 100644 --- a/homeassistant/components/spotify/__init__.py +++ b/homeassistant/components/spotify/__init__.py @@ -6,10 +6,10 @@ from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN from homeassistant.components.spotify import config_flow -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_CREDENTIALS, CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv from homeassistant.helpers.config_entry_oauth2_flow import ( OAuth2Session, @@ -84,13 +84,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: } if not set(session.token["scope"].split(" ")).issuperset(SPOTIFY_SCOPES): - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_REAUTH}, - data=entry.data, - ) - ) + raise ConfigEntryAuthFailed hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, MEDIA_PLAYER_DOMAIN) diff --git a/homeassistant/components/tesla/__init__.py b/homeassistant/components/tesla/__init__.py index 5091d2ea102c2..11b96144ed60e 100644 --- a/homeassistant/components/tesla/__init__.py +++ b/homeassistant/components/tesla/__init__.py @@ -9,7 +9,7 @@ from teslajsonpy.exceptions import IncompleteCredentials, TeslaException import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH, ConfigEntry +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_BATTERY_CHARGING, ATTR_BATTERY_LEVEL, @@ -20,7 +20,8 @@ CONF_USERNAME, HTTP_UNAUTHORIZED, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import callback +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -158,12 +159,11 @@ async def async_setup_entry(hass, config_entry): CONF_WAKE_ON_START, DEFAULT_WAKE_ON_START ) ) - except IncompleteCredentials: - _async_start_reauth(hass, config_entry) - return False + except IncompleteCredentials as ex: + raise ConfigEntryAuthFailed from ex except TeslaException as ex: if ex.code == HTTP_UNAUTHORIZED: - _async_start_reauth(hass, config_entry) + raise ConfigEntryAuthFailed from ex _LOGGER.error("Unable to communicate with Tesla API: %s", ex.message) return False _async_save_tokens(hass, config_entry, access_token, refresh_token) @@ -216,17 +216,6 @@ async def async_unload_entry(hass, config_entry) -> bool: return False -def _async_start_reauth(hass: HomeAssistant, entry: ConfigEntry): - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_REAUTH}, - data=entry.data, - ) - ) - _LOGGER.error("Credentials are no longer valid. Please reauthenticate") - - async def update_listener(hass, config_entry): """Update when config_entry options update.""" controller = hass.data[DOMAIN][config_entry.entry_id]["coordinator"].controller diff --git a/homeassistant/components/totalconnect/__init__.py b/homeassistant/components/totalconnect/__init__.py index 8ef223c49a5f8..4078655f075f3 100644 --- a/homeassistant/components/totalconnect/__init__.py +++ b/homeassistant/components/totalconnect/__init__.py @@ -5,9 +5,10 @@ from total_connect_client import TotalConnectClient import voluptuous as vol -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed import homeassistant.helpers.config_validation as cv from .const import CONF_USERCODES, DOMAIN @@ -46,16 +47,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): password = conf[CONF_PASSWORD] if CONF_USERCODES not in conf: - _LOGGER.warning("No usercodes in TotalConnect configuration") # should only happen for those who used UI before we added usercodes - await hass.config_entries.flow.async_init( - DOMAIN, - context={ - "source": SOURCE_REAUTH, - }, - data=conf, - ) - return False + raise ConfigEntryAuthFailed("No usercodes in TotalConnect configuration") temp_codes = conf[CONF_USERCODES] usercodes = {} @@ -67,18 +60,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): ) if not client.is_valid_credentials(): - _LOGGER.error("TotalConnect authentication failed") - await hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={ - "source": SOURCE_REAUTH, - }, - data=conf, - ) - ) - - return False + raise ConfigEntryAuthFailed("TotalConnect authentication failed") hass.data[DOMAIN][entry.entry_id] = client diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index 094bae0588197..2087f12192829 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -192,8 +192,11 @@ async def async_step_site(self, user_input=None): errors=errors, ) - async def async_step_reauth(self, config_entry: dict): + async def async_step_reauth(self, data: dict): """Trigger a reauthentication flow.""" + config_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) self.reauth_config_entry = config_entry self.context["title_placeholders"] = { diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index c77987bcbddf7..e2ad9636d7acc 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -30,7 +30,6 @@ from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.unifi.switch import BLOCK_SWITCH, POE_SWITCH -from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -39,7 +38,7 @@ CONF_VERIFY_SSL, ) from homeassistant.core import callback -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import aiohttp_client from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity_registry import async_entries_for_config_entry @@ -323,15 +322,8 @@ async def async_setup(self): except CannotConnect as err: raise ConfigEntryNotReady from err - except AuthenticationRequired: - self.hass.async_create_task( - self.hass.config_entries.flow.async_init( - UNIFI_DOMAIN, - context={"source": SOURCE_REAUTH}, - data=self.config_entry, - ) - ) - return False + except AuthenticationRequired as err: + raise ConfigEntryAuthFailed from err for site in sites.values(): if self.site == site["name"]: diff --git a/homeassistant/components/verisure/__init__.py b/homeassistant/components/verisure/__init__.py index 32893aec88be9..55e3d020b1381 100644 --- a/homeassistant/components/verisure/__init__.py +++ b/homeassistant/components/verisure/__init__.py @@ -16,7 +16,7 @@ from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH, ConfigEntry +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_EMAIL, CONF_PASSWORD, @@ -24,6 +24,7 @@ EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed import homeassistant.helpers.config_validation as cv from homeassistant.helpers.storage import STORAGE_DIR @@ -124,12 +125,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = VerisureDataUpdateCoordinator(hass, entry=entry) if not await coordinator.async_login(): - await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_REAUTH}, - data={"entry": entry}, - ) - return False + raise ConfigEntryAuthFailed hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, coordinator.async_logout) diff --git a/homeassistant/components/verisure/config_flow.py b/homeassistant/components/verisure/config_flow.py index 25560b62b1646..3a434cd8b4829 100644 --- a/homeassistant/components/verisure/config_flow.py +++ b/homeassistant/components/verisure/config_flow.py @@ -126,7 +126,7 @@ async def async_step_installation( async def async_step_reauth(self, data: dict[str, Any]) -> dict[str, Any]: """Handle initiation of re-authentication with Verisure.""" - self.entry = data["entry"] + self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 6ef14afb6a6be..d689d4548a9d6 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -14,7 +14,11 @@ from homeassistant import data_entry_flow, loader from homeassistant.const import EVENT_HOMEASSISTANT_STARTED from homeassistant.core import CALLBACK_TYPE, CoreState, HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError +from homeassistant.exceptions import ( + ConfigEntryAuthFailed, + ConfigEntryNotReady, + HomeAssistantError, +) from homeassistant.helpers import device_registry, entity_registry from homeassistant.helpers.event import Event from homeassistant.helpers.typing import UNDEFINED, UndefinedType @@ -259,13 +263,26 @@ async def async_setup( "%s.async_setup_entry did not return boolean", integration.domain ) result = False + except ConfigEntryAuthFailed as ex: + message = str(ex) + auth_base_message = "could not authenticate" + auth_message = ( + f"{auth_base_message}: {message}" if message else auth_base_message + ) + _LOGGER.warning( + "Config entry '%s' for %s integration %s", + self.title, + self.domain, + auth_message, + ) + self._async_process_on_unload() + self.async_start_reauth(hass) + result = False except ConfigEntryNotReady as ex: self.state = ENTRY_STATE_SETUP_RETRY wait_time = 2 ** min(tries, 4) * 5 tries += 1 message = str(ex) - if not message and ex.__cause__: - message = str(ex.__cause__) ready_message = f"ready yet: {message}" if message else "ready yet" if tries == 1: _LOGGER.warning( @@ -494,6 +511,27 @@ def _async_process_on_unload(self) -> None: while self._on_unload: self._on_unload.pop()() + @callback + def async_start_reauth(self, hass: HomeAssistant) -> None: + """Start a reauth flow.""" + flow_context = { + "source": SOURCE_REAUTH, + "entry_id": self.entry_id, + "unique_id": self.unique_id, + } + + for flow in hass.config_entries.flow.async_progress(): + if flow["context"] == flow_context: + return + + hass.async_create_task( + hass.config_entries.flow.async_init( + self.domain, + context=flow_context, + data=self.data, + ) + ) + current_entry: ContextVar[ConfigEntry | None] = ContextVar( "current_entry", default=None diff --git a/homeassistant/exceptions.py b/homeassistant/exceptions.py index b40aa99520d1d..fba00e094cdf2 100644 --- a/homeassistant/exceptions.py +++ b/homeassistant/exceptions.py @@ -98,14 +98,26 @@ def output(self, indent: int) -> Generator: yield from item.output(indent) -class PlatformNotReady(HomeAssistantError): +class IntegrationError(HomeAssistantError): + """Base class for platform and config entry exceptions.""" + + def __str__(self) -> str: + """Return a human readable error.""" + return super().__str__() or str(self.__cause__) + + +class PlatformNotReady(IntegrationError): """Error to indicate that platform is not ready.""" -class ConfigEntryNotReady(HomeAssistantError): +class ConfigEntryNotReady(IntegrationError): """Error to indicate that config entry is not ready.""" +class ConfigEntryAuthFailed(IntegrationError): + """Error to indicate that config entry could not authenticate.""" + + class InvalidStateError(HomeAssistantError): """When an invalid state is encountered.""" diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index dc7386c18a8cb..490a5a2298cdb 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -228,8 +228,6 @@ async def _async_setup_platform( tries += 1 wait_time = min(tries, 6) * PLATFORM_NOT_READY_BASE_WAIT_TIME message = str(ex) - if not message and ex.__cause__: - message = str(ex.__cause__) ready_message = f"ready yet: {message}" if message else "ready yet" if tries == 1: logger.warning( diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index 53e92c433a944..37e234363b8e8 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -11,9 +11,10 @@ import aiohttp import requests +from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import CALLBACK_TYPE, Event, HassJob, HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import entity, event from homeassistant.util.dt import utcnow @@ -149,7 +150,7 @@ async def async_config_entry_first_refresh(self) -> None: fails. Additionally logging is handled by config entry setup to ensure that multiple retries do not cause log spam. """ - await self._async_refresh(log_failures=False) + await self._async_refresh(log_failures=False, raise_on_auth_failed=True) if self.last_update_success: return ex = ConfigEntryNotReady() @@ -160,7 +161,9 @@ async def async_refresh(self) -> None: """Refresh data and log errors.""" await self._async_refresh(log_failures=True) - async def _async_refresh(self, log_failures: bool = True) -> None: + async def _async_refresh( + self, log_failures: bool = True, raise_on_auth_failed: bool = False + ) -> None: """Refresh data.""" if self._unsub_refresh: self._unsub_refresh() @@ -168,6 +171,7 @@ async def _async_refresh(self, log_failures: bool = True) -> None: self._debounced_refresh.async_cancel() start = monotonic() + auth_failed = False try: self.data = await self._async_update_data() @@ -205,6 +209,23 @@ async def _async_refresh(self, log_failures: bool = True) -> None: self.logger.error("Error fetching %s data: %s", self.name, err) self.last_update_success = False + except ConfigEntryAuthFailed as err: + auth_failed = True + self.last_exception = err + if self.last_update_success: + if log_failures: + self.logger.error( + "Authentication failed while fetching %s data: %s", + self.name, + err, + ) + self.last_update_success = False + if raise_on_auth_failed: + raise + + config_entry = config_entries.current_entry.get() + if config_entry: + config_entry.async_start_reauth(self.hass) except NotImplementedError as err: self.last_exception = err raise err @@ -228,7 +249,7 @@ async def _async_refresh(self, log_failures: bool = True) -> None: self.name, monotonic() - start, ) - if self._listeners: + if not auth_failed and self._listeners: self._schedule_refresh() for update_callback in self._listeners: diff --git a/tests/components/abode/test_init.py b/tests/components/abode/test_init.py index b4f3dbd736bff..41219f5ccef27 100644 --- a/tests/components/abode/test_init.py +++ b/tests/components/abode/test_init.py @@ -1,8 +1,9 @@ """Tests for the Abode module.""" from unittest.mock import patch -from abodepy.exceptions import AbodeAuthenticationException +from abodepy.exceptions import AbodeAuthenticationException, AbodeException +from homeassistant import data_entry_flow from homeassistant.components.abode import ( DOMAIN as ABODE_DOMAIN, SERVICE_CAPTURE_IMAGE, @@ -10,6 +11,7 @@ SERVICE_TRIGGER_AUTOMATION, ) from homeassistant.components.alarm_control_panel import DOMAIN as ALARM_DOMAIN +from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY from homeassistant.const import CONF_USERNAME, HTTP_BAD_REQUEST from .common import setup_platform @@ -68,8 +70,23 @@ async def test_invalid_credentials(hass): "homeassistant.components.abode.Abode", side_effect=AbodeAuthenticationException((HTTP_BAD_REQUEST, "auth error")), ), patch( - "homeassistant.components.abode.config_flow.AbodeFlowHandler.async_step_reauth" + "homeassistant.components.abode.config_flow.AbodeFlowHandler.async_step_reauth", + return_value={"type": data_entry_flow.RESULT_TYPE_FORM}, ) as mock_async_step_reauth: await setup_platform(hass, ALARM_DOMAIN) mock_async_step_reauth.assert_called_once() + + +async def test_raise_config_entry_not_ready_when_offline(hass): + """Config entry state is ENTRY_STATE_SETUP_RETRY when abode is offline.""" + with patch( + "homeassistant.components.abode.Abode", + side_effect=AbodeException("any"), + ): + config_entry = await setup_platform(hass, ALARM_DOMAIN) + await hass.async_block_till_done() + + assert config_entry.state == ENTRY_STATE_SETUP_RETRY + + assert hass.config_entries.flow.async_progress() == [] diff --git a/tests/components/august/test_init.py b/tests/components/august/test_init.py index 8b0885f734177..bc9f0048738aa 100644 --- a/tests/components/august/test_init.py +++ b/tests/components/august/test_init.py @@ -271,6 +271,32 @@ async def test_requires_validation_state(hass): assert hass.config_entries.flow.async_progress()[0]["context"]["source"] == "reauth" +async def test_unknown_auth_http_401(hass): + """Config entry state is ENTRY_STATE_SETUP_ERROR when august gets an http.""" + + config_entry = MockConfigEntry( + domain=DOMAIN, + data=_mock_get_config()[DOMAIN], + title="August august", + ) + config_entry.add_to_hass(hass) + assert hass.config_entries.flow.async_progress() == [] + + await setup.async_setup_component(hass, "persistent_notification", {}) + with patch( + "yalexs.authenticator_async.AuthenticatorAsync.async_authenticate", + return_value=_mock_august_authentication("original_token", 1234, None), + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state == ENTRY_STATE_SETUP_ERROR + + flows = hass.config_entries.flow.async_progress() + + assert flows[0]["step_id"] == "reauth_validate" + + async def test_load_unload(hass): """Config entry can be unloaded.""" diff --git a/tests/components/fireservicerota/test_config_flow.py b/tests/components/fireservicerota/test_config_flow.py index 752adb2edc53b..6f4fd21a534d1 100644 --- a/tests/components/fireservicerota/test_config_flow.py +++ b/tests/components/fireservicerota/test_config_flow.py @@ -107,3 +107,42 @@ async def test_step_user(hass): } assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_reauth(hass): + """Test the start of the config flow.""" + entry = MockConfigEntry( + domain=DOMAIN, data=MOCK_CONF, unique_id=MOCK_CONF[CONF_USERNAME] + ) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.fireservicerota.config_flow.FireServiceRota" + ) as mock_fsr: + mock_fireservicerota = mock_fsr.return_value + mock_fireservicerota.request_tokens.return_value = MOCK_TOKEN_INFO + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "reauth", "unique_id": entry.unique_id}, + data=MOCK_CONF, + ) + + await hass.async_block_till_done() + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + with patch( + "homeassistant.components.fireservicerota.config_flow.FireServiceRota" + ) as mock_fsr, patch( + "homeassistant.components.fireservicerota.async_setup_entry", + return_value=True, + ): + mock_fireservicerota = mock_fsr.return_value + mock_fireservicerota.request_tokens.return_value = MOCK_TOKEN_INFO + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PASSWORD: "any"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["reason"] == "reauth_successful" diff --git a/tests/components/fritzbox/test_config_flow.py b/tests/components/fritzbox/test_config_flow.py index f07a78e30de89..5d3fcc181ce6a 100644 --- a/tests/components/fritzbox/test_config_flow.py +++ b/tests/components/fritzbox/test_config_flow.py @@ -103,7 +103,9 @@ async def test_reauth_success(hass: HomeAssistantType, fritz: Mock): mock_config.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_REAUTH}, data=mock_config + DOMAIN, + context={"source": SOURCE_REAUTH, "entry_id": mock_config.entry_id}, + data=mock_config.data, ) assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "reauth_confirm" @@ -130,7 +132,9 @@ async def test_reauth_auth_failed(hass: HomeAssistantType, fritz: Mock): mock_config.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_REAUTH}, data=mock_config + DOMAIN, + context={"source": SOURCE_REAUTH, "entry_id": mock_config.entry_id}, + data=mock_config.data, ) assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "reauth_confirm" @@ -156,7 +160,9 @@ async def test_reauth_not_successful(hass: HomeAssistantType, fritz: Mock): mock_config.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_REAUTH}, data=mock_config + DOMAIN, + context={"source": SOURCE_REAUTH, "entry_id": mock_config.entry_id}, + data=mock_config.data, ) assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "reauth_confirm" diff --git a/tests/components/fritzbox/test_init.py b/tests/components/fritzbox/test_init.py index 08655033f4dea..dafb873fb8a87 100644 --- a/tests/components/fritzbox/test_init.py +++ b/tests/components/fritzbox/test_init.py @@ -1,9 +1,15 @@ """Tests for the AVM Fritz!Box integration.""" -from unittest.mock import Mock, call +from unittest.mock import Mock, call, patch + +from pyfritzhome import LoginError from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN -from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED +from homeassistant.config_entries import ( + ENTRY_STATE_LOADED, + ENTRY_STATE_NOT_LOADED, + ENTRY_STATE_SETUP_ERROR, +) from homeassistant.const import ( CONF_DEVICES, CONF_HOST, @@ -88,3 +94,23 @@ async def test_unload_remove(hass: HomeAssistantType, fritz: Mock): assert entry.state == ENTRY_STATE_NOT_LOADED state = hass.states.get(entity_id) assert state is None + + +async def test_raise_config_entry_not_ready_when_offline(hass): + """Config entry state is ENTRY_STATE_SETUP_RETRY when fritzbox is offline.""" + entry = MockConfigEntry( + domain=FB_DOMAIN, + data={CONF_HOST: "any", **MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0]}, + unique_id="any", + ) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.fritzbox.Fritzhome.login", + side_effect=LoginError("user"), + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + entries = hass.config_entries.async_entries() + config_entry = entries[0] + assert config_entry.state == ENTRY_STATE_SETUP_ERROR diff --git a/tests/components/hyperion/test_light.py b/tests/components/hyperion/test_light.py index bb8fe8d081441..505896fbe0789 100644 --- a/tests/components/hyperion/test_light.py +++ b/tests/components/hyperion/test_light.py @@ -761,7 +761,11 @@ async def test_setup_entry_no_token_reauth(hass: HomeAssistantType) -> None: assert client.async_client_disconnect.called mock_flow_init.assert_called_once_with( DOMAIN, - context={CONF_SOURCE: SOURCE_REAUTH}, + context={ + CONF_SOURCE: SOURCE_REAUTH, + "entry_id": config_entry.entry_id, + "unique_id": config_entry.unique_id, + }, data=config_entry.data, ) assert config_entry.state == ENTRY_STATE_SETUP_ERROR @@ -785,7 +789,11 @@ async def test_setup_entry_bad_token_reauth(hass: HomeAssistantType) -> None: assert client.async_client_disconnect.called mock_flow_init.assert_called_once_with( DOMAIN, - context={CONF_SOURCE: SOURCE_REAUTH}, + context={ + CONF_SOURCE: SOURCE_REAUTH, + "entry_id": config_entry.entry_id, + "unique_id": config_entry.unique_id, + }, data=config_entry.data, ) assert config_entry.state == ENTRY_STATE_SETUP_ERROR diff --git a/tests/components/sonarr/test_config_flow.py b/tests/components/sonarr/test_config_flow.py index 701580ab37ce4..5f32e72aee1f4 100644 --- a/tests/components/sonarr/test_config_flow.py +++ b/tests/components/sonarr/test_config_flow.py @@ -101,13 +101,15 @@ async def test_full_reauth_flow_implementation( hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker ) -> None: """Test the manual reauth flow from start to finish.""" - entry = await setup_integration(hass, aioclient_mock, skip_entry_setup=True) + entry = await setup_integration( + hass, aioclient_mock, skip_entry_setup=True, unique_id="any" + ) assert entry result = await hass.config_entries.flow.async_init( DOMAIN, - context={CONF_SOURCE: SOURCE_REAUTH}, - data={"config_entry_id": entry.entry_id, **entry.data}, + context={CONF_SOURCE: SOURCE_REAUTH, "unique_id": entry.unique_id}, + data=entry.data, ) assert result["type"] == RESULT_TYPE_FORM diff --git a/tests/components/sonarr/test_init.py b/tests/components/sonarr/test_init.py index 16d33a230726e..0e9c253f1b807 100644 --- a/tests/components/sonarr/test_init.py +++ b/tests/components/sonarr/test_init.py @@ -35,8 +35,12 @@ async def test_config_entry_reauth( mock_flow_init.assert_called_once_with( DOMAIN, - context={CONF_SOURCE: SOURCE_REAUTH}, - data={"config_entry_id": entry.entry_id, **entry.data}, + context={ + CONF_SOURCE: SOURCE_REAUTH, + "entry_id": entry.entry_id, + "unique_id": entry.unique_id, + }, + data=entry.data, ) diff --git a/tests/components/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py index 106c18524142a..43d14981bbb65 100644 --- a/tests/components/unifi/test_config_flow.py +++ b/tests/components/unifi/test_config_flow.py @@ -380,8 +380,12 @@ async def test_reauth_flow_update_configuration(hass, aioclient_mock): result = await hass.config_entries.flow.async_init( UNIFI_DOMAIN, - context={"source": SOURCE_REAUTH}, - data=config_entry, + context={ + "source": SOURCE_REAUTH, + "unique_id": config_entry.unique_id, + "entry_id": config_entry.entry_id, + }, + data=config_entry.data, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM diff --git a/tests/components/verisure/test_config_flow.py b/tests/components/verisure/test_config_flow.py index c53c418c72b48..b9af9450132de 100644 --- a/tests/components/verisure/test_config_flow.py +++ b/tests/components/verisure/test_config_flow.py @@ -204,7 +204,13 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data={"entry": entry} + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": entry.unique_id, + "entry_id": entry.entry_id, + }, + data=entry.data, ) assert result["step_id"] == "reauth_confirm" assert result["type"] == RESULT_TYPE_FORM @@ -255,7 +261,13 @@ async def test_reauth_flow_invalid_login(hass: HomeAssistant) -> None: entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data={"entry": entry} + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": entry.unique_id, + "entry_id": entry.entry_id, + }, + data=entry.data, ) with patch( @@ -290,7 +302,13 @@ async def test_reauth_flow_unknown_error(hass: HomeAssistant) -> None: entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data={"entry": entry} + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": entry.unique_id, + "entry_id": entry.entry_id, + }, + data=entry.data, ) with patch( diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 24d635d52a354..dbfe48129c1aa 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -1,16 +1,21 @@ """Test the config manager.""" import asyncio from datetime import timedelta +import logging from unittest.mock import AsyncMock, Mock, patch import pytest from homeassistant import config_entries, data_entry_flow, loader -from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE, EVENT_HOMEASSISTANT_STARTED +from homeassistant.const import EVENT_HOMEASSISTANT_STARTED from homeassistant.core import CoreState, callback -from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError +from homeassistant.exceptions import ( + ConfigEntryAuthFailed, + ConfigEntryNotReady, + HomeAssistantError, +) from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.aiohttp_client import async_create_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.setup import async_setup_component from homeassistant.util import dt @@ -36,6 +41,10 @@ class MockFlowHandler(config_entries.ConfigFlow): VERSION = 1 + async def async_step_reauth(self, data): + """Mock Reauth.""" + return self.async_show_form(step_id="reauth") + with patch.dict( config_entries.HANDLERS, {"comp": MockFlowHandler, "test": MockFlowHandler} ): @@ -2531,56 +2540,130 @@ async def test_entry_reload_calls_on_unload_listeners(hass, manager): assert entry.state == config_entries.ENTRY_STATE_LOADED -async def test_entry_reload_cleans_up_aiohttp_session(hass, manager): - """Test reload cleans up aiohttp sessions their close listener created by the config entry.""" - entry = MockConfigEntry(domain="comp", state=config_entries.ENTRY_STATE_LOADED) - entry.add_to_hass(hass) - async_setup_calls = 0 +async def test_setup_raise_auth_failed(hass, caplog): + """Test a setup raising ConfigEntryAuthFailed.""" + entry = MockConfigEntry(title="test_title", domain="test") + + mock_setup_entry = AsyncMock( + side_effect=ConfigEntryAuthFailed("The password is no longer valid") + ) + mock_integration(hass, MockModule("test", async_setup_entry=mock_setup_entry)) + mock_entity_platform(hass, "config_flow.test", None) + + await entry.async_setup(hass) + await hass.async_block_till_done() + assert "could not authenticate: The password is no longer valid" in caplog.text + + assert entry.state == config_entries.ENTRY_STATE_SETUP_ERROR + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert flows[0]["context"]["entry_id"] == entry.entry_id + assert flows[0]["context"]["source"] == config_entries.SOURCE_REAUTH + + caplog.clear() + entry.state = config_entries.ENTRY_STATE_NOT_LOADED + + await entry.async_setup(hass) + await hass.async_block_till_done() + assert "could not authenticate: The password is no longer valid" in caplog.text + + # Verify multiple ConfigEntryAuthFailed does not generate a second flow + assert entry.state == config_entries.ENTRY_STATE_SETUP_ERROR + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + +async def test_setup_raise_auth_failed_from_first_coordinator_update(hass, caplog): + """Test async_config_entry_first_refresh raises ConfigEntryAuthFailed.""" + entry = MockConfigEntry(title="test_title", domain="test") + + async def async_setup_entry(hass, entry): + """Mock setup entry with a simple coordinator.""" - async def async_setup_entry(hass, _): - """Mock setup entry.""" - nonlocal async_setup_calls - async_setup_calls += 1 - async_create_clientsession(hass) + async def _async_update_data(): + raise ConfigEntryAuthFailed("The password is no longer valid") + + coordinator = DataUpdateCoordinator( + hass, + logging.getLogger(__name__), + name="any", + update_method=_async_update_data, + update_interval=timedelta(seconds=1000), + ) + + await coordinator.async_config_entry_first_refresh() return True - async_setup = AsyncMock(return_value=True) - async_unload_entry = AsyncMock(return_value=True) + mock_integration(hass, MockModule("test", async_setup_entry=async_setup_entry)) + mock_entity_platform(hass, "config_flow.test", None) - mock_integration( - hass, - MockModule( - "comp", - async_setup=async_setup, - async_setup_entry=async_setup_entry, - async_unload_entry=async_unload_entry, - ), - ) - mock_entity_platform(hass, "config_flow.comp", None) + await entry.async_setup(hass) + await hass.async_block_till_done() + assert "could not authenticate: The password is no longer valid" in caplog.text - assert await manager.async_reload(entry.entry_id) - assert len(async_unload_entry.mock_calls) == 1 - assert async_setup_calls == 1 - assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.state == config_entries.ENTRY_STATE_SETUP_ERROR + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert flows[0]["context"]["entry_id"] == entry.entry_id + assert flows[0]["context"]["source"] == config_entries.SOURCE_REAUTH - original_close_listeners = hass.bus.async_listeners()[EVENT_HOMEASSISTANT_CLOSE] + caplog.clear() + entry.state = config_entries.ENTRY_STATE_NOT_LOADED - assert await manager.async_reload(entry.entry_id) - assert len(async_unload_entry.mock_calls) == 2 - assert async_setup_calls == 2 - assert entry.state == config_entries.ENTRY_STATE_LOADED + await entry.async_setup(hass) + await hass.async_block_till_done() + assert "could not authenticate: The password is no longer valid" in caplog.text - assert ( - hass.bus.async_listeners()[EVENT_HOMEASSISTANT_CLOSE] - == original_close_listeners - ) + # Verify multiple ConfigEntryAuthFailed does not generate a second flow + assert entry.state == config_entries.ENTRY_STATE_SETUP_ERROR + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + +async def test_setup_raise_auth_failed_from_future_coordinator_update(hass, caplog): + """Test a coordinator raises ConfigEntryAuthFailed in the future.""" + entry = MockConfigEntry(title="test_title", domain="test") + + async def async_setup_entry(hass, entry): + """Mock setup entry with a simple coordinator.""" + + async def _async_update_data(): + raise ConfigEntryAuthFailed("The password is no longer valid") + + coordinator = DataUpdateCoordinator( + hass, + logging.getLogger(__name__), + name="any", + update_method=_async_update_data, + update_interval=timedelta(seconds=1000), + ) + + await coordinator.async_refresh() + return True + + mock_integration(hass, MockModule("test", async_setup_entry=async_setup_entry)) + mock_entity_platform(hass, "config_flow.test", None) + + await entry.async_setup(hass) + await hass.async_block_till_done() + assert "Authentication failed while fetching" in caplog.text + assert "The password is no longer valid" in caplog.text - assert await manager.async_reload(entry.entry_id) - assert len(async_unload_entry.mock_calls) == 3 - assert async_setup_calls == 3 assert entry.state == config_entries.ENTRY_STATE_LOADED + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert flows[0]["context"]["entry_id"] == entry.entry_id + assert flows[0]["context"]["source"] == config_entries.SOURCE_REAUTH - assert ( - hass.bus.async_listeners()[EVENT_HOMEASSISTANT_CLOSE] - == original_close_listeners - ) + caplog.clear() + entry.state = config_entries.ENTRY_STATE_NOT_LOADED + + await entry.async_setup(hass) + await hass.async_block_till_done() + assert "Authentication failed while fetching" in caplog.text + assert "The password is no longer valid" in caplog.text + + # Verify multiple ConfigEntryAuthFailed does not generate a second flow + assert entry.state == config_entries.ENTRY_STATE_LOADED + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 From 7e4be921a81dd0f0415c47f4ceac12189a89979f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 10 Apr 2021 08:19:16 +0200 Subject: [PATCH 0169/1317] Add helper to get an entity's supported features (#48825) * Add helper to check entity's supported features * Move get_supported_features to helpers/entity.py, add tests * Fix error handling and improve tests --- .../components/light/device_action.py | 32 +--- homeassistant/helpers/entity.py | 18 ++ tests/components/light/test_device_action.py | 156 +++++++++++------- tests/helpers/test_entity.py | 30 +++- 4 files changed, 153 insertions(+), 83 deletions(-) diff --git a/homeassistant/components/light/device_action.py b/homeassistant/components/light/device_action.py index 4c37647f1683b..9cdb5764d70db 100644 --- a/homeassistant/components/light/device_action.py +++ b/homeassistant/components/light/device_action.py @@ -11,15 +11,10 @@ VALID_BRIGHTNESS_PCT, VALID_FLASH, ) -from homeassistant.const import ( - ATTR_ENTITY_ID, - ATTR_SUPPORTED_FEATURES, - CONF_DOMAIN, - CONF_TYPE, - SERVICE_TURN_ON, -) -from homeassistant.core import Context, HomeAssistant +from homeassistant.const import ATTR_ENTITY_ID, CONF_DOMAIN, CONF_TYPE, SERVICE_TURN_ON +from homeassistant.core import Context, HomeAssistant, HomeAssistantError from homeassistant.helpers import config_validation as cv, entity_registry +from homeassistant.helpers.entity import get_supported_features from homeassistant.helpers.typing import ConfigType, TemplateVarsType from . import ATTR_BRIGHTNESS_PCT, ATTR_BRIGHTNESS_STEP_PCT, DOMAIN, SUPPORT_BRIGHTNESS @@ -88,12 +83,7 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]: if entry.domain != DOMAIN: continue - state = hass.states.get(entry.entity_id) - - if state: - supported_features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - else: - supported_features = entry.supported_features + supported_features = get_supported_features(hass, entry.entity_id) if supported_features & SUPPORT_BRIGHTNESS: actions.extend( @@ -133,16 +123,10 @@ async def async_get_action_capabilities(hass: HomeAssistant, config: dict) -> di if config[CONF_TYPE] != toggle_entity.CONF_TURN_ON: return {} - registry = await entity_registry.async_get_registry(hass) - entry = registry.async_get(config[ATTR_ENTITY_ID]) - state = hass.states.get(config[ATTR_ENTITY_ID]) - - supported_features = 0 - - if state: - supported_features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - elif entry: - supported_features = entry.supported_features + try: + supported_features = get_supported_features(hass, config[ATTR_ENTITY_ID]) + except HomeAssistantError: + supported_features = 0 extra_fields = {} diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 0074c0ba5e89d..f30832479c2c8 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -29,6 +29,7 @@ ) from homeassistant.core import CALLBACK_TYPE, Context, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError, NoEntitySpecifiedError +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import EntityPlatform from homeassistant.helpers.entity_registry import RegistryEntry from homeassistant.helpers.event import Event, async_track_entity_registry_updated_event @@ -86,6 +87,23 @@ def async_generate_entity_id( return test_string +def get_supported_features(hass: HomeAssistant, entity_id: str) -> int: + """Get supported features for an entity. + + First try the statemachine, then entity registry. + """ + state = hass.states.get(entity_id) + if state: + return state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + + entity_registry = er.async_get(hass) + entry = entity_registry.async_get(entity_id) + if not entry: + raise HomeAssistantError(f"Unknown entity {entity_id}") + + return entry.supported_features or 0 + + class Entity(ABC): """An abstract class for Home Assistant entities.""" diff --git a/tests/components/light/test_device_action.py b/tests/components/light/test_device_action.py index 4760dfd1c5377..5d6ca2f4a2cf0 100644 --- a/tests/components/light/test_device_action.py +++ b/tests/components/light/test_device_action.py @@ -107,13 +107,13 @@ async def test_get_action_capabilities(hass, device_reg, entity_reg): config_entry_id=config_entry.entry_id, connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - entity_reg.async_get_or_create( + # Test with entity without optional capabilities + entity_id = entity_reg.async_get_or_create( DOMAIN, "test", "5678", device_id=device_entry.id, - ) - + ).entity_id actions = await async_get_device_automations(hass, "action", device_entry.id) assert len(actions) == 3 for action in actions: @@ -122,47 +122,96 @@ async def test_get_action_capabilities(hass, device_reg, entity_reg): ) assert capabilities == {"extra_fields": []} - -async def test_get_action_capabilities_brightness(hass, device_reg, entity_reg): - """Test we get the expected capabilities from a light action.""" - config_entry = MockConfigEntry(domain="test", data={}) - config_entry.add_to_hass(hass) - device_entry = device_reg.async_get_or_create( - config_entry_id=config_entry.entry_id, - connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - ) - entity_reg.async_get_or_create( - DOMAIN, - "test", - "5678", - device_id=device_entry.id, - supported_features=SUPPORT_BRIGHTNESS, - ) - - expected_capabilities = { - "extra_fields": [ - { - "name": "brightness_pct", - "optional": True, - "type": "float", - "valueMax": 100, - "valueMin": 0, - } - ] - } - actions = await async_get_device_automations(hass, "action", device_entry.id) - assert len(actions) == 5 + # Test without entity + entity_reg.async_remove(entity_id) for action in actions: capabilities = await async_get_device_automation_capabilities( hass, "action", action ) - if action["type"] == "turn_on": - assert capabilities == expected_capabilities - else: - assert capabilities == {"extra_fields": []} + assert capabilities == {"extra_fields": []} -async def test_get_action_capabilities_flash(hass, device_reg, entity_reg): +@pytest.mark.parametrize( + "set_state,num_actions,supported_features_reg,supported_features_state,expected_capabilities", + [ + ( + False, + 5, + SUPPORT_BRIGHTNESS, + 0, + { + "turn_on": [ + { + "name": "brightness_pct", + "optional": True, + "type": "float", + "valueMax": 100, + "valueMin": 0, + } + ] + }, + ), + ( + True, + 5, + 0, + SUPPORT_BRIGHTNESS, + { + "turn_on": [ + { + "name": "brightness_pct", + "optional": True, + "type": "float", + "valueMax": 100, + "valueMin": 0, + } + ] + }, + ), + ( + False, + 4, + SUPPORT_FLASH, + 0, + { + "turn_on": [ + { + "name": "flash", + "optional": True, + "type": "select", + "options": [("short", "short"), ("long", "long")], + } + ] + }, + ), + ( + True, + 4, + 0, + SUPPORT_FLASH, + { + "turn_on": [ + { + "name": "flash", + "optional": True, + "type": "select", + "options": [("short", "short"), ("long", "long")], + } + ] + }, + ), + ], +) +async def test_get_action_capabilities_features( + hass, + device_reg, + entity_reg, + set_state, + num_actions, + supported_features_reg, + supported_features_state, + expected_capabilities, +): """Test we get the expected capabilities from a light action.""" config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -170,35 +219,26 @@ async def test_get_action_capabilities_flash(hass, device_reg, entity_reg): config_entry_id=config_entry.entry_id, connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - entity_reg.async_get_or_create( + entity_id = entity_reg.async_get_or_create( DOMAIN, "test", "5678", device_id=device_entry.id, - supported_features=SUPPORT_FLASH, - ) - - expected_capabilities = { - "extra_fields": [ - { - "name": "flash", - "optional": True, - "type": "select", - "options": [("short", "short"), ("long", "long")], - } - ] - } + supported_features=supported_features_reg, + ).entity_id + if set_state: + hass.states.async_set( + entity_id, None, {"supported_features": supported_features_state} + ) actions = await async_get_device_automations(hass, "action", device_entry.id) - assert len(actions) == 4 + assert len(actions) == num_actions for action in actions: capabilities = await async_get_device_automation_capabilities( hass, "action", action ) - if action["type"] == "turn_on": - assert capabilities == expected_capabilities - else: - assert capabilities == {"extra_fields": []} + expected = {"extra_fields": expected_capabilities.get(action["type"], [])} + assert capabilities == expected async def test_action(hass, calls): @@ -209,7 +249,7 @@ async def test_action(hass, calls): assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() - ent1, ent2, ent3 = platform.ENTITIES + ent1 = platform.ENTITIES[0] assert await async_setup_component( hass, diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index b8d0fc7dc9ce7..6eeabb59eba0c 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -8,7 +8,7 @@ import pytest from homeassistant.const import ATTR_DEVICE_CLASS, STATE_UNAVAILABLE, STATE_UNKNOWN -from homeassistant.core import Context +from homeassistant.core import Context, HomeAssistantError from homeassistant.helpers import entity, entity_registry from tests.common import ( @@ -744,3 +744,31 @@ async def test_removing_entity_unavailable(hass): state = hass.states.get("hello.world") assert state is not None assert state.state == STATE_UNAVAILABLE + + +async def test_get_supported_features_entity_registry(hass): + """Test get_supported_features falls back to entity registry.""" + entity_reg = mock_registry(hass) + entity_id = entity_reg.async_get_or_create( + "hello", "world", "5678", supported_features=456 + ).entity_id + assert entity.get_supported_features(hass, entity_id) == 456 + + +async def test_get_supported_features_prioritize_state(hass): + """Test get_supported_features gives priority to state.""" + entity_reg = mock_registry(hass) + entity_id = entity_reg.async_get_or_create( + "hello", "world", "5678", supported_features=456 + ).entity_id + assert entity.get_supported_features(hass, entity_id) == 456 + + hass.states.async_set(entity_id, None, {"supported_features": 123}) + + assert entity.get_supported_features(hass, entity_id) == 123 + + +async def test_get_supported_features_raises_on_unknown(hass): + """Test get_supported_features raises on unknown entity_id.""" + with pytest.raises(HomeAssistantError): + entity.get_supported_features(hass, "hello.world") From 7e30ab2fb278e261c15bd7c2a5431cc04476ca47 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sat, 10 Apr 2021 12:37:20 +0200 Subject: [PATCH 0170/1317] Add missing internal quality scale label (#48947) Co-authored-by: Franck Nijhof --- homeassistant/components/air_quality/manifest.json | 3 ++- homeassistant/components/calendar/manifest.json | 3 ++- homeassistant/components/default_config/manifest.json | 3 ++- homeassistant/components/dhcp/manifest.json | 3 ++- homeassistant/components/geo_location/manifest.json | 3 ++- homeassistant/components/image_processing/manifest.json | 3 ++- homeassistant/components/logbook/manifest.json | 3 ++- homeassistant/components/mailbox/manifest.json | 3 ++- homeassistant/components/media_source/manifest.json | 3 ++- homeassistant/components/my/manifest.json | 3 ++- homeassistant/components/remote/manifest.json | 3 ++- homeassistant/components/search/manifest.json | 3 ++- homeassistant/components/ssdp/manifest.json | 3 ++- homeassistant/components/stt/manifest.json | 3 ++- homeassistant/components/tts/manifest.json | 4 ++-- homeassistant/components/vacuum/manifest.json | 3 ++- homeassistant/components/water_heater/manifest.json | 3 ++- homeassistant/components/webhook/manifest.json | 3 ++- 18 files changed, 36 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/air_quality/manifest.json b/homeassistant/components/air_quality/manifest.json index c7086bb2e8f7e..55fbdbdafd1dc 100644 --- a/homeassistant/components/air_quality/manifest.json +++ b/homeassistant/components/air_quality/manifest.json @@ -2,5 +2,6 @@ "domain": "air_quality", "name": "Air Quality", "documentation": "https://www.home-assistant.io/integrations/air_quality", - "codeowners": [] + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/calendar/manifest.json b/homeassistant/components/calendar/manifest.json index 1ae68100c069b..2455744ee4eee 100644 --- a/homeassistant/components/calendar/manifest.json +++ b/homeassistant/components/calendar/manifest.json @@ -3,5 +3,6 @@ "name": "Calendar", "documentation": "https://www.home-assistant.io/integrations/calendar", "dependencies": ["http"], - "codeowners": [] + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/default_config/manifest.json b/homeassistant/components/default_config/manifest.json index 0f4b940cc3681..74c6b228a6f44 100644 --- a/homeassistant/components/default_config/manifest.json +++ b/homeassistant/components/default_config/manifest.json @@ -32,5 +32,6 @@ "zeroconf", "zone" ], - "codeowners": [] + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/dhcp/manifest.json b/homeassistant/components/dhcp/manifest.json index 80cc6b116c96e..e93e521b8823c 100644 --- a/homeassistant/components/dhcp/manifest.json +++ b/homeassistant/components/dhcp/manifest.json @@ -7,5 +7,6 @@ ], "codeowners": [ "@bdraco" - ] + ], + "quality_scale": "internal" } diff --git a/homeassistant/components/geo_location/manifest.json b/homeassistant/components/geo_location/manifest.json index c5d3a6eba2e7b..c222df8b2aa3e 100644 --- a/homeassistant/components/geo_location/manifest.json +++ b/homeassistant/components/geo_location/manifest.json @@ -2,5 +2,6 @@ "domain": "geo_location", "name": "Geolocation", "documentation": "https://www.home-assistant.io/integrations/geo_location", - "codeowners": [] + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/image_processing/manifest.json b/homeassistant/components/image_processing/manifest.json index 3ff3fb3725492..0541f4898c919 100644 --- a/homeassistant/components/image_processing/manifest.json +++ b/homeassistant/components/image_processing/manifest.json @@ -3,5 +3,6 @@ "name": "Image Processing", "documentation": "https://www.home-assistant.io/integrations/image_processing", "dependencies": ["camera"], - "codeowners": [] + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/logbook/manifest.json b/homeassistant/components/logbook/manifest.json index 26586013108c8..58bc71959b30b 100644 --- a/homeassistant/components/logbook/manifest.json +++ b/homeassistant/components/logbook/manifest.json @@ -3,5 +3,6 @@ "name": "Logbook", "documentation": "https://www.home-assistant.io/integrations/logbook", "dependencies": ["frontend", "http", "recorder"], - "codeowners": [] + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/mailbox/manifest.json b/homeassistant/components/mailbox/manifest.json index 7bbdcfa78cf3d..9d8a1403332df 100644 --- a/homeassistant/components/mailbox/manifest.json +++ b/homeassistant/components/mailbox/manifest.json @@ -3,5 +3,6 @@ "name": "Mailbox", "documentation": "https://www.home-assistant.io/integrations/mailbox", "dependencies": ["http"], - "codeowners": [] + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/media_source/manifest.json b/homeassistant/components/media_source/manifest.json index d941c85acedcf..3b00df4300bf7 100644 --- a/homeassistant/components/media_source/manifest.json +++ b/homeassistant/components/media_source/manifest.json @@ -3,5 +3,6 @@ "name": "Media Source", "documentation": "https://www.home-assistant.io/integrations/media_source", "dependencies": ["http"], - "codeowners": ["@hunterjm"] + "codeowners": ["@hunterjm"], + "quality_scale": "internal" } diff --git a/homeassistant/components/my/manifest.json b/homeassistant/components/my/manifest.json index 3b9e253f35327..8c88b092e1c7d 100644 --- a/homeassistant/components/my/manifest.json +++ b/homeassistant/components/my/manifest.json @@ -3,5 +3,6 @@ "name": "My Home Assistant", "documentation": "https://www.home-assistant.io/integrations/my", "dependencies": ["frontend"], - "codeowners": ["@home-assistant/core"] + "codeowners": ["@home-assistant/core"], + "quality_scale": "internal" } diff --git a/homeassistant/components/remote/manifest.json b/homeassistant/components/remote/manifest.json index 30c442b540b6b..e2caf2d56067e 100644 --- a/homeassistant/components/remote/manifest.json +++ b/homeassistant/components/remote/manifest.json @@ -2,5 +2,6 @@ "domain": "remote", "name": "Remote", "documentation": "https://www.home-assistant.io/integrations/remote", - "codeowners": [] + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/search/manifest.json b/homeassistant/components/search/manifest.json index 273a517d11180..b9ce211511210 100644 --- a/homeassistant/components/search/manifest.json +++ b/homeassistant/components/search/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/search", "dependencies": ["websocket_api"], "after_dependencies": ["scene", "group", "automation", "script"], - "codeowners": ["@home-assistant/core"] + "codeowners": ["@home-assistant/core"], + "quality_scale": "internal" } diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index 938ad979daf7b..5fd635db3f190 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/ssdp", "requirements": ["defusedxml==0.6.0", "netdisco==2.8.2", "async-upnp-client==0.16.0"], "after_dependencies": ["zeroconf"], - "codeowners": [] + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/stt/manifest.json b/homeassistant/components/stt/manifest.json index a3529dcd0b55c..43c5c8684a364 100644 --- a/homeassistant/components/stt/manifest.json +++ b/homeassistant/components/stt/manifest.json @@ -3,5 +3,6 @@ "name": "Speech-to-Text (STT)", "documentation": "https://www.home-assistant.io/integrations/stt", "dependencies": ["http"], - "codeowners": ["@pvizeli"] + "codeowners": ["@pvizeli"], + "quality_scale": "internal" } diff --git a/homeassistant/components/tts/manifest.json b/homeassistant/components/tts/manifest.json index 07cee3b867b61..8f7d203c215af 100644 --- a/homeassistant/components/tts/manifest.json +++ b/homeassistant/components/tts/manifest.json @@ -5,6 +5,6 @@ "requirements": ["mutagen==1.45.1"], "dependencies": ["http"], "after_dependencies": ["media_player"], - "quality_scale": "internal", - "codeowners": ["@pvizeli"] + "codeowners": ["@pvizeli"], + "quality_scale": "internal" } diff --git a/homeassistant/components/vacuum/manifest.json b/homeassistant/components/vacuum/manifest.json index a497bab1380e2..2a874b36a1c1b 100644 --- a/homeassistant/components/vacuum/manifest.json +++ b/homeassistant/components/vacuum/manifest.json @@ -2,5 +2,6 @@ "domain": "vacuum", "name": "Vacuum", "documentation": "https://www.home-assistant.io/integrations/vacuum", - "codeowners": [] + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/water_heater/manifest.json b/homeassistant/components/water_heater/manifest.json index 32221d46a7fe4..ab12a8ab8203b 100644 --- a/homeassistant/components/water_heater/manifest.json +++ b/homeassistant/components/water_heater/manifest.json @@ -2,5 +2,6 @@ "domain": "water_heater", "name": "Water Heater", "documentation": "https://www.home-assistant.io/integrations/water_heater", - "codeowners": [] + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/webhook/manifest.json b/homeassistant/components/webhook/manifest.json index 17c0a2c7dbee8..509563bb4b0d8 100644 --- a/homeassistant/components/webhook/manifest.json +++ b/homeassistant/components/webhook/manifest.json @@ -3,5 +3,6 @@ "name": "Webhook", "documentation": "https://www.home-assistant.io/integrations/webhook", "dependencies": ["http"], - "codeowners": [] + "codeowners": [], + "quality_scale": "internal" } From a0a8638a2d7a23f610e89d848840826fd376d44a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 10 Apr 2021 00:42:42 -1000 Subject: [PATCH 0171/1317] Bump nexia to 0.9.6 (#48982) - Now returns None when a humidity sensor cannot be read instead of throwing an exception --- homeassistant/components/nexia/manifest.json | 2 +- homeassistant/components/nexia/util.py | 2 ++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nexia/manifest.json b/homeassistant/components/nexia/manifest.json index cb3493ebc5598..253400c886d15 100644 --- a/homeassistant/components/nexia/manifest.json +++ b/homeassistant/components/nexia/manifest.json @@ -1,7 +1,7 @@ { "domain": "nexia", "name": "Nexia", - "requirements": ["nexia==0.9.5"], + "requirements": ["nexia==0.9.6"], "codeowners": ["@bdraco"], "documentation": "https://www.home-assistant.io/integrations/nexia", "config_flow": true, diff --git a/homeassistant/components/nexia/util.py b/homeassistant/components/nexia/util.py index 665aa137065cb..74272a3c7fd4f 100644 --- a/homeassistant/components/nexia/util.py +++ b/homeassistant/components/nexia/util.py @@ -13,4 +13,6 @@ def is_invalid_auth_code(http_status_code): def percent_conv(val): """Convert an actual percentage (0.0-1.0) to 0-100 scale.""" + if val is None: + return None return round(val * 100.0, 1) diff --git a/requirements_all.txt b/requirements_all.txt index ccad87480c7b2..9e13cc4c5438f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -988,7 +988,7 @@ netdisco==2.8.2 neurio==0.3.1 # homeassistant.components.nexia -nexia==0.9.5 +nexia==0.9.6 # homeassistant.components.nextcloud nextcloudmonitor==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index becdbf700d341..923f0e6efc0c5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -530,7 +530,7 @@ nessclient==0.9.15 netdisco==2.8.2 # homeassistant.components.nexia -nexia==0.9.5 +nexia==0.9.6 # homeassistant.components.notify_events notify-events==1.0.4 From 1a38d2089d0d4a162608b6c4d5a62f00f121154b Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 10 Apr 2021 15:21:11 +0200 Subject: [PATCH 0172/1317] Bump python-typing-update to v0.3.3 (#48992) * Bump python-typing-update to 0.3.3 * Changes after update --- .pre-commit-config.yaml | 2 +- .../components/denonavr/config_flow.py | 22 ++++++++++--------- homeassistant/components/denonavr/receiver.py | 8 ++++--- .../components/kostal_plenticore/helper.py | 11 +++++----- .../components/kostal_plenticore/sensor.py | 18 ++++++++------- homeassistant/components/modbus/__init__.py | 6 +++-- tests/components/axis/conftest.py | 4 ++-- tests/components/climacell/test_weather.py | 6 +++-- tests/components/onewire/__init__.py | 7 +++--- 9 files changed, 48 insertions(+), 36 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 97093bc8dbed3..9ea2ea51348c6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -69,7 +69,7 @@ repos: - id: prettier stages: [manual] - repo: https://github.com/cdce8p/python-typing-update - rev: v0.3.2 + rev: v0.3.3 hooks: # Run `python-typing-update` hook manually from time to time # to update python typing syntax. diff --git a/homeassistant/components/denonavr/config_flow.py b/homeassistant/components/denonavr/config_flow.py index f2c37d9fc7567..adcd4e26b6f79 100644 --- a/homeassistant/components/denonavr/config_flow.py +++ b/homeassistant/components/denonavr/config_flow.py @@ -1,6 +1,8 @@ """Config flow to configure Denon AVR receivers using their HTTP interface.""" +from __future__ import annotations + import logging -from typing import Any, Dict, Optional +from typing import Any from urllib.parse import urlparse import denonavr @@ -44,7 +46,7 @@ def __init__(self, config_entry: config_entries.ConfigEntry): """Init object.""" self.config_entry = config_entry - async def async_step_init(self, user_input: Optional[Dict[str, Any]] = None): + async def async_step_init(self, user_input: dict[str, Any] | None = None): """Manage the options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) @@ -96,7 +98,7 @@ def async_get_options_flow( """Get the options flow.""" return OptionsFlowHandler(config_entry) - async def async_step_user(self, user_input: Optional[Dict[str, Any]] = None): + async def async_step_user(self, user_input: dict[str, Any] | None = None): """Handle a flow initialized by the user.""" errors = {} if user_input is not None: @@ -123,8 +125,8 @@ async def async_step_user(self, user_input: Optional[Dict[str, Any]] = None): ) async def async_step_select( - self, user_input: Optional[Dict[str, Any]] = None - ) -> Dict[str, Any]: + self, user_input: dict[str, Any] | None = None + ) -> dict[str, Any]: """Handle multiple receivers found.""" errors = {} if user_input is not None: @@ -144,8 +146,8 @@ async def async_step_select( ) async def async_step_confirm( - self, user_input: Optional[Dict[str, Any]] = None - ) -> Dict[str, Any]: + self, user_input: dict[str, Any] | None = None + ) -> dict[str, Any]: """Allow the user to confirm adding the device.""" if user_input is not None: return await self.async_step_connect() @@ -154,8 +156,8 @@ async def async_step_confirm( return self.async_show_form(step_id="confirm") async def async_step_connect( - self, user_input: Optional[Dict[str, Any]] = None - ) -> Dict[str, Any]: + self, user_input: dict[str, Any] | None = None + ) -> dict[str, Any]: """Connect to the receiver.""" connect_denonavr = ConnectDenonAVR( self.host, @@ -204,7 +206,7 @@ async def async_step_connect( }, ) - async def async_step_ssdp(self, discovery_info: Dict[str, Any]) -> Dict[str, Any]: + async def async_step_ssdp(self, discovery_info: dict[str, Any]) -> dict[str, Any]: """Handle a discovered Denon AVR. This flow is triggered by the SSDP component. It will check if the diff --git a/homeassistant/components/denonavr/receiver.py b/homeassistant/components/denonavr/receiver.py index 31d91c0a9baab..8b50373799b49 100644 --- a/homeassistant/components/denonavr/receiver.py +++ b/homeassistant/components/denonavr/receiver.py @@ -1,6 +1,8 @@ """Code to handle a DenonAVR receiver.""" +from __future__ import annotations + import logging -from typing import Callable, Optional +from typing import Callable from denonavr import DenonAVR @@ -18,7 +20,7 @@ def __init__( zone2: bool, zone3: bool, async_client_getter: Callable, - entry_state: Optional[str] = None, + entry_state: str | None = None, ): """Initialize the class.""" self._async_client_getter = async_client_getter @@ -35,7 +37,7 @@ def __init__( self._zones["Zone3"] = None @property - def receiver(self) -> Optional[DenonAVR]: + def receiver(self) -> DenonAVR | None: """Return the class containing all connections to the receiver.""" return self._receiver diff --git a/homeassistant/components/kostal_plenticore/helper.py b/homeassistant/components/kostal_plenticore/helper.py index 6f9cc4f5ee0a7..a78896a179dd0 100644 --- a/homeassistant/components/kostal_plenticore/helper.py +++ b/homeassistant/components/kostal_plenticore/helper.py @@ -1,9 +1,10 @@ """Code to handle the Plenticore API.""" +from __future__ import annotations + import asyncio from collections import defaultdict from datetime import datetime, timedelta import logging -from typing import Dict, Union from aiohttp.client_exceptions import ClientError from kostal.plenticore import PlenticoreApiClient, PlenticoreAuthenticationException @@ -151,7 +152,7 @@ def stop_fetch_data(self, module_id: str, data_id: str) -> None: class ProcessDataUpdateCoordinator(PlenticoreUpdateCoordinator): """Implementation of PlenticoreUpdateCoordinator for process data.""" - async def _async_update_data(self) -> Dict[str, Dict[str, str]]: + async def _async_update_data(self) -> dict[str, dict[str, str]]: client = self._plenticore.client if not self._fetch or client is None: @@ -172,7 +173,7 @@ async def _async_update_data(self) -> Dict[str, Dict[str, str]]: class SettingDataUpdateCoordinator(PlenticoreUpdateCoordinator): """Implementation of PlenticoreUpdateCoordinator for settings data.""" - async def _async_update_data(self) -> Dict[str, Dict[str, str]]: + async def _async_update_data(self) -> dict[str, dict[str, str]]: client = self._plenticore.client if not self._fetch or client is None: @@ -223,7 +224,7 @@ def get_method(cls, name: str) -> callable: return getattr(cls, name) @staticmethod - def format_round(state: str) -> Union[int, str]: + def format_round(state: str) -> int | str: """Return the given state value as rounded integer.""" try: return round(float(state)) @@ -231,7 +232,7 @@ def format_round(state: str) -> Union[int, str]: return state @staticmethod - def format_energy(state: str) -> Union[float, str]: + def format_energy(state: str) -> float | str: """Return the given state value as energy value, scaled to kWh.""" try: return round(float(state) / 1000, 1) diff --git a/homeassistant/components/kostal_plenticore/sensor.py b/homeassistant/components/kostal_plenticore/sensor.py index 82b06c96a7731..f9d25f65d90fc 100644 --- a/homeassistant/components/kostal_plenticore/sensor.py +++ b/homeassistant/components/kostal_plenticore/sensor.py @@ -1,7 +1,9 @@ """Platform for Kostal Plenticore sensors.""" +from __future__ import annotations + from datetime import timedelta import logging -from typing import Any, Callable, Dict, Optional +from typing import Any, Callable from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry @@ -109,9 +111,9 @@ def __init__( module_id: str, data_id: str, sensor_name: str, - sensor_data: Dict[str, Any], + sensor_data: dict[str, Any], formatter: Callable[[str], Any], - device_info: Dict[str, Any], + device_info: dict[str, Any], ): """Create a new Sensor Entity for Plenticore process data.""" super().__init__(coordinator) @@ -147,7 +149,7 @@ async def async_will_remove_from_hass(self) -> None: await super().async_will_remove_from_hass() @property - def device_info(self) -> Dict[str, Any]: + def device_info(self) -> dict[str, Any]: """Return the device info.""" return self._device_info @@ -162,17 +164,17 @@ def name(self) -> str: return f"{self.platform_name} {self._sensor_name}" @property - def unit_of_measurement(self) -> Optional[str]: + def unit_of_measurement(self) -> str | None: """Return the unit of this Sensor Entity or None.""" return self._sensor_data.get(ATTR_UNIT_OF_MEASUREMENT) @property - def icon(self) -> Optional[str]: + def icon(self) -> str | None: """Return the icon name of this Sensor Entity or None.""" return self._sensor_data.get(ATTR_ICON) @property - def device_class(self) -> Optional[str]: + def device_class(self) -> str | None: """Return the class of this device, from component DEVICE_CLASSES.""" return self._sensor_data.get(ATTR_DEVICE_CLASS) @@ -182,7 +184,7 @@ def entity_registry_enabled_default(self) -> bool: return self._sensor_data.get(ATTR_ENABLED_DEFAULT, False) @property - def state(self) -> Optional[Any]: + def state(self) -> Any | None: """Return the state of the sensor.""" if self.coordinator.data is None: # None is translated to STATE_UNKNOWN diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index a4e0c21ec5f62..2defb32393d0c 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -1,5 +1,7 @@ """Support for Modbus.""" -from typing import Any, Union +from __future__ import annotations + +from typing import Any import voluptuous as vol @@ -95,7 +97,7 @@ BASE_SCHEMA = vol.Schema({vol.Optional(CONF_NAME, default=DEFAULT_HUB): cv.string}) -def number(value: Any) -> Union[int, float]: +def number(value: Any) -> int | float: """Coerce a value to number without losing precision.""" if isinstance(value, int): return value diff --git a/tests/components/axis/conftest.py b/tests/components/axis/conftest.py index be4483593665b..c816277a3f432 100644 --- a/tests/components/axis/conftest.py +++ b/tests/components/axis/conftest.py @@ -1,6 +1,6 @@ """Axis conftest.""" +from __future__ import annotations -from typing import Optional from unittest.mock import patch from axis.rtsp import ( @@ -34,7 +34,7 @@ def stop_stream(): rtsp_client_mock.return_value.stop = stop_stream - def make_rtsp_call(data: Optional[dict] = None, state: str = ""): + def make_rtsp_call(data: dict | None = None, state: str = ""): """Generate a RTSP call.""" axis_streammanager_session_callback = rtsp_client_mock.call_args[0][4] diff --git a/tests/components/climacell/test_weather.py b/tests/components/climacell/test_weather.py index c49ad8b3c484c..646c5cd114b1c 100644 --- a/tests/components/climacell/test_weather.py +++ b/tests/components/climacell/test_weather.py @@ -1,7 +1,9 @@ """Tests for Climacell weather entity.""" +from __future__ import annotations + from datetime import datetime import logging -from typing import Any, Dict +from typing import Any from unittest.mock import patch import pytest @@ -58,7 +60,7 @@ async def _enable_entity(hass: HomeAssistantType, entity_name: str) -> None: assert updated_entry.disabled is False -async def _setup(hass: HomeAssistantType, config: Dict[str, Any]) -> State: +async def _setup(hass: HomeAssistantType, config: dict[str, Any]) -> State: """Set up entry and return entity state.""" with patch( "homeassistant.util.dt.utcnow", diff --git a/tests/components/onewire/__init__.py b/tests/components/onewire/__init__.py index f133f89d5d626..fdc0c7fe12cca 100644 --- a/tests/components/onewire/__init__.py +++ b/tests/components/onewire/__init__.py @@ -1,6 +1,7 @@ """Tests for 1-Wire integration.""" +from __future__ import annotations -from typing import Any, List, Tuple +from typing import Any from unittest.mock import patch from pyownet.protocol import ProtocolError @@ -129,8 +130,8 @@ def setup_owproxy_mock_devices(owproxy, domain, device_ids) -> None: def setup_sysbus_mock_devices( - domain: str, device_ids: List[str] -) -> Tuple[List[str], List[Any]]: + domain: str, device_ids: list[str] +) -> tuple[list[str], list[Any]]: """Set up mock for sysbus.""" glob_result = [] read_side_effect = [] From e7a3308efa50295cb53acf2abe8c9c53df8353f2 Mon Sep 17 00:00:00 2001 From: EetuRasilainen <81036144+EetuRasilainen@users.noreply.github.com> Date: Sat, 10 Apr 2021 16:32:41 +0200 Subject: [PATCH 0173/1317] Improve schema of media_player.join service (#48342) Co-authored-by: eetu --- homeassistant/components/media_player/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index f98c6eeceafdf..23261ea029e2c 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -307,7 +307,7 @@ async def async_setup(hass, config): ) component.async_register_entity_service( SERVICE_JOIN, - {vol.Required(ATTR_GROUP_MEMBERS): list}, + {vol.Required(ATTR_GROUP_MEMBERS): vol.All(cv.ensure_list, [cv.entity_id])}, "async_join_players", [SUPPORT_GROUPING], ) From 157c1d0ed26bbeaee6c4553fdf09266ed59fcad9 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sat, 10 Apr 2021 16:45:53 +0200 Subject: [PATCH 0174/1317] Fix Zeroconf manifest schema in hassfest script (#49006) --- script/hassfest/manifest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 55bfa717a3fb6..d8f6350911dff 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -74,6 +74,7 @@ def verify_version(value: str): { vol.Required("type"): str, vol.Optional("macaddress"): vol.All(str, verify_uppercase), + vol.Optional("manufacturer"): vol.All(str, verify_lowercase), vol.Optional("name"): vol.All(str, verify_lowercase), } ), From fcf86e59cccfa42751e3f7388a189edf79b01e07 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sat, 10 Apr 2021 16:55:28 +0200 Subject: [PATCH 0175/1317] Log zone cleaning (#47912) --- homeassistant/components/neato/vacuum.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py index e0b3c7b779f03..2415b86fc6279 100644 --- a/homeassistant/components/neato/vacuum.py +++ b/homeassistant/components/neato/vacuum.py @@ -395,6 +395,7 @@ def neato_custom_cleaning(self, mode, navigation, category, zone=None): "Zone '%s' was not found for the robot '%s'", zone, self.entity_id ) return + _LOGGER.info("Start cleaning zone '%s' with robot %s", zone, self.entity_id) self._clean_state = STATE_CLEANING try: From 7ef17bf175c239cbb4c34e8411e8e0b1441b8b90 Mon Sep 17 00:00:00 2001 From: dynasticorpheus Date: Sat, 10 Apr 2021 17:04:43 +0200 Subject: [PATCH 0176/1317] Add support for event type closed to integration folder_watcher (#48226) --- homeassistant/components/folder_watcher/__init__.py | 4 ++++ homeassistant/components/folder_watcher/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/folder_watcher/__init__.py b/homeassistant/components/folder_watcher/__init__.py index 7d3fd5e77a7a8..7d3b1ec7660bf 100644 --- a/homeassistant/components/folder_watcher/__init__.py +++ b/homeassistant/components/folder_watcher/__init__.py @@ -92,6 +92,10 @@ def on_deleted(self, event): """File deleted.""" self.process(event) + def on_closed(self, event): + """File closed.""" + self.process(event) + return EventHandler(patterns, hass) diff --git a/homeassistant/components/folder_watcher/manifest.json b/homeassistant/components/folder_watcher/manifest.json index 60239aeb0d19f..ebb0ab947f5fe 100644 --- a/homeassistant/components/folder_watcher/manifest.json +++ b/homeassistant/components/folder_watcher/manifest.json @@ -2,7 +2,7 @@ "domain": "folder_watcher", "name": "Folder Watcher", "documentation": "https://www.home-assistant.io/integrations/folder_watcher", - "requirements": ["watchdog==1.0.2"], + "requirements": ["watchdog==2.0.2"], "codeowners": [], "quality_scale": "internal" } diff --git a/requirements_all.txt b/requirements_all.txt index 9e13cc4c5438f..02136d3845756 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2305,7 +2305,7 @@ wakeonlan==2.0.0 waqiasync==1.0.0 # homeassistant.components.folder_watcher -watchdog==1.0.2 +watchdog==2.0.2 # homeassistant.components.waterfurnace waterfurnace==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 923f0e6efc0c5..034535fde78f2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1214,7 +1214,7 @@ vultr==0.1.2 wakeonlan==2.0.0 # homeassistant.components.folder_watcher -watchdog==1.0.2 +watchdog==2.0.2 # homeassistant.components.wiffi wiffi==1.0.1 From f8690c29cd08304924a0859fe00617089aad5796 Mon Sep 17 00:00:00 2001 From: amitfin Date: Sat, 10 Apr 2021 18:20:08 +0300 Subject: [PATCH 0177/1317] Bump libhdate dependency (#48695) --- .../components/jewish_calendar/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../jewish_calendar/test_binary_sensor.py | 4 +- .../components/jewish_calendar/test_sensor.py | 162 +++++++++--------- 5 files changed, 86 insertions(+), 86 deletions(-) diff --git a/homeassistant/components/jewish_calendar/manifest.json b/homeassistant/components/jewish_calendar/manifest.json index 500d98dbe9fa5..bd45335797dff 100644 --- a/homeassistant/components/jewish_calendar/manifest.json +++ b/homeassistant/components/jewish_calendar/manifest.json @@ -2,6 +2,6 @@ "domain": "jewish_calendar", "name": "Jewish Calendar", "documentation": "https://www.home-assistant.io/integrations/jewish_calendar", - "requirements": ["hdate==0.9.12"], + "requirements": ["hdate==0.10.2"], "codeowners": ["@tsvi"] } diff --git a/requirements_all.txt b/requirements_all.txt index 02136d3845756..7ec6435949e06 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -738,7 +738,7 @@ hass_splunk==0.1.1 hatasmota==0.2.9 # homeassistant.components.jewish_calendar -hdate==0.9.12 +hdate==0.10.2 # homeassistant.components.heatmiser heatmiserV3==1.1.18 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 034535fde78f2..a5f1c2ef9f615 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -408,7 +408,7 @@ hass-nabucasa==0.43.0 hatasmota==0.2.9 # homeassistant.components.jewish_calendar -hdate==0.9.12 +hdate==0.10.2 # homeassistant.components.here_travel_time herepy==2.0.0 diff --git a/tests/components/jewish_calendar/test_binary_sensor.py b/tests/components/jewish_calendar/test_binary_sensor.py index c21211962263b..1f34532eeb50b 100644 --- a/tests/components/jewish_calendar/test_binary_sensor.py +++ b/tests/components/jewish_calendar/test_binary_sensor.py @@ -248,10 +248,10 @@ async def test_issur_melacha_sensor( ], [ make_nyc_test_params( - dt(2020, 10, 23, 17, 46, 59, 999999), [STATE_OFF, STATE_ON] + dt(2020, 10, 23, 17, 44, 59, 999999), [STATE_OFF, STATE_ON] ), make_nyc_test_params( - dt(2020, 10, 24, 18, 44, 59, 999999), [STATE_ON, STATE_OFF] + dt(2020, 10, 24, 18, 42, 59, 999999), [STATE_ON, STATE_OFF] ), ], ids=["before_candle_lighting", "before_havdalah"], diff --git a/tests/components/jewish_calendar/test_sensor.py b/tests/components/jewish_calendar/test_sensor.py index a5c99c850b8e1..8634f28d8fa70 100644 --- a/tests/components/jewish_calendar/test_sensor.py +++ b/tests/components/jewish_calendar/test_sensor.py @@ -77,7 +77,7 @@ async def test_jewish_calendar_hebrew(hass): "hebrew", "t_set_hakochavim", True, - dt(2018, 9, 8, 19, 48), + dt(2018, 9, 8, 19, 45), ), ( dt(2018, 9, 8), @@ -87,7 +87,7 @@ async def test_jewish_calendar_hebrew(hass): "hebrew", "t_set_hakochavim", False, - dt(2018, 9, 8, 19, 21), + dt(2018, 9, 8, 19, 19), ), ( dt(2018, 10, 14), @@ -204,10 +204,10 @@ async def test_jewish_calendar_sensor( make_nyc_test_params( dt(2018, 9, 1, 16, 0), { - "english_upcoming_candle_lighting": dt(2018, 8, 31, 19, 15), - "english_upcoming_havdalah": dt(2018, 9, 1, 20, 14), - "english_upcoming_shabbat_candle_lighting": dt(2018, 8, 31, 19, 15), - "english_upcoming_shabbat_havdalah": dt(2018, 9, 1, 20, 14), + "english_upcoming_candle_lighting": dt(2018, 8, 31, 19, 12), + "english_upcoming_havdalah": dt(2018, 9, 1, 20, 10), + "english_upcoming_shabbat_candle_lighting": dt(2018, 8, 31, 19, 12), + "english_upcoming_shabbat_havdalah": dt(2018, 9, 1, 20, 10), "english_parshat_hashavua": "Ki Tavo", "hebrew_parshat_hashavua": "כי תבוא", }, @@ -215,10 +215,10 @@ async def test_jewish_calendar_sensor( make_nyc_test_params( dt(2018, 9, 1, 16, 0), { - "english_upcoming_candle_lighting": dt(2018, 8, 31, 19, 15), - "english_upcoming_havdalah": dt(2018, 9, 1, 20, 22), - "english_upcoming_shabbat_candle_lighting": dt(2018, 8, 31, 19, 15), - "english_upcoming_shabbat_havdalah": dt(2018, 9, 1, 20, 22), + "english_upcoming_candle_lighting": dt(2018, 8, 31, 19, 12), + "english_upcoming_havdalah": dt(2018, 9, 1, 20, 18), + "english_upcoming_shabbat_candle_lighting": dt(2018, 8, 31, 19, 12), + "english_upcoming_shabbat_havdalah": dt(2018, 9, 1, 20, 18), "english_parshat_hashavua": "Ki Tavo", "hebrew_parshat_hashavua": "כי תבוא", }, @@ -227,10 +227,10 @@ async def test_jewish_calendar_sensor( make_nyc_test_params( dt(2018, 9, 1, 20, 0), { - "english_upcoming_shabbat_candle_lighting": dt(2018, 8, 31, 19, 15), - "english_upcoming_shabbat_havdalah": dt(2018, 9, 1, 20, 14), - "english_upcoming_candle_lighting": dt(2018, 8, 31, 19, 15), - "english_upcoming_havdalah": dt(2018, 9, 1, 20, 14), + "english_upcoming_shabbat_candle_lighting": dt(2018, 8, 31, 19, 12), + "english_upcoming_shabbat_havdalah": dt(2018, 9, 1, 20, 10), + "english_upcoming_candle_lighting": dt(2018, 8, 31, 19, 12), + "english_upcoming_havdalah": dt(2018, 9, 1, 20, 10), "english_parshat_hashavua": "Ki Tavo", "hebrew_parshat_hashavua": "כי תבוא", }, @@ -238,10 +238,10 @@ async def test_jewish_calendar_sensor( make_nyc_test_params( dt(2018, 9, 1, 20, 21), { - "english_upcoming_candle_lighting": dt(2018, 9, 7, 19, 4), - "english_upcoming_havdalah": dt(2018, 9, 8, 20, 2), - "english_upcoming_shabbat_candle_lighting": dt(2018, 9, 7, 19, 4), - "english_upcoming_shabbat_havdalah": dt(2018, 9, 8, 20, 2), + "english_upcoming_candle_lighting": dt(2018, 9, 7, 19), + "english_upcoming_havdalah": dt(2018, 9, 8, 19, 58), + "english_upcoming_shabbat_candle_lighting": dt(2018, 9, 7, 19), + "english_upcoming_shabbat_havdalah": dt(2018, 9, 8, 19, 58), "english_parshat_hashavua": "Nitzavim", "hebrew_parshat_hashavua": "נצבים", }, @@ -249,10 +249,10 @@ async def test_jewish_calendar_sensor( make_nyc_test_params( dt(2018, 9, 7, 13, 1), { - "english_upcoming_candle_lighting": dt(2018, 9, 7, 19, 4), - "english_upcoming_havdalah": dt(2018, 9, 8, 20, 2), - "english_upcoming_shabbat_candle_lighting": dt(2018, 9, 7, 19, 4), - "english_upcoming_shabbat_havdalah": dt(2018, 9, 8, 20, 2), + "english_upcoming_candle_lighting": dt(2018, 9, 7, 19), + "english_upcoming_havdalah": dt(2018, 9, 8, 19, 58), + "english_upcoming_shabbat_candle_lighting": dt(2018, 9, 7, 19), + "english_upcoming_shabbat_havdalah": dt(2018, 9, 8, 19, 58), "english_parshat_hashavua": "Nitzavim", "hebrew_parshat_hashavua": "נצבים", }, @@ -260,10 +260,10 @@ async def test_jewish_calendar_sensor( make_nyc_test_params( dt(2018, 9, 8, 21, 25), { - "english_upcoming_candle_lighting": dt(2018, 9, 9, 19, 1), - "english_upcoming_havdalah": dt(2018, 9, 11, 19, 57), - "english_upcoming_shabbat_candle_lighting": dt(2018, 9, 14, 18, 52), - "english_upcoming_shabbat_havdalah": dt(2018, 9, 15, 19, 50), + "english_upcoming_candle_lighting": dt(2018, 9, 9, 18, 57), + "english_upcoming_havdalah": dt(2018, 9, 11, 19, 53), + "english_upcoming_shabbat_candle_lighting": dt(2018, 9, 14, 18, 48), + "english_upcoming_shabbat_havdalah": dt(2018, 9, 15, 19, 46), "english_parshat_hashavua": "Vayeilech", "hebrew_parshat_hashavua": "וילך", "english_holiday": "Erev Rosh Hashana", @@ -273,10 +273,10 @@ async def test_jewish_calendar_sensor( make_nyc_test_params( dt(2018, 9, 9, 21, 25), { - "english_upcoming_candle_lighting": dt(2018, 9, 9, 19, 1), - "english_upcoming_havdalah": dt(2018, 9, 11, 19, 57), - "english_upcoming_shabbat_candle_lighting": dt(2018, 9, 14, 18, 52), - "english_upcoming_shabbat_havdalah": dt(2018, 9, 15, 19, 50), + "english_upcoming_candle_lighting": dt(2018, 9, 9, 18, 57), + "english_upcoming_havdalah": dt(2018, 9, 11, 19, 53), + "english_upcoming_shabbat_candle_lighting": dt(2018, 9, 14, 18, 48), + "english_upcoming_shabbat_havdalah": dt(2018, 9, 15, 19, 46), "english_parshat_hashavua": "Vayeilech", "hebrew_parshat_hashavua": "וילך", "english_holiday": "Rosh Hashana I", @@ -286,10 +286,10 @@ async def test_jewish_calendar_sensor( make_nyc_test_params( dt(2018, 9, 10, 21, 25), { - "english_upcoming_candle_lighting": dt(2018, 9, 9, 19, 1), - "english_upcoming_havdalah": dt(2018, 9, 11, 19, 57), - "english_upcoming_shabbat_candle_lighting": dt(2018, 9, 14, 18, 52), - "english_upcoming_shabbat_havdalah": dt(2018, 9, 15, 19, 50), + "english_upcoming_candle_lighting": dt(2018, 9, 9, 18, 57), + "english_upcoming_havdalah": dt(2018, 9, 11, 19, 53), + "english_upcoming_shabbat_candle_lighting": dt(2018, 9, 14, 18, 48), + "english_upcoming_shabbat_havdalah": dt(2018, 9, 15, 19, 46), "english_parshat_hashavua": "Vayeilech", "hebrew_parshat_hashavua": "וילך", "english_holiday": "Rosh Hashana II", @@ -299,10 +299,10 @@ async def test_jewish_calendar_sensor( make_nyc_test_params( dt(2018, 9, 28, 21, 25), { - "english_upcoming_candle_lighting": dt(2018, 9, 28, 18, 28), - "english_upcoming_havdalah": dt(2018, 9, 29, 19, 25), - "english_upcoming_shabbat_candle_lighting": dt(2018, 9, 28, 18, 28), - "english_upcoming_shabbat_havdalah": dt(2018, 9, 29, 19, 25), + "english_upcoming_candle_lighting": dt(2018, 9, 28, 18, 25), + "english_upcoming_havdalah": dt(2018, 9, 29, 19, 22), + "english_upcoming_shabbat_candle_lighting": dt(2018, 9, 28, 18, 25), + "english_upcoming_shabbat_havdalah": dt(2018, 9, 29, 19, 22), "english_parshat_hashavua": "none", "hebrew_parshat_hashavua": "none", }, @@ -310,10 +310,10 @@ async def test_jewish_calendar_sensor( make_nyc_test_params( dt(2018, 9, 29, 21, 25), { - "english_upcoming_candle_lighting": dt(2018, 9, 30, 18, 25), - "english_upcoming_havdalah": dt(2018, 10, 2, 19, 20), - "english_upcoming_shabbat_candle_lighting": dt(2018, 10, 5, 18, 17), - "english_upcoming_shabbat_havdalah": dt(2018, 10, 6, 19, 13), + "english_upcoming_candle_lighting": dt(2018, 9, 30, 18, 22), + "english_upcoming_havdalah": dt(2018, 10, 2, 19, 17), + "english_upcoming_shabbat_candle_lighting": dt(2018, 10, 5, 18, 13), + "english_upcoming_shabbat_havdalah": dt(2018, 10, 6, 19, 11), "english_parshat_hashavua": "Bereshit", "hebrew_parshat_hashavua": "בראשית", "english_holiday": "Hoshana Raba", @@ -323,10 +323,10 @@ async def test_jewish_calendar_sensor( make_nyc_test_params( dt(2018, 9, 30, 21, 25), { - "english_upcoming_candle_lighting": dt(2018, 9, 30, 18, 25), - "english_upcoming_havdalah": dt(2018, 10, 2, 19, 20), - "english_upcoming_shabbat_candle_lighting": dt(2018, 10, 5, 18, 17), - "english_upcoming_shabbat_havdalah": dt(2018, 10, 6, 19, 13), + "english_upcoming_candle_lighting": dt(2018, 9, 30, 18, 22), + "english_upcoming_havdalah": dt(2018, 10, 2, 19, 17), + "english_upcoming_shabbat_candle_lighting": dt(2018, 10, 5, 18, 13), + "english_upcoming_shabbat_havdalah": dt(2018, 10, 6, 19, 11), "english_parshat_hashavua": "Bereshit", "hebrew_parshat_hashavua": "בראשית", "english_holiday": "Shmini Atzeret", @@ -336,10 +336,10 @@ async def test_jewish_calendar_sensor( make_nyc_test_params( dt(2018, 10, 1, 21, 25), { - "english_upcoming_candle_lighting": dt(2018, 9, 30, 18, 25), - "english_upcoming_havdalah": dt(2018, 10, 2, 19, 20), - "english_upcoming_shabbat_candle_lighting": dt(2018, 10, 5, 18, 17), - "english_upcoming_shabbat_havdalah": dt(2018, 10, 6, 19, 13), + "english_upcoming_candle_lighting": dt(2018, 9, 30, 18, 22), + "english_upcoming_havdalah": dt(2018, 10, 2, 19, 17), + "english_upcoming_shabbat_candle_lighting": dt(2018, 10, 5, 18, 13), + "english_upcoming_shabbat_havdalah": dt(2018, 10, 6, 19, 11), "english_parshat_hashavua": "Bereshit", "hebrew_parshat_hashavua": "בראשית", "english_holiday": "Simchat Torah", @@ -349,10 +349,10 @@ async def test_jewish_calendar_sensor( make_jerusalem_test_params( dt(2018, 9, 29, 21, 25), { - "english_upcoming_candle_lighting": dt(2018, 9, 30, 18, 10), - "english_upcoming_havdalah": dt(2018, 10, 1, 19, 2), - "english_upcoming_shabbat_candle_lighting": dt(2018, 10, 5, 18, 3), - "english_upcoming_shabbat_havdalah": dt(2018, 10, 6, 18, 56), + "english_upcoming_candle_lighting": dt(2018, 9, 30, 18, 7), + "english_upcoming_havdalah": dt(2018, 10, 1, 19, 1), + "english_upcoming_shabbat_candle_lighting": dt(2018, 10, 5, 18, 1), + "english_upcoming_shabbat_havdalah": dt(2018, 10, 6, 18, 54), "english_parshat_hashavua": "Bereshit", "hebrew_parshat_hashavua": "בראשית", "english_holiday": "Hoshana Raba", @@ -362,10 +362,10 @@ async def test_jewish_calendar_sensor( make_jerusalem_test_params( dt(2018, 9, 30, 21, 25), { - "english_upcoming_candle_lighting": dt(2018, 9, 30, 18, 10), - "english_upcoming_havdalah": dt(2018, 10, 1, 19, 2), - "english_upcoming_shabbat_candle_lighting": dt(2018, 10, 5, 18, 3), - "english_upcoming_shabbat_havdalah": dt(2018, 10, 6, 18, 56), + "english_upcoming_candle_lighting": dt(2018, 9, 30, 18, 7), + "english_upcoming_havdalah": dt(2018, 10, 1, 19, 1), + "english_upcoming_shabbat_candle_lighting": dt(2018, 10, 5, 18, 1), + "english_upcoming_shabbat_havdalah": dt(2018, 10, 6, 18, 54), "english_parshat_hashavua": "Bereshit", "hebrew_parshat_hashavua": "בראשית", "english_holiday": "Shmini Atzeret", @@ -375,10 +375,10 @@ async def test_jewish_calendar_sensor( make_jerusalem_test_params( dt(2018, 10, 1, 21, 25), { - "english_upcoming_candle_lighting": dt(2018, 10, 5, 18, 3), - "english_upcoming_havdalah": dt(2018, 10, 6, 18, 56), - "english_upcoming_shabbat_candle_lighting": dt(2018, 10, 5, 18, 3), - "english_upcoming_shabbat_havdalah": dt(2018, 10, 6, 18, 56), + "english_upcoming_candle_lighting": dt(2018, 10, 5, 18, 1), + "english_upcoming_havdalah": dt(2018, 10, 6, 18, 54), + "english_upcoming_shabbat_candle_lighting": dt(2018, 10, 5, 18, 1), + "english_upcoming_shabbat_havdalah": dt(2018, 10, 6, 18, 54), "english_parshat_hashavua": "Bereshit", "hebrew_parshat_hashavua": "בראשית", }, @@ -386,9 +386,9 @@ async def test_jewish_calendar_sensor( make_nyc_test_params( dt(2016, 6, 11, 8, 25), { - "english_upcoming_candle_lighting": dt(2016, 6, 10, 20, 7), - "english_upcoming_havdalah": dt(2016, 6, 13, 21, 17), - "english_upcoming_shabbat_candle_lighting": dt(2016, 6, 10, 20, 7), + "english_upcoming_candle_lighting": dt(2016, 6, 10, 20, 9), + "english_upcoming_havdalah": dt(2016, 6, 13, 21, 19), + "english_upcoming_shabbat_candle_lighting": dt(2016, 6, 10, 20, 9), "english_upcoming_shabbat_havdalah": "unknown", "english_parshat_hashavua": "Bamidbar", "hebrew_parshat_hashavua": "במדבר", @@ -399,10 +399,10 @@ async def test_jewish_calendar_sensor( make_nyc_test_params( dt(2016, 6, 12, 8, 25), { - "english_upcoming_candle_lighting": dt(2016, 6, 10, 20, 7), - "english_upcoming_havdalah": dt(2016, 6, 13, 21, 17), - "english_upcoming_shabbat_candle_lighting": dt(2016, 6, 17, 20, 10), - "english_upcoming_shabbat_havdalah": dt(2016, 6, 18, 21, 19), + "english_upcoming_candle_lighting": dt(2016, 6, 10, 20, 9), + "english_upcoming_havdalah": dt(2016, 6, 13, 21, 19), + "english_upcoming_shabbat_candle_lighting": dt(2016, 6, 17, 20, 12), + "english_upcoming_shabbat_havdalah": dt(2016, 6, 18, 21, 21), "english_parshat_hashavua": "Nasso", "hebrew_parshat_hashavua": "נשא", "english_holiday": "Shavuot", @@ -412,10 +412,10 @@ async def test_jewish_calendar_sensor( make_jerusalem_test_params( dt(2017, 9, 21, 8, 25), { - "english_upcoming_candle_lighting": dt(2017, 9, 20, 18, 23), - "english_upcoming_havdalah": dt(2017, 9, 23, 19, 13), - "english_upcoming_shabbat_candle_lighting": dt(2017, 9, 22, 19, 14), - "english_upcoming_shabbat_havdalah": dt(2017, 9, 23, 19, 13), + "english_upcoming_candle_lighting": dt(2017, 9, 20, 18, 20), + "english_upcoming_havdalah": dt(2017, 9, 23, 19, 11), + "english_upcoming_shabbat_candle_lighting": dt(2017, 9, 22, 19, 12), + "english_upcoming_shabbat_havdalah": dt(2017, 9, 23, 19, 11), "english_parshat_hashavua": "Ha'Azinu", "hebrew_parshat_hashavua": "האזינו", "english_holiday": "Rosh Hashana I", @@ -425,10 +425,10 @@ async def test_jewish_calendar_sensor( make_jerusalem_test_params( dt(2017, 9, 22, 8, 25), { - "english_upcoming_candle_lighting": dt(2017, 9, 20, 18, 23), - "english_upcoming_havdalah": dt(2017, 9, 23, 19, 13), - "english_upcoming_shabbat_candle_lighting": dt(2017, 9, 22, 19, 14), - "english_upcoming_shabbat_havdalah": dt(2017, 9, 23, 19, 13), + "english_upcoming_candle_lighting": dt(2017, 9, 20, 18, 20), + "english_upcoming_havdalah": dt(2017, 9, 23, 19, 11), + "english_upcoming_shabbat_candle_lighting": dt(2017, 9, 22, 19, 12), + "english_upcoming_shabbat_havdalah": dt(2017, 9, 23, 19, 11), "english_parshat_hashavua": "Ha'Azinu", "hebrew_parshat_hashavua": "האזינו", "english_holiday": "Rosh Hashana II", @@ -438,10 +438,10 @@ async def test_jewish_calendar_sensor( make_jerusalem_test_params( dt(2017, 9, 23, 8, 25), { - "english_upcoming_candle_lighting": dt(2017, 9, 20, 18, 23), - "english_upcoming_havdalah": dt(2017, 9, 23, 19, 13), - "english_upcoming_shabbat_candle_lighting": dt(2017, 9, 22, 19, 14), - "english_upcoming_shabbat_havdalah": dt(2017, 9, 23, 19, 13), + "english_upcoming_candle_lighting": dt(2017, 9, 20, 18, 20), + "english_upcoming_havdalah": dt(2017, 9, 23, 19, 11), + "english_upcoming_shabbat_candle_lighting": dt(2017, 9, 22, 19, 12), + "english_upcoming_shabbat_havdalah": dt(2017, 9, 23, 19, 11), "english_parshat_hashavua": "Ha'Azinu", "hebrew_parshat_hashavua": "האזינו", "english_holiday": "", From 676af205e40a4f1552c08c095d770412a810aaf6 Mon Sep 17 00:00:00 2001 From: Adrien Brault Date: Sat, 10 Apr 2021 17:22:15 +0200 Subject: [PATCH 0178/1317] Fix light template invalid color temp message (#48337) --- homeassistant/components/template/light.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index e76ba42289b16..2479388eaafb7 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -435,8 +435,9 @@ def _update_temperature(self, render): self._temperature = temperature else: _LOGGER.error( - "Received invalid color temperature : %s. Expected: 0-%s", + "Received invalid color temperature : %s. Expected: %s-%s", temperature, + self.min_mireds, self.max_mireds, ) self._temperature = None From 21744790d3ee37d3a19b57641d3caf48fc448b3b Mon Sep 17 00:00:00 2001 From: Marvin Wichmann Date: Sat, 10 Apr 2021 18:12:43 +0200 Subject: [PATCH 0179/1317] Add KNX source address to Sensor and BinarySensor (#48857) * Add source address to Sensor and BinarySensor * Fix typing * Review: Always use UTC time in state attributes * Review: Add missing UTC conversion in sensor --- homeassistant/components/knx/binary_sensor.py | 14 +++++++++++--- homeassistant/components/knx/const.py | 2 ++ homeassistant/components/knx/sensor.py | 17 +++++++++++++++-- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/knx/binary_sensor.py b/homeassistant/components/knx/binary_sensor.py index 0faeb9f37b467..47462f272d470 100644 --- a/homeassistant/components/knx/binary_sensor.py +++ b/homeassistant/components/knx/binary_sensor.py @@ -9,8 +9,9 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.util import dt -from .const import ATTR_COUNTER, DOMAIN +from .const import ATTR_COUNTER, ATTR_LAST_KNX_UPDATE, ATTR_SOURCE, DOMAIN from .knx_entity import KnxEntity @@ -51,9 +52,16 @@ def is_on(self) -> bool: @property def extra_state_attributes(self) -> dict[str, Any] | None: """Return device specific state attributes.""" + attr: dict[str, Any] = {} + if self._device.counter is not None: - return {ATTR_COUNTER: self._device.counter} - return None + attr[ATTR_COUNTER] = self._device.counter + if self._device.last_telegram is not None: + attr[ATTR_SOURCE] = str(self._device.last_telegram.source_address) + attr[ATTR_LAST_KNX_UPDATE] = str( + dt.as_utc(self._device.last_telegram.timestamp) + ) + return attr @property def force_update(self) -> bool: diff --git a/homeassistant/components/knx/const.py b/homeassistant/components/knx/const.py index dfe357ef33c21..78b3f5ec7f9aa 100644 --- a/homeassistant/components/knx/const.py +++ b/homeassistant/components/knx/const.py @@ -26,6 +26,8 @@ CONF_RESET_AFTER = "reset_after" ATTR_COUNTER = "counter" +ATTR_SOURCE = "source" +ATTR_LAST_KNX_UPDATE = "last_knx_update" class ColorTempModes(Enum): diff --git a/homeassistant/components/knx/sensor.py b/homeassistant/components/knx/sensor.py index f14cf7e5b29fa..f75f483b9fbb5 100644 --- a/homeassistant/components/knx/sensor.py +++ b/homeassistant/components/knx/sensor.py @@ -1,7 +1,7 @@ """Support for KNX/IP sensors.""" from __future__ import annotations -from typing import Callable, Iterable +from typing import Any, Callable, Iterable from xknx.devices import Sensor as XknxSensor @@ -9,8 +9,9 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType +from homeassistant.util import dt -from .const import DOMAIN +from .const import ATTR_LAST_KNX_UPDATE, ATTR_SOURCE, DOMAIN from .knx_entity import KnxEntity @@ -54,6 +55,18 @@ def device_class(self) -> str | None: return device_class return None + @property + def extra_state_attributes(self) -> dict[str, Any] | None: + """Return device specific state attributes.""" + attr: dict[str, Any] = {} + + if self._device.last_telegram is not None: + attr[ATTR_SOURCE] = str(self._device.last_telegram.source_address) + attr[ATTR_LAST_KNX_UPDATE] = str( + dt.as_utc(self._device.last_telegram.timestamp) + ) + return attr + @property def force_update(self) -> bool: """ From e1d4d65ac41108122c0c40ada83a7149c8039938 Mon Sep 17 00:00:00 2001 From: Andreas Oberritter Date: Sat, 10 Apr 2021 20:16:28 +0200 Subject: [PATCH 0180/1317] Bump pysml to 0.0.5 (#49014) --- homeassistant/components/edl21/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/edl21/manifest.json b/homeassistant/components/edl21/manifest.json index c3b65c3b35274..ea960de6b4960 100644 --- a/homeassistant/components/edl21/manifest.json +++ b/homeassistant/components/edl21/manifest.json @@ -2,6 +2,6 @@ "domain": "edl21", "name": "EDL21", "documentation": "https://www.home-assistant.io/integrations/edl21", - "requirements": ["pysml==0.0.3"], + "requirements": ["pysml==0.0.5"], "codeowners": ["@mtdcr"] } diff --git a/requirements_all.txt b/requirements_all.txt index 7ec6435949e06..a141ca54748f2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1720,7 +1720,7 @@ pysmartthings==0.7.6 pysmarty==0.8 # homeassistant.components.edl21 -pysml==0.0.3 +pysml==0.0.5 # homeassistant.components.snmp pysnmp==4.4.12 From 3cd40ac79c981b8cf090e114d3fedfdd7a0d2078 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sat, 10 Apr 2021 20:48:33 +0100 Subject: [PATCH 0181/1317] Set Lyric hold time to use local time instead of utc (#48994) --- homeassistant/components/lyric/climate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lyric/climate.py b/homeassistant/components/lyric/climate.py index 0e3672f952ed8..e57bfd0c514bd 100644 --- a/homeassistant/components/lyric/climate.py +++ b/homeassistant/components/lyric/climate.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from time import gmtime, strftime, time +from time import localtime, strftime, time from aiolyric.objects.device import LyricDevice from aiolyric.objects.location import LyricLocation @@ -82,7 +82,7 @@ vol.Required(ATTR_TIME_PERIOD, default="01:00:00"): vol.All( cv.time_period, cv.positive_timedelta, - lambda td: strftime("%H:%M:%S", gmtime(time() + td.total_seconds())), + lambda td: strftime("%H:%M:%S", localtime(time() + td.total_seconds())), ) } From 654a5326410729d4cd29d992b6bb702da190fc8c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 10 Apr 2021 21:50:12 +0200 Subject: [PATCH 0182/1317] Upgrade wakonlan to 2.0.1 (#48995) --- homeassistant/components/wake_on_lan/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wake_on_lan/manifest.json b/homeassistant/components/wake_on_lan/manifest.json index b98414257720a..8ca0389bea0cd 100644 --- a/homeassistant/components/wake_on_lan/manifest.json +++ b/homeassistant/components/wake_on_lan/manifest.json @@ -2,6 +2,6 @@ "domain": "wake_on_lan", "name": "Wake on LAN", "documentation": "https://www.home-assistant.io/integrations/wake_on_lan", - "requirements": ["wakeonlan==2.0.0"], + "requirements": ["wakeonlan==2.0.1"], "codeowners": ["@ntilley905"] } diff --git a/requirements_all.txt b/requirements_all.txt index a141ca54748f2..36c125dc92511 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2299,7 +2299,7 @@ vtjp==0.1.14 vultr==0.1.2 # homeassistant.components.wake_on_lan -wakeonlan==2.0.0 +wakeonlan==2.0.1 # homeassistant.components.waqi waqiasync==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a5f1c2ef9f615..2b7a572bcc6d3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1211,7 +1211,7 @@ vsure==1.7.3 vultr==0.1.2 # homeassistant.components.wake_on_lan -wakeonlan==2.0.0 +wakeonlan==2.0.1 # homeassistant.components.folder_watcher watchdog==2.0.2 From 5983fac5c213acab799339c7baec43cf4300f196 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 10 Apr 2021 22:03:44 +0200 Subject: [PATCH 0183/1317] Fix use search instead of match to filter logs (#49017) --- homeassistant/components/logger/__init__.py | 2 +- tests/components/logger/test_init.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/logger/__init__.py b/homeassistant/components/logger/__init__.py index fb2920fb6e25d..c7660f2a3f009 100644 --- a/homeassistant/components/logger/__init__.py +++ b/homeassistant/components/logger/__init__.py @@ -114,7 +114,7 @@ def _add_log_filter(logger, patterns): """Add a Filter to the logger based on a regexp of the filter_str.""" def filter_func(logrecord): - return not any(p.match(logrecord.getMessage()) for p in patterns) + return not any(p.search(logrecord.getMessage()) for p in patterns) logger.addFilter(filter_func) diff --git a/tests/components/logger/test_init.py b/tests/components/logger/test_init.py index d2b0e8931b690..6435ef9539421 100644 --- a/tests/components/logger/test_init.py +++ b/tests/components/logger/test_init.py @@ -42,6 +42,7 @@ async def test_log_filtering(hass, caplog): "doesntmatchanything", ".*shouldfilterall.*", "^filterthis:.*", + "in the middle", ], "test.other_filter": [".*otherfilterer"], }, @@ -62,6 +63,7 @@ def msg_test(logger, result, message, *args): filter_logger, False, "this line containing shouldfilterall should be filtered" ) msg_test(filter_logger, True, "this line should not be filtered filterthis:") + msg_test(filter_logger, False, "this in the middle should be filtered") msg_test(filter_logger, False, "filterthis: should be filtered") msg_test(filter_logger, False, "format string shouldfilter%s", "all") msg_test(filter_logger, True, "format string shouldfilter%s", "not") From 42156bafe0e2e0da3b7327a5f72353da25f00a97 Mon Sep 17 00:00:00 2001 From: Nicolas Braem Date: Sat, 10 Apr 2021 23:02:08 +0200 Subject: [PATCH 0184/1317] Change vicare unit of power production current to POWER_WATT (#49000) --- homeassistant/components/vicare/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/vicare/sensor.py b/homeassistant/components/vicare/sensor.py index c988b2a4086fb..d493751ffa5ff 100644 --- a/homeassistant/components/vicare/sensor.py +++ b/homeassistant/components/vicare/sensor.py @@ -12,8 +12,8 @@ DEVICE_CLASS_ENERGY, DEVICE_CLASS_TEMPERATURE, ENERGY_KILO_WATT_HOUR, - ENERGY_WATT_HOUR, PERCENTAGE, + POWER_WATT, TEMP_CELSIUS, TIME_HOURS, ) @@ -229,7 +229,7 @@ SENSOR_POWER_PRODUCTION_CURRENT: { CONF_NAME: "Power production current", CONF_ICON: None, - CONF_UNIT_OF_MEASUREMENT: ENERGY_WATT_HOUR, + CONF_UNIT_OF_MEASUREMENT: POWER_WATT, CONF_GETTER: lambda api: api.getPowerProductionCurrent(), CONF_DEVICE_CLASS: DEVICE_CLASS_ENERGY, }, From 45a92f5791ab5fcd557763a95aef799fffcb8f4e Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sun, 11 Apr 2021 00:04:41 +0000 Subject: [PATCH 0185/1317] [ci skip] Translation update --- .../components/broadlink/translations/de.json | 3 +- .../components/cast/translations/fr.json | 4 +- .../components/climacell/translations/fr.json | 1 + .../components/deconz/translations/fr.json | 4 ++ .../components/emonitor/translations/fr.json | 23 ++++++++ .../enphase_envoy/translations/fr.json | 22 ++++++++ .../components/ezviz/translations/de.json | 28 ++++++++++ .../components/ezviz/translations/fr.json | 52 +++++++++++++++++++ .../components/ezviz/translations/nl.json | 52 +++++++++++++++++++ .../components/ezviz/translations/ru.json | 47 ++++++++++++++++- .../ezviz/translations/zh-Hant.json | 52 +++++++++++++++++++ .../google_travel_time/translations/fr.json | 19 ++++++- .../components/kodi/translations/de.json | 3 +- .../components/konnected/translations/de.json | 1 + .../kostal_plenticore/translations/fr.json | 21 ++++++++ .../components/met/translations/fr.json | 3 ++ .../met_eireann/translations/fr.json | 19 +++++++ .../components/nuki/translations/fr.json | 10 ++++ .../opentherm_gw/translations/fr.json | 3 +- .../components/roomba/translations/fr.json | 2 +- .../components/shelly/translations/de.json | 3 ++ .../components/spotify/translations/de.json | 3 +- .../waze_travel_time/translations/fr.json | 38 ++++++++++++++ .../components/zha/translations/fr.json | 1 + 24 files changed, 405 insertions(+), 9 deletions(-) create mode 100644 homeassistant/components/emonitor/translations/fr.json create mode 100644 homeassistant/components/enphase_envoy/translations/fr.json create mode 100644 homeassistant/components/ezviz/translations/de.json create mode 100644 homeassistant/components/ezviz/translations/fr.json create mode 100644 homeassistant/components/ezviz/translations/nl.json create mode 100644 homeassistant/components/ezviz/translations/zh-Hant.json create mode 100644 homeassistant/components/kostal_plenticore/translations/fr.json create mode 100644 homeassistant/components/met_eireann/translations/fr.json create mode 100644 homeassistant/components/waze_travel_time/translations/fr.json diff --git a/homeassistant/components/broadlink/translations/de.json b/homeassistant/components/broadlink/translations/de.json index 5704efe37c623..7ad3ab95ec9a2 100644 --- a/homeassistant/components/broadlink/translations/de.json +++ b/homeassistant/components/broadlink/translations/de.json @@ -25,7 +25,8 @@ }, "user": { "data": { - "host": "Host" + "host": "Host", + "timeout": "Zeit\u00fcberschreitung" } } } diff --git a/homeassistant/components/cast/translations/fr.json b/homeassistant/components/cast/translations/fr.json index 0acfd327e3e2e..f5ee03a6c0079 100644 --- a/homeassistant/components/cast/translations/fr.json +++ b/homeassistant/components/cast/translations/fr.json @@ -27,7 +27,9 @@ "step": { "options": { "data": { - "known_hosts": "Liste facultative des h\u00f4tes connus si la d\u00e9couverte mDNS ne fonctionne pas." + "ignore_cec": "Liste facultative qui sera transmise \u00e0 pychromecast.IGNORE_CEC.", + "known_hosts": "Liste facultative des h\u00f4tes connus si la d\u00e9couverte mDNS ne fonctionne pas.", + "uuid": "Liste facultative des UUID. Les moulages non r\u00e9pertori\u00e9s ne seront pas ajout\u00e9s." }, "description": "Veuillez saisir la configuration de Google Cast." } diff --git a/homeassistant/components/climacell/translations/fr.json b/homeassistant/components/climacell/translations/fr.json index 3b3aa3d18babf..c0e8d5b88a487 100644 --- a/homeassistant/components/climacell/translations/fr.json +++ b/homeassistant/components/climacell/translations/fr.json @@ -10,6 +10,7 @@ "user": { "data": { "api_key": "Cl\u00e9 d'API", + "api_version": "Version de l'API", "latitude": "Latitude", "longitude": "Longitude", "name": "Nom" diff --git a/homeassistant/components/deconz/translations/fr.json b/homeassistant/components/deconz/translations/fr.json index d24b592ac1029..05d53405e54df 100644 --- a/homeassistant/components/deconz/translations/fr.json +++ b/homeassistant/components/deconz/translations/fr.json @@ -42,6 +42,10 @@ "button_2": "Deuxi\u00e8me bouton", "button_3": "Troisi\u00e8me bouton", "button_4": "Quatri\u00e8me bouton", + "button_5": "5\u00e8me bouton", + "button_6": "6\u00e8me bouton", + "button_7": "7\u00e8me bouton", + "button_8": "8\u00e8me bouton", "close": "Ferm\u00e9", "dim_down": "Assombrir", "dim_up": "\u00c9claircir", diff --git a/homeassistant/components/emonitor/translations/fr.json b/homeassistant/components/emonitor/translations/fr.json new file mode 100644 index 0000000000000..fcfee3bc71083 --- /dev/null +++ b/homeassistant/components/emonitor/translations/fr.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "unknown": "Erreur inattendue" + }, + "flow_title": "SiteSage {name}", + "step": { + "confirm": { + "description": "Voulez-vous configurer {name} ( {host} )?", + "title": "Configurer SiteSage Emonitor" + }, + "user": { + "data": { + "host": "Hote" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/fr.json b/homeassistant/components/enphase_envoy/translations/fr.json new file mode 100644 index 0000000000000..be1d5f3bca3df --- /dev/null +++ b/homeassistant/components/enphase_envoy/translations/fr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification invalide", + "unknown": "Erreur inattendue" + }, + "flow_title": "Envoy\u00e9 {serial} ({host})", + "step": { + "user": { + "data": { + "host": "Hote", + "password": "Mot de passe", + "username": "Nom d'utilisateur" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ezviz/translations/de.json b/homeassistant/components/ezviz/translations/de.json new file mode 100644 index 0000000000000..b849a7f231a9b --- /dev/null +++ b/homeassistant/components/ezviz/translations/de.json @@ -0,0 +1,28 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Passwort", + "username": "Benutzername" + }, + "title": "Verbinden mit Ezviz Cloud" + }, + "user_custom_url": { + "data": { + "password": "Passwort", + "username": "Benutzername" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "Anfrage-Timeout (Sekunden)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ezviz/translations/fr.json b/homeassistant/components/ezviz/translations/fr.json new file mode 100644 index 0000000000000..216cf73c7b755 --- /dev/null +++ b/homeassistant/components/ezviz/translations/fr.json @@ -0,0 +1,52 @@ +{ + "config": { + "abort": { + "already_configured_account": "Le compte est d\u00e9j\u00e0 configur\u00e9", + "ezviz_cloud_account_missing": "Compte cloud Ezviz manquant. Veuillez reconfigurer le compte cloud Ezviz", + "unknown": "Erreur inattendue" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification invalide", + "invalid_host": "Nom d'h\u00f4te ou adresse IP non valide" + }, + "flow_title": "{serial}", + "step": { + "confirm": { + "data": { + "password": "Mot de passe", + "username": "Identifiant" + }, + "description": "Entrez les informations d'identification RTSP pour la cam\u00e9ra Ezviz {serial} avec IP {ip_address}", + "title": "Cam\u00e9ra Ezviz d\u00e9couverte" + }, + "user": { + "data": { + "password": "Mot de passe", + "url": "URL", + "username": "Identifiant" + }, + "title": "Connectez-vous \u00e0 Ezviz Cloud" + }, + "user_custom_url": { + "data": { + "password": "Mot de passe", + "url": "URL", + "username": "Identifiant" + }, + "description": "Sp\u00e9cifiez manuellement l'URL de votre r\u00e9gion", + "title": "Connectez-vous \u00e0 l'URL Ezviz personnalis\u00e9e" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ffmpeg_arguments": "Arguments transmis \u00e0 ffmpeg pour les cam\u00e9ras", + "timeout": "D\u00e9lai d'expiration de la demande (secondes)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ezviz/translations/nl.json b/homeassistant/components/ezviz/translations/nl.json new file mode 100644 index 0000000000000..a6f7b3e985ce5 --- /dev/null +++ b/homeassistant/components/ezviz/translations/nl.json @@ -0,0 +1,52 @@ +{ + "config": { + "abort": { + "already_configured_account": "Account is al geconfigureerd", + "ezviz_cloud_account_missing": "Ezviz-cloudaccount ontbreekt. Configureer het Ezviz-cloudaccount opnieuw", + "unknown": "Onverwachte fout" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "invalid_host": "Ongeldige hostnaam of IP-adres" + }, + "flow_title": "{serial}", + "step": { + "confirm": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam" + }, + "description": "Voer RTSP-gegevens in voor Ezviz camera {serial} met IP {ip_address}", + "title": "Ontdekt Ezviz Camera" + }, + "user": { + "data": { + "password": "Wachtwoord", + "url": "URL", + "username": "Gebruikersnaam" + }, + "title": "Verbind met Ezviz Cloud" + }, + "user_custom_url": { + "data": { + "password": "Wachtwoord", + "url": "URL", + "username": "Gebruikersnaam" + }, + "description": "Geef handmatig de URL van uw regio op", + "title": "Verbind met aangepast Elvis URL" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ffmpeg_arguments": "Argumenten doorgegeven aan ffmpeg voor camera's", + "timeout": "Time-out aanvraag (seconden)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ezviz/translations/ru.json b/homeassistant/components/ezviz/translations/ru.json index f047b071be4c6..c03bbe22daebf 100644 --- a/homeassistant/components/ezviz/translations/ru.json +++ b/homeassistant/components/ezviz/translations/ru.json @@ -1,7 +1,52 @@ { "config": { "abort": { - "already_configured_account": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." + "already_configured_account": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "ezviz_cloud_account_missing": "\u041e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0443\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c Ezviz Cloud. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0435\u0440\u0435\u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0435\u0451.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441." + }, + "flow_title": "{serial}", + "step": { + "confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 RTSP \u0434\u043b\u044f \u043a\u0430\u043c\u0435\u0440\u044b Ezviz {serial} \u0441 IP-\u0430\u0434\u0440\u0435\u0441\u043e\u043c {ip_address}", + "title": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043d\u0430\u044f \u043a\u0430\u043c\u0435\u0440\u0430 Ezviz" + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "url": "URL-\u0430\u0434\u0440\u0435\u0441", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + }, + "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a Ezviz Cloud" + }, + "user_custom_url": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "url": "URL-\u0430\u0434\u0440\u0435\u0441", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + }, + "description": "\u0423\u043a\u0430\u0436\u0438\u0442\u0435 URL-\u0430\u0434\u0440\u0435\u0441 \u0434\u043b\u044f \u0412\u0430\u0448\u0435\u0433\u043e \u0440\u0435\u0433\u0438\u043e\u043d\u0430 \u0432\u0440\u0443\u0447\u043d\u0443\u044e.", + "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u043e\u043c\u0443 URL-\u0430\u0434\u0440\u0435\u0441\u0443 Ezviz" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ffmpeg_arguments": "\u0410\u0440\u0433\u0443\u043c\u0435\u043d\u0442\u044b, \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u043d\u044b\u0435 \u0432 ffmpeg \u0434\u043b\u044f \u043a\u0430\u043c\u0435\u0440", + "timeout": "\u0422\u0430\u0439\u043c-\u0430\u0443\u0442 \u0437\u0430\u043f\u0440\u043e\u0441\u0430 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/ezviz/translations/zh-Hant.json b/homeassistant/components/ezviz/translations/zh-Hant.json new file mode 100644 index 0000000000000..84c5daf14c35d --- /dev/null +++ b/homeassistant/components/ezviz/translations/zh-Hant.json @@ -0,0 +1,52 @@ +{ + "config": { + "abort": { + "already_configured_account": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "ezviz_cloud_account_missing": "\u627e\u4e0d\u5230 Ezviz \u96f2\u5e33\u865f\u3002\u8acb\u91cd\u65b0\u8a2d\u5b9a Ezviz \u96f2\u5e33\u865f", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "invalid_host": "\u7121\u6548\u4e3b\u6a5f\u540d\u7a31\u6216 IP \u4f4d\u5740" + }, + "flow_title": "{serial}", + "step": { + "confirm": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "\u8f38\u5165 IP \u70ba {ip_address} \u7684 Ezviz \u651d\u5f71\u6a5f {serial} RTSP \u6191\u8b49", + "title": "\u81ea\u52d5\u641c\u7d22\u5230\u7684 Ezviz \u651d\u5f71\u6a5f" + }, + "user": { + "data": { + "password": "\u5bc6\u78bc", + "url": "\u7db2\u5740", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "title": "\u9023\u7dda\u81f3 Ezviz \u87a2\u77f3\u96f2" + }, + "user_custom_url": { + "data": { + "password": "\u5bc6\u78bc", + "url": "\u7db2\u5740", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "\u624b\u52d5\u6307\u5b9a\u5340\u57df URL", + "title": "\u9023\u7dda\u81f3\u81ea\u8a02 Ezviz URL" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ffmpeg_arguments": "\u50b3\u905e\u81f3 ffmpeg \u4e4b\u651d\u5f71\u6a5f\u53c3\u6578", + "timeout": "\u8acb\u6c42\u903e\u6642\uff08\u79d2\uff09" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_travel_time/translations/fr.json b/homeassistant/components/google_travel_time/translations/fr.json index b5b59b5329c61..8a4ecc6ac8300 100644 --- a/homeassistant/components/google_travel_time/translations/fr.json +++ b/homeassistant/components/google_travel_time/translations/fr.json @@ -1,11 +1,19 @@ { "config": { + "abort": { + "already_configured": "L'emplacement est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion" + }, "step": { "user": { "data": { "api_key": "common::config_flow::data::api_key", + "destination": "Destination", "origin": "Origine" - } + }, + "description": "Lorsque vous sp\u00e9cifiez l'origine et la destination, vous pouvez fournir un ou plusieurs emplacements s\u00e9par\u00e9s par le caract\u00e8re de tuyau, sous la forme d'une adresse, de coordonn\u00e9es de latitude / longitude ou d'un identifiant de lieu Google. Lorsque vous sp\u00e9cifiez l'emplacement \u00e0 l'aide d'un identifiant de lieu Google, l'identifiant doit \u00eatre pr\u00e9c\u00e9d\u00e9 de `place_id:`." } } }, @@ -13,9 +21,16 @@ "step": { "init": { "data": { + "avoid": "\u00c9viter de", "language": "Langue", + "mode": "Mode voyage", + "time": "Temps", + "time_type": "Type de temps", + "transit_mode": "Mode de transit", + "transit_routing_preference": "Pr\u00e9f\u00e9rence de routage de transport en commun", "units": "Unit\u00e9s" - } + }, + "description": "Vous pouvez \u00e9ventuellement sp\u00e9cifier une heure de d\u00e9part ou une heure d'arriv\u00e9e. Si vous sp\u00e9cifiez une heure de d\u00e9part, vous pouvez entrer \u00abnow\u00bb, un horodatage Unix ou une cha\u00eene de 24 heures comme \u00ab08: 00: 00\u00bb. Si vous sp\u00e9cifiez une heure d'arriv\u00e9e, vous pouvez utiliser un horodatage Unix ou une cha\u00eene de 24 heures comme `08: 00: 00`" } } }, diff --git a/homeassistant/components/kodi/translations/de.json b/homeassistant/components/kodi/translations/de.json index 15fd212fdbd92..5f2badfd78d9e 100644 --- a/homeassistant/components/kodi/translations/de.json +++ b/homeassistant/components/kodi/translations/de.json @@ -29,7 +29,8 @@ "host": "Host", "port": "Port", "ssl": "Verwendet ein SSL Zertifikat" - } + }, + "description": "Kodi-Verbindungsinformationen. Bitte stellen Sie sicher, dass Sie \"Steuerung von Kodi \u00fcber HTTP zulassen\" in System/Einstellungen/Netzwerk/Dienste aktivieren." }, "ws_port": { "data": { diff --git a/homeassistant/components/konnected/translations/de.json b/homeassistant/components/konnected/translations/de.json index 2ec1657990b22..7938f1a68bde2 100644 --- a/homeassistant/components/konnected/translations/de.json +++ b/homeassistant/components/konnected/translations/de.json @@ -86,6 +86,7 @@ "options_misc": { "data": { "api_host": "API-Host-URL \u00fcberschreiben (optional)", + "blink": "LED Panel blinkt beim senden von Status\u00e4nderungen", "override_api_host": "\u00dcberschreiben Sie die Standard-Host-Panel-URL der Home Assistant-API" }, "description": "Bitte w\u00e4hlen Sie das gew\u00fcnschte Verhalten f\u00fcr Ihr Panel", diff --git a/homeassistant/components/kostal_plenticore/translations/fr.json b/homeassistant/components/kostal_plenticore/translations/fr.json new file mode 100644 index 0000000000000..08a75486d7f21 --- /dev/null +++ b/homeassistant/components/kostal_plenticore/translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Erreur inattendue", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "host": "Hote", + "password": "Mot de passe" + } + } + } + }, + "title": "Onduleur solaire Kostal Plenticore" +} \ No newline at end of file diff --git a/homeassistant/components/met/translations/fr.json b/homeassistant/components/met/translations/fr.json index dbf72959799b8..a415779d3c184 100644 --- a/homeassistant/components/met/translations/fr.json +++ b/homeassistant/components/met/translations/fr.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_home": "Aucune coordonn\u00e9e du domicile n'est d\u00e9finie dans la configuration de Home Assistant" + }, "error": { "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9" }, diff --git a/homeassistant/components/met_eireann/translations/fr.json b/homeassistant/components/met_eireann/translations/fr.json new file mode 100644 index 0000000000000..da13cc6cb5984 --- /dev/null +++ b/homeassistant/components/met_eireann/translations/fr.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9" + }, + "step": { + "user": { + "data": { + "elevation": "Altitude", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Nom" + }, + "description": "Entrez votre emplacement pour utiliser les donn\u00e9es m\u00e9t\u00e9orologiques de l'API Met \u00c9ireann", + "title": "Emplacement" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuki/translations/fr.json b/homeassistant/components/nuki/translations/fr.json index 035c07325766c..248acf70133c1 100644 --- a/homeassistant/components/nuki/translations/fr.json +++ b/homeassistant/components/nuki/translations/fr.json @@ -1,11 +1,21 @@ { "config": { + "abort": { + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" + }, "error": { "cannot_connect": "\u00c9chec de la connexion ", "invalid_auth": "Authentification invalide ", "unknown": "Erreur inattendue" }, "step": { + "reauth_confirm": { + "data": { + "token": "Jeton d'acc\u00e8s" + }, + "description": "L'int\u00e9gration Nuki doit s'authentifier de nouveau avec votre pont.", + "title": "R\u00e9authentifier l'int\u00e9gration" + }, "user": { "data": { "host": "Hote", diff --git a/homeassistant/components/opentherm_gw/translations/fr.json b/homeassistant/components/opentherm_gw/translations/fr.json index 7cc5b4ef84890..c9a19eba3ddf8 100644 --- a/homeassistant/components/opentherm_gw/translations/fr.json +++ b/homeassistant/components/opentherm_gw/translations/fr.json @@ -23,7 +23,8 @@ "floor_temperature": "Temp\u00e9rature du sol", "precision": "Pr\u00e9cision", "read_precision": "Pr\u00e9cision de lecture", - "set_precision": "D\u00e9finir la pr\u00e9cision" + "set_precision": "D\u00e9finir la pr\u00e9cision", + "temporary_override_mode": "Mode de neutralisation du point de consigne temporaire" }, "description": "Options pour la passerelle OpenTherm" } diff --git a/homeassistant/components/roomba/translations/fr.json b/homeassistant/components/roomba/translations/fr.json index 1f0e0b029c0c4..767d7a9708a83 100644 --- a/homeassistant/components/roomba/translations/fr.json +++ b/homeassistant/components/roomba/translations/fr.json @@ -34,7 +34,7 @@ "blid": "BLID", "host": "H\u00f4te" }, - "description": "Aucun Roomba ou Braava d\u00e9couvert sur votre r\u00e9seau. Le BLID est la partie du nom d'h\u00f4te du p\u00e9riph\u00e9rique apr\u00e8s `iRobot-`. Veuillez suivre les \u00e9tapes d\u00e9crites dans la documentation \u00e0 {auth_help_url}", + "description": "Aucun Roomba ou Braava d\u00e9couvert sur votre r\u00e9seau. Le BLID est la partie du nom d'h\u00f4te du p\u00e9riph\u00e9rique apr\u00e8s `iRobot-`. Veuillez suivre les \u00e9tapes d\u00e9crites dans la documentation \u00e0 {auth_help_url}\u00b4", "title": "Se connecter manuellement \u00e0 l'appareil" }, "user": { diff --git a/homeassistant/components/shelly/translations/de.json b/homeassistant/components/shelly/translations/de.json index 9d78d362c99c7..7e7cdb89f6620 100644 --- a/homeassistant/components/shelly/translations/de.json +++ b/homeassistant/components/shelly/translations/de.json @@ -11,6 +11,9 @@ }, "flow_title": "Shelly: {name}", "step": { + "confirm_discovery": { + "description": "M\u00f6chten Sie das {Modell} bei {Host} einrichten?\n\nBatteriebetriebene Ger\u00e4te, die passwortgesch\u00fctzt sind, m\u00fcssen aufgeweckt werden, bevor Sie mit dem Einrichten fortfahren.\nBatteriebetriebene Ger\u00e4te, die nicht passwortgesch\u00fctzt sind, werden hinzugef\u00fcgt, wenn das Ger\u00e4t aufwacht. Sie k\u00f6nnen das Ger\u00e4t nun manuell \u00fcber eine Taste am Ger\u00e4t aufwecken oder auf das n\u00e4chste Datenupdate des Ger\u00e4ts warten." + }, "credentials": { "data": { "password": "Passwort", diff --git a/homeassistant/components/spotify/translations/de.json b/homeassistant/components/spotify/translations/de.json index 281803ec66ed9..db9363ec1f70a 100644 --- a/homeassistant/components/spotify/translations/de.json +++ b/homeassistant/components/spotify/translations/de.json @@ -3,7 +3,8 @@ "abort": { "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", "missing_configuration": "Die Spotify-Integration ist nicht konfiguriert. Bitte folgen Sie der Dokumentation.", - "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url})." + "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url}).", + "reauth_account_mismatch": "Das Spotify-Konto, mit dem Sie sich authentifiziert haben, stimmt nicht mit dem Konto \u00fcberein, f\u00fcr das Sie sich erneut authentifizieren m\u00fcssen." }, "create_entry": { "default": "Erfolgreich mit Spotify authentifiziert." diff --git a/homeassistant/components/waze_travel_time/translations/fr.json b/homeassistant/components/waze_travel_time/translations/fr.json new file mode 100644 index 0000000000000..8b977e76d0856 --- /dev/null +++ b/homeassistant/components/waze_travel_time/translations/fr.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "L'emplacement est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "Echec de la connection" + }, + "step": { + "user": { + "data": { + "destination": "Destination", + "origin": "Point de d\u00e9part", + "region": "R\u00e9gion" + }, + "description": "Pour le Point de D\u00e9part et la Destination, entrez l'adresse ou les coordonn\u00e9es GPS de l'emplacement (les coordonn\u00e9es GPS doivent \u00eatre s\u00e9par\u00e9es par une virgule). Vous pouvez \u00e9galement entrer l'ID d'une entit\u00e9 qui fournit ces informations dans son \u00e9tat, un ID d'entit\u00e9 avec des attributs de latitude et de longitude ou un nom de zone." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "avoid_ferries": "\u00c9viter les ferries?", + "avoid_subscription_roads": "\u00c9viter les routes n\u00e9cessitant une vignette / un abonnement?", + "avoid_toll_roads": "\u00c9viter les routes \u00e0 p\u00e9age ?", + "excl_filter": "Sous-cha\u00eene NON dans la description de l'itin\u00e9raire s\u00e9lectionn\u00e9", + "incl_filter": "Sous-cha\u00eene dans la description de l'itin\u00e9raire s\u00e9lectionn\u00e9", + "realtime": "Temps de trajet en temps r\u00e9el?", + "units": "Unit\u00e9s", + "vehicle_type": "Type de v\u00e9hicule" + }, + "description": "Les entr\u00e9es `substring` vous permettront de forcer l'int\u00e9gration \u00e0 utiliser un itin\u00e9raire particulier ou d'\u00e9viter un itin\u00e9raire particulier dans son calcul de voyage dans le temps." + } + } + }, + "title": "Temps de trajet Waze" +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/fr.json b/homeassistant/components/zha/translations/fr.json index 1abfb3a95025a..9e35ef9a541dd 100644 --- a/homeassistant/components/zha/translations/fr.json +++ b/homeassistant/components/zha/translations/fr.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "Impossible de se connecter au p\u00e9riph\u00e9rique ZHA." }, + "flow_title": "ZHA: {name}", "step": { "pick_radio": { "data": { From e68678e336a517d33116a8650023891308af9436 Mon Sep 17 00:00:00 2001 From: Ben Hale Date: Sat, 10 Apr 2021 19:19:31 -0700 Subject: [PATCH 0186/1317] Upgrade aioambient to 1.2.4 (#49035) --- homeassistant/components/ambient_station/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ambient_station/manifest.json b/homeassistant/components/ambient_station/manifest.json index 916f1378fd0d2..51f6703ba5ccd 100644 --- a/homeassistant/components/ambient_station/manifest.json +++ b/homeassistant/components/ambient_station/manifest.json @@ -3,6 +3,6 @@ "name": "Ambient Weather Station", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ambient_station", - "requirements": ["aioambient==1.2.1"], + "requirements": ["aioambient==1.2.4"], "codeowners": ["@bachya"] } diff --git a/requirements_all.txt b/requirements_all.txt index 36c125dc92511..65e72b42535ea 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -135,7 +135,7 @@ aio_geojson_nsw_rfs_incidents==0.3 aio_georss_gdacs==0.4 # homeassistant.components.ambient_station -aioambient==1.2.1 +aioambient==1.2.4 # homeassistant.components.asuswrt aioasuswrt==1.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2b7a572bcc6d3..c8868b00b0a60 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -75,7 +75,7 @@ aio_geojson_nsw_rfs_incidents==0.3 aio_georss_gdacs==0.4 # homeassistant.components.ambient_station -aioambient==1.2.1 +aioambient==1.2.4 # homeassistant.components.asuswrt aioasuswrt==1.3.1 From 62182ea460e3d4f334805b0cf63336a442afbf3a Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sun, 11 Apr 2021 08:42:32 +0200 Subject: [PATCH 0187/1317] Bump ha-philipsjs to 2.7.0 (#49008) This has some improvements to not consider the TV off due to some exceptions that is related to API being buggy rather than off. --- homeassistant/components/philips_js/__init__.py | 6 +++++- homeassistant/components/philips_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/philips_js/__init__.py b/homeassistant/components/philips_js/__init__.py index 7be5efeaf2fd7..b585451cdb076 100644 --- a/homeassistant/components/philips_js/__init__.py +++ b/homeassistant/components/philips_js/__init__.py @@ -134,8 +134,12 @@ def _update_listeners(): async def _notify_task(self): while self.api.on and self.api.notify_change_supported: - if await self.api.notifyChange(130): + res = await self.api.notifyChange(130) + if res: self.async_set_updated_data(None) + elif res is None: + LOGGER.debug("Aborting notify due to unexpected return") + break @callback def _async_notify_stop(self): diff --git a/homeassistant/components/philips_js/manifest.json b/homeassistant/components/philips_js/manifest.json index ad591ad330bdc..36e01d8f3c8a6 100644 --- a/homeassistant/components/philips_js/manifest.json +++ b/homeassistant/components/philips_js/manifest.json @@ -3,7 +3,7 @@ "name": "Philips TV", "documentation": "https://www.home-assistant.io/integrations/philips_js", "requirements": [ - "ha-philipsjs==2.3.2" + "ha-philipsjs==2.7.0" ], "codeowners": [ "@elupus" diff --git a/requirements_all.txt b/requirements_all.txt index 65e72b42535ea..63935c4b0aebd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -720,7 +720,7 @@ guppy3==3.1.0 ha-ffmpeg==3.0.2 # homeassistant.components.philips_js -ha-philipsjs==2.3.2 +ha-philipsjs==2.7.0 # homeassistant.components.habitica habitipy==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c8868b00b0a60..662398d02597b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -393,7 +393,7 @@ guppy3==3.1.0 ha-ffmpeg==3.0.2 # homeassistant.components.philips_js -ha-philipsjs==2.3.2 +ha-philipsjs==2.7.0 # homeassistant.components.habitica habitipy==0.2.0 From 71a410c742ee7946de5d39a20af9a40f4bd26de9 Mon Sep 17 00:00:00 2001 From: Nicolas Braem Date: Sun, 11 Apr 2021 10:52:28 +0200 Subject: [PATCH 0188/1317] Correct vicare power production device class (#49040) --- homeassistant/components/vicare/sensor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/vicare/sensor.py b/homeassistant/components/vicare/sensor.py index d493751ffa5ff..7d224de3835ca 100644 --- a/homeassistant/components/vicare/sensor.py +++ b/homeassistant/components/vicare/sensor.py @@ -10,6 +10,7 @@ CONF_NAME, CONF_UNIT_OF_MEASUREMENT, DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, ENERGY_KILO_WATT_HOUR, PERCENTAGE, @@ -231,7 +232,7 @@ CONF_ICON: None, CONF_UNIT_OF_MEASUREMENT: POWER_WATT, CONF_GETTER: lambda api: api.getPowerProductionCurrent(), - CONF_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + CONF_DEVICE_CLASS: DEVICE_CLASS_POWER, }, SENSOR_POWER_PRODUCTION_TODAY: { CONF_NAME: "Power production today", From e38fce98c4cfaf3eed0bb7fcadd9a55a49108b9e Mon Sep 17 00:00:00 2001 From: Phil Hollenback Date: Sun, 11 Apr 2021 02:13:07 -0700 Subject: [PATCH 0189/1317] Fix non-metric atmospheric pressure in Open Weather Map (#49030) The openweathermap component retrieves atmospheric pressure from the openweathermap api and passes it along without checking the units. The api returns pressure in metric (hPa). If you the use the weather forecast card on a non-metric home assistant install, you will then see the pressure reported as something like '1019 inHg', which is an incorrect combination of metric value and non-metric label. To fix this, check when retrieving the pressure if this is a metric system. If not, convert the value to non-metric inHg before sending it along. Weirdly, this isn't a problem for temperature, so I suspect temp is getting converted somewhere else. --- homeassistant/components/openweathermap/weather.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/openweathermap/weather.py b/homeassistant/components/openweathermap/weather.py index 7908beb61d689..63d63c3014716 100644 --- a/homeassistant/components/openweathermap/weather.py +++ b/homeassistant/components/openweathermap/weather.py @@ -1,6 +1,7 @@ """Support for the OpenWeatherMap (OWM) service.""" from homeassistant.components.weather import WeatherEntity -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import PRESSURE_HPA, PRESSURE_INHG, TEMP_CELSIUS +from homeassistant.util.pressure import convert as pressure_convert from .const import ( ATTR_API_CONDITION, @@ -82,7 +83,12 @@ def temperature_unit(self): @property def pressure(self): """Return the pressure.""" - return self._weather_coordinator.data[ATTR_API_PRESSURE] + pressure = self._weather_coordinator.data[ATTR_API_PRESSURE] + # OpenWeatherMap returns pressure in hPA, so convert to + # inHg if we aren't using metric. + if not self.hass.config.units.is_metric and pressure: + return pressure_convert(pressure, PRESSURE_HPA, PRESSURE_INHG) + return pressure @property def humidity(self): From 34a1dd4120ce9f8edc0e31e94719a462b23f82ea Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sun, 11 Apr 2021 05:59:42 -0400 Subject: [PATCH 0190/1317] Add new attributes to Climacell (#48707) * Add new attributes to Climacell * fix logic * test new properties --- .../components/climacell/__init__.py | 12 +++ homeassistant/components/climacell/const.py | 11 +++ homeassistant/components/climacell/weather.py | 92 ++++++++++++++++++- tests/components/climacell/test_weather.py | 14 ++- tests/fixtures/climacell/v3_realtime.json | 11 +++ tests/fixtures/climacell/v4.json | 5 +- 6 files changed, 140 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/climacell/__init__.py b/homeassistant/components/climacell/__init__.py index 8095f7991bde5..39412520653d0 100644 --- a/homeassistant/components/climacell/__init__.py +++ b/homeassistant/components/climacell/__init__.py @@ -35,28 +35,34 @@ from .const import ( ATTRIBUTION, + CC_ATTR_CLOUD_COVER, CC_ATTR_CONDITION, CC_ATTR_HUMIDITY, CC_ATTR_OZONE, CC_ATTR_PRECIPITATION, CC_ATTR_PRECIPITATION_PROBABILITY, + CC_ATTR_PRECIPITATION_TYPE, CC_ATTR_PRESSURE, CC_ATTR_TEMPERATURE, CC_ATTR_TEMPERATURE_HIGH, CC_ATTR_TEMPERATURE_LOW, CC_ATTR_VISIBILITY, CC_ATTR_WIND_DIRECTION, + CC_ATTR_WIND_GUST, CC_ATTR_WIND_SPEED, + CC_V3_ATTR_CLOUD_COVER, CC_V3_ATTR_CONDITION, CC_V3_ATTR_HUMIDITY, CC_V3_ATTR_OZONE, CC_V3_ATTR_PRECIPITATION, CC_V3_ATTR_PRECIPITATION_DAILY, CC_V3_ATTR_PRECIPITATION_PROBABILITY, + CC_V3_ATTR_PRECIPITATION_TYPE, CC_V3_ATTR_PRESSURE, CC_V3_ATTR_TEMPERATURE, CC_V3_ATTR_VISIBILITY, CC_V3_ATTR_WIND_DIRECTION, + CC_V3_ATTR_WIND_GUST, CC_V3_ATTR_WIND_SPEED, CONF_TIMESTEP, DEFAULT_FORECAST_TYPE, @@ -223,6 +229,9 @@ async def _async_update_data(self) -> dict[str, Any]: CC_V3_ATTR_CONDITION, CC_V3_ATTR_VISIBILITY, CC_V3_ATTR_OZONE, + CC_V3_ATTR_WIND_GUST, + CC_V3_ATTR_CLOUD_COVER, + CC_V3_ATTR_PRECIPITATION_TYPE, ] ) data[FORECASTS][HOURLY] = await self._api.forecast_hourly( @@ -276,6 +285,9 @@ async def _async_update_data(self) -> dict[str, Any]: CC_ATTR_CONDITION, CC_ATTR_VISIBILITY, CC_ATTR_OZONE, + CC_ATTR_WIND_GUST, + CC_ATTR_CLOUD_COVER, + CC_ATTR_PRECIPITATION_TYPE, ], [ CC_ATTR_TEMPERATURE_LOW, diff --git a/homeassistant/components/climacell/const.py b/homeassistant/components/climacell/const.py index 01d85dcc16136..6d451fa6f066a 100644 --- a/homeassistant/components/climacell/const.py +++ b/homeassistant/components/climacell/const.py @@ -35,6 +35,11 @@ NOWCAST: 30, } +# Additional attributes +ATTR_WIND_GUST = "wind_gust" +ATTR_CLOUD_COVER = "cloud_cover" +ATTR_PRECIPITATION_TYPE = "precipitation_type" + # V4 constants CONDITIONS = { WeatherCode.WIND: ATTR_CONDITION_WINDY, @@ -76,6 +81,9 @@ CC_ATTR_VISIBILITY = "visibility" CC_ATTR_PRECIPITATION = "precipitationIntensityAvg" CC_ATTR_PRECIPITATION_PROBABILITY = "precipitationProbability" +CC_ATTR_WIND_GUST = "windGust" +CC_ATTR_CLOUD_COVER = "cloudCover" +CC_ATTR_PRECIPITATION_TYPE = "precipitationType" # V3 constants CONDITIONS_V3 = { @@ -117,3 +125,6 @@ CC_V3_ATTR_PRECIPITATION = "precipitation" CC_V3_ATTR_PRECIPITATION_DAILY = "precipitation_accumulation" CC_V3_ATTR_PRECIPITATION_PROBABILITY = "precipitation_probability" +CC_V3_ATTR_WIND_GUST = "wind_gust" +CC_V3_ATTR_CLOUD_COVER = "cloud_cover" +CC_V3_ATTR_PRECIPITATION_TYPE = "precipitation_type" diff --git a/homeassistant/components/climacell/weather.py b/homeassistant/components/climacell/weather.py index 012f987171ed3..0808a4bd7344d 100644 --- a/homeassistant/components/climacell/weather.py +++ b/homeassistant/components/climacell/weather.py @@ -3,9 +3,17 @@ from datetime import datetime import logging -from typing import Any, Callable - -from pyclimacell.const import CURRENT, DAILY, FORECASTS, HOURLY, NOWCAST, WeatherCode +from typing import Any, Callable, Mapping + +from pyclimacell.const import ( + CURRENT, + DAILY, + FORECASTS, + HOURLY, + NOWCAST, + PrecipitationType, + WeatherCode, +) from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, @@ -38,11 +46,16 @@ from . import ClimaCellEntity from .const import ( + ATTR_CLOUD_COVER, + ATTR_PRECIPITATION_TYPE, + ATTR_WIND_GUST, + CC_ATTR_CLOUD_COVER, CC_ATTR_CONDITION, CC_ATTR_HUMIDITY, CC_ATTR_OZONE, CC_ATTR_PRECIPITATION, CC_ATTR_PRECIPITATION_PROBABILITY, + CC_ATTR_PRECIPITATION_TYPE, CC_ATTR_PRESSURE, CC_ATTR_TEMPERATURE, CC_ATTR_TEMPERATURE_HIGH, @@ -50,13 +63,16 @@ CC_ATTR_TIMESTAMP, CC_ATTR_VISIBILITY, CC_ATTR_WIND_DIRECTION, + CC_ATTR_WIND_GUST, CC_ATTR_WIND_SPEED, + CC_V3_ATTR_CLOUD_COVER, CC_V3_ATTR_CONDITION, CC_V3_ATTR_HUMIDITY, CC_V3_ATTR_OZONE, CC_V3_ATTR_PRECIPITATION, CC_V3_ATTR_PRECIPITATION_DAILY, CC_V3_ATTR_PRECIPITATION_PROBABILITY, + CC_V3_ATTR_PRECIPITATION_TYPE, CC_V3_ATTR_PRESSURE, CC_V3_ATTR_TEMPERATURE, CC_V3_ATTR_TEMPERATURE_HIGH, @@ -64,6 +80,7 @@ CC_V3_ATTR_TIMESTAMP, CC_V3_ATTR_VISIBILITY, CC_V3_ATTR_WIND_DIRECTION, + CC_V3_ATTR_WIND_GUST, CC_V3_ATTR_WIND_SPEED, CLEAR_CONDITIONS, CONDITIONS, @@ -149,6 +166,38 @@ def _forecast_dict( return {k: v for k, v in data.items() if v is not None} + @property + def extra_state_attributes(self) -> Mapping[str, Any] | None: + """Return additional state attributes.""" + wind_gust = self.wind_gust + if wind_gust and self.hass.config.units.is_metric: + wind_gust = distance_convert( + self.wind_gust, LENGTH_MILES, LENGTH_KILOMETERS + ) + cloud_cover = self.cloud_cover + if cloud_cover is not None: + cloud_cover /= 100 + return { + ATTR_CLOUD_COVER: cloud_cover, + ATTR_WIND_GUST: wind_gust, + ATTR_PRECIPITATION_TYPE: self.precipitation_type, + } + + @property + def cloud_cover(self): + """Return cloud cover.""" + raise NotImplementedError + + @property + def wind_gust(self): + """Return wind gust speed.""" + raise NotImplementedError + + @property + def precipitation_type(self): + """Return precipitation type.""" + raise NotImplementedError + class ClimaCellWeatherEntity(BaseClimaCellWeatherEntity): """Entity that talks to ClimaCell v4 API to retrieve weather data.""" @@ -195,6 +244,24 @@ def humidity(self): """Return the humidity.""" return self._get_current_property(CC_ATTR_HUMIDITY) + @property + def wind_gust(self): + """Return the wind gust speed.""" + return self._get_current_property(CC_ATTR_WIND_GUST) + + @property + def cloud_cover(self): + """Reteurn the cloud cover.""" + return self._get_current_property(CC_ATTR_CLOUD_COVER) + + @property + def precipitation_type(self): + """Return precipitation type.""" + precipitation_type = self._get_current_property(CC_ATTR_PRECIPITATION_TYPE) + if precipitation_type is None: + return None + return PrecipitationType(precipitation_type).name.lower() + @property def wind_speed(self): """Return the wind speed.""" @@ -338,6 +405,25 @@ def humidity(self): """Return the humidity.""" return self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_HUMIDITY) + @property + def wind_gust(self): + """Return the wind gust speed.""" + return self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_WIND_GUST) + + @property + def cloud_cover(self): + """Reteurn the cloud cover.""" + return self._get_cc_value( + self.coordinator.data[CURRENT], CC_V3_ATTR_CLOUD_COVER + ) + + @property + def precipitation_type(self): + """Return precipitation type.""" + return self._get_cc_value( + self.coordinator.data[CURRENT], CC_V3_ATTR_PRECIPITATION_TYPE + ) + @property def wind_speed(self): """Return the wind speed.""" diff --git a/tests/components/climacell/test_weather.py b/tests/components/climacell/test_weather.py index 646c5cd114b1c..779b0afa2c0f2 100644 --- a/tests/components/climacell/test_weather.py +++ b/tests/components/climacell/test_weather.py @@ -13,7 +13,13 @@ _get_config_schema, _get_unique_id, ) -from homeassistant.components.climacell.const import ATTRIBUTION, DOMAIN +from homeassistant.components.climacell.const import ( + ATTR_CLOUD_COVER, + ATTR_PRECIPITATION_TYPE, + ATTR_WIND_GUST, + ATTRIBUTION, + DOMAIN, +) from homeassistant.components.weather import ( ATTR_CONDITION_CLOUDY, ATTR_CONDITION_RAINY, @@ -222,6 +228,9 @@ async def test_v3_weather( assert weather_state.attributes[ATTR_WEATHER_VISIBILITY] == 9.994026240000002 assert weather_state.attributes[ATTR_WEATHER_WIND_BEARING] == 320.31 assert weather_state.attributes[ATTR_WEATHER_WIND_SPEED] == 14.62893696 + assert weather_state.attributes[ATTR_CLOUD_COVER] == 1 + assert weather_state.attributes[ATTR_WIND_GUST] == 24.075786240000003 + assert weather_state.attributes[ATTR_PRECIPITATION_TYPE] == "rain" async def test_v4_weather( @@ -382,3 +391,6 @@ async def test_v4_weather( assert weather_state.attributes[ATTR_WEATHER_VISIBILITY] == 13.116153600000002 assert weather_state.attributes[ATTR_WEATHER_WIND_BEARING] == 315.14 assert weather_state.attributes[ATTR_WEATHER_WIND_SPEED] == 15.01517952 + assert weather_state.attributes[ATTR_CLOUD_COVER] == 1 + assert weather_state.attributes[ATTR_WIND_GUST] == 20.34210816 + assert weather_state.attributes[ATTR_PRECIPITATION_TYPE] == "rain" diff --git a/tests/fixtures/climacell/v3_realtime.json b/tests/fixtures/climacell/v3_realtime.json index 8ed05fe538377..c4226ab5ad9b1 100644 --- a/tests/fixtures/climacell/v3_realtime.json +++ b/tests/fixtures/climacell/v3_realtime.json @@ -32,6 +32,17 @@ "value": 52.625, "units": "ppb" }, + "wind_gust": { + "value": 14.96, + "units": "mph" + }, + "precipitation_type": { + "value": "rain" + }, + "cloud_cover": { + "value": 100, + "units": "%" + }, "observation_time": { "value": "2021-03-07T18:54:06.055Z" } diff --git a/tests/fixtures/climacell/v4.json b/tests/fixtures/climacell/v4.json index d667284a4ad89..7d778ba9f5107 100644 --- a/tests/fixtures/climacell/v4.json +++ b/tests/fixtures/climacell/v4.json @@ -7,7 +7,10 @@ "windDirection": 315.14, "weatherCode": 1000, "visibility": 8.15, - "pollutantO3": 46.53 + "pollutantO3": 46.53, + "windGust": 12.64, + "cloudCover": 100, + "precipitationType": 1 }, "forecasts": { "nowcast": [ From 9997ae6932031d1a2647e335c2a9b45680aa397f Mon Sep 17 00:00:00 2001 From: Ruslan Sayfutdinov Date: Sun, 11 Apr 2021 15:56:33 +0100 Subject: [PATCH 0191/1317] Type data parameter as Mapping in async_create_entry (#49050) --- homeassistant/data_entry_flow.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 40c9ace0f8dee..46ec967bd94e5 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -3,6 +3,7 @@ import abc import asyncio +from collections.abc import Mapping from types import MappingProxyType from typing import Any import uuid @@ -318,7 +319,7 @@ def async_create_entry( self, *, title: str, - data: dict, + data: Mapping[str, Any], description: str | None = None, description_placeholders: dict | None = None, ) -> dict[str, Any]: From a261bb35ebac00ad7024e021944789ae96fcbc15 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 11 Apr 2021 06:42:46 -1000 Subject: [PATCH 0192/1317] Bump aiohomekit to 0.2.61 (#49044) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 9580a7ee50d19..d4e7eb83ee3bc 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", "requirements": [ - "aiohomekit==0.2.60" + "aiohomekit==0.2.61" ], "zeroconf": [ "_hap._tcp.local." diff --git a/requirements_all.txt b/requirements_all.txt index 63935c4b0aebd..fe68fc71bd704 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -175,7 +175,7 @@ aioguardian==1.0.4 aioharmony==0.2.7 # homeassistant.components.homekit_controller -aiohomekit==0.2.60 +aiohomekit==0.2.61 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 662398d02597b..17885071b1efa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -112,7 +112,7 @@ aioguardian==1.0.4 aioharmony==0.2.7 # homeassistant.components.homekit_controller -aiohomekit==0.2.60 +aiohomekit==0.2.61 # homeassistant.components.emulated_hue # homeassistant.components.http From f7b6d3164ab9eea81b3fc7f43e53034bf193a2b7 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Sun, 11 Apr 2021 12:35:42 -0500 Subject: [PATCH 0193/1317] Resolve potential roku setup memory leaks (#49025) * resolve potential roku setup memory leaks * Update __init__.py --- homeassistant/components/roku/__init__.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/roku/__init__.py b/homeassistant/components/roku/__init__.py index 4a34926545956..f8294c878dde2 100644 --- a/homeassistant/components/roku/__init__.py +++ b/homeassistant/components/roku/__init__.py @@ -47,10 +47,12 @@ async def async_setup(hass: HomeAssistantType, config: dict) -> bool: async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: """Set up Roku from a config entry.""" - coordinator = RokuDataUpdateCoordinator(hass, host=entry.data[CONF_HOST]) - await coordinator.async_config_entry_first_refresh() + coordinator = hass.data[DOMAIN].get(entry.entry_id) + if not coordinator: + coordinator = RokuDataUpdateCoordinator(hass, host=entry.data[CONF_HOST]) + hass.data[DOMAIN][entry.entry_id] = coordinator - hass.data[DOMAIN][entry.entry_id] = coordinator + await coordinator.async_config_entry_first_refresh() for platform in PLATFORMS: hass.async_create_task( From 30618aae942caa6792f0f8346c46f7da08df1208 Mon Sep 17 00:00:00 2001 From: Ludovico de Nittis Date: Sun, 11 Apr 2021 22:35:04 +0200 Subject: [PATCH 0194/1317] Reintroduce iAlarm integration (#43525) The previous iAlarm integration has been removed because it used webscraping #43010. Since then, the pyialarm library has been updated to use the iAlarm API instead. With this commit I reintroduce the iAlarm integration, leveraging the new HA config flow. Signed-off-by: Ludovico de Nittis --- .coveragerc | 1 + CODEOWNERS | 1 + homeassistant/components/ialarm/__init__.py | 89 +++++++++++++++ .../components/ialarm/alarm_control_panel.py | 64 +++++++++++ .../components/ialarm/config_flow.py | 63 +++++++++++ homeassistant/components/ialarm/const.py | 22 ++++ homeassistant/components/ialarm/manifest.json | 12 ++ homeassistant/components/ialarm/strings.json | 20 ++++ .../components/ialarm/translations/en.json | 20 ++++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/ialarm/__init__.py | 1 + tests/components/ialarm/test_config_flow.py | 104 ++++++++++++++++++ tests/components/ialarm/test_init.py | 85 ++++++++++++++ 15 files changed, 489 insertions(+) create mode 100644 homeassistant/components/ialarm/__init__.py create mode 100644 homeassistant/components/ialarm/alarm_control_panel.py create mode 100644 homeassistant/components/ialarm/config_flow.py create mode 100644 homeassistant/components/ialarm/const.py create mode 100644 homeassistant/components/ialarm/manifest.json create mode 100644 homeassistant/components/ialarm/strings.json create mode 100644 homeassistant/components/ialarm/translations/en.json create mode 100644 tests/components/ialarm/__init__.py create mode 100644 tests/components/ialarm/test_config_flow.py create mode 100644 tests/components/ialarm/test_init.py diff --git a/.coveragerc b/.coveragerc index 2a5e6ecc502ad..3d126dfd23b40 100644 --- a/.coveragerc +++ b/.coveragerc @@ -431,6 +431,7 @@ omit = homeassistant/components/hvv_departures/sensor.py homeassistant/components/hvv_departures/__init__.py homeassistant/components/hydrawise/* + homeassistant/components/ialarm/alarm_control_panel.py homeassistant/components/iammeter/sensor.py homeassistant/components/iaqualink/binary_sensor.py homeassistant/components/iaqualink/climate.py diff --git a/CODEOWNERS b/CODEOWNERS index 5f2fd6588a627..860ee9f0665c7 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -214,6 +214,7 @@ homeassistant/components/hunterdouglas_powerview/* @bdraco homeassistant/components/hvv_departures/* @vigonotion homeassistant/components/hydrawise/* @ptcryan homeassistant/components/hyperion/* @dermotduffy +homeassistant/components/ialarm/* @RyuzakiKK homeassistant/components/iammeter/* @lewei50 homeassistant/components/iaqualink/* @flz homeassistant/components/icloud/* @Quentame @nzapponi diff --git a/homeassistant/components/ialarm/__init__.py b/homeassistant/components/ialarm/__init__.py new file mode 100644 index 0000000000000..03d07a15394e6 --- /dev/null +++ b/homeassistant/components/ialarm/__init__.py @@ -0,0 +1,89 @@ +"""iAlarm integration.""" +import asyncio +import logging + +from async_timeout import timeout +from pyialarm import IAlarm + +from homeassistant.components.alarm_control_panel import SCAN_INTERVAL +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DATA_COORDINATOR, DOMAIN, IALARM_TO_HASS + +PLATFORM = "alarm_control_panel" +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up iAlarm config.""" + host = entry.data[CONF_HOST] + port = entry.data[CONF_PORT] + ialarm = IAlarm(host, port) + + try: + async with timeout(10): + mac = await hass.async_add_executor_job(ialarm.get_mac) + except (asyncio.TimeoutError, ConnectionError) as ex: + raise ConfigEntryNotReady from ex + + coordinator = IAlarmDataUpdateCoordinator(hass, ialarm, mac) + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {}) + + hass.data[DOMAIN][entry.entry_id] = { + DATA_COORDINATOR: coordinator, + } + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, PLATFORM) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload iAlarm config.""" + unload_ok = await hass.config_entries.async_forward_entry_unload(entry, PLATFORM) + + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +class IAlarmDataUpdateCoordinator(DataUpdateCoordinator): + """Class to manage fetching iAlarm data.""" + + def __init__(self, hass, ialarm, mac): + """Initialize global iAlarm data updater.""" + self.ialarm = ialarm + self.state = None + self.host = ialarm.host + self.mac = mac + + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=SCAN_INTERVAL, + ) + + def _update_data(self) -> None: + """Fetch data from iAlarm via sync functions.""" + status = self.ialarm.get_status() + _LOGGER.debug("iAlarm status: %s", status) + + self.state = IALARM_TO_HASS.get(status) + + async def _async_update_data(self) -> None: + """Fetch data from iAlarm.""" + try: + async with timeout(10): + await self.hass.async_add_executor_job(self._update_data) + except ConnectionError as error: + raise UpdateFailed(error) from error diff --git a/homeassistant/components/ialarm/alarm_control_panel.py b/homeassistant/components/ialarm/alarm_control_panel.py new file mode 100644 index 0000000000000..a33162b7afd22 --- /dev/null +++ b/homeassistant/components/ialarm/alarm_control_panel.py @@ -0,0 +1,64 @@ +"""Interfaces with iAlarm control panels.""" +import logging + +from homeassistant.components.alarm_control_panel import AlarmControlPanelEntity +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, +) +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DATA_COORDINATOR, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, entry, async_add_entities) -> None: + """Set up a iAlarm alarm control panel based on a config entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] + async_add_entities([IAlarmPanel(coordinator)], False) + + +class IAlarmPanel(CoordinatorEntity, AlarmControlPanelEntity): + """Representation of an iAlarm device.""" + + @property + def device_info(self): + """Return device info for this device.""" + return { + "identifiers": {(DOMAIN, self.unique_id)}, + "name": self.name, + "manufacturer": "Antifurto365 - Meian", + } + + @property + def unique_id(self): + """Return a unique id.""" + return self.coordinator.mac + + @property + def name(self): + """Return the name.""" + return "iAlarm" + + @property + def state(self): + """Return the state of the device.""" + return self.coordinator.state + + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY + + def alarm_disarm(self, code=None): + """Send disarm command.""" + self.coordinator.ialarm.disarm() + + def alarm_arm_home(self, code=None): + """Send arm home command.""" + self.coordinator.ialarm.arm_stay() + + def alarm_arm_away(self, code=None): + """Send arm away command.""" + self.coordinator.ialarm.arm_away() diff --git a/homeassistant/components/ialarm/config_flow.py b/homeassistant/components/ialarm/config_flow.py new file mode 100644 index 0000000000000..64eab90719b51 --- /dev/null +++ b/homeassistant/components/ialarm/config_flow.py @@ -0,0 +1,63 @@ +"""Config flow for Antifurto365 iAlarm integration.""" +import logging + +from pyialarm import IAlarm +import voluptuous as vol + +from homeassistant import config_entries, core +from homeassistant.const import CONF_HOST, CONF_PORT + +# pylint: disable=unused-import +from .const import DEFAULT_PORT, DOMAIN + +_LOGGER = logging.getLogger(__name__) + +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Required(CONF_PORT, default=DEFAULT_PORT): int, + } +) + + +async def _get_device_mac(hass: core.HomeAssistant, host, port): + ialarm = IAlarm(host, port) + return await hass.async_add_executor_job(ialarm.get_mac) + + +class IAlarmConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Antifurto365 iAlarm.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + mac = None + + if user_input is None: + return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA) + + host = user_input[CONF_HOST] + port = user_input[CONF_PORT] + + try: + # If we are able to get the MAC address, we are able to establish + # a connection to the device. + mac = await _get_device_mac(self.hass, host, port) + except ConnectionError: + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + if errors: + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) + + await self.async_set_unique_id(mac) + self._abort_if_unique_id_configured() + + return self.async_create_entry(title=user_input[CONF_HOST], data=user_input) diff --git a/homeassistant/components/ialarm/const.py b/homeassistant/components/ialarm/const.py new file mode 100644 index 0000000000000..c6eaf0ec97907 --- /dev/null +++ b/homeassistant/components/ialarm/const.py @@ -0,0 +1,22 @@ +"""Constants for the iAlarm integration.""" +from pyialarm import IAlarm + +from homeassistant.const import ( + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED, +) + +DATA_COORDINATOR = "ialarm" + +DEFAULT_PORT = 18034 + +DOMAIN = "ialarm" + +IALARM_TO_HASS = { + IAlarm.ARMED_AWAY: STATE_ALARM_ARMED_AWAY, + IAlarm.ARMED_STAY: STATE_ALARM_ARMED_HOME, + IAlarm.DISARMED: STATE_ALARM_DISARMED, + IAlarm.TRIGGERED: STATE_ALARM_TRIGGERED, +} diff --git a/homeassistant/components/ialarm/manifest.json b/homeassistant/components/ialarm/manifest.json new file mode 100644 index 0000000000000..1e4c0383922e0 --- /dev/null +++ b/homeassistant/components/ialarm/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "ialarm", + "name": "Antifurto365 iAlarm", + "documentation": "https://www.home-assistant.io/integrations/ialarm", + "requirements": [ + "pyialarm==1.5" + ], + "codeowners": [ + "@RyuzakiKK" + ], + "config_flow": true +} diff --git a/homeassistant/components/ialarm/strings.json b/homeassistant/components/ialarm/strings.json new file mode 100644 index 0000000000000..5976a95ea5dda --- /dev/null +++ b/homeassistant/components/ialarm/strings.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]", + "pin": "[%key:common::config_flow::data::pin%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/ialarm/translations/en.json b/homeassistant/components/ialarm/translations/en.json new file mode 100644 index 0000000000000..2ea7a7ab66945 --- /dev/null +++ b/homeassistant/components/ialarm/translations/en.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port" + } + } + } + }, + "title": "Antifurto365 iAlarm" +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 808f18c319d50..25429296d8e45 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -109,6 +109,7 @@ "hunterdouglas_powerview", "hvv_departures", "hyperion", + "ialarm", "iaqualink", "icloud", "ifttt", diff --git a/requirements_all.txt b/requirements_all.txt index fe68fc71bd704..998fcec879609 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1448,6 +1448,9 @@ pyhomematic==0.1.72 # homeassistant.components.homeworks pyhomeworks==0.0.6 +# homeassistant.components.ialarm +pyialarm==1.5 + # homeassistant.components.icloud pyicloud==0.10.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 17885071b1efa..480435be44152 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -783,6 +783,9 @@ pyhiveapi==0.4.1 # homeassistant.components.homematic pyhomematic==0.1.72 +# homeassistant.components.ialarm +pyialarm==1.5 + # homeassistant.components.icloud pyicloud==0.10.2 diff --git a/tests/components/ialarm/__init__.py b/tests/components/ialarm/__init__.py new file mode 100644 index 0000000000000..51cccfad0233e --- /dev/null +++ b/tests/components/ialarm/__init__.py @@ -0,0 +1 @@ +"""Tests for the Antifurto365 iAlarm integration.""" diff --git a/tests/components/ialarm/test_config_flow.py b/tests/components/ialarm/test_config_flow.py new file mode 100644 index 0000000000000..54da9a18b1a94 --- /dev/null +++ b/tests/components/ialarm/test_config_flow.py @@ -0,0 +1,104 @@ +"""Test the Antifurto365 iAlarm config flow.""" +from unittest.mock import patch + +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.ialarm.const import DOMAIN +from homeassistant.const import CONF_HOST, CONF_PORT + +from tests.common import MockConfigEntry + +TEST_DATA = {CONF_HOST: "1.1.1.1", CONF_PORT: 18034} + +TEST_MAC = "00:00:54:12:34:56" + + +async def test_form(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] is None + + with patch( + "homeassistant.components.ialarm.config_flow.IAlarm.get_status", + return_value=1, + ), patch( + "homeassistant.components.ialarm.config_flow.IAlarm.get_mac", + return_value=TEST_MAC, + ), patch( + "homeassistant.components.ialarm.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], TEST_DATA + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == TEST_DATA["host"] + assert result2["data"] == TEST_DATA + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_cannot_connect(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.ialarm.config_flow.IAlarm.get_mac", + side_effect=ConnectionError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], TEST_DATA + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_exception(hass): + """Test we handle unknown exception.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.ialarm.config_flow.IAlarm.get_mac", + side_effect=Exception, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], TEST_DATA + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": "unknown"} + + +async def test_form_already_exists(hass): + """Test that a flow with an existing host aborts.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=TEST_MAC, + data=TEST_DATA, + ) + + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.ialarm.config_flow.IAlarm.get_mac", + return_value=TEST_MAC, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], TEST_DATA + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["reason"] == "already_configured" diff --git a/tests/components/ialarm/test_init.py b/tests/components/ialarm/test_init.py new file mode 100644 index 0000000000000..2f1936aff81a5 --- /dev/null +++ b/tests/components/ialarm/test_init.py @@ -0,0 +1,85 @@ +"""Test the Antifurto365 iAlarm init.""" +from unittest.mock import Mock, patch +from uuid import uuid4 + +import pytest + +from homeassistant.components.ialarm.const import DOMAIN +from homeassistant.config_entries import ( + ENTRY_STATE_LOADED, + ENTRY_STATE_NOT_LOADED, + ENTRY_STATE_SETUP_RETRY, +) +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + + +@pytest.fixture(name="ialarm_api") +def ialarm_api_fixture(): + """Set up IAlarm API fixture.""" + with patch("homeassistant.components.ialarm.IAlarm") as mock_ialarm_api: + yield mock_ialarm_api + + +@pytest.fixture(name="mock_config_entry") +def mock_config_fixture(): + """Return a fake config entry.""" + return MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "192.168.10.20", CONF_PORT: 18034}, + entry_id=str(uuid4()), + ) + + +async def test_setup_entry(hass, ialarm_api, mock_config_entry): + """Test setup entry.""" + ialarm_api.return_value.get_mac = Mock(return_value="00:00:54:12:34:56") + + mock_config_entry.add_to_hass(hass) + await async_setup_component( + hass, + DOMAIN, + { + "ialarm": { + CONF_HOST: "192.168.10.20", + CONF_PORT: 18034, + }, + }, + ) + await hass.async_block_till_done() + ialarm_api.return_value.get_mac.assert_called_once() + assert mock_config_entry.state == ENTRY_STATE_LOADED + + +async def test_setup_not_ready(hass, ialarm_api, mock_config_entry): + """Test setup failed because we can't connect to the alarm system.""" + ialarm_api.return_value.get_mac = Mock(side_effect=ConnectionError) + + mock_config_entry.add_to_hass(hass) + await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + assert mock_config_entry.state == ENTRY_STATE_SETUP_RETRY + + +async def test_unload_entry(hass, ialarm_api, mock_config_entry): + """Test being able to unload an entry.""" + ialarm_api.return_value.get_mac = Mock(return_value="00:00:54:12:34:56") + + mock_config_entry.add_to_hass(hass) + await async_setup_component( + hass, + DOMAIN, + { + "ialarm": { + CONF_HOST: "192.168.10.20", + CONF_PORT: 18034, + }, + }, + ) + await hass.async_block_till_done() + + assert mock_config_entry.state == ENTRY_STATE_LOADED + assert await hass.config_entries.async_unload(mock_config_entry.entry_id) + assert mock_config_entry.state == ENTRY_STATE_NOT_LOADED From eb2949a20f226ff8e01fd40f4e1d5be46099b996 Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Sun, 11 Apr 2021 14:35:25 -0600 Subject: [PATCH 0195/1317] Add set_wait_time command support to Litter-Robot (#48300) Co-authored-by: J. Nick Koston --- .../components/litterrobot/__init__.py | 9 +- .../components/litterrobot/entity.py | 113 +++++++++++++ homeassistant/components/litterrobot/hub.py | 88 ++-------- .../components/litterrobot/manifest.json | 2 +- .../components/litterrobot/sensor.py | 56 ++++--- .../components/litterrobot/services.yaml | 48 ++++++ .../components/litterrobot/strings.json | 2 +- .../components/litterrobot/switch.py | 57 ++++--- .../litterrobot/translations/en.json | 2 +- .../components/litterrobot/vacuum.py | 157 +++++++++++------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/litterrobot/common.py | 6 +- tests/components/litterrobot/conftest.py | 81 +++++---- .../litterrobot/test_config_flow.py | 5 +- tests/components/litterrobot/test_init.py | 22 ++- tests/components/litterrobot/test_sensor.py | 5 +- tests/components/litterrobot/test_switch.py | 4 +- tests/components/litterrobot/test_vacuum.py | 94 ++++++++--- 19 files changed, 496 insertions(+), 259 deletions(-) create mode 100644 homeassistant/components/litterrobot/entity.py create mode 100644 homeassistant/components/litterrobot/services.yaml diff --git a/homeassistant/components/litterrobot/__init__.py b/homeassistant/components/litterrobot/__init__.py index 84e6822dc13f4..6fea013f54ca7 100644 --- a/homeassistant/components/litterrobot/__init__.py +++ b/homeassistant/components/litterrobot/__init__.py @@ -30,10 +30,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): except LitterRobotException as ex: raise ConfigEntryNotReady from ex - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + if hub.account.robots: + for platform in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, platform) + ) return True diff --git a/homeassistant/components/litterrobot/entity.py b/homeassistant/components/litterrobot/entity.py new file mode 100644 index 0000000000000..89a8c80a0df94 --- /dev/null +++ b/homeassistant/components/litterrobot/entity.py @@ -0,0 +1,113 @@ +"""Litter-Robot entities for common data and methods.""" +from __future__ import annotations + +from datetime import time +import logging +from types import MethodType +from typing import Any + +from pylitterbot import Robot +from pylitterbot.exceptions import InvalidCommandException + +from homeassistant.core import callback +from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.update_coordinator import CoordinatorEntity +import homeassistant.util.dt as dt_util + +from .const import DOMAIN +from .hub import LitterRobotHub + +_LOGGER = logging.getLogger(__name__) + +REFRESH_WAIT_TIME_SECONDS = 8 + + +class LitterRobotEntity(CoordinatorEntity): + """Generic Litter-Robot entity representing common data and methods.""" + + def __init__(self, robot: Robot, entity_type: str, hub: LitterRobotHub) -> None: + """Pass coordinator to CoordinatorEntity.""" + super().__init__(hub.coordinator) + self.robot = robot + self.entity_type = entity_type + self.hub = hub + + @property + def name(self) -> str: + """Return the name of this entity.""" + return f"{self.robot.name} {self.entity_type}" + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return f"{self.robot.serial}-{self.entity_type}" + + @property + def device_info(self) -> dict[str, Any]: + """Return the device information for a Litter-Robot.""" + return { + "identifiers": {(DOMAIN, self.robot.serial)}, + "name": self.robot.name, + "manufacturer": "Litter-Robot", + "model": self.robot.model, + } + + +class LitterRobotControlEntity(LitterRobotEntity): + """A Litter-Robot entity that can control the unit.""" + + def __init__(self, robot: Robot, entity_type: str, hub: LitterRobotHub) -> None: + """Init a Litter-Robot control entity.""" + super().__init__(robot=robot, entity_type=entity_type, hub=hub) + self._refresh_callback = None + + async def perform_action_and_refresh( + self, action: MethodType, *args: Any, **kwargs: Any + ) -> bool: + """Perform an action and initiates a refresh of the robot data after a few seconds.""" + + try: + await action(*args, **kwargs) + except InvalidCommandException as ex: + _LOGGER.error(ex) + return False + + self.async_cancel_refresh_callback() + self._refresh_callback = async_call_later( + self.hass, REFRESH_WAIT_TIME_SECONDS, self.async_call_later_callback + ) + return True + + async def async_call_later_callback(self, *_) -> None: + """Perform refresh request on callback.""" + self._refresh_callback = None + await self.coordinator.async_request_refresh() + + async def async_will_remove_from_hass(self) -> None: + """Cancel refresh callback when entity is being removed from hass.""" + self.async_cancel_refresh_callback() + + @callback + def async_cancel_refresh_callback(self): + """Clear the refresh callback if it has not already fired.""" + if self._refresh_callback is not None: + self._refresh_callback() + self._refresh_callback = None + + @staticmethod + def parse_time_at_default_timezone(time_str: str) -> time | None: + """Parse a time string and add default timezone.""" + parsed_time = dt_util.parse_time(time_str) + + if parsed_time is None: + return None + + return ( + dt_util.start_of_local_day() + .replace( + hour=parsed_time.hour, + minute=parsed_time.minute, + second=parsed_time.second, + ) + .timetz() + ) diff --git a/homeassistant/components/litterrobot/hub.py b/homeassistant/components/litterrobot/hub.py index 86c3aff5462ca..6a9155b9eafb5 100644 --- a/homeassistant/components/litterrobot/hub.py +++ b/homeassistant/components/litterrobot/hub.py @@ -1,41 +1,31 @@ -"""A wrapper 'hub' for the Litter-Robot API and base entity for common attributes.""" -from __future__ import annotations - -from datetime import time, timedelta +"""A wrapper 'hub' for the Litter-Robot API.""" +from datetime import timedelta import logging -from types import MethodType -from typing import Any -import pylitterbot +from pylitterbot import Account from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.helpers.event import async_call_later -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) -import homeassistant.util.dt as dt_util +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -REFRESH_WAIT_TIME = 12 -UPDATE_INTERVAL = 10 +UPDATE_INTERVAL_SECONDS = 10 class LitterRobotHub: """A Litter-Robot hub wrapper class.""" - def __init__(self, hass: HomeAssistant, data: dict): + def __init__(self, hass: HomeAssistant, data: dict) -> None: """Initialize the Litter-Robot hub.""" self._data = data self.account = None self.logged_in = False - async def _async_update_data(): + async def _async_update_data() -> bool: """Update all device states from the Litter-Robot API.""" await self.account.refresh_robots() return True @@ -45,13 +35,13 @@ async def _async_update_data(): _LOGGER, name=DOMAIN, update_method=_async_update_data, - update_interval=timedelta(seconds=UPDATE_INTERVAL), + update_interval=timedelta(seconds=UPDATE_INTERVAL_SECONDS), ) - async def login(self, load_robots: bool = False): + async def login(self, load_robots: bool = False) -> None: """Login to Litter-Robot.""" self.logged_in = False - self.account = pylitterbot.Account() + self.account = Account() try: await self.account.connect( username=self._data[CONF_USERNAME], @@ -66,61 +56,3 @@ async def login(self, load_robots: bool = False): except LitterRobotException as ex: _LOGGER.error("Unable to connect to Litter-Robot API") raise ex - - -class LitterRobotEntity(CoordinatorEntity): - """Generic Litter-Robot entity representing common data and methods.""" - - def __init__(self, robot: pylitterbot.Robot, entity_type: str, hub: LitterRobotHub): - """Pass coordinator to CoordinatorEntity.""" - super().__init__(hub.coordinator) - self.robot = robot - self.entity_type = entity_type - self.hub = hub - - @property - def name(self): - """Return the name of this entity.""" - return f"{self.robot.name} {self.entity_type}" - - @property - def unique_id(self): - """Return a unique ID.""" - return f"{self.robot.serial}-{self.entity_type}" - - @property - def device_info(self): - """Return the device information for a Litter-Robot.""" - return { - "identifiers": {(DOMAIN, self.robot.serial)}, - "name": self.robot.name, - "manufacturer": "Litter-Robot", - "model": self.robot.model, - } - - async def perform_action_and_refresh(self, action: MethodType, *args: Any): - """Perform an action and initiates a refresh of the robot data after a few seconds.""" - - async def async_call_later_callback(*_) -> None: - await self.hub.coordinator.async_request_refresh() - - await action(*args) - async_call_later(self.hass, REFRESH_WAIT_TIME, async_call_later_callback) - - @staticmethod - def parse_time_at_default_timezone(time_str: str) -> time | None: - """Parse a time string and add default timezone.""" - parsed_time = dt_util.parse_time(time_str) - - if parsed_time is None: - return None - - return ( - dt_util.start_of_local_day() - .replace( - hour=parsed_time.hour, - minute=parsed_time.minute, - second=parsed_time.second, - ) - .timetz() - ) diff --git a/homeassistant/components/litterrobot/manifest.json b/homeassistant/components/litterrobot/manifest.json index 8fa7ab8dcb540..1e440fabe1a1b 100644 --- a/homeassistant/components/litterrobot/manifest.json +++ b/homeassistant/components/litterrobot/manifest.json @@ -3,6 +3,6 @@ "name": "Litter-Robot", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/litterrobot", - "requirements": ["pylitterbot==2021.2.8"], + "requirements": ["pylitterbot==2021.3.1"], "codeowners": ["@natekspencer"] } diff --git a/homeassistant/components/litterrobot/sensor.py b/homeassistant/components/litterrobot/sensor.py index 8038fdbb2cbc6..022a372ac68af 100644 --- a/homeassistant/components/litterrobot/sensor.py +++ b/homeassistant/components/litterrobot/sensor.py @@ -1,13 +1,19 @@ """Support for Litter-Robot sensors.""" from __future__ import annotations +from typing import Callable + from pylitterbot.robot import Robot from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import DEVICE_CLASS_TIMESTAMP, PERCENTAGE +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity from .const import DOMAIN -from .hub import LitterRobotEntity, LitterRobotHub +from .entity import LitterRobotEntity +from .hub import LitterRobotHub def icon_for_gauge_level(gauge_level: int | None = None, offset: int = 0) -> str: @@ -22,66 +28,76 @@ def icon_for_gauge_level(gauge_level: int | None = None, offset: int = 0) -> str class LitterRobotPropertySensor(LitterRobotEntity, SensorEntity): - """Litter-Robot property sensors.""" + """Litter-Robot property sensor.""" def __init__( self, robot: Robot, entity_type: str, hub: LitterRobotHub, sensor_attribute: str - ): - """Pass coordinator to CoordinatorEntity.""" + ) -> None: + """Pass robot, entity_type and hub to LitterRobotEntity.""" super().__init__(robot, entity_type, hub) self.sensor_attribute = sensor_attribute @property - def state(self): + def state(self) -> str: """Return the state.""" return getattr(self.robot, self.sensor_attribute) class LitterRobotWasteSensor(LitterRobotPropertySensor): - """Litter-Robot sensors.""" + """Litter-Robot waste sensor.""" @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str: """Return unit of measurement.""" return PERCENTAGE @property - def icon(self): + def icon(self) -> str: """Return the icon to use in the frontend, if any.""" return icon_for_gauge_level(self.state, 10) class LitterRobotSleepTimeSensor(LitterRobotPropertySensor): - """Litter-Robot sleep time sensors.""" + """Litter-Robot sleep time sensor.""" @property - def state(self): + def state(self) -> str | None: """Return the state.""" - if self.robot.sleep_mode_active: + if self.robot.sleep_mode_enabled: return super().state.isoformat() return None @property - def device_class(self): + def device_class(self) -> str: """Return the device class, if any.""" return DEVICE_CLASS_TIMESTAMP -ROBOT_SENSORS = [ - (LitterRobotWasteSensor, "Waste Drawer", "waste_drawer_gauge"), +ROBOT_SENSORS: list[tuple[type[LitterRobotPropertySensor], str, str]] = [ + (LitterRobotWasteSensor, "Waste Drawer", "waste_drawer_level"), (LitterRobotSleepTimeSensor, "Sleep Mode Start Time", "sleep_mode_start_time"), (LitterRobotSleepTimeSensor, "Sleep Mode End Time", "sleep_mode_end_time"), ] -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: Callable[[list[Entity], bool], None], +) -> None: """Set up Litter-Robot sensors using config entry.""" - hub = hass.data[DOMAIN][config_entry.entry_id] + hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] entities = [] for robot in hub.account.robots: for (sensor_class, entity_type, sensor_attribute) in ROBOT_SENSORS: - entities.append(sensor_class(robot, entity_type, hub, sensor_attribute)) - - if entities: - async_add_entities(entities, True) + entities.append( + sensor_class( + robot=robot, + entity_type=entity_type, + hub=hub, + sensor_attribute=sensor_attribute, + ) + ) + + async_add_entities(entities, True) diff --git a/homeassistant/components/litterrobot/services.yaml b/homeassistant/components/litterrobot/services.yaml new file mode 100644 index 0000000000000..5ca25e1b1b8b9 --- /dev/null +++ b/homeassistant/components/litterrobot/services.yaml @@ -0,0 +1,48 @@ +# Describes the format for available Litter-Robot services + +reset_waste_drawer: + name: Reset waste drawer + description: Reset the waste drawer level. + target: + +set_sleep_mode: + name: Set sleep mode + description: Set the sleep mode and start time. + target: + fields: + enabled: + name: Enabled + description: Whether sleep mode should be enabled. + required: true + example: true + selector: + boolean: + start_time: + name: Start time + description: The start time at which the Litter-Robot will enter sleep mode and prevent an automatic clean cycle for 8 hours. + required: false + example: '"22:30:00"' + selector: + time: + +set_wait_time: + name: Set wait time + description: Set the wait time, in minutes, between when your cat uses the Litter-Robot and when the unit cycles automatically. + target: + fields: + minutes: + name: Minutes + description: Minutes to wait. + required: true + example: 7 + values: + - 3 + - 7 + - 15 + default: 7 + selector: + select: + options: + - "3" + - "7" + - "15" diff --git a/homeassistant/components/litterrobot/strings.json b/homeassistant/components/litterrobot/strings.json index 96dc8b371d143..f7a539fe0e6e0 100644 --- a/homeassistant/components/litterrobot/strings.json +++ b/homeassistant/components/litterrobot/strings.json @@ -14,7 +14,7 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" } } } diff --git a/homeassistant/components/litterrobot/switch.py b/homeassistant/components/litterrobot/switch.py index 9164cc35e900a..2896458acff56 100644 --- a/homeassistant/components/litterrobot/switch.py +++ b/homeassistant/components/litterrobot/switch.py @@ -1,68 +1,79 @@ """Support for Litter-Robot switches.""" +from __future__ import annotations + +from typing import Any, Callable + from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity from .const import DOMAIN -from .hub import LitterRobotEntity +from .entity import LitterRobotControlEntity +from .hub import LitterRobotHub -class LitterRobotNightLightModeSwitch(LitterRobotEntity, SwitchEntity): +class LitterRobotNightLightModeSwitch(LitterRobotControlEntity, SwitchEntity): """Litter-Robot Night Light Mode Switch.""" @property - def is_on(self): + def is_on(self) -> bool: """Return true if switch is on.""" - return self.robot.night_light_active + return self.robot.night_light_mode_enabled @property - def icon(self): + def icon(self) -> str: """Return the icon.""" return "mdi:lightbulb-on" if self.is_on else "mdi:lightbulb-off" - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" await self.perform_action_and_refresh(self.robot.set_night_light, True) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" await self.perform_action_and_refresh(self.robot.set_night_light, False) -class LitterRobotPanelLockoutSwitch(LitterRobotEntity, SwitchEntity): +class LitterRobotPanelLockoutSwitch(LitterRobotControlEntity, SwitchEntity): """Litter-Robot Panel Lockout Switch.""" @property - def is_on(self): + def is_on(self) -> bool: """Return true if switch is on.""" - return self.robot.panel_lock_active + return self.robot.panel_lock_enabled @property - def icon(self): + def icon(self) -> str: """Return the icon.""" return "mdi:lock" if self.is_on else "mdi:lock-open" - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" await self.perform_action_and_refresh(self.robot.set_panel_lockout, True) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" await self.perform_action_and_refresh(self.robot.set_panel_lockout, False) -ROBOT_SWITCHES = { - "Night Light Mode": LitterRobotNightLightModeSwitch, - "Panel Lockout": LitterRobotPanelLockoutSwitch, -} +ROBOT_SWITCHES: list[tuple[type[LitterRobotControlEntity], str]] = [ + (LitterRobotNightLightModeSwitch, "Night Light Mode"), + (LitterRobotPanelLockoutSwitch, "Panel Lockout"), +] -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: Callable[[list[Entity], bool], None], +) -> None: """Set up Litter-Robot switches using config entry.""" - hub = hass.data[DOMAIN][config_entry.entry_id] + hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] entities = [] for robot in hub.account.robots: - for switch_type, switch_class in ROBOT_SWITCHES.items(): - entities.append(switch_class(robot, switch_type, hub)) + for switch_class, switch_type in ROBOT_SWITCHES: + entities.append(switch_class(robot=robot, entity_type=switch_type, hub=hub)) - if entities: - async_add_entities(entities, True) + async_add_entities(entities, True) diff --git a/homeassistant/components/litterrobot/translations/en.json b/homeassistant/components/litterrobot/translations/en.json index cb0e7bed7ea2b..a6c0889765f5e 100644 --- a/homeassistant/components/litterrobot/translations/en.json +++ b/homeassistant/components/litterrobot/translations/en.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Device is already configured" + "already_configured": "Account is already configured" }, "error": { "cannot_connect": "Failed to connect", diff --git a/homeassistant/components/litterrobot/vacuum.py b/homeassistant/components/litterrobot/vacuum.py index a36ef6563610b..32fc92cd55afa 100644 --- a/homeassistant/components/litterrobot/vacuum.py +++ b/homeassistant/components/litterrobot/vacuum.py @@ -1,11 +1,17 @@ """Support for Litter-Robot "Vacuum".""" -from pylitterbot import Robot +from __future__ import annotations + +from typing import Any, Callable + +from pylitterbot.enums import LitterBoxStatus +from pylitterbot.robot import VALID_WAIT_TIMES +import voluptuous as vol from homeassistant.components.vacuum import ( STATE_CLEANING, STATE_DOCKED, STATE_ERROR, - SUPPORT_SEND_COMMAND, + STATE_PAUSED, SUPPORT_START, SUPPORT_STATE, SUPPORT_STATUS, @@ -13,111 +19,134 @@ SUPPORT_TURN_ON, VacuumEntity, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_validation as cv, entity_platform +from homeassistant.helpers.entity import Entity from .const import DOMAIN -from .hub import LitterRobotEntity +from .entity import LitterRobotControlEntity +from .hub import LitterRobotHub SUPPORT_LITTERROBOT = ( - SUPPORT_SEND_COMMAND - | SUPPORT_START - | SUPPORT_STATE - | SUPPORT_STATUS - | SUPPORT_TURN_OFF - | SUPPORT_TURN_ON + SUPPORT_START | SUPPORT_STATE | SUPPORT_STATUS | SUPPORT_TURN_OFF | SUPPORT_TURN_ON ) TYPE_LITTER_BOX = "Litter Box" +SERVICE_RESET_WASTE_DRAWER = "reset_waste_drawer" +SERVICE_SET_SLEEP_MODE = "set_sleep_mode" +SERVICE_SET_WAIT_TIME = "set_wait_time" + -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: Callable[[list[Entity], bool], None], +) -> None: """Set up Litter-Robot cleaner using config entry.""" - hub = hass.data[DOMAIN][config_entry.entry_id] + hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] entities = [] for robot in hub.account.robots: - entities.append(LitterRobotCleaner(robot, TYPE_LITTER_BOX, hub)) - - if entities: - async_add_entities(entities, True) - - -class LitterRobotCleaner(LitterRobotEntity, VacuumEntity): + entities.append( + LitterRobotCleaner(robot=robot, entity_type=TYPE_LITTER_BOX, hub=hub) + ) + + async_add_entities(entities, True) + + platform = entity_platform.current_platform.get() + platform.async_register_entity_service( + SERVICE_RESET_WASTE_DRAWER, + {}, + "async_reset_waste_drawer", + ) + platform.async_register_entity_service( + SERVICE_SET_SLEEP_MODE, + { + vol.Required("enabled"): cv.boolean, + vol.Optional("start_time"): cv.time, + }, + "async_set_sleep_mode", + ) + platform.async_register_entity_service( + SERVICE_SET_WAIT_TIME, + {vol.Required("minutes"): vol.In(VALID_WAIT_TIMES)}, + "async_set_wait_time", + ) + + +class LitterRobotCleaner(LitterRobotControlEntity, VacuumEntity): """Litter-Robot "Vacuum" Cleaner.""" @property - def supported_features(self): + def supported_features(self) -> int: """Flag cleaner robot features that are supported.""" return SUPPORT_LITTERROBOT @property - def state(self): + def state(self) -> str: """Return the state of the cleaner.""" switcher = { - Robot.UnitStatus.CLEAN_CYCLE: STATE_CLEANING, - Robot.UnitStatus.EMPTY_CYCLE: STATE_CLEANING, - Robot.UnitStatus.CLEAN_CYCLE_COMPLETE: STATE_DOCKED, - Robot.UnitStatus.CAT_SENSOR_TIMING: STATE_DOCKED, - Robot.UnitStatus.DRAWER_FULL_1: STATE_DOCKED, - Robot.UnitStatus.DRAWER_FULL_2: STATE_DOCKED, - Robot.UnitStatus.READY: STATE_DOCKED, - Robot.UnitStatus.OFF: STATE_OFF, + LitterBoxStatus.CLEAN_CYCLE: STATE_CLEANING, + LitterBoxStatus.EMPTY_CYCLE: STATE_CLEANING, + LitterBoxStatus.CLEAN_CYCLE_COMPLETE: STATE_DOCKED, + LitterBoxStatus.CAT_SENSOR_TIMING: STATE_DOCKED, + LitterBoxStatus.DRAWER_FULL_1: STATE_DOCKED, + LitterBoxStatus.DRAWER_FULL_2: STATE_DOCKED, + LitterBoxStatus.READY: STATE_DOCKED, + LitterBoxStatus.CAT_SENSOR_INTERRUPTED: STATE_PAUSED, + LitterBoxStatus.OFF: STATE_OFF, } - return switcher.get(self.robot.unit_status, STATE_ERROR) + return switcher.get(self.robot.status, STATE_ERROR) @property - def status(self): + def status(self) -> str: """Return the status of the cleaner.""" - return f"{self.robot.unit_status.label}{' (Sleeping)' if self.robot.is_sleeping else ''}" + return ( + f"{self.robot.status.text}{' (Sleeping)' if self.robot.is_sleeping else ''}" + ) - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the cleaner on, starting a clean cycle.""" await self.perform_action_and_refresh(self.robot.set_power_status, True) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the unit off, stopping any cleaning in progress as is.""" await self.perform_action_and_refresh(self.robot.set_power_status, False) - async def async_start(self): + async def async_start(self) -> None: """Start a clean cycle.""" await self.perform_action_and_refresh(self.robot.start_cleaning) - async def async_send_command(self, command, params=None, **kwargs): - """Send command. - - Available commands: - - reset_waste_drawer - * params: none - - set_sleep_mode - * params: - - enabled: bool - - sleep_time: str (optional) - - """ - if command == "reset_waste_drawer": - # Normally we need to request a refresh of data after a command is sent. - # However, the API for resetting the waste drawer returns a refreshed - # data set for the robot. Thus, we only need to tell hass to update the - # state of devices associated with this robot. - await self.robot.reset_waste_drawer() - self.hub.coordinator.async_set_updated_data(True) - elif command == "set_sleep_mode": - await self.perform_action_and_refresh( - self.robot.set_sleep_mode, - params.get("enabled"), - self.parse_time_at_default_timezone(params.get("sleep_time")), - ) - else: - raise NotImplementedError() + async def async_reset_waste_drawer(self) -> None: + """Reset the waste drawer level.""" + await self.robot.reset_waste_drawer() + self.coordinator.async_set_updated_data(True) + + async def async_set_sleep_mode( + self, enabled: bool, start_time: str | None = None + ) -> None: + """Set the sleep mode.""" + await self.perform_action_and_refresh( + self.robot.set_sleep_mode, + enabled, + self.parse_time_at_default_timezone(start_time), + ) + + async def async_set_wait_time(self, minutes: int) -> None: + """Set the wait time.""" + await self.perform_action_and_refresh(self.robot.set_wait_time, minutes) @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return device specific state attributes.""" return { "clean_cycle_wait_time_minutes": self.robot.clean_cycle_wait_time_minutes, "is_sleeping": self.robot.is_sleeping, - "sleep_mode_active": self.robot.sleep_mode_active, + "sleep_mode_enabled": self.robot.sleep_mode_enabled, "power_status": self.robot.power_status, - "unit_status_code": self.robot.unit_status.value, + "status_code": self.robot.status_code, "last_seen": self.robot.last_seen, } diff --git a/requirements_all.txt b/requirements_all.txt index 998fcec879609..d10f866636e4e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1515,7 +1515,7 @@ pylibrespot-java==0.1.0 pylitejet==0.3.0 # homeassistant.components.litterrobot -pylitterbot==2021.2.8 +pylitterbot==2021.3.1 # homeassistant.components.loopenergy pyloopenergy==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 480435be44152..a0732f55aa25e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -826,7 +826,7 @@ pylibrespot-java==0.1.0 pylitejet==0.3.0 # homeassistant.components.litterrobot -pylitterbot==2021.2.8 +pylitterbot==2021.3.1 # homeassistant.components.lutron_caseta pylutron-caseta==0.9.0 diff --git a/tests/components/litterrobot/common.py b/tests/components/litterrobot/common.py index ed893a3a75686..19a6b5617c706 100644 --- a/tests/components/litterrobot/common.py +++ b/tests/components/litterrobot/common.py @@ -1,4 +1,6 @@ """Common utils for Litter-Robot tests.""" +from datetime import datetime + from homeassistant.components.litterrobot import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME @@ -9,7 +11,7 @@ ROBOT_SERIAL = "LR3C012345" ROBOT_DATA = { "powerStatus": "AC", - "lastSeen": "2021-02-01T15:30:00.000000", + "lastSeen": datetime.now().isoformat(), "cleanCycleWaitTimeMinutes": "7", "unitStatus": "RDY", "litterRobotNickname": ROBOT_NAME, @@ -22,3 +24,5 @@ "nightLightActive": "1", "sleepModeActive": "112:50:19", } + +VACUUM_ENTITY_ID = "vacuum.test_litter_box" diff --git a/tests/components/litterrobot/conftest.py b/tests/components/litterrobot/conftest.py index 11ed66fcb5297..237317545a168 100644 --- a/tests/components/litterrobot/conftest.py +++ b/tests/components/litterrobot/conftest.py @@ -1,60 +1,79 @@ """Configure pytest for Litter-Robot tests.""" -from __future__ import annotations - +from typing import Any, Optional from unittest.mock import AsyncMock, MagicMock, patch -import pylitterbot -from pylitterbot import Robot +from pylitterbot import Account, Robot +from pylitterbot.exceptions import InvalidCommandException import pytest from homeassistant.components import litterrobot +from homeassistant.core import HomeAssistant from .common import CONFIG, ROBOT_DATA from tests.common import MockConfigEntry -def create_mock_robot(unit_status_code: str | None = None): +def create_mock_robot( + robot_data: Optional[dict] = None, side_effect: Optional[Any] = None +) -> Robot: """Create a mock Litter-Robot device.""" - if not ( - unit_status_code - and Robot.UnitStatus(unit_status_code) != Robot.UnitStatus.UNKNOWN - ): - unit_status_code = ROBOT_DATA["unitStatus"] - - with patch.dict(ROBOT_DATA, {"unitStatus": unit_status_code}): - robot = Robot(data=ROBOT_DATA) - robot.start_cleaning = AsyncMock() - robot.set_power_status = AsyncMock() - robot.reset_waste_drawer = AsyncMock() - robot.set_sleep_mode = AsyncMock() - robot.set_night_light = AsyncMock() - robot.set_panel_lockout = AsyncMock() - return robot - - -def create_mock_account(unit_status_code: str | None = None): + if not robot_data: + robot_data = {} + + robot = Robot(data={**ROBOT_DATA, **robot_data}) + robot.start_cleaning = AsyncMock(side_effect=side_effect) + robot.set_power_status = AsyncMock(side_effect=side_effect) + robot.reset_waste_drawer = AsyncMock(side_effect=side_effect) + robot.set_sleep_mode = AsyncMock(side_effect=side_effect) + robot.set_night_light = AsyncMock(side_effect=side_effect) + robot.set_panel_lockout = AsyncMock(side_effect=side_effect) + robot.set_wait_time = AsyncMock(side_effect=side_effect) + return robot + + +def create_mock_account( + robot_data: Optional[dict] = None, + side_effect: Optional[Any] = None, + skip_robots: bool = False, +) -> MagicMock: """Create a mock Litter-Robot account.""" - account = MagicMock(spec=pylitterbot.Account) + account = MagicMock(spec=Account) account.connect = AsyncMock() account.refresh_robots = AsyncMock() - account.robots = [create_mock_robot(unit_status_code)] + account.robots = [] if skip_robots else [create_mock_robot(robot_data, side_effect)] return account @pytest.fixture -def mock_account(): +def mock_account() -> MagicMock: """Mock a Litter-Robot account.""" return create_mock_account() @pytest.fixture -def mock_account_with_error(): +def mock_account_with_no_robots() -> MagicMock: + """Mock a Litter-Robot account.""" + return create_mock_account(skip_robots=True) + + +@pytest.fixture +def mock_account_with_error() -> MagicMock: """Mock a Litter-Robot account with error.""" - return create_mock_account("BR") + return create_mock_account({"unitStatus": "BR"}) + + +@pytest.fixture +def mock_account_with_side_effects() -> MagicMock: + """Mock a Litter-Robot account with side effects.""" + return create_mock_account( + side_effect=InvalidCommandException("Invalid command: oops") + ) -async def setup_integration(hass, mock_account, platform_domain=None): +async def setup_integration( + hass: HomeAssistant, mock_account: MagicMock, platform_domain: Optional[str] = None +) -> MockConfigEntry: """Load a Litter-Robot platform with the provided hub.""" entry = MockConfigEntry( domain=litterrobot.DOMAIN, @@ -62,7 +81,9 @@ async def setup_integration(hass, mock_account, platform_domain=None): ) entry.add_to_hass(hass) - with patch("pylitterbot.Account", return_value=mock_account), patch( + with patch( + "homeassistant.components.litterrobot.hub.Account", return_value=mock_account + ), patch( "homeassistant.components.litterrobot.PLATFORMS", [platform_domain] if platform_domain else [], ): diff --git a/tests/components/litterrobot/test_config_flow.py b/tests/components/litterrobot/test_config_flow.py index 5068ecf721bdd..33b22b6a1bd44 100644 --- a/tests/components/litterrobot/test_config_flow.py +++ b/tests/components/litterrobot/test_config_flow.py @@ -20,7 +20,10 @@ async def test_form(hass, mock_account): assert result["type"] == "form" assert result["errors"] == {} - with patch("pylitterbot.Account", return_value=mock_account), patch( + with patch( + "homeassistant.components.litterrobot.hub.Account", + return_value=mock_account, + ), patch( "homeassistant.components.litterrobot.async_setup", return_value=True ) as mock_setup, patch( "homeassistant.components.litterrobot.async_setup_entry", diff --git a/tests/components/litterrobot/test_init.py b/tests/components/litterrobot/test_init.py index 7cd36f33883c1..22a6ea210223c 100644 --- a/tests/components/litterrobot/test_init.py +++ b/tests/components/litterrobot/test_init.py @@ -5,12 +5,18 @@ import pytest from homeassistant.components import litterrobot +from homeassistant.components.vacuum import ( + DOMAIN as VACUUM_DOMAIN, + SERVICE_START, + STATE_DOCKED, +) from homeassistant.config_entries import ( ENTRY_STATE_SETUP_ERROR, ENTRY_STATE_SETUP_RETRY, ) +from homeassistant.const import ATTR_ENTITY_ID -from .common import CONFIG +from .common import CONFIG, VACUUM_ENTITY_ID from .conftest import setup_integration from tests.common import MockConfigEntry @@ -18,7 +24,19 @@ async def test_unload_entry(hass, mock_account): """Test being able to unload an entry.""" - entry = await setup_integration(hass, mock_account) + entry = await setup_integration(hass, mock_account, VACUUM_DOMAIN) + + vacuum = hass.states.get(VACUUM_ENTITY_ID) + assert vacuum + assert vacuum.state == STATE_DOCKED + + await hass.services.async_call( + VACUUM_DOMAIN, + SERVICE_START, + {ATTR_ENTITY_ID: VACUUM_ENTITY_ID}, + blocking=True, + ) + getattr(mock_account.robots[0], "start_cleaning").assert_called_once() assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/litterrobot/test_sensor.py b/tests/components/litterrobot/test_sensor.py index 7f1570c553eeb..a5f5b955882fa 100644 --- a/tests/components/litterrobot/test_sensor.py +++ b/tests/components/litterrobot/test_sensor.py @@ -16,14 +16,13 @@ async def test_waste_drawer_sensor(hass, mock_account): sensor = hass.states.get(WASTE_DRAWER_ENTITY_ID) assert sensor - assert sensor.state == "50" + assert sensor.state == "50.0" assert sensor.attributes["unit_of_measurement"] == PERCENTAGE async def test_sleep_time_sensor_with_none_state(hass): """Tests the sleep mode start time sensor where sleep mode is inactive.""" - robot = create_mock_robot() - robot.sleep_mode_active = False + robot = create_mock_robot({"sleepModeActive": "0"}) sensor = LitterRobotSleepTimeSensor( robot, "Sleep Mode Start Time", Mock(), "sleep_mode_start_time" ) diff --git a/tests/components/litterrobot/test_switch.py b/tests/components/litterrobot/test_switch.py index 69154bef8f5c8..2659b1cc04994 100644 --- a/tests/components/litterrobot/test_switch.py +++ b/tests/components/litterrobot/test_switch.py @@ -3,7 +3,7 @@ import pytest -from homeassistant.components.litterrobot.hub import REFRESH_WAIT_TIME +from homeassistant.components.litterrobot.entity import REFRESH_WAIT_TIME_SECONDS from homeassistant.components.switch import ( DOMAIN as PLATFORM_DOMAIN, SERVICE_TURN_OFF, @@ -56,6 +56,6 @@ async def test_on_off_commands(hass, mock_account, entity_id, robot_command): blocking=True, ) - future = utcnow() + timedelta(seconds=REFRESH_WAIT_TIME) + future = utcnow() + timedelta(seconds=REFRESH_WAIT_TIME_SECONDS) async_fire_time_changed(hass, future) assert getattr(mock_account.robots[0], robot_command).call_count == count diff --git a/tests/components/litterrobot/test_vacuum.py b/tests/components/litterrobot/test_vacuum.py index 2db2ef21546be..67c526e4a3031 100644 --- a/tests/components/litterrobot/test_vacuum.py +++ b/tests/components/litterrobot/test_vacuum.py @@ -3,42 +3,60 @@ import pytest -from homeassistant.components.litterrobot.hub import REFRESH_WAIT_TIME +from homeassistant.components.litterrobot import DOMAIN +from homeassistant.components.litterrobot.entity import REFRESH_WAIT_TIME_SECONDS +from homeassistant.components.litterrobot.vacuum import ( + SERVICE_RESET_WASTE_DRAWER, + SERVICE_SET_SLEEP_MODE, + SERVICE_SET_WAIT_TIME, +) from homeassistant.components.vacuum import ( - ATTR_PARAMS, DOMAIN as PLATFORM_DOMAIN, - SERVICE_SEND_COMMAND, SERVICE_START, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_DOCKED, STATE_ERROR, ) -from homeassistant.const import ATTR_COMMAND, ATTR_ENTITY_ID +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant from homeassistant.util.dt import utcnow +from .common import VACUUM_ENTITY_ID from .conftest import setup_integration from tests.common import async_fire_time_changed -ENTITY_ID = "vacuum.test_litter_box" +COMPONENT_SERVICE_DOMAIN = { + SERVICE_RESET_WASTE_DRAWER: DOMAIN, + SERVICE_SET_SLEEP_MODE: DOMAIN, + SERVICE_SET_WAIT_TIME: DOMAIN, +} -async def test_vacuum(hass, mock_account): +async def test_vacuum(hass: HomeAssistant, mock_account): """Tests the vacuum entity was set up.""" await setup_integration(hass, mock_account, PLATFORM_DOMAIN) + assert hass.services.has_service(DOMAIN, SERVICE_RESET_WASTE_DRAWER) - vacuum = hass.states.get(ENTITY_ID) + vacuum = hass.states.get(VACUUM_ENTITY_ID) assert vacuum assert vacuum.state == STATE_DOCKED assert vacuum.attributes["is_sleeping"] is False -async def test_vacuum_with_error(hass, mock_account_with_error): +async def test_no_robots(hass: HomeAssistant, mock_account_with_no_robots): + """Tests the vacuum entity was set up.""" + await setup_integration(hass, mock_account_with_no_robots, PLATFORM_DOMAIN) + + assert not hass.services.has_service(DOMAIN, SERVICE_RESET_WASTE_DRAWER) + + +async def test_vacuum_with_error(hass: HomeAssistant, mock_account_with_error): """Tests a vacuum entity with an error.""" await setup_integration(hass, mock_account_with_error, PLATFORM_DOMAIN) - vacuum = hass.states.get(ENTITY_ID) + vacuum = hass.states.get(VACUUM_ENTITY_ID) assert vacuum assert vacuum.state == STATE_ERROR @@ -50,46 +68,70 @@ async def test_vacuum_with_error(hass, mock_account_with_error): (SERVICE_TURN_OFF, "set_power_status", None), (SERVICE_TURN_ON, "set_power_status", None), ( - SERVICE_SEND_COMMAND, + SERVICE_RESET_WASTE_DRAWER, "reset_waste_drawer", - {ATTR_COMMAND: "reset_waste_drawer"}, + None, ), ( - SERVICE_SEND_COMMAND, + SERVICE_SET_SLEEP_MODE, "set_sleep_mode", - { - ATTR_COMMAND: "set_sleep_mode", - ATTR_PARAMS: {"enabled": True, "sleep_time": "22:30"}, - }, + {"enabled": True, "start_time": "22:30"}, ), ( - SERVICE_SEND_COMMAND, + SERVICE_SET_SLEEP_MODE, "set_sleep_mode", - { - ATTR_COMMAND: "set_sleep_mode", - ATTR_PARAMS: {"enabled": True, "sleep_time": None}, - }, + {"enabled": True}, + ), + ( + SERVICE_SET_SLEEP_MODE, + "set_sleep_mode", + {"enabled": False}, + ), + ( + SERVICE_SET_WAIT_TIME, + "set_wait_time", + {"minutes": 3}, ), ], ) -async def test_commands(hass, mock_account, service, command, extra): +async def test_commands(hass: HomeAssistant, mock_account, service, command, extra): """Test sending commands to the vacuum.""" await setup_integration(hass, mock_account, PLATFORM_DOMAIN) - vacuum = hass.states.get(ENTITY_ID) + vacuum = hass.states.get(VACUUM_ENTITY_ID) assert vacuum assert vacuum.state == STATE_DOCKED - data = {ATTR_ENTITY_ID: ENTITY_ID} + data = {ATTR_ENTITY_ID: VACUUM_ENTITY_ID} if extra: data.update(extra) await hass.services.async_call( - PLATFORM_DOMAIN, + COMPONENT_SERVICE_DOMAIN.get(service, PLATFORM_DOMAIN), service, data, blocking=True, ) - future = utcnow() + timedelta(seconds=REFRESH_WAIT_TIME) + future = utcnow() + timedelta(seconds=REFRESH_WAIT_TIME_SECONDS) async_fire_time_changed(hass, future) getattr(mock_account.robots[0], command).assert_called_once() + + +async def test_invalid_commands( + hass: HomeAssistant, caplog, mock_account_with_side_effects +): + """Test sending invalid commands to the vacuum.""" + await setup_integration(hass, mock_account_with_side_effects, PLATFORM_DOMAIN) + + vacuum = hass.states.get(VACUUM_ENTITY_ID) + assert vacuum + assert vacuum.state == STATE_DOCKED + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_WAIT_TIME, + {ATTR_ENTITY_ID: VACUUM_ENTITY_ID, "minutes": 15}, + blocking=True, + ) + mock_account_with_side_effects.robots[0].set_wait_time.assert_called_once() + assert "Invalid command: oops" in caplog.text From b86bba246a8d3d0d01fd1e502d77d9168236ee12 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 11 Apr 2021 10:36:26 -1000 Subject: [PATCH 0196/1317] Downgrade logger message about homekit id missing (#49079) This can happen if the TXT record is received after the PTR record and should not generate a warning since it will get processed later --- homeassistant/components/homekit_controller/config_flow.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index fcf83918fda07..4a3deee4d117d 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -209,8 +209,11 @@ async def async_step_zeroconf(self, discovery_info): } if "id" not in properties: - _LOGGER.warning( - "HomeKit device %s: id not exposed, in violation of spec", properties + # This can happen if the TXT record is received after the PTR record + # we will wait for the next update in this case + _LOGGER.debug( + "HomeKit device %s: id not exposed; TXT record may have not yet been received", + properties, ) return self.async_abort(reason="invalid_properties") From 71e0e42792614afbb7d63f76214dc438a2b9231d Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Sun, 11 Apr 2021 22:36:44 +0200 Subject: [PATCH 0197/1317] Add Rituals Perfume Genie sensor platform (#48270) Co-authored-by: J. Nick Koston --- .coveragerc | 2 + .../rituals_perfume_genie/__init__.py | 47 +++-- .../components/rituals_perfume_genie/const.py | 7 +- .../rituals_perfume_genie/entity.py | 44 +++++ .../rituals_perfume_genie/sensor.py | 168 ++++++++++++++++++ .../rituals_perfume_genie/switch.py | 93 ++++------ .../rituals_perfume_genie/test_config_flow.py | 3 - 7 files changed, 290 insertions(+), 74 deletions(-) create mode 100644 homeassistant/components/rituals_perfume_genie/entity.py create mode 100644 homeassistant/components/rituals_perfume_genie/sensor.py diff --git a/.coveragerc b/.coveragerc index 3d126dfd23b40..982db1eeade82 100644 --- a/.coveragerc +++ b/.coveragerc @@ -826,6 +826,8 @@ omit = homeassistant/components/rest/switch.py homeassistant/components/ring/camera.py homeassistant/components/ripple/sensor.py + homeassistant/components/rituals_perfume_genie/entity.py + homeassistant/components/rituals_perfume_genie/sensor.py homeassistant/components/rituals_perfume_genie/switch.py homeassistant/components/rituals_perfume_genie/__init__.py homeassistant/components/rocketchat/notify.py diff --git a/homeassistant/components/rituals_perfume_genie/__init__.py b/homeassistant/components/rituals_perfume_genie/__init__.py index f2fd13a9ef451..610700e8fe5af 100644 --- a/homeassistant/components/rituals_perfume_genie/__init__.py +++ b/homeassistant/components/rituals_perfume_genie/__init__.py @@ -1,5 +1,6 @@ """The Rituals Perfume Genie integration.""" import asyncio +from datetime import timedelta import logging from aiohttp.client_exceptions import ClientConnectorError @@ -9,19 +10,16 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import ACCOUNT_HASH, DOMAIN +from .const import ACCOUNT_HASH, COORDINATORS, DEVICES, DOMAIN, HUB, HUBLOT -_LOGGER = logging.getLogger(__name__) +PLATFORMS = ["switch", "sensor"] EMPTY_CREDENTIALS = "" -PLATFORMS = ["switch"] - - -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Rituals Perfume Genie component.""" - return True +_LOGGER = logging.getLogger(__name__) +UPDATE_INTERVAL = timedelta(seconds=30) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): @@ -31,11 +29,40 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): account.data = {ACCOUNT_HASH: entry.data.get(ACCOUNT_HASH)} try: - await account.get_devices() + account_devices = await account.get_devices() except ClientConnectorError as ex: raise ConfigEntryNotReady from ex - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = account + hublots = [] + devices = {} + for device in account_devices: + hublot = device.data[HUB][HUBLOT] + hublots.append(hublot) + devices[hublot] = device + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { + COORDINATORS: {}, + DEVICES: devices, + } + + for hublot in hublots: + device = hass.data[DOMAIN][entry.entry_id][DEVICES][hublot] + + async def async_update_data(): + await device.update_data() + return device.data + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name=f"{DOMAIN}-{hublot}", + update_method=async_update_data, + update_interval=UPDATE_INTERVAL, + ) + + await coordinator.async_refresh() + + hass.data[DOMAIN][entry.entry_id][COORDINATORS][hublot] = coordinator for platform in PLATFORMS: hass.async_create_task( diff --git a/homeassistant/components/rituals_perfume_genie/const.py b/homeassistant/components/rituals_perfume_genie/const.py index 075d79ec8de9e..16189c8335eda 100644 --- a/homeassistant/components/rituals_perfume_genie/const.py +++ b/homeassistant/components/rituals_perfume_genie/const.py @@ -1,5 +1,10 @@ """Constants for the Rituals Perfume Genie integration.""" - DOMAIN = "rituals_perfume_genie" +COORDINATORS = "coordinators" +DEVICES = "devices" + ACCOUNT_HASH = "account_hash" +ATTRIBUTES = "attributes" +HUB = "hub" +HUBLOT = "hublot" diff --git a/homeassistant/components/rituals_perfume_genie/entity.py b/homeassistant/components/rituals_perfume_genie/entity.py new file mode 100644 index 0000000000000..ba8f583d04217 --- /dev/null +++ b/homeassistant/components/rituals_perfume_genie/entity.py @@ -0,0 +1,44 @@ +"""Base class for Rituals Perfume Genie diffuser entity.""" +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import ATTRIBUTES, DOMAIN, HUB, HUBLOT + +MANUFACTURER = "Rituals Cosmetics" +MODEL = "Diffuser" + +SENSORS = "sensors" +ROOMNAME = "roomnamec" +VERSION = "versionc" + + +class DiffuserEntity(CoordinatorEntity): + """Representation of a diffuser entity.""" + + def __init__(self, diffuser, coordinator, entity_suffix): + """Init from config, hookup diffuser and coordinator.""" + super().__init__(coordinator) + self._diffuser = diffuser + self._entity_suffix = entity_suffix + self._hublot = self.coordinator.data[HUB][HUBLOT] + self._hubname = self.coordinator.data[HUB][ATTRIBUTES][ROOMNAME] + + @property + def unique_id(self): + """Return the unique ID of the entity.""" + return f"{self._hublot}{self._entity_suffix}" + + @property + def name(self): + """Return the name of the entity.""" + return f"{self._hubname}{self._entity_suffix}" + + @property + def device_info(self): + """Return information about the device.""" + return { + "name": self._hubname, + "identifiers": {(DOMAIN, self._hublot)}, + "manufacturer": MANUFACTURER, + "model": MODEL, + "sw_version": self.coordinator.data[HUB][SENSORS][VERSION], + } diff --git a/homeassistant/components/rituals_perfume_genie/sensor.py b/homeassistant/components/rituals_perfume_genie/sensor.py new file mode 100644 index 0000000000000..4a3ac34cc5872 --- /dev/null +++ b/homeassistant/components/rituals_perfume_genie/sensor.py @@ -0,0 +1,168 @@ +"""Support for Rituals Perfume Genie sensors.""" +from homeassistant.const import ( + ATTR_BATTERY_CHARGING, + ATTR_BATTERY_LEVEL, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_SIGNAL_STRENGTH, + PERCENTAGE, +) + +from .const import COORDINATORS, DEVICES, DOMAIN, HUB +from .entity import SENSORS, DiffuserEntity + +ID = "id" +TITLE = "title" +ICON = "icon" +WIFI = "wific" +BATTERY = "battc" +PERFUME = "rfidc" +FILL = "fillc" + +BATTERY_CHARGING_ID = 21 +PERFUME_NO_CARTRIDGE_ID = 19 +FILL_NO_CARTRIDGE_ID = 12 + +BATTERY_SUFFIX = " Battery" +PERFUME_SUFFIX = " Perfume" +FILL_SUFFIX = " Fill" +WIFI_SUFFIX = " Wifi" + +ATTR_SIGNAL_STRENGTH = "signal_strength" + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the diffuser sensors.""" + diffusers = hass.data[DOMAIN][config_entry.entry_id][DEVICES] + coordinators = hass.data[DOMAIN][config_entry.entry_id][COORDINATORS] + entities = [] + for hublot, diffuser in diffusers.items(): + coordinator = coordinators[hublot] + entities.append(DiffuserPerfumeSensor(diffuser, coordinator)) + entities.append(DiffuserFillSensor(diffuser, coordinator)) + entities.append(DiffuserWifiSensor(diffuser, coordinator)) + if BATTERY in diffuser.data[HUB][SENSORS]: + entities.append(DiffuserBatterySensor(diffuser, coordinator)) + + async_add_entities(entities) + + +class DiffuserPerfumeSensor(DiffuserEntity): + """Representation of a diffuser perfume sensor.""" + + def __init__(self, diffuser, coordinator): + """Initialize the perfume sensor.""" + super().__init__(diffuser, coordinator, PERFUME_SUFFIX) + + @property + def icon(self): + """Return the perfume sensor icon.""" + if self.coordinator.data[HUB][SENSORS][PERFUME][ID] == PERFUME_NO_CARTRIDGE_ID: + return "mdi:tag-remove" + return "mdi:tag-text" + + @property + def state(self): + """Return the state of the perfume sensor.""" + return self.coordinator.data[HUB][SENSORS][PERFUME][TITLE] + + +class DiffuserFillSensor(DiffuserEntity): + """Representation of a diffuser fill sensor.""" + + def __init__(self, diffuser, coordinator): + """Initialize the fill sensor.""" + super().__init__(diffuser, coordinator, FILL_SUFFIX) + + @property + def icon(self): + """Return the fill sensor icon.""" + if self.coordinator.data[HUB][SENSORS][FILL][ID] == FILL_NO_CARTRIDGE_ID: + return "mdi:beaker-question" + return "mdi:beaker" + + @property + def state(self): + """Return the state of the fill sensor.""" + return self.coordinator.data[HUB][SENSORS][FILL][TITLE] + + +class DiffuserBatterySensor(DiffuserEntity): + """Representation of a diffuser battery sensor.""" + + def __init__(self, diffuser, coordinator): + """Initialize the battery sensor.""" + super().__init__(diffuser, coordinator, BATTERY_SUFFIX) + + @property + def state(self): + """Return the state of the battery sensor.""" + # Use ICON because TITLE may change in the future. + # ICON filename does not match the image. + return { + "battery-charge.png": 100, + "battery-full.png": 100, + "battery-75.png": 50, + "battery-50.png": 25, + "battery-low.png": 10, + }[self.coordinator.data[HUB][SENSORS][BATTERY][ICON]] + + @property + def _charging(self): + """Return battery charging state.""" + return bool( + self.coordinator.data[HUB][SENSORS][BATTERY][ID] == BATTERY_CHARGING_ID + ) + + @property + def device_class(self): + """Return the class of the battery sensor.""" + return DEVICE_CLASS_BATTERY + + @property + def extra_state_attributes(self): + """Return the battery state attributes.""" + return { + ATTR_BATTERY_LEVEL: self.coordinator.data[HUB][SENSORS][BATTERY][TITLE], + ATTR_BATTERY_CHARGING: self._charging, + } + + @property + def unit_of_measurement(self): + """Return the battery unit of measurement.""" + return PERCENTAGE + + +class DiffuserWifiSensor(DiffuserEntity): + """Representation of a diffuser wifi sensor.""" + + def __init__(self, diffuser, coordinator): + """Initialize the wifi sensor.""" + super().__init__(diffuser, coordinator, WIFI_SUFFIX) + + @property + def state(self): + """Return the state of the wifi sensor.""" + # Use ICON because TITLE may change in the future. + return { + "icon-signal.png": 100, + "icon-signal-75.png": 70, + "icon-signal-low.png": 25, + "icon-signal-0.png": 0, + }[self.coordinator.data[HUB][SENSORS][WIFI][ICON]] + + @property + def device_class(self): + """Return the class of the wifi sensor.""" + return DEVICE_CLASS_SIGNAL_STRENGTH + + @property + def extra_state_attributes(self): + """Return the wifi state attributes.""" + return { + ATTR_SIGNAL_STRENGTH: self.coordinator.data[HUB][SENSORS][WIFI][TITLE], + } + + @property + def unit_of_measurement(self): + """Return the wifi unit of measurement.""" + return PERCENTAGE diff --git a/homeassistant/components/rituals_perfume_genie/switch.py b/homeassistant/components/rituals_perfume_genie/switch.py index bc8e2b5e17599..d1fff166f6e0c 100644 --- a/homeassistant/components/rituals_perfume_genie/switch.py +++ b/homeassistant/components/rituals_perfume_genie/switch.py @@ -1,104 +1,77 @@ """Support for Rituals Perfume Genie switches.""" -from datetime import timedelta -import logging - -import aiohttp - from homeassistant.components.switch import SwitchEntity +from homeassistant.core import callback -from .const import DOMAIN +from .const import ATTRIBUTES, COORDINATORS, DEVICES, DOMAIN, HUB +from .entity import DiffuserEntity -_LOGGER = logging.getLogger(__name__) - -SCAN_INTERVAL = timedelta(seconds=30) +STATUS = "status" +FAN = "fanc" +SPEED = "speedc" +ROOM = "roomc" ON_STATE = "1" AVAILABLE_STATE = 1 -MANUFACTURER = "Rituals Cosmetics" -MODEL = "Diffuser" -ICON = "mdi:fan" - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the diffuser switch.""" - account = hass.data[DOMAIN][config_entry.entry_id] - diffusers = await account.get_devices() - + diffusers = hass.data[DOMAIN][config_entry.entry_id][DEVICES] + coordinators = hass.data[DOMAIN][config_entry.entry_id][COORDINATORS] entities = [] - for diffuser in diffusers: - entities.append(DiffuserSwitch(diffuser)) + for hublot, diffuser in diffusers.items(): + coordinator = coordinators[hublot] + entities.append(DiffuserSwitch(diffuser, coordinator)) - async_add_entities(entities, True) + async_add_entities(entities) -class DiffuserSwitch(SwitchEntity): +class DiffuserSwitch(SwitchEntity, DiffuserEntity): """Representation of a diffuser switch.""" - def __init__(self, diffuser): - """Initialize the switch.""" - self._diffuser = diffuser - self._available = True - - @property - def device_info(self): - """Return information about the device.""" - return { - "name": self._diffuser.data["hub"]["attributes"]["roomnamec"], - "identifiers": {(DOMAIN, self._diffuser.data["hub"]["hublot"])}, - "manufacturer": MANUFACTURER, - "model": MODEL, - "sw_version": self._diffuser.data["hub"]["sensors"]["versionc"], - } - - @property - def unique_id(self): - """Return the unique ID of the device.""" - return self._diffuser.data["hub"]["hublot"] + def __init__(self, diffuser, coordinator): + """Initialize the diffuser switch.""" + super().__init__(diffuser, coordinator, "") + self._is_on = self.coordinator.data[HUB][ATTRIBUTES][FAN] == ON_STATE @property def available(self): """Return if the device is available.""" - return self._available - - @property - def name(self): - """Return the name of the device.""" - return self._diffuser.data["hub"]["attributes"]["roomnamec"] + return self.coordinator.data[HUB][STATUS] == AVAILABLE_STATE @property def icon(self): """Return the icon of the device.""" - return ICON + return "mdi:fan" @property def extra_state_attributes(self): """Return the device state attributes.""" attributes = { - "fan_speed": self._diffuser.data["hub"]["attributes"]["speedc"], - "room_size": self._diffuser.data["hub"]["attributes"]["roomc"], + "fan_speed": self.coordinator.data[HUB][ATTRIBUTES][SPEED], + "room_size": self.coordinator.data[HUB][ATTRIBUTES][ROOM], } return attributes @property def is_on(self): """If the device is currently on or off.""" - return self._diffuser.data["hub"]["attributes"]["fanc"] == ON_STATE + return self._is_on async def async_turn_on(self, **kwargs): """Turn the device on.""" await self._diffuser.turn_on() + self._is_on = True + self.schedule_update_ha_state() async def async_turn_off(self, **kwargs): """Turn the device off.""" await self._diffuser.turn_off() - - async def async_update(self): - """Update the data of the device.""" - try: - await self._diffuser.update_data() - except aiohttp.ClientError: - self._available = False - _LOGGER.error("Unable to retrieve data from rituals.sense-company.com") - else: - self._available = self._diffuser.data["hub"]["status"] == AVAILABLE_STATE + self._is_on = False + self.schedule_update_ha_state() + + @callback + def _handle_coordinator_update(self): + """Handle updated data from the coordinator.""" + self._is_on = self.coordinator.data[HUB][ATTRIBUTES][FAN] == ON_STATE + self.async_write_ha_state() diff --git a/tests/components/rituals_perfume_genie/test_config_flow.py b/tests/components/rituals_perfume_genie/test_config_flow.py index 92c3e15c2478c..e5c64dd54c91c 100644 --- a/tests/components/rituals_perfume_genie/test_config_flow.py +++ b/tests/components/rituals_perfume_genie/test_config_flow.py @@ -32,8 +32,6 @@ async def test_form(hass): "homeassistant.components.rituals_perfume_genie.config_flow.Account", side_effect=_mock_account, ), patch( - "homeassistant.components.rituals_perfume_genie.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.rituals_perfume_genie.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -49,7 +47,6 @@ async def test_form(hass): assert result2["type"] == "create_entry" assert result2["title"] == TEST_EMAIL assert isinstance(result2["data"][ACCOUNT_HASH], str) - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 From 1d28f485d3125bae9457294f3733f45ffef1bc86 Mon Sep 17 00:00:00 2001 From: mptei Date: Sun, 11 Apr 2021 23:01:30 +0200 Subject: [PATCH 0198/1317] Patch ip interface instead of XKNX in knx (#49064) * knx: Deeper tests. * Set rate_limit to 0; removed waiting for queue --- tests/components/knx/__init__.py | 26 +++++++++++ tests/components/knx/conftest.py | 15 ++++++ tests/components/knx/test_expose.py | 72 ++++++++++------------------- 3 files changed, 65 insertions(+), 48 deletions(-) create mode 100644 tests/components/knx/conftest.py diff --git a/tests/components/knx/__init__.py b/tests/components/knx/__init__.py index eaa84714dc5a3..1c9bfaf15b898 100644 --- a/tests/components/knx/__init__.py +++ b/tests/components/knx/__init__.py @@ -1 +1,27 @@ """Tests for the KNX integration.""" + +from unittest.mock import DEFAULT, patch + +from homeassistant.components.knx.const import DOMAIN as KNX_DOMAIN +from homeassistant.setup import async_setup_component + + +async def setup_knx_integration(hass, knx_ip_interface, config=None): + """Create the KNX gateway.""" + if config is None: + config = {} + + # To get the XKNX object from the constructor call + def side_effect(*args, **kwargs): + knx_ip_interface.xknx = args[0] + # switch off rate delimiter + knx_ip_interface.xknx.rate_limit = 0 + return DEFAULT + + with patch( + "xknx.xknx.KNXIPInterface", + return_value=knx_ip_interface, + side_effect=side_effect, + ): + await async_setup_component(hass, KNX_DOMAIN, {KNX_DOMAIN: config}) + await hass.async_block_till_done() diff --git a/tests/components/knx/conftest.py b/tests/components/knx/conftest.py new file mode 100644 index 0000000000000..b7c27774f7862 --- /dev/null +++ b/tests/components/knx/conftest.py @@ -0,0 +1,15 @@ +"""conftest for knx.""" + +from unittest.mock import AsyncMock, Mock + +import pytest + + +@pytest.fixture(autouse=True) +def knx_ip_interface_mock(): + """Create a knx ip interface mock.""" + mock = Mock() + mock.start = AsyncMock() + mock.stop = AsyncMock() + mock.send_telegram = AsyncMock() + return mock diff --git a/tests/components/knx/test_expose.py b/tests/components/knx/test_expose.py index 1590a7bb2465e..908ef0a56f8ed 100644 --- a/tests/components/knx/test_expose.py +++ b/tests/components/knx/test_expose.py @@ -1,46 +1,18 @@ """Test knx expose.""" -from unittest.mock import AsyncMock, Mock, patch -import pytest - -from homeassistant.components.knx import ( - CONF_KNX_EXPOSE, - CONFIG_SCHEMA as KNX_CONFIG_SCHEMA, - KNX_ADDRESS, -) -from homeassistant.components.knx.const import DOMAIN as KNX_DOMAIN +from homeassistant.components.knx import CONF_KNX_EXPOSE, KNX_ADDRESS from homeassistant.const import CONF_ATTRIBUTE, CONF_ENTITY_ID, CONF_TYPE -from homeassistant.setup import async_setup_component - - -async def setup_knx_integration(hass, knx_mock, config=None): - """Create the KNX gateway.""" - if config is None: - config = {} - with patch("homeassistant.components.knx.XKNX", return_value=knx_mock): - await async_setup_component( - hass, KNX_DOMAIN, KNX_CONFIG_SCHEMA({KNX_DOMAIN: config}) - ) - await hass.async_block_till_done() +from . import setup_knx_integration -@pytest.fixture(autouse=True) -def xknx_mock(): - """Create a simple XKNX mock.""" - xknx_mock = Mock() - xknx_mock.telegrams = AsyncMock() - xknx_mock.start = AsyncMock() - xknx_mock.stop = AsyncMock() - return xknx_mock - -async def test_binary_expose(hass, xknx_mock): +async def test_binary_expose(hass, knx_ip_interface_mock): """Test that a binary expose sends only telegrams on state change.""" entity_id = "fake.entity" await setup_knx_integration( hass, - xknx_mock, + knx_ip_interface_mock, { CONF_KNX_EXPOSE: { CONF_TYPE: "binary", @@ -52,33 +24,37 @@ async def test_binary_expose(hass, xknx_mock): assert not hass.states.async_all() # Change state to on - xknx_mock.reset_mock() + knx_ip_interface_mock.reset_mock() hass.states.async_set(entity_id, "on", {}) await hass.async_block_till_done() - assert xknx_mock.telegrams.put.call_count == 1, "Expected telegram for state change" + assert ( + knx_ip_interface_mock.send_telegram.call_count == 1 + ), "Expected telegram for state change" # Change attribute; keep state - xknx_mock.reset_mock() + knx_ip_interface_mock.reset_mock() hass.states.async_set(entity_id, "on", {"brightness": 180}) await hass.async_block_till_done() assert ( - xknx_mock.telegrams.put.call_count == 0 + knx_ip_interface_mock.send_telegram.call_count == 0 ), "Expected no telegram; state not changed" # Change attribute and state - xknx_mock.reset_mock() + knx_ip_interface_mock.reset_mock() hass.states.async_set(entity_id, "off", {"brightness": 0}) await hass.async_block_till_done() - assert xknx_mock.telegrams.put.call_count == 1, "Expected telegram for state change" + assert ( + knx_ip_interface_mock.send_telegram.call_count == 1 + ), "Expected telegram for state change" -async def test_expose_attribute(hass, xknx_mock): +async def test_expose_attribute(hass, knx_ip_interface_mock): """Test that an expose sends only telegrams on attribute change.""" entity_id = "fake.entity" attribute = "fake_attribute" await setup_knx_integration( hass, - xknx_mock, + knx_ip_interface_mock, { CONF_KNX_EXPOSE: { CONF_TYPE: "percentU8", @@ -91,25 +67,25 @@ async def test_expose_attribute(hass, xknx_mock): assert not hass.states.async_all() # Change state to on; no attribute - xknx_mock.reset_mock() + knx_ip_interface_mock.reset_mock() hass.states.async_set(entity_id, "on", {}) await hass.async_block_till_done() - assert xknx_mock.telegrams.put.call_count == 0 + assert knx_ip_interface_mock.send_telegram.call_count == 0 # Change attribute; keep state - xknx_mock.reset_mock() + knx_ip_interface_mock.reset_mock() hass.states.async_set(entity_id, "on", {attribute: 1}) await hass.async_block_till_done() - assert xknx_mock.telegrams.put.call_count == 1 + assert knx_ip_interface_mock.send_telegram.call_count == 1 # Change state keep attribute - xknx_mock.reset_mock() + knx_ip_interface_mock.reset_mock() hass.states.async_set(entity_id, "off", {attribute: 1}) await hass.async_block_till_done() - assert xknx_mock.telegrams.put.call_count == 0 + assert knx_ip_interface_mock.send_telegram.call_count == 0 # Change state and attribute - xknx_mock.reset_mock() + knx_ip_interface_mock.reset_mock() hass.states.async_set(entity_id, "on", {attribute: 0}) await hass.async_block_till_done() - assert xknx_mock.telegrams.put.call_count == 1 + assert knx_ip_interface_mock.send_telegram.call_count == 1 From 41ff6fc27846e4ead0cc0a3648b80317020a8c31 Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Sun, 11 Apr 2021 15:12:59 -0700 Subject: [PATCH 0199/1317] Catch unknown equipment values (#49073) * Catch unknown equipment values * Catch unknown equipment values * Remove warning spam. --- homeassistant/components/screenlogic/__init__.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/screenlogic/__init__.py b/homeassistant/components/screenlogic/__init__.py index 11f71cfd33711..c5c082cd509d0 100644 --- a/homeassistant/components/screenlogic/__init__.py +++ b/homeassistant/components/screenlogic/__init__.py @@ -195,9 +195,15 @@ def device_info(self): """Return device information for the controller.""" controller_type = self.config_data["controller_type"] hardware_type = self.config_data["hardware_type"] + try: + equipment_model = EQUIPMENT.CONTROLLER_HARDWARE[controller_type][ + hardware_type + ] + except KeyError: + equipment_model = f"Unknown Model C:{controller_type} H:{hardware_type}" return { "connections": {(dr.CONNECTION_NETWORK_MAC, self.mac)}, "name": self.gateway_name, "manufacturer": "Pentair", - "model": EQUIPMENT.CONTROLLER_HARDWARE[controller_type][hardware_type], + "model": equipment_model, } From 74d7293ab8f481c61905e24a2f61586d370aaa76 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 12 Apr 2021 01:53:07 +0200 Subject: [PATCH 0200/1317] mqtt fan percentage to speed_range and received speed_state fix (#49060) * percentage to speed_range and get speed state fix * Update homeassistant/components/mqtt/fan.py * Update homeassistant/components/mqtt/fan.py * Update homeassistant/components/mqtt/fan.py * Update homeassistant/components/mqtt/fan.py Co-authored-by: J. Nick Koston --- homeassistant/components/mqtt/fan.py | 16 ++++++++-------- tests/components/mqtt/test_fan.py | 20 ++++++++++++++++++-- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 6009b941c5c3d..24c4c805dfda2 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -1,6 +1,7 @@ """Support for MQTT fans.""" import functools import logging +import math import voluptuous as vol @@ -441,13 +442,12 @@ def speed_received(msg): ) return - if not self._feature_percentage: - if speed in self._legacy_speeds_list_no_off: - self._percentage = ordered_list_item_to_percentage( - self._legacy_speeds_list_no_off, speed - ) - elif speed == SPEED_OFF: - self._percentage = 0 + if speed in self._legacy_speeds_list_no_off: + self._percentage = ordered_list_item_to_percentage( + self._legacy_speeds_list_no_off, speed + ) + elif speed == SPEED_OFF: + self._percentage = 0 self.async_write_ha_state() @@ -592,7 +592,7 @@ async def async_set_percentage(self, percentage: int) -> None: This method is a coroutine. """ - percentage_payload = int( + percentage_payload = math.ceil( percentage_to_ranged_value(self._speed_range, percentage) ) mqtt_payload = self._command_templates[ATTR_PERCENTAGE](percentage_payload) diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index 5caec9b7473f7..bfa1f387bcd59 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -618,7 +618,7 @@ async def test_sending_mqtt_commands_with_alternate_speed_range(hass, mqtt_mock) "percentage_state_topic": "percentage-state-topic1", "percentage_command_topic": "percentage-command-topic1", "speed_range_min": 1, - "speed_range_max": 100, + "speed_range_max": 3, }, { "platform": "mqtt", @@ -651,9 +651,25 @@ async def test_sending_mqtt_commands_with_alternate_speed_range(hass, mqtt_mock) state = hass.states.get("fan.test1") assert state.attributes.get(ATTR_ASSUMED_STATE) + await common.async_set_percentage(hass, "fan.test1", 33) + mqtt_mock.async_publish.assert_called_once_with( + "percentage-command-topic1", "1", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("fan.test1") + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await common.async_set_percentage(hass, "fan.test1", 66) + mqtt_mock.async_publish.assert_called_once_with( + "percentage-command-topic1", "2", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("fan.test1") + assert state.attributes.get(ATTR_ASSUMED_STATE) + await common.async_set_percentage(hass, "fan.test1", 100) mqtt_mock.async_publish.assert_called_once_with( - "percentage-command-topic1", "100", 0, False + "percentage-command-topic1", "3", 0, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("fan.test1") From 1145856c45a10d5cd163d25d946d39fe37f7bf24 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 12 Apr 2021 01:53:44 +0200 Subject: [PATCH 0201/1317] Fix cast options flow overwriting data (#49051) --- homeassistant/components/cast/config_flow.py | 2 +- tests/components/cast/test_config_flow.py | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/cast/config_flow.py b/homeassistant/components/cast/config_flow.py index 464283e07f30c..86d85588967e3 100644 --- a/homeassistant/components/cast/config_flow.py +++ b/homeassistant/components/cast/config_flow.py @@ -133,7 +133,7 @@ async def async_step_options(self, user_input=None): ) if not bad_cec and not bad_hosts and not bad_uuid: - updated_config = {} + updated_config = dict(current_config) updated_config[CONF_IGNORE_CEC] = ignore_cec updated_config[CONF_KNOWN_HOSTS] = known_hosts updated_config[CONF_UUID] = wanted_uuid diff --git a/tests/components/cast/test_config_flow.py b/tests/components/cast/test_config_flow.py index 064406df717aa..1febd9d880324 100644 --- a/tests/components/cast/test_config_flow.py +++ b/tests/components/cast/test_config_flow.py @@ -166,6 +166,7 @@ async def test_option_flow(hass, parameter_data): assert result["step_id"] == "options" data_schema = result["data_schema"].schema assert set(data_schema) == {"known_hosts"} + orig_data = dict(config_entry.data) # Reconfigure ignore_cec, known_hosts, uuid context = {"source": "user", "show_advanced_options": True} @@ -201,7 +202,12 @@ async def test_option_flow(hass, parameter_data): ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["data"] is None - assert config_entry.data == {"ignore_cec": [], "known_hosts": [], "uuid": []} + assert config_entry.data == { + **orig_data, + "ignore_cec": [], + "known_hosts": [], + "uuid": [], + } async def test_known_hosts(hass, castbrowser_mock, castbrowser_constructor_mock): From 9585defca014a29f101ece2d379fd32cf0cdb72c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 12 Apr 2021 01:54:43 +0200 Subject: [PATCH 0202/1317] Add device_tracker scanners to hass.config.components (#49063) --- homeassistant/components/device_tracker/legacy.py | 8 +++----- tests/components/device_tracker/test_init.py | 3 +++ 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/device_tracker/legacy.py b/homeassistant/components/device_tracker/legacy.py index a90d92944a4d8..2614bd4228a0f 100644 --- a/homeassistant/components/device_tracker/legacy.py +++ b/homeassistant/components/device_tracker/legacy.py @@ -250,21 +250,19 @@ async def async_setup_legacy(self, hass, tracker, discovery_info=None): else: raise HomeAssistantError("Invalid legacy device_tracker platform.") - if setup: - hass.config.components.add(full_name) - if scanner: async_setup_scanner_platform( hass, self.config, scanner, tracker.async_see, self.type ) - return - if not setup: + if not setup and not scanner: LOGGER.error( "Error setting up platform %s %s", self.type, self.name ) return + hass.config.components.add(full_name) + except Exception: # pylint: disable=broad-except LOGGER.exception( "Error setting up platform %s %s", self.type, self.name diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index af0c7658ac77e..6155ed7d1db56 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -120,6 +120,7 @@ async def test_reading_yaml_config(hass, yaml_devices): assert device.config_picture == config.config_picture assert device.consider_home == config.consider_home assert device.icon == config.icon + assert f"{device_tracker.DOMAIN}.test" in hass.config.components @patch("homeassistant.components.device_tracker.const.LOGGER.warning") @@ -558,6 +559,8 @@ async def test_bad_platform(hass): with assert_setup_component(0, device_tracker.DOMAIN): assert await async_setup_component(hass, device_tracker.DOMAIN, config) + assert f"{device_tracker.DOMAIN}.bad_platform" not in hass.config.components + async def test_adding_unknown_device_to_config(mock_device_tracker_conf, hass): """Test the adding of unknown devices to configuration file.""" From c7d19d511501209b202f5837b6ff06192906af89 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Mon, 12 Apr 2021 00:04:19 +0000 Subject: [PATCH 0203/1317] [ci skip] Translation update --- .../components/airvisual/translations/sv.json | 6 --- .../components/august/translations/sv.json | 11 ++++ .../components/climacell/translations/sv.json | 14 +++++ .../components/deconz/translations/sv.json | 4 ++ .../components/emonitor/translations/sv.json | 11 ++++ .../enphase_envoy/translations/sv.json | 19 +++++++ .../components/ezviz/translations/pl.json | 52 +++++++++++++++++++ .../components/ezviz/translations/sv.json | 30 +++++++++++ .../faa_delays/translations/sv.json | 15 ++++++ .../google_travel_time/translations/sv.json | 27 ++++++++++ .../components/hive/translations/sv.json | 21 ++++++++ .../home_plus_control/translations/sv.json | 9 ++++ .../huisbaasje/translations/sv.json | 7 +++ .../components/ialarm/translations/en.json | 6 +-- .../components/ialarm/translations/fr.json | 20 +++++++ .../kostal_plenticore/translations/sv.json | 18 +++++++ .../met_eireann/translations/sv.json | 14 +++++ .../components/mullvad/translations/sv.json | 19 +++++++ .../components/netatmo/translations/sv.json | 5 ++ .../components/nut/translations/sv.json | 4 ++ .../openweathermap/translations/sv.json | 2 - .../philips_js/translations/sv.json | 15 ++++++ .../screenlogic/translations/sv.json | 17 ++++++ .../components/upb/translations/sv.json | 2 +- .../components/verisure/translations/sv.json | 19 +++++++ .../water_heater/translations/sv.json | 7 +++ .../waze_travel_time/translations/sv.json | 26 ++++++++++ .../components/wolflink/translations/sv.json | 12 +++++ .../components/zha/translations/ru.json | 2 +- 29 files changed, 401 insertions(+), 13 deletions(-) create mode 100644 homeassistant/components/climacell/translations/sv.json create mode 100644 homeassistant/components/emonitor/translations/sv.json create mode 100644 homeassistant/components/enphase_envoy/translations/sv.json create mode 100644 homeassistant/components/ezviz/translations/pl.json create mode 100644 homeassistant/components/ezviz/translations/sv.json create mode 100644 homeassistant/components/faa_delays/translations/sv.json create mode 100644 homeassistant/components/google_travel_time/translations/sv.json create mode 100644 homeassistant/components/hive/translations/sv.json create mode 100644 homeassistant/components/home_plus_control/translations/sv.json create mode 100644 homeassistant/components/huisbaasje/translations/sv.json create mode 100644 homeassistant/components/ialarm/translations/fr.json create mode 100644 homeassistant/components/kostal_plenticore/translations/sv.json create mode 100644 homeassistant/components/met_eireann/translations/sv.json create mode 100644 homeassistant/components/mullvad/translations/sv.json create mode 100644 homeassistant/components/philips_js/translations/sv.json create mode 100644 homeassistant/components/screenlogic/translations/sv.json create mode 100644 homeassistant/components/verisure/translations/sv.json create mode 100644 homeassistant/components/water_heater/translations/sv.json create mode 100644 homeassistant/components/waze_travel_time/translations/sv.json create mode 100644 homeassistant/components/wolflink/translations/sv.json diff --git a/homeassistant/components/airvisual/translations/sv.json b/homeassistant/components/airvisual/translations/sv.json index ecc1c397ec46d..9faebc9e9608c 100644 --- a/homeassistant/components/airvisual/translations/sv.json +++ b/homeassistant/components/airvisual/translations/sv.json @@ -5,12 +5,6 @@ "invalid_api_key": "Ogiltig API-nyckel" }, "step": { - "geography": { - "data": { - "latitude": "Latitud", - "longitude": "Longitud" - } - }, "node_pro": { "data": { "ip_address": "Enhets IP-adress / v\u00e4rdnamn", diff --git a/homeassistant/components/august/translations/sv.json b/homeassistant/components/august/translations/sv.json index a3a0b891bc639..1ebdfab9fd21a 100644 --- a/homeassistant/components/august/translations/sv.json +++ b/homeassistant/components/august/translations/sv.json @@ -9,6 +9,11 @@ "unknown": "Ov\u00e4ntat fel" }, "step": { + "reauth_validate": { + "data": { + "password": "L\u00f6senord" + } + }, "user": { "data": { "login_method": "Inloggningsmetod", @@ -19,6 +24,12 @@ "description": "Om inloggningsmetoden \u00e4r \"e-post\" \u00e4r anv\u00e4ndarnamnet e-postadressen. Om inloggningsmetoden \u00e4r \"telefon\" \u00e4r anv\u00e4ndarnamnet telefonnumret i formatet \"+ NNNNNNNN\".", "title": "St\u00e4ll in ett August-konto" }, + "user_validate": { + "data": { + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" + } + }, "validation": { "data": { "code": "Verifieringskod" diff --git a/homeassistant/components/climacell/translations/sv.json b/homeassistant/components/climacell/translations/sv.json new file mode 100644 index 0000000000000..e6e7a77926f15 --- /dev/null +++ b/homeassistant/components/climacell/translations/sv.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_version": "API-version", + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Namn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/translations/sv.json b/homeassistant/components/deconz/translations/sv.json index c9814734af09f..d7ec321ff36bd 100644 --- a/homeassistant/components/deconz/translations/sv.json +++ b/homeassistant/components/deconz/translations/sv.json @@ -35,6 +35,10 @@ "button_2": "Andra knappen", "button_3": "Tredje knappen", "button_4": "Fj\u00e4rde knappen", + "button_5": "Femte knappen", + "button_6": "Sj\u00e4tte knappen", + "button_7": "Sjunde knappen", + "button_8": "\u00c5ttonde knappen", "close": "St\u00e4ng", "dim_down": "Dimma ned", "dim_up": "Dimma upp", diff --git a/homeassistant/components/emonitor/translations/sv.json b/homeassistant/components/emonitor/translations/sv.json new file mode 100644 index 0000000000000..c5ad71d784df8 --- /dev/null +++ b/homeassistant/components/emonitor/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Kunde inte ansluta", + "unknown": "Ov\u00e4ntat fel" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/sv.json b/homeassistant/components/enphase_envoy/translations/sv.json new file mode 100644 index 0000000000000..ecc6740fc9df8 --- /dev/null +++ b/homeassistant/components/enphase_envoy/translations/sv.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Kunde inte ansluta", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ezviz/translations/pl.json b/homeassistant/components/ezviz/translations/pl.json new file mode 100644 index 0000000000000..a8413da6188b8 --- /dev/null +++ b/homeassistant/components/ezviz/translations/pl.json @@ -0,0 +1,52 @@ +{ + "config": { + "abort": { + "already_configured_account": "Konto jest ju\u017c skonfigurowane", + "ezviz_cloud_account_missing": "Brak konta Ezviz. Skonfiguruj ponownie konto Ezviz.", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "invalid_host": "Nieprawid\u0142owa nazwa hosta lub adres IP" + }, + "flow_title": "{serial}", + "step": { + "confirm": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + }, + "description": "Wpisz dane logowania RTSP dla kamery Ezviz {serial} z IP {ip_address}", + "title": "Wykryto kamer\u0119 Ezviz" + }, + "user": { + "data": { + "password": "Has\u0142o", + "url": "URL", + "username": "Nazwa u\u017cytkownika" + }, + "title": "Po\u0142\u0105czenie z chmur\u0105 Ezviz" + }, + "user_custom_url": { + "data": { + "password": "Has\u0142o", + "url": "URL", + "username": "Nazwa u\u017cytkownika" + }, + "description": "R\u0119cznie okre\u015bl adres URL dla swojego regionu", + "title": "Po\u0142\u0105czenie z niestandardowym adresem URL Ezviz" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ffmpeg_arguments": "Argumenty przekazane do ffmpeg dla kamer", + "timeout": "Limit czasu \u017c\u0105dania (w sekundach)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ezviz/translations/sv.json b/homeassistant/components/ezviz/translations/sv.json new file mode 100644 index 0000000000000..4c047d755737c --- /dev/null +++ b/homeassistant/components/ezviz/translations/sv.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "unknown": "Ov\u00e4ntat fel" + }, + "error": { + "cannot_connect": "Kunde inte ansluta" + }, + "step": { + "confirm": { + "data": { + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" + } + }, + "user": { + "data": { + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" + } + }, + "user_custom_url": { + "data": { + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/faa_delays/translations/sv.json b/homeassistant/components/faa_delays/translations/sv.json new file mode 100644 index 0000000000000..bd797004301e6 --- /dev/null +++ b/homeassistant/components/faa_delays/translations/sv.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Kunde inte ansluta", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "id": "Flygplats" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_travel_time/translations/sv.json b/homeassistant/components/google_travel_time/translations/sv.json new file mode 100644 index 0000000000000..18a9d3d507e3f --- /dev/null +++ b/homeassistant/components/google_travel_time/translations/sv.json @@ -0,0 +1,27 @@ +{ + "config": { + "error": { + "cannot_connect": "Kunde inte ansluta" + }, + "step": { + "user": { + "data": { + "destination": "Destination", + "origin": "Ursprung" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "avoid": "Undvik", + "language": "Spr\u00e5k", + "time": "Tid", + "units": "Enheter" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hive/translations/sv.json b/homeassistant/components/hive/translations/sv.json new file mode 100644 index 0000000000000..6d76a51e90b09 --- /dev/null +++ b/homeassistant/components/hive/translations/sv.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "reauth": { + "data": { + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" + } + }, + "user": { + "data": { + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/home_plus_control/translations/sv.json b/homeassistant/components/home_plus_control/translations/sv.json new file mode 100644 index 0000000000000..5307b489a72f0 --- /dev/null +++ b/homeassistant/components/home_plus_control/translations/sv.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "pick_implementation": { + "title": "V\u00e4lj autentiseringsmetod" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huisbaasje/translations/sv.json b/homeassistant/components/huisbaasje/translations/sv.json new file mode 100644 index 0000000000000..d52e8b8362cb6 --- /dev/null +++ b/homeassistant/components/huisbaasje/translations/sv.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "Kunde inte ansluta" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ialarm/translations/en.json b/homeassistant/components/ialarm/translations/en.json index 2ea7a7ab66945..39069f3d2b105 100644 --- a/homeassistant/components/ialarm/translations/en.json +++ b/homeassistant/components/ialarm/translations/en.json @@ -11,10 +11,10 @@ "user": { "data": { "host": "Host", + "pin": "PIN Code", "port": "Port" } } } - }, - "title": "Antifurto365 iAlarm" -} + } +} \ No newline at end of file diff --git a/homeassistant/components/ialarm/translations/fr.json b/homeassistant/components/ialarm/translations/fr.json new file mode 100644 index 0000000000000..8cfb9a624706d --- /dev/null +++ b/homeassistant/components/ialarm/translations/fr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "Echec de la connexion", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te", + "pin": "Code PIN", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kostal_plenticore/translations/sv.json b/homeassistant/components/kostal_plenticore/translations/sv.json new file mode 100644 index 0000000000000..70aba340c35df --- /dev/null +++ b/homeassistant/components/kostal_plenticore/translations/sv.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Kunde inte ansluta", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "password": "L\u00f6senord" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/met_eireann/translations/sv.json b/homeassistant/components/met_eireann/translations/sv.json new file mode 100644 index 0000000000000..80cb677367759 --- /dev/null +++ b/homeassistant/components/met_eireann/translations/sv.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "user": { + "data": { + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Namn" + }, + "title": "Plats" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mullvad/translations/sv.json b/homeassistant/components/mullvad/translations/sv.json new file mode 100644 index 0000000000000..ecc6740fc9df8 --- /dev/null +++ b/homeassistant/components/mullvad/translations/sv.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Kunde inte ansluta", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/sv.json b/homeassistant/components/netatmo/translations/sv.json index 37badeaab533c..32dfd2db6a0fc 100644 --- a/homeassistant/components/netatmo/translations/sv.json +++ b/homeassistant/components/netatmo/translations/sv.json @@ -3,5 +3,10 @@ "create_entry": { "default": "Autentiserad med Netatmo." } + }, + "device_automation": { + "trigger_subtype": { + "schedule": "schema" + } } } \ No newline at end of file diff --git a/homeassistant/components/nut/translations/sv.json b/homeassistant/components/nut/translations/sv.json index 70dccdca51ecc..45832197f681c 100644 --- a/homeassistant/components/nut/translations/sv.json +++ b/homeassistant/components/nut/translations/sv.json @@ -31,6 +31,10 @@ } }, "options": { + "error": { + "cannot_connect": "Kunde inte ansluta", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "init": { "data": { diff --git a/homeassistant/components/openweathermap/translations/sv.json b/homeassistant/components/openweathermap/translations/sv.json index 108d4575e5580..cafa9c0fdd07b 100644 --- a/homeassistant/components/openweathermap/translations/sv.json +++ b/homeassistant/components/openweathermap/translations/sv.json @@ -8,8 +8,6 @@ "data": { "api_key": "OpenWeatherMap API-nyckel", "language": "Spr\u00e5k", - "latitude": "Latitud", - "longitude": "Longitud", "mode": "L\u00e4ge", "name": "Integrationens namn" }, diff --git a/homeassistant/components/philips_js/translations/sv.json b/homeassistant/components/philips_js/translations/sv.json new file mode 100644 index 0000000000000..418a59f0bdc7e --- /dev/null +++ b/homeassistant/components/philips_js/translations/sv.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "invalid_pin": "Ogiltig PIN-kod" + }, + "step": { + "pair": { + "data": { + "pin": "PIN-kod" + }, + "title": "Para ihop" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/screenlogic/translations/sv.json b/homeassistant/components/screenlogic/translations/sv.json new file mode 100644 index 0000000000000..7be3515deb0d7 --- /dev/null +++ b/homeassistant/components/screenlogic/translations/sv.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Kunde inte ansluta" + }, + "step": { + "gateway_entry": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upb/translations/sv.json b/homeassistant/components/upb/translations/sv.json index aae50ea51054c..f1f229cc17544 100644 --- a/homeassistant/components/upb/translations/sv.json +++ b/homeassistant/components/upb/translations/sv.json @@ -3,7 +3,7 @@ "error": { "cannot_connect": "Det gick inte att ansluta till UPB PIM, f\u00f6rs\u00f6k igen.", "invalid_upb_file": "Saknar eller ogiltig UPB UPStart-exportfil, kontrollera filens namn och s\u00f6kv\u00e4g.", - "unknown": "Ov\u00e4ntat fel." + "unknown": "Ov\u00e4ntat fel" }, "step": { "user": { diff --git a/homeassistant/components/verisure/translations/sv.json b/homeassistant/components/verisure/translations/sv.json new file mode 100644 index 0000000000000..3d3dbdb8bdae8 --- /dev/null +++ b/homeassistant/components/verisure/translations/sv.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "L\u00f6senord" + } + }, + "user": { + "data": { + "password": "L\u00f6senord" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/water_heater/translations/sv.json b/homeassistant/components/water_heater/translations/sv.json new file mode 100644 index 0000000000000..37de0012a7936 --- /dev/null +++ b/homeassistant/components/water_heater/translations/sv.json @@ -0,0 +1,7 @@ +{ + "state": { + "_": { + "heat_pump": "V\u00e4rmepump" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/waze_travel_time/translations/sv.json b/homeassistant/components/waze_travel_time/translations/sv.json new file mode 100644 index 0000000000000..84113d1284e06 --- /dev/null +++ b/homeassistant/components/waze_travel_time/translations/sv.json @@ -0,0 +1,26 @@ +{ + "config": { + "error": { + "cannot_connect": "Kunde inte ansluta" + }, + "step": { + "user": { + "data": { + "destination": "Destination", + "origin": "Ursprung", + "region": "Region" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "units": "Enheter", + "vehicle_type": "Fordonstyp" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/sv.json b/homeassistant/components/wolflink/translations/sv.json new file mode 100644 index 0000000000000..7887994287655 --- /dev/null +++ b/homeassistant/components/wolflink/translations/sv.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/ru.json b/homeassistant/components/zha/translations/ru.json index 6ee8d58f50d09..e4084d0b2f663 100644 --- a/homeassistant/components/zha/translations/ru.json +++ b/homeassistant/components/zha/translations/ru.json @@ -84,7 +84,7 @@ "remote_button_long_release": "{subtype} \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u0434\u043e\u043b\u0433\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", "remote_button_quadruple_press": "{subtype} \u043d\u0430\u0436\u0430\u0442\u0430 \u0447\u0435\u0442\u044b\u0440\u0435 \u0440\u0430\u0437\u0430", "remote_button_quintuple_press": "{subtype} \u043d\u0430\u0436\u0430\u0442\u0430 \u043f\u044f\u0442\u044c \u0440\u0430\u0437", - "remote_button_short_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430", + "remote_button_short_press": "{subtype} \u043d\u0430\u0436\u0430\u0442\u0430", "remote_button_short_release": "{subtype} \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u043a\u043e\u0440\u043e\u0442\u043a\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", "remote_button_triple_press": "{subtype} \u043d\u0430\u0436\u0430\u0442\u0430 \u0442\u0440\u0438 \u0440\u0430\u0437\u0430" } From f538ea182754274083537d57d6fa53efa73d28a2 Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Sun, 11 Apr 2021 21:44:22 -0500 Subject: [PATCH 0204/1317] Release ownership of amcrest integration (#49086) I no longer use this integration and others have taken over maintenance. --- CODEOWNERS | 1 - homeassistant/components/amcrest/manifest.json | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 860ee9f0665c7..838ed6cb143fa 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -35,7 +35,6 @@ homeassistant/components/almond/* @gcampax @balloob homeassistant/components/alpha_vantage/* @fabaff homeassistant/components/ambiclimate/* @danielhiversen homeassistant/components/ambient_station/* @bachya -homeassistant/components/amcrest/* @pnbruckner homeassistant/components/analytics/* @home-assistant/core @ludeeus homeassistant/components/androidtv/* @JeffLIrion homeassistant/components/apache_kafka/* @bachya diff --git a/homeassistant/components/amcrest/manifest.json b/homeassistant/components/amcrest/manifest.json index 0b7a59edb79c4..869b65658d64f 100644 --- a/homeassistant/components/amcrest/manifest.json +++ b/homeassistant/components/amcrest/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/amcrest", "requirements": ["amcrest==1.7.1"], "dependencies": ["ffmpeg"], - "codeowners": ["@pnbruckner"] + "codeowners": [] } From eac104127737ab56d0c9a59dbacfa2d7e88b4569 Mon Sep 17 00:00:00 2001 From: Corbeno Date: Sun, 11 Apr 2021 22:14:11 -0500 Subject: [PATCH 0205/1317] Create DataUpdateCoordinator for each proxmoxve vm/container (#45171) Co-authored-by: J. Nick Koston --- CODEOWNERS | 2 +- .../components/proxmoxve/__init__.py | 133 ++++++++++-------- .../components/proxmoxve/binary_sensor.py | 47 ++++--- .../components/proxmoxve/manifest.json | 2 +- 4 files changed, 104 insertions(+), 80 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 838ed6cb143fa..ff0372b0d1f03 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -367,7 +367,7 @@ homeassistant/components/powerwall/* @bdraco @jrester homeassistant/components/profiler/* @bdraco homeassistant/components/progettihwsw/* @ardaseremet homeassistant/components/prometheus/* @knyar -homeassistant/components/proxmoxve/* @k4ds3 @jhollowe +homeassistant/components/proxmoxve/* @k4ds3 @jhollowe @Corbeno homeassistant/components/ps4/* @ktnrg45 homeassistant/components/push/* @dgomes homeassistant/components/pvoutput/* @fabaff diff --git a/homeassistant/components/proxmoxve/__init__.py b/homeassistant/components/proxmoxve/__init__.py index a149c8b6034fd..5777bb3054c4e 100644 --- a/homeassistant/components/proxmoxve/__init__.py +++ b/homeassistant/components/proxmoxve/__init__.py @@ -5,7 +5,8 @@ from proxmoxer import ProxmoxAPI from proxmoxer.backends.https import AuthenticationError from proxmoxer.core import ResourceException -from requests.exceptions import SSLError +import requests.exceptions +from requests.exceptions import ConnectTimeout, SSLError import voluptuous as vol from homeassistant.const import ( @@ -31,7 +32,7 @@ CONF_VMS = "vms" CONF_CONTAINERS = "containers" -COORDINATOR = "coordinator" +COORDINATORS = "coordinators" API_DATA = "api_data" DEFAULT_PORT = 8006 @@ -90,6 +91,7 @@ async def async_setup(hass: HomeAssistant, config: dict): def build_client() -> ProxmoxAPI: """Build the Proxmox client connection.""" hass.data[PROXMOX_CLIENTS] = {} + for entry in config[DOMAIN]: host = entry[CONF_HOST] port = entry[CONF_PORT] @@ -98,6 +100,8 @@ def build_client() -> ProxmoxAPI: password = entry[CONF_PASSWORD] verify_ssl = entry[CONF_VERIFY_SSL] + hass.data[PROXMOX_CLIENTS][host] = None + try: # Construct an API client with the given data for the given host proxmox_client = ProxmoxClient( @@ -111,91 +115,100 @@ def build_client() -> ProxmoxAPI: continue except SSLError: _LOGGER.error( - 'Unable to verify proxmox server SSL. Try using "verify_ssl: false"' + "Unable to verify proxmox server SSL. " + 'Try using "verify_ssl: false" for proxmox instance %s:%d', + host, + port, ) continue + except ConnectTimeout: + _LOGGER.warning("Connection to host %s timed out during setup", host) + continue + + hass.data[PROXMOX_CLIENTS][host] = proxmox_client - return proxmox_client + await hass.async_add_executor_job(build_client) - proxmox_client = await hass.async_add_executor_job(build_client) + coordinators = hass.data[DOMAIN][COORDINATORS] = {} - async def async_update_data() -> dict: - """Fetch data from API endpoint.""" + # Create a coordinator for each vm/container + for host_config in config[DOMAIN]: + host_name = host_config["host"] + coordinators[host_name] = {} + + proxmox_client = hass.data[PROXMOX_CLIENTS][host_name] + + # Skip invalid hosts + if proxmox_client is None: + continue proxmox = proxmox_client.get_api_client() - def poll_api() -> dict: - data = {} + for node_config in host_config["nodes"]: + node_name = node_config["node"] + node_coordinators = coordinators[host_name][node_name] = {} - for host_config in config[DOMAIN]: - host_name = host_config["host"] + for vm_id in node_config["vms"]: + coordinator = create_coordinator_container_vm( + hass, proxmox, host_name, node_name, vm_id, TYPE_VM + ) - data[host_name] = {} + # Fetch initial data + await coordinator.async_refresh() - for node_config in host_config["nodes"]: - node_name = node_config["node"] - data[host_name][node_name] = {} + node_coordinators[vm_id] = coordinator - for vm_id in node_config["vms"]: - data[host_name][node_name][vm_id] = {} + for container_id in node_config["containers"]: + coordinator = create_coordinator_container_vm( + hass, proxmox, host_name, node_name, container_id, TYPE_CONTAINER + ) - vm_status = call_api_container_vm( - proxmox, node_name, vm_id, TYPE_VM - ) + # Fetch initial data + await coordinator.async_refresh() - if vm_status is None: - _LOGGER.warning("Vm/Container %s unable to be found", vm_id) - data[host_name][node_name][vm_id] = None - continue + node_coordinators[container_id] = coordinator - data[host_name][node_name][vm_id] = parse_api_container_vm( - vm_status - ) + for component in PLATFORMS: + await hass.async_create_task( + hass.helpers.discovery.async_load_platform( + component, DOMAIN, {"config": config}, config + ) + ) - for container_id in node_config["containers"]: - data[host_name][node_name][container_id] = {} + return True - container_status = call_api_container_vm( - proxmox, node_name, container_id, TYPE_CONTAINER - ) - if container_status is None: - _LOGGER.error( - "Vm/Container %s unable to be found", container_id - ) - data[host_name][node_name][container_id] = None - continue +def create_coordinator_container_vm( + hass, proxmox, host_name, node_name, vm_id, vm_type +): + """Create and return a DataUpdateCoordinator for a vm/container.""" - data[host_name][node_name][ - container_id - ] = parse_api_container_vm(container_status) + async def async_update_data(): + """Call the api and handle the response.""" - return data + def poll_api(): + """Call the api.""" + vm_status = call_api_container_vm(proxmox, node_name, vm_id, vm_type) + return vm_status - return await hass.async_add_executor_job(poll_api) + vm_status = await hass.async_add_executor_job(poll_api) - coordinator = DataUpdateCoordinator( + if vm_status is None: + _LOGGER.warning( + "Vm/Container %s unable to be found in node %s", vm_id, node_name + ) + return None + + return parse_api_container_vm(vm_status) + + return DataUpdateCoordinator( hass, _LOGGER, - name="proxmox_coordinator", + name=f"proxmox_coordinator_{host_name}_{node_name}_{vm_id}", update_method=async_update_data, update_interval=timedelta(seconds=UPDATE_INTERVAL), ) - hass.data[DOMAIN][COORDINATOR] = coordinator - - # Fetch initial data - await coordinator.async_config_entry_first_refresh() - - for platform in PLATFORMS: - await hass.async_create_task( - hass.helpers.discovery.async_load_platform( - platform, DOMAIN, {"config": config}, config - ) - ) - - return True - def parse_api_container_vm(status): """Get the container or vm api data and return it formatted in a dictionary. @@ -216,7 +229,7 @@ def call_api_container_vm(proxmox, node_name, vm_id, machine_type): status = proxmox.nodes(node_name).qemu(vm_id).status.current.get() elif machine_type == TYPE_CONTAINER: status = proxmox.nodes(node_name).lxc(vm_id).status.current.get() - except ResourceException: + except (ResourceException, requests.exceptions.ConnectionError): return None return status diff --git a/homeassistant/components/proxmoxve/binary_sensor.py b/homeassistant/components/proxmoxve/binary_sensor.py index 1151c2ec33299..fedb513e5b4e1 100644 --- a/homeassistant/components/proxmoxve/binary_sensor.py +++ b/homeassistant/components/proxmoxve/binary_sensor.py @@ -1,8 +1,9 @@ """Binary sensor to read Proxmox VE data.""" -from homeassistant.const import STATE_OFF, STATE_ON + +from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from . import COORDINATOR, DOMAIN, ProxmoxEntity +from . import COORDINATORS, DOMAIN, PROXMOX_CLIENTS, ProxmoxEntity async def async_setup_platform(hass, config, add_entities, discovery_info=None): @@ -10,41 +11,45 @@ async def async_setup_platform(hass, config, add_entities, discovery_info=None): if discovery_info is None: return - coordinator = hass.data[DOMAIN][COORDINATOR] - sensors = [] for host_config in discovery_info["config"][DOMAIN]: host_name = host_config["host"] + host_name_coordinators = hass.data[DOMAIN][COORDINATORS][host_name] + + if hass.data[PROXMOX_CLIENTS][host_name] is None: + continue for node_config in host_config["nodes"]: node_name = node_config["node"] for vm_id in node_config["vms"]: - coordinator_data = coordinator.data[host_name][node_name][vm_id] + coordinator = host_name_coordinators[node_name][vm_id] + coordinator_data = coordinator.data # unfound vm case if coordinator_data is None: continue vm_name = coordinator_data["name"] - vm_status = create_binary_sensor( + vm_sensor = create_binary_sensor( coordinator, host_name, node_name, vm_id, vm_name ) - sensors.append(vm_status) + sensors.append(vm_sensor) for container_id in node_config["containers"]: - coordinator_data = coordinator.data[host_name][node_name][container_id] + coordinator = host_name_coordinators[node_name][container_id] + coordinator_data = coordinator.data # unfound container case if coordinator_data is None: continue container_name = coordinator_data["name"] - container_status = create_binary_sensor( + container_sensor = create_binary_sensor( coordinator, host_name, node_name, container_id, container_name ) - sensors.append(container_status) + sensors.append(container_sensor) add_entities(sensors) @@ -62,7 +67,7 @@ def create_binary_sensor(coordinator, host_name, node_name, vm_id, name): ) -class ProxmoxBinarySensor(ProxmoxEntity): +class ProxmoxBinarySensor(ProxmoxEntity, BinarySensorEntity): """A binary sensor for reading Proxmox VE data.""" def __init__( @@ -80,12 +85,18 @@ def __init__( coordinator, unique_id, name, icon, host_name, node_name, vm_id ) - self._state = None - @property - def state(self): + def is_on(self): """Return the state of the binary sensor.""" - data = self.coordinator.data[self._host_name][self._node_name][self._vm_id] - if data["status"] == "running": - return STATE_ON - return STATE_OFF + data = self.coordinator.data + + if data is None: + return None + + return data["status"] == "running" + + @property + def available(self): + """Return sensor availability.""" + + return super().available and self.coordinator.data is not None diff --git a/homeassistant/components/proxmoxve/manifest.json b/homeassistant/components/proxmoxve/manifest.json index a47ce0a28eea3..0f0029dff3239 100644 --- a/homeassistant/components/proxmoxve/manifest.json +++ b/homeassistant/components/proxmoxve/manifest.json @@ -2,6 +2,6 @@ "domain": "proxmoxve", "name": "Proxmox VE", "documentation": "https://www.home-assistant.io/integrations/proxmoxve", - "codeowners": ["@k4ds3", "@jhollowe"], + "codeowners": ["@k4ds3", "@jhollowe", "@Corbeno"], "requirements": ["proxmoxer==1.1.1"] } From 2d5edeb1ef2f797957153e289f912d27ee022318 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 11 Apr 2021 22:49:09 -0700 Subject: [PATCH 0206/1317] Set hass when adding template attribute (#49094) --- .../components/template/template_entity.py | 2 ++ .../template/test_template_entity.py | 22 +++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 tests/components/template/test_template_entity.py diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py index f8909206dec2a..4f72511fe2435 100644 --- a/homeassistant/components/template/template_entity.py +++ b/homeassistant/components/template/template_entity.py @@ -211,6 +211,8 @@ def add_template_attribute( if the template or validator resulted in an error. """ + assert self.hass is not None, "hass cannot be None" + template.hass = self.hass attribute = _TemplateAttribute( self, attribute, template, validator, on_update, none_on_template_error ) diff --git a/tests/components/template/test_template_entity.py b/tests/components/template/test_template_entity.py new file mode 100644 index 0000000000000..ae812370d936b --- /dev/null +++ b/tests/components/template/test_template_entity.py @@ -0,0 +1,22 @@ +"""Test template entity.""" +import pytest + +from homeassistant.components.template import template_entity +from homeassistant.helpers import template + + +async def test_template_entity_requires_hass_set(): + """Test template entity requires hass to be set before accepting templates.""" + entity = template_entity.TemplateEntity() + + with pytest.raises(AssertionError): + entity.add_template_attribute("_hello", template.Template("Hello")) + + entity.hass = object() + entity.add_template_attribute("_hello", template.Template("Hello", None)) + + tpl_with_hass = template.Template("Hello", entity.hass) + entity.add_template_attribute("_hello", tpl_with_hass) + + # Because hass is set in `add_template_attribute`, both templates match `tpl_with_hass` + assert len(entity._template_attrs.get(tpl_with_hass, [])) == 2 From 9368891b1baef0e38be2a8aa9436c1dc2623bde3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 11 Apr 2021 20:43:54 -1000 Subject: [PATCH 0207/1317] Live db migrations and recovery (#49036) Co-authored-by: Paulus Schoutsen --- homeassistant/components/recorder/__init__.py | 395 +++++++++++------- .../components/recorder/migration.py | 31 +- homeassistant/components/recorder/purge.py | 5 +- homeassistant/components/recorder/util.py | 41 +- tests/components/recorder/test_init.py | 213 ++++++++-- tests/components/recorder/test_migrate.py | 164 +++++++- tests/components/recorder/test_purge.py | 36 ++ tests/components/recorder/test_util.py | 97 +---- 8 files changed, 677 insertions(+), 305 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index f93d965a4b9d0..10b987b04f7b5 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -3,7 +3,7 @@ import asyncio import concurrent.futures -from datetime import datetime +from datetime import datetime, timedelta import logging import queue import sqlite3 @@ -12,6 +12,7 @@ from typing import Any, Callable, NamedTuple from sqlalchemy import create_engine, event as sqlalchemy_event, exc, select +from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.orm import scoped_session, sessionmaker from sqlalchemy.pool import StaticPool import voluptuous as vol @@ -20,7 +21,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, CONF_EXCLUDE, - EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, @@ -33,6 +34,7 @@ INCLUDE_EXCLUDE_FILTER_SCHEMA_INNER, convert_include_exclude_filter, ) +from homeassistant.helpers.event import async_track_time_interval, track_time_change from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util @@ -56,6 +58,8 @@ ATTR_REPACK = "repack" ATTR_APPLY_FILTER = "apply_filter" +MAX_QUEUE_BACKLOG = 30000 + SERVICE_PURGE_SCHEMA = vol.Schema( { vol.Optional(ATTR_KEEP_DAYS): cv.positive_int, @@ -99,6 +103,7 @@ { vol.Optional(DOMAIN, default=dict): vol.All( cv.deprecated(CONF_PURGE_INTERVAL), + cv.deprecated(CONF_DB_INTEGRITY_CHECK), FILTER_SCHEMA.extend( { vol.Optional(CONF_AUTO_PURGE, default=True): cv.boolean, @@ -176,11 +181,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: commit_interval = conf[CONF_COMMIT_INTERVAL] db_max_retries = conf[CONF_DB_MAX_RETRIES] db_retry_wait = conf[CONF_DB_RETRY_WAIT] - db_integrity_check = conf[CONF_DB_INTEGRITY_CHECK] - - db_url = conf.get(CONF_DB_URL) - if not db_url: - db_url = DEFAULT_URL.format(hass_config_path=hass.config.path(DEFAULT_DB_FILE)) + db_url = conf.get(CONF_DB_URL) or DEFAULT_URL.format( + hass_config_path=hass.config.path(DEFAULT_DB_FILE) + ) exclude = conf[CONF_EXCLUDE] exclude_t = exclude.get(CONF_EVENT_TYPES, []) instance = hass.data[DATA_INSTANCE] = Recorder( @@ -193,10 +196,17 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: db_retry_wait=db_retry_wait, entity_filter=entity_filter, exclude_t=exclude_t, - db_integrity_check=db_integrity_check, ) instance.async_initialize() instance.start() + _async_register_services(hass, instance) + + return await instance.async_db_ready + + +@callback +def _async_register_services(hass, instance): + """Register recorder services.""" async def async_handle_purge_service(service): """Handle calls to the purge service.""" @@ -223,8 +233,6 @@ async def async_handle_disable_service(service): schema=SERVICE_DISABLE_SCHEMA, ) - return await instance.async_db_ready - class PurgeTask(NamedTuple): """Object to store information about purge task.""" @@ -252,7 +260,6 @@ def __init__( db_retry_wait: int, entity_filter: Callable[[str], bool], exclude_t: list[str], - db_integrity_check: bool, ) -> None: """Initialize the recorder.""" threading.Thread.__init__(self, name="Recorder") @@ -266,8 +273,8 @@ def __init__( self.db_url = uri self.db_max_retries = db_max_retries self.db_retry_wait = db_retry_wait - self.db_integrity_check = db_integrity_check self.async_db_ready = asyncio.Future() + self.async_recorder_ready = asyncio.Event() self._queue_watch = threading.Event() self.engine: Any = None self.run_info: Any = None @@ -283,6 +290,9 @@ def __init__( self.event_session = None self.get_session = None self._completed_database_setup = None + self._event_listener = None + + self._queue_watcher = None self.enabled = True @@ -293,9 +303,37 @@ def set_enable(self, enable): @callback def async_initialize(self): """Initialize the recorder.""" - self.hass.bus.async_listen( + self._event_listener = self.hass.bus.async_listen( MATCH_ALL, self.event_listener, event_filter=self._async_event_filter ) + self._queue_watcher = async_track_time_interval( + self.hass, self._async_check_queue, timedelta(minutes=10) + ) + + @callback + def _async_check_queue(self, *_): + """Periodic check of the queue size to ensure we do not exaust memory. + + The queue grows during migraton or if something really goes wrong. + """ + size = self.queue.qsize() + _LOGGER.debug("Recorder queue size is: %s", size) + if self.queue.qsize() <= MAX_QUEUE_BACKLOG: + return + _LOGGER.error( + "The recorder queue reached the maximum size of %s; Events are no longer being recorded", + MAX_QUEUE_BACKLOG, + ) + self._stop_queue_watcher_and_event_listener() + + def _stop_queue_watcher_and_event_listener(self): + """Stop watching the queue.""" + if self._queue_watcher: + self._queue_watcher() + self._queue_watcher = None + if self._event_listener: + self._event_listener() + self._event_listener = None @callback def _async_event_filter(self, event): @@ -314,89 +352,152 @@ def do_adhoc_purge(self, **kwargs): self.queue.put(PurgeTask(keep_days, repack, apply_filter)) - def run(self): - """Start processing events to save.""" + @callback + def async_register(self, shutdown_task, hass_started): + """Post connection initialize.""" - if not self._setup_recorder(): - return + def shutdown(event): + """Shut down the Recorder.""" + if not hass_started.done(): + hass_started.set_result(shutdown_task) + self.queue.put(None) + self.join() - shutdown_task = object() - hass_started = concurrent.futures.Future() + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown) + + if self.hass.state == CoreState.running: + hass_started.set_result(None) + return @callback - def register(): - """Post connection initialize.""" - self.async_db_ready.set_result(True) + def async_hass_started(event): + """Notify that hass has started.""" + hass_started.set_result(None) - def shutdown(event): - """Shut down the Recorder.""" - if not hass_started.done(): - hass_started.set_result(shutdown_task) - self.queue.put(None) - self.join() + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, async_hass_started) - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown) + @callback + def async_connection_failed(self): + """Connect failed tasks.""" + self.async_db_ready.set_result(False) + persistent_notification.async_create( + self.hass, + "The recorder could not start, check [the logs](/config/logs)", + "Recorder", + ) + self._stop_queue_watcher_and_event_listener() - if self.hass.state == CoreState.running: - hass_started.set_result(None) - else: + @callback + def async_connection_success(self): + """Connect success tasks.""" + self.async_db_ready.set_result(True) - @callback - def notify_hass_started(event): - """Notify that hass has started.""" - hass_started.set_result(None) + @callback + def _async_recorder_ready(self): + """Mark recorder ready.""" + self.async_recorder_ready.set() - self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, notify_hass_started - ) + @callback + def async_purge(self, now): + """Trigger the purge.""" + self.queue.put(PurgeTask(self.keep_days, repack=False, apply_filter=False)) - self.hass.add_job(register) - result = hass_started.result() + def run(self): + """Start processing events to save.""" + shutdown_task = object() + hass_started = concurrent.futures.Future() + + self.hass.add_job(self.async_register, shutdown_task, hass_started) + + current_version = self._setup_recorder() + + if current_version is None: + self.hass.add_job(self.async_connection_failed) + return + + schema_is_current = migration.schema_is_current(current_version) + if schema_is_current: + self._setup_run() + + self.hass.add_job(self.async_connection_success) # If shutdown happened before Home Assistant finished starting - if result is shutdown_task: + if hass_started.result() is shutdown_task: # Make sure we cleanly close the run if # we restart before startup finishes self._shutdown() return - # Start periodic purge - if self.auto_purge: - - @callback - def async_purge(now): - """Trigger the purge.""" - self.queue.put( - PurgeTask(self.keep_days, repack=False, apply_filter=False) + # We wait to start the migration until startup has finished + # since it can be cpu intensive and we do not want it to compete + # with startup which is also cpu intensive + if not schema_is_current: + if self._migrate_schema_and_setup_run(current_version): + if not self._event_listener: + # If the schema migration takes so longer that the end + # queue watcher safety kicks in because MAX_QUEUE_BACKLOG + # is reached, we need to reinitialize the listener. + self.hass.add_job(self.async_initialize) + else: + persistent_notification.create( + self.hass, + "The database migration failed, check [the logs](/config/logs)." + "Database Migration Failed", + "recorder_database_migration", ) + self._shutdown() + return + # Start periodic purge + if self.auto_purge: # Purge every night at 4:12am - self.hass.helpers.event.track_time_change( - async_purge, hour=4, minute=12, second=0 - ) + track_time_change(self.hass, self.async_purge, hour=4, minute=12, second=0) _LOGGER.debug("Recorder processing the queue") + self.hass.add_job(self._async_recorder_ready) + self._run_event_loop() + + def _run_event_loop(self): + """Run the event loop for the recorder.""" # Use a session for the event read loop # with a commit every time the event time # has changed. This reduces the disk io. - while True: - event = self.queue.get() + while event := self.queue.get(): + try: + self._process_one_event_or_recover(event) + except Exception as err: # pylint: disable=broad-except + _LOGGER.exception("Error while processing event %s: %s", event, err) - if event is None: - self._shutdown() - return + self._shutdown() + def _process_one_event_or_recover(self, event): + """Process an event, reconnect, or recover a malformed database.""" + try: self._process_one_event(event) + return + except exc.DatabaseError as err: + if self._handle_database_error(err): + return + _LOGGER.exception( + "Unhandled database error while processing event %s: %s", event, err + ) + except SQLAlchemyError as err: + _LOGGER.exception( + "SQLAlchemyError error processing event %s: %s", event, err + ) - def _setup_recorder(self) -> bool: - """Create schema and connect to the database.""" + # Reset the session if an SQLAlchemyError (including DatabaseError) + # happens to rollback and recover + self._reopen_event_session() + + def _setup_recorder(self) -> None | int: + """Create connect to the database and get the schema version.""" tries = 1 while tries <= self.db_max_retries: try: self._setup_connection() - migration.migrate_schema(self) - self._setup_run() + return migration.get_schema_version(self) except Exception as err: # pylint: disable=broad-except _LOGGER.exception( "Error during connection setup to %s: %s (retrying in %s seconds)", @@ -404,37 +505,47 @@ def _setup_recorder(self) -> bool: err, self.db_retry_wait, ) - else: - _LOGGER.debug("Connected to recorder database") - self._open_event_session() - return True - tries += 1 time.sleep(self.db_retry_wait) - @callback - def connection_failed(): - """Connect failed tasks.""" - self.async_db_ready.set_result(False) - persistent_notification.async_create( - self.hass, - "The recorder could not start, please check the log", - "Recorder", - ) + return None - self.hass.add_job(connection_failed) - return False + def _migrate_schema_and_setup_run(self, current_version) -> bool: + """Migrate schema to the latest version.""" + persistent_notification.create( + self.hass, + "System performance will temporarily degrade during the database upgrade. Do not power down or restart the system until the upgrade completes. Integrations that read the database, such as logbook and history, may return inconsistent results until the upgrade completes.", + "Database upgrade in progress", + "recorder_database_migration", + ) + + try: + migration.migrate_schema(self, current_version) + except exc.DatabaseError as err: + if self._handle_database_error(err): + return True + _LOGGER.exception("Database error during schema migration") + return False + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error during schema migration") + return False + else: + self._setup_run() + return True + finally: + persistent_notification.dismiss(self.hass, "recorder_database_migration") + + def _run_purge(self, keep_days, repack, apply_filter): + """Purge the database.""" + if purge.purge_old_data(self, keep_days, repack, apply_filter): + return + # Schedule a new purge task if this one didn't finish + self.queue.put(PurgeTask(keep_days, repack, apply_filter)) def _process_one_event(self, event): """Process one event.""" if isinstance(event, PurgeTask): - # Schedule a new purge task if this one didn't finish - if not purge.purge_old_data( - self, event.keep_days, event.repack, event.apply_filter - ): - self.queue.put( - PurgeTask(event.keep_days, event.repack, event.apply_filter) - ) + self._run_purge(event.keep_days, event.repack, event.apply_filter) return if isinstance(event, WaitTask): self._queue_watch.set() @@ -448,7 +559,7 @@ def _process_one_event(self, event): self._timechanges_seen += 1 if self._timechanges_seen >= self.commit_interval: self._timechanges_seen = 0 - self._commit_event_session_or_recover() + self._commit_event_session_or_retry() return if not self.enabled: @@ -464,10 +575,6 @@ def _process_one_event(self, event): except (TypeError, ValueError): _LOGGER.warning("Event is not JSON serializable: %s", event) return - except Exception as err: # pylint: disable=broad-except - # Must catch the exception to prevent the loop from collapsing - _LOGGER.exception("Error adding event: %s", err) - return if event.event_type == EVENT_STATE_CHANGED: try: @@ -492,34 +599,21 @@ def _process_one_event(self, event): "State is not JSON serializable: %s", event.data.get("new_state"), ) - except Exception as err: # pylint: disable=broad-except - # Must catch the exception to prevent the loop from collapsing - _LOGGER.exception("Error adding state change: %s", err) # If they do not have a commit interval # than we commit right away if not self.commit_interval: - self._commit_event_session_or_recover() - - def _commit_event_session_or_recover(self): - """Commit changes to the database and recover if the database fails when possible.""" - try: self._commit_event_session_or_retry() - return - except exc.DatabaseError as err: - if isinstance(err.__cause__, sqlite3.DatabaseError): - _LOGGER.exception( - "Unrecoverable sqlite3 database corruption detected: %s", err - ) - self._handle_sqlite_corruption() - return - _LOGGER.exception("Unexpected error saving events: %s", err) - except Exception as err: # pylint: disable=broad-except - # Must catch the exception to prevent the loop from collapsing - _LOGGER.exception("Unexpected error saving events: %s", err) - self._reopen_event_session() - return + def _handle_database_error(self, err): + """Handle a database error that may result in moving away the corrupt db.""" + if isinstance(err.__cause__, sqlite3.DatabaseError): + _LOGGER.exception( + "Unrecoverable sqlite3 database corruption detected: %s", err + ) + self._handle_sqlite_corruption() + return True + return False def _commit_event_session_or_retry(self): tries = 1 @@ -566,44 +660,41 @@ def _commit_event_session(self): def _handle_sqlite_corruption(self): """Handle the sqlite3 database being corrupt.""" + self._close_event_session() self._close_connection() move_away_broken_database(dburl_to_path(self.db_url)) self._setup_recorder() + self._setup_run() - def _reopen_event_session(self): - """Rollback the event session and reopen it after a failure.""" + def _close_event_session(self): + """Close the event session.""" self._old_states = {} + if not self.event_session: + return + try: self.event_session.rollback() self.event_session.close() - except Exception as err: # pylint: disable=broad-except - # Must catch the exception to prevent the loop from collapsing + except SQLAlchemyError as err: _LOGGER.exception( "Error while rolling back and closing the event session: %s", err ) + def _reopen_event_session(self): + """Rollback the event session and reopen it after a failure.""" + self._close_event_session() self._open_event_session() def _open_event_session(self): """Open the event session.""" - try: - self.event_session = self.get_session() - self.event_session.expire_on_commit = False - except Exception as err: # pylint: disable=broad-except - _LOGGER.exception("Error while creating new event session: %s", err) + self.event_session = self.get_session() + self.event_session.expire_on_commit = False def _send_keep_alive(self): - try: - _LOGGER.debug("Sending keepalive") - self.event_session.connection().scalar(select([1])) - return - except Exception as err: # pylint: disable=broad-except - _LOGGER.error( - "Error in database connectivity during keepalive: %s", - err, - ) - self._reopen_event_session() + """Send a keep alive to keep the db connection open.""" + _LOGGER.debug("Sending keepalive") + self.event_session.connection().scalar(select([1])) @callback def event_listener(self, event): @@ -663,20 +754,7 @@ def setup_recorder_connection(dbapi_connection, connection_record): kwargs["echo"] = False if self._using_file_sqlite: - with self.hass.timeout.freeze(DOMAIN): - # - # Here we run an sqlite3 quick_check. In the majority - # of cases, the quick_check takes under 10 seconds. - # - # On systems with very large databases and - # very slow disk or cpus, this can take a while. - # - validate_or_move_away_sqlite_database( - self.db_url, self.db_integrity_check - ) - - if self.engine is not None: - self.engine.dispose() + validate_or_move_away_sqlite_database(self.db_url) self.engine = create_engine(self.db_url, **kwargs) @@ -684,6 +762,7 @@ def setup_recorder_connection(dbapi_connection, connection_record): Base.metadata.create_all(self.engine) self.get_session = scoped_session(sessionmaker(bind=self.engine)) + _LOGGER.debug("Connected to recorder database") @property def _using_file_sqlite(self): @@ -716,18 +795,24 @@ def _setup_run(self): session.flush() session.expunge(self.run_info) - def _shutdown(self): - """Save end time for current run.""" - if self.event_session is not None: + self._open_event_session() + + def _end_session(self): + """End the recorder session.""" + if self.event_session is None: + return + try: self.run_info.end = dt_util.utcnow() self.event_session.add(self.run_info) - try: - self._commit_event_session_or_retry() - self.event_session.close() - except Exception as err: # pylint: disable=broad-except - _LOGGER.exception( - "Error saving the event session during shutdown: %s", err - ) + self._commit_event_session_or_retry() + self.event_session.close() + except Exception as err: # pylint: disable=broad-except + _LOGGER.exception("Error saving the event session during shutdown: %s", err) self.run_info = None + + def _shutdown(self): + """Save end time for current run.""" + self._stop_queue_watcher_and_event_listener() + self._end_session() self._close_connection() diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index fa93f6155617c..5f138d01f176c 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -11,15 +11,14 @@ ) from sqlalchemy.schema import AddConstraint, DropConstraint -from .const import DOMAIN from .models import SCHEMA_VERSION, TABLE_STATES, Base, SchemaChanges from .util import session_scope _LOGGER = logging.getLogger(__name__) -def migrate_schema(instance): - """Check if the schema needs to be upgraded.""" +def get_schema_version(instance): + """Get the schema version.""" with session_scope(session=instance.get_session()) as session: res = ( session.query(SchemaChanges) @@ -34,21 +33,27 @@ def migrate_schema(instance): "No schema version found. Inspected version: %s", current_version ) - if current_version == SCHEMA_VERSION: - return + return current_version + + +def schema_is_current(current_version): + """Check if the schema is current.""" + return current_version == SCHEMA_VERSION + +def migrate_schema(instance, current_version): + """Check if the schema needs to be upgraded.""" + with session_scope(session=instance.get_session()) as session: _LOGGER.warning( "Database is about to upgrade. Schema version: %s", current_version ) + for version in range(current_version, SCHEMA_VERSION): + new_version = version + 1 + _LOGGER.info("Upgrading recorder db schema to version %s", new_version) + _apply_update(instance.engine, new_version, current_version) + session.add(SchemaChanges(schema_version=new_version)) - with instance.hass.timeout.freeze(DOMAIN): - for version in range(current_version, SCHEMA_VERSION): - new_version = version + 1 - _LOGGER.info("Upgrading recorder db schema to version %s", new_version) - _apply_update(instance.engine, new_version, current_version) - session.add(SchemaChanges(schema_version=new_version)) - - _LOGGER.info("Upgrade to version %s done", new_version) + _LOGGER.info("Upgrade to version %s done", new_version) def _create_index(engine, table_name, index_name): diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index ef626a744c4fa..424070156b05c 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -6,7 +6,7 @@ import time from typing import TYPE_CHECKING -from sqlalchemy.exc import OperationalError, SQLAlchemyError +from sqlalchemy.exc import OperationalError from sqlalchemy.orm.session import Session from sqlalchemy.sql.expression import distinct @@ -69,8 +69,7 @@ def purge_old_data( return False _LOGGER.warning("Error purging history: %s", err) - except SQLAlchemyError as err: - _LOGGER.warning("Error purging history: %s", err) + return True diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index c17fb33d365ed..89f74c44f4e67 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -14,8 +14,13 @@ from homeassistant.helpers.typing import HomeAssistantType import homeassistant.util.dt as dt_util -from .const import CONF_DB_INTEGRITY_CHECK, DATA_INSTANCE, SQLITE_URL_PREFIX -from .models import ALL_TABLES, process_timestamp +from .const import DATA_INSTANCE, SQLITE_URL_PREFIX +from .models import ( + ALL_TABLES, + TABLE_RECORDER_RUNS, + TABLE_SCHEMA_CHANGES, + process_timestamp, +) _LOGGER = logging.getLogger(__name__) @@ -117,7 +122,7 @@ def execute(qry, to_native=False, validate_entity_ids=True): time.sleep(QUERY_RETRY_WAIT) -def validate_or_move_away_sqlite_database(dburl: str, db_integrity_check: bool) -> bool: +def validate_or_move_away_sqlite_database(dburl: str) -> bool: """Ensure that the database is valid or move it away.""" dbpath = dburl_to_path(dburl) @@ -125,7 +130,7 @@ def validate_or_move_away_sqlite_database(dburl: str, db_integrity_check: bool) # Database does not exist yet, this is OK return True - if not validate_sqlite_database(dbpath, db_integrity_check): + if not validate_sqlite_database(dbpath): move_away_broken_database(dbpath) return False @@ -161,18 +166,21 @@ def basic_sanity_check(cursor): """Check tables to make sure select does not fail.""" for table in ALL_TABLES: - cursor.execute(f"SELECT * FROM {table} LIMIT 1;") # nosec # not injection + if table in (TABLE_RECORDER_RUNS, TABLE_SCHEMA_CHANGES): + cursor.execute(f"SELECT * FROM {table};") # nosec # not injection + else: + cursor.execute(f"SELECT * FROM {table} LIMIT 1;") # nosec # not injection return True -def validate_sqlite_database(dbpath: str, db_integrity_check: bool) -> bool: +def validate_sqlite_database(dbpath: str) -> bool: """Run a quick check on an sqlite database to see if it is corrupt.""" import sqlite3 # pylint: disable=import-outside-toplevel try: conn = sqlite3.connect(dbpath) - run_checks_on_open_db(dbpath, conn.cursor(), db_integrity_check) + run_checks_on_open_db(dbpath, conn.cursor()) conn.close() except sqlite3.DatabaseError: _LOGGER.exception("The database at %s is corrupt or malformed", dbpath) @@ -181,24 +189,14 @@ def validate_sqlite_database(dbpath: str, db_integrity_check: bool) -> bool: return True -def run_checks_on_open_db(dbpath, cursor, db_integrity_check): +def run_checks_on_open_db(dbpath, cursor): """Run checks that will generate a sqlite3 exception if there is corruption.""" sanity_check_passed = basic_sanity_check(cursor) last_run_was_clean = last_run_was_recently_clean(cursor) if sanity_check_passed and last_run_was_clean: _LOGGER.debug( - "The quick_check will be skipped as the system was restarted cleanly and passed the basic sanity check" - ) - return - - if not db_integrity_check: - # Always warn so when it does fail they remember it has - # been manually disabled - _LOGGER.warning( - "The quick_check on the sqlite3 database at %s was skipped because %s was disabled", - dbpath, - CONF_DB_INTEGRITY_CHECK, + "The system was restarted cleanly and passed the basic sanity check" ) return @@ -214,11 +212,6 @@ def run_checks_on_open_db(dbpath, cursor, db_integrity_check): dbpath, ) - _LOGGER.info( - "A quick_check is being performed on the sqlite3 database at %s", dbpath - ) - cursor.execute("PRAGMA QUICK_CHECK") - def move_away_broken_database(dbfile: str) -> None: """Move away a broken sqlite3 database.""" diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index b3c58995b37e2..67032e9f077d2 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -3,13 +3,14 @@ from datetime import datetime, timedelta from unittest.mock import patch -from sqlalchemy.exc import OperationalError +from sqlalchemy.exc import OperationalError, SQLAlchemyError +from homeassistant.components import recorder from homeassistant.components.recorder import ( CONF_DB_URL, CONFIG_SCHEMA, - DATA_INSTANCE, DOMAIN, + KEEPALIVE_TIME, SERVICE_DISABLE, SERVICE_ENABLE, SERVICE_PURGE, @@ -19,15 +20,17 @@ run_information_from_instance, run_information_with_session, ) +from homeassistant.components.recorder.const import DATA_INSTANCE from homeassistant.components.recorder.models import Events, RecorderRuns, States from homeassistant.components.recorder.util import session_scope from homeassistant.const import ( + EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, MATCH_ALL, STATE_LOCKED, STATE_UNLOCKED, ) -from homeassistant.core import Context, CoreState, callback +from homeassistant.core import Context, CoreState, HomeAssistant, callback from homeassistant.helpers.typing import HomeAssistantType from homeassistant.setup import async_setup_component, setup_component from homeassistant.util import dt as dt_util @@ -41,18 +44,35 @@ from .conftest import SetupRecorderInstanceT from tests.common import ( + async_fire_time_changed, async_init_recorder_component, fire_time_changed, get_test_home_assistant, ) +def _default_recorder(hass): + """Return a recorder with reasonable defaults.""" + return Recorder( + hass, + auto_purge=True, + keep_days=7, + commit_interval=1, + uri="sqlite://", + db_max_retries=10, + db_retry_wait=3, + entity_filter=CONFIG_SCHEMA({DOMAIN: {}}), + exclude_t=[], + ) + + async def test_shutdown_before_startup_finishes(hass): """Test shutdown before recorder starts is clean.""" hass.state = CoreState.not_running await async_init_recorder_component(hass) + await hass.data[DATA_INSTANCE].async_db_ready await hass.async_block_till_done() session = await hass.async_add_executor_job(hass.data[DATA_INSTANCE].get_session) @@ -69,6 +89,31 @@ async def test_shutdown_before_startup_finishes(hass): assert run_info.end is not None +async def test_state_gets_saved_when_set_before_start_event( + hass: HomeAssistant, async_setup_recorder_instance: SetupRecorderInstanceT +): + """Test we can record an event when starting with not running.""" + + hass.state = CoreState.not_running + + await async_init_recorder_component(hass) + + entity_id = "test.recorder" + state = "restoring_from_db" + attributes = {"test_attr": 5, "test_attr_10": "nice"} + + hass.states.async_set(entity_id, state, attributes) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + + await async_wait_recording_done_without_instance(hass) + + with session_scope(hass=hass) as session: + db_states = list(session.query(States)) + assert len(db_states) == 1 + assert db_states[0].event_id > 0 + + async def test_saving_state( hass: HomeAssistantType, async_setup_recorder_instance: SetupRecorderInstanceT ): @@ -92,6 +137,58 @@ async def test_saving_state( assert state == _state_empty_context(hass, entity_id) +async def test_saving_many_states( + hass: HomeAssistantType, async_setup_recorder_instance: SetupRecorderInstanceT +): + """Test we expire after many commits.""" + instance = await async_setup_recorder_instance(hass) + + entity_id = "test.recorder" + attributes = {"test_attr": 5, "test_attr_10": "nice"} + + with patch.object( + hass.data[DATA_INSTANCE].event_session, "expire_all" + ) as expire_all, patch.object(recorder, "EXPIRE_AFTER_COMMITS", 2): + for _ in range(3): + hass.states.async_set(entity_id, "on", attributes) + await async_wait_recording_done(hass, instance) + hass.states.async_set(entity_id, "off", attributes) + await async_wait_recording_done(hass, instance) + + assert expire_all.called + + with session_scope(hass=hass) as session: + db_states = list(session.query(States)) + assert len(db_states) == 6 + assert db_states[0].event_id > 0 + + +async def test_saving_state_with_intermixed_time_changes( + hass: HomeAssistantType, async_setup_recorder_instance: SetupRecorderInstanceT +): + """Test saving states with intermixed time changes.""" + instance = await async_setup_recorder_instance(hass) + + entity_id = "test.recorder" + state = "restoring_from_db" + attributes = {"test_attr": 5, "test_attr_10": "nice"} + attributes2 = {"test_attr": 10, "test_attr_10": "mean"} + + for _ in range(KEEPALIVE_TIME + 1): + async_fire_time_changed(hass, dt_util.utcnow()) + hass.states.async_set(entity_id, state, attributes) + for _ in range(KEEPALIVE_TIME + 1): + async_fire_time_changed(hass, dt_util.utcnow()) + hass.states.async_set(entity_id, state, attributes2) + + await async_wait_recording_done(hass, instance) + + with session_scope(hass=hass) as session: + db_states = list(session.query(States)) + assert len(db_states) == 2 + assert db_states[0].event_id > 0 + + def test_saving_state_with_exception(hass, hass_recorder, caplog): """Test saving and restoring a state.""" hass = hass_recorder() @@ -130,6 +227,44 @@ def _throw_if_state_in_session(*args, **kwargs): assert "Error saving events" not in caplog.text +def test_saving_state_with_sqlalchemy_exception(hass, hass_recorder, caplog): + """Test saving state when there is an SQLAlchemyError.""" + hass = hass_recorder() + + entity_id = "test.recorder" + state = "restoring_from_db" + attributes = {"test_attr": 5, "test_attr_10": "nice"} + + def _throw_if_state_in_session(*args, **kwargs): + for obj in hass.data[DATA_INSTANCE].event_session: + if isinstance(obj, States): + raise SQLAlchemyError( + "insert the state", "fake params", "forced to fail" + ) + + with patch("time.sleep"), patch.object( + hass.data[DATA_INSTANCE].event_session, + "flush", + side_effect=_throw_if_state_in_session, + ): + hass.states.set(entity_id, "fail", attributes) + wait_recording_done(hass) + + assert "SQLAlchemyError error processing event" in caplog.text + + caplog.clear() + hass.states.set(entity_id, state, attributes) + wait_recording_done(hass) + + with session_scope(hass=hass) as session: + db_states = list(session.query(States)) + assert len(db_states) >= 1 + + assert "Error executing query" not in caplog.text + assert "Error saving events" not in caplog.text + assert "SQLAlchemyError error processing event" not in caplog.text + + def test_saving_event(hass, hass_recorder): """Test saving and restoring an event.""" hass = hass_recorder() @@ -171,6 +306,25 @@ def event_listener(event): ) +def test_saving_state_with_commit_interval_zero(hass_recorder): + """Test saving a state with a commit interval of zero.""" + hass = hass_recorder({"commit_interval": 0}) + assert hass.data[DATA_INSTANCE].commit_interval == 0 + + entity_id = "test.recorder" + state = "restoring_from_db" + attributes = {"test_attr": 5, "test_attr_10": "nice"} + + hass.states.set(entity_id, state, attributes) + + wait_recording_done(hass) + + with session_scope(hass=hass) as session: + db_states = list(session.query(States)) + assert len(db_states) == 1 + assert db_states[0].event_id > 0 + + def _add_entities(hass, entity_ids): """Add entities.""" attributes = {"test_attr": 5, "test_attr_10": "nice"} @@ -351,26 +505,27 @@ def test_saving_state_and_removing_entity(hass, hass_recorder): assert states[2].state is None -def test_recorder_setup_failure(): +def test_recorder_setup_failure(hass): """Test some exceptions.""" - hass = get_test_home_assistant() + with patch.object(Recorder, "_setup_connection") as setup, patch( + "homeassistant.components.recorder.time.sleep" + ): + setup.side_effect = ImportError("driver not found") + rec = _default_recorder(hass) + rec.async_initialize() + rec.start() + rec.join() + + hass.stop() + +def test_recorder_setup_failure_without_event_listener(hass): + """Test recorder setup failure when the event listener is not setup.""" with patch.object(Recorder, "_setup_connection") as setup, patch( "homeassistant.components.recorder.time.sleep" ): setup.side_effect = ImportError("driver not found") - rec = Recorder( - hass, - auto_purge=True, - keep_days=7, - commit_interval=1, - uri="sqlite://", - db_max_retries=10, - db_retry_wait=3, - entity_filter=CONFIG_SCHEMA({DOMAIN: {}}), - exclude_t=[], - db_integrity_check=False, - ) + rec = _default_recorder(hass) rec.start() rec.join() @@ -481,6 +636,7 @@ def test_saving_state_with_serializable_data(hass_recorder, caplog): """Test saving data that cannot be serialized does not crash.""" hass = hass_recorder() + hass.bus.fire("bad_event", {"fail": CannotSerializeMe()}) hass.states.set("test.one", "on", {"fail": CannotSerializeMe()}) wait_recording_done(hass) hass.states.set("test.two", "on", {}) @@ -699,15 +855,20 @@ def _create_tmpdir_for_test_db(): hass.states.async_set("test.lost", "on", {}) - await async_wait_recording_done_without_instance(hass) - await hass.async_add_executor_job(corrupt_db_file, test_db_file) - await async_wait_recording_done_without_instance(hass) - - # This state will not be recorded because - # the database corruption will be discovered - # and we will have to rollback to recover - hass.states.async_set("test.one", "off", {}) - await async_wait_recording_done_without_instance(hass) + with patch.object( + hass.data[DATA_INSTANCE].event_session, + "close", + side_effect=OperationalError("statement", {}, []), + ): + await async_wait_recording_done_without_instance(hass) + await hass.async_add_executor_job(corrupt_db_file, test_db_file) + await async_wait_recording_done_without_instance(hass) + + # This state will not be recorded because + # the database corruption will be discovered + # and we will have to rollback to recover + hass.states.async_set("test.one", "off", {}) + await async_wait_recording_done_without_instance(hass) assert "Unrecoverable sqlite3 database corruption detected" in caplog.text assert "The system will rename the corrupt database file" in caplog.text diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index c4e0d32adcf06..113598ff6dece 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -1,19 +1,41 @@ """The tests for the Recorder component.""" # pylint: disable=protected-access +import datetime +import sqlite3 from unittest.mock import Mock, PropertyMock, call, patch import pytest from sqlalchemy import create_engine -from sqlalchemy.exc import InternalError, OperationalError, ProgrammingError +from sqlalchemy.exc import ( + DatabaseError, + InternalError, + OperationalError, + ProgrammingError, +) from sqlalchemy.pool import StaticPool from homeassistant.bootstrap import async_setup_component -from homeassistant.components.recorder import RecorderRuns, const, migration, models +from homeassistant.components import recorder +from homeassistant.components.recorder import RecorderRuns, migration, models +from homeassistant.components.recorder.const import DATA_INSTANCE +from homeassistant.components.recorder.models import States +from homeassistant.components.recorder.util import session_scope import homeassistant.util.dt as dt_util +from .common import async_wait_recording_done_without_instance + +from tests.common import async_fire_time_changed, async_mock_service from tests.components.recorder import models_original +def _get_native_states(hass, entity_id): + with session_scope(hass=hass) as session: + return [ + state.to_native() + for state in session.query(States).filter(States.entity_id == entity_id) + ] + + def create_engine_test(*args, **kwargs): """Test version of create_engine that initializes with old schema. @@ -26,6 +48,7 @@ def create_engine_test(*args, **kwargs): async def test_schema_update_calls(hass): """Test that schema migrations occur in correct order.""" + await async_setup_component(hass, "persistent_notification", {}) with patch( "homeassistant.components.recorder.create_engine", new=create_engine_test ), patch( @@ -35,16 +58,147 @@ async def test_schema_update_calls(hass): await async_setup_component( hass, "recorder", {"recorder": {"db_url": "sqlite://"}} ) - await hass.async_block_till_done() + await async_wait_recording_done_without_instance(hass) update.assert_has_calls( [ - call(hass.data[const.DATA_INSTANCE].engine, version + 1, 0) + call(hass.data[DATA_INSTANCE].engine, version + 1, 0) for version in range(0, models.SCHEMA_VERSION) ] ) +async def test_database_migration_failed(hass): + """Test we notify if the migration fails.""" + await async_setup_component(hass, "persistent_notification", {}) + create_calls = async_mock_service(hass, "persistent_notification", "create") + dismiss_calls = async_mock_service(hass, "persistent_notification", "dismiss") + + with patch( + "homeassistant.components.recorder.create_engine", new=create_engine_test + ), patch( + "homeassistant.components.recorder.migration._apply_update", + side_effect=ValueError, + ): + await async_setup_component( + hass, "recorder", {"recorder": {"db_url": "sqlite://"}} + ) + hass.states.async_set("my.entity", "on", {}) + hass.states.async_set("my.entity", "off", {}) + await hass.async_block_till_done() + await hass.async_add_executor_job(hass.data[DATA_INSTANCE].join) + await hass.async_block_till_done() + + assert len(create_calls) == 2 + assert len(dismiss_calls) == 1 + + +async def test_database_migration_encounters_corruption(hass): + """Test we move away the database if its corrupt.""" + await async_setup_component(hass, "persistent_notification", {}) + + sqlite3_exception = DatabaseError("statement", {}, []) + sqlite3_exception.__cause__ = sqlite3.DatabaseError() + + with patch( + "homeassistant.components.recorder.migration.schema_is_current", + side_effect=[False, True], + ), patch( + "homeassistant.components.recorder.migration.migrate_schema", + side_effect=sqlite3_exception, + ), patch( + "homeassistant.components.recorder.move_away_broken_database" + ) as move_away: + await async_setup_component( + hass, "recorder", {"recorder": {"db_url": "sqlite://"}} + ) + hass.states.async_set("my.entity", "on", {}) + hass.states.async_set("my.entity", "off", {}) + await async_wait_recording_done_without_instance(hass) + + assert move_away.called + + +async def test_database_migration_encounters_corruption_not_sqlite(hass): + """Test we fail on database error when we cannot recover.""" + await async_setup_component(hass, "persistent_notification", {}) + create_calls = async_mock_service(hass, "persistent_notification", "create") + dismiss_calls = async_mock_service(hass, "persistent_notification", "dismiss") + + with patch( + "homeassistant.components.recorder.migration.schema_is_current", + side_effect=[False, True], + ), patch( + "homeassistant.components.recorder.migration.migrate_schema", + side_effect=DatabaseError("statement", {}, []), + ), patch( + "homeassistant.components.recorder.move_away_broken_database" + ) as move_away: + await async_setup_component( + hass, "recorder", {"recorder": {"db_url": "sqlite://"}} + ) + hass.states.async_set("my.entity", "on", {}) + hass.states.async_set("my.entity", "off", {}) + await hass.async_block_till_done() + await hass.async_add_executor_job(hass.data[DATA_INSTANCE].join) + await hass.async_block_till_done() + + assert not move_away.called + assert len(create_calls) == 2 + assert len(dismiss_calls) == 1 + + +async def test_events_during_migration_are_queued(hass): + """Test that events during migration are queued.""" + + await async_setup_component(hass, "persistent_notification", {}) + with patch( + "homeassistant.components.recorder.create_engine", new=create_engine_test + ): + await async_setup_component( + hass, "recorder", {"recorder": {"db_url": "sqlite://"}} + ) + hass.states.async_set("my.entity", "on", {}) + hass.states.async_set("my.entity", "off", {}) + await hass.async_block_till_done() + async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(hours=2)) + await hass.async_block_till_done() + async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(hours=4)) + await hass.data[DATA_INSTANCE].async_recorder_ready.wait() + await async_wait_recording_done_without_instance(hass) + + db_states = await hass.async_add_executor_job(_get_native_states, hass, "my.entity") + assert len(db_states) == 2 + + +async def test_events_during_migration_queue_exhausted(hass): + """Test that events during migration takes so long the queue is exhausted.""" + await async_setup_component(hass, "persistent_notification", {}) + + with patch( + "homeassistant.components.recorder.create_engine", new=create_engine_test + ), patch.object(recorder, "MAX_QUEUE_BACKLOG", 1): + await async_setup_component( + hass, "recorder", {"recorder": {"db_url": "sqlite://"}} + ) + hass.states.async_set("my.entity", "on", {}) + await hass.async_block_till_done() + async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(hours=2)) + await hass.async_block_till_done() + async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(hours=4)) + await hass.async_block_till_done() + hass.states.async_set("my.entity", "off", {}) + await hass.data[DATA_INSTANCE].async_recorder_ready.wait() + await async_wait_recording_done_without_instance(hass) + + db_states = await hass.async_add_executor_job(_get_native_states, hass, "my.entity") + assert len(db_states) == 1 + hass.states.async_set("my.entity", "on", {}) + await async_wait_recording_done_without_instance(hass) + db_states = await hass.async_add_executor_job(_get_native_states, hass, "my.entity") + assert len(db_states) == 2 + + async def test_schema_migrate(hass): """Test the full schema migration logic. @@ -53,6 +207,8 @@ async def test_schema_migrate(hass): inspection could quickly become quite cumbersome. """ + await async_setup_component(hass, "persistent_notification", {}) + def _mock_setup_run(self): self.run_info = RecorderRuns( start=self.recording_start, created=dt_util.utcnow() diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index f2fa9bf640026..b97873df62e2a 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -1,7 +1,10 @@ """Test data purging.""" from datetime import datetime, timedelta import json +import sqlite3 +from unittest.mock import patch +from sqlalchemy.exc import DatabaseError from sqlalchemy.orm.session import Session from homeassistant.components import recorder @@ -16,6 +19,7 @@ async_recorder_block_till_done, async_wait_purge_done, async_wait_recording_done, + async_wait_recording_done_without_instance, ) from .conftest import SetupRecorderInstanceT @@ -52,6 +56,38 @@ async def test_purge_old_states( assert states.count() == 2 +async def test_purge_old_states_encouters_database_corruption( + hass: HomeAssistantType, async_setup_recorder_instance: SetupRecorderInstanceT +): + """Test database image image is malformed while deleting old states.""" + instance = await async_setup_recorder_instance(hass) + + await _add_test_states(hass, instance) + await async_wait_recording_done_without_instance(hass) + + sqlite3_exception = DatabaseError("statement", {}, []) + sqlite3_exception.__cause__ = sqlite3.DatabaseError() + + with patch( + "homeassistant.components.recorder.move_away_broken_database" + ) as move_away, patch( + "homeassistant.components.recorder.purge.purge_old_data", + side_effect=sqlite3_exception, + ): + await hass.services.async_call( + recorder.DOMAIN, recorder.SERVICE_PURGE, {"keep_days": 0} + ) + await hass.async_block_till_done() + await async_wait_recording_done_without_instance(hass) + + assert move_away.called + + # Ensure the whole database was reset due to the database error + with session_scope(hass=hass) as session: + states_after_purge = session.query(States) + assert states_after_purge.count() == 0 + + async def test_purge_old_events( hass: HomeAssistantType, async_setup_recorder_instance: SetupRecorderInstanceT ): diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index c814570416c32..4da635209b334 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -74,75 +74,28 @@ def to_native(validate_entity_id=True): assert e_mock.call_count == 2 -def test_validate_or_move_away_sqlite_database_with_integrity_check( - hass, tmpdir, caplog -): - """Ensure a malformed sqlite database is moved away. - - A quick_check is run here - """ - - db_integrity_check = True - - test_dir = tmpdir.mkdir("test_validate_or_move_away_sqlite_database") - test_db_file = f"{test_dir}/broken.db" - dburl = f"{SQLITE_URL_PREFIX}{test_db_file}" - - assert util.validate_sqlite_database(test_db_file, db_integrity_check) is False - assert os.path.exists(test_db_file) is True - assert ( - util.validate_or_move_away_sqlite_database(dburl, db_integrity_check) is False - ) - - corrupt_db_file(test_db_file) - - assert util.validate_sqlite_database(dburl, db_integrity_check) is False - - assert ( - util.validate_or_move_away_sqlite_database(dburl, db_integrity_check) is False - ) - - assert "corrupt or malformed" in caplog.text - - assert util.validate_sqlite_database(dburl, db_integrity_check) is False - - assert util.validate_or_move_away_sqlite_database(dburl, db_integrity_check) is True - - -def test_validate_or_move_away_sqlite_database_without_integrity_check( - hass, tmpdir, caplog -): - """Ensure a malformed sqlite database is moved away. - - The quick_check is skipped, but we can still find - corruption if the whole database is unreadable - """ - - db_integrity_check = False +def test_validate_or_move_away_sqlite_database(hass, tmpdir, caplog): + """Ensure a malformed sqlite database is moved away.""" test_dir = tmpdir.mkdir("test_validate_or_move_away_sqlite_database") test_db_file = f"{test_dir}/broken.db" dburl = f"{SQLITE_URL_PREFIX}{test_db_file}" - assert util.validate_sqlite_database(test_db_file, db_integrity_check) is False + assert util.validate_sqlite_database(test_db_file) is False assert os.path.exists(test_db_file) is True - assert ( - util.validate_or_move_away_sqlite_database(dburl, db_integrity_check) is False - ) + assert util.validate_or_move_away_sqlite_database(dburl) is False corrupt_db_file(test_db_file) - assert util.validate_sqlite_database(dburl, db_integrity_check) is False + assert util.validate_sqlite_database(dburl) is False - assert ( - util.validate_or_move_away_sqlite_database(dburl, db_integrity_check) is False - ) + assert util.validate_or_move_away_sqlite_database(dburl) is False assert "corrupt or malformed" in caplog.text - assert util.validate_sqlite_database(dburl, db_integrity_check) is False + assert util.validate_sqlite_database(dburl) is False - assert util.validate_or_move_away_sqlite_database(dburl, db_integrity_check) is True + assert util.validate_or_move_away_sqlite_database(dburl) is True async def test_last_run_was_recently_clean(hass): @@ -197,12 +150,10 @@ def test_combined_checks(hass_recorder, caplog): cursor = hass.data[DATA_INSTANCE].engine.raw_connection().cursor() - assert util.run_checks_on_open_db("fake_db_path", cursor, False) is None - assert "skipped because db_integrity_check was disabled" in caplog.text + assert util.run_checks_on_open_db("fake_db_path", cursor) is None + assert "could not validate that the sqlite3 database" in caplog.text caplog.clear() - assert util.run_checks_on_open_db("fake_db_path", cursor, True) is None - assert "could not validate that the sqlite3 database" in caplog.text # We are patching recorder.util here in order # to avoid creating the full database on disk @@ -210,50 +161,36 @@ def test_combined_checks(hass_recorder, caplog): "homeassistant.components.recorder.util.basic_sanity_check", return_value=False ): caplog.clear() - assert util.run_checks_on_open_db("fake_db_path", cursor, False) is None - assert "skipped because db_integrity_check was disabled" in caplog.text - - caplog.clear() - assert util.run_checks_on_open_db("fake_db_path", cursor, True) is None + assert util.run_checks_on_open_db("fake_db_path", cursor) is None assert "could not validate that the sqlite3 database" in caplog.text # We are patching recorder.util here in order # to avoid creating the full database on disk with patch("homeassistant.components.recorder.util.last_run_was_recently_clean"): caplog.clear() - assert util.run_checks_on_open_db("fake_db_path", cursor, False) is None - assert ( - "system was restarted cleanly and passed the basic sanity check" - in caplog.text - ) - - caplog.clear() - assert util.run_checks_on_open_db("fake_db_path", cursor, True) is None - assert ( - "system was restarted cleanly and passed the basic sanity check" - in caplog.text - ) + assert util.run_checks_on_open_db("fake_db_path", cursor) is None + assert "restarted cleanly and passed the basic sanity check" in caplog.text caplog.clear() with patch( "homeassistant.components.recorder.util.last_run_was_recently_clean", side_effect=sqlite3.DatabaseError, ), pytest.raises(sqlite3.DatabaseError): - util.run_checks_on_open_db("fake_db_path", cursor, False) + util.run_checks_on_open_db("fake_db_path", cursor) caplog.clear() with patch( "homeassistant.components.recorder.util.last_run_was_recently_clean", side_effect=sqlite3.DatabaseError, ), pytest.raises(sqlite3.DatabaseError): - util.run_checks_on_open_db("fake_db_path", cursor, True) + util.run_checks_on_open_db("fake_db_path", cursor) cursor.execute("DROP TABLE events;") caplog.clear() with pytest.raises(sqlite3.DatabaseError): - util.run_checks_on_open_db("fake_db_path", cursor, False) + util.run_checks_on_open_db("fake_db_path", cursor) caplog.clear() with pytest.raises(sqlite3.DatabaseError): - util.run_checks_on_open_db("fake_db_path", cursor, True) + util.run_checks_on_open_db("fake_db_path", cursor) From a6100760016f4aaa12cbdeeab9eccd4b6c998b92 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 12 Apr 2021 10:02:04 +0200 Subject: [PATCH 0208/1317] Support min()/max() as template function (#48996) --- homeassistant/helpers/template.py | 3 +++ tests/helpers/test_template.py | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index ea338e22b8441..83c347c7cb234 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -1457,6 +1457,9 @@ def __init__(self, hass, limited=False, strict=False): self.globals["timedelta"] = timedelta self.globals["strptime"] = strptime self.globals["urlencode"] = urlencode + self.globals["max"] = max + self.globals["min"] = min + if hass is None: return diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 06b313218ca8e..0e8b2f76843a7 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -567,11 +567,15 @@ def test_from_json(hass): def test_min(hass): """Test the min filter.""" assert template.Template("{{ [1, 2, 3] | min }}", hass).async_render() == 1 + assert template.Template("{{ min([1, 2, 3]) }}", hass).async_render() == 1 + assert template.Template("{{ min(1, 2, 3) }}", hass).async_render() == 1 def test_max(hass): """Test the max filter.""" assert template.Template("{{ [1, 2, 3] | max }}", hass).async_render() == 3 + assert template.Template("{{ max([1, 2, 3]) }}", hass).async_render() == 3 + assert template.Template("{{ max(1, 2, 3) }}", hass).async_render() == 3 def test_ord(hass): From 73f227b6514e32c6be5eb1c937a5d74077994a37 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 11 Apr 2021 22:31:25 -1000 Subject: [PATCH 0209/1317] Use shared httpx client in enphase_envoy (#48709) * Use shared httpx client in enphase_envoy * test fix * f * bump version --- homeassistant/components/enphase_envoy/__init__.py | 11 ++++++++--- homeassistant/components/enphase_envoy/config_flow.py | 9 +++++++-- homeassistant/components/enphase_envoy/manifest.json | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 19 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/enphase_envoy/__init__.py b/homeassistant/components/enphase_envoy/__init__.py index 1b8d09b1f1d81..faa9247b4e71f 100644 --- a/homeassistant/components/enphase_envoy/__init__.py +++ b/homeassistant/components/enphase_envoy/__init__.py @@ -13,6 +13,7 @@ from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.httpx_client import get_async_client from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import COORDINATOR, DOMAIN, NAME, PLATFORMS, SENSORS @@ -27,8 +28,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: config = entry.data name = config[CONF_NAME] + envoy_reader = EnvoyReader( - config[CONF_HOST], config[CONF_USERNAME], config[CONF_PASSWORD] + config[CONF_HOST], + config[CONF_USERNAME], + config[CONF_PASSWORD], + async_client=get_async_client(hass), ) try: @@ -36,7 +41,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except httpx.HTTPStatusError as err: _LOGGER.error("Authentication failure during setup: %s", err) return - except (AttributeError, httpx.HTTPError) as err: + except (RuntimeError, httpx.HTTPError) as err: raise ConfigEntryNotReady from err async def async_update_data(): @@ -63,7 +68,7 @@ async def async_update_data(): coordinator = DataUpdateCoordinator( hass, _LOGGER, - name="envoy {name}", + name=f"envoy {name}", update_method=async_update_data, update_interval=SCAN_INTERVAL, ) diff --git a/homeassistant/components/enphase_envoy/config_flow.py b/homeassistant/components/enphase_envoy/config_flow.py index 41d72c09a31b7..934b02be31104 100644 --- a/homeassistant/components/enphase_envoy/config_flow.py +++ b/homeassistant/components/enphase_envoy/config_flow.py @@ -18,6 +18,7 @@ ) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.httpx_client import get_async_client from .const import DOMAIN @@ -31,14 +32,18 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: """Validate the user input allows us to connect.""" envoy_reader = EnvoyReader( - data[CONF_HOST], data[CONF_USERNAME], data[CONF_PASSWORD], inverters=True + data[CONF_HOST], + data[CONF_USERNAME], + data[CONF_PASSWORD], + inverters=True, + async_client=get_async_client(hass), ) try: await envoy_reader.getData() except httpx.HTTPStatusError as err: raise InvalidAuth from err - except (AttributeError, httpx.HTTPError) as err: + except (RuntimeError, httpx.HTTPError) as err: raise CannotConnect from err diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index 236010607372b..9b8f01f254702 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -3,11 +3,11 @@ "name": "Enphase Envoy", "documentation": "https://www.home-assistant.io/integrations/enphase_envoy", "requirements": [ - "envoy_reader==0.18.3" + "envoy_reader==0.18.4" ], "codeowners": [ "@gtdiehl" ], "config_flow": true, "zeroconf": [{ "type": "_enphase-envoy._tcp.local."}] -} \ No newline at end of file +} diff --git a/requirements_all.txt b/requirements_all.txt index d10f866636e4e..a58cf60f2a534 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -557,7 +557,7 @@ env_canada==0.2.5 # envirophat==0.0.6 # homeassistant.components.enphase_envoy -envoy_reader==0.18.3 +envoy_reader==0.18.4 # homeassistant.components.season ephem==3.7.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a0732f55aa25e..667b49a8335af 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -303,7 +303,7 @@ emulated_roku==0.2.1 enocean==0.50 # homeassistant.components.enphase_envoy -envoy_reader==0.18.3 +envoy_reader==0.18.4 # homeassistant.components.season ephem==3.7.7.0 From 06a8ffe94d2204091c904b4c61a9cf26db162311 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Mon, 12 Apr 2021 04:41:20 -0400 Subject: [PATCH 0210/1317] Bump pyeconet to 0.1.14 (#49067) * Bump pyeconet to fix crash * Bump pyeconet from beta version * Update requirements_all --- homeassistant/components/econet/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/econet/manifest.json b/homeassistant/components/econet/manifest.json index c658542295e86..379fd8953590a 100644 --- a/homeassistant/components/econet/manifest.json +++ b/homeassistant/components/econet/manifest.json @@ -4,6 +4,6 @@ "name": "Rheem EcoNet Products", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/econet", - "requirements": ["pyeconet==0.1.13"], + "requirements": ["pyeconet==0.1.14"], "codeowners": ["@vangorra", "@w1ll1am23"] } \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index a58cf60f2a534..862faace7acd1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1364,7 +1364,7 @@ pydroid-ipcam==0.8 pyebox==1.1.4 # homeassistant.components.econet -pyeconet==0.1.13 +pyeconet==0.1.14 # homeassistant.components.edimax pyedimax==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 667b49a8335af..fa1494895fd84 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -735,7 +735,7 @@ pydexcom==0.2.0 pydispatcher==2.0.5 # homeassistant.components.econet -pyeconet==0.1.13 +pyeconet==0.1.14 # homeassistant.components.everlights pyeverlights==0.1.0 From 9c11f6547a67780716b494e1ea243f306e5e6a53 Mon Sep 17 00:00:00 2001 From: Zero King Date: Mon, 12 Apr 2021 09:56:35 +0000 Subject: [PATCH 0211/1317] Fix forecast pressure unit in OpenWeatherMap (#49069) --- homeassistant/components/openweathermap/const.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/openweathermap/const.py b/homeassistant/components/openweathermap/const.py index 36d38ff4688f9..bde7e74159c86 100644 --- a/homeassistant/components/openweathermap/const.py +++ b/homeassistant/components/openweathermap/const.py @@ -245,7 +245,11 @@ SENSOR_NAME: "Precipitation probability", SENSOR_UNIT: PERCENTAGE, }, - ATTR_FORECAST_PRESSURE: {SENSOR_NAME: "Pressure"}, + ATTR_FORECAST_PRESSURE: { + SENSOR_NAME: "Pressure", + SENSOR_UNIT: PRESSURE_HPA, + SENSOR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, + }, ATTR_FORECAST_TEMP: { SENSOR_NAME: "Temperature", SENSOR_UNIT: TEMP_CELSIUS, From fe80afdb862d16c7a442f444d3bfd04b3c8c5e01 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 12 Apr 2021 07:08:42 -0400 Subject: [PATCH 0212/1317] Add support for custom configurations in ZHA (#48423) * initial configuration options * first crack at saving the data * constants * implement initial options * make more dynamic * fix unload and reload of the config entry * update unload --- homeassistant/components/zha/__init__.py | 17 +++++- homeassistant/components/zha/api.py | 62 ++++++++++++++++++++ homeassistant/components/zha/core/const.py | 17 ++++++ homeassistant/components/zha/core/device.py | 12 +++- homeassistant/components/zha/core/gateway.py | 8 +-- homeassistant/components/zha/core/helpers.py | 19 +++++- homeassistant/components/zha/light.py | 19 +++++- 7 files changed, 142 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 707e0292c4516..43b95a9c2f2b8 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -29,6 +29,7 @@ DATA_ZHA_DISPATCHERS, DATA_ZHA_GATEWAY, DATA_ZHA_PLATFORM_LOADED, + DATA_ZHA_SHUTDOWN_TASK, DOMAIN, PLATFORMS, SIGNAL_ADD_ENTITIES, @@ -121,7 +122,9 @@ async def async_zha_shutdown(event): await zha_data[DATA_ZHA_GATEWAY].shutdown() await zha_data[DATA_ZHA_GATEWAY].async_update_device_storage() - hass.bus.async_listen_once(ha_const.EVENT_HOMEASSISTANT_STOP, async_zha_shutdown) + zha_data[DATA_ZHA_SHUTDOWN_TASK] = hass.bus.async_listen_once( + ha_const.EVENT_HOMEASSISTANT_STOP, async_zha_shutdown + ) asyncio.create_task(async_load_entities(hass)) return True @@ -129,6 +132,7 @@ async def async_zha_shutdown(event): async def async_unload_entry(hass, config_entry): """Unload ZHA config entry.""" await hass.data[DATA_ZHA][DATA_ZHA_GATEWAY].shutdown() + await hass.data[DATA_ZHA][DATA_ZHA_GATEWAY].async_update_device_storage() GROUP_PROBE.cleanup() api.async_unload_api(hass) @@ -137,8 +141,15 @@ async def async_unload_entry(hass, config_entry): for unsub_dispatcher in dispatchers: unsub_dispatcher() - for platform in PLATFORMS: - await hass.config_entries.async_forward_entry_unload(config_entry, platform) + # our components don't have unload methods so no need to look at return values + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(config_entry, platform) + for platform in PLATFORMS + ] + ) + + hass.data[DATA_ZHA][DATA_ZHA_SHUTDOWN_TASK]() return True diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index 7e265d03c0940..b5b29534ed903 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -7,6 +7,7 @@ from typing import Any import voluptuous as vol +from zigpy.config.validators import cv_boolean from zigpy.types.named import EUI64 import zigpy.zdo.types as zdo_types @@ -40,6 +41,7 @@ CLUSTER_COMMANDS_SERVER, CLUSTER_TYPE_IN, CLUSTER_TYPE_OUT, + CUSTOM_CONFIGURATION, DATA_ZHA, DATA_ZHA_GATEWAY, DOMAIN, @@ -52,6 +54,7 @@ WARNING_DEVICE_SQUAWK_MODE_ARMED, WARNING_DEVICE_STROBE_HIGH, WARNING_DEVICE_STROBE_YES, + ZHA_CONFIG_SCHEMAS, ) from .core.group import GroupMember from .core.helpers import ( @@ -882,6 +885,63 @@ async def async_binding_operation(zha_gateway, source_ieee, target_ieee, operati zdo.debug(fmt, *(log_msg[2] + (outcome,))) +@websocket_api.require_admin +@websocket_api.async_response +@websocket_api.websocket_command({vol.Required(TYPE): "zha/configuration"}) +async def websocket_get_configuration(hass, connection, msg): + """Get ZHA configuration.""" + zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + import voluptuous_serialize # pylint: disable=import-outside-toplevel + + def custom_serializer(schema: Any) -> Any: + """Serialize additional types for voluptuous_serialize.""" + if schema is cv_boolean: + return {"type": "bool"} + if schema is vol.Schema: + return voluptuous_serialize.convert( + schema, custom_serializer=custom_serializer + ) + + return cv.custom_serializer(schema) + + data = {"schemas": {}, "data": {}} + for section, schema in ZHA_CONFIG_SCHEMAS.items(): + data["schemas"][section] = voluptuous_serialize.convert( + schema, custom_serializer=custom_serializer + ) + data["data"][section] = zha_gateway.config_entry.options.get( + CUSTOM_CONFIGURATION, {} + ).get(section, {}) + connection.send_result(msg[ID], data) + + +@websocket_api.require_admin +@websocket_api.async_response +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zha/configuration/update", + vol.Required("data"): ZHA_CONFIG_SCHEMAS, + } +) +async def websocket_update_zha_configuration(hass, connection, msg): + """Update the ZHA configuration.""" + zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + options = zha_gateway.config_entry.options + data_to_save = {**options, **{CUSTOM_CONFIGURATION: msg["data"]}} + + _LOGGER.info( + "Updating ZHA custom configuration options from %s to %s", + options, + data_to_save, + ) + + hass.config_entries.async_update_entry( + zha_gateway.config_entry, options=data_to_save + ) + status = await hass.config_entries.async_reload(zha_gateway.config_entry.entry_id) + connection.send_result(msg[ID], status) + + @callback def async_load_api(hass): """Set up the web socket API.""" @@ -1189,6 +1249,8 @@ async def warning_device_warn(service): websocket_api.async_register_command(hass, websocket_bind_devices) websocket_api.async_register_command(hass, websocket_unbind_devices) websocket_api.async_register_command(hass, websocket_update_topology) + websocket_api.async_register_command(hass, websocket_get_configuration) + websocket_api.async_register_command(hass, websocket_update_zha_configuration) @callback diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index 2c968a5f02d1a..f43d9febc55f0 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -5,6 +5,7 @@ import logging import bellows.zigbee.application +import voluptuous as vol from zigpy.config import CONF_DEVICE_PATH # noqa: F401 # pylint: disable=unused-import import zigpy_cc.zigbee.application import zigpy_deconz.zigbee.application @@ -22,6 +23,7 @@ from homeassistant.components.number import DOMAIN as NUMBER from homeassistant.components.sensor import DOMAIN as SENSOR from homeassistant.components.switch import DOMAIN as SWITCH +import homeassistant.helpers.config_validation as cv from .typing import CALLABLE_T @@ -118,13 +120,24 @@ CONF_BAUDRATE = "baudrate" CONF_DATABASE = "database_path" +CONF_DEFAULT_LIGHT_TRANSITION = "default_light_transition" CONF_DEVICE_CONFIG = "device_config" +CONF_ENABLE_IDENTIFY_ON_JOIN = "enable_identify_on_join" CONF_ENABLE_QUIRKS = "enable_quirks" CONF_FLOWCONTROL = "flow_control" CONF_RADIO_TYPE = "radio_type" CONF_USB_PATH = "usb_path" CONF_ZIGPY = "zigpy_config" +CONF_ZHA_OPTIONS_SCHEMA = vol.Schema( + { + vol.Optional(CONF_DEFAULT_LIGHT_TRANSITION): cv.positive_int, + vol.Required(CONF_ENABLE_IDENTIFY_ON_JOIN, default=True): cv.boolean, + } +) + +CUSTOM_CONFIGURATION = "custom_configuration" + DATA_DEVICE_CONFIG = "zha_device_config" DATA_ZHA = "zha" DATA_ZHA_CONFIG = "config" @@ -133,6 +146,7 @@ DATA_ZHA_DISPATCHERS = "zha_dispatchers" DATA_ZHA_GATEWAY = "zha_gateway" DATA_ZHA_PLATFORM_LOADED = "platform_loaded" +DATA_ZHA_SHUTDOWN_TASK = "zha_shutdown_task" DEBUG_COMP_BELLOWS = "bellows" DEBUG_COMP_ZHA = "homeassistant.components.zha" @@ -176,6 +190,9 @@ PRESET_SCHEDULE = "schedule" PRESET_COMPLEX = "complex" +ZHA_OPTIONS = "zha_options" +ZHA_CONFIG_SCHEMAS = {ZHA_OPTIONS: CONF_ZHA_OPTIONS_SCHEMA} + class RadioType(enum.Enum): """Possible options for radio type.""" diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 65605b2f7a3ff..ab3c9b3b9e6d3 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -56,6 +56,7 @@ CLUSTER_COMMANDS_SERVER, CLUSTER_TYPE_IN, CLUSTER_TYPE_OUT, + CONF_ENABLE_IDENTIFY_ON_JOIN, EFFECT_DEFAULT_VARIANT, EFFECT_OKAY, POWER_BATTERY_OR_UNKNOWN, @@ -66,7 +67,7 @@ UNKNOWN_MANUFACTURER, UNKNOWN_MODEL, ) -from .helpers import LogMixin +from .helpers import LogMixin, async_get_zha_config_value _LOGGER = logging.getLogger(__name__) CONSIDER_UNAVAILABLE_MAINS = 60 * 60 * 2 # 2 hours @@ -395,13 +396,20 @@ def device_info(self): async def async_configure(self): """Configure the device.""" + should_identify = async_get_zha_config_value( + self._zha_gateway.config_entry, CONF_ENABLE_IDENTIFY_ON_JOIN, True + ) self.debug("started configuration") await self._channels.async_configure() self.debug("completed configuration") entry = self.gateway.zha_storage.async_create_or_update_device(self) self.debug("stored in registry: %s", entry) - if self._channels.identify_ch is not None and not self.skip_configuration: + if ( + should_identify + and self._channels.identify_ch is not None + and not self.skip_configuration + ): await self._channels.identify_ch.trigger_effect( EFFECT_OKAY, EFFECT_DEFAULT_VARIANT ) diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 96e4a7c3eb818..4a9e6c2820360 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -127,7 +127,7 @@ def __init__(self, hass, config, config_entry): } self.debug_enabled = False self._log_relay_handler = LogRelayHandler(hass, self) - self._config_entry = config_entry + self.config_entry = config_entry self._unsubs = [] async def async_initialize(self): @@ -139,7 +139,7 @@ async def async_initialize(self): self.ha_device_registry = await get_dev_reg(self._hass) self.ha_entity_registry = await get_ent_reg(self._hass) - radio_type = self._config_entry.data[CONF_RADIO_TYPE] + radio_type = self.config_entry.data[CONF_RADIO_TYPE] app_controller_cls = RadioType[radio_type].controller self.radio_description = RadioType[radio_type].description @@ -150,7 +150,7 @@ async def async_initialize(self): os.path.join(self._hass.config.config_dir, DEFAULT_DATABASE_NAME), ) app_config[CONF_DATABASE] = database - app_config[CONF_DEVICE] = self._config_entry.data[CONF_DEVICE] + app_config[CONF_DEVICE] = self.config_entry.data[CONF_DEVICE] app_config = app_controller_cls.SCHEMA(app_config) try: @@ -506,7 +506,7 @@ def _async_get_or_create_device( zha_device = ZHADevice.new(self._hass, zigpy_device, self, restored) self._devices[zigpy_device.ieee] = zha_device device_registry_device = self.ha_device_registry.async_get_or_create( - config_entry_id=self._config_entry.entry_id, + config_entry_id=self.config_entry.entry_id, connections={(CONNECTION_ZIGBEE, str(zha_device.ieee))}, identifiers={(DOMAIN, str(zha_device.ieee))}, name=zha_device.name, diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index cf3d040f02053..f8fb12e159627 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -24,7 +24,14 @@ from homeassistant.core import State, callback -from .const import CLUSTER_TYPE_IN, CLUSTER_TYPE_OUT, DATA_ZHA, DATA_ZHA_GATEWAY +from .const import ( + CLUSTER_TYPE_IN, + CLUSTER_TYPE_OUT, + CUSTOM_CONFIGURATION, + DATA_ZHA, + DATA_ZHA_GATEWAY, + ZHA_OPTIONS, +) from .registries import BINDABLE_CLUSTERS from .typing import ZhaDeviceType, ZigpyClusterType @@ -122,6 +129,16 @@ def async_is_bindable_target(source_zha_device, target_zha_device): return False +@callback +def async_get_zha_config_value(config_entry, config_key, default): + """Get the value for the specified configuration from the zha config entry.""" + return ( + config_entry.options.get(CUSTOM_CONFIGURATION, {}) + .get(ZHA_OPTIONS, {}) + .get(config_key, default) + ) + + async def async_get_zha_device(hass, device_id): """Get a ZHA device for the given device registry id.""" device_registry = await hass.helpers.device_registry.async_get_registry() diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 72807458d266a..6701a9bb3c78d 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -47,6 +47,7 @@ CHANNEL_COLOR, CHANNEL_LEVEL, CHANNEL_ON_OFF, + CONF_DEFAULT_LIGHT_TRANSITION, DATA_ZHA, DATA_ZHA_DISPATCHERS, EFFECT_BLINK, @@ -56,7 +57,7 @@ SIGNAL_ATTR_UPDATED, SIGNAL_SET_LEVEL, ) -from .core.helpers import LogMixin +from .core.helpers import LogMixin, async_get_zha_config_value from .core.registries import ZHA_ENTITIES from .core.typing import ZhaDeviceType from .entity import ZhaEntity, ZhaGroupEntity @@ -139,6 +140,7 @@ def __init__(self, *args, **kwargs): self._level_channel = None self._color_channel = None self._identify_channel = None + self._default_transition = None @property def extra_state_attributes(self) -> dict[str, Any]: @@ -207,7 +209,13 @@ def supported_features(self): async def async_turn_on(self, **kwargs): """Turn the entity on.""" transition = kwargs.get(light.ATTR_TRANSITION) - duration = transition * 10 if transition else DEFAULT_TRANSITION + duration = ( + transition * 10 + if transition + else self._default_transition * 10 + if self._default_transition + else DEFAULT_TRANSITION + ) brightness = kwargs.get(light.ATTR_BRIGHTNESS) effect = kwargs.get(light.ATTR_EFFECT) flash = kwargs.get(light.ATTR_FLASH) @@ -389,6 +397,10 @@ def __init__(self, unique_id, zha_device: ZhaDeviceType, channels, **kwargs): if effect_list: self._effect_list = effect_list + self._default_transition = async_get_zha_config_value( + zha_device.gateway.config_entry, CONF_DEFAULT_LIGHT_TRANSITION, 0 + ) + @callback def async_set_state(self, attr_id, attr_name, value): """Set the state.""" @@ -544,6 +556,9 @@ def __init__( self._color_channel = group.endpoint[Color.cluster_id] self._identify_channel = group.endpoint[Identify.cluster_id] self._debounced_member_refresh = None + self._default_transition = async_get_zha_config_value( + zha_device.gateway.config_entry, CONF_DEFAULT_LIGHT_TRANSITION, 0 + ) async def async_added_to_hass(self): """Run when about to be added to hass.""" From 05468a50f40ba9670d325de292c7adf2b366af50 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 12 Apr 2021 13:57:30 +0200 Subject: [PATCH 0213/1317] Fix xbox type hint (#49102) --- homeassistant/components/xbox/media_player.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/xbox/media_player.py b/homeassistant/components/xbox/media_player.py index e57f3971042ce..be798cd999aef 100644 --- a/homeassistant/components/xbox/media_player.py +++ b/homeassistant/components/xbox/media_player.py @@ -2,7 +2,6 @@ from __future__ import annotations import re -from typing import List from xbox.webapi.api.client import XboxLiveClient from xbox.webapi.api.provider.catalog.models import Image @@ -233,7 +232,7 @@ def device_info(self): } -def _find_media_image(images=List[Image]) -> Image | None: +def _find_media_image(images: list[Image]) -> Image | None: purpose_order = ["FeaturePromotionalSquareArt", "Tile", "Logo", "BoxArt"] for purpose in purpose_order: for image in images: From 885f528711eac4db2fc10b5eb7a564a8f68e793f Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 12 Apr 2021 17:07:18 +0200 Subject: [PATCH 0214/1317] Replace old style type comments (#49103) --- homeassistant/components/stream/core.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py index 076eb3596d7a3..cac4aa1eccb33 100644 --- a/homeassistant/components/stream/core.py +++ b/homeassistant/components/stream/core.py @@ -8,6 +8,8 @@ from aiohttp import web import attr +import av.container +import av.video from homeassistant.components.http import HomeAssistantView from homeassistant.core import HomeAssistant, callback @@ -24,9 +26,9 @@ class StreamBuffer: """Represent a segment.""" segment: io.BytesIO = attr.ib() - output = attr.ib() # type=av.OutputContainer - vstream = attr.ib() # type=av.VideoStream - astream = attr.ib(default=None) # type=Optional[av.AudioStream] + output: av.container.OutputContainer = attr.ib() + vstream: av.video.VideoStream = attr.ib() + astream = attr.ib(default=None) # type=Optional[av.audio.AudioStream] @attr.s From ebc2bec08dc00bb1e89d4344854546d2c23f7601 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Mon, 12 Apr 2021 17:02:59 +0100 Subject: [PATCH 0215/1317] Reduce reporting delta for ZHA humidity channel (#49070) --- homeassistant/components/zha/core/channels/measurement.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zha/core/channels/measurement.py b/homeassistant/components/zha/core/channels/measurement.py index 78ff12a9bf300..99d062d4c3ed3 100644 --- a/homeassistant/components/zha/core/channels/measurement.py +++ b/homeassistant/components/zha/core/channels/measurement.py @@ -57,7 +57,7 @@ class RelativeHumidity(ZigbeeChannel): REPORT_CONFIG = [ { "attr": "measured_value", - "config": (REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 50), + "config": (REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 100), } ] From dbb771e19cc78d0c34b455c354461bb0046004dc Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 12 Apr 2021 11:29:45 -0500 Subject: [PATCH 0216/1317] Check all endpoints for zwave_js.climate hvac_action (#49115) --- homeassistant/components/zwave_js/climate.py | 1 + tests/components/zwave_js/test_climate.py | 2 ++ tests/components/zwave_js/test_services.py | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py index c64a5ef788fc9..41ea873c5f892 100644 --- a/homeassistant/components/zwave_js/climate.py +++ b/homeassistant/components/zwave_js/climate.py @@ -150,6 +150,7 @@ def __init__( THERMOSTAT_OPERATING_STATE_PROPERTY, command_class=CommandClass.THERMOSTAT_OPERATING_STATE, add_to_watched_value_ids=True, + check_all_endpoints=True, ) self._current_temp = self.get_zwave_value( THERMOSTAT_CURRENT_TEMP_PROPERTY, diff --git a/tests/components/zwave_js/test_climate.py b/tests/components/zwave_js/test_climate.py index 83a607f3add39..2084e771546f6 100644 --- a/tests/components/zwave_js/test_climate.py +++ b/tests/components/zwave_js/test_climate.py @@ -12,6 +12,7 @@ ATTR_PRESET_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + CURRENT_HVAC_COOL, CURRENT_HVAC_IDLE, DOMAIN as CLIMATE_DOMAIN, HVAC_MODE_COOL, @@ -351,6 +352,7 @@ async def test_thermostat_different_endpoints( assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 22.8 assert state.attributes[ATTR_FAN_MODE] == "Auto low" assert state.attributes[ATTR_FAN_STATE] == "Idle / off" + assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_COOL async def test_setpoint_thermostat(hass, client, climate_danfoss_lc_13, integration): diff --git a/tests/components/zwave_js/test_services.py b/tests/components/zwave_js/test_services.py index 7bdba7894d2f5..956361d39536a 100644 --- a/tests/components/zwave_js/test_services.py +++ b/tests/components/zwave_js/test_services.py @@ -528,7 +528,7 @@ async def test_poll_value( }, blocking=True, ) - assert len(client.async_send_command.call_args_list) == 7 + assert len(client.async_send_command.call_args_list) == 8 # Test polling against an invalid entity raises ValueError with pytest.raises(ValueError): From f5545badac89b594b245b6639b8b47b6980ae494 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 12 Apr 2021 18:32:12 +0200 Subject: [PATCH 0217/1317] Quote media_source paths (#49054) * Quote path in async_sign_path * Address review comments, add tests * Update tests/testing_config/media/Epic Sax Guy 10 Hours.mp4 Co-authored-by: Paulus Schoutsen --- homeassistant/components/cast/media_player.py | 3 ++- homeassistant/components/http/auth.py | 10 ++++++++-- homeassistant/components/media_source/__init__.py | 3 ++- tests/components/media_source/test_init.py | 12 +++++++----- tests/components/media_source/test_local_source.py | 3 +++ tests/testing_config/media/Epic Sax Guy 10 Hours.mp4 | 1 + 6 files changed, 23 insertions(+), 9 deletions(-) create mode 100644 tests/testing_config/media/Epic Sax Guy 10 Hours.mp4 diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index 016d5162d2380..afd6065cb9878 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -7,6 +7,7 @@ import functools as ft import json import logging +from urllib.parse import quote import pychromecast from pychromecast.controllers.homeassistant import HomeAssistantController @@ -472,7 +473,7 @@ async def async_play_media(self, media_type, media_id, **kwargs): media_id = async_sign_path( self.hass, refresh_token.id, - media_id, + quote(media_id), timedelta(seconds=media_source.DEFAULT_EXPIRY_TIME), ) diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index 3267c9cc70e74..382758194832f 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -1,6 +1,7 @@ """Authentication for HTTP component.""" import logging import secrets +from urllib.parse import unquote from aiohttp import hdrs from aiohttp.web import middleware @@ -30,11 +31,16 @@ def async_sign_path(hass, refresh_token_id, path, expiration): now = dt_util.utcnow() encoded = jwt.encode( - {"iss": refresh_token_id, "path": path, "iat": now, "exp": now + expiration}, + { + "iss": refresh_token_id, + "path": unquote(path), + "iat": now, + "exp": now + expiration, + }, secret, algorithm="HS256", ) - return f"{path}?{SIGN_QUERY_PARAM}=" f"{encoded.decode()}" + return f"{path}?{SIGN_QUERY_PARAM}={encoded.decode()}" @callback diff --git a/homeassistant/components/media_source/__init__.py b/homeassistant/components/media_source/__init__.py index 0ef5d460580ff..5b027a99bf9ce 100644 --- a/homeassistant/components/media_source/__init__.py +++ b/homeassistant/components/media_source/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations from datetime import timedelta +from urllib.parse import quote import voluptuous as vol @@ -123,7 +124,7 @@ async def websocket_resolve_media(hass, connection, msg): url = async_sign_path( hass, connection.refresh_token_id, - url, + quote(url), timedelta(seconds=msg["expires"]), ) diff --git a/tests/components/media_source/test_init.py b/tests/components/media_source/test_init.py index 0dda9f67fbe36..d8ee73ebc2f11 100644 --- a/tests/components/media_source/test_init.py +++ b/tests/components/media_source/test_init.py @@ -1,5 +1,6 @@ """Test Media Source initialization.""" from unittest.mock import patch +from urllib.parse import quote import pytest @@ -45,7 +46,7 @@ async def test_async_browse_media(hass): media = await media_source.async_browse_media(hass, "") assert isinstance(media, media_source.models.BrowseMediaSource) assert media.title == "media/" - assert len(media.children) == 1 + assert len(media.children) == 2 # Test invalid media content with pytest.raises(ValueError): @@ -133,14 +134,15 @@ async def test_websocket_browse_media(hass, hass_ws_client): assert msg["error"]["message"] == "test" -async def test_websocket_resolve_media(hass, hass_ws_client): +@pytest.mark.parametrize("filename", ["test.mp3", "Epic Sax Guy 10 Hours.mp4"]) +async def test_websocket_resolve_media(hass, hass_ws_client, filename): """Test browse media websocket.""" assert await async_setup_component(hass, const.DOMAIN, {}) await hass.async_block_till_done() client = await hass_ws_client(hass) - media = media_source.models.PlayMedia("/media/local/test.mp3", "audio/mpeg") + media = media_source.models.PlayMedia(f"/media/local/{filename}", "audio/mpeg") with patch( "homeassistant.components.media_source.async_resolve_media", @@ -150,7 +152,7 @@ async def test_websocket_resolve_media(hass, hass_ws_client): { "id": 1, "type": "media_source/resolve_media", - "media_content_id": f"{const.URI_SCHEME}{const.DOMAIN}/local/test.mp3", + "media_content_id": f"{const.URI_SCHEME}{const.DOMAIN}/local/{filename}", } ) @@ -158,7 +160,7 @@ async def test_websocket_resolve_media(hass, hass_ws_client): assert msg["success"] assert msg["id"] == 1 - assert msg["result"]["url"].startswith(media.url) + assert msg["result"]["url"].startswith(quote(media.url)) assert msg["result"]["mime_type"] == media.mime_type with patch( diff --git a/tests/components/media_source/test_local_source.py b/tests/components/media_source/test_local_source.py index e3e2a3f1617d6..aff4f92be0228 100644 --- a/tests/components/media_source/test_local_source.py +++ b/tests/components/media_source/test_local_source.py @@ -95,5 +95,8 @@ async def test_media_view(hass, hass_client): resp = await client.get("/media/local/test.mp3") assert resp.status == 200 + resp = await client.get("/media/local/Epic Sax Guy 10 Hours.mp4") + assert resp.status == 200 + resp = await client.get("/media/recordings/test.mp3") assert resp.status == 200 diff --git a/tests/testing_config/media/Epic Sax Guy 10 Hours.mp4 b/tests/testing_config/media/Epic Sax Guy 10 Hours.mp4 new file mode 100644 index 0000000000000..23bd6ccc56486 --- /dev/null +++ b/tests/testing_config/media/Epic Sax Guy 10 Hours.mp4 @@ -0,0 +1 @@ +I play the sax From 106dc4d28ad59cb192c60fc7a354cafa86899ea4 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 12 Apr 2021 18:43:14 +0200 Subject: [PATCH 0218/1317] Don't import stdlib typing types from helpers.typing (#49104) --- homeassistant/components/edl21/sensor.py | 4 ++-- homeassistant/components/isy994/entity.py | 6 +++--- homeassistant/components/kodi/config_flow.py | 20 ++++++++++--------- .../command_line/test_binary_sensor.py | 8 ++++++-- tests/components/command_line/test_cover.py | 7 +++++-- tests/components/command_line/test_notify.py | 7 +++++-- tests/components/command_line/test_sensor.py | 7 +++++-- tests/components/command_line/test_switch.py | 7 +++++-- 8 files changed, 42 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/edl21/sensor.py b/homeassistant/components/edl21/sensor.py index 090b2780ec468..64f78530ffa54 100644 --- a/homeassistant/components/edl21/sensor.py +++ b/homeassistant/components/edl21/sensor.py @@ -1,4 +1,5 @@ """Support for EDL21 Smart Meters.""" +from __future__ import annotations from datetime import timedelta import logging @@ -16,7 +17,6 @@ async_dispatcher_send, ) from homeassistant.helpers.entity_registry import async_get_registry -from homeassistant.helpers.typing import Optional from homeassistant.util.dt import utcnow _LOGGER = logging.getLogger(__name__) @@ -258,7 +258,7 @@ def old_unique_id(self) -> str: return self._obis @property - def name(self) -> Optional[str]: + def name(self) -> str | None: """Return a name.""" return self._name diff --git a/homeassistant/components/isy994/entity.py b/homeassistant/components/isy994/entity.py index f3dbe579dd85d..25a2dc428a6b1 100644 --- a/homeassistant/components/isy994/entity.py +++ b/homeassistant/components/isy994/entity.py @@ -1,4 +1,5 @@ """Representation of ISYEntity Types.""" +from __future__ import annotations from pyisy.constants import ( COMMAND_FRIENDLY_NAME, @@ -11,7 +12,6 @@ from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import Dict from .const import _LOGGER, DOMAIN @@ -134,7 +134,7 @@ class ISYNodeEntity(ISYEntity): """Representation of a ISY Nodebase (Node/Group) entity.""" @property - def extra_state_attributes(self) -> Dict: + def extra_state_attributes(self) -> dict: """Get the state attributes for the device. The 'aux_properties' in the pyisy Node class are combined with the @@ -186,7 +186,7 @@ def __init__(self, name: str, status, actions=None) -> None: self._actions = actions @property - def extra_state_attributes(self) -> Dict: + def extra_state_attributes(self) -> dict: """Get the state attributes for the device.""" attr = {} if self._actions: diff --git a/homeassistant/components/kodi/config_flow.py b/homeassistant/components/kodi/config_flow.py index 0f5509a4e6639..4c0b6bb0da175 100644 --- a/homeassistant/components/kodi/config_flow.py +++ b/homeassistant/components/kodi/config_flow.py @@ -1,4 +1,6 @@ """Config flow for Kodi integration.""" +from __future__ import annotations + import logging from pykodi import CannotConnectError, InvalidAuthError, Kodi, get_kodi_connection @@ -16,7 +18,7 @@ ) from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.typing import DiscoveryInfoType, Optional +from homeassistant.helpers.typing import DiscoveryInfoType from .const import ( CONF_WS_PORT, @@ -90,14 +92,14 @@ class KodiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize flow.""" - self._host: Optional[str] = None - self._port: Optional[int] = DEFAULT_PORT - self._ws_port: Optional[int] = DEFAULT_WS_PORT - self._name: Optional[str] = None - self._username: Optional[str] = None - self._password: Optional[str] = None - self._ssl: Optional[bool] = DEFAULT_SSL - self._discovery_name: Optional[str] = None + self._host: str | None = None + self._port: int | None = DEFAULT_PORT + self._ws_port: int | None = DEFAULT_WS_PORT + self._name: str | None = None + self._username: str | None = None + self._password: str | None = None + self._ssl: bool | None = DEFAULT_SSL + self._discovery_name: str | None = None async def async_step_zeroconf(self, discovery_info: DiscoveryInfoType): """Handle zeroconf discovery.""" diff --git a/tests/components/command_line/test_binary_sensor.py b/tests/components/command_line/test_binary_sensor.py index 21209a8b60dc0..aa6395096c3d6 100644 --- a/tests/components/command_line/test_binary_sensor.py +++ b/tests/components/command_line/test_binary_sensor.py @@ -1,12 +1,16 @@ """The tests for the Command line Binary sensor platform.""" +from __future__ import annotations + +from typing import Any + from homeassistant import setup from homeassistant.components.binary_sensor import DOMAIN from homeassistant.const import STATE_OFF, STATE_ON -from homeassistant.helpers.typing import Any, Dict, HomeAssistantType +from homeassistant.helpers.typing import HomeAssistantType async def setup_test_entity( - hass: HomeAssistantType, config_dict: Dict[str, Any] + hass: HomeAssistantType, config_dict: dict[str, Any] ) -> None: """Set up a test command line binary_sensor entity.""" assert await setup.async_setup_component( diff --git a/tests/components/command_line/test_cover.py b/tests/components/command_line/test_cover.py index 093c1e86212d1..8ee69e8b5cbc4 100644 --- a/tests/components/command_line/test_cover.py +++ b/tests/components/command_line/test_cover.py @@ -1,6 +1,9 @@ """The tests the cover command line platform.""" +from __future__ import annotations + import os import tempfile +from typing import Any from unittest.mock import patch from homeassistant import config as hass_config, setup @@ -12,14 +15,14 @@ SERVICE_RELOAD, SERVICE_STOP_COVER, ) -from homeassistant.helpers.typing import Any, Dict, HomeAssistantType +from homeassistant.helpers.typing import HomeAssistantType import homeassistant.util.dt as dt_util from tests.common import async_fire_time_changed async def setup_test_entity( - hass: HomeAssistantType, config_dict: Dict[str, Any] + hass: HomeAssistantType, config_dict: dict[str, Any] ) -> None: """Set up a test command line notify service.""" assert await setup.async_setup_component( diff --git a/tests/components/command_line/test_notify.py b/tests/components/command_line/test_notify.py index 4166b9e8bbf25..b22b0323aadc0 100644 --- a/tests/components/command_line/test_notify.py +++ b/tests/components/command_line/test_notify.py @@ -1,16 +1,19 @@ """The tests for the command line notification platform.""" +from __future__ import annotations + import os import subprocess import tempfile +from typing import Any from unittest.mock import patch from homeassistant import setup from homeassistant.components.notify import DOMAIN -from homeassistant.helpers.typing import Any, Dict, HomeAssistantType +from homeassistant.helpers.typing import HomeAssistantType async def setup_test_service( - hass: HomeAssistantType, config_dict: Dict[str, Any] + hass: HomeAssistantType, config_dict: dict[str, Any] ) -> None: """Set up a test command line notify service.""" assert await setup.async_setup_component( diff --git a/tests/components/command_line/test_sensor.py b/tests/components/command_line/test_sensor.py index 66472c5feba9e..7e1f7707ca136 100644 --- a/tests/components/command_line/test_sensor.py +++ b/tests/components/command_line/test_sensor.py @@ -1,13 +1,16 @@ """The tests for the Command line sensor platform.""" +from __future__ import annotations + +from typing import Any from unittest.mock import patch from homeassistant import setup from homeassistant.components.sensor import DOMAIN -from homeassistant.helpers.typing import Any, Dict, HomeAssistantType +from homeassistant.helpers.typing import HomeAssistantType async def setup_test_entities( - hass: HomeAssistantType, config_dict: Dict[str, Any] + hass: HomeAssistantType, config_dict: dict[str, Any] ) -> None: """Set up a test command line sensor entity.""" assert await setup.async_setup_component( diff --git a/tests/components/command_line/test_switch.py b/tests/components/command_line/test_switch.py index 0e31999f92889..4439e6fdcb5e4 100644 --- a/tests/components/command_line/test_switch.py +++ b/tests/components/command_line/test_switch.py @@ -1,8 +1,11 @@ """The tests for the Command line switch platform.""" +from __future__ import annotations + import json import os import subprocess import tempfile +from typing import Any from unittest.mock import patch from homeassistant import setup @@ -14,14 +17,14 @@ STATE_OFF, STATE_ON, ) -from homeassistant.helpers.typing import Any, Dict, HomeAssistantType +from homeassistant.helpers.typing import HomeAssistantType import homeassistant.util.dt as dt_util from tests.common import async_fire_time_changed async def setup_test_entity( - hass: HomeAssistantType, config_dict: Dict[str, Any] + hass: HomeAssistantType, config_dict: dict[str, Any] ) -> None: """Set up a test command line switch entity.""" assert await setup.async_setup_component( From ff5fbea1fb8c7e11ad3d0fe2e69f2f1c15409383 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 12 Apr 2021 20:22:28 +0200 Subject: [PATCH 0219/1317] Improve trace of template conditions (#49101) * Improve trace of template conditions * Refactor * Fix wait_template trace * Update tests --- homeassistant/helpers/condition.py | 27 +++++++++++++++++--- homeassistant/helpers/script.py | 2 +- homeassistant/helpers/trace.py | 5 ++++ tests/components/trace/test_websocket_api.py | 13 +++++++--- tests/helpers/test_condition.py | 8 ++++++ tests/helpers/test_script.py | 22 +++++++++------- 6 files changed, 60 insertions(+), 17 deletions(-) diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index 6adcb4d1fd975..18ef4c2082e67 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -96,6 +96,18 @@ def condition_trace_set_result(result: bool, **kwargs: Any) -> None: node.set_result(result=result, **kwargs) +def condition_trace_update_result(result: bool, **kwargs: Any) -> None: + """Update the result of TraceElement at the top of the stack.""" + node = trace_stack_top(trace_stack_cv) + + # The condition function may be called directly, in which case tracing + # is not setup + if not node: + return + + node.update_result(result=result, **kwargs) + + @contextmanager def trace_condition(variables: TemplateVarsType) -> Generator: """Trace condition evaluation.""" @@ -118,7 +130,7 @@ def wrapper(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool: """Trace condition.""" with trace_condition(variables): result = condition(hass, variables) - condition_trace_set_result(result) + condition_trace_update_result(result) return result return wrapper @@ -644,15 +656,22 @@ def template( def async_template( - hass: HomeAssistant, value_template: Template, variables: TemplateVarsType = None + hass: HomeAssistant, + value_template: Template, + variables: TemplateVarsType = None, + trace_result: bool = True, ) -> bool: """Test if template condition matches.""" try: - value: str = value_template.async_render(variables, parse_result=False) + info = value_template.async_render_to_info(variables, parse_result=False) + value = info.result() except TemplateError as ex: raise ConditionErrorMessage("template", str(ex)) from ex - return value.lower() == "true" + result = value.lower() == "true" + if trace_result: + condition_trace_set_result(result, entities=list(info.entities)) + return result def async_template_from_config( diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index bf52fc81b6a39..84e7b0639e5e5 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -456,7 +456,7 @@ async def _async_wait_template_step(self): wait_template.hass = self._hass # check if condition already okay - if condition.async_template(self._hass, wait_template, self._variables): + if condition.async_template(self._hass, wait_template, self._variables, False): self._variables["wait"]["completed"] = True return diff --git a/homeassistant/helpers/trace.py b/homeassistant/helpers/trace.py index c92766036c639..32e387d972fa1 100644 --- a/homeassistant/helpers/trace.py +++ b/homeassistant/helpers/trace.py @@ -51,6 +51,11 @@ def set_result(self, **kwargs: Any) -> None: """Set result.""" self._result = {**kwargs} + def update_result(self, **kwargs: Any) -> None: + """Set result.""" + old_result = self._result or {} + self._result = {**old_result, **kwargs} + def as_dict(self) -> dict[str, Any]: """Return dictionary version of this TraceElement.""" result: dict[str, Any] = {"path": self.path, "timestamp": self._timestamp} diff --git a/tests/components/trace/test_websocket_api.py b/tests/components/trace/test_websocket_api.py index 0b7b78b3f1a56..8f8428dd51730 100644 --- a/tests/components/trace/test_websocket_api.py +++ b/tests/components/trace/test_websocket_api.py @@ -223,7 +223,8 @@ def next_id(): assert len(trace["trace"].get("condition/0", [])) == len(condition_results) for idx, condition_result in enumerate(condition_results): assert trace["trace"]["condition/0"][idx]["result"] == { - "result": condition_result + "result": condition_result, + "entities": [], } contexts[trace["context"]["id"]] = { "run_id": trace["run_id"], @@ -261,7 +262,10 @@ def next_id(): trace = response["result"] assert set(trace["trace"]) == extra_trace_keys[2] assert len(trace["trace"]["condition/0"]) == 1 - assert trace["trace"]["condition/0"][0]["result"] == {"result": False} + assert trace["trace"]["condition/0"][0]["result"] == { + "result": False, + "entities": [], + } assert trace["config"] == moon_config assert trace["context"] assert "error" not in trace @@ -303,7 +307,10 @@ def next_id(): assert "error" not in trace["trace"][f"{prefix}/0"][0] assert trace["trace"][f"{prefix}/0"][0]["result"] == moon_action assert len(trace["trace"]["condition/0"]) == 1 - assert trace["trace"]["condition/0"][0]["result"] == {"result": True} + assert trace["trace"]["condition/0"][0]["result"] == { + "result": True, + "entities": [], + } assert trace["config"] == moon_config assert trace["context"] assert "error" not in trace diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index 05f348ddfeb06..d46c343dfb166 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -237,6 +237,14 @@ async def test_and_condition_with_template(hass): hass.states.async_set("sensor.temperature", 120) assert not test(hass) + assert_condition_trace( + { + "": [{"result": {"result": False}}], + "conditions/0": [ + {"result": {"entities": ["sensor.temperature"], "result": False}} + ], + } + ) hass.states.async_set("sensor.temperature", 105) assert not test(hass) diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 7224dd706778e..a0207edcbddbd 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -1449,7 +1449,7 @@ async def test_condition_basic(hass, caplog): { "0": [{"result": {"event": "test_event", "event_data": {}}}], "1": [{"result": {"result": True}}], - "1/condition": [{"result": {"result": True}}], + "1/condition": [{"result": {"entities": ["test.entity"], "result": True}}], "2": [{"result": {"event": "test_event", "event_data": {}}}], } ) @@ -1466,7 +1466,7 @@ async def test_condition_basic(hass, caplog): { "0": [{"result": {"event": "test_event", "event_data": {}}}], "1": [{"error_type": script._StopScript, "result": {"result": False}}], - "1/condition": [{"result": {"result": False}}], + "1/condition": [{"result": {"entities": ["test.entity"], "result": False}}], }, expected_script_execution="aborted", ) @@ -1764,9 +1764,9 @@ async def test_repeat_var_in_condition(hass, condition): }, ], "0/repeat/while/0": [ - {"result": {"result": True}}, - {"result": {"result": True}}, - {"result": {"result": False}}, + {"result": {"entities": [], "result": True}}, + {"result": {"entities": [], "result": True}}, + {"result": {"entities": [], "result": False}}, ], "0/repeat/sequence/0": [ {"result": {"event": "test_event", "event_data": {}}} @@ -1797,8 +1797,8 @@ async def test_repeat_var_in_condition(hass, condition): }, ], "0/repeat/until/0": [ - {"result": {"result": False}}, - {"result": {"result": True}}, + {"result": {"entities": [], "result": False}}, + {"result": {"entities": [], "result": True}}, ], } assert_action_trace(expected_trace) @@ -2058,10 +2058,14 @@ async def test_choose(hass, caplog, var, result): expected_trace = {"0": [{"result": {"choice": expected_choice}}]} if var >= 1: expected_trace["0/choose/0"] = [{"result": {"result": var == 1}}] - expected_trace["0/choose/0/conditions/0"] = [{"result": {"result": var == 1}}] + expected_trace["0/choose/0/conditions/0"] = [ + {"result": {"entities": [], "result": var == 1}} + ] if var >= 2: expected_trace["0/choose/1"] = [{"result": {"result": var == 2}}] - expected_trace["0/choose/1/conditions/0"] = [{"result": {"result": var == 2}}] + expected_trace["0/choose/1/conditions/0"] = [ + {"result": {"entities": [], "result": var == 2}} + ] if var == 1: expected_trace["0/choose/0/sequence/0"] = [ {"result": {"event": "test_event", "event_data": {"choice": "first"}}} From b98ca49a56a441f84bd49eb29e510f27f6b566ab Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 12 Apr 2021 16:12:38 -0400 Subject: [PATCH 0220/1317] Add min and max temp properties to zwave_js.climate (#49125) --- homeassistant/components/zwave_js/climate.py | 39 +++++++++++++++++++- tests/components/zwave_js/test_climate.py | 4 ++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py index 41ea873c5f892..0cad9de8065be 100644 --- a/homeassistant/components/zwave_js/climate.py +++ b/homeassistant/components/zwave_js/climate.py @@ -18,7 +18,11 @@ ) from zwave_js_server.model.value import Value as ZwaveValue -from homeassistant.components.climate import ClimateEntity +from homeassistant.components.climate import ( + DEFAULT_MAX_TEMP, + DEFAULT_MIN_TEMP, + ClimateEntity, +) from homeassistant.components.climate.const import ( ATTR_HVAC_MODE, ATTR_TARGET_TEMP_HIGH, @@ -49,6 +53,7 @@ ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.temperature import convert_temperature from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN from .discovery import ZwaveDiscoveryInfo @@ -375,6 +380,38 @@ def supported_features(self) -> int: """Return the list of supported features.""" return self._supported_features + @property + def min_temp(self) -> float: + """Return the minimum temperature.""" + min_temp = DEFAULT_MIN_TEMP + base_unit = TEMP_CELSIUS + try: + temp = self._setpoint_value(self._current_mode_setpoint_enums[0]) + if temp.metadata.min: + min_temp = temp.metadata.min + base_unit = self.temperature_unit + # In case of any error, we fallback to the default + except (IndexError, ValueError, TypeError): + pass + + return convert_temperature(min_temp, base_unit, self.temperature_unit) + + @property + def max_temp(self) -> float: + """Return the maximum temperature.""" + max_temp = DEFAULT_MAX_TEMP + base_unit = TEMP_CELSIUS + try: + temp = self._setpoint_value(self._current_mode_setpoint_enums[0]) + if temp.metadata.max: + max_temp = temp.metadata.max + base_unit = self.temperature_unit + # In case of any error, we fallback to the default + except (IndexError, ValueError, TypeError): + pass + + return convert_temperature(max_temp, base_unit, self.temperature_unit) + async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new target fan mode.""" if not self._fan_mode: diff --git a/tests/components/zwave_js/test_climate.py b/tests/components/zwave_js/test_climate.py index 2084e771546f6..8682ce98b5be0 100644 --- a/tests/components/zwave_js/test_climate.py +++ b/tests/components/zwave_js/test_climate.py @@ -9,6 +9,8 @@ ATTR_HVAC_ACTION, ATTR_HVAC_MODE, ATTR_HVAC_MODES, + ATTR_MAX_TEMP, + ATTR_MIN_TEMP, ATTR_PRESET_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, @@ -448,6 +450,8 @@ async def test_thermostat_heatit(hass, client, climate_heatit_z_trm3, integratio assert state.attributes[ATTR_TEMPERATURE] == 22.5 assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE assert state.attributes[ATTR_SUPPORTED_FEATURES] == SUPPORT_TARGET_TEMPERATURE + assert state.attributes[ATTR_MIN_TEMP] == 5 + assert state.attributes[ATTR_MAX_TEMP] == 35 async def test_thermostat_srt321_hrt4_zw(hass, client, srt321_hrt4_zw, integration): From de4b1eebdd6826658574c53d0026d1db1c6cfb66 Mon Sep 17 00:00:00 2001 From: Ludovico de Nittis Date: Mon, 12 Apr 2021 23:24:15 +0200 Subject: [PATCH 0221/1317] iAlarm small code quality improvements (#49126) --- homeassistant/components/ialarm/strings.json | 3 +-- tests/components/ialarm/test_init.py | 26 +++----------------- 2 files changed, 5 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/ialarm/strings.json b/homeassistant/components/ialarm/strings.json index 5976a95ea5dda..1ac7a25e6f89b 100644 --- a/homeassistant/components/ialarm/strings.json +++ b/homeassistant/components/ialarm/strings.json @@ -4,8 +4,7 @@ "user": { "data": { "host": "[%key:common::config_flow::data::host%]", - "port": "[%key:common::config_flow::data::port%]", - "pin": "[%key:common::config_flow::data::pin%]" + "port": "[%key:common::config_flow::data::port%]" } } }, diff --git a/tests/components/ialarm/test_init.py b/tests/components/ialarm/test_init.py index 2f1936aff81a5..8998b4e0d18b9 100644 --- a/tests/components/ialarm/test_init.py +++ b/tests/components/ialarm/test_init.py @@ -11,7 +11,6 @@ ENTRY_STATE_SETUP_RETRY, ) from homeassistant.const import CONF_HOST, CONF_PORT -from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -38,17 +37,9 @@ async def test_setup_entry(hass, ialarm_api, mock_config_entry): ialarm_api.return_value.get_mac = Mock(return_value="00:00:54:12:34:56") mock_config_entry.add_to_hass(hass) - await async_setup_component( - hass, - DOMAIN, - { - "ialarm": { - CONF_HOST: "192.168.10.20", - CONF_PORT: 18034, - }, - }, - ) + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() + ialarm_api.return_value.get_mac.assert_called_once() assert mock_config_entry.state == ENTRY_STATE_LOADED @@ -58,7 +49,7 @@ async def test_setup_not_ready(hass, ialarm_api, mock_config_entry): ialarm_api.return_value.get_mac = Mock(side_effect=ConnectionError) mock_config_entry.add_to_hass(hass) - await async_setup_component(hass, DOMAIN, {}) + assert not await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() assert mock_config_entry.state == ENTRY_STATE_SETUP_RETRY @@ -68,16 +59,7 @@ async def test_unload_entry(hass, ialarm_api, mock_config_entry): ialarm_api.return_value.get_mac = Mock(return_value="00:00:54:12:34:56") mock_config_entry.add_to_hass(hass) - await async_setup_component( - hass, - DOMAIN, - { - "ialarm": { - CONF_HOST: "192.168.10.20", - CONF_PORT: 18034, - }, - }, - ) + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() assert mock_config_entry.state == ENTRY_STATE_LOADED From 7256e333e470c6455919bc8acfd6ea71ce1455a6 Mon Sep 17 00:00:00 2001 From: treylok Date: Mon, 12 Apr 2021 16:44:13 -0500 Subject: [PATCH 0222/1317] Add Ecobee humidifier (#45003) --- homeassistant/components/ecobee/const.py | 2 +- homeassistant/components/ecobee/humidifier.py | 123 +++++++++++++++++ tests/components/ecobee/common.py | 27 ++++ tests/components/ecobee/conftest.py | 17 +++ tests/components/ecobee/test_climate.py | 1 + tests/components/ecobee/test_humidifier.py | 130 ++++++++++++++++++ tests/fixtures/ecobee/ecobee-data.json | 43 ++++++ tests/fixtures/ecobee/ecobee-token.json | 7 + 8 files changed, 349 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/ecobee/humidifier.py create mode 100644 tests/components/ecobee/common.py create mode 100644 tests/components/ecobee/conftest.py create mode 100644 tests/components/ecobee/test_humidifier.py create mode 100644 tests/fixtures/ecobee/ecobee-data.json create mode 100644 tests/fixtures/ecobee/ecobee-token.json diff --git a/homeassistant/components/ecobee/const.py b/homeassistant/components/ecobee/const.py index 44abafe83804d..caf25690a9dbf 100644 --- a/homeassistant/components/ecobee/const.py +++ b/homeassistant/components/ecobee/const.py @@ -37,7 +37,7 @@ "vulcanSmart": "ecobee4 Smart", } -PLATFORMS = ["binary_sensor", "climate", "sensor", "weather"] +PLATFORMS = ["binary_sensor", "climate", "humidifier", "sensor", "weather"] MANUFACTURER = "ecobee" diff --git a/homeassistant/components/ecobee/humidifier.py b/homeassistant/components/ecobee/humidifier.py new file mode 100644 index 0000000000000..5067d5080cbab --- /dev/null +++ b/homeassistant/components/ecobee/humidifier.py @@ -0,0 +1,123 @@ +"""Support for using humidifier with ecobee thermostats.""" +from datetime import timedelta + +from homeassistant.components.humidifier import HumidifierEntity +from homeassistant.components.humidifier.const import ( + DEFAULT_MAX_HUMIDITY, + DEFAULT_MIN_HUMIDITY, + DEVICE_CLASS_HUMIDIFIER, + MODE_AUTO, + SUPPORT_MODES, +) + +from .const import DOMAIN + +SCAN_INTERVAL = timedelta(minutes=3) + +MODE_MANUAL = "manual" +MODE_OFF = "off" + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the ecobee thermostat humidifier entity.""" + data = hass.data[DOMAIN] + entities = [] + for index in range(len(data.ecobee.thermostats)): + thermostat = data.ecobee.get_thermostat(index) + if thermostat["settings"]["hasHumidifier"]: + entities.append(EcobeeHumidifier(data, index)) + + async_add_entities(entities, True) + + +class EcobeeHumidifier(HumidifierEntity): + """A humidifier class for an ecobee thermostat with humidifer attached.""" + + def __init__(self, data, thermostat_index): + """Initialize ecobee humidifier platform.""" + self.data = data + self.thermostat_index = thermostat_index + self.thermostat = self.data.ecobee.get_thermostat(self.thermostat_index) + self._name = self.thermostat["name"] + self._last_humidifier_on_mode = MODE_MANUAL + + self.update_without_throttle = False + + async def async_update(self): + """Get the latest state from the thermostat.""" + if self.update_without_throttle: + await self.data.update(no_throttle=True) + self.update_without_throttle = False + else: + await self.data.update() + self.thermostat = self.data.ecobee.get_thermostat(self.thermostat_index) + if self.mode != MODE_OFF: + self._last_humidifier_on_mode = self.mode + + @property + def available_modes(self): + """Return the list of available modes.""" + return [MODE_OFF, MODE_AUTO, MODE_MANUAL] + + @property + def device_class(self): + """Return the device class type.""" + return DEVICE_CLASS_HUMIDIFIER + + @property + def is_on(self): + """Return True if the humidifier is on.""" + return self.mode != MODE_OFF + + @property + def max_humidity(self): + """Return the maximum humidity.""" + return DEFAULT_MAX_HUMIDITY + + @property + def min_humidity(self): + """Return the minimum humidity.""" + return DEFAULT_MIN_HUMIDITY + + @property + def mode(self): + """Return the current mode, e.g., off, auto, manual.""" + return self.thermostat["settings"]["humidifierMode"] + + @property + def name(self): + """Return the name of the ecobee thermostat.""" + return self._name + + @property + def supported_features(self): + """Return the list of supported features.""" + return SUPPORT_MODES + + @property + def target_humidity(self) -> int: + """Return the desired humidity set point.""" + return int(self.thermostat["runtime"]["desiredHumidity"]) + + def set_mode(self, mode): + """Set humidifier mode (auto, off, manual).""" + if mode.lower() not in (self.available_modes): + raise ValueError( + f"Invalid mode value: {mode} Valid values are {', '.join(self.available_modes)}." + ) + + self.data.ecobee.set_humidifier_mode(self.thermostat_index, mode) + self.update_without_throttle = True + + def set_humidity(self, humidity): + """Set the humidity level.""" + self.data.ecobee.set_humidity(self.thermostat_index, humidity) + self.update_without_throttle = True + + def turn_off(self, **kwargs): + """Set humidifier to off mode.""" + self.set_mode(MODE_OFF) + + def turn_on(self, **kwargs): + """Set humidifier to on mode.""" + self.set_mode(self._last_humidifier_on_mode) diff --git a/tests/components/ecobee/common.py b/tests/components/ecobee/common.py new file mode 100644 index 0000000000000..0422b35f787fb --- /dev/null +++ b/tests/components/ecobee/common.py @@ -0,0 +1,27 @@ +"""Common methods used across tests for Ecobee.""" +from unittest.mock import patch + +from homeassistant.components.ecobee.const import CONF_REFRESH_TOKEN, DOMAIN +from homeassistant.const import CONF_API_KEY +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + + +async def setup_platform(hass, platform): + """Set up the ecobee platform.""" + mock_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_API_KEY: "ABC123", + CONF_REFRESH_TOKEN: "EFG456", + }, + ) + mock_entry.add_to_hass(hass) + + with patch("homeassistant.components.ecobee.const.PLATFORMS", [platform]): + assert await async_setup_component(hass, DOMAIN, {}) + + await hass.async_block_till_done() + + return mock_entry diff --git a/tests/components/ecobee/conftest.py b/tests/components/ecobee/conftest.py new file mode 100644 index 0000000000000..a7766af2ff92b --- /dev/null +++ b/tests/components/ecobee/conftest.py @@ -0,0 +1,17 @@ +"""Fixtures for tests.""" +import pytest + +from tests.common import load_fixture + + +@pytest.fixture(autouse=True) +def requests_mock_fixture(requests_mock): + """Fixture to provide a requests mocker.""" + requests_mock.get( + "https://api.ecobee.com/1/thermostat", + text=load_fixture("ecobee/ecobee-data.json"), + ) + requests_mock.post( + "https://api.ecobee.com/token", + text=load_fixture("ecobee/ecobee-token.json"), + ) diff --git a/tests/components/ecobee/test_climate.py b/tests/components/ecobee/test_climate.py index 95b4b290b70a4..270c6cfec1527 100644 --- a/tests/components/ecobee/test_climate.py +++ b/tests/components/ecobee/test_climate.py @@ -164,6 +164,7 @@ async def test_extra_state_attributes(ecobee_fixture, thermostat): "fan_min_on_time": 10, "equipment_running": "auxHeat2", } == thermostat.extra_state_attributes + ecobee_fixture["equipmentStatus"] = "compCool1" assert { "fan": "off", diff --git a/tests/components/ecobee/test_humidifier.py b/tests/components/ecobee/test_humidifier.py new file mode 100644 index 0000000000000..dd58decfb32be --- /dev/null +++ b/tests/components/ecobee/test_humidifier.py @@ -0,0 +1,130 @@ +"""The test for the ecobee thermostat humidifier module.""" +from unittest.mock import patch + +import pytest + +from homeassistant.components.ecobee.humidifier import MODE_MANUAL, MODE_OFF +from homeassistant.components.humidifier import DOMAIN as HUMIDIFIER_DOMAIN +from homeassistant.components.humidifier.const import ( + ATTR_AVAILABLE_MODES, + ATTR_HUMIDITY, + ATTR_MAX_HUMIDITY, + ATTR_MIN_HUMIDITY, + DEFAULT_MAX_HUMIDITY, + DEFAULT_MIN_HUMIDITY, + DEVICE_CLASS_HUMIDIFIER, + MODE_AUTO, + SERVICE_SET_HUMIDITY, + SERVICE_SET_MODE, + SUPPORT_MODES, +) +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, + ATTR_MODE, + ATTR_SUPPORTED_FEATURES, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, +) + +from .common import setup_platform + +DEVICE_ID = "humidifier.ecobee" + + +async def test_attributes(hass): + """Test the humidifier attributes are correct.""" + await setup_platform(hass, HUMIDIFIER_DOMAIN) + + state = hass.states.get(DEVICE_ID) + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_MIN_HUMIDITY) == DEFAULT_MIN_HUMIDITY + assert state.attributes.get(ATTR_MAX_HUMIDITY) == DEFAULT_MAX_HUMIDITY + assert state.attributes.get(ATTR_HUMIDITY) == 40 + assert state.attributes.get(ATTR_AVAILABLE_MODES) == [ + MODE_OFF, + MODE_AUTO, + MODE_MANUAL, + ] + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "ecobee" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_HUMIDIFIER + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == SUPPORT_MODES + + +async def test_turn_on(hass): + """Test the humidifer can be turned on.""" + with patch("pyecobee.Ecobee.set_humidifier_mode") as mock_turn_on: + await setup_platform(hass, HUMIDIFIER_DOMAIN) + + await hass.services.async_call( + HUMIDIFIER_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: DEVICE_ID}, + blocking=True, + ) + await hass.async_block_till_done() + mock_turn_on.assert_called_once_with(0, "manual") + + +async def test_turn_off(hass): + """Test the humidifer can be turned off.""" + with patch("pyecobee.Ecobee.set_humidifier_mode") as mock_turn_off: + await setup_platform(hass, HUMIDIFIER_DOMAIN) + + await hass.services.async_call( + HUMIDIFIER_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: DEVICE_ID}, + blocking=True, + ) + await hass.async_block_till_done() + mock_turn_off.assert_called_once_with(0, STATE_OFF) + + +async def test_set_mode(hass): + """Test the humidifer can change modes.""" + with patch("pyecobee.Ecobee.set_humidifier_mode") as mock_set_mode: + await setup_platform(hass, HUMIDIFIER_DOMAIN) + + await hass.services.async_call( + HUMIDIFIER_DOMAIN, + SERVICE_SET_MODE, + {ATTR_ENTITY_ID: DEVICE_ID, ATTR_MODE: MODE_AUTO}, + blocking=True, + ) + await hass.async_block_till_done() + mock_set_mode.assert_called_once_with(0, MODE_AUTO) + + await hass.services.async_call( + HUMIDIFIER_DOMAIN, + SERVICE_SET_MODE, + {ATTR_ENTITY_ID: DEVICE_ID, ATTR_MODE: MODE_MANUAL}, + blocking=True, + ) + await hass.async_block_till_done() + mock_set_mode.assert_called_with(0, MODE_MANUAL) + + with pytest.raises(ValueError): + await hass.services.async_call( + HUMIDIFIER_DOMAIN, + SERVICE_SET_MODE, + {ATTR_ENTITY_ID: DEVICE_ID, ATTR_MODE: "ModeThatDoesntExist"}, + blocking=True, + ) + + +async def test_set_humidity(hass): + """Test the humidifer can set humidity level.""" + with patch("pyecobee.Ecobee.set_humidity") as mock_set_humidity: + await setup_platform(hass, HUMIDIFIER_DOMAIN) + + await hass.services.async_call( + HUMIDIFIER_DOMAIN, + SERVICE_SET_HUMIDITY, + {ATTR_ENTITY_ID: DEVICE_ID, ATTR_HUMIDITY: 60}, + blocking=True, + ) + await hass.async_block_till_done() + mock_set_humidity.assert_called_once_with(0, 60) diff --git a/tests/fixtures/ecobee/ecobee-data.json b/tests/fixtures/ecobee/ecobee-data.json new file mode 100644 index 0000000000000..2727103c9b147 --- /dev/null +++ b/tests/fixtures/ecobee/ecobee-data.json @@ -0,0 +1,43 @@ +{ + "thermostatList": [ + {"name": "ecobee", + "program": { + "climates": [ + {"name": "Climate1", "climateRef": "c1"}, + {"name": "Climate2", "climateRef": "c2"} + ], + "currentClimateRef": "c1" + }, + "runtime": { + "actualTemperature": 300, + "actualHumidity": 15, + "desiredHeat": 400, + "desiredCool": 200, + "desiredFanMode": "on", + "desiredHumidity": 40 + }, + "settings": { + "hvacMode": "auto", + "heatStages": 1, + "coolStages": 1, + "fanMinOnTime": 10, + "heatCoolMinDelta": 50, + "holdAction": "nextTransition", + "hasHumidifier": true, + "humidifierMode": "off", + "humidity": "30" + }, + "equipmentStatus": "fan", + "events": [ + { + "name": "Event1", + "running": true, + "type": "hold", + "holdClimateRef": "away", + "endDate": "2022-01-01 10:00:00", + "startDate": "2022-02-02 11:00:00" + } + ]} + ] + +} \ No newline at end of file diff --git a/tests/fixtures/ecobee/ecobee-token.json b/tests/fixtures/ecobee/ecobee-token.json new file mode 100644 index 0000000000000..6ee8305a5929b --- /dev/null +++ b/tests/fixtures/ecobee/ecobee-token.json @@ -0,0 +1,7 @@ +{ + "access_token": "Rc7JE8P7XUgSCPogLOx2VLMfITqQQrjg", + "token_type": "Bearer", + "expires_in": 3599, + "refresh_token": "og2Obost3ucRo1ofo0EDoslGltmFMe2g", + "scope": "smartWrite" +} \ No newline at end of file From 88d2fb4aa66c036dbc260e225719b5c0bb517d86 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 13 Apr 2021 00:06:52 +0200 Subject: [PATCH 0223/1317] Bump yeelight version to 0.6.0 (#49111) --- homeassistant/components/yeelight/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 3c708d575602b..25909c74443b9 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -3,7 +3,7 @@ "name": "Yeelight", "documentation": "https://www.home-assistant.io/integrations/yeelight", "requirements": [ - "yeelight==0.5.4" + "yeelight==0.6.0" ], "codeowners": [ "@rytilahti", diff --git a/requirements_all.txt b/requirements_all.txt index 862faace7acd1..ed7bb1c444734 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2363,7 +2363,7 @@ yalesmartalarmclient==0.1.6 yalexs==1.1.10 # homeassistant.components.yeelight -yeelight==0.5.4 +yeelight==0.6.0 # homeassistant.components.yeelightsunflower yeelightsunflower==0.0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fa1494895fd84..7b3805f2a7ab8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1248,7 +1248,7 @@ xmltodict==0.12.0 yalexs==1.1.10 # homeassistant.components.yeelight -yeelight==0.5.4 +yeelight==0.6.0 # homeassistant.components.onvif zeep[async]==4.0.0 From ff8e4fb77ff651e9eeda1a34a9de032c8fb52003 Mon Sep 17 00:00:00 2001 From: Unai Date: Tue, 13 Apr 2021 00:14:29 +0200 Subject: [PATCH 0224/1317] Upgrade maxcube-api to 0.4.2 (#49106) Upgrade to maxcube-api 0.4.2 to fix pending issues in HA 2021.4.x: - Interpret correctly S command error responses (https://github.com/home-assistant/core/issues/49075) - Support application timezone configuration (https://github.com/home-assistant/core/issues/49076) --- homeassistant/components/maxcube/__init__.py | 3 ++- homeassistant/components/maxcube/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/maxcube/conftest.py | 3 ++- 5 files changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/maxcube/__init__.py b/homeassistant/components/maxcube/__init__.py index e38f08809a749..4d610dfc04ffd 100644 --- a/homeassistant/components/maxcube/__init__.py +++ b/homeassistant/components/maxcube/__init__.py @@ -10,6 +10,7 @@ from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SCAN_INTERVAL import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform +from homeassistant.util.dt import now _LOGGER = logging.getLogger(__name__) @@ -59,7 +60,7 @@ def setup(hass, config): scan_interval = gateway[CONF_SCAN_INTERVAL].total_seconds() try: - cube = MaxCube(host, port) + cube = MaxCube(host, port, now=now) hass.data[DATA_KEY][host] = MaxCubeHandle(cube, scan_interval) except timeout as ex: _LOGGER.error("Unable to connect to Max!Cube gateway: %s", str(ex)) diff --git a/homeassistant/components/maxcube/manifest.json b/homeassistant/components/maxcube/manifest.json index ddc21bd2358f0..75b5a5fcb6df4 100644 --- a/homeassistant/components/maxcube/manifest.json +++ b/homeassistant/components/maxcube/manifest.json @@ -2,6 +2,6 @@ "domain": "maxcube", "name": "eQ-3 MAX!", "documentation": "https://www.home-assistant.io/integrations/maxcube", - "requirements": ["maxcube-api==0.4.1"], + "requirements": ["maxcube-api==0.4.2"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index ed7bb1c444734..4de94af64a157 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -918,7 +918,7 @@ magicseaweed==1.0.3 matrix-client==0.3.2 # homeassistant.components.maxcube -maxcube-api==0.4.1 +maxcube-api==0.4.2 # homeassistant.components.mythicbeastsdns mbddns==0.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7b3805f2a7ab8..458a1a4ab5e70 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -490,7 +490,7 @@ logi_circle==0.2.2 luftdaten==0.6.4 # homeassistant.components.maxcube -maxcube-api==0.4.1 +maxcube-api==0.4.2 # homeassistant.components.mythicbeastsdns mbddns==0.1.2 diff --git a/tests/components/maxcube/conftest.py b/tests/components/maxcube/conftest.py index 6b283cf87c025..b36072190c4b7 100644 --- a/tests/components/maxcube/conftest.py +++ b/tests/components/maxcube/conftest.py @@ -10,6 +10,7 @@ from homeassistant.components.maxcube import DOMAIN from homeassistant.setup import async_setup_component +from homeassistant.util.dt import now @pytest.fixture @@ -105,5 +106,5 @@ async def cube(hass, hass_config, room, thermostat, wallthermostat, windowshutte assert await async_setup_component(hass, DOMAIN, hass_config) await hass.async_block_till_done() gateway = hass_config[DOMAIN]["gateways"][0] - mock.assert_called_with(gateway["host"], gateway.get("port", 62910)) + mock.assert_called_with(gateway["host"], gateway.get("port", 62910), now=now) return cube From 5c71ba578db1ba433c3275184016b006ec6cae67 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Tue, 13 Apr 2021 01:52:51 +0300 Subject: [PATCH 0225/1317] Fix Shelly brightness offset (#49007) --- homeassistant/components/shelly/light.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/shelly/light.py b/homeassistant/components/shelly/light.py index a9e137968758a..370522415fd78 100644 --- a/homeassistant/components/shelly/light.py +++ b/homeassistant/components/shelly/light.py @@ -118,15 +118,16 @@ def brightness(self) -> int: """Brightness of light.""" if self.mode == "color": if self.control_result: - brightness = self.control_result["gain"] + brightness_pct = self.control_result["gain"] else: - brightness = self.block.gain + brightness_pct = self.block.gain else: if self.control_result: - brightness = self.control_result["brightness"] + brightness_pct = self.control_result["brightness"] else: - brightness = self.block.brightness - return int(brightness / 100 * 255) + brightness_pct = self.block.brightness + + return round(255 * brightness_pct / 100) @property def white_value(self) -> int: @@ -188,11 +189,11 @@ async def async_turn_on(self, **kwargs) -> None: set_mode = None params = {"turn": "on"} if ATTR_BRIGHTNESS in kwargs: - tmp_brightness = int(kwargs[ATTR_BRIGHTNESS] / 255 * 100) + brightness_pct = int(100 * (kwargs[ATTR_BRIGHTNESS] + 1) / 255) if hasattr(self.block, "gain"): - params["gain"] = tmp_brightness + params["gain"] = brightness_pct if hasattr(self.block, "brightness"): - params["brightness"] = tmp_brightness + params["brightness"] = brightness_pct if ATTR_COLOR_TEMP in kwargs: color_temp = color_temperature_mired_to_kelvin(kwargs[ATTR_COLOR_TEMP]) color_temp = min(self._max_kelvin, max(self._min_kelvin, color_temp)) From 93c68f8be6df24e670bfd90426bf9e5268c0eb91 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Tue, 13 Apr 2021 00:04:04 +0000 Subject: [PATCH 0226/1317] [ci skip] Translation update --- .../advantage_air/translations/zh-Hant.json | 2 +- .../agent_dvr/translations/zh-Hant.json | 2 +- .../airnow/translations/zh-Hant.json | 2 +- .../alarmdecoder/translations/zh-Hant.json | 2 +- .../apple_tv/translations/zh-Hant.json | 4 +- .../arcam_fmj/translations/zh-Hant.json | 2 +- .../components/atag/translations/zh-Hant.json | 2 +- .../components/axis/translations/zh-Hant.json | 4 +- .../blebox/translations/zh-Hant.json | 2 +- .../blink/translations/zh-Hant.json | 2 +- .../components/bond/translations/zh-Hant.json | 2 +- .../braviatv/translations/zh-Hant.json | 2 +- .../broadlink/translations/zh-Hant.json | 2 +- .../brother/translations/zh-Hant.json | 2 +- .../bsblan/translations/zh-Hant.json | 2 +- .../control4/translations/zh-Hant.json | 2 +- .../daikin/translations/zh-Hant.json | 2 +- .../denonavr/translations/zh-Hant.json | 2 +- .../directv/translations/zh-Hant.json | 2 +- .../doorbird/translations/zh-Hant.json | 2 +- .../components/dsmr/translations/zh-Hant.json | 2 +- .../dunehd/translations/zh-Hant.json | 4 +- .../components/eafm/translations/zh-Hant.json | 2 +- .../econet/translations/zh-Hant.json | 2 +- .../elgato/translations/zh-Hant.json | 2 +- .../emonitor/translations/zh-Hant.json | 2 +- .../emulated_roku/translations/zh-Hant.json | 2 +- .../enphase_envoy/translations/zh-Hant.json | 2 +- .../esphome/translations/zh-Hant.json | 2 +- .../components/ezviz/translations/ko.json | 45 ++++++++++++++++ .../components/ezviz/translations/no.json | 52 +++++++++++++++++++ .../components/flo/translations/zh-Hant.json | 2 +- .../forked_daapd/translations/zh-Hant.json | 2 +- .../foscam/translations/zh-Hant.json | 2 +- .../freebox/translations/zh-Hant.json | 2 +- .../fritzbox/translations/zh-Hant.json | 2 +- .../translations/zh-Hant.json | 2 +- .../glances/translations/zh-Hant.json | 2 +- .../guardian/translations/zh-Hant.json | 2 +- .../harmony/translations/zh-Hant.json | 2 +- .../hlk_sw16/translations/zh-Hant.json | 2 +- .../translations/zh-Hant.json | 2 +- .../huawei_lte/translations/zh-Hant.json | 2 +- .../components/hue/translations/zh-Hant.json | 2 +- .../huisbaasje/translations/zh-Hant.json | 2 +- .../translations/zh-Hant.json | 2 +- .../hvv_departures/translations/zh-Hant.json | 2 +- .../components/ialarm/translations/ca.json | 20 +++++++ .../components/ialarm/translations/et.json | 20 +++++++ .../components/ialarm/translations/ko.json | 20 +++++++ .../components/ialarm/translations/nl.json | 20 +++++++ .../components/ialarm/translations/no.json | 20 +++++++ .../components/ialarm/translations/ru.json | 20 +++++++ .../ialarm/translations/zh-Hant.json | 20 +++++++ .../components/ipp/translations/zh-Hant.json | 2 +- .../isy994/translations/zh-Hant.json | 2 +- .../kmtronic/translations/zh-Hant.json | 2 +- .../components/kodi/translations/zh-Hant.json | 2 +- .../konnected/translations/zh-Hant.json | 2 +- .../translations/zh-Hant.json | 2 +- .../litterrobot/translations/ca.json | 2 +- .../litterrobot/translations/et.json | 2 +- .../litterrobot/translations/ru.json | 2 +- .../litterrobot/translations/zh-Hant.json | 2 +- .../lutron_caseta/translations/zh-Hant.json | 2 +- .../mikrotik/translations/zh-Hant.json | 2 +- .../monoprice/translations/zh-Hant.json | 2 +- .../motion_blinds/translations/zh-Hant.json | 2 +- .../mullvad/translations/zh-Hant.json | 2 +- .../mysensors/translations/zh-Hant.json | 4 +- .../neato/translations/zh-Hant.json | 2 +- .../nexia/translations/zh-Hant.json | 2 +- .../nightscout/translations/zh-Hant.json | 2 +- .../nuheat/translations/zh-Hant.json | 2 +- .../components/nut/translations/zh-Hant.json | 2 +- .../onewire/translations/zh-Hant.json | 2 +- .../onvif/translations/zh-Hant.json | 2 +- .../opentherm_gw/translations/zh-Hant.json | 2 +- .../components/ozw/translations/zh-Hant.json | 2 +- .../panasonic_viera/translations/zh-Hant.json | 2 +- .../philips_js/translations/zh-Hant.json | 2 +- .../poolsense/translations/zh-Hant.json | 2 +- .../powerwall/translations/zh-Hant.json | 2 +- .../progettihwsw/translations/zh-Hant.json | 2 +- .../components/ps4/translations/zh-Hant.json | 2 +- .../rachio/translations/zh-Hant.json | 2 +- .../rainmachine/translations/zh-Hant.json | 2 +- .../recollect_waste/translations/zh-Hant.json | 2 +- .../rfxtrx/translations/zh-Hant.json | 2 +- .../components/ring/translations/zh-Hant.json | 2 +- .../risco/translations/zh-Hant.json | 2 +- .../translations/zh-Hant.json | 2 +- .../components/roku/translations/zh-Hant.json | 2 +- .../roomba/translations/zh-Hant.json | 2 +- .../components/roon/translations/zh-Hant.json | 2 +- .../translations/zh-Hant.json | 2 +- .../samsungtv/translations/zh-Hant.json | 2 +- .../screenlogic/translations/zh-Hant.json | 2 +- .../sense/translations/zh-Hant.json | 2 +- .../shelly/translations/zh-Hant.json | 2 +- .../smappee/translations/zh-Hant.json | 2 +- .../translations/zh-Hant.json | 2 +- .../smarttub/translations/zh-Hant.json | 2 +- .../components/sms/translations/zh-Hant.json | 2 +- .../solaredge/translations/zh-Hant.json | 4 +- .../solarlog/translations/zh-Hant.json | 4 +- .../somfy_mylink/translations/zh-Hant.json | 2 +- .../songpal/translations/zh-Hant.json | 2 +- .../squeezebox/translations/zh-Hant.json | 2 +- .../syncthru/translations/zh-Hant.json | 2 +- .../synology_dsm/translations/zh-Hant.json | 2 +- .../components/tado/translations/zh-Hant.json | 2 +- .../tradfri/translations/zh-Hant.json | 2 +- .../transmission/translations/zh-Hant.json | 2 +- .../twinkly/translations/zh-Hant.json | 2 +- .../components/upb/translations/zh-Hant.json | 2 +- .../components/upnp/translations/zh-Hant.json | 2 +- .../velbus/translations/zh-Hant.json | 4 +- .../vilfo/translations/zh-Hant.json | 2 +- .../vizio/translations/zh-Hant.json | 2 +- .../volumio/translations/zh-Hant.json | 2 +- .../wilight/translations/zh-Hant.json | 2 +- .../components/wled/translations/zh-Hant.json | 2 +- .../wolflink/translations/zh-Hant.json | 2 +- .../xiaomi_aqara/translations/zh-Hant.json | 2 +- .../xiaomi_miio/translations/zh-Hant.json | 2 +- .../yeelight/translations/zh-Hant.json | 2 +- .../zwave/translations/zh-Hant.json | 2 +- .../zwave_js/translations/zh-Hant.json | 2 +- 129 files changed, 364 insertions(+), 127 deletions(-) create mode 100644 homeassistant/components/ezviz/translations/ko.json create mode 100644 homeassistant/components/ezviz/translations/no.json create mode 100644 homeassistant/components/ialarm/translations/ca.json create mode 100644 homeassistant/components/ialarm/translations/et.json create mode 100644 homeassistant/components/ialarm/translations/ko.json create mode 100644 homeassistant/components/ialarm/translations/nl.json create mode 100644 homeassistant/components/ialarm/translations/no.json create mode 100644 homeassistant/components/ialarm/translations/ru.json create mode 100644 homeassistant/components/ialarm/translations/zh-Hant.json diff --git a/homeassistant/components/advantage_air/translations/zh-Hant.json b/homeassistant/components/advantage_air/translations/zh-Hant.json index 9d1cd4210f4aa..a6d7280b06919 100644 --- a/homeassistant/components/advantage_air/translations/zh-Hant.json +++ b/homeassistant/components/advantage_air/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" diff --git a/homeassistant/components/agent_dvr/translations/zh-Hant.json b/homeassistant/components/agent_dvr/translations/zh-Hant.json index aa0ac965a844d..9f5e123008a8c 100644 --- a/homeassistant/components/agent_dvr/translations/zh-Hant.json +++ b/homeassistant/components/agent_dvr/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", diff --git a/homeassistant/components/airnow/translations/zh-Hant.json b/homeassistant/components/airnow/translations/zh-Hant.json index 0f6008e75a600..0cdb4a11bedc6 100644 --- a/homeassistant/components/airnow/translations/zh-Hant.json +++ b/homeassistant/components/airnow/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/alarmdecoder/translations/zh-Hant.json b/homeassistant/components/alarmdecoder/translations/zh-Hant.json index a43e80d36295d..d1a96eedd15c9 100644 --- a/homeassistant/components/alarmdecoder/translations/zh-Hant.json +++ b/homeassistant/components/alarmdecoder/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "create_entry": { "default": "\u6210\u529f\u9023\u7dda\u81f3 AlarmDecoder\u3002" diff --git a/homeassistant/components/apple_tv/translations/zh-Hant.json b/homeassistant/components/apple_tv/translations/zh-Hant.json index 269e207e8a419..ea6cbf7d3d4fe 100644 --- a/homeassistant/components/apple_tv/translations/zh-Hant.json +++ b/homeassistant/components/apple_tv/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured_device": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured_device": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "backoff": "\u88dd\u7f6e\u4e0d\u63a5\u53d7\u6b64\u6b21\u914d\u5c0d\u8acb\u6c42\uff08\u53ef\u80fd\u8f38\u5165\u592a\u591a\u6b21\u7121\u6548\u7684 PIN \u78bc\uff09\uff0c\u8acb\u7a0d\u5f8c\u518d\u8a66\u3002", "device_did_not_pair": "\u88dd\u7f6e\u6c92\u6709\u5617\u8a66\u914d\u5c0d\u5b8c\u6210\u904e\u7a0b\u3002", @@ -10,7 +10,7 @@ "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "error": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", "no_usable_service": "\u627e\u5230\u7684\u88dd\u7f6e\u7121\u6cd5\u8b58\u5225\u4ee5\u9032\u884c\u9023\u7dda\u3002\u5047\u5982\u6b64\u8a0a\u606f\u91cd\u8907\u767c\u751f\u3002\u8acb\u8a66\u8457\u6307\u5b9a\u7279\u5b9a IP \u4f4d\u5740\u6216\u91cd\u555f Apple TV\u3002", diff --git a/homeassistant/components/arcam_fmj/translations/zh-Hant.json b/homeassistant/components/arcam_fmj/translations/zh-Hant.json index 853b498a51ecd..4c7455f844404 100644 --- a/homeassistant/components/arcam_fmj/translations/zh-Hant.json +++ b/homeassistant/components/arcam_fmj/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "cannot_connect": "\u9023\u7dda\u5931\u6557" }, diff --git a/homeassistant/components/atag/translations/zh-Hant.json b/homeassistant/components/atag/translations/zh-Hant.json index b616437aa2110..8eb427b95eec7 100644 --- a/homeassistant/components/atag/translations/zh-Hant.json +++ b/homeassistant/components/atag/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/axis/translations/zh-Hant.json b/homeassistant/components/axis/translations/zh-Hant.json index 293f08c5f0596..892cb8fb6df25 100644 --- a/homeassistant/components/axis/translations/zh-Hant.json +++ b/homeassistant/components/axis/translations/zh-Hant.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "link_local_address": "\u4e0d\u652f\u63f4\u9023\u7d50\u672c\u5730\u7aef\u4f4d\u5740", "not_axis_device": "\u6240\u767c\u73fe\u7684\u88dd\u7f6e\u4e26\u975e Axis \u88dd\u7f6e" }, "error": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" diff --git a/homeassistant/components/blebox/translations/zh-Hant.json b/homeassistant/components/blebox/translations/zh-Hant.json index b84105745ac8e..a763442db7de1 100644 --- a/homeassistant/components/blebox/translations/zh-Hant.json +++ b/homeassistant/components/blebox/translations/zh-Hant.json @@ -2,7 +2,7 @@ "config": { "abort": { "address_already_configured": "\u4f4d\u65bc {address} \u7684 BleBox \u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002", - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/blink/translations/zh-Hant.json b/homeassistant/components/blink/translations/zh-Hant.json index 6874efb6e316c..4596b55df9d0f 100644 --- a/homeassistant/components/blink/translations/zh-Hant.json +++ b/homeassistant/components/blink/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/bond/translations/zh-Hant.json b/homeassistant/components/bond/translations/zh-Hant.json index 8bb8e178869aa..de54be7fff3cb 100644 --- a/homeassistant/components/bond/translations/zh-Hant.json +++ b/homeassistant/components/bond/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/braviatv/translations/zh-Hant.json b/homeassistant/components/braviatv/translations/zh-Hant.json index 53dc9ead653aa..f736b601a7488 100644 --- a/homeassistant/components/braviatv/translations/zh-Hant.json +++ b/homeassistant/components/braviatv/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "no_ip_control": "\u96fb\u8996\u4e0a\u7684 IP \u5df2\u95dc\u9589\u6216\u4e0d\u652f\u63f4\u6b64\u6b3e\u96fb\u8996\u3002" }, "error": { diff --git a/homeassistant/components/broadlink/translations/zh-Hant.json b/homeassistant/components/broadlink/translations/zh-Hant.json index 2e0864c9f7210..01f093a0bd655 100644 --- a/homeassistant/components/broadlink/translations/zh-Hant.json +++ b/homeassistant/components/broadlink/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_host": "\u7121\u6548\u4e3b\u6a5f\u540d\u7a31\u6216 IP \u4f4d\u5740", diff --git a/homeassistant/components/brother/translations/zh-Hant.json b/homeassistant/components/brother/translations/zh-Hant.json index d8208e6ce4eec..80555f52e8d4e 100644 --- a/homeassistant/components/brother/translations/zh-Hant.json +++ b/homeassistant/components/brother/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "unsupported_model": "\u4e0d\u652f\u63f4\u6b64\u6b3e\u5370\u8868\u6a5f\u3002" }, "error": { diff --git a/homeassistant/components/bsblan/translations/zh-Hant.json b/homeassistant/components/bsblan/translations/zh-Hant.json index 3fefe08f98b07..ebe0ca6237081 100644 --- a/homeassistant/components/bsblan/translations/zh-Hant.json +++ b/homeassistant/components/bsblan/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" diff --git a/homeassistant/components/control4/translations/zh-Hant.json b/homeassistant/components/control4/translations/zh-Hant.json index bc955f119e962..b150264c4ae99 100644 --- a/homeassistant/components/control4/translations/zh-Hant.json +++ b/homeassistant/components/control4/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/daikin/translations/zh-Hant.json b/homeassistant/components/daikin/translations/zh-Hant.json index b1a19792a08a1..a6d4b4598b12b 100644 --- a/homeassistant/components/daikin/translations/zh-Hant.json +++ b/homeassistant/components/daikin/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557" }, "error": { diff --git a/homeassistant/components/denonavr/translations/zh-Hant.json b/homeassistant/components/denonavr/translations/zh-Hant.json index 1aaa5b040723c..053dd143e1620 100644 --- a/homeassistant/components/denonavr/translations/zh-Hant.json +++ b/homeassistant/components/denonavr/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002\u95dc\u9589\u4e3b\u96fb\u6e90\u3001\u5c07\u4e59\u592a\u7db2\u8def\u65b7\u7dda\u5f8c\u91cd\u65b0\u9023\u7dda\uff0c\u53ef\u80fd\u6703\u6709\u6240\u5e6b\u52a9", "not_denonavr_manufacturer": "\u4e26\u975e Denon AVR \u7db2\u8def\u63a5\u6536\u5668\uff0c\u6240\u63a2\u7d22\u4e4b\u88fd\u9020\u5ee0\u5546\u4e0d\u7b26\u5408", diff --git a/homeassistant/components/directv/translations/zh-Hant.json b/homeassistant/components/directv/translations/zh-Hant.json index e19ff18b36403..d38bbb90528c0 100644 --- a/homeassistant/components/directv/translations/zh-Hant.json +++ b/homeassistant/components/directv/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "error": { diff --git a/homeassistant/components/doorbird/translations/zh-Hant.json b/homeassistant/components/doorbird/translations/zh-Hant.json index bb1d109bb8086..b475a474ed932 100644 --- a/homeassistant/components/doorbird/translations/zh-Hant.json +++ b/homeassistant/components/doorbird/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "link_local_address": "\u4e0d\u652f\u63f4\u9023\u7d50\u672c\u5730\u7aef\u4f4d\u5740", "not_doorbird_device": "\u6b64\u88dd\u7f6e\u4e26\u975e DoorBird" }, diff --git a/homeassistant/components/dsmr/translations/zh-Hant.json b/homeassistant/components/dsmr/translations/zh-Hant.json index cbbc3dc8f538a..52e77cd352008 100644 --- a/homeassistant/components/dsmr/translations/zh-Hant.json +++ b/homeassistant/components/dsmr/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" } }, "options": { diff --git a/homeassistant/components/dunehd/translations/zh-Hant.json b/homeassistant/components/dunehd/translations/zh-Hant.json index ce7a120122323..a81055b957606 100644 --- a/homeassistant/components/dunehd/translations/zh-Hant.json +++ b/homeassistant/components/dunehd/translations/zh-Hant.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_host": "\u7121\u6548\u4e3b\u6a5f\u540d\u7a31\u6216 IP \u4f4d\u5740" }, diff --git a/homeassistant/components/eafm/translations/zh-Hant.json b/homeassistant/components/eafm/translations/zh-Hant.json index 73083d2b7359a..3718d2203a9d2 100644 --- a/homeassistant/components/eafm/translations/zh-Hant.json +++ b/homeassistant/components/eafm/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "no_stations": "\u627e\u4e0d\u5230\u7b26\u5408\u7684\u76e3\u63a7\u7ad9\u3002" }, "step": { diff --git a/homeassistant/components/econet/translations/zh-Hant.json b/homeassistant/components/econet/translations/zh-Hant.json index 50824c198145a..cb328b9c0e50d 100644 --- a/homeassistant/components/econet/translations/zh-Hant.json +++ b/homeassistant/components/econet/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" }, diff --git a/homeassistant/components/elgato/translations/zh-Hant.json b/homeassistant/components/elgato/translations/zh-Hant.json index 8f301b73b3e01..6f113fed4a524 100644 --- a/homeassistant/components/elgato/translations/zh-Hant.json +++ b/homeassistant/components/elgato/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557" }, "error": { diff --git a/homeassistant/components/emonitor/translations/zh-Hant.json b/homeassistant/components/emonitor/translations/zh-Hant.json index 371cf7575423f..1a7dc36fc5af6 100644 --- a/homeassistant/components/emonitor/translations/zh-Hant.json +++ b/homeassistant/components/emonitor/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/emulated_roku/translations/zh-Hant.json b/homeassistant/components/emulated_roku/translations/zh-Hant.json index ee877f78967ed..eaea59f072e6a 100644 --- a/homeassistant/components/emulated_roku/translations/zh-Hant.json +++ b/homeassistant/components/emulated_roku/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "step": { "user": { diff --git a/homeassistant/components/enphase_envoy/translations/zh-Hant.json b/homeassistant/components/enphase_envoy/translations/zh-Hant.json index bf901948b244b..c6ae58a74c0ad 100644 --- a/homeassistant/components/enphase_envoy/translations/zh-Hant.json +++ b/homeassistant/components/enphase_envoy/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/esphome/translations/zh-Hant.json b/homeassistant/components/esphome/translations/zh-Hant.json index 4e719a7957f9e..6e9e43eae026a 100644 --- a/homeassistant/components/esphome/translations/zh-Hant.json +++ b/homeassistant/components/esphome/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d" }, "error": { diff --git a/homeassistant/components/ezviz/translations/ko.json b/homeassistant/components/ezviz/translations/ko.json new file mode 100644 index 0000000000000..8ed11a874b0d4 --- /dev/null +++ b/homeassistant/components/ezviz/translations/ko.json @@ -0,0 +1,45 @@ +{ + "config": { + "abort": { + "already_configured_account": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "invalid_host": "\ud638\uc2a4\ud2b8 \uc774\ub984 \ub610\ub294 IP \uc8fc\uc18c\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "flow_title": "{serial}", + "step": { + "confirm": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + } + }, + "user": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "url": "URL \uc8fc\uc18c", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + } + }, + "user_custom_url": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "url": "URL \uc8fc\uc18c", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "\uc694\uccad \uc81c\ud55c \uc2dc\uac04 (\ucd08)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ezviz/translations/no.json b/homeassistant/components/ezviz/translations/no.json new file mode 100644 index 0000000000000..306babef86c9b --- /dev/null +++ b/homeassistant/components/ezviz/translations/no.json @@ -0,0 +1,52 @@ +{ + "config": { + "abort": { + "already_configured_account": "Kontoen er allerede konfigurert", + "ezviz_cloud_account_missing": "Ezviz sky-konto mangler. Vennligst konfigurer Ezviz sky-konto p\u00e5 nytt", + "unknown": "Uventet feil" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "invalid_host": "Ugyldig vertsnavn eller IP-adresse" + }, + "flow_title": "{serial}", + "step": { + "confirm": { + "data": { + "password": "Passord", + "username": "Brukernavn" + }, + "description": "Angi RTSP-legitimasjon for Ezviz-kameraet {serial} med IP {ip_address}", + "title": "Oppdaget Ezviz Kamera" + }, + "user": { + "data": { + "password": "Passord", + "url": "URL", + "username": "Brukernavn" + }, + "title": "Koble til Ezviz Cloud" + }, + "user_custom_url": { + "data": { + "password": "Passord", + "url": "URL", + "username": "Brukernavn" + }, + "description": "Angi url-adressen for omr\u00e5det manuelt", + "title": "Koble til tilpasset Ezviz URL" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ffmpeg_arguments": "Argumenter sendt til ffmpeg for kameraer", + "timeout": "Be om tidsavbrudd (sekunder)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flo/translations/zh-Hant.json b/homeassistant/components/flo/translations/zh-Hant.json index cad7d736a9d26..011a2f61c1e69 100644 --- a/homeassistant/components/flo/translations/zh-Hant.json +++ b/homeassistant/components/flo/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/forked_daapd/translations/zh-Hant.json b/homeassistant/components/forked_daapd/translations/zh-Hant.json index 0ac0bac013b33..17839b6074899 100644 --- a/homeassistant/components/forked_daapd/translations/zh-Hant.json +++ b/homeassistant/components/forked_daapd/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "not_forked_daapd": "\u88dd\u7f6e\u4e26\u975e forked-daapd \u4f3a\u670d\u5668\u3002" }, "error": { diff --git a/homeassistant/components/foscam/translations/zh-Hant.json b/homeassistant/components/foscam/translations/zh-Hant.json index a0920c935487e..d10746842a869 100644 --- a/homeassistant/components/foscam/translations/zh-Hant.json +++ b/homeassistant/components/foscam/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/freebox/translations/zh-Hant.json b/homeassistant/components/freebox/translations/zh-Hant.json index 734498585f303..6cf0a90f4c08f 100644 --- a/homeassistant/components/freebox/translations/zh-Hant.json +++ b/homeassistant/components/freebox/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/fritzbox/translations/zh-Hant.json b/homeassistant/components/fritzbox/translations/zh-Hant.json index 71a7478526768..9c901bd92e0dd 100644 --- a/homeassistant/components/fritzbox/translations/zh-Hant.json +++ b/homeassistant/components/fritzbox/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", "not_supported": "\u5df2\u9023\u7dda\u81f3 AVM FRITZ!Box \u4f46\u7121\u6cd5\u63a7\u5236\u667a\u80fd\u5bb6\u5ead\u88dd\u7f6e\u3002", diff --git a/homeassistant/components/fritzbox_callmonitor/translations/zh-Hant.json b/homeassistant/components/fritzbox_callmonitor/translations/zh-Hant.json index d159f5df0f970..3e7da079b18fb 100644 --- a/homeassistant/components/fritzbox_callmonitor/translations/zh-Hant.json +++ b/homeassistant/components/fritzbox_callmonitor/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "insufficient_permissions": "\u4f7f\u7528\u8005\u6c92\u6709\u8db3\u5920\u6b0a\u9650\u4ee5\u5b58\u53d6 AVM FRITZ!Box \u8a2d\u5b9a\u53ca\u96fb\u8a71\u7c3f\u3002", "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e" }, diff --git a/homeassistant/components/glances/translations/zh-Hant.json b/homeassistant/components/glances/translations/zh-Hant.json index d81ca02f6ba87..3b0ddcd947a5e 100644 --- a/homeassistant/components/glances/translations/zh-Hant.json +++ b/homeassistant/components/glances/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/guardian/translations/zh-Hant.json b/homeassistant/components/guardian/translations/zh-Hant.json index bf3a1606e6e7b..e2a8c03dbbf41 100644 --- a/homeassistant/components/guardian/translations/zh-Hant.json +++ b/homeassistant/components/guardian/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "cannot_connect": "\u9023\u7dda\u5931\u6557" }, diff --git a/homeassistant/components/harmony/translations/zh-Hant.json b/homeassistant/components/harmony/translations/zh-Hant.json index 608a2150c615e..cf835421fc1e3 100644 --- a/homeassistant/components/harmony/translations/zh-Hant.json +++ b/homeassistant/components/harmony/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/hlk_sw16/translations/zh-Hant.json b/homeassistant/components/hlk_sw16/translations/zh-Hant.json index cad7d736a9d26..011a2f61c1e69 100644 --- a/homeassistant/components/hlk_sw16/translations/zh-Hant.json +++ b/homeassistant/components/hlk_sw16/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/homematicip_cloud/translations/zh-Hant.json b/homeassistant/components/homematicip_cloud/translations/zh-Hant.json index 066ce89c2b2f0..72136ef4e5387 100644 --- a/homeassistant/components/homematicip_cloud/translations/zh-Hant.json +++ b/homeassistant/components/homematicip_cloud/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "connection_aborted": "\u9023\u7dda\u5931\u6557", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, diff --git a/homeassistant/components/huawei_lte/translations/zh-Hant.json b/homeassistant/components/huawei_lte/translations/zh-Hant.json index c8b067c887cd9..48b568b43d663 100644 --- a/homeassistant/components/huawei_lte/translations/zh-Hant.json +++ b/homeassistant/components/huawei_lte/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "not_huawei_lte": "\u4e26\u975e\u83ef\u70ba LTE \u88dd\u7f6e" }, diff --git a/homeassistant/components/hue/translations/zh-Hant.json b/homeassistant/components/hue/translations/zh-Hant.json index ffb2b3a0e50b3..f1b8a70f070ef 100644 --- a/homeassistant/components/hue/translations/zh-Hant.json +++ b/homeassistant/components/hue/translations/zh-Hant.json @@ -2,7 +2,7 @@ "config": { "abort": { "all_configured": "\u6240\u6709 Philips Hue Bridge \u7686\u5df2\u8a2d\u5b9a\u5b8c\u6210", - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "cannot_connect": "\u9023\u7dda\u5931\u6557", "discover_timeout": "\u7121\u6cd5\u641c\u5c0b\u5230 Hue Bridge", diff --git a/homeassistant/components/huisbaasje/translations/zh-Hant.json b/homeassistant/components/huisbaasje/translations/zh-Hant.json index b1e9558637643..cb71ec3006091 100644 --- a/homeassistant/components/huisbaasje/translations/zh-Hant.json +++ b/homeassistant/components/huisbaasje/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/hunterdouglas_powerview/translations/zh-Hant.json b/homeassistant/components/hunterdouglas_powerview/translations/zh-Hant.json index e78e05855c974..1e02677fa4482 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/zh-Hant.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/hvv_departures/translations/zh-Hant.json b/homeassistant/components/hvv_departures/translations/zh-Hant.json index df1eb910d230b..613fcc2f9e574 100644 --- a/homeassistant/components/hvv_departures/translations/zh-Hant.json +++ b/homeassistant/components/hvv_departures/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/ialarm/translations/ca.json b/homeassistant/components/ialarm/translations/ca.json new file mode 100644 index 0000000000000..371c251850349 --- /dev/null +++ b/homeassistant/components/ialarm/translations/ca.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "pin": "Codi PIN", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ialarm/translations/et.json b/homeassistant/components/ialarm/translations/et.json new file mode 100644 index 0000000000000..d77ca5140b686 --- /dev/null +++ b/homeassistant/components/ialarm/translations/et.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "host": "Host", + "pin": "PIN kood", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ialarm/translations/ko.json b/homeassistant/components/ialarm/translations/ko.json new file mode 100644 index 0000000000000..7eb20913d2d97 --- /dev/null +++ b/homeassistant/components/ialarm/translations/ko.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "pin": "PIN \ucf54\ub4dc", + "port": "\ud3ec\ud2b8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ialarm/translations/nl.json b/homeassistant/components/ialarm/translations/nl.json new file mode 100644 index 0000000000000..6ae046200c589 --- /dev/null +++ b/homeassistant/components/ialarm/translations/nl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "host": "Host", + "pin": "PIN-code", + "port": "Poort" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ialarm/translations/no.json b/homeassistant/components/ialarm/translations/no.json new file mode 100644 index 0000000000000..016ba859abd0c --- /dev/null +++ b/homeassistant/components/ialarm/translations/no.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "host": "Vert", + "pin": "PIN kode", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ialarm/translations/ru.json b/homeassistant/components/ialarm/translations/ru.json new file mode 100644 index 0000000000000..03f43f1b62ff0 --- /dev/null +++ b/homeassistant/components/ialarm/translations/ru.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "pin": "PIN-\u043a\u043e\u0434", + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ialarm/translations/zh-Hant.json b/homeassistant/components/ialarm/translations/zh-Hant.json new file mode 100644 index 0000000000000..ef436312d7e7e --- /dev/null +++ b/homeassistant/components/ialarm/translations/zh-Hant.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "pin": "PIN \u78bc", + "port": "\u901a\u8a0a\u57e0" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/zh-Hant.json b/homeassistant/components/ipp/translations/zh-Hant.json index f5d4446def55c..7a0abd19d98cc 100644 --- a/homeassistant/components/ipp/translations/zh-Hant.json +++ b/homeassistant/components/ipp/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557", "connection_upgrade": "\u7531\u65bc\u9700\u8981\u5148\u5347\u7d1a\u9023\u7dda\u3001\u9023\u7dda\u81f3\u5370\u8868\u6a5f\u5931\u6557\u3002", "ipp_error": "\u767c\u751f IPP \u932f\u8aa4\u3002", diff --git a/homeassistant/components/isy994/translations/zh-Hant.json b/homeassistant/components/isy994/translations/zh-Hant.json index 9ab55c19a7819..0fbaefb498c57 100644 --- a/homeassistant/components/isy994/translations/zh-Hant.json +++ b/homeassistant/components/isy994/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/kmtronic/translations/zh-Hant.json b/homeassistant/components/kmtronic/translations/zh-Hant.json index 5027bc2f5b261..e697c5e6dddd6 100644 --- a/homeassistant/components/kmtronic/translations/zh-Hant.json +++ b/homeassistant/components/kmtronic/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/kodi/translations/zh-Hant.json b/homeassistant/components/kodi/translations/zh-Hant.json index 11d962f9d15fd..735df8510603f 100644 --- a/homeassistant/components/kodi/translations/zh-Hant.json +++ b/homeassistant/components/kodi/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "no_uuid": "Kodi \u5be6\u4f8b\u6c92\u6709\u552f\u4e00 ID\u3002\u901a\u5e38\u662f\u56e0\u70ba Kodi \u7248\u672c\u904e\u820a\uff08\u4f4e\u65bc 17.x\uff09\u3002\u53ef\u4ee5\u624b\u52d5\u8a2d\u5b9a\u6574\u5408\u6216\u66f4\u65b0\u81f3\u6700\u65b0\u7248\u672c Kodi\u3002", diff --git a/homeassistant/components/konnected/translations/zh-Hant.json b/homeassistant/components/konnected/translations/zh-Hant.json index 604dc28b5716c..ad85d9c306002 100644 --- a/homeassistant/components/konnected/translations/zh-Hant.json +++ b/homeassistant/components/konnected/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "not_konn_panel": "\u4e26\u975e\u53ef\u8b58\u5225 Konnected.io \u88dd\u7f6e", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" diff --git a/homeassistant/components/kostal_plenticore/translations/zh-Hant.json b/homeassistant/components/kostal_plenticore/translations/zh-Hant.json index b1fef7a714316..f115cf74c8990 100644 --- a/homeassistant/components/kostal_plenticore/translations/zh-Hant.json +++ b/homeassistant/components/kostal_plenticore/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/litterrobot/translations/ca.json b/homeassistant/components/litterrobot/translations/ca.json index 9677f944330e8..b7ca6053fbcb9 100644 --- a/homeassistant/components/litterrobot/translations/ca.json +++ b/homeassistant/components/litterrobot/translations/ca.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "El dispositiu ja est\u00e0 configurat" + "already_configured": "El compte ja ha estat configurat" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", diff --git a/homeassistant/components/litterrobot/translations/et.json b/homeassistant/components/litterrobot/translations/et.json index ce02ca14929ad..c3881a2033729 100644 --- a/homeassistant/components/litterrobot/translations/et.json +++ b/homeassistant/components/litterrobot/translations/et.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + "already_configured": "Kasutaja on juba seadistatud" }, "error": { "cannot_connect": "\u00dchendamine nurjus", diff --git a/homeassistant/components/litterrobot/translations/ru.json b/homeassistant/components/litterrobot/translations/ru.json index aef0fdff54e60..c31f79d1d046f 100644 --- a/homeassistant/components/litterrobot/translations/ru.json +++ b/homeassistant/components/litterrobot/translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", diff --git a/homeassistant/components/litterrobot/translations/zh-Hant.json b/homeassistant/components/litterrobot/translations/zh-Hant.json index d232b491b68e5..b07b7115b074f 100644 --- a/homeassistant/components/litterrobot/translations/zh-Hant.json +++ b/homeassistant/components/litterrobot/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/lutron_caseta/translations/zh-Hant.json b/homeassistant/components/lutron_caseta/translations/zh-Hant.json index 50762fafac183..9e388e52288c9 100644 --- a/homeassistant/components/lutron_caseta/translations/zh-Hant.json +++ b/homeassistant/components/lutron_caseta/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557", "not_lutron_device": "\u6240\u767c\u73fe\u7684\u88dd\u7f6e\u4e26\u975e Lutron \u88dd\u7f6e" }, diff --git a/homeassistant/components/mikrotik/translations/zh-Hant.json b/homeassistant/components/mikrotik/translations/zh-Hant.json index 6c3049eff01ac..3872814e417bb 100644 --- a/homeassistant/components/mikrotik/translations/zh-Hant.json +++ b/homeassistant/components/mikrotik/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/monoprice/translations/zh-Hant.json b/homeassistant/components/monoprice/translations/zh-Hant.json index b54a678398036..75ed7f1563395 100644 --- a/homeassistant/components/monoprice/translations/zh-Hant.json +++ b/homeassistant/components/monoprice/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/motion_blinds/translations/zh-Hant.json b/homeassistant/components/motion_blinds/translations/zh-Hant.json index 0f2f9881ebd09..1c538d7de146e 100644 --- a/homeassistant/components/motion_blinds/translations/zh-Hant.json +++ b/homeassistant/components/motion_blinds/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "connection_error": "\u9023\u7dda\u5931\u6557" }, diff --git a/homeassistant/components/mullvad/translations/zh-Hant.json b/homeassistant/components/mullvad/translations/zh-Hant.json index d78c36b72d77c..9a72286991cf7 100644 --- a/homeassistant/components/mullvad/translations/zh-Hant.json +++ b/homeassistant/components/mullvad/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/mysensors/translations/zh-Hant.json b/homeassistant/components/mysensors/translations/zh-Hant.json index d0067c2d0ced0..f70fc897b2283 100644 --- a/homeassistant/components/mysensors/translations/zh-Hant.json +++ b/homeassistant/components/mysensors/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557", "duplicate_persistence_file": "Persistence \u6a94\u6848\u5df2\u4f7f\u7528\u4e2d", "duplicate_topic": "\u4e3b\u984c\u5df2\u4f7f\u7528\u4e2d", @@ -20,7 +20,7 @@ "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "error": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557", "duplicate_persistence_file": "Persistence \u6a94\u6848\u5df2\u4f7f\u7528\u4e2d", "duplicate_topic": "\u4e3b\u984c\u5df2\u4f7f\u7528\u4e2d", diff --git a/homeassistant/components/neato/translations/zh-Hant.json b/homeassistant/components/neato/translations/zh-Hant.json index beddee423a461..35e90146b3685 100644 --- a/homeassistant/components/neato/translations/zh-Hant.json +++ b/homeassistant/components/neato/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", diff --git a/homeassistant/components/nexia/translations/zh-Hant.json b/homeassistant/components/nexia/translations/zh-Hant.json index 0dc0931afe520..0e5f79ddc90df 100644 --- a/homeassistant/components/nexia/translations/zh-Hant.json +++ b/homeassistant/components/nexia/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/nightscout/translations/zh-Hant.json b/homeassistant/components/nightscout/translations/zh-Hant.json index 7b480bcc0f7af..83b7066b23c3e 100644 --- a/homeassistant/components/nightscout/translations/zh-Hant.json +++ b/homeassistant/components/nightscout/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/nuheat/translations/zh-Hant.json b/homeassistant/components/nuheat/translations/zh-Hant.json index d04a5b165b1ce..7987032ee8f11 100644 --- a/homeassistant/components/nuheat/translations/zh-Hant.json +++ b/homeassistant/components/nuheat/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/nut/translations/zh-Hant.json b/homeassistant/components/nut/translations/zh-Hant.json index 822d2e785f22c..5f48541792d88 100644 --- a/homeassistant/components/nut/translations/zh-Hant.json +++ b/homeassistant/components/nut/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/onewire/translations/zh-Hant.json b/homeassistant/components/onewire/translations/zh-Hant.json index 9c606534a5b09..f9ee1b5e2c216 100644 --- a/homeassistant/components/onewire/translations/zh-Hant.json +++ b/homeassistant/components/onewire/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/onvif/translations/zh-Hant.json b/homeassistant/components/onvif/translations/zh-Hant.json index b21982fede835..9450b3e95699c 100644 --- a/homeassistant/components/onvif/translations/zh-Hant.json +++ b/homeassistant/components/onvif/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "no_h264": "\u8a72\u88dd\u7f6e\u4e0d\u652f\u63f4 H264 \u4e32\u6d41\uff0c\u8acb\u6aa2\u67e5\u88dd\u7f6e\u8a2d\u5b9a\u3002", "no_mac": "\u7121\u6cd5\u70ba ONVIF \u88dd\u7f6e\u8a2d\u5b9a\u552f\u4e00 ID\u3002", diff --git a/homeassistant/components/opentherm_gw/translations/zh-Hant.json b/homeassistant/components/opentherm_gw/translations/zh-Hant.json index 8273eb1de983c..fff046ef24467 100644 --- a/homeassistant/components/opentherm_gw/translations/zh-Hant.json +++ b/homeassistant/components/opentherm_gw/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "error": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557", "id_exists": "\u9598\u9053\u5668 ID \u5df2\u5b58\u5728" }, diff --git a/homeassistant/components/ozw/translations/zh-Hant.json b/homeassistant/components/ozw/translations/zh-Hant.json index 37ab2ea9c9e5f..9651b75386dff 100644 --- a/homeassistant/components/ozw/translations/zh-Hant.json +++ b/homeassistant/components/ozw/translations/zh-Hant.json @@ -4,7 +4,7 @@ "addon_info_failed": "\u53d6\u5f97 OpenZWave \u9644\u52a0\u5143\u4ef6\u8cc7\u8a0a\u5931\u6557\u3002", "addon_install_failed": "OpenZWave \u9644\u52a0\u5143\u4ef6\u5b89\u88dd\u5931\u6557\u3002", "addon_set_config_failed": "OpenZWave a\u9644\u52a0\u5143\u4ef6\u8a2d\u5b9a\u5931\u6557\u3002", - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "mqtt_required": "MQTT \u6574\u5408\u5c1a\u672a\u8a2d\u5b9a", "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" diff --git a/homeassistant/components/panasonic_viera/translations/zh-Hant.json b/homeassistant/components/panasonic_viera/translations/zh-Hant.json index 1b39556f45124..5b3e5ada9720f 100644 --- a/homeassistant/components/panasonic_viera/translations/zh-Hant.json +++ b/homeassistant/components/panasonic_viera/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, diff --git a/homeassistant/components/philips_js/translations/zh-Hant.json b/homeassistant/components/philips_js/translations/zh-Hant.json index 7ae9c8893d557..de7f02b7a2137 100644 --- a/homeassistant/components/philips_js/translations/zh-Hant.json +++ b/homeassistant/components/philips_js/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/poolsense/translations/zh-Hant.json b/homeassistant/components/poolsense/translations/zh-Hant.json index 93a99ba1d317c..62ffca35e9fda 100644 --- a/homeassistant/components/poolsense/translations/zh-Hant.json +++ b/homeassistant/components/poolsense/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" diff --git a/homeassistant/components/powerwall/translations/zh-Hant.json b/homeassistant/components/powerwall/translations/zh-Hant.json index 44e79e935cdf1..06925ef5a413c 100644 --- a/homeassistant/components/powerwall/translations/zh-Hant.json +++ b/homeassistant/components/powerwall/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { diff --git a/homeassistant/components/progettihwsw/translations/zh-Hant.json b/homeassistant/components/progettihwsw/translations/zh-Hant.json index 815ee581e69a3..040c3dff1d77b 100644 --- a/homeassistant/components/progettihwsw/translations/zh-Hant.json +++ b/homeassistant/components/progettihwsw/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/ps4/translations/zh-Hant.json b/homeassistant/components/ps4/translations/zh-Hant.json index 77bfa7bfdb11d..4475700481a2a 100644 --- a/homeassistant/components/ps4/translations/zh-Hant.json +++ b/homeassistant/components/ps4/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "credential_error": "\u53d6\u5f97\u6191\u8b49\u932f\u8aa4\u3002", "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", "port_987_bind_error": "\u7121\u6cd5\u7d81\u5b9a\u901a\u8a0a\u57e0 987\u3002\u8acb\u53c3\u8003 [documentation](https://www.home-assistant.io/components/ps4/) \u4ee5\u7372\u5f97\u66f4\u591a\u8cc7\u8a0a\u3002", diff --git a/homeassistant/components/rachio/translations/zh-Hant.json b/homeassistant/components/rachio/translations/zh-Hant.json index b800daee779c6..a65e4e279f93d 100644 --- a/homeassistant/components/rachio/translations/zh-Hant.json +++ b/homeassistant/components/rachio/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/rainmachine/translations/zh-Hant.json b/homeassistant/components/rainmachine/translations/zh-Hant.json index 9b5829cf209be..2cb80edb39bc9 100644 --- a/homeassistant/components/rainmachine/translations/zh-Hant.json +++ b/homeassistant/components/rainmachine/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" diff --git a/homeassistant/components/recollect_waste/translations/zh-Hant.json b/homeassistant/components/recollect_waste/translations/zh-Hant.json index 75615c1cce7f5..2444a2027200b 100644 --- a/homeassistant/components/recollect_waste/translations/zh-Hant.json +++ b/homeassistant/components/recollect_waste/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "invalid_place_or_service_id": "\u5730\u9ede\u6216\u670d\u52d9 ID \u7121\u6548" diff --git a/homeassistant/components/rfxtrx/translations/zh-Hant.json b/homeassistant/components/rfxtrx/translations/zh-Hant.json index 24e5ee56d7610..fbbfeb5d6a06b 100644 --- a/homeassistant/components/rfxtrx/translations/zh-Hant.json +++ b/homeassistant/components/rfxtrx/translations/zh-Hant.json @@ -37,7 +37,7 @@ }, "options": { "error": { - "already_configured_device": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured_device": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "invalid_event_code": "\u4e8b\u4ef6\u4ee3\u78bc\u7121\u6548", "invalid_input_2262_off": "\u547d\u4ee4\u95dc\u9589\u8f38\u5165\u7121\u6548", "invalid_input_2262_on": "\u547d\u4ee4\u958b\u555f\u8f38\u5165\u7121\u6548", diff --git a/homeassistant/components/ring/translations/zh-Hant.json b/homeassistant/components/ring/translations/zh-Hant.json index 9f3c91e2a7c49..9215c7ebe3861 100644 --- a/homeassistant/components/ring/translations/zh-Hant.json +++ b/homeassistant/components/ring/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", diff --git a/homeassistant/components/risco/translations/zh-Hant.json b/homeassistant/components/risco/translations/zh-Hant.json index c76871bcecd04..7553ec3e36a0c 100644 --- a/homeassistant/components/risco/translations/zh-Hant.json +++ b/homeassistant/components/risco/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/rituals_perfume_genie/translations/zh-Hant.json b/homeassistant/components/rituals_perfume_genie/translations/zh-Hant.json index c91a500edd81f..f7fb5fcbab302 100644 --- a/homeassistant/components/rituals_perfume_genie/translations/zh-Hant.json +++ b/homeassistant/components/rituals_perfume_genie/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/roku/translations/zh-Hant.json b/homeassistant/components/roku/translations/zh-Hant.json index 429c03a991ea8..a0d755d89971a 100644 --- a/homeassistant/components/roku/translations/zh-Hant.json +++ b/homeassistant/components/roku/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, diff --git a/homeassistant/components/roomba/translations/zh-Hant.json b/homeassistant/components/roomba/translations/zh-Hant.json index 830258ff2b654..edbe4ba64a411 100644 --- a/homeassistant/components/roomba/translations/zh-Hant.json +++ b/homeassistant/components/roomba/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557", "not_irobot_device": "\u6240\u767c\u73fe\u7684\u88dd\u7f6e\u4e26\u975e iRobot \u88dd\u7f6e", "short_blid": "BLID \u906d\u622a\u77ed" diff --git a/homeassistant/components/roon/translations/zh-Hant.json b/homeassistant/components/roon/translations/zh-Hant.json index 39099753f39ed..525270fa90d13 100644 --- a/homeassistant/components/roon/translations/zh-Hant.json +++ b/homeassistant/components/roon/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "duplicate_entry": "\u8a72\u4e3b\u6a5f\u7aef\u5df2\u7d93\u8a2d\u5b9a\u3002", diff --git a/homeassistant/components/ruckus_unleashed/translations/zh-Hant.json b/homeassistant/components/ruckus_unleashed/translations/zh-Hant.json index cad7d736a9d26..011a2f61c1e69 100644 --- a/homeassistant/components/ruckus_unleashed/translations/zh-Hant.json +++ b/homeassistant/components/ruckus_unleashed/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/samsungtv/translations/zh-Hant.json b/homeassistant/components/samsungtv/translations/zh-Hant.json index 00b442399c1a3..6dfa1bd4f9162 100644 --- a/homeassistant/components/samsungtv/translations/zh-Hant.json +++ b/homeassistant/components/samsungtv/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "auth_missing": "Home Assistant \u672a\u7372\u5f97\u9a57\u8b49\u4ee5\u9023\u7dda\u81f3\u6b64\u4e09\u661f\u96fb\u8996\u3002\u8acb\u6aa2\u67e5\u60a8\u7684\u96fb\u8996\u8a2d\u5b9a\u4ee5\u76e1\u8208\u9a57\u8b49\u3002", "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/screenlogic/translations/zh-Hant.json b/homeassistant/components/screenlogic/translations/zh-Hant.json index 40ca94fd779a3..d028c77f54ba8 100644 --- a/homeassistant/components/screenlogic/translations/zh-Hant.json +++ b/homeassistant/components/screenlogic/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" diff --git a/homeassistant/components/sense/translations/zh-Hant.json b/homeassistant/components/sense/translations/zh-Hant.json index d819bfd4bbd96..c97983c0b035e 100644 --- a/homeassistant/components/sense/translations/zh-Hant.json +++ b/homeassistant/components/sense/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/shelly/translations/zh-Hant.json b/homeassistant/components/shelly/translations/zh-Hant.json index abc0b627423e9..d0e255560be5d 100644 --- a/homeassistant/components/shelly/translations/zh-Hant.json +++ b/homeassistant/components/shelly/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "unsupported_firmware": "\u88dd\u7f6e\u4f7f\u7528\u7684\u97cc\u9ad4\u4e0d\u652f\u63f4\u3002" }, "error": { diff --git a/homeassistant/components/smappee/translations/zh-Hant.json b/homeassistant/components/smappee/translations/zh-Hant.json index 4f41b5a1e5603..b867b1888c514 100644 --- a/homeassistant/components/smappee/translations/zh-Hant.json +++ b/homeassistant/components/smappee/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured_device": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured_device": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_configured_local_device": "\u672c\u5730\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\uff0c\u8acb\u5148\u9032\u884c\u79fb\u9664\u5f8c\u518d\u8a2d\u5b9a\u96f2\u7aef\u88dd\u7f6e\u3002", "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/smart_meter_texas/translations/zh-Hant.json b/homeassistant/components/smart_meter_texas/translations/zh-Hant.json index d232b491b68e5..b07b7115b074f 100644 --- a/homeassistant/components/smart_meter_texas/translations/zh-Hant.json +++ b/homeassistant/components/smart_meter_texas/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/smarttub/translations/zh-Hant.json b/homeassistant/components/smarttub/translations/zh-Hant.json index 9491e7d2f2570..880b809db0cd9 100644 --- a/homeassistant/components/smarttub/translations/zh-Hant.json +++ b/homeassistant/components/smarttub/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { diff --git a/homeassistant/components/sms/translations/zh-Hant.json b/homeassistant/components/sms/translations/zh-Hant.json index 35952af999b84..12cfbc75384ba 100644 --- a/homeassistant/components/sms/translations/zh-Hant.json +++ b/homeassistant/components/sms/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { diff --git a/homeassistant/components/solaredge/translations/zh-Hant.json b/homeassistant/components/solaredge/translations/zh-Hant.json index 18cf04cf5a533..24dbeccdf4752 100644 --- a/homeassistant/components/solaredge/translations/zh-Hant.json +++ b/homeassistant/components/solaredge/translations/zh-Hant.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "site_exists": "site_id \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "could_not_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 solaredge API", "invalid_api_key": "API \u5bc6\u9470\u7121\u6548", "site_exists": "site_id \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", diff --git a/homeassistant/components/solarlog/translations/zh-Hant.json b/homeassistant/components/solarlog/translations/zh-Hant.json index b97772a8d46f2..9782e22ee16e2 100644 --- a/homeassistant/components/solarlog/translations/zh-Hant.json +++ b/homeassistant/components/solarlog/translations/zh-Hant.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557" }, "step": { diff --git a/homeassistant/components/somfy_mylink/translations/zh-Hant.json b/homeassistant/components/somfy_mylink/translations/zh-Hant.json index 2abb6a64f7c8a..7e495cfaceebc 100644 --- a/homeassistant/components/somfy_mylink/translations/zh-Hant.json +++ b/homeassistant/components/somfy_mylink/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/songpal/translations/zh-Hant.json b/homeassistant/components/songpal/translations/zh-Hant.json index ddb334d754553..857daf2bce547 100644 --- a/homeassistant/components/songpal/translations/zh-Hant.json +++ b/homeassistant/components/songpal/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "not_songpal_device": "\u4e26\u975e Songpal \u88dd\u7f6e" }, "error": { diff --git a/homeassistant/components/squeezebox/translations/zh-Hant.json b/homeassistant/components/squeezebox/translations/zh-Hant.json index 067374f6c10b3..f2239e98dbac2 100644 --- a/homeassistant/components/squeezebox/translations/zh-Hant.json +++ b/homeassistant/components/squeezebox/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "no_server_found": "\u627e\u4e0d\u5230 LMS \u4f3a\u670d\u5668\u3002" }, "error": { diff --git a/homeassistant/components/syncthru/translations/zh-Hant.json b/homeassistant/components/syncthru/translations/zh-Hant.json index fbbc85c4a1ef6..6d2cbec0f0cb9 100644 --- a/homeassistant/components/syncthru/translations/zh-Hant.json +++ b/homeassistant/components/syncthru/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "invalid_url": "\u7db2\u5740\u7121\u6548", diff --git a/homeassistant/components/synology_dsm/translations/zh-Hant.json b/homeassistant/components/synology_dsm/translations/zh-Hant.json index 19a231ccd6052..af8103b8189e6 100644 --- a/homeassistant/components/synology_dsm/translations/zh-Hant.json +++ b/homeassistant/components/synology_dsm/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/tado/translations/zh-Hant.json b/homeassistant/components/tado/translations/zh-Hant.json index 9126e0e4ea4f2..e7f1f41ce3bfd 100644 --- a/homeassistant/components/tado/translations/zh-Hant.json +++ b/homeassistant/components/tado/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/tradfri/translations/zh-Hant.json b/homeassistant/components/tradfri/translations/zh-Hant.json index 9a48c1bc525da..36c6e124f98ce 100644 --- a/homeassistant/components/tradfri/translations/zh-Hant.json +++ b/homeassistant/components/tradfri/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d" }, "error": { diff --git a/homeassistant/components/transmission/translations/zh-Hant.json b/homeassistant/components/transmission/translations/zh-Hant.json index 5329ceb31ec64..b6769274148d0 100644 --- a/homeassistant/components/transmission/translations/zh-Hant.json +++ b/homeassistant/components/transmission/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/twinkly/translations/zh-Hant.json b/homeassistant/components/twinkly/translations/zh-Hant.json index 7e6a113e1e0b8..15fde13f9a991 100644 --- a/homeassistant/components/twinkly/translations/zh-Hant.json +++ b/homeassistant/components/twinkly/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "device_exists": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "device_exists": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" diff --git a/homeassistant/components/upb/translations/zh-Hant.json b/homeassistant/components/upb/translations/zh-Hant.json index b121c005fa788..0adb3f45e6614 100644 --- a/homeassistant/components/upb/translations/zh-Hant.json +++ b/homeassistant/components/upb/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/upnp/translations/zh-Hant.json b/homeassistant/components/upnp/translations/zh-Hant.json index 64423efed3e0b..ceb8dda3263ca 100644 --- a/homeassistant/components/upnp/translations/zh-Hant.json +++ b/homeassistant/components/upnp/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "incomplete_discovery": "\u672a\u5b8c\u6210\u63a2\u7d22", "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e" }, diff --git a/homeassistant/components/velbus/translations/zh-Hant.json b/homeassistant/components/velbus/translations/zh-Hant.json index f9bbe99d9ce3d..ec0c1ca2c6333 100644 --- a/homeassistant/components/velbus/translations/zh-Hant.json +++ b/homeassistant/components/velbus/translations/zh-Hant.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557" }, "step": { diff --git a/homeassistant/components/vilfo/translations/zh-Hant.json b/homeassistant/components/vilfo/translations/zh-Hant.json index 88180f9bacf82..eb3dba826be15 100644 --- a/homeassistant/components/vilfo/translations/zh-Hant.json +++ b/homeassistant/components/vilfo/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/vizio/translations/zh-Hant.json b/homeassistant/components/vizio/translations/zh-Hant.json index f4ac22716d18a..67d685d1b6e34 100644 --- a/homeassistant/components/vizio/translations/zh-Hant.json +++ b/homeassistant/components/vizio/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured_device": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured_device": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557", "updated_entry": "\u6b64\u5be6\u9ad4\u5df2\u7d93\u8a2d\u5b9a\uff0c\u4f46\u8a2d\u5b9a\u4e4b\u540d\u7a31\u3001App \u53ca/\u6216\u9078\u9805\u8207\u5148\u524d\u532f\u5165\u7684\u5be6\u9ad4\u9078\u9805\u503c\u4e0d\u5408\uff0c\u56e0\u6b64\u8a2d\u5b9a\u5c07\u6703\u8ddf\u8457\u66f4\u65b0\u3002" }, diff --git a/homeassistant/components/volumio/translations/zh-Hant.json b/homeassistant/components/volumio/translations/zh-Hant.json index f5573973728e4..f792fd854653a 100644 --- a/homeassistant/components/volumio/translations/zh-Hant.json +++ b/homeassistant/components/volumio/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3\u5df2\u63a2\u7d22\u5230\u7684 Volumio" }, "error": { diff --git a/homeassistant/components/wilight/translations/zh-Hant.json b/homeassistant/components/wilight/translations/zh-Hant.json index 0a86501c8f64d..fed2fb77904c4 100644 --- a/homeassistant/components/wilight/translations/zh-Hant.json +++ b/homeassistant/components/wilight/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "not_supported_device": "\u4e0d\u652f\u63f4\u6b64\u6b3e WiLight \u88dd\u7f6e\u3002", "not_wilight_device": "\u6b64\u88dd\u7f6e\u4e26\u975e WiLight" }, diff --git a/homeassistant/components/wled/translations/zh-Hant.json b/homeassistant/components/wled/translations/zh-Hant.json index 0073bb2248468..8841f15a4257d 100644 --- a/homeassistant/components/wled/translations/zh-Hant.json +++ b/homeassistant/components/wled/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557" }, "error": { diff --git a/homeassistant/components/wolflink/translations/zh-Hant.json b/homeassistant/components/wolflink/translations/zh-Hant.json index 2a0dbc2544e1b..d7f78e499921b 100644 --- a/homeassistant/components/wolflink/translations/zh-Hant.json +++ b/homeassistant/components/wolflink/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json b/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json index 5d2d097e832d5..56c530682a3f6 100644 --- a/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json +++ b/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "not_xiaomi_aqara": "\u4e26\u975e\u5c0f\u7c73 Aqara \u7db2\u95dc\uff0c\u6240\u63a2\u7d22\u4e4b\u88dd\u7f6e\u8207\u5df2\u77e5\u7db2\u95dc\u4e0d\u7b26\u5408" }, diff --git a/homeassistant/components/xiaomi_miio/translations/zh-Hant.json b/homeassistant/components/xiaomi_miio/translations/zh-Hant.json index db1d825cea878..8dc36f11f55b0 100644 --- a/homeassistant/components/xiaomi_miio/translations/zh-Hant.json +++ b/homeassistant/components/xiaomi_miio/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d" }, "error": { diff --git a/homeassistant/components/yeelight/translations/zh-Hant.json b/homeassistant/components/yeelight/translations/zh-Hant.json index d9bf3c123b409..fe21b9e535b82 100644 --- a/homeassistant/components/yeelight/translations/zh-Hant.json +++ b/homeassistant/components/yeelight/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e" }, "error": { diff --git a/homeassistant/components/zwave/translations/zh-Hant.json b/homeassistant/components/zwave/translations/zh-Hant.json index 545da7b2ee74f..d786a6b881efa 100644 --- a/homeassistant/components/zwave/translations/zh-Hant.json +++ b/homeassistant/components/zwave/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { diff --git a/homeassistant/components/zwave_js/translations/zh-Hant.json b/homeassistant/components/zwave_js/translations/zh-Hant.json index 10b003f71e8cb..d35c9e8a26039 100644 --- a/homeassistant/components/zwave_js/translations/zh-Hant.json +++ b/homeassistant/components/zwave_js/translations/zh-Hant.json @@ -7,7 +7,7 @@ "addon_missing_discovery_info": "\u7f3a\u5c11 Z-Wave JS \u9644\u52a0\u5143\u4ef6\u63a2\u7d22\u8cc7\u8a0a\u3002", "addon_set_config_failed": "Z-Wave JS \u9644\u52a0\u5143\u4ef6\u8a2d\u5b9a\u5931\u6557\u3002", "addon_start_failed": "Z-Wave JS \u9644\u52a0\u5143\u4ef6\u555f\u59cb\u5931\u6557\u3002", - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "cannot_connect": "\u9023\u7dda\u5931\u6557" }, From 65126cec3e8b075d48b2df124117ae26abeaf2b2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 12 Apr 2021 17:15:50 -0700 Subject: [PATCH 0227/1317] Allow top level non-trigger template entities (#48976) --- homeassistant/components/template/__init__.py | 54 ++++-- homeassistant/components/template/config.py | 117 +++--------- homeassistant/components/template/const.py | 1 + homeassistant/components/template/sensor.py | 169 +++++++++++++----- tests/components/template/test_init.py | 25 ++- tests/components/template/test_sensor.py | 2 +- 6 files changed, 211 insertions(+), 157 deletions(-) diff --git a/homeassistant/components/template/__init__.py b/homeassistant/components/template/__init__.py index 3b10e708e518b..dfacac17a9b8f 100644 --- a/homeassistant/components/template/__init__.py +++ b/homeassistant/components/template/__init__.py @@ -6,7 +6,6 @@ from typing import Callable from homeassistant import config as conf_util -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import EVENT_HOMEASSISTANT_START, SERVICE_RELOAD from homeassistant.core import CoreState, Event, callback from homeassistant.exceptions import HomeAssistantError @@ -57,23 +56,41 @@ async def _reload_config(call: Event) -> None: return True -async def _process_config(hass, config): +async def _process_config(hass, hass_config): """Process config.""" - coordinators: list[TriggerUpdateCoordinator] | None = hass.data.get(DOMAIN) + coordinators: list[TriggerUpdateCoordinator] | None = hass.data.pop(DOMAIN, None) # Remove old ones if coordinators: for coordinator in coordinators: coordinator.async_remove() - async def init_coordinator(hass, conf): - coordinator = TriggerUpdateCoordinator(hass, conf) - await coordinator.async_setup(config) + async def init_coordinator(hass, conf_section): + coordinator = TriggerUpdateCoordinator(hass, conf_section) + await coordinator.async_setup(hass_config) return coordinator - hass.data[DOMAIN] = await asyncio.gather( - *[init_coordinator(hass, conf) for conf in config[DOMAIN]] - ) + coordinator_tasks = [] + + for conf_section in hass_config[DOMAIN]: + if CONF_TRIGGER in conf_section: + coordinator_tasks.append(init_coordinator(hass, conf_section)) + continue + + for platform_domain in PLATFORMS: + if platform_domain in conf_section: + hass.async_create_task( + discovery.async_load_platform( + hass, + platform_domain, + DOMAIN, + {"entities": conf_section[platform_domain]}, + hass_config, + ) + ) + + if coordinator_tasks: + hass.data[DOMAIN] = await asyncio.gather(*coordinator_tasks) class TriggerUpdateCoordinator(update_coordinator.DataUpdateCoordinator): @@ -110,16 +127,17 @@ async def async_setup(self, hass_config): EVENT_HOMEASSISTANT_START, self._attach_triggers ) - for platform_domain in (SENSOR_DOMAIN,): - self.hass.async_create_task( - discovery.async_load_platform( - self.hass, - platform_domain, - DOMAIN, - {"coordinator": self, "entities": self.config[platform_domain]}, - hass_config, + for platform_domain in PLATFORMS: + if platform_domain in self.config: + self.hass.async_create_task( + discovery.async_load_platform( + self.hass, + platform_domain, + DOMAIN, + {"coordinator": self, "entities": self.config[platform_domain]}, + hass_config, + ) ) - ) async def _attach_triggers(self, start_event=None) -> None: """Attach the triggers.""" diff --git a/homeassistant/components/template/config.py b/homeassistant/components/template/config.py index 5d1a66836f3e2..8c015d70f1a5e 100644 --- a/homeassistant/components/template/config.py +++ b/homeassistant/components/template/config.py @@ -3,101 +3,29 @@ import voluptuous as vol -from homeassistant.components.sensor import ( - DEVICE_CLASSES_SCHEMA as SENSOR_DEVICE_CLASSES_SCHEMA, - DOMAIN as SENSOR_DOMAIN, -) +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config import async_log_exception, config_without_domain -from homeassistant.const import ( - CONF_DEVICE_CLASS, - CONF_ENTITY_PICTURE_TEMPLATE, - CONF_FRIENDLY_NAME, - CONF_FRIENDLY_NAME_TEMPLATE, - CONF_ICON, - CONF_ICON_TEMPLATE, - CONF_NAME, - CONF_SENSORS, - CONF_STATE, - CONF_UNIQUE_ID, - CONF_UNIT_OF_MEASUREMENT, - CONF_VALUE_TEMPLATE, -) -from homeassistant.helpers import config_validation as cv, template +from homeassistant.const import CONF_SENSORS, CONF_UNIQUE_ID +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.trigger import async_validate_trigger_config -from .const import ( - CONF_ATTRIBUTE_TEMPLATES, - CONF_ATTRIBUTES, - CONF_AVAILABILITY, - CONF_AVAILABILITY_TEMPLATE, - CONF_PICTURE, - CONF_TRIGGER, - DOMAIN, -) -from .sensor import SENSOR_SCHEMA as PLATFORM_SENSOR_SCHEMA - -LEGACY_SENSOR = { - CONF_ICON_TEMPLATE: CONF_ICON, - CONF_ENTITY_PICTURE_TEMPLATE: CONF_PICTURE, - CONF_AVAILABILITY_TEMPLATE: CONF_AVAILABILITY, - CONF_ATTRIBUTE_TEMPLATES: CONF_ATTRIBUTES, - CONF_FRIENDLY_NAME_TEMPLATE: CONF_NAME, - CONF_FRIENDLY_NAME: CONF_NAME, - CONF_VALUE_TEMPLATE: CONF_STATE, -} - - -SENSOR_SCHEMA = vol.Schema( - { - vol.Optional(CONF_NAME): cv.template, - vol.Required(CONF_STATE): cv.template, - vol.Optional(CONF_ICON): cv.template, - vol.Optional(CONF_PICTURE): cv.template, - vol.Optional(CONF_AVAILABILITY): cv.template, - vol.Optional(CONF_ATTRIBUTES): vol.Schema({cv.string: cv.template}), - vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, - vol.Optional(CONF_DEVICE_CLASS): SENSOR_DEVICE_CLASSES_SCHEMA, - vol.Optional(CONF_UNIQUE_ID): cv.string, - } -) +from . import sensor as sensor_platform +from .const import CONF_TRIGGER, DOMAIN CONFIG_SECTION_SCHEMA = vol.Schema( { vol.Optional(CONF_UNIQUE_ID): cv.string, - vol.Required(CONF_TRIGGER): cv.TRIGGER_SCHEMA, - vol.Optional(SENSOR_DOMAIN): vol.All(cv.ensure_list, [SENSOR_SCHEMA]), - vol.Optional(CONF_SENSORS): cv.schema_with_slug_keys(PLATFORM_SENSOR_SCHEMA), + vol.Optional(CONF_TRIGGER): cv.TRIGGER_SCHEMA, + vol.Optional(SENSOR_DOMAIN): vol.All( + cv.ensure_list, [sensor_platform.SENSOR_SCHEMA] + ), + vol.Optional(CONF_SENSORS): cv.schema_with_slug_keys( + sensor_platform.LEGACY_SENSOR_SCHEMA + ), } ) -def _rewrite_legacy_to_modern_trigger_conf(cfg: dict): - """Rewrite a legacy to a modern trigger-basd conf.""" - logging.getLogger(__name__).warning( - "The entity definition format under template: differs from the platform configuration format. See https://www.home-assistant.io/integrations/template#configuration-for-trigger-based-template-sensors" - ) - sensor = list(cfg[SENSOR_DOMAIN]) if SENSOR_DOMAIN in cfg else [] - - for device_id, entity_cfg in cfg[CONF_SENSORS].items(): - entity_cfg = {**entity_cfg} - - for from_key, to_key in LEGACY_SENSOR.items(): - if from_key not in entity_cfg or to_key in entity_cfg: - continue - - val = entity_cfg.pop(from_key) - if isinstance(val, str): - val = template.Template(val) - entity_cfg[to_key] = val - - if CONF_NAME not in entity_cfg: - entity_cfg[CONF_NAME] = template.Template(device_id) - - sensor.append(entity_cfg) - - return {**cfg, "sensor": sensor} - - async def async_validate_config(hass, config): """Validate config.""" if DOMAIN not in config: @@ -108,15 +36,26 @@ async def async_validate_config(hass, config): for cfg in cv.ensure_list(config[DOMAIN]): try: cfg = CONFIG_SECTION_SCHEMA(cfg) - cfg[CONF_TRIGGER] = await async_validate_trigger_config( - hass, cfg[CONF_TRIGGER] - ) + + if CONF_TRIGGER in cfg: + cfg[CONF_TRIGGER] = await async_validate_trigger_config( + hass, cfg[CONF_TRIGGER] + ) except vol.Invalid as err: async_log_exception(err, DOMAIN, cfg, hass) continue - if CONF_TRIGGER in cfg and CONF_SENSORS in cfg: - cfg = _rewrite_legacy_to_modern_trigger_conf(cfg) + if CONF_SENSORS in cfg: + logging.getLogger(__name__).warning( + "The entity definition format under template: differs from the platform " + "configuration format. See " + "https://www.home-assistant.io/integrations/template#configuration-for-trigger-based-template-sensors" + ) + sensors = list(cfg[SENSOR_DOMAIN]) if SENSOR_DOMAIN in cfg else [] + sensors.extend( + sensor_platform.rewrite_legacy_to_modern_conf(cfg[CONF_SENSORS]) + ) + cfg = {**cfg, "sensor": sensors} config_sections.append(cfg) diff --git a/homeassistant/components/template/const.py b/homeassistant/components/template/const.py index 971d4a864c95c..661953bcfa55c 100644 --- a/homeassistant/components/template/const.py +++ b/homeassistant/components/template/const.py @@ -24,3 +24,4 @@ CONF_AVAILABILITY = "availability" CONF_ATTRIBUTES = "attributes" CONF_PICTURE = "picture" +CONF_OBJECT_ID = "object_id" diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index 4631a77584710..224756c201285 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -16,23 +16,59 @@ CONF_ENTITY_PICTURE_TEMPLATE, CONF_FRIENDLY_NAME, CONF_FRIENDLY_NAME_TEMPLATE, + CONF_ICON, CONF_ICON_TEMPLATE, + CONF_NAME, CONF_SENSORS, CONF_STATE, CONF_UNIQUE_ID, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import TemplateError -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.entity import async_generate_entity_id -from .const import CONF_ATTRIBUTE_TEMPLATES, CONF_AVAILABILITY_TEMPLATE, CONF_TRIGGER +from .const import ( + CONF_ATTRIBUTE_TEMPLATES, + CONF_ATTRIBUTES, + CONF_AVAILABILITY, + CONF_AVAILABILITY_TEMPLATE, + CONF_OBJECT_ID, + CONF_PICTURE, + CONF_TRIGGER, +) from .template_entity import TemplateEntity from .trigger_entity import TriggerEntity -SENSOR_SCHEMA = vol.All( +LEGACY_FIELDS = { + CONF_ICON_TEMPLATE: CONF_ICON, + CONF_ENTITY_PICTURE_TEMPLATE: CONF_PICTURE, + CONF_AVAILABILITY_TEMPLATE: CONF_AVAILABILITY, + CONF_ATTRIBUTE_TEMPLATES: CONF_ATTRIBUTES, + CONF_FRIENDLY_NAME_TEMPLATE: CONF_NAME, + CONF_FRIENDLY_NAME: CONF_NAME, + CONF_VALUE_TEMPLATE: CONF_STATE, +} + + +SENSOR_SCHEMA = vol.Schema( + { + vol.Optional(CONF_NAME): cv.template, + vol.Required(CONF_STATE): cv.template, + vol.Optional(CONF_ICON): cv.template, + vol.Optional(CONF_PICTURE): cv.template, + vol.Optional(CONF_AVAILABILITY): cv.template, + vol.Optional(CONF_ATTRIBUTES): vol.Schema({cv.string: cv.template}), + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_UNIQUE_ID): cv.string, + } +) + + +LEGACY_SENSOR_SCHEMA = vol.All( cv.deprecated(ATTR_ENTITY_ID), vol.Schema( { @@ -54,50 +90,78 @@ ) -def trigger_warning(val): - """Warn if a trigger is defined.""" +def extra_validation_checks(val): + """Run extra validation checks.""" if CONF_TRIGGER in val: raise vol.Invalid( "You can only add triggers to template entities if they are defined under `template:`. " "See the template documentation for more information: https://www.home-assistant.io/integrations/template/" ) + if CONF_SENSORS not in val and SENSOR_DOMAIN not in val: + raise vol.Invalid(f"Required key {SENSOR_DOMAIN} not defined") + return val +def rewrite_legacy_to_modern_conf(cfg: dict[str, dict]) -> list[dict]: + """Rewrite a legacy sensor definitions to modern ones.""" + sensors = [] + + for object_id, entity_cfg in cfg.items(): + entity_cfg = {**entity_cfg, CONF_OBJECT_ID: object_id} + + for from_key, to_key in LEGACY_FIELDS.items(): + if from_key not in entity_cfg or to_key in entity_cfg: + continue + + val = entity_cfg.pop(from_key) + if isinstance(val, str): + val = template.Template(val) + entity_cfg[to_key] = val + + if CONF_NAME not in entity_cfg: + entity_cfg[CONF_NAME] = template.Template(object_id) + + sensors.append(entity_cfg) + + return sensors + + PLATFORM_SCHEMA = vol.All( PLATFORM_SCHEMA.extend( { vol.Optional(CONF_TRIGGER): cv.match_all, # to raise custom warning - vol.Required(CONF_SENSORS): cv.schema_with_slug_keys(SENSOR_SCHEMA), + vol.Required(CONF_SENSORS): cv.schema_with_slug_keys(LEGACY_SENSOR_SCHEMA), } ), - trigger_warning, + extra_validation_checks, ) @callback -def _async_create_template_tracking_entities(hass, config): +def _async_create_template_tracking_entities(async_add_entities, hass, definitions): """Create the template sensors.""" sensors = [] - for device, device_config in config[CONF_SENSORS].items(): - state_template = device_config[CONF_VALUE_TEMPLATE] - icon_template = device_config.get(CONF_ICON_TEMPLATE) - entity_picture_template = device_config.get(CONF_ENTITY_PICTURE_TEMPLATE) - availability_template = device_config.get(CONF_AVAILABILITY_TEMPLATE) - friendly_name = device_config.get(CONF_FRIENDLY_NAME, device) - friendly_name_template = device_config.get(CONF_FRIENDLY_NAME_TEMPLATE) - unit_of_measurement = device_config.get(CONF_UNIT_OF_MEASUREMENT) - device_class = device_config.get(CONF_DEVICE_CLASS) - attribute_templates = device_config.get(CONF_ATTRIBUTE_TEMPLATES, {}) - unique_id = device_config.get(CONF_UNIQUE_ID) + for entity_conf in definitions: + # Still available on legacy + object_id = entity_conf.get(CONF_OBJECT_ID) + + state_template = entity_conf[CONF_STATE] + icon_template = entity_conf.get(CONF_ICON) + entity_picture_template = entity_conf.get(CONF_PICTURE) + availability_template = entity_conf.get(CONF_AVAILABILITY) + friendly_name_template = entity_conf.get(CONF_NAME) + unit_of_measurement = entity_conf.get(CONF_UNIT_OF_MEASUREMENT) + device_class = entity_conf.get(CONF_DEVICE_CLASS) + attribute_templates = entity_conf.get(CONF_ATTRIBUTES, {}) + unique_id = entity_conf.get(CONF_UNIQUE_ID) sensors.append( SensorTemplate( hass, - device, - friendly_name, + object_id, friendly_name_template, unit_of_measurement, state_template, @@ -110,18 +174,29 @@ def _async_create_template_tracking_entities(hass, config): ) ) - return sensors + async_add_entities(sensors) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the template sensors.""" if discovery_info is None: - async_add_entities(_async_create_template_tracking_entities(hass, config)) - else: + _async_create_template_tracking_entities( + async_add_entities, + hass, + rewrite_legacy_to_modern_conf(config[CONF_SENSORS]), + ) + return + + if "coordinator" in discovery_info: async_add_entities( TriggerSensorEntity(hass, discovery_info["coordinator"], config) for config in discovery_info["entities"] ) + return + + _async_create_template_tracking_entities( + async_add_entities, hass, discovery_info["entities"] + ) class SensorTemplate(TemplateEntity, SensorEntity): @@ -129,18 +204,17 @@ class SensorTemplate(TemplateEntity, SensorEntity): def __init__( self, - hass, - device_id, - friendly_name, - friendly_name_template, - unit_of_measurement, - state_template, - icon_template, - entity_picture_template, - availability_template, - device_class, - attribute_templates, - unique_id, + hass: HomeAssistant, + object_id: str | None, + friendly_name_template: template.Template | None, + unit_of_measurement: str | None, + state_template: template.Template, + icon_template: template.Template | None, + entity_picture_template: template.Template | None, + availability_template: template.Template | None, + device_class: str | None, + attribute_templates: dict[str, template.Template], + unique_id: str | None, ): """Initialize the sensor.""" super().__init__( @@ -149,11 +223,22 @@ def __init__( icon_template=icon_template, entity_picture_template=entity_picture_template, ) - self.entity_id = async_generate_entity_id( - ENTITY_ID_FORMAT, device_id, hass=hass - ) - self._name = friendly_name + if object_id is not None: + self.entity_id = async_generate_entity_id( + ENTITY_ID_FORMAT, object_id, hass=hass + ) + + self._name: str | None = None self._friendly_name_template = friendly_name_template + + # Try to render the name as it can influence the entity ID + if friendly_name_template: + friendly_name_template.hass = hass + try: + self._name = friendly_name_template.async_render(parse_result=False) + except template.TemplateError: + pass + self._unit_of_measurement = unit_of_measurement self._template = state_template self._state = None @@ -164,7 +249,7 @@ def __init__( async def async_added_to_hass(self): """Register callbacks.""" self.add_template_attribute("_state", self._template, None, self._update_state) - if self._friendly_name_template is not None: + if self._friendly_name_template and not self._friendly_name_template.is_static: self.add_template_attribute("_name", self._friendly_name_template) await super().async_added_to_hass() diff --git a/tests/components/template/test_init.py b/tests/components/template/test_init.py index 0f8dff4026fe2..3c098a0729ff1 100644 --- a/tests/components/template/test_init.py +++ b/tests/components/template/test_init.py @@ -28,26 +28,37 @@ async def test_reloadable(hass): }, }, }, - "template": { - "trigger": {"platform": "event", "event_type": "event_1"}, - "sensor": { - "name": "top level", - "state": "{{ trigger.event.data.source }}", + "template": [ + { + "trigger": {"platform": "event", "event_type": "event_1"}, + "sensor": { + "name": "top level", + "state": "{{ trigger.event.data.source }}", + }, }, - }, + { + "sensor": { + "name": "top level state", + "state": "{{ states.sensor.top_level.state }} + 2", + }, + }, + ], }, ) await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() + assert hass.states.get("sensor.top_level_state").state == "unknown + 2" hass.bus.async_fire("event_1", {"source": "init"}) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 3 + assert len(hass.states.async_all()) == 4 assert hass.states.get("sensor.state").state == "mytest" assert hass.states.get("sensor.top_level").state == "init" + await hass.async_block_till_done() + assert hass.states.get("sensor.top_level_state").state == "init + 2" yaml_path = path.join( _get_fixtures_base_path(), diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py index d146f5d88defa..b510a0c75f87d 100644 --- a/tests/components/template/test_sensor.py +++ b/tests/components/template/test_sensor.py @@ -26,7 +26,7 @@ from tests.common import assert_setup_component, async_fire_time_changed -async def test_template(hass): +async def test_template_legacy(hass): """Test template.""" with assert_setup_component(1, sensor.DOMAIN): assert await async_setup_component( From 53853f035df1d131bed133236f57eb68b267f1b0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 12 Apr 2021 14:18:38 -1000 Subject: [PATCH 0228/1317] Prevent calling stop or restart services during db upgrade (#49098) --- .../components/homeassistant/__init__.py | 58 ++++++-- homeassistant/components/recorder/__init__.py | 27 +++- .../components/websocket_api/commands.py | 5 +- homeassistant/helpers/recorder.py | 15 ++ tests/components/homeassistant/test_init.py | 133 +++++++++++++++--- tests/components/recorder/test_migrate.py | 30 ++++ .../components/websocket_api/test_commands.py | 2 +- tests/helpers/test_recorder.py | 32 +++++ 8 files changed, 270 insertions(+), 32 deletions(-) create mode 100644 homeassistant/helpers/recorder.py create mode 100644 tests/helpers/test_recorder.py diff --git a/homeassistant/components/homeassistant/__init__.py b/homeassistant/components/homeassistant/__init__.py index 67eb94a97e73b..86be5862e7c33 100644 --- a/homeassistant/components/homeassistant/__init__.py +++ b/homeassistant/components/homeassistant/__init__.py @@ -20,7 +20,8 @@ ) import homeassistant.core as ha from homeassistant.exceptions import HomeAssistantError, Unauthorized, UnknownUser -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import config_validation as cv, recorder +from homeassistant.helpers.event import async_call_later from homeassistant.helpers.service import ( async_extract_config_entry_ids, async_extract_referenced_entity_ids, @@ -47,6 +48,10 @@ ) +SHUTDOWN_SERVICES = (SERVICE_HOMEASSISTANT_STOP, SERVICE_HOMEASSISTANT_RESTART) +WEBSOCKET_RECEIVE_DELAY = 1 + + async def async_setup(hass: ha.HomeAssistant, config: dict) -> bool: """Set up general services related to Home Assistant.""" @@ -125,26 +130,61 @@ async def async_handle_turn_service(service): async def async_handle_core_service(call): """Service handler for handling core services.""" + if ( + call.service in SHUTDOWN_SERVICES + and await recorder.async_migration_in_progress(hass) + ): + _LOGGER.error( + "The system cannot %s while a database upgrade in progress", + call.service, + ) + raise HomeAssistantError( + f"The system cannot {call.service} while a database upgrade in progress." + ) + if call.service == SERVICE_HOMEASSISTANT_STOP: - hass.async_create_task(hass.async_stop()) + # We delay the stop by WEBSOCKET_RECEIVE_DELAY to ensure the frontend + # can receive the response before the webserver shuts down + @ha.callback + def _async_stop(_): + # This must not be a tracked task otherwise + # the task itself will block stop + asyncio.create_task(hass.async_stop()) + + async_call_later(hass, WEBSOCKET_RECEIVE_DELAY, _async_stop) return - try: - errors = await conf_util.async_check_ha_config_file(hass) - except HomeAssistantError: - return + errors = await conf_util.async_check_ha_config_file(hass) if errors: - _LOGGER.error(errors) + _LOGGER.error( + "The system cannot %s because the configuration is not valid: %s", + call.service, + errors, + ) hass.components.persistent_notification.async_create( "Config error. See [the logs](/config/logs) for details.", "Config validating", f"{ha.DOMAIN}.check_config", ) - return + raise HomeAssistantError( + f"The system cannot {call.service} because the configuration is not valid: {errors}" + ) if call.service == SERVICE_HOMEASSISTANT_RESTART: - hass.async_create_task(hass.async_stop(RESTART_EXIT_CODE)) + # We delay the restart by WEBSOCKET_RECEIVE_DELAY to ensure the frontend + # can receive the response before the webserver shuts down + @ha.callback + def _async_stop_with_code(_): + # This must not be a tracked task otherwise + # the task itself will block restart + asyncio.create_task(hass.async_stop(RESTART_EXIT_CODE)) + + async_call_later( + hass, + WEBSOCKET_RECEIVE_DELAY, + _async_stop_with_code, + ) async def async_handle_update_service(call): """Service handler for updating an entity.""" diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 10b987b04f7b5..98199bab430bd 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -36,6 +36,7 @@ ) from homeassistant.helpers.event import async_track_time_interval, track_time_change from homeassistant.helpers.typing import ConfigType +from homeassistant.loader import bind_hass import homeassistant.util.dt as dt_util from . import migration, purge @@ -132,6 +133,18 @@ ) +@bind_hass +async def async_migration_in_progress(hass: HomeAssistant) -> bool: + """Determine is a migration is in progress. + + This is a thin wrapper that allows us to change + out the implementation later. + """ + if DATA_INSTANCE not in hass.data: + return False + return hass.data[DATA_INSTANCE].migration_in_progress + + def run_information(hass, point_in_time: datetime | None = None): """Return information about current run. @@ -291,7 +304,8 @@ def __init__( self.get_session = None self._completed_database_setup = None self._event_listener = None - + self.async_migration_event = asyncio.Event() + self.migration_in_progress = False self._queue_watcher = None self.enabled = True @@ -418,11 +432,13 @@ def run(self): schema_is_current = migration.schema_is_current(current_version) if schema_is_current: self._setup_run() + else: + self.migration_in_progress = True self.hass.add_job(self.async_connection_success) - # If shutdown happened before Home Assistant finished starting if hass_started.result() is shutdown_task: + self.migration_in_progress = False # Make sure we cleanly close the run if # we restart before startup finishes self._shutdown() @@ -510,6 +526,11 @@ def _setup_recorder(self) -> None | int: return None + @callback + def _async_migration_started(self): + """Set the migration started event.""" + self.async_migration_event.set() + def _migrate_schema_and_setup_run(self, current_version) -> bool: """Migrate schema to the latest version.""" persistent_notification.create( @@ -518,6 +539,7 @@ def _migrate_schema_and_setup_run(self, current_version) -> bool: "Database upgrade in progress", "recorder_database_migration", ) + self.hass.add_job(self._async_migration_started) try: migration.migrate_schema(self, current_version) @@ -533,6 +555,7 @@ def _migrate_schema_and_setup_run(self, current_version) -> bool: self._setup_run() return True finally: + self.migration_in_progress = False persistent_notification.dismiss(self.hass, "recorder_database_migration") def _run_purge(self, keep_days, repack, apply_filter): diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 4045477f75e7f..af2c914bfbd2b 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -8,7 +8,7 @@ from homeassistant.bootstrap import SIGNAL_BOOTSTRAP_INTEGRATONS from homeassistant.components.websocket_api.const import ERR_NOT_FOUND from homeassistant.const import EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL -from homeassistant.core import DOMAIN as HASS_DOMAIN, callback +from homeassistant.core import callback from homeassistant.exceptions import ( HomeAssistantError, ServiceNotFound, @@ -157,9 +157,6 @@ def handle_unsubscribe_events(hass, connection, msg): async def handle_call_service(hass, connection, msg): """Handle call service command.""" blocking = True - if msg["domain"] == HASS_DOMAIN and msg["service"] in ["restart", "stop"]: - blocking = False - # We do not support templates. target = msg.get("target") if template.is_complex(target): diff --git a/homeassistant/helpers/recorder.py b/homeassistant/helpers/recorder.py new file mode 100644 index 0000000000000..e3ed3428a2abe --- /dev/null +++ b/homeassistant/helpers/recorder.py @@ -0,0 +1,15 @@ +"""Helpers to check recorder.""" + + +from homeassistant.core import HomeAssistant + + +async def async_migration_in_progress(hass: HomeAssistant) -> bool: + """Check to see if a recorder migration is in progress.""" + if "recorder" not in hass.config.components: + return False + from homeassistant.components import ( # pylint: disable=import-outside-toplevel + recorder, + ) + + return await recorder.async_migration_in_progress(hass) diff --git a/tests/components/homeassistant/test_init.py b/tests/components/homeassistant/test_init.py index 2e2eaf991afd2..451c226eb87b5 100644 --- a/tests/components/homeassistant/test_init.py +++ b/tests/components/homeassistant/test_init.py @@ -1,6 +1,7 @@ """The tests for Core components.""" # pylint: disable=protected-access import asyncio +from datetime import timedelta import unittest from unittest.mock import Mock, patch @@ -33,10 +34,12 @@ from homeassistant.exceptions import HomeAssistantError, Unauthorized from homeassistant.helpers import entity from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util from tests.common import ( MockConfigEntry, async_capture_events, + async_fire_time_changed, async_mock_service, get_test_home_assistant, mock_registry, @@ -213,22 +216,6 @@ def test_reload_core_with_wrong_conf(self, mock_process, mock_error): assert mock_error.called assert mock_process.called is False - @patch("homeassistant.core.HomeAssistant.async_stop", return_value=None) - def test_stop_homeassistant(self, mock_stop): - """Test stop service.""" - stop(self.hass) - self.hass.block_till_done() - assert mock_stop.called - - @patch("homeassistant.core.HomeAssistant.async_stop", return_value=None) - @patch("homeassistant.config.async_check_ha_config_file", return_value=None) - def test_restart_homeassistant(self, mock_check, mock_restart): - """Test stop service.""" - restart(self.hass) - self.hass.block_till_done() - assert mock_restart.called - assert mock_check.called - @patch("homeassistant.core.HomeAssistant.async_stop", return_value=None) @patch( "homeassistant.config.async_check_ha_config_file", @@ -447,3 +434,117 @@ async def test_reload_config_entry_by_entry_id(hass): assert len(mock_reload.mock_calls) == 1 assert mock_reload.mock_calls[0][1][0] == "8955375327824e14ba89e4b29cc3ec9a" + + +@pytest.mark.parametrize( + "service", [SERVICE_HOMEASSISTANT_RESTART, SERVICE_HOMEASSISTANT_STOP] +) +async def test_raises_when_db_upgrade_in_progress(hass, service, caplog): + """Test an exception is raised when the database migration is in progress.""" + await async_setup_component(hass, "homeassistant", {}) + + with pytest.raises(HomeAssistantError), patch( + "homeassistant.helpers.recorder.async_migration_in_progress", + return_value=True, + ) as mock_async_migration_in_progress: + await hass.services.async_call( + "homeassistant", + service, + blocking=True, + ) + assert "The system cannot" in caplog.text + assert "while a database upgrade in progress" in caplog.text + + assert mock_async_migration_in_progress.called + caplog.clear() + + with patch( + "homeassistant.helpers.recorder.async_migration_in_progress", + return_value=False, + ) as mock_async_migration_in_progress, patch( + "homeassistant.config.async_check_ha_config_file", return_value=None + ): + await hass.services.async_call( + "homeassistant", + service, + blocking=True, + ) + assert "The system cannot" not in caplog.text + assert "while a database upgrade in progress" not in caplog.text + + assert mock_async_migration_in_progress.called + + +async def test_raises_when_config_is_invalid(hass, caplog): + """Test an exception is raised when the configuration is invalid.""" + await async_setup_component(hass, "homeassistant", {}) + + with pytest.raises(HomeAssistantError), patch( + "homeassistant.helpers.recorder.async_migration_in_progress", + return_value=False, + ), patch( + "homeassistant.config.async_check_ha_config_file", return_value=["Error 1"] + ) as mock_async_check_ha_config_file: + await hass.services.async_call( + "homeassistant", + SERVICE_HOMEASSISTANT_RESTART, + blocking=True, + ) + assert "The system cannot" in caplog.text + assert "because the configuration is not valid" in caplog.text + assert "Error 1" in caplog.text + + assert mock_async_check_ha_config_file.called + caplog.clear() + + with patch( + "homeassistant.helpers.recorder.async_migration_in_progress", + return_value=False, + ), patch( + "homeassistant.config.async_check_ha_config_file", return_value=None + ) as mock_async_check_ha_config_file: + await hass.services.async_call( + "homeassistant", + SERVICE_HOMEASSISTANT_RESTART, + blocking=True, + ) + + assert mock_async_check_ha_config_file.called + + +async def test_restart_homeassistant(hass): + """Test we can restart when there is no configuration error.""" + await async_setup_component(hass, "homeassistant", {}) + with patch( + "homeassistant.config.async_check_ha_config_file", return_value=None + ) as mock_check, patch( + "homeassistant.core.HomeAssistant.async_stop", return_value=None + ) as mock_restart: + await hass.services.async_call( + "homeassistant", + SERVICE_HOMEASSISTANT_RESTART, + blocking=True, + ) + assert mock_check.called + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=2)) + await hass.async_block_till_done() + assert mock_restart.called + + +async def test_stop_homeassistant(hass): + """Test we can stop when there is a configuration error.""" + await async_setup_component(hass, "homeassistant", {}) + with patch( + "homeassistant.config.async_check_ha_config_file", return_value=None + ) as mock_check, patch( + "homeassistant.core.HomeAssistant.async_stop", return_value=None + ) as mock_restart: + await hass.services.async_call( + "homeassistant", + SERVICE_HOMEASSISTANT_STOP, + blocking=True, + ) + assert not mock_check.called + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=2)) + await hass.async_block_till_done() + assert mock_restart.called diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index 113598ff6dece..ab5c7d54a28e3 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -48,6 +48,7 @@ def create_engine_test(*args, **kwargs): async def test_schema_update_calls(hass): """Test that schema migrations occur in correct order.""" + assert await recorder.async_migration_in_progress(hass) is False await async_setup_component(hass, "persistent_notification", {}) with patch( "homeassistant.components.recorder.create_engine", new=create_engine_test @@ -60,6 +61,7 @@ async def test_schema_update_calls(hass): ) await async_wait_recording_done_without_instance(hass) + assert await recorder.async_migration_in_progress(hass) is False update.assert_has_calls( [ call(hass.data[DATA_INSTANCE].engine, version + 1, 0) @@ -68,11 +70,30 @@ async def test_schema_update_calls(hass): ) +async def test_migration_in_progress(hass): + """Test that we can check for migration in progress.""" + assert await recorder.async_migration_in_progress(hass) is False + await async_setup_component(hass, "persistent_notification", {}) + + with patch( + "homeassistant.components.recorder.create_engine", new=create_engine_test + ): + await async_setup_component( + hass, "recorder", {"recorder": {"db_url": "sqlite://"}} + ) + await hass.data[DATA_INSTANCE].async_migration_event.wait() + assert await recorder.async_migration_in_progress(hass) is True + await async_wait_recording_done_without_instance(hass) + + assert await recorder.async_migration_in_progress(hass) is False + + async def test_database_migration_failed(hass): """Test we notify if the migration fails.""" await async_setup_component(hass, "persistent_notification", {}) create_calls = async_mock_service(hass, "persistent_notification", "create") dismiss_calls = async_mock_service(hass, "persistent_notification", "dismiss") + assert await recorder.async_migration_in_progress(hass) is False with patch( "homeassistant.components.recorder.create_engine", new=create_engine_test @@ -89,6 +110,7 @@ async def test_database_migration_failed(hass): await hass.async_add_executor_job(hass.data[DATA_INSTANCE].join) await hass.async_block_till_done() + assert await recorder.async_migration_in_progress(hass) is False assert len(create_calls) == 2 assert len(dismiss_calls) == 1 @@ -96,6 +118,7 @@ async def test_database_migration_failed(hass): async def test_database_migration_encounters_corruption(hass): """Test we move away the database if its corrupt.""" await async_setup_component(hass, "persistent_notification", {}) + assert await recorder.async_migration_in_progress(hass) is False sqlite3_exception = DatabaseError("statement", {}, []) sqlite3_exception.__cause__ = sqlite3.DatabaseError() @@ -116,6 +139,7 @@ async def test_database_migration_encounters_corruption(hass): hass.states.async_set("my.entity", "off", {}) await async_wait_recording_done_without_instance(hass) + assert await recorder.async_migration_in_progress(hass) is False assert move_away.called @@ -124,6 +148,7 @@ async def test_database_migration_encounters_corruption_not_sqlite(hass): await async_setup_component(hass, "persistent_notification", {}) create_calls = async_mock_service(hass, "persistent_notification", "create") dismiss_calls = async_mock_service(hass, "persistent_notification", "dismiss") + assert await recorder.async_migration_in_progress(hass) is False with patch( "homeassistant.components.recorder.migration.schema_is_current", @@ -143,6 +168,7 @@ async def test_database_migration_encounters_corruption_not_sqlite(hass): await hass.async_add_executor_job(hass.data[DATA_INSTANCE].join) await hass.async_block_till_done() + assert await recorder.async_migration_in_progress(hass) is False assert not move_away.called assert len(create_calls) == 2 assert len(dismiss_calls) == 1 @@ -151,6 +177,7 @@ async def test_database_migration_encounters_corruption_not_sqlite(hass): async def test_events_during_migration_are_queued(hass): """Test that events during migration are queued.""" + assert await recorder.async_migration_in_progress(hass) is False await async_setup_component(hass, "persistent_notification", {}) with patch( "homeassistant.components.recorder.create_engine", new=create_engine_test @@ -167,6 +194,7 @@ async def test_events_during_migration_are_queued(hass): await hass.data[DATA_INSTANCE].async_recorder_ready.wait() await async_wait_recording_done_without_instance(hass) + assert await recorder.async_migration_in_progress(hass) is False db_states = await hass.async_add_executor_job(_get_native_states, hass, "my.entity") assert len(db_states) == 2 @@ -174,6 +202,7 @@ async def test_events_during_migration_are_queued(hass): async def test_events_during_migration_queue_exhausted(hass): """Test that events during migration takes so long the queue is exhausted.""" await async_setup_component(hass, "persistent_notification", {}) + assert await recorder.async_migration_in_progress(hass) is False with patch( "homeassistant.components.recorder.create_engine", new=create_engine_test @@ -191,6 +220,7 @@ async def test_events_during_migration_queue_exhausted(hass): await hass.data[DATA_INSTANCE].async_recorder_ready.wait() await async_wait_recording_done_without_instance(hass) + assert await recorder.async_migration_in_progress(hass) is False db_states = await hass.async_add_executor_job(_get_native_states, hass, "my.entity") assert len(db_states) == 1 hass.states.async_set("my.entity", "on", {}) diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 67abb7b2b53cd..3ec021c3e3b5f 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -126,7 +126,7 @@ async def test_call_service_blocking(hass, websocket_client, command): assert msg["type"] == const.TYPE_RESULT assert msg["success"] mock_call.assert_called_once_with( - ANY, "homeassistant", "restart", ANY, blocking=False, context=ANY, target=ANY + ANY, "homeassistant", "restart", ANY, blocking=True, context=ANY, target=ANY ) diff --git a/tests/helpers/test_recorder.py b/tests/helpers/test_recorder.py new file mode 100644 index 0000000000000..60d60a2335e57 --- /dev/null +++ b/tests/helpers/test_recorder.py @@ -0,0 +1,32 @@ +"""The tests for the recorder helpers.""" + +from unittest.mock import patch + +from homeassistant.helpers import recorder + +from tests.common import async_init_recorder_component + + +async def test_async_migration_in_progress(hass): + """Test async_migration_in_progress wraps the recorder.""" + with patch( + "homeassistant.components.recorder.async_migration_in_progress", + return_value=False, + ): + assert await recorder.async_migration_in_progress(hass) is False + + # The recorder is not loaded + with patch( + "homeassistant.components.recorder.async_migration_in_progress", + return_value=True, + ): + assert await recorder.async_migration_in_progress(hass) is False + + await async_init_recorder_component(hass) + + # The recorder is now loaded + with patch( + "homeassistant.components.recorder.async_migration_in_progress", + return_value=True, + ): + assert await recorder.async_migration_in_progress(hass) is True From cc40e681e2214c11324d2533ab97d1d501e45d1b Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 12 Apr 2021 20:26:49 -0400 Subject: [PATCH 0229/1317] Lazy load zwave_js platforms when the first entity needs to be created (#49016) * Lazy load zwave_js platforms when the first entity needs to be created * switch order to make things easier to understand * await task instead of using wait_for_done callback * gather tasks * switch from asyncio.create_task to hass.async_create_task * unsubscribe from callbacks before unloading platforms * Clean up as much as possible during entry unload, even if a platform unload fails --- homeassistant/components/zwave_js/__init__.py | 118 +++++++++++------- homeassistant/components/zwave_js/const.py | 12 +- 2 files changed, 72 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 10cc2543921e8..45aef87bf804a 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -54,11 +54,11 @@ CONF_USB_PATH, CONF_USE_ADDON, DATA_CLIENT, + DATA_PLATFORM_SETUP, DATA_UNSUBSCRIBE, DOMAIN, EVENT_DEVICE_ADDED_TO_REGISTRY, LOGGER, - PLATFORMS, ZWAVE_JS_NOTIFICATION_EVENT, ZWAVE_JS_VALUE_NOTIFICATION_EVENT, ) @@ -113,49 +113,69 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: client = ZwaveClient(entry.data[CONF_URL], async_get_clientsession(hass)) dev_reg = device_registry.async_get(hass) ent_reg = entity_registry.async_get(hass) + entry_hass_data: dict = hass.data[DOMAIN].setdefault(entry.entry_id, {}) - @callback - def async_on_node_ready(node: ZwaveNode) -> None: + unsubscribe_callbacks: list[Callable] = [] + entry_hass_data[DATA_CLIENT] = client + entry_hass_data[DATA_UNSUBSCRIBE] = unsubscribe_callbacks + entry_hass_data[DATA_PLATFORM_SETUP] = {} + + async def async_on_node_ready(node: ZwaveNode) -> None: """Handle node ready event.""" LOGGER.debug("Processing node %s", node) + platform_setup_tasks = entry_hass_data[DATA_PLATFORM_SETUP] + # register (or update) node in device registry register_node_in_dev_reg(hass, entry, dev_reg, client, node) # run discovery on all node values and create/update entities for disc_info in async_discover_values(node): - LOGGER.debug("Discovered entity: %s", disc_info) - # This migration logic was added in 2021.3 to handle a breaking change to # the value_id format. Some time in the future, this call (as well as the # helper functions) can be removed. async_migrate_discovered_value(ent_reg, client, disc_info) + if disc_info.platform not in platform_setup_tasks: + platform_setup_tasks[disc_info.platform] = hass.async_create_task( + hass.config_entries.async_forward_entry_setup( + entry, disc_info.platform + ) + ) + + await platform_setup_tasks[disc_info.platform] + + LOGGER.debug("Discovered entity: %s", disc_info) async_dispatcher_send( hass, f"{DOMAIN}_{entry.entry_id}_add_{disc_info.platform}", disc_info ) + # add listener for stateless node value notification events - node.on( - "value notification", - lambda event: async_on_value_notification(event["value_notification"]), + unsubscribe_callbacks.append( + node.on( + "value notification", + lambda event: async_on_value_notification(event["value_notification"]), + ) ) # add listener for stateless node notification events - node.on( - "notification", lambda event: async_on_notification(event["notification"]) + unsubscribe_callbacks.append( + node.on( + "notification", + lambda event: async_on_notification(event["notification"]), + ) ) - @callback - def async_on_node_added(node: ZwaveNode) -> None: + async def async_on_node_added(node: ZwaveNode) -> None: """Handle node added event.""" # we only want to run discovery when the node has reached ready state, # otherwise we'll have all kinds of missing info issues. if node.ready: - async_on_node_ready(node) + await async_on_node_ready(node) return # if node is not yet ready, register one-time callback for ready state LOGGER.debug("Node added: %s - waiting for it to become ready", node.node_id) node.once( "ready", - lambda event: async_on_node_ready(event["node"]), + lambda event: hass.async_create_task(async_on_node_ready(event["node"])), ) # we do submit the node to device registry so user has # some visual feedback that something is (in the process of) being added @@ -234,7 +254,6 @@ def async_on_notification( hass.bus.async_fire(ZWAVE_JS_NOTIFICATION_EVENT, event_data) - entry_hass_data: dict = hass.data[DOMAIN].setdefault(entry.entry_id, {}) # connect and throw error if connection failed try: async with timeout(CONNECT_TIMEOUT): @@ -256,10 +275,6 @@ def async_on_notification( entry_hass_data[DATA_CONNECT_FAILED_LOGGED] = False entry_hass_data[DATA_INVALID_SERVER_VERSION_LOGGED] = False - unsubscribe_callbacks: list[Callable] = [] - entry_hass_data[DATA_CLIENT] = client - entry_hass_data[DATA_UNSUBSCRIBE] = unsubscribe_callbacks - services = ZWaveServices(hass, ent_reg) services.async_register() @@ -268,14 +283,6 @@ def async_on_notification( async def start_platforms() -> None: """Start platforms and perform discovery.""" - # wait until all required platforms are ready - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_setup(entry, platform) - for platform in PLATFORMS - ] - ) - driver_ready = asyncio.Event() async def handle_ha_shutdown(event: Event) -> None: @@ -313,17 +320,28 @@ async def handle_ha_shutdown(event: Event) -> None: dev_reg.async_remove_device(device.id) # run discovery on all ready nodes - for node in client.driver.controller.nodes.values(): - async_on_node_added(node) + await asyncio.gather( + *[ + async_on_node_added(node) + for node in client.driver.controller.nodes.values() + ] + ) # listen for new nodes being added to the mesh - client.driver.controller.on( - "node added", lambda event: async_on_node_added(event["node"]) + unsubscribe_callbacks.append( + client.driver.controller.on( + "node added", + lambda event: hass.async_create_task( + async_on_node_added(event["node"]) + ), + ) ) # listen for nodes being removed from the mesh # NOTE: This will not remove nodes that were removed when HA was not running - client.driver.controller.on( - "node removed", lambda event: async_on_node_removed(event["node"]) + unsubscribe_callbacks.append( + client.driver.controller.on( + "node removed", lambda event: async_on_node_removed(event["node"]) + ) ) platform_task = hass.async_create_task(start_platforms()) @@ -355,7 +373,7 @@ async def client_listen( # All model instances will be replaced when the new state is acquired. if should_reload: LOGGER.info("Disconnected from server. Reloading integration") - asyncio.create_task(hass.config_entries.async_reload(entry.entry_id)) + hass.async_create_task(hass.config_entries.async_reload(entry.entry_id)) async def disconnect_client( @@ -368,8 +386,13 @@ async def disconnect_client( """Disconnect client.""" listen_task.cancel() platform_task.cancel() + platform_setup_tasks = ( + hass.data[DOMAIN].get(entry.entry_id, {}).get(DATA_PLATFORM_SETUP, {}).values() + ) + for task in platform_setup_tasks: + task.cancel() - await asyncio.gather(listen_task, platform_task) + await asyncio.gather(listen_task, platform_task, *platform_setup_tasks) if client.connected: await client.disconnect() @@ -378,22 +401,23 @@ async def disconnect_client( async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) - if not unload_ok: - return False - info = hass.data[DOMAIN].pop(entry.entry_id) for unsub in info[DATA_UNSUBSCRIBE]: unsub() + tasks = [] + for platform, task in info[DATA_PLATFORM_SETUP].items(): + if task.done(): + tasks.append( + hass.config_entries.async_forward_entry_unload(entry, platform) + ) + else: + task.cancel() + tasks.append(task) + + unload_ok = all(await asyncio.gather(*tasks)) + if DATA_CLIENT_LISTEN_TASK in info: await disconnect_client( hass, @@ -412,7 +436,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: LOGGER.error("Failed to stop the Z-Wave JS add-on: %s", err) return False - return True + return unload_ok async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index 1c9f78b175166..afd899e0ee06a 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -8,20 +8,10 @@ CONF_USB_PATH = "usb_path" CONF_USE_ADDON = "use_addon" DOMAIN = "zwave_js" -PLATFORMS = [ - "binary_sensor", - "climate", - "cover", - "fan", - "light", - "lock", - "number", - "sensor", - "switch", -] DATA_CLIENT = "client" DATA_UNSUBSCRIBE = "unsubs" +DATA_PLATFORM_SETUP = "platform_setup" EVENT_DEVICE_ADDED_TO_REGISTRY = f"{DOMAIN}_device_added_to_registry" From 5bf3469ffc08353b128863a082270c0aed14f3e0 Mon Sep 17 00:00:00 2001 From: Mike O'Driscoll Date: Mon, 12 Apr 2021 20:32:36 -0400 Subject: [PATCH 0230/1317] ZHA support Quotra LED On quirk (#49137) The Quotra-Vision QV-RGBCCT doesn't support the move_to_level_with_onoff command in ZCL spec. Force on with this device. --- homeassistant/components/zha/light.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 6701a9bb3c78d..9a74a23fc2e15 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -533,7 +533,7 @@ class HueLight(Light): @STRICT_MATCH( channel_names=CHANNEL_ON_OFF, aux_channels={CHANNEL_COLOR, CHANNEL_LEVEL}, - manufacturers="Jasco", + manufacturers={"Jasco", "Quotra-Vision"}, ) class ForceOnLight(Light): """Representation of a light which does not respect move_to_level_with_on_off.""" From 63d42867e83644c08ca87e68d32f95dd0a8517d0 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Tue, 13 Apr 2021 01:35:38 -0700 Subject: [PATCH 0231/1317] Add Hyperion device support (#47881) * Add Hyperion device support. * Update to the new typing annotations. * Add device cleanup logic. * Fixes based on the excellent feedback from emontnemery --- homeassistant/components/hyperion/__init__.py | 35 ++-- homeassistant/components/hyperion/const.py | 2 + homeassistant/components/hyperion/light.py | 82 ++++++--- homeassistant/components/hyperion/switch.py | 79 +++++---- tests/components/hyperion/__init__.py | 21 ++- tests/components/hyperion/test_config_flow.py | 9 +- tests/components/hyperion/test_light.py | 132 ++++++++++++--- tests/components/hyperion/test_switch.py | 156 ++++++++++++++---- 8 files changed, 388 insertions(+), 128 deletions(-) diff --git a/homeassistant/components/hyperion/__init__.py b/homeassistant/components/hyperion/__init__.py index 93f3c35f5145e..0aa94e13cac48 100644 --- a/homeassistant/components/hyperion/__init__.py +++ b/homeassistant/components/hyperion/__init__.py @@ -15,14 +15,11 @@ from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.entity_registry import ( - async_entries_for_config_entry, - async_get_registry, -) from homeassistant.helpers.typing import ConfigType, HomeAssistantType from .const import ( @@ -72,6 +69,11 @@ def get_hyperion_unique_id(server_id: str, instance: int, name: str) -> str: return f"{server_id}_{instance}_{name}" +def get_hyperion_device_id(server_id: str, instance: int) -> str: + """Get an id for a Hyperion device/instance.""" + return f"{server_id}_{instance}" + + def split_hyperion_unique_id(unique_id: str) -> tuple[str, int, str] | None: """Split a unique_id into a (server_id, instance, type) tuple.""" data = tuple(unique_id.split("_", 2)) @@ -202,7 +204,7 @@ async def async_instances_to_clients(response: dict[str, Any]) -> None: async def async_instances_to_clients_raw(instances: list[dict[str, Any]]) -> None: """Convert instances to Hyperion clients.""" - registry = await async_get_registry(hass) + device_registry = dr.async_get(hass) running_instances: set[int] = set() stopped_instances: set[int] = set() existing_instances = hass.data[DOMAIN][config_entry.entry_id][ @@ -249,15 +251,20 @@ async def async_instances_to_clients_raw(instances: list[dict[str, Any]]) -> Non hass, SIGNAL_INSTANCE_REMOVE.format(config_entry.entry_id), instance_num ) - # Deregister entities that belong to removed instances. - for entry in async_entries_for_config_entry(registry, config_entry.entry_id): - data = split_hyperion_unique_id(entry.unique_id) - if not data: - continue - if data[0] == server_id and ( - data[1] not in running_instances and data[1] not in stopped_instances - ): - registry.async_remove(entry.entity_id) + # Ensure every device associated with this config entry is still in the list of + # motionEye cameras, otherwise remove the device (and thus entities). + known_devices = { + get_hyperion_device_id(server_id, instance_num) + for instance_num in running_instances | stopped_instances + } + for device_entry in dr.async_entries_for_config_entry( + device_registry, config_entry.entry_id + ): + for (kind, key) in device_entry.identifiers: + if kind == DOMAIN and key in known_devices: + break + else: + device_registry.async_remove_device(device_entry.id) hyperion_client.set_callbacks( { diff --git a/homeassistant/components/hyperion/const.py b/homeassistant/components/hyperion/const.py index 994ef580c9153..87600f7c27bcf 100644 --- a/homeassistant/components/hyperion/const.py +++ b/homeassistant/components/hyperion/const.py @@ -40,6 +40,8 @@ DOMAIN = "hyperion" +HYPERION_MANUFACTURER_NAME = "Hyperion" +HYPERION_MODEL_NAME = f"{HYPERION_MANUFACTURER_NAME}-NG" HYPERION_RELEASES_URL = "https://github.com/hyperion-project/hyperion.ng/releases" HYPERION_VERSION_WARN_CUTOFF = "2.0.0-alpha.9" diff --git a/homeassistant/components/hyperion/light.py b/homeassistant/components/hyperion/light.py index 248a45ec753bd..5ab74f1141be2 100644 --- a/homeassistant/components/hyperion/light.py +++ b/homeassistant/components/hyperion/light.py @@ -26,7 +26,11 @@ from homeassistant.helpers.typing import HomeAssistantType import homeassistant.util.color as color_util -from . import get_hyperion_unique_id, listen_for_instance_updates +from . import ( + get_hyperion_device_id, + get_hyperion_unique_id, + listen_for_instance_updates, +) from .const import ( CONF_EFFECT_HIDE_LIST, CONF_INSTANCE_CLIENTS, @@ -34,6 +38,8 @@ DEFAULT_ORIGIN, DEFAULT_PRIORITY, DOMAIN, + HYPERION_MANUFACTURER_NAME, + HYPERION_MODEL_NAME, NAME_SUFFIX_HYPERION_LIGHT, NAME_SUFFIX_HYPERION_PRIORITY_LIGHT, SIGNAL_ENTITY_REMOVE, @@ -85,24 +91,17 @@ async def async_setup_entry( def instance_add(instance_num: int, instance_name: str) -> None: """Add entities for a new Hyperion instance.""" assert server_id + args = ( + server_id, + instance_num, + instance_name, + config_entry.options, + entry_data[CONF_INSTANCE_CLIENTS][instance_num], + ) async_add_entities( [ - HyperionLight( - get_hyperion_unique_id( - server_id, instance_num, TYPE_HYPERION_LIGHT - ), - f"{instance_name} {NAME_SUFFIX_HYPERION_LIGHT}", - config_entry.options, - entry_data[CONF_INSTANCE_CLIENTS][instance_num], - ), - HyperionPriorityLight( - get_hyperion_unique_id( - server_id, instance_num, TYPE_HYPERION_PRIORITY_LIGHT - ), - f"{instance_name} {NAME_SUFFIX_HYPERION_PRIORITY_LIGHT}", - config_entry.options, - entry_data[CONF_INSTANCE_CLIENTS][instance_num], - ), + HyperionLight(*args), + HyperionPriorityLight(*args), ] ) @@ -127,14 +126,17 @@ class HyperionBaseLight(LightEntity): def __init__( self, - unique_id: str, - name: str, + server_id: str, + instance_num: int, + instance_name: str, options: MappingProxyType[str, Any], hyperion_client: client.HyperionClient, ) -> None: """Initialize the light.""" - self._unique_id = unique_id - self._name = name + self._unique_id = self._compute_unique_id(server_id, instance_num) + self._name = self._compute_name(instance_name) + self._device_id = get_hyperion_device_id(server_id, instance_num) + self._instance_name = instance_name self._options = options self._client = hyperion_client @@ -156,6 +158,14 @@ def __init__( f"{const.KEY_CLIENT}-{const.KEY_UPDATE}": self._update_client, } + def _compute_unique_id(self, server_id: str, instance_num: int) -> str: + """Compute a unique id for this instance.""" + raise NotImplementedError + + def _compute_name(self, instance_name: str) -> str: + """Compute the name of the light.""" + raise NotImplementedError + @property def entity_registry_enabled_default(self) -> bool: """Whether or not the entity is enabled by default.""" @@ -216,6 +226,16 @@ def unique_id(self) -> str: """Return a unique id for this instance.""" return self._unique_id + @property + def device_info(self) -> dict[str, Any] | None: + """Return device information.""" + return { + "identifiers": {(DOMAIN, self._device_id)}, + "name": self._instance_name, + "manufacturer": HYPERION_MANUFACTURER_NAME, + "model": HYPERION_MODEL_NAME, + } + def _get_option(self, key: str) -> Any: """Get a value from the provided options.""" defaults = { @@ -412,7 +432,7 @@ async def async_added_to_hass(self) -> None: self.async_on_remove( async_dispatcher_connect( self.hass, - SIGNAL_ENTITY_REMOVE.format(self._unique_id), + SIGNAL_ENTITY_REMOVE.format(self.unique_id), functools.partial(self.async_remove, force_remove=True), ) ) @@ -455,6 +475,14 @@ class HyperionLight(HyperionBaseLight): shown state rather than exclusively the HA priority. """ + def _compute_unique_id(self, server_id: str, instance_num: int) -> str: + """Compute a unique id for this instance.""" + return get_hyperion_unique_id(server_id, instance_num, TYPE_HYPERION_LIGHT) + + def _compute_name(self, instance_name: str) -> str: + """Compute the name of the light.""" + return f"{instance_name} {NAME_SUFFIX_HYPERION_LIGHT}".strip() + @property def is_on(self) -> bool: """Return true if light is on.""" @@ -504,6 +532,16 @@ async def async_turn_off(self, **kwargs: Any) -> None: class HyperionPriorityLight(HyperionBaseLight): """A Hyperion light that only acts on a single Hyperion priority.""" + def _compute_unique_id(self, server_id: str, instance_num: int) -> str: + """Compute a unique id for this instance.""" + return get_hyperion_unique_id( + server_id, instance_num, TYPE_HYPERION_PRIORITY_LIGHT + ) + + def _compute_name(self, instance_name: str) -> str: + """Compute the name of the light.""" + return f"{instance_name} {NAME_SUFFIX_HYPERION_PRIORITY_LIGHT}".strip() + @property def entity_registry_enabled_default(self) -> bool: """Whether or not the entity is enabled by default.""" diff --git a/homeassistant/components/hyperion/switch.py b/homeassistant/components/hyperion/switch.py index 4a4f8d4da135d..dce92df6f3573 100644 --- a/homeassistant/components/hyperion/switch.py +++ b/homeassistant/components/hyperion/switch.py @@ -33,11 +33,17 @@ from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import slugify -from . import get_hyperion_unique_id, listen_for_instance_updates +from . import ( + get_hyperion_device_id, + get_hyperion_unique_id, + listen_for_instance_updates, +) from .const import ( COMPONENT_TO_NAME, CONF_INSTANCE_CLIENTS, DOMAIN, + HYPERION_MANUFACTURER_NAME, + HYPERION_MODEL_NAME, NAME_SUFFIX_HYPERION_COMPONENT_SWITCH, SIGNAL_ENTITY_REMOVE, TYPE_HYPERION_COMPONENT_SWITCH_BASE, @@ -55,6 +61,26 @@ ] +def _component_to_unique_id(server_id: str, component: str, instance_num: int) -> str: + """Convert a component to a unique_id.""" + return get_hyperion_unique_id( + server_id, + instance_num, + slugify( + f"{TYPE_HYPERION_COMPONENT_SWITCH_BASE} {COMPONENT_TO_NAME[component]}" + ), + ) + + +def _component_to_switch_name(component: str, instance_name: str) -> str: + """Convert a component to a switch name.""" + return ( + f"{instance_name} " + f"{NAME_SUFFIX_HYPERION_COMPONENT_SWITCH} " + f"{COMPONENT_TO_NAME.get(component, component.capitalize())}" + ) + + async def async_setup_entry( hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable ) -> bool: @@ -62,27 +88,6 @@ async def async_setup_entry( entry_data = hass.data[DOMAIN][config_entry.entry_id] server_id = config_entry.unique_id - def component_to_switch_type(component: str) -> str: - """Convert a component to a switch type string.""" - return slugify( - f"{TYPE_HYPERION_COMPONENT_SWITCH_BASE} {COMPONENT_TO_NAME[component]}" - ) - - def component_to_unique_id(component: str, instance_num: int) -> str: - """Convert a component to a unique_id.""" - assert server_id - return get_hyperion_unique_id( - server_id, instance_num, component_to_switch_type(component) - ) - - def component_to_switch_name(component: str, instance_name: str) -> str: - """Convert a component to a switch name.""" - return ( - f"{instance_name} " - f"{NAME_SUFFIX_HYPERION_COMPONENT_SWITCH} " - f"{COMPONENT_TO_NAME.get(component, component.capitalize())}" - ) - @callback def instance_add(instance_num: int, instance_name: str) -> None: """Add entities for a new Hyperion instance.""" @@ -91,8 +96,9 @@ def instance_add(instance_num: int, instance_name: str) -> None: for component in COMPONENT_SWITCHES: switches.append( HyperionComponentSwitch( - component_to_unique_id(component, instance_num), - component_to_switch_name(component, instance_name), + server_id, + instance_num, + instance_name, component, entry_data[CONF_INSTANCE_CLIENTS][instance_num], ), @@ -107,7 +113,7 @@ def instance_remove(instance_num: int) -> None: async_dispatcher_send( hass, SIGNAL_ENTITY_REMOVE.format( - component_to_unique_id(component, instance_num), + _component_to_unique_id(server_id, component, instance_num), ), ) @@ -120,14 +126,19 @@ class HyperionComponentSwitch(SwitchEntity): def __init__( self, - unique_id: str, - name: str, + server_id: str, + instance_num: int, + instance_name: str, component_name: str, hyperion_client: client.HyperionClient, ) -> None: """Initialize the switch.""" - self._unique_id = unique_id - self._name = name + self._unique_id = _component_to_unique_id( + server_id, component_name, instance_num + ) + self._device_id = get_hyperion_device_id(server_id, instance_num) + self._name = _component_to_switch_name(component_name, instance_name) + self._instance_name = instance_name self._component_name = component_name self._client = hyperion_client self._client_callbacks = { @@ -168,6 +179,16 @@ def available(self) -> bool: """Return server availability.""" return bool(self._client.has_loaded_state) + @property + def device_info(self) -> dict[str, Any] | None: + """Return device information.""" + return { + "identifiers": {(DOMAIN, self._device_id)}, + "name": self._instance_name, + "manufacturer": HYPERION_MANUFACTURER_NAME, + "model": HYPERION_MODEL_NAME, + } + async def _async_send_set_component(self, value: bool) -> None: """Send a component control request.""" await self._client.async_send_set_component( diff --git a/tests/components/hyperion/__init__.py b/tests/components/hyperion/__init__.py index d0653f88b8359..7938527a12d0c 100644 --- a/tests/components/hyperion/__init__.py +++ b/tests/components/hyperion/__init__.py @@ -7,9 +7,11 @@ from hyperion import const +from homeassistant.components.hyperion import get_hyperion_unique_id from homeassistant.components.hyperion.const import CONF_PRIORITY, DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.typing import HomeAssistantType from tests.common import MockConfigEntry @@ -20,7 +22,7 @@ TEST_INSTANCE = 1 TEST_ID = "default" TEST_SYSINFO_ID = "f9aab089-f85a-55cf-b7c1-222a72faebe9" -TEST_SYSINFO_VERSION = "2.0.0-alpha.8" +TEST_SYSINFO_VERSION = "2.0.0-alpha.9" TEST_PRIORITY = 180 TEST_ENTITY_ID_1 = "light.test_instance_1" TEST_ENTITY_ID_2 = "light.test_instance_2" @@ -168,3 +170,20 @@ def call_registered_callback( for call in client.add_callbacks.call_args_list: if key in call[0][0]: call[0][0][key](*args, **kwargs) + + +def register_test_entity( + hass: HomeAssistantType, domain: str, type_name: str, entity_id: str +) -> None: + """Register a test entity.""" + unique_id = get_hyperion_unique_id(TEST_SYSINFO_ID, TEST_INSTANCE, type_name) + entity_id = entity_id.split(".")[1] + + entity_registry = er.async_get(hass) + entity_registry.async_get_or_create( + domain, + DOMAIN, + unique_id, + suggested_object_id=entity_id, + disabled_by=None, + ) diff --git a/tests/components/hyperion/test_config_flow.py b/tests/components/hyperion/test_config_flow.py index 7cf0556eddf2f..381dc018407ec 100644 --- a/tests/components/hyperion/test_config_flow.py +++ b/tests/components/hyperion/test_config_flow.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from typing import Any +from typing import Any, Awaitable from unittest.mock import AsyncMock, Mock, patch from hyperion import const @@ -419,13 +419,13 @@ async def test_auth_create_token_approval_declined_task_canceled( class CanceledAwaitableMock(AsyncMock): """A canceled awaitable mock.""" - def __await__(self): + def __await__(self) -> None: raise asyncio.CancelledError mock_task = CanceledAwaitableMock() - task_coro = None + task_coro: Awaitable | None = None - def create_task(arg): + def create_task(arg: Any) -> CanceledAwaitableMock: nonlocal task_coro task_coro = arg return mock_task @@ -453,6 +453,7 @@ def create_task(arg): result = await _configure_flow(hass, result) # This await will advance to the next step. + assert task_coro await task_coro # Assert that cancel is called on the task. diff --git a/tests/components/hyperion/test_light.py b/tests/components/hyperion/test_light.py index 505896fbe0789..a774a5ba86812 100644 --- a/tests/components/hyperion/test_light.py +++ b/tests/components/hyperion/test_light.py @@ -1,15 +1,22 @@ """Tests for the Hyperion integration.""" from __future__ import annotations +from datetime import timedelta from unittest.mock import AsyncMock, Mock, call, patch from hyperion import const -from homeassistant.components.hyperion import light as hyperion_light +from homeassistant.components.hyperion import ( + get_hyperion_device_id, + light as hyperion_light, +) from homeassistant.components.hyperion.const import ( CONF_EFFECT_HIDE_LIST, DEFAULT_ORIGIN, DOMAIN, + HYPERION_MANUFACTURER_NAME, + HYPERION_MODEL_NAME, + TYPE_HYPERION_PRIORITY_LIGHT, ) from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -19,6 +26,7 @@ ) from homeassistant.config_entries import ( ENTRY_STATE_SETUP_ERROR, + RELOAD_AFTER_UPDATE_DELAY, SOURCE_REAUTH, ConfigEntry, ) @@ -31,18 +39,21 @@ SERVICE_TURN_OFF, SERVICE_TURN_ON, ) -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.util import dt import homeassistant.util.color as color_util from . import ( TEST_AUTH_NOT_REQUIRED_RESP, TEST_AUTH_REQUIRED_RESP, + TEST_CONFIG_ENTRY_ID, TEST_ENTITY_ID_1, TEST_ENTITY_ID_2, TEST_ENTITY_ID_3, TEST_HOST, TEST_ID, + TEST_INSTANCE, TEST_INSTANCE_1, TEST_INSTANCE_2, TEST_INSTANCE_3, @@ -53,9 +64,12 @@ add_test_config_entry, call_registered_callback, create_mock_client, + register_test_entity, setup_test_config_entry, ) +from tests.common import async_fire_time_changed + COLOR_BLACK = color_util.COLORS["black"] @@ -814,11 +828,13 @@ async def test_priority_light_async_updates( client = create_mock_client() client.priorities = [{**priority_template}] - with patch( - "homeassistant.components.hyperion.light.HyperionPriorityLight.entity_registry_enabled_default" - ) as enabled_by_default_mock: - enabled_by_default_mock.return_value = True - await setup_test_config_entry(hass, hyperion_client=client) + register_test_entity( + hass, + LIGHT_DOMAIN, + TYPE_HYPERION_PRIORITY_LIGHT, + TEST_PRIORITY_LIGHT_ENTITY_ID_1, + ) + await setup_test_config_entry(hass, hyperion_client=client) # == Scenario: Color at HA priority will show light as on. entity_state = hass.states.get(TEST_PRIORITY_LIGHT_ENTITY_ID_1) @@ -974,11 +990,13 @@ async def test_priority_light_async_updates_off_sets_black( } ] - with patch( - "homeassistant.components.hyperion.light.HyperionPriorityLight.entity_registry_enabled_default" - ) as enabled_by_default_mock: - enabled_by_default_mock.return_value = True - await setup_test_config_entry(hass, hyperion_client=client) + register_test_entity( + hass, + LIGHT_DOMAIN, + TYPE_HYPERION_PRIORITY_LIGHT, + TEST_PRIORITY_LIGHT_ENTITY_ID_1, + ) + await setup_test_config_entry(hass, hyperion_client=client) client.async_send_clear = AsyncMock(return_value=True) client.async_send_set_color = AsyncMock(return_value=True) @@ -1026,11 +1044,13 @@ async def test_priority_light_prior_color_preserved_after_black( client.priorities = [] client.visible_priority = None - with patch( - "homeassistant.components.hyperion.light.HyperionPriorityLight.entity_registry_enabled_default" - ) as enabled_by_default_mock: - enabled_by_default_mock.return_value = True - await setup_test_config_entry(hass, hyperion_client=client) + register_test_entity( + hass, + LIGHT_DOMAIN, + TYPE_HYPERION_PRIORITY_LIGHT, + TEST_PRIORITY_LIGHT_ENTITY_ID_1, + ) + await setup_test_config_entry(hass, hyperion_client=client) # Turn the light on full green... # On (=), 100% (=), solid (=), [0,0,255] (=) @@ -1132,11 +1152,13 @@ async def test_priority_light_has_no_external_sources(hass: HomeAssistantType) - client = create_mock_client() client.priorities = [] - with patch( - "homeassistant.components.hyperion.light.HyperionPriorityLight.entity_registry_enabled_default" - ) as enabled_by_default_mock: - enabled_by_default_mock.return_value = True - await setup_test_config_entry(hass, hyperion_client=client) + register_test_entity( + hass, + LIGHT_DOMAIN, + TYPE_HYPERION_PRIORITY_LIGHT, + TEST_PRIORITY_LIGHT_ENTITY_ID_1, + ) + await setup_test_config_entry(hass, hyperion_client=client) entity_state = hass.states.get(TEST_PRIORITY_LIGHT_ENTITY_ID_1) assert entity_state @@ -1153,9 +1175,75 @@ async def test_light_option_effect_hide_list(hass: HomeAssistantType) -> None: ) entity_state = hass.states.get(TEST_ENTITY_ID_1) + assert entity_state assert entity_state.attributes["effect_list"] == [ "Solid", "BOBLIGHTSERVER", "GRABBER", "One", ] + + +async def test_device_info(hass: HomeAssistantType) -> None: + """Verify device information includes expected details.""" + client = create_mock_client() + + register_test_entity( + hass, + LIGHT_DOMAIN, + TYPE_HYPERION_PRIORITY_LIGHT, + TEST_PRIORITY_LIGHT_ENTITY_ID_1, + ) + await setup_test_config_entry(hass, hyperion_client=client) + + device_id = get_hyperion_device_id(TEST_SYSINFO_ID, TEST_INSTANCE) + device_registry = dr.async_get(hass) + + device = device_registry.async_get_device({(DOMAIN, device_id)}) + assert device + assert device.config_entries == {TEST_CONFIG_ENTRY_ID} + assert device.identifiers == {(DOMAIN, device_id)} + assert device.manufacturer == HYPERION_MANUFACTURER_NAME + assert device.model == HYPERION_MODEL_NAME + assert device.name == TEST_INSTANCE_1["friendly_name"] + + entity_registry = await er.async_get_registry(hass) + entities_from_device = [ + entry.entity_id + for entry in er.async_entries_for_device(entity_registry, device.id) + ] + assert TEST_PRIORITY_LIGHT_ENTITY_ID_1 in entities_from_device + assert TEST_ENTITY_ID_1 in entities_from_device + + +async def test_lights_can_be_enabled(hass: HomeAssistantType) -> None: + """Verify lights can be enabled.""" + client = create_mock_client() + await setup_test_config_entry(hass, hyperion_client=client) + + entity_registry = er.async_get(hass) + entry = entity_registry.async_get(TEST_PRIORITY_LIGHT_ENTITY_ID_1) + assert entry + assert entry.disabled + assert entry.disabled_by == "integration" + entity_state = hass.states.get(TEST_PRIORITY_LIGHT_ENTITY_ID_1) + assert not entity_state + + with patch( + "homeassistant.components.hyperion.client.HyperionClient", + return_value=client, + ): + updated_entry = entity_registry.async_update_entity( + TEST_PRIORITY_LIGHT_ENTITY_ID_1, disabled_by=None + ) + assert not updated_entry.disabled + await hass.async_block_till_done() + + async_fire_time_changed( # type: ignore[no-untyped-call] + hass, + dt.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), + ) + await hass.async_block_till_done() + + entity_state = hass.states.get(TEST_PRIORITY_LIGHT_ENTITY_ID_1) + assert entity_state diff --git a/tests/components/hyperion/test_switch.py b/tests/components/hyperion/test_switch.py index 34030787e20b3..af1336bf0f86d 100644 --- a/tests/components/hyperion/test_switch.py +++ b/tests/components/hyperion/test_switch.py @@ -1,27 +1,41 @@ """Tests for the Hyperion integration.""" +from datetime import timedelta from unittest.mock import AsyncMock, call, patch from hyperion.const import ( KEY_COMPONENT, KEY_COMPONENTID_ALL, - KEY_COMPONENTID_BLACKBORDER, - KEY_COMPONENTID_BOBLIGHTSERVER, - KEY_COMPONENTID_FORWARDER, - KEY_COMPONENTID_GRABBER, - KEY_COMPONENTID_LEDDEVICE, - KEY_COMPONENTID_SMOOTHING, - KEY_COMPONENTID_V4L, KEY_COMPONENTSTATE, KEY_STATE, ) -from homeassistant.components.hyperion.const import COMPONENT_TO_NAME +from homeassistant.components.hyperion import get_hyperion_device_id +from homeassistant.components.hyperion.const import ( + COMPONENT_TO_NAME, + DOMAIN, + HYPERION_MANUFACTURER_NAME, + HYPERION_MODEL_NAME, + TYPE_HYPERION_COMPONENT_SWITCH_BASE, +) from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.typing import HomeAssistantType -from homeassistant.util import slugify +from homeassistant.util import dt, slugify + +from . import ( + TEST_CONFIG_ENTRY_ID, + TEST_INSTANCE, + TEST_INSTANCE_1, + TEST_SYSINFO_ID, + call_registered_callback, + create_mock_client, + register_test_entity, + setup_test_config_entry, +) -from . import call_registered_callback, create_mock_client, setup_test_config_entry +from tests.common import async_fire_time_changed TEST_COMPONENTS = [ {"enabled": True, "name": "ALL"}, @@ -45,11 +59,13 @@ async def test_switch_turn_on_off(hass: HomeAssistantType) -> None: client.components = TEST_COMPONENTS # Setup component switch. - with patch( - "homeassistant.components.hyperion.switch.HyperionComponentSwitch.entity_registry_enabled_default" - ) as enabled_by_default_mock: - enabled_by_default_mock.return_value = True - await setup_test_config_entry(hass, hyperion_client=client) + register_test_entity( + hass, + SWITCH_DOMAIN, + f"{TYPE_HYPERION_COMPONENT_SWITCH_BASE}_all", + TEST_SWITCH_COMPONENT_ALL_ENTITY_ID, + ) + await setup_test_config_entry(hass, hyperion_client=client) # Verify switch is on (as per TEST_COMPONENTS above). entity_state = hass.states.get(TEST_SWITCH_COMPONENT_ALL_ENTITY_ID) @@ -111,28 +127,96 @@ async def test_switch_has_correct_entities(hass: HomeAssistantType) -> None: client.components = TEST_COMPONENTS # Setup component switch. - with patch( - "homeassistant.components.hyperion.switch.HyperionComponentSwitch.entity_registry_enabled_default" - ) as enabled_by_default_mock: - enabled_by_default_mock.return_value = True - await setup_test_config_entry(hass, hyperion_client=client) + for component in TEST_COMPONENTS: + name = slugify(COMPONENT_TO_NAME[str(component["name"])]) + register_test_entity( + hass, + SWITCH_DOMAIN, + f"{TYPE_HYPERION_COMPONENT_SWITCH_BASE}_{name}", + f"{TEST_SWITCH_COMPONENT_BASE_ENTITY_ID}_{name}", + ) + await setup_test_config_entry(hass, hyperion_client=client) - entity_state = hass.states.get(TEST_SWITCH_COMPONENT_ALL_ENTITY_ID) + for component in TEST_COMPONENTS: + name = slugify(COMPONENT_TO_NAME[str(component["name"])]) + entity_id = TEST_SWITCH_COMPONENT_BASE_ENTITY_ID + "_" + name + entity_state = hass.states.get(entity_id) + assert entity_state, f"Couldn't find entity: {entity_id}" + + +async def test_device_info(hass: HomeAssistantType) -> None: + """Verify device information includes expected details.""" + client = create_mock_client() + client.components = TEST_COMPONENTS - for component in ( - KEY_COMPONENTID_ALL, - KEY_COMPONENTID_SMOOTHING, - KEY_COMPONENTID_BLACKBORDER, - KEY_COMPONENTID_FORWARDER, - KEY_COMPONENTID_BOBLIGHTSERVER, - KEY_COMPONENTID_GRABBER, - KEY_COMPONENTID_LEDDEVICE, - KEY_COMPONENTID_V4L, - ): - entity_id = ( - TEST_SWITCH_COMPONENT_BASE_ENTITY_ID - + "_" - + slugify(COMPONENT_TO_NAME[component]) + for component in TEST_COMPONENTS: + name = slugify(COMPONENT_TO_NAME[str(component["name"])]) + register_test_entity( + hass, + SWITCH_DOMAIN, + f"{TYPE_HYPERION_COMPONENT_SWITCH_BASE}_{name}", + f"{TEST_SWITCH_COMPONENT_BASE_ENTITY_ID}_{name}", ) + await setup_test_config_entry(hass, hyperion_client=client) + assert hass.states.get(TEST_SWITCH_COMPONENT_ALL_ENTITY_ID) is not None + + device_identifer = get_hyperion_device_id(TEST_SYSINFO_ID, TEST_INSTANCE) + device_registry = dr.async_get(hass) + + device = device_registry.async_get_device({(DOMAIN, device_identifer)}) + assert device + assert device.config_entries == {TEST_CONFIG_ENTRY_ID} + assert device.identifiers == {(DOMAIN, device_identifer)} + assert device.manufacturer == HYPERION_MANUFACTURER_NAME + assert device.model == HYPERION_MODEL_NAME + assert device.name == TEST_INSTANCE_1["friendly_name"] + + entity_registry = await er.async_get_registry(hass) + entities_from_device = [ + entry.entity_id + for entry in er.async_entries_for_device(entity_registry, device.id) + ] + + for component in TEST_COMPONENTS: + name = slugify(COMPONENT_TO_NAME[str(component["name"])]) + entity_id = TEST_SWITCH_COMPONENT_BASE_ENTITY_ID + "_" + name + assert entity_id in entities_from_device + + +async def test_switches_can_be_enabled(hass: HomeAssistantType) -> None: + """Verify switches can be enabled.""" + client = create_mock_client() + client.components = TEST_COMPONENTS + await setup_test_config_entry(hass, hyperion_client=client) + + entity_registry = er.async_get(hass) + + for component in TEST_COMPONENTS: + name = slugify(COMPONENT_TO_NAME[str(component["name"])]) + entity_id = TEST_SWITCH_COMPONENT_BASE_ENTITY_ID + "_" + name + + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.disabled + assert entry.disabled_by == "integration" entity_state = hass.states.get(entity_id) - assert entity_state, f"Couldn't find entity: {entity_id}" + assert not entity_state + + with patch( + "homeassistant.components.hyperion.client.HyperionClient", + return_value=client, + ): + updated_entry = entity_registry.async_update_entity( + entity_id, disabled_by=None + ) + assert not updated_entry.disabled + await hass.async_block_till_done() + + async_fire_time_changed( # type: ignore[no-untyped-call] + hass, + dt.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), + ) + await hass.async_block_till_done() + + entity_state = hass.states.get(entity_id) + assert entity_state From 4ce6d00a2279ca00306ba03bca4a218f90ff24a3 Mon Sep 17 00:00:00 2001 From: Clifford Roche Date: Tue, 13 Apr 2021 05:54:03 -0400 Subject: [PATCH 0232/1317] Improve the discovery process for Gree (#45449) * Add support for async device discovery * FIx missing dispatcher cleanup breaking integration reload * Update homeassistant/components/gree/climate.py Co-authored-by: Erik Montnemery * Update homeassistant/components/gree/switch.py Co-authored-by: Erik Montnemery * Update homeassistant/components/gree/bridge.py Co-authored-by: Erik Montnemery * Working on feedback * Improving load/unload tests * Update homeassistant/components/gree/__init__.py Co-authored-by: Erik Montnemery * Working on more feedback * Add tests covering async discovery scenarios * Remove unnecessary shutdown * Update homeassistant/components/gree/__init__.py Co-authored-by: Erik Montnemery * Code refactor from reviews Co-authored-by: Erik Montnemery --- homeassistant/components/gree/__init__.py | 66 ++++---- homeassistant/components/gree/bridge.py | 71 ++++---- homeassistant/components/gree/climate.py | 22 ++- homeassistant/components/gree/config_flow.py | 8 +- homeassistant/components/gree/const.py | 10 ++ homeassistant/components/gree/manifest.json | 2 +- homeassistant/components/gree/switch.py | 20 ++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/gree/common.py | 37 ++++ tests/components/gree/conftest.py | 28 +--- tests/components/gree/test_climate.py | 168 +++++++++++++------ tests/components/gree/test_config_flow.py | 60 +++++-- tests/components/gree/test_init.py | 33 ++-- tests/components/gree/test_switch.py | 10 +- 15 files changed, 358 insertions(+), 181 deletions(-) diff --git a/homeassistant/components/gree/__init__.py b/homeassistant/components/gree/__init__.py index 92b56a4804e69..b215d4eb9119d 100644 --- a/homeassistant/components/gree/__init__.py +++ b/homeassistant/components/gree/__init__.py @@ -1,14 +1,23 @@ """The Gree Climate integration.""" import asyncio +from datetime import timedelta import logging from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant - -from .bridge import CannotConnect, DeviceDataUpdateCoordinator, DeviceHelper -from .const import COORDINATOR, DOMAIN +from homeassistant.helpers.event import async_track_time_interval + +from .bridge import DiscoveryService +from .const import ( + COORDINATORS, + DATA_DISCOVERY_INTERVAL, + DATA_DISCOVERY_SERVICE, + DISCOVERY_SCAN_INTERVAL, + DISPATCHERS, + DOMAIN, +) _LOGGER = logging.getLogger(__name__) @@ -21,31 +30,11 @@ async def async_setup(hass: HomeAssistant, config: dict): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Gree Climate from a config entry.""" - devices = [] + gree_discovery = DiscoveryService(hass) + hass.data[DATA_DISCOVERY_SERVICE] = gree_discovery - # First we'll grab as many devices as we can find on the network - # it's necessary to bind static devices anyway - _LOGGER.debug("Scanning network for Gree devices") + hass.data[DOMAIN].setdefault(DISPATCHERS, []) - for device_info in await DeviceHelper.find_devices(): - try: - device = await DeviceHelper.try_bind_device(device_info) - except CannotConnect: - _LOGGER.error("Unable to bind to gree device: %s", device_info) - continue - - _LOGGER.debug( - "Adding Gree device at %s:%i (%s)", - device.device_info.ip, - device.device_info.port, - device.device_info.name, - ) - devices.append(device) - - coordinators = [DeviceDataUpdateCoordinator(hass, d) for d in devices] - await asyncio.gather(*[x.async_refresh() for x in coordinators]) - - hass.data[DOMAIN][COORDINATOR] = coordinators hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, CLIMATE_DOMAIN) ) @@ -53,11 +42,31 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.config_entries.async_forward_entry_setup(entry, SWITCH_DOMAIN) ) + async def _async_scan_update(_=None): + await gree_discovery.discovery.scan() + + _LOGGER.debug("Scanning network for Gree devices") + await _async_scan_update() + + hass.data[DOMAIN][DATA_DISCOVERY_INTERVAL] = async_track_time_interval( + hass, _async_scan_update, timedelta(seconds=DISCOVERY_SCAN_INTERVAL) + ) + return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" + if hass.data[DOMAIN].get(DISPATCHERS) is not None: + for cleanup in hass.data[DOMAIN][DISPATCHERS]: + cleanup() + + if hass.data[DOMAIN].get(DATA_DISCOVERY_INTERVAL) is not None: + hass.data[DOMAIN].pop(DATA_DISCOVERY_INTERVAL)() + + if hass.data.get(DATA_DISCOVERY_SERVICE) is not None: + hass.data.pop(DATA_DISCOVERY_SERVICE) + results = asyncio.gather( hass.config_entries.async_forward_entry_unload(entry, CLIMATE_DOMAIN), hass.config_entries.async_forward_entry_unload(entry, SWITCH_DOMAIN), @@ -65,8 +74,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all(await results) if unload_ok: - hass.data[DOMAIN].pop("devices", None) - hass.data[DOMAIN].pop(CLIMATE_DOMAIN, None) - hass.data[DOMAIN].pop(SWITCH_DOMAIN, None) + hass.data[DOMAIN].pop(COORDINATORS, None) + hass.data[DOMAIN].pop(DISPATCHERS, None) return unload_ok diff --git a/homeassistant/components/gree/bridge.py b/homeassistant/components/gree/bridge.py index af523f385aa3e..87f02ab82c4d9 100644 --- a/homeassistant/components/gree/bridge.py +++ b/homeassistant/components/gree/bridge.py @@ -5,14 +5,20 @@ import logging from greeclimate.device import Device, DeviceInfo -from greeclimate.discovery import Discovery +from greeclimate.discovery import Discovery, Listener from greeclimate.exceptions import DeviceNotBoundError, DeviceTimeoutError -from homeassistant import exceptions from homeassistant.core import HomeAssistant +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import DOMAIN, MAX_ERRORS +from .const import ( + COORDINATORS, + DISCOVERY_TIMEOUT, + DISPATCH_DEVICE_DISCOVERED, + DOMAIN, + MAX_ERRORS, +) _LOGGER = logging.getLogger(__name__) @@ -36,6 +42,8 @@ async def _async_update_data(self): """Update the state of the device.""" try: await self.device.update_state() + except DeviceNotBoundError as error: + raise UpdateFailed(f"Device {self.name} is unavailable") from error except DeviceTimeoutError as error: self._error_count += 1 @@ -46,16 +54,7 @@ async def _async_update_data(self): self.name, self.device.device_info, ) - raise UpdateFailed(error) from error - else: - if not self.last_update_success and self._error_count: - _LOGGER.warning( - "Device is available: %s (%s)", - self.name, - str(self.device.device_info), - ) - - self._error_count = 0 + raise UpdateFailed(f"Device {self.name} is unavailable") from error async def push_state_update(self): """Send state updates to the physical device.""" @@ -69,28 +68,38 @@ async def push_state_update(self): ) -class DeviceHelper: - """Device search and bind wrapper for Gree platform.""" +class DiscoveryService(Listener): + """Discovery event handler for gree devices.""" + + def __init__(self, hass) -> None: + """Initialize discovery service.""" + super().__init__() + self.hass = hass - @staticmethod - async def try_bind_device(device_info: DeviceInfo) -> Device: - """Try and bing with a discovered device. + self.discovery = Discovery(DISCOVERY_TIMEOUT) + self.discovery.add_listener(self) + + hass.data[DOMAIN].setdefault(COORDINATORS, []) + + async def device_found(self, device_info: DeviceInfo) -> None: + """Handle new device found on the network.""" - Note the you must bind with the device very quickly after it is discovered, or the - process may not be completed correctly, raising a `CannotConnect` error. - """ device = Device(device_info) try: await device.bind() - except DeviceNotBoundError as exception: - raise CannotConnect from exception - return device - - @staticmethod - async def find_devices() -> list[DeviceInfo]: - """Gather a list of device infos from the local network.""" - return await Discovery.search_devices() + except DeviceNotBoundError: + _LOGGER.error("Unable to bind to gree device: %s", device_info) + except DeviceTimeoutError: + _LOGGER.error("Timeout trying to bind to gree device: %s", device_info) + _LOGGER.info( + "Adding Gree device %s at %s:%i", + device.device_info.name, + device.device_info.ip, + device.device_info.port, + ) + coordo = DeviceDataUpdateCoordinator(self.hass, device) + self.hass.data[DOMAIN][COORDINATORS].append(coordo) + await coordo.async_refresh() -class CannotConnect(exceptions.HomeAssistantError): - """Error to indicate we cannot connect.""" + async_dispatcher_send(self.hass, DISPATCH_DEVICE_DISCOVERED, coordo) diff --git a/homeassistant/components/gree/climate.py b/homeassistant/components/gree/climate.py index a5ef39be071d4..e468195ff9258 100644 --- a/homeassistant/components/gree/climate.py +++ b/homeassistant/components/gree/climate.py @@ -43,11 +43,15 @@ TEMP_CELSIUS, TEMP_FAHRENHEIT, ) +from homeassistant.core import callback from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( - COORDINATOR, + COORDINATORS, + DISPATCH_DEVICE_DISCOVERED, + DISPATCHERS, DOMAIN, FAN_MEDIUM_HIGH, FAN_MEDIUM_LOW, @@ -97,11 +101,17 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Gree HVAC device from a config entry.""" - async_add_entities( - [ - GreeClimateEntity(coordinator) - for coordinator in hass.data[DOMAIN][COORDINATOR] - ] + + @callback + def init_device(coordinator): + """Register the device.""" + async_add_entities([GreeClimateEntity(coordinator)]) + + for coordinator in hass.data[DOMAIN][COORDINATORS]: + init_device(coordinator) + + hass.data[DOMAIN][DISPATCHERS].append( + async_dispatcher_connect(hass, DISPATCH_DEVICE_DISCOVERED, init_device) ) diff --git a/homeassistant/components/gree/config_flow.py b/homeassistant/components/gree/config_flow.py index 76ea2159e2f0c..cc61eabe12cfd 100644 --- a/homeassistant/components/gree/config_flow.py +++ b/homeassistant/components/gree/config_flow.py @@ -1,14 +1,16 @@ """Config flow for Gree.""" +from greeclimate.discovery import Discovery + from homeassistant import config_entries from homeassistant.helpers import config_entry_flow -from .bridge import DeviceHelper -from .const import DOMAIN +from .const import DISCOVERY_TIMEOUT, DOMAIN async def _async_has_devices(hass) -> bool: """Return if there are devices that can be discovered.""" - devices = await DeviceHelper.find_devices() + gree_discovery = Discovery(DISCOVERY_TIMEOUT) + devices = await gree_discovery.scan(wait_for=DISCOVERY_TIMEOUT) return len(devices) > 0 diff --git a/homeassistant/components/gree/const.py b/homeassistant/components/gree/const.py index 9c64506225671..2d9a48496b2e6 100644 --- a/homeassistant/components/gree/const.py +++ b/homeassistant/components/gree/const.py @@ -1,5 +1,15 @@ """Constants for the Gree Climate integration.""" +COORDINATORS = "coordinators" + +DATA_DISCOVERY_SERVICE = "gree_discovery" +DATA_DISCOVERY_INTERVAL = "gree_discovery_interval" + +DISCOVERY_SCAN_INTERVAL = 300 +DISCOVERY_TIMEOUT = 8 +DISPATCH_DEVICE_DISCOVERED = "gree_device_discovered" +DISPATCHERS = "dispatchers" + DOMAIN = "gree" COORDINATOR = "coordinator" diff --git a/homeassistant/components/gree/manifest.json b/homeassistant/components/gree/manifest.json index 0d2bed3ff28c1..c163fc152fdd5 100644 --- a/homeassistant/components/gree/manifest.json +++ b/homeassistant/components/gree/manifest.json @@ -3,6 +3,6 @@ "name": "Gree Climate", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/gree", - "requirements": ["greeclimate==0.10.3"], + "requirements": ["greeclimate==0.11.4"], "codeowners": ["@cmroche"] } \ No newline at end of file diff --git a/homeassistant/components/gree/switch.py b/homeassistant/components/gree/switch.py index 12c94ddec6189..7f659d7e64bf2 100644 --- a/homeassistant/components/gree/switch.py +++ b/homeassistant/components/gree/switch.py @@ -2,19 +2,27 @@ from __future__ import annotations from homeassistant.components.switch import DEVICE_CLASS_SWITCH, SwitchEntity +from homeassistant.core import callback from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import COORDINATOR, DOMAIN +from .const import COORDINATORS, DISPATCH_DEVICE_DISCOVERED, DISPATCHERS, DOMAIN async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Gree HVAC device from a config entry.""" - async_add_entities( - [ - GreeSwitchEntity(coordinator) - for coordinator in hass.data[DOMAIN][COORDINATOR] - ] + + @callback + def init_device(coordinator): + """Register the device.""" + async_add_entities([GreeSwitchEntity(coordinator)]) + + for coordinator in hass.data[DOMAIN][COORDINATORS]: + init_device(coordinator) + + hass.data[DOMAIN][DISPATCHERS].append( + async_dispatcher_connect(hass, DISPATCH_DEVICE_DISCOVERED, init_device) ) diff --git a/requirements_all.txt b/requirements_all.txt index 4de94af64a157..d256c4b4a8e14 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -699,7 +699,7 @@ gpiozero==1.5.1 gps3==0.33.3 # homeassistant.components.gree -greeclimate==0.10.3 +greeclimate==0.11.4 # homeassistant.components.greeneye_monitor greeneye_monitor==2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 458a1a4ab5e70..0c68adf2cdb0e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -384,7 +384,7 @@ google-nest-sdm==0.2.12 googlemaps==2.5.1 # homeassistant.components.gree -greeclimate==0.10.3 +greeclimate==0.11.4 # homeassistant.components.profiler guppy3==3.1.0 diff --git a/tests/components/gree/common.py b/tests/components/gree/common.py index d9fcfba39ce9b..2c9c295da1c47 100644 --- a/tests/components/gree/common.py +++ b/tests/components/gree/common.py @@ -1,6 +1,43 @@ """Common helpers for gree test cases.""" +import asyncio +import logging from unittest.mock import AsyncMock, Mock +from greeclimate.discovery import Listener + +from homeassistant.components.gree.const import DISCOVERY_TIMEOUT + +_LOGGER = logging.getLogger(__name__) + + +class FakeDiscovery: + """Mock class replacing Gree device discovery.""" + + def __init__(self, timeout: int = DISCOVERY_TIMEOUT) -> None: + """Initialize the class.""" + self.mock_devices = [build_device_mock()] + self.timeout = timeout + self._listeners = [] + self.scan_count = 0 + + def add_listener(self, listener: Listener) -> None: + """Add an event listener.""" + self._listeners.append(listener) + + async def scan(self, wait_for: int = 0): + """Search for devices, return mocked data.""" + self.scan_count += 1 + _LOGGER.info("CALLED SCAN %d TIMES", self.scan_count) + + infos = [x.device_info for x in self.mock_devices] + for listener in self._listeners: + [await listener.device_found(x) for x in infos] + + if wait_for: + await asyncio.sleep(wait_for) + + return infos + def build_device_info_mock( name="fake-device-1", ipAddress="1.1.1.1", mac="aabbcc112233" diff --git a/tests/components/gree/conftest.py b/tests/components/gree/conftest.py index bc9a6451dce93..6aabb95a1bbda 100644 --- a/tests/components/gree/conftest.py +++ b/tests/components/gree/conftest.py @@ -1,36 +1,24 @@ """Pytest module configuration.""" -from unittest.mock import AsyncMock, patch +from unittest.mock import patch import pytest -from .common import build_device_info_mock, build_device_mock +from .common import FakeDiscovery, build_device_mock -@pytest.fixture(name="discovery") +@pytest.fixture(autouse=True, name="discovery") def discovery_fixture(): - """Patch the discovery service.""" - with patch( - "homeassistant.components.gree.bridge.Discovery.search_devices", - new_callable=AsyncMock, - return_value=[build_device_info_mock()], - ) as mock: + """Patch the discovery object.""" + with patch("homeassistant.components.gree.bridge.Discovery") as mock: + mock.return_value = FakeDiscovery() yield mock -@pytest.fixture(name="device") +@pytest.fixture(autouse=True, name="device") def device_fixture(): - """Path the device search and bind.""" + """Patch the device search and bind.""" with patch( "homeassistant.components.gree.bridge.Device", return_value=build_device_mock(), ) as mock: yield mock - - -@pytest.fixture(name="setup") -def setup_fixture(): - """Patch the climate setup.""" - with patch( - "homeassistant.components.gree.climate.async_setup_entry", return_value=True - ) as setup: - yield setup diff --git a/tests/components/gree/test_climate.py b/tests/components/gree/test_climate.py index d85976c2410b4..62dd7ca545f68 100644 --- a/tests/components/gree/test_climate.py +++ b/tests/components/gree/test_climate.py @@ -97,7 +97,7 @@ async def test_discovery_setup(hass, discovery, device): name="fake-device-2", ipAddress="2.2.2.2", mac="bbccdd223344" ) - discovery.return_value = [MockDevice1.device_info, MockDevice2.device_info] + discovery.return_value.mock_devices = [MockDevice1, MockDevice2] device.side_effect = [MockDevice1, MockDevice2] await async_setup_gree(hass) @@ -106,24 +106,127 @@ async def test_discovery_setup(hass, discovery, device): assert len(hass.states.async_all(DOMAIN)) == 2 -async def test_discovery_setup_connection_error(hass, discovery, device): +async def test_discovery_setup_connection_error(hass, discovery, device, mock_now): """Test gree integration is setup.""" - MockDevice1 = build_device_mock(name="fake-device-1") + MockDevice1 = build_device_mock( + name="fake-device-1", ipAddress="1.1.1.1", mac="aabbcc112233" + ) + MockDevice1.bind = AsyncMock(side_effect=DeviceNotBoundError) + MockDevice1.update_state = AsyncMock(side_effect=DeviceNotBoundError) + + discovery.return_value.mock_devices = [MockDevice1] + device.return_value = MockDevice1 + + await async_setup_gree(hass) + await hass.async_block_till_done() + + assert len(hass.states.async_all(DOMAIN)) == 1 + state = hass.states.get(ENTITY_ID) + assert state.name == "fake-device-1" + assert state.state == STATE_UNAVAILABLE + + +async def test_discovery_after_setup(hass, discovery, device, mock_now): + """Test gree devices don't change after multiple discoveries.""" + MockDevice1 = build_device_mock( + name="fake-device-1", ipAddress="1.1.1.1", mac="aabbcc112233" + ) MockDevice1.bind = AsyncMock(side_effect=DeviceNotBoundError) - MockDevice2 = build_device_mock(name="fake-device-2") - MockDevice2.bind = AsyncMock(side_effect=DeviceNotBoundError) + MockDevice2 = build_device_mock( + name="fake-device-2", ipAddress="2.2.2.2", mac="bbccdd223344" + ) + MockDevice2.bind = AsyncMock(side_effect=DeviceTimeoutError) + discovery.return_value.mock_devices = [MockDevice1, MockDevice2] device.side_effect = [MockDevice1, MockDevice2] await async_setup_gree(hass) await hass.async_block_till_done() - assert discovery.call_count == 1 - assert not hass.states.async_all(DOMAIN) + assert discovery.return_value.scan_count == 1 + assert len(hass.states.async_all(DOMAIN)) == 2 + + # rediscover the same devices shouldn't change anything + discovery.return_value.mock_devices = [MockDevice1, MockDevice2] + device.side_effect = [MockDevice1, MockDevice2] + + next_update = mock_now + timedelta(minutes=6) + with patch("homeassistant.util.dt.utcnow", return_value=next_update): + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + + assert discovery.return_value.scan_count == 2 + assert len(hass.states.async_all(DOMAIN)) == 2 + + +async def test_discovery_add_device_after_setup(hass, discovery, device, mock_now): + """Test gree devices can be added after initial setup.""" + MockDevice1 = build_device_mock( + name="fake-device-1", ipAddress="1.1.1.1", mac="aabbcc112233" + ) + MockDevice1.bind = AsyncMock(side_effect=DeviceNotBoundError) + + MockDevice2 = build_device_mock( + name="fake-device-2", ipAddress="2.2.2.2", mac="bbccdd223344" + ) + MockDevice2.bind = AsyncMock(side_effect=DeviceTimeoutError) + + discovery.return_value.mock_devices = [MockDevice1] + device.side_effect = [MockDevice1] + + await async_setup_gree(hass) + await hass.async_block_till_done() + + assert discovery.return_value.scan_count == 1 + assert len(hass.states.async_all(DOMAIN)) == 1 + + # rediscover the same devices shouldn't change anything + discovery.return_value.mock_devices = [MockDevice2] + device.side_effect = [MockDevice2] + + next_update = mock_now + timedelta(minutes=6) + with patch("homeassistant.util.dt.utcnow", return_value=next_update): + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + + assert discovery.return_value.scan_count == 2 + assert len(hass.states.async_all(DOMAIN)) == 2 + + +async def test_discovery_device_bind_after_setup(hass, discovery, device, mock_now): + """Test gree devices can be added after a late device bind.""" + MockDevice1 = build_device_mock( + name="fake-device-1", ipAddress="1.1.1.1", mac="aabbcc112233" + ) + MockDevice1.bind = AsyncMock(side_effect=DeviceNotBoundError) + MockDevice1.update_state = AsyncMock(side_effect=DeviceNotBoundError) + + discovery.return_value.mock_devices = [MockDevice1] + device.return_value = MockDevice1 + await async_setup_gree(hass) + await hass.async_block_till_done() + + assert len(hass.states.async_all(DOMAIN)) == 1 + state = hass.states.get(ENTITY_ID) + assert state.name == "fake-device-1" + assert state.state == STATE_UNAVAILABLE + + # Now the device becomes available + MockDevice1.bind.side_effect = None + MockDevice1.update_state.side_effect = None + + next_update = mock_now + timedelta(minutes=5) + with patch("homeassistant.util.dt.utcnow", return_value=next_update): + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state.state != STATE_UNAVAILABLE -async def test_update_connection_failure(hass, discovery, device, mock_now): + +async def test_update_connection_failure(hass, device, mock_now): """Testing update hvac connection failure exception.""" device().update_state.side_effect = [ DEFAULT_MOCK, @@ -229,11 +332,10 @@ async def test_send_command_device_timeout(hass, discovery, device, mock_now): # Send failure should not raise exceptions or change device state assert await hass.services.async_call( DOMAIN, - SERVICE_SET_HVAC_MODE, - {ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVAC_MODE_AUTO}, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_ID}, blocking=True, ) - await hass.async_block_till_done() state = hass.states.get(ENTITY_ID) assert state is not None @@ -244,45 +346,6 @@ async def test_send_power_on(hass, discovery, device, mock_now): """Test for sending power on command to the device.""" await async_setup_gree(hass) - assert await hass.services.async_call( - DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: ENTITY_ID}, - blocking=True, - ) - - state = hass.states.get(ENTITY_ID) - assert state is not None - assert state.state != HVAC_MODE_OFF - - -async def test_send_power_on_device_timeout(hass, discovery, device, mock_now): - """Test for sending power on command to the device with a device timeout.""" - device().push_state_update.side_effect = DeviceTimeoutError - - await async_setup_gree(hass) - - assert await hass.services.async_call( - DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: ENTITY_ID}, - blocking=True, - ) - - state = hass.states.get(ENTITY_ID) - assert state is not None - assert state.state != HVAC_MODE_OFF - - -async def test_send_power_off(hass, discovery, device, mock_now): - """Test for sending power off command to the device.""" - await async_setup_gree(hass) - - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - assert await hass.services.async_call( DOMAIN, SERVICE_TURN_OFF, @@ -301,11 +364,6 @@ async def test_send_power_off_device_timeout(hass, discovery, device, mock_now): await async_setup_gree(hass) - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - assert await hass.services.async_call( DOMAIN, SERVICE_TURN_OFF, diff --git a/tests/components/gree/test_config_flow.py b/tests/components/gree/test_config_flow.py index bb5f59b573d13..a3e881d6dafdb 100644 --- a/tests/components/gree/test_config_flow.py +++ b/tests/components/gree/test_config_flow.py @@ -1,20 +1,60 @@ """Tests for the Gree Integration.""" +from unittest.mock import patch + from homeassistant import config_entries, data_entry_flow from homeassistant.components.gree.const import DOMAIN as GREE_DOMAIN +from .common import FakeDiscovery + + +async def test_creating_entry_sets_up_climate(hass): + """Test setting up Gree creates the climate components.""" + with patch( + "homeassistant.components.gree.climate.async_setup_entry", return_value=True + ) as setup, patch( + "homeassistant.components.gree.bridge.Discovery", return_value=FakeDiscovery() + ), patch( + "homeassistant.components.gree.config_flow.Discovery", + return_value=FakeDiscovery(), + ): + result = await hass.config_entries.flow.async_init( + GREE_DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + # Confirmation form + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM -async def test_creating_entry_sets_up_climate(hass, discovery, device, setup): + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + await hass.async_block_till_done() + + assert len(setup.mock_calls) == 1 + + +async def test_creating_entry_has_no_devices(hass): """Test setting up Gree creates the climate components.""" - result = await hass.config_entries.flow.async_init( - GREE_DOMAIN, context={"source": config_entries.SOURCE_USER} - ) + with patch( + "homeassistant.components.gree.climate.async_setup_entry", return_value=True + ) as setup, patch( + "homeassistant.components.gree.bridge.Discovery", return_value=FakeDiscovery() + ) as discovery, patch( + "homeassistant.components.gree.config_flow.Discovery", + return_value=FakeDiscovery(), + ) as discovery2: + discovery.return_value.mock_devices = [] + discovery2.return_value.mock_devices = [] + + result = await hass.config_entries.flow.async_init( + GREE_DOMAIN, context={"source": config_entries.SOURCE_USER} + ) - # Confirmation form - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + # Confirmation form + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - await hass.async_block_till_done() + await hass.async_block_till_done() - assert len(setup.mock_calls) == 1 + assert len(setup.mock_calls) == 0 diff --git a/tests/components/gree/test_init.py b/tests/components/gree/test_init.py index bf999ee9e6f11..7443ae1e94c06 100644 --- a/tests/components/gree/test_init.py +++ b/tests/components/gree/test_init.py @@ -1,5 +1,4 @@ """Tests for the Gree Integration.""" - from unittest.mock import patch from homeassistant.components.gree.const import DOMAIN as GREE_DOMAIN @@ -9,31 +8,39 @@ from tests.common import MockConfigEntry -async def test_setup_simple(hass, discovery, device): +async def test_setup_simple(hass): """Test gree integration is setup.""" - await async_setup_component(hass, GREE_DOMAIN, {}) - await hass.async_block_till_done() - - # No flows started - assert len(hass.config_entries.flow.async_progress()) == 0 - - -async def test_unload_config_entry(hass, discovery, device): - """Test that the async_unload_entry works.""" - # As we have currently no configuration, we just to pass the domain here. entry = MockConfigEntry(domain=GREE_DOMAIN) entry.add_to_hass(hass) with patch( "homeassistant.components.gree.climate.async_setup_entry", return_value=True, - ) as climate_setup: + ) as climate_setup, patch( + "homeassistant.components.gree.switch.async_setup_entry", + return_value=True, + ) as switch_setup: assert await async_setup_component(hass, GREE_DOMAIN, {}) await hass.async_block_till_done() assert len(climate_setup.mock_calls) == 1 + assert len(switch_setup.mock_calls) == 1 assert entry.state == ENTRY_STATE_LOADED + # No flows started + assert len(hass.config_entries.flow.async_progress()) == 0 + + +async def test_unload_config_entry(hass): + """Test that the async_unload_entry works.""" + # As we have currently no configuration, we just to pass the domain here. + entry = MockConfigEntry(domain=GREE_DOMAIN) + entry.add_to_hass(hass) + + assert await async_setup_component(hass, GREE_DOMAIN, {}) + await hass.async_block_till_done() + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() assert entry.state == ENTRY_STATE_NOT_LOADED diff --git a/tests/components/gree/test_switch.py b/tests/components/gree/test_switch.py index 89a8b224f1a30..39ad536880ca3 100644 --- a/tests/components/gree/test_switch.py +++ b/tests/components/gree/test_switch.py @@ -26,7 +26,7 @@ async def async_setup_gree(hass): await hass.async_block_till_done() -async def test_send_panel_light_on(hass, discovery, device): +async def test_send_panel_light_on(hass): """Test for sending power on command to the device.""" await async_setup_gree(hass) @@ -42,7 +42,7 @@ async def test_send_panel_light_on(hass, discovery, device): assert state.state == STATE_ON -async def test_send_panel_light_on_device_timeout(hass, discovery, device): +async def test_send_panel_light_on_device_timeout(hass, device): """Test for sending power on command to the device with a device timeout.""" device().push_state_update.side_effect = DeviceTimeoutError @@ -60,7 +60,7 @@ async def test_send_panel_light_on_device_timeout(hass, discovery, device): assert state.state == STATE_ON -async def test_send_panel_light_off(hass, discovery, device): +async def test_send_panel_light_off(hass): """Test for sending power on command to the device.""" await async_setup_gree(hass) @@ -76,7 +76,7 @@ async def test_send_panel_light_off(hass, discovery, device): assert state.state == STATE_OFF -async def test_send_panel_light_toggle(hass, discovery, device): +async def test_send_panel_light_toggle(hass): """Test for sending power on command to the device.""" await async_setup_gree(hass) @@ -117,7 +117,7 @@ async def test_send_panel_light_toggle(hass, discovery, device): assert state.state == STATE_ON -async def test_panel_light_name(hass, discovery, device): +async def test_panel_light_name(hass): """Test for name property.""" await async_setup_gree(hass) state = hass.states.get(ENTITY_ID) From c71a1a34facd630e1b928ff7635449a19541de6a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Apr 2021 12:10:30 +0200 Subject: [PATCH 0233/1317] Bump actions/setup-python from v2.2.1 to v2.2.2 (#49156) Bumps [actions/setup-python](https://github.com/actions/setup-python) from v2.2.1 to v2.2.2. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v2.2.1...dc73133d4da04e56a135ae2246682783cc7c7cb6) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5d43b124c4953..a9dc1df883683 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -28,7 +28,7 @@ jobs: uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v2.2.1 + uses: actions/setup-python@v2.2.2 with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Generate partial Python venv restore key @@ -85,7 +85,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.2.1 + uses: actions/setup-python@v2.2.2 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -125,7 +125,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.2.1 + uses: actions/setup-python@v2.2.2 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -165,7 +165,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.2.1 + uses: actions/setup-python@v2.2.2 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -227,7 +227,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.2.1 + uses: actions/setup-python@v2.2.2 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -270,7 +270,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.2.1 + uses: actions/setup-python@v2.2.2 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -313,7 +313,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.2.1 + uses: actions/setup-python@v2.2.2 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -353,7 +353,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.2.1 + uses: actions/setup-python@v2.2.2 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -396,7 +396,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.2.1 + uses: actions/setup-python@v2.2.2 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -447,7 +447,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.2.1 + uses: actions/setup-python@v2.2.2 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -518,7 +518,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.2.1 + uses: actions/setup-python@v2.2.2 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} From 0742b046b91d9b067023d45e8cb75efe248f8578 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Apr 2021 12:11:42 +0200 Subject: [PATCH 0234/1317] Bump actions/cache from v2.1.4 to v2.1.5 (#49157) Bumps [actions/cache](https://github.com/actions/cache) from v2.1.4 to v2.1.5. - [Release notes](https://github.com/actions/cache/releases) - [Commits](https://github.com/actions/cache/compare/v2.1.4...1a9e2138d905efd099035b49d8b7a3888c653ca8) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 54 +++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a9dc1df883683..71b69ebfe8619 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -40,7 +40,7 @@ jobs: hashFiles('homeassistant/package_constraints.txt') }}" - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2.1.4 + uses: actions/cache@v2.1.5 with: path: venv key: >- @@ -64,7 +64,7 @@ jobs: hashFiles('.pre-commit-config.yaml') }}" - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2.1.4 + uses: actions/cache@v2.1.5 with: path: ${{ env.PRE_COMMIT_CACHE }} key: >- @@ -91,7 +91,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2.1.4 + uses: actions/cache@v2.1.5 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -103,7 +103,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2.1.4 + uses: actions/cache@v2.1.5 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} @@ -131,7 +131,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2.1.4 + uses: actions/cache@v2.1.5 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -143,7 +143,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2.1.4 + uses: actions/cache@v2.1.5 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} @@ -171,7 +171,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2.1.4 + uses: actions/cache@v2.1.5 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -183,7 +183,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2.1.4 + uses: actions/cache@v2.1.5 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} @@ -233,7 +233,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2.1.4 + uses: actions/cache@v2.1.5 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -245,7 +245,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2.1.4 + uses: actions/cache@v2.1.5 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} @@ -276,7 +276,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2.1.4 + uses: actions/cache@v2.1.5 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -288,7 +288,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2.1.4 + uses: actions/cache@v2.1.5 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} @@ -319,7 +319,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2.1.4 + uses: actions/cache@v2.1.5 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -331,7 +331,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2.1.4 + uses: actions/cache@v2.1.5 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} @@ -359,7 +359,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2.1.4 + uses: actions/cache@v2.1.5 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -371,7 +371,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2.1.4 + uses: actions/cache@v2.1.5 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} @@ -402,7 +402,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2.1.4 + uses: actions/cache@v2.1.5 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -414,7 +414,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2.1.4 + uses: actions/cache@v2.1.5 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} @@ -453,7 +453,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2.1.4 + uses: actions/cache@v2.1.5 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -465,7 +465,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2.1.4 + uses: actions/cache@v2.1.5 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} @@ -495,7 +495,7 @@ jobs: uses: actions/checkout@v2 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v2.1.4 + uses: actions/cache@v2.1.5 with: path: venv key: ${{ runner.os }}-${{ matrix.python-version }}-${{ @@ -524,7 +524,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2.1.4 + uses: actions/cache@v2.1.5 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -560,7 +560,7 @@ jobs: hashFiles('homeassistant/package_constraints.txt') }}" - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v2.1.4 + uses: actions/cache@v2.1.5 with: path: venv key: >- @@ -597,7 +597,7 @@ jobs: uses: actions/checkout@v2 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v2.1.4 + uses: actions/cache@v2.1.5 with: path: venv key: ${{ runner.os }}-${{ matrix.python-version }}-${{ @@ -628,7 +628,7 @@ jobs: uses: actions/checkout@v2 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v2.1.4 + uses: actions/cache@v2.1.5 with: path: venv key: ${{ runner.os }}-${{ matrix.python-version }}-${{ @@ -662,7 +662,7 @@ jobs: uses: actions/checkout@v2 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v2.1.4 + uses: actions/cache@v2.1.5 with: path: venv key: ${{ runner.os }}-${{ matrix.python-version }}-${{ @@ -720,7 +720,7 @@ jobs: uses: actions/checkout@v2 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v2.1.4 + uses: actions/cache@v2.1.5 with: path: venv key: ${{ runner.os }}-${{ matrix.python-version }}-${{ From 2cc9ae1af1975c8c2825472e71f2a10db51d9174 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 13 Apr 2021 00:21:52 -1000 Subject: [PATCH 0235/1317] Use named constants for core shutdown timeouts (#49146) This is intended to make them easier to reference outside the core code base. --- homeassistant/core.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 6ad722e0d18b4..d172b3445e81d 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -87,6 +87,11 @@ from homeassistant.config_entries import ConfigEntries +STAGE_1_SHUTDOWN_TIMEOUT = 120 +STAGE_2_SHUTDOWN_TIMEOUT = 60 +STAGE_3_SHUTDOWN_TIMEOUT = 30 + + block_async_io.enable() T = TypeVar("T") @@ -528,7 +533,7 @@ async def async_stop(self, exit_code: int = 0, *, force: bool = False) -> None: self.async_track_tasks() self.bus.async_fire(EVENT_HOMEASSISTANT_STOP) try: - async with self.timeout.async_timeout(120): + async with self.timeout.async_timeout(STAGE_1_SHUTDOWN_TIMEOUT): await self.async_block_till_done() except asyncio.TimeoutError: _LOGGER.warning( @@ -539,7 +544,7 @@ async def async_stop(self, exit_code: int = 0, *, force: bool = False) -> None: self.state = CoreState.final_write self.bus.async_fire(EVENT_HOMEASSISTANT_FINAL_WRITE) try: - async with self.timeout.async_timeout(60): + async with self.timeout.async_timeout(STAGE_2_SHUTDOWN_TIMEOUT): await self.async_block_till_done() except asyncio.TimeoutError: _LOGGER.warning( @@ -558,7 +563,7 @@ async def async_stop(self, exit_code: int = 0, *, force: bool = False) -> None: shutdown_run_callback_threadsafe(self.loop) try: - async with self.timeout.async_timeout(30): + async with self.timeout.async_timeout(STAGE_3_SHUTDOWN_TIMEOUT): await self.async_block_till_done() except asyncio.TimeoutError: _LOGGER.warning( From 91821fa6ad14a7bd85832c0b0f9c74cb4bdd96a0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 13 Apr 2021 00:29:30 -1000 Subject: [PATCH 0236/1317] Name the dhcp watcher thread (#49144) When getting py-spy reports, it is helpful to get thread names to make it easier to track down issues. --- homeassistant/components/dhcp/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/dhcp/__init__.py b/homeassistant/components/dhcp/__init__.py index e21b6cf88dce8..5d0b31c878850 100644 --- a/homeassistant/components/dhcp/__init__.py +++ b/homeassistant/components/dhcp/__init__.py @@ -312,6 +312,8 @@ async def async_start(self): ) self._sniffer.start() + if self._sniffer.thread: + self._sniffer.thread.name = self.__class__.__name__ def handle_dhcp_packet(self, packet): """Process a dhcp packet.""" From 51a7a724d6f8914c5410f0e902c3da0cf4ed8a26 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 13 Apr 2021 00:31:55 -1000 Subject: [PATCH 0237/1317] Bump aiodiscover to 1.3.4 (#49142) - Changelog: https://github.com/bdraco/aiodiscover/compare/v1.3.3...v1.3.4 (bumps pyroute2>=0.5.18 to fix https://github.com/svinota/pyroute2/issues/717) --- homeassistant/components/dhcp/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/dhcp/manifest.json b/homeassistant/components/dhcp/manifest.json index e93e521b8823c..6ab395e7d829f 100644 --- a/homeassistant/components/dhcp/manifest.json +++ b/homeassistant/components/dhcp/manifest.json @@ -3,7 +3,7 @@ "name": "DHCP Discovery", "documentation": "https://www.home-assistant.io/integrations/dhcp", "requirements": [ - "scapy==2.4.4", "aiodiscover==1.3.3" + "scapy==2.4.4", "aiodiscover==1.3.4" ], "codeowners": [ "@bdraco" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3b29c32ff18fb..58649ee587f7a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,6 +1,6 @@ PyJWT==1.7.1 PyNaCl==1.3.0 -aiodiscover==1.3.3 +aiodiscover==1.3.4 aiohttp==3.7.4.post0 aiohttp_cors==0.7.0 astral==2.2 diff --git a/requirements_all.txt b/requirements_all.txt index d256c4b4a8e14..3d76cd9e1e6f0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -147,7 +147,7 @@ aioazuredevops==1.3.5 aiobotocore==1.2.2 # homeassistant.components.dhcp -aiodiscover==1.3.3 +aiodiscover==1.3.4 # homeassistant.components.dnsip # homeassistant.components.minecraft_server diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0c68adf2cdb0e..26c97639e21f9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -87,7 +87,7 @@ aioazuredevops==1.3.5 aiobotocore==1.2.2 # homeassistant.components.dhcp -aiodiscover==1.3.3 +aiodiscover==1.3.4 # homeassistant.components.dnsip # homeassistant.components.minecraft_server From 5365fb6c724c1385cfaa1e24d1ed0291e6564f14 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 13 Apr 2021 00:44:07 -1000 Subject: [PATCH 0238/1317] Fix setting up remotes that lack a supported features list in homekit (#49152) --- homeassistant/components/homekit/util.py | 2 +- tests/components/homekit/test_config_flow.py | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index a746355e12405..673abc5da67ee 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -507,5 +507,5 @@ def state_needs_accessory_mode(state): or state.domain == MEDIA_PLAYER_DOMAIN and state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TV or state.domain == REMOTE_DOMAIN - and state.attributes.get(ATTR_SUPPORTED_FEATURES) & SUPPORT_ACTIVITY + and state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) & SUPPORT_ACTIVITY ) diff --git a/tests/components/homekit/test_config_flow.py b/tests/components/homekit/test_config_flow.py index 268046752650e..c06e8aaa5ad7c 100644 --- a/tests/components/homekit/test_config_flow.py +++ b/tests/components/homekit/test_config_flow.py @@ -145,6 +145,8 @@ async def test_setup_creates_entries_for_accessory_mode_devices(hass): hass.states.async_set("camera.one", "on") hass.states.async_set("camera.existing", "on") hass.states.async_set("media_player.two", "on", {"device_class": "tv"}) + hass.states.async_set("remote.standard", "on") + hass.states.async_set("remote.activity", "on", {"supported_features": 4}) bridge_mode_entry = MockConfigEntry( domain=DOMAIN, @@ -178,7 +180,7 @@ async def test_setup_creates_entries_for_accessory_mode_devices(hass): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {"include_domains": ["camera", "media_player", "light"]}, + {"include_domains": ["camera", "media_player", "light", "remote"]}, ) assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM assert result2["step_id"] == "pairing" @@ -205,7 +207,7 @@ async def test_setup_creates_entries_for_accessory_mode_devices(hass): "filter": { "exclude_domains": [], "exclude_entities": [], - "include_domains": ["media_player", "light"], + "include_domains": ["media_player", "light", "remote"], "include_entities": [], }, "exclude_accessory_mode": True, @@ -222,7 +224,8 @@ async def test_setup_creates_entries_for_accessory_mode_devices(hass): # 3 - new bridge # 4 - camera.one in accessory mode # 5 - media_player.two in accessory mode - assert len(mock_setup_entry.mock_calls) == 5 + # 6 - remote.activity in accessory mode + assert len(mock_setup_entry.mock_calls) == 6 async def test_import(hass): From 2b79c91813b6b7f0007dcf9b56f60a26f93f1fd6 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Tue, 13 Apr 2021 13:07:05 +0200 Subject: [PATCH 0239/1317] Clean up camera service schema (#49151) --- homeassistant/components/amcrest/camera.py | 43 ++++++++----------- homeassistant/components/camera/__init__.py | 40 ++++++----------- homeassistant/components/local_file/camera.py | 13 +++--- .../components/logi_circle/__init__.py | 26 +++++------ 4 files changed, 47 insertions(+), 75 deletions(-) diff --git a/homeassistant/components/amcrest/camera.py b/homeassistant/components/amcrest/camera.py index f57b9e62bae4f..140069a10249a 100644 --- a/homeassistant/components/amcrest/camera.py +++ b/homeassistant/components/amcrest/camera.py @@ -8,12 +8,7 @@ from haffmpeg.camera import CameraMjpeg import voluptuous as vol -from homeassistant.components.camera import ( - CAMERA_SERVICE_SCHEMA, - SUPPORT_ON_OFF, - SUPPORT_STREAM, - Camera, -) +from homeassistant.components.camera import SUPPORT_ON_OFF, SUPPORT_STREAM, Camera from homeassistant.components.ffmpeg import DATA_FFMPEG from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON from homeassistant.helpers.aiohttp_client import ( @@ -82,30 +77,26 @@ _CBW_BW = "bw" _CBW = [_CBW_COLOR, _CBW_AUTO, _CBW_BW] -_SRV_GOTO_SCHEMA = CAMERA_SERVICE_SCHEMA.extend( - {vol.Required(_ATTR_PRESET): vol.All(vol.Coerce(int), vol.Range(min=1))} -) -_SRV_CBW_SCHEMA = CAMERA_SERVICE_SCHEMA.extend( - {vol.Required(_ATTR_COLOR_BW): vol.In(_CBW)} -) -_SRV_PTZ_SCHEMA = CAMERA_SERVICE_SCHEMA.extend( - { - vol.Required(_ATTR_PTZ_MOV): vol.In(_MOV), - vol.Optional(_ATTR_PTZ_TT, default=_DEFAULT_TT): cv.small_float, - } -) +_SRV_GOTO_SCHEMA = { + vol.Required(_ATTR_PRESET): vol.All(vol.Coerce(int), vol.Range(min=1)) +} +_SRV_CBW_SCHEMA = {vol.Required(_ATTR_COLOR_BW): vol.In(_CBW)} +_SRV_PTZ_SCHEMA = { + vol.Required(_ATTR_PTZ_MOV): vol.In(_MOV), + vol.Optional(_ATTR_PTZ_TT, default=_DEFAULT_TT): cv.small_float, +} CAMERA_SERVICES = { - _SRV_EN_REC: (CAMERA_SERVICE_SCHEMA, "async_enable_recording", ()), - _SRV_DS_REC: (CAMERA_SERVICE_SCHEMA, "async_disable_recording", ()), - _SRV_EN_AUD: (CAMERA_SERVICE_SCHEMA, "async_enable_audio", ()), - _SRV_DS_AUD: (CAMERA_SERVICE_SCHEMA, "async_disable_audio", ()), - _SRV_EN_MOT_REC: (CAMERA_SERVICE_SCHEMA, "async_enable_motion_recording", ()), - _SRV_DS_MOT_REC: (CAMERA_SERVICE_SCHEMA, "async_disable_motion_recording", ()), + _SRV_EN_REC: ({}, "async_enable_recording", ()), + _SRV_DS_REC: ({}, "async_disable_recording", ()), + _SRV_EN_AUD: ({}, "async_enable_audio", ()), + _SRV_DS_AUD: ({}, "async_disable_audio", ()), + _SRV_EN_MOT_REC: ({}, "async_enable_motion_recording", ()), + _SRV_DS_MOT_REC: ({}, "async_disable_motion_recording", ()), _SRV_GOTO: (_SRV_GOTO_SCHEMA, "async_goto_preset", (_ATTR_PRESET,)), _SRV_CBW: (_SRV_CBW_SCHEMA, "async_set_color_bw", (_ATTR_COLOR_BW,)), - _SRV_TOUR_ON: (CAMERA_SERVICE_SCHEMA, "async_start_tour", ()), - _SRV_TOUR_OFF: (CAMERA_SERVICE_SCHEMA, "async_stop_tour", ()), + _SRV_TOUR_ON: ({}, "async_start_tour", ()), + _SRV_TOUR_OFF: ({}, "async_stop_tour", ()), _SRV_PTZ_CTRL: ( _SRV_PTZ_SCHEMA, "async_ptz_control", diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 707398575878f..3a2fe8ba417b5 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -89,26 +89,18 @@ MIN_STREAM_INTERVAL = 0.5 # seconds -CAMERA_SERVICE_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids}) +CAMERA_SERVICE_SNAPSHOT = {vol.Required(ATTR_FILENAME): cv.template} -CAMERA_SERVICE_SNAPSHOT = CAMERA_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_FILENAME): cv.template} -) - -CAMERA_SERVICE_PLAY_STREAM = CAMERA_SERVICE_SCHEMA.extend( - { - vol.Required(ATTR_MEDIA_PLAYER): cv.entities_domain(DOMAIN_MP), - vol.Optional(ATTR_FORMAT, default="hls"): vol.In(OUTPUT_FORMATS), - } -) +CAMERA_SERVICE_PLAY_STREAM = { + vol.Required(ATTR_MEDIA_PLAYER): cv.entities_domain(DOMAIN_MP), + vol.Optional(ATTR_FORMAT, default="hls"): vol.In(OUTPUT_FORMATS), +} -CAMERA_SERVICE_RECORD = CAMERA_SERVICE_SCHEMA.extend( - { - vol.Required(CONF_FILENAME): cv.template, - vol.Optional(CONF_DURATION, default=30): vol.Coerce(int), - vol.Optional(CONF_LOOKBACK, default=0): vol.Coerce(int), - } -) +CAMERA_SERVICE_RECORD = { + vol.Required(CONF_FILENAME): cv.template, + vol.Optional(CONF_DURATION, default=30): vol.Coerce(int), + vol.Optional(CONF_LOOKBACK, default=0): vol.Coerce(int), +} WS_TYPE_CAMERA_THUMBNAIL = "camera_thumbnail" SCHEMA_WS_CAMERA_THUMBNAIL = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( @@ -271,17 +263,13 @@ def update_tokens(time): hass.helpers.event.async_track_time_interval(update_tokens, TOKEN_CHANGE_INTERVAL) component.async_register_entity_service( - SERVICE_ENABLE_MOTION, CAMERA_SERVICE_SCHEMA, "async_enable_motion_detection" - ) - component.async_register_entity_service( - SERVICE_DISABLE_MOTION, CAMERA_SERVICE_SCHEMA, "async_disable_motion_detection" - ) - component.async_register_entity_service( - SERVICE_TURN_OFF, CAMERA_SERVICE_SCHEMA, "async_turn_off" + SERVICE_ENABLE_MOTION, {}, "async_enable_motion_detection" ) component.async_register_entity_service( - SERVICE_TURN_ON, CAMERA_SERVICE_SCHEMA, "async_turn_on" + SERVICE_DISABLE_MOTION, {}, "async_disable_motion_detection" ) + component.async_register_entity_service(SERVICE_TURN_OFF, {}, "async_turn_off") + component.async_register_entity_service(SERVICE_TURN_ON, {}, "async_turn_on") component.async_register_entity_service( SERVICE_SNAPSHOT, CAMERA_SERVICE_SNAPSHOT, async_handle_snapshot_service ) diff --git a/homeassistant/components/local_file/camera.py b/homeassistant/components/local_file/camera.py index c94aeff24b0fd..86a075c1a14b3 100644 --- a/homeassistant/components/local_file/camera.py +++ b/homeassistant/components/local_file/camera.py @@ -5,11 +5,7 @@ import voluptuous as vol -from homeassistant.components.camera import ( - CAMERA_SERVICE_SCHEMA, - PLATFORM_SCHEMA, - Camera, -) +from homeassistant.components.camera import PLATFORM_SCHEMA, Camera from homeassistant.const import ATTR_ENTITY_ID, CONF_FILE_PATH, CONF_NAME from homeassistant.helpers import config_validation as cv @@ -24,8 +20,11 @@ } ) -CAMERA_SERVICE_UPDATE_FILE_PATH = CAMERA_SERVICE_SCHEMA.extend( - {vol.Required(CONF_FILE_PATH): cv.string} +CAMERA_SERVICE_UPDATE_FILE_PATH = vol.Schema( + { + vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, + vol.Required(CONF_FILE_PATH): cv.string, + } ) diff --git a/homeassistant/components/logi_circle/__init__.py b/homeassistant/components/logi_circle/__init__.py index 3364cd725c71b..c51833bc43f0e 100644 --- a/homeassistant/components/logi_circle/__init__.py +++ b/homeassistant/components/logi_circle/__init__.py @@ -8,7 +8,7 @@ import voluptuous as vol from homeassistant import config_entries -from homeassistant.components.camera import ATTR_FILENAME, CAMERA_SERVICE_SCHEMA +from homeassistant.components.camera import ATTR_FILENAME from homeassistant.const import ( ATTR_MODE, CONF_API_KEY, @@ -72,23 +72,17 @@ extra=vol.ALLOW_EXTRA, ) -LOGI_CIRCLE_SERVICE_SET_CONFIG = CAMERA_SERVICE_SCHEMA.extend( - { - vol.Required(ATTR_MODE): vol.In([LED_MODE_KEY, RECORDING_MODE_KEY]), - vol.Required(ATTR_VALUE): cv.boolean, - } -) +LOGI_CIRCLE_SERVICE_SET_CONFIG = { + vol.Required(ATTR_MODE): vol.In([LED_MODE_KEY, RECORDING_MODE_KEY]), + vol.Required(ATTR_VALUE): cv.boolean, +} -LOGI_CIRCLE_SERVICE_SNAPSHOT = CAMERA_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_FILENAME): cv.template} -) +LOGI_CIRCLE_SERVICE_SNAPSHOT = {vol.Required(ATTR_FILENAME): cv.template} -LOGI_CIRCLE_SERVICE_RECORD = CAMERA_SERVICE_SCHEMA.extend( - { - vol.Required(ATTR_FILENAME): cv.template, - vol.Required(ATTR_DURATION): cv.positive_int, - } -) +LOGI_CIRCLE_SERVICE_RECORD = { + vol.Required(ATTR_FILENAME): cv.template, + vol.Required(ATTR_DURATION): cv.positive_int, +} async def async_setup(hass, config): From 769923e8dd5880c7d3a82793b40bb4ae039cb95b Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 13 Apr 2021 08:18:51 -0400 Subject: [PATCH 0240/1317] Raise exception for invalid call to DeviceRegistry.async_get_or_create (#49038) * Raise exception instead of returning None for DeviceRegistry.async_get_or_create * fix entity_platform logic --- homeassistant/exceptions.py | 15 +++++++++++++++ homeassistant/helpers/device_registry.py | 12 +++++++++--- homeassistant/helpers/entity_platform.py | 12 +++++++++--- tests/helpers/test_device_registry.py | 20 ++++++++++++-------- 4 files changed, 45 insertions(+), 14 deletions(-) diff --git a/homeassistant/exceptions.py b/homeassistant/exceptions.py index fba00e094cdf2..a081cfe3cc2c5 100644 --- a/homeassistant/exceptions.py +++ b/homeassistant/exceptions.py @@ -183,3 +183,18 @@ def __init__(self, value: str, property_name: str, max_length: int) -> None: self.value = value self.property_name = property_name self.max_length = max_length + + +class RequiredParameterMissing(HomeAssistantError): + """Raised when a required parameter is missing from a function call.""" + + def __init__(self, parameter_names: list[str]) -> None: + """Initialize error.""" + super().__init__( + self, + ( + "Call must include at least one of the following parameters: " + f"{', '.join(parameter_names)}" + ), + ) + self.parameter_names = parameter_names diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index e0e5130a94f27..80c54ed296f52 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -10,6 +10,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STARTED from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.exceptions import RequiredParameterMissing from homeassistant.loader import bind_hass import homeassistant.util.uuid as uuid_util @@ -259,10 +260,10 @@ def async_get_or_create( # To disable a device if it gets created disabled_by: str | None | UndefinedType = UNDEFINED, suggested_area: str | None | UndefinedType = UNDEFINED, - ) -> DeviceEntry | None: + ) -> DeviceEntry: """Get device. Create if it doesn't exist.""" if not identifiers and not connections: - return None + raise RequiredParameterMissing(["identifiers", "connections"]) if identifiers is None: identifiers = set() @@ -300,7 +301,7 @@ def async_get_or_create( else: via_device_id = UNDEFINED - return self._async_update_device( + device = self._async_update_device( device.id, add_config_entry_id=config_entry_id, via_device_id=via_device_id, @@ -315,6 +316,11 @@ def async_get_or_create( suggested_area=suggested_area, ) + # This is safe because _async_update_device will always return a device + # in this use case. + assert device + return device + @callback def async_update_device( self, diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 490a5a2298cdb..25996c81d9d32 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -24,7 +24,11 @@ split_entity_id, valid_entity_id, ) -from homeassistant.exceptions import HomeAssistantError, PlatformNotReady +from homeassistant.exceptions import ( + HomeAssistantError, + PlatformNotReady, + RequiredParameterMissing, +) from homeassistant.helpers import ( config_validation as cv, device_registry as dev_reg, @@ -434,9 +438,11 @@ async def _async_add_entity( # type: ignore[no-untyped-def] if key in device_info: processed_dev_info[key] = device_info[key] - device = device_registry.async_get_or_create(**processed_dev_info) - if device: + try: + device = device_registry.async_get_or_create(**processed_dev_info) device_id = device.id + except RequiredParameterMissing: + pass disabled_by: str | None = None if not entity.entity_registry_enabled_default: diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index c5328000269b1..1a768662fc772 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -6,6 +6,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STARTED from homeassistant.core import CoreState, callback +from homeassistant.exceptions import RequiredParameterMissing from homeassistant.helpers import device_registry, entity_registry from tests.common import ( @@ -114,18 +115,21 @@ async def test_requirement_for_identifier_or_connection(registry): manufacturer="manufacturer", model="model", ) - entry3 = registry.async_get_or_create( - config_entry_id="1234", - connections=set(), - identifiers=set(), - manufacturer="manufacturer", - model="model", - ) assert len(registry.devices) == 2 assert entry assert entry2 - assert entry3 is None + + with pytest.raises(RequiredParameterMissing) as exc_info: + registry.async_get_or_create( + config_entry_id="1234", + connections=set(), + identifiers=set(), + manufacturer="manufacturer", + model="model", + ) + + assert exc_info.value.parameter_names == ["identifiers", "connections"] async def test_multiple_config_entries(registry): From 916ba0be118efd6fb1fe25f9552110dbe2e963f3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 13 Apr 2021 15:09:50 +0200 Subject: [PATCH 0241/1317] Don't receive homeassistant_* events from MQTT eventstream (#49158) --- .../components/mqtt_eventstream/__init__.py | 22 +++++++++++-- .../components/mqtt_eventstream/test_init.py | 31 ++++++++++++++++++- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mqtt_eventstream/__init__.py b/homeassistant/components/mqtt_eventstream/__init__.py index 328b9395eeac3..d31d6d1cd537d 100644 --- a/homeassistant/components/mqtt_eventstream/__init__.py +++ b/homeassistant/components/mqtt_eventstream/__init__.py @@ -7,6 +7,11 @@ from homeassistant.const import ( ATTR_SERVICE_DATA, EVENT_CALL_SERVICE, + EVENT_HOMEASSISTANT_CLOSE, + EVENT_HOMEASSISTANT_FINAL_WRITE, + EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STARTED, + EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL, @@ -37,6 +42,14 @@ extra=vol.ALLOW_EXTRA, ) +BLOCKED_EVENTS = [ + EVENT_HOMEASSISTANT_CLOSE, + EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STARTED, + EVENT_HOMEASSISTANT_STOP, + EVENT_HOMEASSISTANT_FINAL_WRITE, +] + async def async_setup(hass, config): """Set up the MQTT eventstream component.""" @@ -45,16 +58,15 @@ async def async_setup(hass, config): pub_topic = conf.get(CONF_PUBLISH_TOPIC) sub_topic = conf.get(CONF_SUBSCRIBE_TOPIC) ignore_event = conf.get(CONF_IGNORE_EVENT) + ignore_event.append(EVENT_TIME_CHANGED) @callback def _event_publisher(event): """Handle events by publishing them on the MQTT queue.""" if event.origin != EventOrigin.local: return - if event.event_type == EVENT_TIME_CHANGED: - return - # User-defined events to ignore + # Events to ignore if event.event_type in ignore_event: return @@ -84,6 +96,10 @@ def _event_receiver(msg): event_type = event.get("event_type") event_data = event.get("event_data") + # Don't fire HOMEASSISTANT_* events on this instance + if event_type in BLOCKED_EVENTS: + return + # Special case handling for event STATE_CHANGED # We will try to convert state dicts back to State objects # Copied over from the _handle_api_post_events_event method diff --git a/tests/components/mqtt_eventstream/test_init.py b/tests/components/mqtt_eventstream/test_init.py index 7f6b22bda90b5..6a1633cb111f4 100644 --- a/tests/components/mqtt_eventstream/test_init.py +++ b/tests/components/mqtt_eventstream/test_init.py @@ -3,7 +3,7 @@ from unittest.mock import ANY, patch import homeassistant.components.mqtt_eventstream as eventstream -from homeassistant.const import EVENT_STATE_CHANGED +from homeassistant.const import EVENT_STATE_CHANGED, MATCH_ALL from homeassistant.core import State, callback from homeassistant.helpers.json import JSONEncoder from homeassistant.setup import async_setup_component @@ -114,6 +114,7 @@ async def test_time_event_does_not_send_message(hass, mqtt_mock): mqtt_mock.async_publish.reset_mock() async_fire_time_changed(hass, dt_util.utcnow()) + await hass.async_block_till_done() assert not mqtt_mock.async_publish.called @@ -140,6 +141,33 @@ def listener(_): assert len(calls) == 1 + await hass.async_block_till_done() + + +async def test_receiving_blocked_event_fires_hass_event(hass, mqtt_mock): + """Test the receiving of blocked event does not fire.""" + sub_topic = "foo" + assert await add_eventstream(hass, sub_topic=sub_topic) + await hass.async_block_till_done() + + calls = [] + + @callback + def listener(_): + calls.append(1) + + hass.bus.async_listen(MATCH_ALL, listener) + await hass.async_block_till_done() + + for event in eventstream.BLOCKED_EVENTS: + payload = json.dumps({"event_type": event, "event_data": {}}, cls=JSONEncoder) + async_fire_mqtt_message(hass, sub_topic, payload) + await hass.async_block_till_done() + + assert len(calls) == 0 + + await hass.async_block_till_done() + async def test_ignored_event_doesnt_send_over_stream(hass, mqtt_mock): """Test the ignoring of sending events if defined.""" @@ -159,6 +187,7 @@ async def test_ignored_event_doesnt_send_over_stream(hass, mqtt_mock): # Set a state of an entity mock_state_change_event(hass, State(e_id, "on")) await hass.async_block_till_done() + await hass.async_block_till_done() assert not mqtt_mock.async_publish.called From 0ca3186caf0b1c28d045a0ab413e1dcc254ec085 Mon Sep 17 00:00:00 2001 From: muppet3000 Date: Tue, 13 Apr 2021 14:40:30 +0100 Subject: [PATCH 0242/1317] Add 'mix' system support for Growatt integration (#49026) * Added 'mix' system support for Growatt integration * Changed Growatt "Last Data Update" to a timestamp * Changed Growatt "Last Data Update" to UTC * Accepted suggested change for Growatt "Last Data Update" Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- CODEOWNERS | 2 +- .../components/growatt_server/manifest.json | 4 +- .../components/growatt_server/sensor.py | 251 +++++++++++++++++- requirements_all.txt | 2 +- 4 files changed, 251 insertions(+), 8 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index ff0372b0d1f03..862df7b687a80 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -183,7 +183,7 @@ homeassistant/components/gpsd/* @fabaff homeassistant/components/gree/* @cmroche homeassistant/components/greeneye_monitor/* @jkeljo homeassistant/components/group/* @home-assistant/core -homeassistant/components/growatt_server/* @indykoning +homeassistant/components/growatt_server/* @indykoning @muppet3000 homeassistant/components/guardian/* @bachya homeassistant/components/habitica/* @ASMfreaK @leikoilja homeassistant/components/harmony/* @ehendrix23 @bramkragten @bdraco @mkeesey diff --git a/homeassistant/components/growatt_server/manifest.json b/homeassistant/components/growatt_server/manifest.json index d60f91d191c3d..8da456aa76a69 100644 --- a/homeassistant/components/growatt_server/manifest.json +++ b/homeassistant/components/growatt_server/manifest.json @@ -2,6 +2,6 @@ "domain": "growatt_server", "name": "Growatt", "documentation": "https://www.home-assistant.io/integrations/growatt_server/", - "requirements": ["growattServer==0.1.1"], - "codeowners": ["@indykoning"] + "requirements": ["growattServer==1.0.0"], + "codeowners": ["@indykoning", "@muppet3000"] } diff --git a/homeassistant/components/growatt_server/sensor.py b/homeassistant/components/growatt_server/sensor.py index 86b88872a8ab0..6464dee6729dd 100644 --- a/homeassistant/components/growatt_server/sensor.py +++ b/homeassistant/components/growatt_server/sensor.py @@ -18,27 +18,28 @@ DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_TIMESTAMP, DEVICE_CLASS_VOLTAGE, ELECTRICAL_CURRENT_AMPERE, ENERGY_KILO_WATT_HOUR, FREQUENCY_HERTZ, PERCENTAGE, + POWER_KILO_WATT, POWER_WATT, TEMP_CELSIUS, VOLT, ) import homeassistant.helpers.config_validation as cv -from homeassistant.util import Throttle +from homeassistant.util import Throttle, dt _LOGGER = logging.getLogger(__name__) CONF_PLANT_ID = "plant_id" DEFAULT_PLANT_ID = "0" DEFAULT_NAME = "Growatt" -SCAN_INTERVAL = datetime.timedelta(minutes=5) +SCAN_INTERVAL = datetime.timedelta(minutes=1) # Sensor type order is: Sensor name, Unit of measurement, api data name, additional options - TOTAL_SENSOR_TYPES = { "total_money_today": ("Total money today", CURRENCY_EURO, "plantMoneyText", {}), "total_money_total": ("Money lifetime", CURRENCY_EURO, "totalMoneyText", {}), @@ -345,7 +346,207 @@ ), } -SENSOR_TYPES = {**TOTAL_SENSOR_TYPES, **INVERTER_SENSOR_TYPES, **STORAGE_SENSOR_TYPES} +MIX_SENSOR_TYPES = { + # Values from 'mix_info' API call + "mix_statement_of_charge": ( + "Statement of charge", + PERCENTAGE, + "capacity", + {"device_class": DEVICE_CLASS_BATTERY}, + ), + "mix_battery_charge_today": ( + "Battery charged today", + ENERGY_KILO_WATT_HOUR, + "eBatChargeToday", + {"device_class": DEVICE_CLASS_ENERGY}, + ), + "mix_battery_charge_lifetime": ( + "Lifetime battery charged", + ENERGY_KILO_WATT_HOUR, + "eBatChargeTotal", + {"device_class": DEVICE_CLASS_ENERGY}, + ), + "mix_battery_discharge_today": ( + "Battery discharged today", + ENERGY_KILO_WATT_HOUR, + "eBatDisChargeToday", + {"device_class": DEVICE_CLASS_ENERGY}, + ), + "mix_battery_discharge_lifetime": ( + "Lifetime battery discharged", + ENERGY_KILO_WATT_HOUR, + "eBatDisChargeTotal", + {"device_class": DEVICE_CLASS_ENERGY}, + ), + "mix_solar_generation_today": ( + "Solar energy today", + ENERGY_KILO_WATT_HOUR, + "epvToday", + {"device_class": DEVICE_CLASS_ENERGY}, + ), + "mix_solar_generation_lifetime": ( + "Lifetime solar energy", + ENERGY_KILO_WATT_HOUR, + "epvTotal", + {"device_class": DEVICE_CLASS_ENERGY}, + ), + "mix_battery_discharge_w": ( + "Battery discharging W", + POWER_WATT, + "pDischarge1", + {"device_class": DEVICE_CLASS_POWER}, + ), + "mix_battery_voltage": ( + "Battery voltage", + VOLT, + "vbat", + {"device_class": DEVICE_CLASS_VOLTAGE}, + ), + "mix_pv1_voltage": ( + "PV1 voltage", + VOLT, + "vpv1", + {"device_class": DEVICE_CLASS_VOLTAGE}, + ), + "mix_pv2_voltage": ( + "PV2 voltage", + VOLT, + "vpv2", + {"device_class": DEVICE_CLASS_VOLTAGE}, + ), + # Values from 'mix_totals' API call + "mix_load_consumption_today": ( + "Load consumption today", + ENERGY_KILO_WATT_HOUR, + "elocalLoadToday", + {"device_class": DEVICE_CLASS_ENERGY}, + ), + "mix_load_consumption_lifetime": ( + "Lifetime load consumption", + ENERGY_KILO_WATT_HOUR, + "elocalLoadTotal", + {"device_class": DEVICE_CLASS_ENERGY}, + ), + "mix_export_to_grid_today": ( + "Export to grid today", + ENERGY_KILO_WATT_HOUR, + "etoGridToday", + {"device_class": DEVICE_CLASS_ENERGY}, + ), + "mix_export_to_grid_lifetime": ( + "Lifetime export to grid", + ENERGY_KILO_WATT_HOUR, + "etogridTotal", + {"device_class": DEVICE_CLASS_ENERGY}, + ), + # Values from 'mix_system_status' API call + "mix_battery_charge": ( + "Battery charging", + POWER_KILO_WATT, + "chargePower", + {"device_class": DEVICE_CLASS_POWER}, + ), + "mix_load_consumption": ( + "Load consumption", + POWER_KILO_WATT, + "pLocalLoad", + {"device_class": DEVICE_CLASS_POWER}, + ), + "mix_wattage_pv_1": ( + "PV1 Wattage", + POWER_WATT, + "pPv1", + {"device_class": DEVICE_CLASS_POWER}, + ), + "mix_wattage_pv_2": ( + "PV2 Wattage", + POWER_WATT, + "pPv2", + {"device_class": DEVICE_CLASS_POWER}, + ), + "mix_wattage_pv_all": ( + "All PV Wattage", + POWER_KILO_WATT, + "ppv", + {"device_class": DEVICE_CLASS_POWER}, + ), + "mix_export_to_grid": ( + "Export to grid", + POWER_KILO_WATT, + "pactogrid", + {"device_class": DEVICE_CLASS_POWER}, + ), + "mix_import_from_grid": ( + "Import from grid", + POWER_KILO_WATT, + "pactouser", + {"device_class": DEVICE_CLASS_POWER}, + ), + "mix_battery_discharge_kw": ( + "Battery discharging kW", + POWER_KILO_WATT, + "pdisCharge1", + {"device_class": DEVICE_CLASS_POWER}, + ), + "mix_grid_voltage": ( + "Grid voltage", + VOLT, + "vAc1", + {"device_class": DEVICE_CLASS_VOLTAGE}, + ), + # Values from 'mix_detail' API call + "mix_system_production_today": ( + "System production today (self-consumption + export)", + ENERGY_KILO_WATT_HOUR, + "eCharge", + {"device_class": DEVICE_CLASS_ENERGY}, + ), + "mix_load_consumption_solar_today": ( + "Load consumption today (solar)", + ENERGY_KILO_WATT_HOUR, + "eChargeToday", + {"device_class": DEVICE_CLASS_ENERGY}, + ), + "mix_self_consumption_today": ( + "Self consumption today (solar + battery)", + ENERGY_KILO_WATT_HOUR, + "eChargeToday1", + {"device_class": DEVICE_CLASS_ENERGY}, + ), + "mix_load_consumption_battery_today": ( + "Load consumption today (battery)", + ENERGY_KILO_WATT_HOUR, + "echarge1", + {"device_class": DEVICE_CLASS_ENERGY}, + ), + "mix_import_from_grid_today": ( + "Import from grid today (load)", + ENERGY_KILO_WATT_HOUR, + "etouser", + {"device_class": DEVICE_CLASS_ENERGY}, + ), + # This sensor is manually created using the most recent X-Axis value from the chartData + "mix_last_update": ( + "Last Data Update", + None, + "lastdataupdate", + {"device_class": DEVICE_CLASS_TIMESTAMP}, + ), + # Values from 'dashboard_data' API call + "mix_import_from_grid_today_combined": ( + "Import from grid today (load + charging)", + ENERGY_KILO_WATT_HOUR, + "etouser_combined", # This id is not present in the raw API data, it is added by the sensor + {"device_class": DEVICE_CLASS_ENERGY}, + ), +} + +SENSOR_TYPES = { + **TOTAL_SENSOR_TYPES, + **INVERTER_SENSOR_TYPES, + **STORAGE_SENSOR_TYPES, + **MIX_SENSOR_TYPES, +} PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -396,6 +597,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): elif device["deviceType"] == "storage": probe.plant_id = plant_id sensors = STORAGE_SENSOR_TYPES + elif device["deviceType"] == "mix": + probe.plant_id = plant_id + sensors = MIX_SENSOR_TYPES else: _LOGGER.debug( "Device type %s was found but is not supported right now", @@ -504,6 +708,45 @@ def update(self): self.plant_id, self.device_id ) self.data = {**storage_info_detail, **storage_energy_overview} + elif self.growatt_type == "mix": + mix_info = self.api.mix_info(self.device_id) + mix_totals = self.api.mix_totals(self.device_id, self.plant_id) + mix_system_status = self.api.mix_system_status( + self.device_id, self.plant_id + ) + + mix_detail = self.api.mix_detail( + self.device_id, self.plant_id, date=datetime.datetime.now() + ) + # Get the chart data and work out the time of the last entry, use this as the last time data was published to the Growatt Server + mix_chart_entries = mix_detail["chartData"] + sorted_keys = sorted(mix_chart_entries) + + # Create datetime from the latest entry + date_now = dt.now().date() + last_updated_time = dt.parse_time(str(sorted_keys[-1])) + combined_timestamp = datetime.datetime.combine( + date_now, last_updated_time + ) + # Convert datetime to UTC + combined_timestamp_utc = dt.as_utc(combined_timestamp) + mix_detail["lastdataupdate"] = combined_timestamp_utc.isoformat() + + # Dashboard data is largely inaccurate for mix system but it is the only call with the ability to return the combined + # imported from grid value that is the combination of charging AND load consumption + dashboard_data = self.api.dashboard_data(self.plant_id) + # Dashboard values have units e.g. "kWh" as part of their returned string, so we remove it + dashboard_values_for_mix = { + # etouser is already used by the results from 'mix_detail' so we rebrand it as 'etouser_combined' + "etouser_combined": dashboard_data["etouser"].replace("kWh", "") + } + self.data = { + **mix_info, + **mix_totals, + **mix_system_status, + **mix_detail, + **dashboard_values_for_mix, + } except json.decoder.JSONDecodeError: _LOGGER.error("Unable to fetch data from Growatt server") diff --git a/requirements_all.txt b/requirements_all.txt index 3d76cd9e1e6f0..6632496219344 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -708,7 +708,7 @@ greeneye_monitor==2.1 greenwavereality==0.5.1 # homeassistant.components.growatt_server -growattServer==0.1.1 +growattServer==1.0.0 # homeassistant.components.gstreamer gstreamer-player==1.1.2 From de569982a45c87f41524befc0386127552c6b6dd Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 13 Apr 2021 16:32:39 +0200 Subject: [PATCH 0243/1317] Fix services for Armcrest & Logi Circle (#49166) --- homeassistant/components/amcrest/camera.py | 37 ++++++++++--------- .../components/logi_circle/__init__.py | 30 ++++++++++----- 2 files changed, 41 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/amcrest/camera.py b/homeassistant/components/amcrest/camera.py index 140069a10249a..92453d24144e4 100644 --- a/homeassistant/components/amcrest/camera.py +++ b/homeassistant/components/amcrest/camera.py @@ -10,7 +10,7 @@ from homeassistant.components.camera import SUPPORT_ON_OFF, SUPPORT_STREAM, Camera from homeassistant.components.ffmpeg import DATA_FFMPEG -from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON +from homeassistant.const import ATTR_ENTITY_ID, CONF_NAME, STATE_OFF, STATE_ON from homeassistant.helpers.aiohttp_client import ( async_aiohttp_proxy_stream, async_aiohttp_proxy_web, @@ -77,26 +77,29 @@ _CBW_BW = "bw" _CBW = [_CBW_COLOR, _CBW_AUTO, _CBW_BW] -_SRV_GOTO_SCHEMA = { - vol.Required(_ATTR_PRESET): vol.All(vol.Coerce(int), vol.Range(min=1)) -} -_SRV_CBW_SCHEMA = {vol.Required(_ATTR_COLOR_BW): vol.In(_CBW)} -_SRV_PTZ_SCHEMA = { - vol.Required(_ATTR_PTZ_MOV): vol.In(_MOV), - vol.Optional(_ATTR_PTZ_TT, default=_DEFAULT_TT): cv.small_float, -} +_SRV_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids}) +_SRV_GOTO_SCHEMA = _SRV_SCHEMA.extend( + {vol.Required(_ATTR_PRESET): vol.All(vol.Coerce(int), vol.Range(min=1))} +) +_SRV_CBW_SCHEMA = _SRV_SCHEMA.extend({vol.Required(_ATTR_COLOR_BW): vol.In(_CBW)}) +_SRV_PTZ_SCHEMA = _SRV_SCHEMA.extend( + { + vol.Required(_ATTR_PTZ_MOV): vol.In(_MOV), + vol.Optional(_ATTR_PTZ_TT, default=_DEFAULT_TT): cv.small_float, + } +) CAMERA_SERVICES = { - _SRV_EN_REC: ({}, "async_enable_recording", ()), - _SRV_DS_REC: ({}, "async_disable_recording", ()), - _SRV_EN_AUD: ({}, "async_enable_audio", ()), - _SRV_DS_AUD: ({}, "async_disable_audio", ()), - _SRV_EN_MOT_REC: ({}, "async_enable_motion_recording", ()), - _SRV_DS_MOT_REC: ({}, "async_disable_motion_recording", ()), + _SRV_EN_REC: (_SRV_SCHEMA, "async_enable_recording", ()), + _SRV_DS_REC: (_SRV_SCHEMA, "async_disable_recording", ()), + _SRV_EN_AUD: (_SRV_SCHEMA, "async_enable_audio", ()), + _SRV_DS_AUD: (_SRV_SCHEMA, "async_disable_audio", ()), + _SRV_EN_MOT_REC: (_SRV_SCHEMA, "async_enable_motion_recording", ()), + _SRV_DS_MOT_REC: (_SRV_SCHEMA, "async_disable_motion_recording", ()), _SRV_GOTO: (_SRV_GOTO_SCHEMA, "async_goto_preset", (_ATTR_PRESET,)), _SRV_CBW: (_SRV_CBW_SCHEMA, "async_set_color_bw", (_ATTR_COLOR_BW,)), - _SRV_TOUR_ON: ({}, "async_start_tour", ()), - _SRV_TOUR_OFF: ({}, "async_stop_tour", ()), + _SRV_TOUR_ON: (_SRV_SCHEMA, "async_start_tour", ()), + _SRV_TOUR_OFF: (_SRV_SCHEMA, "async_stop_tour", ()), _SRV_PTZ_CTRL: ( _SRV_PTZ_SCHEMA, "async_ptz_control", diff --git a/homeassistant/components/logi_circle/__init__.py b/homeassistant/components/logi_circle/__init__.py index c51833bc43f0e..2b6553f9d3203 100644 --- a/homeassistant/components/logi_circle/__init__.py +++ b/homeassistant/components/logi_circle/__init__.py @@ -10,6 +10,7 @@ from homeassistant import config_entries from homeassistant.components.camera import ATTR_FILENAME from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_MODE, CONF_API_KEY, CONF_CLIENT_ID, @@ -72,17 +73,28 @@ extra=vol.ALLOW_EXTRA, ) -LOGI_CIRCLE_SERVICE_SET_CONFIG = { - vol.Required(ATTR_MODE): vol.In([LED_MODE_KEY, RECORDING_MODE_KEY]), - vol.Required(ATTR_VALUE): cv.boolean, -} +LOGI_CIRCLE_SERVICE_SET_CONFIG = vol.Schema( + { + vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, + vol.Required(ATTR_MODE): vol.In([LED_MODE_KEY, RECORDING_MODE_KEY]), + vol.Required(ATTR_VALUE): cv.boolean, + } +) -LOGI_CIRCLE_SERVICE_SNAPSHOT = {vol.Required(ATTR_FILENAME): cv.template} +LOGI_CIRCLE_SERVICE_SNAPSHOT = vol.Schema( + { + vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, + vol.Required(ATTR_FILENAME): cv.template, + } +) -LOGI_CIRCLE_SERVICE_RECORD = { - vol.Required(ATTR_FILENAME): cv.template, - vol.Required(ATTR_DURATION): cv.positive_int, -} +LOGI_CIRCLE_SERVICE_RECORD = vol.Schema( + { + vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, + vol.Required(ATTR_FILENAME): cv.template, + vol.Required(ATTR_DURATION): cv.positive_int, + } +) async def async_setup(hass, config): From fe6d6895aa331de9acbb8bc787739a6b3e7a8655 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 13 Apr 2021 10:37:55 -0400 Subject: [PATCH 0244/1317] Migrate existing zwave_js entities if endpoint has changed (#48963) * Migrate existing zwave_js entities if endpoint has changed * better function name * cleanup code * return as early as we can * use defaultdict instead of setdefault * PR comments * re-add missing logic * set defaultdict outside of for loop * additional cleanup * parametrize tests * fix reinterview logic * test that we skip migration when multiple entities are found * Update tests/components/zwave_js/test_init.py Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- homeassistant/components/zwave_js/__init__.py | 39 +++- homeassistant/components/zwave_js/migrate.py | 182 ++++++++++++--- tests/components/zwave_js/test_init.py | 219 ++++++++++-------- 3 files changed, 306 insertions(+), 134 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 45aef87bf804a..b6f781d4a341f 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from collections import defaultdict from typing import Callable from async_timeout import timeout @@ -87,7 +88,7 @@ def register_node_in_dev_reg( dev_reg: device_registry.DeviceRegistry, client: ZwaveClient, node: ZwaveNode, -) -> None: +) -> device_registry.DeviceEntry: """Register node in dev reg.""" params = { "config_entry_id": entry.entry_id, @@ -103,6 +104,10 @@ def register_node_in_dev_reg( async_dispatcher_send(hass, EVENT_DEVICE_ADDED_TO_REGISTRY, device) + # We can assert here because we will always get a device + assert device + return device + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Z-Wave JS from a config entry.""" @@ -120,6 +125,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry_hass_data[DATA_UNSUBSCRIBE] = unsubscribe_callbacks entry_hass_data[DATA_PLATFORM_SETUP] = {} + registered_unique_ids: dict[str, dict[str, set[str]]] = defaultdict(dict) + async def async_on_node_ready(node: ZwaveNode) -> None: """Handle node ready event.""" LOGGER.debug("Processing node %s", node) @@ -127,26 +134,37 @@ async def async_on_node_ready(node: ZwaveNode) -> None: platform_setup_tasks = entry_hass_data[DATA_PLATFORM_SETUP] # register (or update) node in device registry - register_node_in_dev_reg(hass, entry, dev_reg, client, node) + device = register_node_in_dev_reg(hass, entry, dev_reg, client, node) + # We only want to create the defaultdict once, even on reinterviews + if device.id not in registered_unique_ids: + registered_unique_ids[device.id] = defaultdict(set) # run discovery on all node values and create/update entities for disc_info in async_discover_values(node): + platform = disc_info.platform + # This migration logic was added in 2021.3 to handle a breaking change to # the value_id format. Some time in the future, this call (as well as the # helper functions) can be removed. - async_migrate_discovered_value(ent_reg, client, disc_info) - if disc_info.platform not in platform_setup_tasks: - platform_setup_tasks[disc_info.platform] = hass.async_create_task( - hass.config_entries.async_forward_entry_setup( - entry, disc_info.platform - ) + async_migrate_discovered_value( + hass, + ent_reg, + registered_unique_ids[device.id][platform], + device, + client, + disc_info, + ) + + if platform not in platform_setup_tasks: + platform_setup_tasks[platform] = hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, platform) ) - await platform_setup_tasks[disc_info.platform] + await platform_setup_tasks[platform] LOGGER.debug("Discovered entity: %s", disc_info) async_dispatcher_send( - hass, f"{DOMAIN}_{entry.entry_id}_add_{disc_info.platform}", disc_info + hass, f"{DOMAIN}_{entry.entry_id}_add_{platform}", disc_info ) # add listener for stateless node value notification events @@ -189,6 +207,7 @@ def async_on_node_removed(node: ZwaveNode) -> None: device = dev_reg.async_get_device({dev_id}) # note: removal of entity registry entry is handled by core dev_reg.async_remove_device(device.id) # type: ignore + registered_unique_ids.pop(device.id, None) # type: ignore @callback def async_on_value_notification(notification: ValueNotification) -> None: diff --git a/homeassistant/components/zwave_js/migrate.py b/homeassistant/components/zwave_js/migrate.py index 997d34c844530..ea4b978cab5f6 100644 --- a/homeassistant/components/zwave_js/migrate.py +++ b/homeassistant/components/zwave_js/migrate.py @@ -1,13 +1,20 @@ """Functions used to migrate unique IDs for Z-Wave JS entities.""" from __future__ import annotations +from dataclasses import dataclass import logging from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.model.value import Value as ZwaveValue -from homeassistant.core import callback -from homeassistant.helpers.entity_registry import EntityRegistry +from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import DeviceEntry +from homeassistant.helpers.entity_registry import ( + EntityRegistry, + RegistryEntry, + async_entries_for_device, +) from .const import DOMAIN from .discovery import ZwaveDiscoveryInfo @@ -16,8 +23,88 @@ _LOGGER = logging.getLogger(__name__) +@dataclass +class ValueID: + """Class to represent a Value ID.""" + + command_class: str + endpoint: str + property_: str + property_key: str | None = None + + @staticmethod + def from_unique_id(unique_id: str) -> ValueID: + """ + Get a ValueID from a unique ID. + + This also works for Notification CC Binary Sensors which have their own unique ID + format. + """ + return ValueID.from_string_id(unique_id.split(".")[1]) + + @staticmethod + def from_string_id(value_id_str: str) -> ValueID: + """Get a ValueID from a string representation of the value ID.""" + parts = value_id_str.split("-") + property_key = parts[4] if len(parts) > 4 else None + return ValueID(parts[1], parts[2], parts[3], property_key=property_key) + + def is_same_value_different_endpoints(self, other: ValueID) -> bool: + """Return whether two value IDs are the same excluding endpoint.""" + return ( + self.command_class == other.command_class + and self.property_ == other.property_ + and self.property_key == other.property_key + and self.endpoint != other.endpoint + ) + + +@callback +def async_migrate_old_entity( + hass: HomeAssistant, + ent_reg: EntityRegistry, + registered_unique_ids: set[str], + platform: str, + device: DeviceEntry, + unique_id: str, +) -> None: + """Migrate existing entity if current one can't be found and an old one exists.""" + # If we can find an existing entity with this unique ID, there's nothing to migrate + if ent_reg.async_get_entity_id(platform, DOMAIN, unique_id): + return + + value_id = ValueID.from_unique_id(unique_id) + + # Look for existing entities in the registry that could be the same value but on + # a different endpoint + existing_entity_entries: list[RegistryEntry] = [] + for entry in async_entries_for_device(ent_reg, device.id): + # If entity is not in the domain for this discovery info or entity has already + # been processed, skip it + if entry.domain != platform or entry.unique_id in registered_unique_ids: + continue + + old_ent_value_id = ValueID.from_unique_id(entry.unique_id) + + if value_id.is_same_value_different_endpoints(old_ent_value_id): + existing_entity_entries.append(entry) + # We can return early if we get more than one result + if len(existing_entity_entries) > 1: + return + + # If we couldn't find any results, return early + if not existing_entity_entries: + return + + entry = existing_entity_entries[0] + state = hass.states.get(entry.entity_id) + + if not state or state.state == STATE_UNAVAILABLE: + async_migrate_unique_id(ent_reg, platform, entry.unique_id, unique_id) + + @callback -def async_migrate_entity( +def async_migrate_unique_id( ent_reg: EntityRegistry, platform: str, old_unique_id: str, new_unique_id: str ) -> None: """Check if entity with old unique ID exists, and if so migrate it to new ID.""" @@ -29,10 +116,7 @@ def async_migrate_entity( new_unique_id, ) try: - ent_reg.async_update_entity( - entity_id, - new_unique_id=new_unique_id, - ) + ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id) except ValueError: _LOGGER.debug( ( @@ -46,43 +130,87 @@ def async_migrate_entity( @callback def async_migrate_discovered_value( - ent_reg: EntityRegistry, client: ZwaveClient, disc_info: ZwaveDiscoveryInfo + hass: HomeAssistant, + ent_reg: EntityRegistry, + registered_unique_ids: set[str], + device: DeviceEntry, + client: ZwaveClient, + disc_info: ZwaveDiscoveryInfo, ) -> None: """Migrate unique ID for entity/entities tied to discovered value.""" + new_unique_id = get_unique_id( client.driver.controller.home_id, disc_info.primary_value.value_id, ) + # On reinterviews, there is no point in going through this logic again for already + # discovered values + if new_unique_id in registered_unique_ids: + return + + # Migration logic was added in 2021.3 to handle a breaking change to the value_id + # format. Some time in the future, the logic to migrate unique IDs can be removed. + # 2021.2.*, 2021.3.0b0, and 2021.3.0 formats - for value_id in get_old_value_ids(disc_info.primary_value): - old_unique_id = get_unique_id( + old_unique_ids = [ + get_unique_id( client.driver.controller.home_id, value_id, ) - # Most entities have the same ID format, but notification binary sensors - # have a state key in their ID so we need to handle them differently - if ( - disc_info.platform == "binary_sensor" - and disc_info.platform_hint == "notification" - ): - for state_key in disc_info.primary_value.metadata.states: - # ignore idle key (0) - if state_key == "0": - continue - - async_migrate_entity( + for value_id in get_old_value_ids(disc_info.primary_value) + ] + + if ( + disc_info.platform == "binary_sensor" + and disc_info.platform_hint == "notification" + ): + for state_key in disc_info.primary_value.metadata.states: + # ignore idle key (0) + if state_key == "0": + continue + + new_bin_sensor_unique_id = f"{new_unique_id}.{state_key}" + + # On reinterviews, there is no point in going through this logic again + # for already discovered values + if new_bin_sensor_unique_id in registered_unique_ids: + continue + + # Unique ID migration + for old_unique_id in old_unique_ids: + async_migrate_unique_id( ent_reg, disc_info.platform, f"{old_unique_id}.{state_key}", - f"{new_unique_id}.{state_key}", + new_bin_sensor_unique_id, ) - # Once we've iterated through all state keys, we can move on to the - # next item - continue + # Migrate entities in case upstream changes cause endpoint change + async_migrate_old_entity( + hass, + ent_reg, + registered_unique_ids, + disc_info.platform, + device, + new_bin_sensor_unique_id, + ) + registered_unique_ids.add(new_bin_sensor_unique_id) + + # Once we've iterated through all state keys, we are done + return - async_migrate_entity(ent_reg, disc_info.platform, old_unique_id, new_unique_id) + # Unique ID migration + for old_unique_id in old_unique_ids: + async_migrate_unique_id( + ent_reg, disc_info.platform, old_unique_id, new_unique_id + ) + + # Migrate entities in case upstream changes cause endpoint change + async_migrate_old_entity( + hass, ent_reg, registered_unique_ids, disc_info.platform, device, new_unique_id + ) + registered_unique_ids.add(new_unique_id) @callback diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index 3e7f79b9cec27..32fcdbcc84a7d 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -162,19 +162,27 @@ async def test_unique_id_migration_dupes( entity_entry = ent_reg.async_get(AIR_TEMPERATURE_SENSOR) new_unique_id = f"{client.driver.controller.home_id}.52-49-0-Air temperature" assert entity_entry.unique_id == new_unique_id + assert ent_reg.async_get_entity_id("sensor", DOMAIN, old_unique_id_1) is None + assert ent_reg.async_get_entity_id("sensor", DOMAIN, old_unique_id_2) is None - assert ent_reg.async_get(f"{AIR_TEMPERATURE_SENSOR}_1") is None - -async def test_unique_id_migration_v1(hass, multisensor_6_state, client, integration): - """Test unique ID is migrated from old format to new (version 1).""" +@pytest.mark.parametrize( + "id", + [ + ("52.52-49-00-Air temperature-00"), + ("52.52-49-0-Air temperature-00-00"), + ("52-49-0-Air temperature-00-00"), + ], +) +async def test_unique_id_migration(hass, multisensor_6_state, client, integration, id): + """Test unique ID is migrated from old format to new.""" ent_reg = er.async_get(hass) # Migrate version 1 entity_name = AIR_TEMPERATURE_SENSOR.split(".")[1] # Create entity RegistryEntry using old unique ID format - old_unique_id = f"{client.driver.controller.home_id}.52.52-49-00-Air temperature-00" + old_unique_id = f"{client.driver.controller.home_id}.{id}" entity_entry = ent_reg.async_get_or_create( "sensor", DOMAIN, @@ -197,17 +205,28 @@ async def test_unique_id_migration_v1(hass, multisensor_6_state, client, integra entity_entry = ent_reg.async_get(AIR_TEMPERATURE_SENSOR) new_unique_id = f"{client.driver.controller.home_id}.52-49-0-Air temperature" assert entity_entry.unique_id == new_unique_id + assert ent_reg.async_get_entity_id("sensor", DOMAIN, old_unique_id) is None -async def test_unique_id_migration_v2(hass, multisensor_6_state, client, integration): - """Test unique ID is migrated from old format to new (version 2).""" +@pytest.mark.parametrize( + "id", + [ + ("32.32-50-00-value-W_Consumed"), + ("32.32-50-0-value-66049-W_Consumed"), + ("32-50-0-value-66049-W_Consumed"), + ], +) +async def test_unique_id_migration_property_key( + hass, hank_binary_switch_state, client, integration, id +): + """Test unique ID with property key is migrated from old format to new.""" ent_reg = er.async_get(hass) - # Migrate version 2 - ILLUMINANCE_SENSOR = "sensor.multisensor_6_illuminance" - entity_name = ILLUMINANCE_SENSOR.split(".")[1] + + SENSOR_NAME = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed" + entity_name = SENSOR_NAME.split(".")[1] # Create entity RegistryEntry using old unique ID format - old_unique_id = f"{client.driver.controller.home_id}.52.52-49-0-Illuminance-00-00" + old_unique_id = f"{client.driver.controller.home_id}.{id}" entity_entry = ent_reg.async_get_or_create( "sensor", DOMAIN, @@ -216,40 +235,42 @@ async def test_unique_id_migration_v2(hass, multisensor_6_state, client, integra config_entry=integration, original_name=entity_name, ) - assert entity_entry.entity_id == ILLUMINANCE_SENSOR + assert entity_entry.entity_id == SENSOR_NAME assert entity_entry.unique_id == old_unique_id # Add a ready node, unique ID should be migrated - node = Node(client, multisensor_6_state) + node = Node(client, hank_binary_switch_state) event = {"node": node} client.driver.controller.emit("node added", event) await hass.async_block_till_done() # Check that new RegistryEntry is using new unique ID format - entity_entry = ent_reg.async_get(ILLUMINANCE_SENSOR) - new_unique_id = f"{client.driver.controller.home_id}.52-49-0-Illuminance" + entity_entry = ent_reg.async_get(SENSOR_NAME) + new_unique_id = f"{client.driver.controller.home_id}.32-50-0-value-66049" assert entity_entry.unique_id == new_unique_id + assert ent_reg.async_get_entity_id("sensor", DOMAIN, old_unique_id) is None -async def test_unique_id_migration_v3(hass, multisensor_6_state, client, integration): - """Test unique ID is migrated from old format to new (version 3).""" +async def test_unique_id_migration_notification_binary_sensor( + hass, multisensor_6_state, client, integration +): + """Test unique ID is migrated from old format to new for a notification binary sensor.""" ent_reg = er.async_get(hass) - # Migrate version 2 - ILLUMINANCE_SENSOR = "sensor.multisensor_6_illuminance" - entity_name = ILLUMINANCE_SENSOR.split(".")[1] + + entity_name = NOTIFICATION_MOTION_BINARY_SENSOR.split(".")[1] # Create entity RegistryEntry using old unique ID format - old_unique_id = f"{client.driver.controller.home_id}.52-49-0-Illuminance-00-00" + old_unique_id = f"{client.driver.controller.home_id}.52.52-113-00-Home Security-Motion sensor status.8" entity_entry = ent_reg.async_get_or_create( - "sensor", + "binary_sensor", DOMAIN, old_unique_id, suggested_object_id=entity_name, config_entry=integration, original_name=entity_name, ) - assert entity_entry.entity_id == ILLUMINANCE_SENSOR + assert entity_entry.entity_id == NOTIFICATION_MOTION_BINARY_SENSOR assert entity_entry.unique_id == old_unique_id # Add a ready node, unique ID should be migrated @@ -260,22 +281,29 @@ async def test_unique_id_migration_v3(hass, multisensor_6_state, client, integra await hass.async_block_till_done() # Check that new RegistryEntry is using new unique ID format - entity_entry = ent_reg.async_get(ILLUMINANCE_SENSOR) - new_unique_id = f"{client.driver.controller.home_id}.52-49-0-Illuminance" + entity_entry = ent_reg.async_get(NOTIFICATION_MOTION_BINARY_SENSOR) + new_unique_id = f"{client.driver.controller.home_id}.52-113-0-Home Security-Motion sensor status.8" assert entity_entry.unique_id == new_unique_id + assert ent_reg.async_get_entity_id("binary_sensor", DOMAIN, old_unique_id) is None -async def test_unique_id_migration_property_key_v1( +async def test_old_entity_migration( hass, hank_binary_switch_state, client, integration ): - """Test unique ID with property key is migrated from old format to new (version 1).""" + """Test old entity on a different endpoint is migrated to a new one.""" + node = Node(client, hank_binary_switch_state) + ent_reg = er.async_get(hass) + dev_reg = dr.async_get(hass) + device = dev_reg.async_get_or_create( + config_entry_id=integration.entry_id, identifiers={get_device_id(client, node)} + ) SENSOR_NAME = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed" entity_name = SENSOR_NAME.split(".")[1] - # Create entity RegistryEntry using old unique ID format - old_unique_id = f"{client.driver.controller.home_id}.32.32-50-00-value-W_Consumed" + # Create entity RegistryEntry using fake endpoint + old_unique_id = f"{client.driver.controller.home_id}.32-50-1-value-66049" entity_entry = ent_reg.async_get_or_create( "sensor", DOMAIN, @@ -283,105 +311,98 @@ async def test_unique_id_migration_property_key_v1( suggested_object_id=entity_name, config_entry=integration, original_name=entity_name, + device_id=device.id, ) assert entity_entry.entity_id == SENSOR_NAME assert entity_entry.unique_id == old_unique_id - # Add a ready node, unique ID should be migrated - node = Node(client, hank_binary_switch_state) - event = {"node": node} + # Do this twice to make sure re-interview doesn't do anything weird + for i in range(0, 2): + # Add a ready node, unique ID should be migrated + event = {"node": node} + client.driver.controller.emit("node added", event) + await hass.async_block_till_done() - client.driver.controller.emit("node added", event) - await hass.async_block_till_done() + # Check that new RegistryEntry is using new unique ID format + entity_entry = ent_reg.async_get(SENSOR_NAME) + new_unique_id = f"{client.driver.controller.home_id}.32-50-0-value-66049" + assert entity_entry.unique_id == new_unique_id + assert ent_reg.async_get_entity_id("sensor", DOMAIN, old_unique_id) is None - # Check that new RegistryEntry is using new unique ID format - entity_entry = ent_reg.async_get(SENSOR_NAME) - new_unique_id = f"{client.driver.controller.home_id}.32-50-0-value-66049" - assert entity_entry.unique_id == new_unique_id - -async def test_unique_id_migration_property_key_v2( +async def test_skip_old_entity_migration_for_multiple( hass, hank_binary_switch_state, client, integration ): - """Test unique ID with property key is migrated from old format to new (version 2).""" + """Test that multiple entities of the same value but on a different endpoint get skipped.""" + node = Node(client, hank_binary_switch_state) + ent_reg = er.async_get(hass) + dev_reg = dr.async_get(hass) + device = dev_reg.async_get_or_create( + config_entry_id=integration.entry_id, identifiers={get_device_id(client, node)} + ) SENSOR_NAME = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed" entity_name = SENSOR_NAME.split(".")[1] - # Create entity RegistryEntry using old unique ID format - old_unique_id = ( - f"{client.driver.controller.home_id}.32.32-50-0-value-66049-W_Consumed" - ) + # Create two entity entrrys using different endpoints + old_unique_id_1 = f"{client.driver.controller.home_id}.32-50-1-value-66049" entity_entry = ent_reg.async_get_or_create( "sensor", DOMAIN, - old_unique_id, - suggested_object_id=entity_name, + old_unique_id_1, + suggested_object_id=f"{entity_name}_1", config_entry=integration, - original_name=entity_name, + original_name=f"{entity_name}_1", + device_id=device.id, ) - assert entity_entry.entity_id == SENSOR_NAME - assert entity_entry.unique_id == old_unique_id - - # Add a ready node, unique ID should be migrated - node = Node(client, hank_binary_switch_state) - event = {"node": node} - - client.driver.controller.emit("node added", event) - await hass.async_block_till_done() - - # Check that new RegistryEntry is using new unique ID format - entity_entry = ent_reg.async_get(SENSOR_NAME) - new_unique_id = f"{client.driver.controller.home_id}.32-50-0-value-66049" - assert entity_entry.unique_id == new_unique_id - - -async def test_unique_id_migration_property_key_v3( - hass, hank_binary_switch_state, client, integration -): - """Test unique ID with property key is migrated from old format to new (version 3).""" - ent_reg = er.async_get(hass) - - SENSOR_NAME = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed" - entity_name = SENSOR_NAME.split(".")[1] + assert entity_entry.entity_id == f"{SENSOR_NAME}_1" + assert entity_entry.unique_id == old_unique_id_1 - # Create entity RegistryEntry using old unique ID format - old_unique_id = f"{client.driver.controller.home_id}.32-50-0-value-66049-W_Consumed" + # Create two entity entrrys using different endpoints + old_unique_id_2 = f"{client.driver.controller.home_id}.32-50-2-value-66049" entity_entry = ent_reg.async_get_or_create( "sensor", DOMAIN, - old_unique_id, - suggested_object_id=entity_name, + old_unique_id_2, + suggested_object_id=f"{entity_name}_2", config_entry=integration, - original_name=entity_name, + original_name=f"{entity_name}_2", + device_id=device.id, ) - assert entity_entry.entity_id == SENSOR_NAME - assert entity_entry.unique_id == old_unique_id - + assert entity_entry.entity_id == f"{SENSOR_NAME}_2" + assert entity_entry.unique_id == old_unique_id_2 # Add a ready node, unique ID should be migrated - node = Node(client, hank_binary_switch_state) event = {"node": node} - client.driver.controller.emit("node added", event) await hass.async_block_till_done() - # Check that new RegistryEntry is using new unique ID format + # Check that new RegistryEntry is created using new unique ID format entity_entry = ent_reg.async_get(SENSOR_NAME) new_unique_id = f"{client.driver.controller.home_id}.32-50-0-value-66049" assert entity_entry.unique_id == new_unique_id + # Check that the old entities stuck around because we skipped the migration step + assert ent_reg.async_get_entity_id("sensor", DOMAIN, old_unique_id_1) + assert ent_reg.async_get_entity_id("sensor", DOMAIN, old_unique_id_2) -async def test_unique_id_migration_notification_binary_sensor( + +async def test_old_entity_migration_notification_binary_sensor( hass, multisensor_6_state, client, integration ): - """Test unique ID is migrated from old format to new for a notification binary sensor.""" + """Test old entity on a different endpoint is migrated to a new one for a notification binary sensor.""" + node = Node(client, multisensor_6_state) + ent_reg = er.async_get(hass) + dev_reg = dr.async_get(hass) + device = dev_reg.async_get_or_create( + config_entry_id=integration.entry_id, identifiers={get_device_id(client, node)} + ) entity_name = NOTIFICATION_MOTION_BINARY_SENSOR.split(".")[1] # Create entity RegistryEntry using old unique ID format - old_unique_id = f"{client.driver.controller.home_id}.52.52-113-00-Home Security-Motion sensor status.8" + old_unique_id = f"{client.driver.controller.home_id}.52-113-1-Home Security-Motion sensor status.8" entity_entry = ent_reg.async_get_or_create( "binary_sensor", DOMAIN, @@ -389,21 +410,25 @@ async def test_unique_id_migration_notification_binary_sensor( suggested_object_id=entity_name, config_entry=integration, original_name=entity_name, + device_id=device.id, ) assert entity_entry.entity_id == NOTIFICATION_MOTION_BINARY_SENSOR assert entity_entry.unique_id == old_unique_id - # Add a ready node, unique ID should be migrated - node = Node(client, multisensor_6_state) - event = {"node": node} - - client.driver.controller.emit("node added", event) - await hass.async_block_till_done() - - # Check that new RegistryEntry is using new unique ID format - entity_entry = ent_reg.async_get(NOTIFICATION_MOTION_BINARY_SENSOR) - new_unique_id = f"{client.driver.controller.home_id}.52-113-0-Home Security-Motion sensor status.8" - assert entity_entry.unique_id == new_unique_id + # Do this twice to make sure re-interview doesn't do anything weird + for _ in range(0, 2): + # Add a ready node, unique ID should be migrated + event = {"node": node} + client.driver.controller.emit("node added", event) + await hass.async_block_till_done() + + # Check that new RegistryEntry is using new unique ID format + entity_entry = ent_reg.async_get(NOTIFICATION_MOTION_BINARY_SENSOR) + new_unique_id = f"{client.driver.controller.home_id}.52-113-0-Home Security-Motion sensor status.8" + assert entity_entry.unique_id == new_unique_id + assert ( + ent_reg.async_get_entity_id("binary_sensor", DOMAIN, old_unique_id) is None + ) async def test_on_node_added_not_ready( From 82790cd28d8de6996f6e7b0dfaf467764a1d954a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 13 Apr 2021 04:51:56 -1000 Subject: [PATCH 0245/1317] Do not compile static templates (#49148) self._compiled_code is unreachable if self.is_static --- homeassistant/helpers/template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 83c347c7cb234..7909572bede99 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -330,7 +330,7 @@ def _env(self) -> TemplateEnvironment: def ensure_valid(self) -> None: """Return if template is valid.""" - if self._compiled_code is not None: + if self.is_static or self._compiled_code is not None: return try: From beea2dd35f71e76a862bb61a9c5fd344796e57cb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 13 Apr 2021 07:58:44 -0700 Subject: [PATCH 0246/1317] Internally work with modern config syntax for template binary sensor platform config (#48981) --- .../components/template/binary_sensor.py | 173 ++++++++++++++---- .../components/template/test_binary_sensor.py | 16 +- 2 files changed, 150 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index 1088652cd0a0b..42f23b23336f3 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -1,4 +1,6 @@ """Support for exposing a templated binary sensor.""" +from __future__ import annotations + import voluptuous as vol from homeassistant.components.binary_sensor import ( @@ -12,26 +14,64 @@ ATTR_FRIENDLY_NAME, CONF_DEVICE_CLASS, CONF_ENTITY_PICTURE_TEMPLATE, + CONF_FRIENDLY_NAME, + CONF_FRIENDLY_NAME_TEMPLATE, + CONF_ICON, CONF_ICON_TEMPLATE, + CONF_NAME, CONF_SENSORS, + CONF_STATE, CONF_UNIQUE_ID, + CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import TemplateError +from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.event import async_call_later -from homeassistant.helpers.template import result_as_boolean -from .const import CONF_AVAILABILITY_TEMPLATE +from .const import ( + CONF_ATTRIBUTES, + CONF_AVAILABILITY, + CONF_AVAILABILITY_TEMPLATE, + CONF_OBJECT_ID, + CONF_PICTURE, +) from .template_entity import TemplateEntity CONF_DELAY_ON = "delay_on" CONF_DELAY_OFF = "delay_off" CONF_ATTRIBUTE_TEMPLATES = "attribute_templates" -SENSOR_SCHEMA = vol.All( +LEGACY_FIELDS = { + CONF_ICON_TEMPLATE: CONF_ICON, + CONF_ENTITY_PICTURE_TEMPLATE: CONF_PICTURE, + CONF_AVAILABILITY_TEMPLATE: CONF_AVAILABILITY, + CONF_ATTRIBUTE_TEMPLATES: CONF_ATTRIBUTES, + CONF_FRIENDLY_NAME_TEMPLATE: CONF_NAME, + CONF_FRIENDLY_NAME: CONF_NAME, + CONF_VALUE_TEMPLATE: CONF_STATE, +} + +BINARY_SENSOR_SCHEMA = vol.Schema( + { + vol.Optional(CONF_NAME): cv.template, + vol.Required(CONF_STATE): cv.template, + vol.Optional(CONF_ICON): cv.template, + vol.Optional(CONF_PICTURE): cv.template, + vol.Optional(CONF_AVAILABILITY): cv.template, + vol.Optional(CONF_ATTRIBUTES): vol.Schema({cv.string: cv.template}), + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_UNIQUE_ID): cv.string, + vol.Optional(CONF_DELAY_ON): vol.Any(cv.positive_time_period, cv.template), + vol.Optional(CONF_DELAY_OFF): vol.Any(cv.positive_time_period, cv.template), + } +) + +LEGACY_BINARY_SENSOR_SCHEMA = vol.All( cv.deprecated(ATTR_ENTITY_ID), vol.Schema( { @@ -52,51 +92,85 @@ ), ) + +def rewrite_legacy_to_modern_conf(cfg: dict[str, dict]) -> list[dict]: + """Rewrite legacy binary sensor definitions to modern ones.""" + sensors = [] + + for object_id, entity_cfg in cfg.items(): + entity_cfg = {**entity_cfg, CONF_OBJECT_ID: object_id} + + for from_key, to_key in LEGACY_FIELDS.items(): + if from_key not in entity_cfg or to_key in entity_cfg: + continue + + val = entity_cfg.pop(from_key) + if isinstance(val, str): + val = template.Template(val) + entity_cfg[to_key] = val + + if CONF_NAME not in entity_cfg: + entity_cfg[CONF_NAME] = template.Template(object_id) + + sensors.append(entity_cfg) + + return sensors + + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Required(CONF_SENSORS): cv.schema_with_slug_keys(SENSOR_SCHEMA)} + { + vol.Required(CONF_SENSORS): cv.schema_with_slug_keys( + LEGACY_BINARY_SENSOR_SCHEMA + ), + } ) -async def _async_create_entities(hass, config): +@callback +def _async_create_template_tracking_entities(async_add_entities, hass, definitions): """Create the template binary sensors.""" sensors = [] - for device, device_config in config[CONF_SENSORS].items(): - value_template = device_config[CONF_VALUE_TEMPLATE] - icon_template = device_config.get(CONF_ICON_TEMPLATE) - entity_picture_template = device_config.get(CONF_ENTITY_PICTURE_TEMPLATE) - availability_template = device_config.get(CONF_AVAILABILITY_TEMPLATE) - attribute_templates = device_config.get(CONF_ATTRIBUTE_TEMPLATES, {}) + for entity_conf in definitions: + # Still available on legacy + object_id = entity_conf.get(CONF_OBJECT_ID) - friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device) - device_class = device_config.get(CONF_DEVICE_CLASS) - delay_on_raw = device_config.get(CONF_DELAY_ON) - delay_off_raw = device_config.get(CONF_DELAY_OFF) - unique_id = device_config.get(CONF_UNIQUE_ID) + value = entity_conf[CONF_STATE] + icon = entity_conf.get(CONF_ICON) + entity_picture = entity_conf.get(CONF_PICTURE) + availability = entity_conf.get(CONF_AVAILABILITY) + attributes = entity_conf.get(CONF_ATTRIBUTES, {}) + friendly_name = entity_conf.get(CONF_NAME) + device_class = entity_conf.get(CONF_DEVICE_CLASS) + delay_on_raw = entity_conf.get(CONF_DELAY_ON) + delay_off_raw = entity_conf.get(CONF_DELAY_OFF) + unique_id = entity_conf.get(CONF_UNIQUE_ID) sensors.append( BinarySensorTemplate( hass, - device, + object_id, friendly_name, device_class, - value_template, - icon_template, - entity_picture_template, - availability_template, + value, + icon, + entity_picture, + availability, delay_on_raw, delay_off_raw, - attribute_templates, + attributes, unique_id, ) ) - return sensors + async_add_entities(sensors) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the template binary sensors.""" - async_add_entities(await _async_create_entities(hass, config)) + _async_create_template_tracking_entities( + async_add_entities, hass, rewrite_legacy_to_modern_conf(config[CONF_SENSORS]) + ) class BinarySensorTemplate(TemplateEntity, BinarySensorEntity): @@ -104,18 +178,18 @@ class BinarySensorTemplate(TemplateEntity, BinarySensorEntity): def __init__( self, - hass, - device, - friendly_name, - device_class, - value_template, - icon_template, - entity_picture_template, - availability_template, + hass: HomeAssistant, + object_id: str | None, + friendly_name: template.Template | None, + device_class: str, + value_template: template.Template, + icon_template: template.Template | None, + entity_picture_template: template.Template | None, + availability_template: template.Template | None, delay_on_raw, delay_off_raw, - attribute_templates, - unique_id, + attribute_templates: dict[str, template.Template], + unique_id: str | None, ): """Initialize the Template binary sensor.""" super().__init__( @@ -124,8 +198,22 @@ def __init__( icon_template=icon_template, entity_picture_template=entity_picture_template, ) - self.entity_id = async_generate_entity_id(ENTITY_ID_FORMAT, device, hass=hass) - self._name = friendly_name + if object_id is not None: + self.entity_id = async_generate_entity_id( + ENTITY_ID_FORMAT, object_id, hass=hass + ) + + self._name: str | None = None + self._friendly_name_template: template.Template | None = friendly_name + + # Try to render the name as it can influence the entity ID + if friendly_name: + friendly_name.hass = hass + try: + self._name = friendly_name.async_render(parse_result=False) + except template.TemplateError: + pass + self._device_class = device_class self._template = value_template self._state = None @@ -139,6 +227,11 @@ def __init__( async def async_added_to_hass(self): """Register callbacks.""" self.add_template_attribute("_state", self._template, None, self._update_state) + if ( + self._friendly_name_template is not None + and not self._friendly_name_template.is_static + ): + self.add_template_attribute("_name", self._friendly_name_template) if self._delay_on_raw is not None: try: @@ -166,7 +259,11 @@ def _update_state(self, result): self._delay_cancel() self._delay_cancel = None - state = None if isinstance(result, TemplateError) else result_as_boolean(result) + state = ( + None + if isinstance(result, TemplateError) + else template.result_as_boolean(result) + ) if state == self._state: return diff --git a/tests/components/template/test_binary_sensor.py b/tests/components/template/test_binary_sensor.py index 76602b394339e..e6bdf83e2ff8f 100644 --- a/tests/components/template/test_binary_sensor.py +++ b/tests/components/template/test_binary_sensor.py @@ -26,13 +26,19 @@ async def test_setup(hass): "sensors": { "test": { "friendly_name": "virtual thingy", - "value_template": "{{ foo }}", + "value_template": "{{ True }}", "device_class": "motion", } }, } } assert await setup.async_setup_component(hass, binary_sensor.DOMAIN, config) + await hass.async_block_till_done() + state = hass.states.get("binary_sensor.test") + assert state is not None + assert state.name == "virtual thingy" + assert state.state == "on" + assert state.attributes["device_class"] == "motion" async def test_setup_no_sensors(hass): @@ -40,6 +46,8 @@ async def test_setup_no_sensors(hass): assert await setup.async_setup_component( hass, binary_sensor.DOMAIN, {"binary_sensor": {"platform": "template"}} ) + await hass.async_block_till_done() + assert len(hass.states.async_entity_ids()) == 0 async def test_setup_invalid_device(hass): @@ -49,6 +57,8 @@ async def test_setup_invalid_device(hass): binary_sensor.DOMAIN, {"binary_sensor": {"platform": "template", "sensors": {"foo bar": {}}}}, ) + await hass.async_block_till_done() + assert len(hass.states.async_entity_ids()) == 0 async def test_setup_invalid_device_class(hass): @@ -68,6 +78,8 @@ async def test_setup_invalid_device_class(hass): } }, ) + await hass.async_block_till_done() + assert len(hass.states.async_entity_ids()) == 0 async def test_setup_invalid_missing_template(hass): @@ -82,6 +94,8 @@ async def test_setup_invalid_missing_template(hass): } }, ) + await hass.async_block_till_done() + assert len(hass.states.async_entity_ids()) == 0 async def test_icon_template(hass): From 0f454bc4566135698293ee8bcfca80a366ee2b65 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 13 Apr 2021 11:32:17 -0400 Subject: [PATCH 0247/1317] Don't assert the device registry entry in zwave_js (#49178) --- homeassistant/components/zwave_js/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index b6f781d4a341f..37d85b81ebe5e 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -104,8 +104,6 @@ def register_node_in_dev_reg( async_dispatcher_send(hass, EVENT_DEVICE_ADDED_TO_REGISTRY, device) - # We can assert here because we will always get a device - assert device return device From e9f0891354236e01fc470b32b766f68ce33820ec Mon Sep 17 00:00:00 2001 From: Andreas Oberritter Date: Tue, 13 Apr 2021 17:49:28 +0200 Subject: [PATCH 0248/1317] Add edl21 OBIS IDs for Holley DTZ541-ZEBA (#49170) --- homeassistant/components/edl21/sensor.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/homeassistant/components/edl21/sensor.py b/homeassistant/components/edl21/sensor.py index 64f78530ffa54..16502632f4f17 100644 --- a/homeassistant/components/edl21/sensor.py +++ b/homeassistant/components/edl21/sensor.py @@ -48,7 +48,10 @@ class EDL21: _OBIS_NAMES = { # A=1: Electricity # C=0: General purpose objects + # D=0: Free ID-numbers for utilities "1-0:0.0.9*255": "Electricity ID", + # D=2: Program entries + "1-0:0.2.0*0": "Configuration program version number", # C=1: Active power + # D=8: Time integral 1 # E=0: Total @@ -68,6 +71,10 @@ class EDL21: "1-0:2.8.1*255": "Negative active energy in tariff T1", # E=2: Rate 2 "1-0:2.8.2*255": "Negative active energy in tariff T2", + # C=14: Supply frequency + # D=7: Instantaneous value + # E=0: Total + "1-0:14.7.0*255": "Supply frequency", # C=15: Active power absolute # D=7: Instantaneous value # E=0: Total @@ -100,12 +107,18 @@ class EDL21: # D=7: Instantaneous value # E=0: Total "1-0:76.7.0*255": "L3 active instantaneous power", + # C=81: Angles + # D=7: Instantaneous value + # E=26: U(L3) x I(L3) + "1-0:81.7.26*255": "U(L3)/I(L3) phase angle", # C=96: Electricity-related service entries "1-0:96.1.0*255": "Metering point ID 1", + "1-0:96.5.0*255": "Internal operating status", } _OBIS_BLACKLIST = { # C=96: Electricity-related service entries "1-0:96.50.1*1", # Manufacturer specific + "1-0:96.90.2*1", # Manufacturer specific # A=129: Manufacturer specific "129-129:199.130.3*255", # Iskraemeco: Manufacturer "129-129:199.130.5*255", # Iskraemeco: Public Key From 926c2489f02f590d193f896fc219b961a958c8dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Klomp?= Date: Tue, 13 Apr 2021 18:21:01 +0200 Subject: [PATCH 0249/1317] Implement SMA config flow (#48003) Co-authored-by: J. Nick Koston Co-authored-by: Johann Kellerman --- .coveragerc | 1 + CODEOWNERS | 2 +- homeassistant/components/sma/__init__.py | 200 ++++++++++++++- homeassistant/components/sma/config_flow.py | 141 +++++++++++ homeassistant/components/sma/const.py | 21 ++ homeassistant/components/sma/manifest.json | 5 +- homeassistant/components/sma/sensor.py | 236 ++++++++---------- homeassistant/components/sma/strings.json | 27 ++ .../components/sma/translations/en.json | 27 ++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/sma/__init__.py | 127 +++++++++- tests/components/sma/test_config_flow.py | 170 +++++++++++++ tests/components/sma/test_sensor.py | 37 +-- 15 files changed, 839 insertions(+), 160 deletions(-) create mode 100644 homeassistant/components/sma/config_flow.py create mode 100644 homeassistant/components/sma/const.py create mode 100644 homeassistant/components/sma/strings.json create mode 100644 homeassistant/components/sma/translations/en.json create mode 100644 tests/components/sma/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 982db1eeade82..2f93792a3f6b7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -899,6 +899,7 @@ omit = homeassistant/components/slack/notify.py homeassistant/components/sinch/* homeassistant/components/slide/* + homeassistant/components/sma/__init__.py homeassistant/components/sma/sensor.py homeassistant/components/smappee/__init__.py homeassistant/components/smappee/api.py diff --git a/CODEOWNERS b/CODEOWNERS index 862df7b687a80..a2ab0082cac26 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -428,7 +428,7 @@ homeassistant/components/sisyphus/* @jkeljo homeassistant/components/sky_hub/* @rogerselwyn homeassistant/components/slack/* @bachya homeassistant/components/slide/* @ualex73 -homeassistant/components/sma/* @kellerza +homeassistant/components/sma/* @kellerza @rklomp homeassistant/components/smappee/* @bsmappee homeassistant/components/smart_meter_texas/* @grahamwetzler homeassistant/components/smarthab/* @outadoc diff --git a/homeassistant/components/sma/__init__.py b/homeassistant/components/sma/__init__.py index 97d7147596c3f..5a4123ec10b6b 100644 --- a/homeassistant/components/sma/__init__.py +++ b/homeassistant/components/sma/__init__.py @@ -1 +1,199 @@ -"""The sma component.""" +"""The sma integration.""" +import asyncio +from datetime import timedelta +import logging + +import pysma + +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry, ConfigEntryNotReady +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PATH, + CONF_SCAN_INTERVAL, + CONF_SENSORS, + CONF_SSL, + CONF_VERIFY_SSL, + EVENT_HOMEASSISTANT_STOP, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import ( + CONF_CUSTOM, + CONF_FACTOR, + CONF_GROUP, + CONF_KEY, + CONF_UNIT, + DEFAULT_SCAN_INTERVAL, + DOMAIN, + PLATFORMS, + PYSMA_COORDINATOR, + PYSMA_OBJECT, + PYSMA_REMOVE_LISTENER, + PYSMA_SENSORS, +) + +_LOGGER = logging.getLogger(__name__) + + +async def _parse_legacy_options(entry: ConfigEntry, sensor_def: pysma.Sensors) -> None: + """Parse legacy configuration options. + + This will parse the legacy CONF_SENSORS and CONF_CUSTOM configuration options + to support deprecated yaml config from platform setup. + """ + + # Add sensors from the custom config + sensor_def.add( + [ + pysma.Sensor(o[CONF_KEY], n, o[CONF_UNIT], o[CONF_FACTOR], o.get(CONF_PATH)) + for n, o in entry.data.get(CONF_CUSTOM).items() + ] + ) + + # Parsing of sensors configuration + config_sensors = entry.data.get(CONF_SENSORS) + if not config_sensors: + return + + # Find and replace sensors removed from pysma + # This only alters the config, the actual sensor migration takes place in _migrate_old_unique_ids + for sensor in config_sensors.copy(): + if sensor in pysma.LEGACY_MAP: + config_sensors.remove(sensor) + config_sensors.append(pysma.LEGACY_MAP[sensor]["new_sensor"]) + + # Only sensors from config should be enabled + for sensor in sensor_def: + sensor.enabled = sensor.name in config_sensors + + +async def _migrate_old_unique_ids( + hass: HomeAssistant, entry: ConfigEntry, sensor_def: pysma.Sensors +) -> None: + """Migrate legacy sensor entity_id format to new format.""" + entity_registry = er.async_get(hass) + + # Create list of all possible sensor names + possible_sensors = list( + set( + entry.data.get(CONF_SENSORS) + + [s.name for s in sensor_def] + + list(pysma.LEGACY_MAP) + ) + ) + + for sensor in possible_sensors: + if sensor in sensor_def: + pysma_sensor = sensor_def[sensor] + original_key = pysma_sensor.key + elif sensor in pysma.LEGACY_MAP: + # If sensor was removed from pysma we will remap it to the new sensor + legacy_sensor = pysma.LEGACY_MAP[sensor] + pysma_sensor = sensor_def[legacy_sensor["new_sensor"]] + original_key = legacy_sensor["old_key"] + else: + _LOGGER.error("%s does not exist", sensor) + continue + + # Find entity_id using previous format of unique ID + entity_id = entity_registry.async_get_entity_id( + "sensor", "sma", f"sma-{original_key}-{sensor}" + ) + + if not entity_id: + continue + + # Change entity_id to new format using the device serial in entry.unique_id + new_unique_id = f"{entry.unique_id}-{pysma_sensor.key}_{pysma_sensor.key_idx}" + entity_registry.async_update_entity(entity_id, new_unique_id=new_unique_id) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up sma from a config entry.""" + # Init all default sensors + sensor_def = pysma.Sensors() + + if entry.source == SOURCE_IMPORT: + await _parse_legacy_options(entry, sensor_def) + await _migrate_old_unique_ids(hass, entry, sensor_def) + + # Init the SMA interface + protocol = "https" if entry.data.get(CONF_SSL) else "http" + url = f"{protocol}://{entry.data.get(CONF_HOST)}" + verify_ssl = entry.data.get(CONF_VERIFY_SSL) + group = entry.data.get(CONF_GROUP) + password = entry.data.get(CONF_PASSWORD) + + session = async_get_clientsession(hass, verify_ssl=verify_ssl) + sma = pysma.SMA(session, url, password, group) + + # Define the coordinator + async def async_update_data(): + """Update the used SMA sensors.""" + values = await sma.read(sensor_def) + if not values: + raise UpdateFailed + + interval = timedelta( + seconds=entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) + ) + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name="sma", + update_method=async_update_data, + update_interval=interval, + ) + + try: + await coordinator.async_config_entry_first_refresh() + except ConfigEntryNotReady: + await sma.close_session() + raise + + # Ensure we logout on shutdown + async def async_close_session(event): + """Close the session.""" + await sma.close_session() + + remove_stop_listener = hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, async_close_session + ) + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = { + PYSMA_OBJECT: sma, + PYSMA_COORDINATOR: coordinator, + PYSMA_SENSORS: sensor_def, + PYSMA_REMOVE_LISTENER: remove_stop_listener, + } + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + if unload_ok: + data = hass.data[DOMAIN].pop(entry.entry_id) + await data[PYSMA_OBJECT].close_session() + data[PYSMA_REMOVE_LISTENER]() + + return unload_ok diff --git a/homeassistant/components/sma/config_flow.py b/homeassistant/components/sma/config_flow.py new file mode 100644 index 0000000000000..08c1aed2e7bbd --- /dev/null +++ b/homeassistant/components/sma/config_flow.py @@ -0,0 +1,141 @@ +"""Config flow for the sma integration.""" +from __future__ import annotations + +import logging +from typing import Any + +import aiohttp +import pysma +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_SENSORS, + CONF_SSL, + CONF_VERIFY_SSL, +) +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv + +from .const import CONF_CUSTOM, CONF_GROUP, DEVICE_INFO, GROUPS +from .const import DOMAIN # pylint: disable=unused-import + +_LOGGER = logging.getLogger(__name__) + + +async def validate_input( + hass: core.HomeAssistant, data: dict[str, Any] +) -> dict[str, Any]: + """Validate the user input allows us to connect.""" + session = async_get_clientsession(hass, verify_ssl=data[CONF_VERIFY_SSL]) + + protocol = "https" if data[CONF_SSL] else "http" + url = f"{protocol}://{data[CONF_HOST]}" + + sma = pysma.SMA(session, url, data[CONF_PASSWORD], group=data[CONF_GROUP]) + + if await sma.new_session() is False: + raise InvalidAuth + + device_info = await sma.device_info() + await sma.close_session() + + if not device_info: + raise CannotRetrieveDeviceInfo + + return device_info + + +class SmaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for SMA.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + def __init__(self) -> None: + """Initialize.""" + self._data = { + CONF_HOST: vol.UNDEFINED, + CONF_SSL: False, + CONF_VERIFY_SSL: True, + CONF_GROUP: GROUPS[0], + CONF_PASSWORD: vol.UNDEFINED, + CONF_SENSORS: [], + CONF_CUSTOM: {}, + DEVICE_INFO: {}, + } + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> dict[str, Any]: + """First step in config flow.""" + errors = {} + if user_input is not None: + self._data[CONF_HOST] = user_input[CONF_HOST] + self._data[CONF_SSL] = user_input[CONF_SSL] + self._data[CONF_VERIFY_SSL] = user_input[CONF_VERIFY_SSL] + self._data[CONF_GROUP] = user_input[CONF_GROUP] + self._data[CONF_PASSWORD] = user_input[CONF_PASSWORD] + + try: + self._data[DEVICE_INFO] = await validate_input(self.hass, user_input) + except aiohttp.ClientError: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except CannotRetrieveDeviceInfo: + errors["base"] = "cannot_retrieve_device_info" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + if not errors: + await self.async_set_unique_id(self._data[DEVICE_INFO]["serial"]) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=self._data[CONF_HOST], data=self._data + ) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_HOST, default=self._data[CONF_HOST]): cv.string, + vol.Optional(CONF_SSL, default=self._data[CONF_SSL]): cv.boolean, + vol.Optional( + CONF_VERIFY_SSL, default=self._data[CONF_VERIFY_SSL] + ): cv.boolean, + vol.Optional(CONF_GROUP, default=self._data[CONF_GROUP]): vol.In( + GROUPS + ), + vol.Required(CONF_PASSWORD): cv.string, + } + ), + errors=errors, + ) + + async def async_step_import( + self, import_config: dict[str, Any] | None + ) -> dict[str, Any]: + """Import a config flow from configuration.""" + device_info = await validate_input(self.hass, import_config) + import_config[DEVICE_INFO] = device_info + + # If unique is configured import was already run + # This means remap was already done, so we can abort + await self.async_set_unique_id(device_info["serial"]) + self._abort_if_unique_id_configured(import_config) + + return self.async_create_entry( + title=import_config[CONF_HOST], data=import_config + ) + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" + + +class CannotRetrieveDeviceInfo(exceptions.HomeAssistantError): + """Error to indicate we cannot retrieve the device information.""" diff --git a/homeassistant/components/sma/const.py b/homeassistant/components/sma/const.py new file mode 100644 index 0000000000000..2e1086e48a2a4 --- /dev/null +++ b/homeassistant/components/sma/const.py @@ -0,0 +1,21 @@ +"""Constants for the sma integration.""" + +DOMAIN = "sma" + +PYSMA_COORDINATOR = "coordinator" +PYSMA_OBJECT = "pysma" +PYSMA_REMOVE_LISTENER = "remove_listener" +PYSMA_SENSORS = "pysma_sensors" + +PLATFORMS = ["sensor"] + +CONF_CUSTOM = "custom" +CONF_FACTOR = "factor" +CONF_GROUP = "group" +CONF_KEY = "key" +CONF_UNIT = "unit" +DEVICE_INFO = "device_info" + +DEFAULT_SCAN_INTERVAL = 5 + +GROUPS = ["user", "installer"] diff --git a/homeassistant/components/sma/manifest.json b/homeassistant/components/sma/manifest.json index 9cadec377a24c..f38038d8eb1b0 100644 --- a/homeassistant/components/sma/manifest.json +++ b/homeassistant/components/sma/manifest.json @@ -1,7 +1,8 @@ { "domain": "sma", "name": "SMA Solar", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sma", - "requirements": ["pysma==0.3.5"], - "codeowners": ["@kellerza"] + "requirements": ["pysma==0.4.3"], + "codeowners": ["@kellerza", "@rklomp"] } diff --git a/homeassistant/components/sma/sensor.py b/homeassistant/components/sma/sensor.py index 2290f3a330f9f..ea5b5666408c5 100644 --- a/homeassistant/components/sma/sensor.py +++ b/homeassistant/components/sma/sensor.py @@ -1,41 +1,51 @@ """SMA Solar Webconnect interface.""" -from datetime import timedelta +from __future__ import annotations + import logging +from typing import Any, Callable, Coroutine import pysma import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, CONF_PATH, - CONF_SCAN_INTERVAL, CONF_SENSORS, CONF_SSL, CONF_VERIFY_SSL, - EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.core import callback -from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import async_track_time_interval - -_LOGGER = logging.getLogger(__name__) +from homeassistant.helpers.typing import StateType +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) -CONF_CUSTOM = "custom" -CONF_FACTOR = "factor" -CONF_GROUP = "group" -CONF_KEY = "key" -CONF_UNIT = "unit" +from .const import ( + CONF_CUSTOM, + CONF_FACTOR, + CONF_GROUP, + CONF_KEY, + CONF_UNIT, + DEVICE_INFO, + DOMAIN, + GROUPS, + PYSMA_COORDINATOR, + PYSMA_SENSORS, +) -GROUPS = ["user", "installer"] +_LOGGER = logging.getLogger(__name__) -def _check_sensor_schema(conf): +def _check_sensor_schema(conf: dict[str, Any]) -> dict[str, Any]: """Check sensors and attributes are valid.""" try: valid = [s.name for s in pysma.Sensors()] + valid += pysma.LEGACY_MAP.keys() except (ImportError, AttributeError): return conf @@ -83,146 +93,114 @@ def _check_sensor_schema(conf): ) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up SMA WebConnect sensor.""" - # Check config again during load - dependency available - config = _check_sensor_schema(config) - - # Init all default sensors - sensor_def = pysma.Sensors() - - # Sensor from the custom config - sensor_def.add( - [ - pysma.Sensor(o[CONF_KEY], n, o[CONF_UNIT], o[CONF_FACTOR], o.get(CONF_PATH)) - for n, o in config[CONF_CUSTOM].items() - ] +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigEntry, + async_add_entities: Callable[[], Coroutine], + discovery_info=None, +) -> None: + """Import the platform into a config entry.""" + _LOGGER.warning( + "Loading SMA via platform setup is deprecated. " + "Please remove it from your configuration" ) - # Use all sensors by default - config_sensors = config[CONF_SENSORS] - hass_sensors = [] - used_sensors = [] - - if isinstance(config_sensors, dict): # will be remove from 0.99 - if not config_sensors: # Use all sensors by default - config_sensors = {s.name: [] for s in sensor_def} - - # Prepare all Home Assistant sensor entities - for name, attr in config_sensors.items(): - sub_sensors = [sensor_def[s] for s in attr] - hass_sensors.append(SMAsensor(sensor_def[name], sub_sensors)) - used_sensors.append(name) - used_sensors.extend(attr) - - if isinstance(config_sensors, list): - if not config_sensors: # Use all sensors by default - config_sensors = [s.name for s in sensor_def] - used_sensors = list(set(config_sensors + list(config[CONF_CUSTOM]))) - for sensor in used_sensors: - hass_sensors.append(SMAsensor(sensor_def[sensor], [])) - - used_sensors = [sensor_def[s] for s in set(used_sensors)] - async_add_entities(hass_sensors) - - # Init the SMA interface - session = async_get_clientsession(hass, verify_ssl=config[CONF_VERIFY_SSL]) - grp = config[CONF_GROUP] - - protocol = "https" if config[CONF_SSL] else "http" - url = f"{protocol}://{config[CONF_HOST]}" - - sma = pysma.SMA(session, url, config[CONF_PASSWORD], group=grp) - - # Ensure we logout on shutdown - async def async_close_session(event): - """Close the session.""" - await sma.close_session() - - hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, async_close_session) - - backoff = 0 - backoff_step = 0 - - async def async_sma(event): - """Update all the SMA sensors.""" - nonlocal backoff, backoff_step - if backoff > 1: - backoff -= 1 - return + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config + ) + ) - values = await sma.read(used_sensors) - if not values: - try: - backoff = [1, 1, 1, 6, 30][backoff_step] - backoff_step += 1 - except IndexError: - backoff = 60 - return - backoff_step = 0 - for sensor in hass_sensors: - sensor.async_update_values() +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: Callable[[], Coroutine], +) -> None: + """Set up SMA sensors.""" + sma_data = hass.data[DOMAIN][config_entry.entry_id] + + coordinator = sma_data[PYSMA_COORDINATOR] + used_sensors = sma_data[PYSMA_SENSORS] + + entities = [] + for sensor in used_sensors: + entities.append( + SMAsensor( + coordinator, + config_entry.unique_id, + config_entry.data[DEVICE_INFO], + sensor, + ) + ) - interval = config.get(CONF_SCAN_INTERVAL) or timedelta(seconds=5) - async_track_time_interval(hass, async_sma, interval) + async_add_entities(entities) -class SMAsensor(SensorEntity): +class SMAsensor(CoordinatorEntity, SensorEntity): """Representation of a SMA sensor.""" - def __init__(self, pysma_sensor, sub_sensors): + def __init__( + self, + coordinator: DataUpdateCoordinator, + config_entry_unique_id: str, + device_info: dict[str, Any], + pysma_sensor: pysma.Sensor, + ) -> None: """Initialize the sensor.""" + super().__init__(coordinator) self._sensor = pysma_sensor - self._sub_sensors = sub_sensors # Can be remove from 0.99 + self._enabled_default = self._sensor.enabled + self._config_entry_unique_id = config_entry_unique_id + self._device_info = device_info - self._attr = {s.name: "" for s in sub_sensors} - self._state = self._sensor.value + # Set sensor enabled to False. + # Will be enabled by async_added_to_hass if actually used. + self._sensor.enabled = False @property - def name(self): + def name(self) -> str: """Return the name of the sensor.""" return self._sensor.name @property - def state(self): + def state(self) -> StateType: """Return the state of the sensor.""" - return self._state + return self._sensor.value @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str | None: """Return the unit the value is expressed in.""" return self._sensor.unit @property - def extra_state_attributes(self): # Can be remove from 0.99 - """Return the state attributes of the sensor.""" - return self._attr + def unique_id(self) -> str: + """Return a unique identifier for this sensor.""" + return ( + f"{self._config_entry_unique_id}-{self._sensor.key}_{self._sensor.key_idx}" + ) @property - def poll(self): - """SMA sensors are updated & don't poll.""" - return False - - @callback - def async_update_values(self): - """Update this sensor.""" - update = False - - for sens in self._sub_sensors: # Can be remove from 0.99 - newval = f"{sens.value} {sens.unit}" - if self._attr[sens.name] != newval: - update = True - self._attr[sens.name] = newval - - if self._sensor.value != self._state: - update = True - self._state = self._sensor.value - - if update: - self.async_write_ha_state() + def device_info(self) -> dict[str, Any]: + """Return the device information.""" + return { + "identifiers": {(DOMAIN, self._config_entry_unique_id)}, + "name": self._device_info["name"], + "manufacturer": self._device_info["manufacturer"], + "model": self._device_info["type"], + } @property - def unique_id(self): - """Return a unique identifier for this sensor.""" - return f"sma-{self._sensor.key}-{self._sensor.name}" + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + return self._enabled_default + + async def async_added_to_hass(self) -> None: + """Run when entity about to be added to hass.""" + await super().async_added_to_hass() + self._sensor.enabled = True + + async def async_will_remove_from_hass(self) -> None: + """Run when entity will be removed from hass.""" + await super().async_will_remove_from_hass() + self._sensor.enabled = False diff --git a/homeassistant/components/sma/strings.json b/homeassistant/components/sma/strings.json new file mode 100644 index 0000000000000..f5dc6c16c880e --- /dev/null +++ b/homeassistant/components/sma/strings.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]" + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "cannot_retrieve_device_info": "Successfully connected, but unable to retrieve the device information", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "step": { + "user": { + "data": { + "group": "Group", + "host": "[%key:common::config_flow::data::host%]", + "password": "[%key:common::config_flow::data::password%]", + "ssl": "[%key:common::config_flow::data::ssl%]", + "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" + }, + "description": "Enter your SMA device information.", + "title": "Set up SMA Solar" + } + } + } +} diff --git a/homeassistant/components/sma/translations/en.json b/homeassistant/components/sma/translations/en.json new file mode 100644 index 0000000000000..71b8ce55bd57b --- /dev/null +++ b/homeassistant/components/sma/translations/en.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress" + }, + "error": { + "cannot_connect": "Failed to connect", + "cannot_retrieve_device_info": "Successfully connected, but unable to retrieve the device information", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "group": "Group", + "host": "Host", + "password": "Password", + "ssl": "Uses an SSL certificate", + "verify_ssl": "Verify SSL certificate" + }, + "description": "Enter your SMA device information.", + "title": "Set up SMA Solar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 25429296d8e45..151b95a8f2030 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -210,6 +210,7 @@ "shelly", "shopping_list", "simplisafe", + "sma", "smappee", "smart_meter_texas", "smarthab", diff --git a/requirements_all.txt b/requirements_all.txt index 6632496219344..95c521ec8721c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1708,7 +1708,7 @@ pysignalclirestapi==0.3.4 pyskyqhub==0.1.3 # homeassistant.components.sma -pysma==0.3.5 +pysma==0.4.3 # homeassistant.components.smappee pysmappee==0.2.17 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 26c97639e21f9..9d5da4349adbe 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -935,7 +935,7 @@ pyserial==3.5 pysignalclirestapi==0.3.4 # homeassistant.components.sma -pysma==0.3.5 +pysma==0.4.3 # homeassistant.components.smappee pysmappee==0.2.17 diff --git a/tests/components/sma/__init__.py b/tests/components/sma/__init__.py index 124f481135ea9..05e9dc9f4cf41 100644 --- a/tests/components/sma/__init__.py +++ b/tests/components/sma/__init__.py @@ -1 +1,126 @@ -"""SMA tests.""" +"""Tests for the sma integration.""" +from unittest.mock import patch + +from homeassistant.components.sma.const import DOMAIN +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry + +MOCK_DEVICE = { + "manufacturer": "SMA", + "name": "SMA Device Name", + "type": "Sunny Boy 3.6", + "serial": "123456789", +} + +MOCK_USER_INPUT = { + "host": "1.1.1.1", + "ssl": True, + "verify_ssl": False, + "group": "user", + "password": "password", +} + +MOCK_IMPORT = { + "platform": "sma", + "host": "1.1.1.1", + "ssl": True, + "verify_ssl": False, + "group": "user", + "password": "password", + "sensors": ["pv_power", "daily_yield", "total_yield", "not_existing_sensors"], + "custom": { + "yesterday_consumption": { + "factor": 1000.0, + "key": "6400_00543A01", + "unit": "kWh", + } + }, +} + +MOCK_CUSTOM_SENSOR = { + "name": "yesterday_consumption", + "key": "6400_00543A01", + "unit": "kWh", + "factor": 1000, +} + +MOCK_CUSTOM_SENSOR2 = { + "name": "device_type_id", + "key": "6800_08822000", + "unit": "", + "path": '"1"[0].val[0].tag', +} + +MOCK_SETUP_DATA = dict( + { + "custom": {}, + "device_info": MOCK_DEVICE, + "sensors": [], + }, + **MOCK_USER_INPUT, +) + +MOCK_CUSTOM_SETUP_DATA = dict( + { + "custom": { + MOCK_CUSTOM_SENSOR["name"]: { + "factor": MOCK_CUSTOM_SENSOR["factor"], + "key": MOCK_CUSTOM_SENSOR["key"], + "path": None, + "unit": MOCK_CUSTOM_SENSOR["unit"], + }, + MOCK_CUSTOM_SENSOR2["name"]: { + "factor": 1.0, + "key": MOCK_CUSTOM_SENSOR2["key"], + "path": MOCK_CUSTOM_SENSOR2["path"], + "unit": MOCK_CUSTOM_SENSOR2["unit"], + }, + }, + "device_info": MOCK_DEVICE, + "sensors": [], + }, + **MOCK_USER_INPUT, +) + +MOCK_LEGACY_ENTRY = er.RegistryEntry( + entity_id="sensor.pv_power", + unique_id="sma-6100_0046C200-pv_power", + platform="sma", + unit_of_measurement="W", + original_name="pv_power", +) + + +async def init_integration(hass): + """Create a fake SMA Config Entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + title=MOCK_DEVICE["name"], + unique_id=MOCK_DEVICE["serial"], + data=MOCK_CUSTOM_SETUP_DATA, + source="import", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.helpers.update_coordinator.DataUpdateCoordinator.async_config_entry_first_refresh" + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + return entry + + +def _patch_validate_input(return_value=MOCK_DEVICE, side_effect=None): + return patch( + "homeassistant.components.sma.config_flow.validate_input", + return_value=return_value, + side_effect=side_effect, + ) + + +def _patch_async_setup_entry(return_value=True): + return patch( + "homeassistant.components.sma.async_setup_entry", + return_value=return_value, + ) diff --git a/tests/components/sma/test_config_flow.py b/tests/components/sma/test_config_flow.py new file mode 100644 index 0000000000000..d248b2206dae7 --- /dev/null +++ b/tests/components/sma/test_config_flow.py @@ -0,0 +1,170 @@ +"""Test the sma config flow.""" +from unittest.mock import patch + +import aiohttp + +from homeassistant import setup +from homeassistant.components.sma.const import DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) +from homeassistant.helpers import entity_registry as er + +from . import ( + MOCK_DEVICE, + MOCK_IMPORT, + MOCK_LEGACY_ENTRY, + MOCK_SETUP_DATA, + MOCK_USER_INPUT, + _patch_async_setup_entry, + _patch_validate_input, +) + + +async def test_form(hass, aioclient_mock): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {} + + with patch("pysma.SMA.new_session", return_value=True), patch( + "pysma.SMA.device_info", return_value=MOCK_DEVICE + ), _patch_async_setup_entry() as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_USER_INPUT, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == MOCK_USER_INPUT["host"] + assert result["data"] == MOCK_SETUP_DATA + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_cannot_connect(hass, aioclient_mock): + """Test we handle cannot connect error.""" + aioclient_mock.get("https://1.1.1.1/data/l10n/en-US.json", exc=aiohttp.ClientError) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_USER_INPUT, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_form_invalid_auth(hass, aioclient_mock): + """Test we handle invalid auth error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + with patch("pysma.SMA.new_session", return_value=False): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_USER_INPUT, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {"base": "invalid_auth"} + + +async def test_form_cannot_retrieve_device_info(hass, aioclient_mock): + """Test we handle cannot retrieve device info error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + with patch("pysma.SMA.new_session", return_value=True), patch( + "pysma.SMA.read", return_value=False + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_USER_INPUT, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {"base": "cannot_retrieve_device_info"} + + +async def test_form_unexpected_exception(hass): + """Test we handle unexpected exception.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + with _patch_validate_input(side_effect=Exception): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_USER_INPUT, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {"base": "unknown"} + + +async def test_form_already_configured(hass): + """Test starting a flow by user when already configured.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + with _patch_validate_input(): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_USER_INPUT, + ) + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["result"].unique_id == MOCK_DEVICE["serial"] + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + with _patch_validate_input(): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_USER_INPUT, + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_import(hass): + """Test we can import.""" + entity_registry = er.async_get(hass) + entity_registry._register_entry(MOCK_LEGACY_ENTRY) + + await setup.async_setup_component(hass, "persistent_notification", {}) + + with _patch_validate_input(): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=MOCK_IMPORT, + ) + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == MOCK_USER_INPUT["host"] + assert result["data"] == MOCK_IMPORT + + assert MOCK_LEGACY_ENTRY.original_name not in result["data"]["sensors"] + assert "pv_power_a" in result["data"]["sensors"] + + entity = entity_registry.async_get(MOCK_LEGACY_ENTRY.entity_id) + assert entity.unique_id == f"{MOCK_DEVICE['serial']}-6380_40251E00_0" diff --git a/tests/components/sma/test_sensor.py b/tests/components/sma/test_sensor.py index 8aa8c3e5b4cc0..7d5be09222c78 100644 --- a/tests/components/sma/test_sensor.py +++ b/tests/components/sma/test_sensor.py @@ -1,32 +1,21 @@ -"""SMA sensor tests.""" -from homeassistant.components.sensor import DOMAIN -from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, VOLT -from homeassistant.setup import async_setup_component +"""Test the sma sensor platform.""" +from homeassistant.const import ( + ATTR_UNIT_OF_MEASUREMENT, + ENERGY_KILO_WATT_HOUR, + POWER_WATT, +) -from tests.common import assert_setup_component +from . import MOCK_CUSTOM_SENSOR, init_integration -BASE_CFG = { - "platform": "sma", - "host": "1.1.1.1", - "password": "", - "custom": {"my_sensor": {"key": "1234567890123", "unit": VOLT}}, -} - -async def test_sma_config(hass): - """Test new config.""" - sensors = ["current_consumption"] - - with assert_setup_component(1): - assert await async_setup_component( - hass, DOMAIN, {DOMAIN: dict(BASE_CFG, sensors=sensors)} - ) - await hass.async_block_till_done() +async def test_sensors(hass): + """Test states of the sensors.""" + await init_integration(hass) state = hass.states.get("sensor.current_consumption") assert state - assert ATTR_UNIT_OF_MEASUREMENT in state.attributes - assert "current_consumption" not in state.attributes + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT - state = hass.states.get("sensor.my_sensor") + state = hass.states.get(f"sensor.{MOCK_CUSTOM_SENSOR['name']}") assert state + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR From 05aeff55911d52fc4936ba7fa2cd85d677b90ef4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 13 Apr 2021 09:31:01 -0700 Subject: [PATCH 0250/1317] Describe Google Assistant events (#49141) Co-authored-by: Martin Hjelmare --- homeassistant/components/alexa/logbook.py | 4 +- .../components/cloud/google_config.py | 8 +++ .../components/google_assistant/__init__.py | 9 ++- .../components/google_assistant/logbook.py | 29 ++++++++ tests/components/alexa/test_init.py | 6 +- tests/components/cloud/test_google_config.py | 16 +++++ .../google_assistant/test_logbook.py | 72 +++++++++++++++++++ 7 files changed, 137 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/google_assistant/logbook.py create mode 100644 tests/components/google_assistant/test_logbook.py diff --git a/homeassistant/components/alexa/logbook.py b/homeassistant/components/alexa/logbook.py index efc188a7f8bcc..153c7b7d61a37 100644 --- a/homeassistant/components/alexa/logbook.py +++ b/homeassistant/components/alexa/logbook.py @@ -17,10 +17,10 @@ def async_describe_logbook_event(event): if entity_id: state = hass.states.get(entity_id) name = state.name if state else entity_id - message = f"send command {data['request']['namespace']}/{data['request']['name']} for {name}" + message = f"sent command {data['request']['namespace']}/{data['request']['name']} for {name}" else: message = ( - f"send command {data['request']['namespace']}/{data['request']['name']}" + f"sent command {data['request']['namespace']}/{data['request']['name']}" ) return {"name": "Amazon Alexa", "message": message, "entity_id": entity_id} diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index 62ca1b15a7154..41f62c32c39c9 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -5,10 +5,12 @@ from hass_nabucasa import Cloud, cloud_api from hass_nabucasa.google_report_state import ErrorResponse +from homeassistant.components.google_assistant.const import DOMAIN as GOOGLE_DOMAIN from homeassistant.components.google_assistant.helpers import AbstractConfig from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES, HTTP_OK from homeassistant.core import CoreState, split_entity_id from homeassistant.helpers import entity_registry +from homeassistant.setup import async_setup_component from .const import ( CONF_ENTITY_CONFIG, @@ -84,6 +86,9 @@ async def async_initialize(self): """Perform async initialization of config.""" await super().async_initialize() + if self.enabled and GOOGLE_DOMAIN not in self.hass.config.components: + await async_setup_component(self.hass, GOOGLE_DOMAIN, {}) + # Remove old/wrong user agent ids remove_agent_user_ids = [] for agent_user_id in self._store.agent_user_ids: @@ -164,6 +169,9 @@ async def _async_request_sync_devices(self, agent_user_id: str): async def _async_prefs_updated(self, prefs): """Handle updated preferences.""" + if self.enabled and GOOGLE_DOMAIN not in self.hass.config.components: + await async_setup_component(self.hass, GOOGLE_DOMAIN, {}) + if self.should_report_state != self.is_reporting_state: if self.should_report_state: self.async_enable_report_state() diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index 7793ed4d65974..13516783233ad 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -86,12 +86,17 @@ def _check_report_state(data): _check_report_state, ) -CONFIG_SCHEMA = vol.Schema({DOMAIN: GOOGLE_ASSISTANT_SCHEMA}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + {vol.Optional(DOMAIN): GOOGLE_ASSISTANT_SCHEMA}, extra=vol.ALLOW_EXTRA +) async def async_setup(hass: HomeAssistant, yaml_config: dict[str, Any]): """Activate Google Actions component.""" - config = yaml_config.get(DOMAIN, {}) + if DOMAIN not in yaml_config: + return True + + config = yaml_config[DOMAIN] google_config = GoogleConfig(hass, config) await google_config.async_initialize() diff --git a/homeassistant/components/google_assistant/logbook.py b/homeassistant/components/google_assistant/logbook.py new file mode 100644 index 0000000000000..ef2bccd2c6510 --- /dev/null +++ b/homeassistant/components/google_assistant/logbook.py @@ -0,0 +1,29 @@ +"""Describe logbook events.""" +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import callback + +from .const import DOMAIN, EVENT_COMMAND_RECEIVED + +COMMON_COMMAND_PREFIX = "action.devices.commands." + + +@callback +def async_describe_events(hass, async_describe_event): + """Describe logbook events.""" + + @callback + def async_describe_logbook_event(event): + """Describe a logbook event.""" + entity_id = event.data[ATTR_ENTITY_ID] + state = hass.states.get(entity_id) + name = state.name if state else entity_id + + command = event.data["execution"]["command"] + if command.startswith(COMMON_COMMAND_PREFIX): + command = command[len(COMMON_COMMAND_PREFIX) :] + + message = f"sent command {command} for {name} (via {event.data['source']})" + + return {"name": "Google Assistant", "message": message, "entity_id": entity_id} + + async_describe_event(DOMAIN, EVENT_COMMAND_RECEIVED, async_describe_logbook_event) diff --git a/tests/components/alexa/test_init.py b/tests/components/alexa/test_init.py index c0972351cce47..eac4b32e5bad1 100644 --- a/tests/components/alexa/test_init.py +++ b/tests/components/alexa/test_init.py @@ -51,19 +51,19 @@ async def test_humanify_alexa_event(hass): event1, event2, event3 = results assert event1["name"] == "Amazon Alexa" - assert event1["message"] == "send command Alexa.Discovery/Discover" + assert event1["message"] == "sent command Alexa.Discovery/Discover" assert event1["entity_id"] is None assert event2["name"] == "Amazon Alexa" assert ( event2["message"] - == "send command Alexa.PowerController/TurnOn for Kitchen Light" + == "sent command Alexa.PowerController/TurnOn for Kitchen Light" ) assert event2["entity_id"] == "light.kitchen" assert event3["name"] == "Amazon Alexa" assert ( event3["message"] - == "send command Alexa.PowerController/TurnOn for light.non_existing" + == "sent command Alexa.PowerController/TurnOn for light.non_existing" ) assert event3["entity_id"] == "light.non_existing" diff --git a/tests/components/cloud/test_google_config.py b/tests/components/cloud/test_google_config.py index 5f29a41c6e0c5..bc430347e0802 100644 --- a/tests/components/cloud/test_google_config.py +++ b/tests/components/cloud/test_google_config.py @@ -225,3 +225,19 @@ def test_enabled_requires_valid_sub(hass, mock_expired_cloud_login, cloud_prefs) ) assert not config.enabled + + +async def test_setup_integration(hass, mock_conf, cloud_prefs): + """Test that we set up the integration if used.""" + mock_conf._cloud.subscription_expired = False + + assert "google_assistant" not in hass.config.components + + await mock_conf.async_initialize() + assert "google_assistant" in hass.config.components + + hass.config.components.remove("google_assistant") + + await cloud_prefs.async_update() + await hass.async_block_till_done() + assert "google_assistant" in hass.config.components diff --git a/tests/components/google_assistant/test_logbook.py b/tests/components/google_assistant/test_logbook.py new file mode 100644 index 0000000000000..4f996ba038f9e --- /dev/null +++ b/tests/components/google_assistant/test_logbook.py @@ -0,0 +1,72 @@ +"""The tests for Google Assistant logbook.""" +from homeassistant.components import logbook +from homeassistant.components.google_assistant.const import ( + DOMAIN, + EVENT_COMMAND_RECEIVED, + SOURCE_CLOUD, + SOURCE_LOCAL, +) +from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME +from homeassistant.setup import async_setup_component + +from tests.components.logbook.test_init import MockLazyEventPartialState + + +async def test_humanify_command_received(hass): + """Test humanifying command event.""" + hass.config.components.add("recorder") + hass.config.components.add("frontend") + hass.config.components.add("google_assistant") + assert await async_setup_component(hass, "logbook", {}) + entity_attr_cache = logbook.EntityAttributeCache(hass) + + hass.states.async_set( + "light.kitchen", "on", {ATTR_FRIENDLY_NAME: "The Kitchen Lights"} + ) + + events = list( + logbook.humanify( + hass, + [ + MockLazyEventPartialState( + EVENT_COMMAND_RECEIVED, + { + "request_id": "abcd", + ATTR_ENTITY_ID: "light.kitchen", + "execution": { + "command": "action.devices.commands.OnOff", + "params": {"on": True}, + }, + "source": SOURCE_LOCAL, + }, + ), + MockLazyEventPartialState( + EVENT_COMMAND_RECEIVED, + { + "request_id": "abcd", + ATTR_ENTITY_ID: "light.non_existing", + "execution": { + "command": "action.devices.commands.OnOff", + "params": {"on": False}, + }, + "source": SOURCE_CLOUD, + }, + ), + ], + entity_attr_cache, + {}, + ) + ) + + assert len(events) == 2 + event1, event2 = events + + assert event1["name"] == "Google Assistant" + assert event1["domain"] == DOMAIN + assert event1["message"] == "sent command OnOff for The Kitchen Lights (via local)" + assert event1["entity_id"] == "light.kitchen" + + assert event2["name"] == "Google Assistant" + assert event2["domain"] == DOMAIN + assert event2["message"] == "sent command OnOff for light.non_existing (via cloud)" + assert event2["entity_id"] == "light.non_existing" From 28347e19c5281580c134ccc69295a6eb86c7fb05 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 13 Apr 2021 09:31:23 -0700 Subject: [PATCH 0251/1317] Fix Hue service being removed on entry reload (#48663) --- homeassistant/components/hue/__init__.py | 110 ++++++++++-------- homeassistant/components/hue/bridge.py | 29 ++--- homeassistant/components/hue/const.py | 4 + tests/components/hue/test_bridge.py | 8 +- tests/components/hue/test_init.py | 4 +- .../hue/test_init_multiple_bridges.py | 25 ++-- 6 files changed, 92 insertions(+), 88 deletions(-) diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index 7e749b70396d1..68f48e47550f0 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -3,19 +3,18 @@ import logging from aiohue.util import normalize_bridge_id +import voluptuous as vol from homeassistant import config_entries, core from homeassistant.components import persistent_notification -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import config_validation as cv, device_registry as dr +from homeassistant.helpers.service import verify_domain_control -from .bridge import ( +from .bridge import HueBridge +from .const import ( ATTR_GROUP_NAME, ATTR_SCENE_NAME, - SCENE_SCHEMA, - SERVICE_HUE_SCENE, - HueBridge, -) -from .const import ( + ATTR_TRANSITION, CONF_ALLOW_HUE_GROUPS, CONF_ALLOW_UNREACHABLE, DEFAULT_ALLOW_HUE_GROUPS, @@ -24,46 +23,7 @@ ) _LOGGER = logging.getLogger(__name__) - - -async def async_setup(hass, config): - """Set up the Hue platform.""" - - async def hue_activate_scene(call, skip_reload=True): - """Handle activation of Hue scene.""" - # Get parameters - group_name = call.data[ATTR_GROUP_NAME] - scene_name = call.data[ATTR_SCENE_NAME] - - # Call the set scene function on each bridge - tasks = [ - bridge.hue_activate_scene( - call, updated=skip_reload, hide_warnings=skip_reload - ) - for bridge in hass.data[DOMAIN].values() - if isinstance(bridge, HueBridge) - ] - results = await asyncio.gather(*tasks) - - # Did *any* bridge succeed? If not, refresh / retry - # Note that we'll get a "None" value for a successful call - if None not in results: - if skip_reload: - await hue_activate_scene(call, skip_reload=False) - return - _LOGGER.warning( - "No bridge was able to activate " "scene %s in group %s", - scene_name, - group_name, - ) - - # Register a local handler for scene activation - hass.services.async_register( - DOMAIN, SERVICE_HUE_SCENE, hue_activate_scene, schema=SCENE_SCHEMA - ) - - hass.data[DOMAIN] = {} - return True +SERVICE_HUE_SCENE = "hue_activate_scene" async def async_setup_entry( @@ -104,7 +64,9 @@ async def async_setup_entry( if not await bridge.async_setup(): return False - hass.data[DOMAIN][entry.entry_id] = bridge + _register_services(hass) + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = bridge config = bridge.api.config # For backwards compat @@ -172,5 +134,55 @@ async def async_setup_entry( async def async_unload_entry(hass, entry): """Unload a config entry.""" bridge = hass.data[DOMAIN].pop(entry.entry_id) - hass.services.async_remove(DOMAIN, SERVICE_HUE_SCENE) + if len(hass.data[DOMAIN]) == 0: + hass.data.pop(DOMAIN) + hass.services.async_remove(DOMAIN, SERVICE_HUE_SCENE) return await bridge.async_reset() + + +@core.callback +def _register_services(hass): + """Register Hue services.""" + + async def hue_activate_scene(call, skip_reload=True): + """Handle activation of Hue scene.""" + # Get parameters + group_name = call.data[ATTR_GROUP_NAME] + scene_name = call.data[ATTR_SCENE_NAME] + + # Call the set scene function on each bridge + tasks = [ + bridge.hue_activate_scene( + call.data, updated=skip_reload, hide_warnings=skip_reload + ) + for bridge in hass.data[DOMAIN].values() + if isinstance(bridge, HueBridge) + ] + results = await asyncio.gather(*tasks) + + # Did *any* bridge succeed? If not, refresh / retry + # Note that we'll get a "None" value for a successful call + if None not in results: + if skip_reload: + await hue_activate_scene(call, skip_reload=False) + return + _LOGGER.warning( + "No bridge was able to activate " "scene %s in group %s", + scene_name, + group_name, + ) + + if DOMAIN not in hass.data: + # Register a local handler for scene activation + hass.services.async_register( + DOMAIN, + SERVICE_HUE_SCENE, + verify_domain_control(hass, DOMAIN)(hue_activate_scene), + schema=vol.Schema( + { + vol.Required(ATTR_GROUP_NAME): cv.string, + vol.Required(ATTR_SCENE_NAME): cv.string, + vol.Optional(ATTR_TRANSITION): cv.positive_int, + } + ), + ) diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index c14caa89620e4..2a306fe77bb2d 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -7,14 +7,16 @@ import aiohue import async_timeout import slugify as unicode_slug -import voluptuous as vol from homeassistant import core from homeassistant.const import HTTP_INTERNAL_SERVER_ERROR from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import aiohttp_client, config_validation as cv +from homeassistant.helpers import aiohttp_client from .const import ( + ATTR_GROUP_NAME, + ATTR_SCENE_NAME, + ATTR_TRANSITION, CONF_ALLOW_HUE_GROUPS, CONF_ALLOW_UNREACHABLE, DEFAULT_ALLOW_HUE_GROUPS, @@ -25,17 +27,6 @@ from .helpers import create_config_flow from .sensor_base import SensorManager -SERVICE_HUE_SCENE = "hue_activate_scene" -ATTR_GROUP_NAME = "group_name" -ATTR_SCENE_NAME = "scene_name" -ATTR_TRANSITION = "transition" -SCENE_SCHEMA = vol.Schema( - { - vol.Required(ATTR_GROUP_NAME): cv.string, - vol.Required(ATTR_SCENE_NAME): cv.string, - vol.Optional(ATTR_TRANSITION): cv.positive_int, - } -) # How long should we sleep if the hub is busy HUB_BUSY_SLEEP = 0.5 _LOGGER = logging.getLogger(__name__) @@ -202,11 +193,11 @@ async def async_reset(self): # None and True are OK return False not in results - async def hue_activate_scene(self, call, updated=False, hide_warnings=False): + async def hue_activate_scene(self, data, skip_reload=False, hide_warnings=False): """Service to call directly into bridge to set scenes.""" - group_name = call.data[ATTR_GROUP_NAME] - scene_name = call.data[ATTR_SCENE_NAME] - transition = call.data.get(ATTR_TRANSITION) + group_name = data[ATTR_GROUP_NAME] + scene_name = data[ATTR_SCENE_NAME] + transition = data.get(ATTR_TRANSITION) group = next( (group for group in self.api.groups.values() if group.name == group_name), @@ -226,10 +217,10 @@ async def hue_activate_scene(self, call, updated=False, hide_warnings=False): ) # If we can't find it, fetch latest info. - if not updated and (group is None or scene is None): + if not skip_reload and (group is None or scene is None): await self.async_request_call(self.api.groups.update) await self.async_request_call(self.api.scenes.update) - return await self.hue_activate_scene(call, updated=True) + return await self.hue_activate_scene(data, skip_reload=True) if group is None: if not hide_warnings: diff --git a/homeassistant/components/hue/const.py b/homeassistant/components/hue/const.py index 8d01617073b8d..5313584659dc2 100644 --- a/homeassistant/components/hue/const.py +++ b/homeassistant/components/hue/const.py @@ -18,3 +18,7 @@ GROUP_TYPE_ROOM = "Room" GROUP_TYPE_LUMINAIRE = "Luminaire" GROUP_TYPE_LIGHT_SOURCE = "LightSource" + +ATTR_GROUP_NAME = "group_name" +ATTR_SCENE_NAME = "scene_name" +ATTR_TRANSITION = "transition" diff --git a/tests/components/hue/test_bridge.py b/tests/components/hue/test_bridge.py index 093f6356b0900..9792eefba5e86 100644 --- a/tests/components/hue/test_bridge.py +++ b/tests/components/hue/test_bridge.py @@ -185,7 +185,7 @@ async def test_hue_activate_scene(hass, mock_api): call = Mock() call.data = {"group_name": "Group 1", "scene_name": "Cozy dinner"} with patch("aiohue.Bridge", return_value=mock_api): - assert await hue_bridge.hue_activate_scene(call) is None + assert await hue_bridge.hue_activate_scene(call.data) is None assert len(mock_api.mock_requests) == 3 assert mock_api.mock_requests[2]["json"]["scene"] == "scene_1" @@ -220,7 +220,7 @@ async def test_hue_activate_scene_transition(hass, mock_api): call = Mock() call.data = {"group_name": "Group 1", "scene_name": "Cozy dinner", "transition": 30} with patch("aiohue.Bridge", return_value=mock_api): - assert await hue_bridge.hue_activate_scene(call) is None + assert await hue_bridge.hue_activate_scene(call.data) is None assert len(mock_api.mock_requests) == 3 assert mock_api.mock_requests[2]["json"]["scene"] == "scene_1" @@ -255,7 +255,7 @@ async def test_hue_activate_scene_group_not_found(hass, mock_api): call = Mock() call.data = {"group_name": "Group 1", "scene_name": "Cozy dinner"} with patch("aiohue.Bridge", return_value=mock_api): - assert await hue_bridge.hue_activate_scene(call) is False + assert await hue_bridge.hue_activate_scene(call.data) is False async def test_hue_activate_scene_scene_not_found(hass, mock_api): @@ -285,4 +285,4 @@ async def test_hue_activate_scene_scene_not_found(hass, mock_api): call = Mock() call.data = {"group_name": "Group 1", "scene_name": "Cozy dinner"} with patch("aiohue.Bridge", return_value=mock_api): - assert await hue_bridge.hue_activate_scene(call) is False + assert await hue_bridge.hue_activate_scene(call.data) is False diff --git a/tests/components/hue/test_init.py b/tests/components/hue/test_init.py index 1f6ba83e2caa0..0c1d75c2ce2eb 100644 --- a/tests/components/hue/test_init.py +++ b/tests/components/hue/test_init.py @@ -27,7 +27,7 @@ async def test_setup_with_no_config(hass): assert len(hass.config_entries.flow.async_progress()) == 0 # No configs stored - assert hass.data[hue.DOMAIN] == {} + assert hue.DOMAIN not in hass.data async def test_unload_entry(hass, mock_bridge_setup): @@ -41,7 +41,7 @@ async def test_unload_entry(hass, mock_bridge_setup): mock_bridge_setup.async_reset = AsyncMock(return_value=True) assert await hue.async_unload_entry(hass, entry) assert len(mock_bridge_setup.async_reset.mock_calls) == 1 - assert hass.data[hue.DOMAIN] == {} + assert hue.DOMAIN not in hass.data async def test_setting_unique_id(hass, mock_bridge_setup): diff --git a/tests/components/hue/test_init_multiple_bridges.py b/tests/components/hue/test_init_multiple_bridges.py index 19b4da44a4d42..4e5378ae5e190 100644 --- a/tests/components/hue/test_init_multiple_bridges.py +++ b/tests/components/hue/test_init_multiple_bridges.py @@ -1,6 +1,5 @@ """Test Hue init with multiple bridges.""" - -from unittest.mock import Mock, patch +from unittest.mock import AsyncMock, Mock, patch from aiohue.groups import Groups from aiohue.lights import Lights @@ -13,6 +12,8 @@ from homeassistant.components.hue import sensor_base as hue_sensor_base from homeassistant.setup import async_setup_component +from tests.common import MockConfigEntry + async def setup_component(hass): """Hue component.""" @@ -109,10 +110,9 @@ async def test_hue_activate_scene_zero_responds( async def setup_bridge(hass, mock_bridge, config_entry): """Load the Hue light platform with the provided bridge.""" mock_bridge.config_entry = config_entry - hass.data[hue.DOMAIN][config_entry.entry_id] = mock_bridge - await hass.config_entries.async_forward_entry_setup(config_entry, "light") - # To flush out the service call to update the group - await hass.async_block_till_done() + config_entry.add_to_hass(hass) + with patch("homeassistant.components.hue.HueBridge", return_value=mock_bridge): + await hass.config_entries.async_setup(config_entry.entry_id) @pytest.fixture @@ -129,14 +129,10 @@ def mock_config_entry2(hass): def create_config_entry(): """Mock a config entry.""" - return config_entries.ConfigEntry( - 1, - hue.DOMAIN, - "Mock Title", - {"host": "mock-host"}, - "test", - config_entries.CONN_CLASS_LOCAL_POLL, - system_options={}, + return MockConfigEntry( + domain=hue.DOMAIN, + data={"host": "mock-host"}, + connection_class=config_entries.CONN_CLASS_LOCAL_POLL, ) @@ -163,6 +159,7 @@ def create_mock_bridge(hass): api=Mock(), reset_jobs=[], spec=hue.HueBridge, + async_setup=AsyncMock(return_value=True), ) bridge.sensor_manager = hue_sensor_base.SensorManager(bridge) bridge.mock_requests = [] From ba93a033a55fc26a6d9a45ccc18183e08bd82211 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 13 Apr 2021 09:31:41 -0700 Subject: [PATCH 0252/1317] Cloud to set up Alexa conditionally (#49136) --- .../components/cloud/alexa_config.py | 10 ++++++ homeassistant/components/cloud/client.py | 1 + homeassistant/components/cloud/manifest.json | 4 +-- tests/components/cloud/test_alexa_config.py | 32 ++++++++++++++----- 4 files changed, 37 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/cloud/alexa_config.py b/homeassistant/components/cloud/alexa_config.py index 138b2db0b8c75..393bfdfc2cd2d 100644 --- a/homeassistant/components/cloud/alexa_config.py +++ b/homeassistant/components/cloud/alexa_config.py @@ -9,6 +9,7 @@ from hass_nabucasa import Cloud, cloud_api from homeassistant.components.alexa import ( + DOMAIN as ALEXA_DOMAIN, config as alexa_config, entities as alexa_entities, errors as alexa_errors, @@ -18,6 +19,7 @@ from homeassistant.core import HomeAssistant, callback, split_entity_id from homeassistant.helpers import entity_registry from homeassistant.helpers.event import async_call_later +from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow from .const import CONF_ENTITY_CONFIG, CONF_FILTER, PREF_SHOULD_EXPOSE, RequireRelink @@ -103,6 +105,11 @@ def user_identifier(self): """Return an identifier for the user that represents this config.""" return self._cloud_user + async def async_initialize(self): + """Initialize the Alexa config.""" + if self.enabled and ALEXA_DOMAIN not in self.hass.config.components: + await async_setup_component(self.hass, ALEXA_DOMAIN, {}) + def should_expose(self, entity_id): """If an entity should be exposed.""" if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES: @@ -160,6 +167,9 @@ async def async_get_access_token(self): async def _async_prefs_updated(self, prefs): """Handle updated preferences.""" + if ALEXA_DOMAIN not in self.hass.config.components and self.enabled: + await async_setup_component(self.hass, ALEXA_DOMAIN, {}) + if self.should_report_state != self.is_reporting_states: if self.should_report_state: await self.async_enable_proactive_mode() diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index f451a4faddb10..6c09169ef3466 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -90,6 +90,7 @@ async def get_alexa_config(self) -> alexa_config.AlexaConfig: self._alexa_config = alexa_config.AlexaConfig( self._hass, self.alexa_user_config, cloud_user, self._prefs, self.cloud ) + await self._alexa_config.async_initialize() return self._alexa_config diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index 08bccf5eb6557..e51451be39709 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Cloud", "documentation": "https://www.home-assistant.io/integrations/cloud", "requirements": ["hass-nabucasa==0.43.0"], - "dependencies": ["http", "webhook", "alexa"], - "after_dependencies": ["google_assistant"], + "dependencies": ["http", "webhook"], + "after_dependencies": ["google_assistant", "alexa"], "codeowners": ["@home-assistant/cloud"] } diff --git a/tests/components/cloud/test_alexa_config.py b/tests/components/cloud/test_alexa_config.py index 8e104f641b2a7..83c2a5aa2d14c 100644 --- a/tests/components/cloud/test_alexa_config.py +++ b/tests/components/cloud/test_alexa_config.py @@ -2,6 +2,8 @@ import contextlib from unittest.mock import AsyncMock, Mock, patch +import pytest + from homeassistant.components.cloud import ALEXA_SCHEMA, alexa_config from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED from homeassistant.util.dt import utcnow @@ -9,15 +11,22 @@ from tests.common import async_fire_time_changed -async def test_alexa_config_expose_entity_prefs(hass, cloud_prefs): +@pytest.fixture() +def cloud_stub(): + """Stub the cloud.""" + return Mock(is_logged_in=True, subscription_expired=False) + + +async def test_alexa_config_expose_entity_prefs(hass, cloud_prefs, cloud_stub): """Test Alexa config should expose using prefs.""" entity_conf = {"should_expose": False} await cloud_prefs.async_update( alexa_entity_configs={"light.kitchen": entity_conf}, alexa_default_expose=["light"], + alexa_enabled=True, ) conf = alexa_config.AlexaConfig( - hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, None + hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, cloud_stub ) assert not conf.should_expose("light.kitchen") @@ -27,16 +36,19 @@ async def test_alexa_config_expose_entity_prefs(hass, cloud_prefs): entity_conf["should_expose"] = None assert conf.should_expose("light.kitchen") + assert "alexa" not in hass.config.components await cloud_prefs.async_update( alexa_default_expose=["sensor"], ) + await hass.async_block_till_done() + assert "alexa" in hass.config.components assert not conf.should_expose("light.kitchen") -async def test_alexa_config_report_state(hass, cloud_prefs): +async def test_alexa_config_report_state(hass, cloud_prefs, cloud_stub): """Test Alexa config should expose using prefs.""" conf = alexa_config.AlexaConfig( - hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, None + hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, cloud_stub ) assert cloud_prefs.alexa_report_state is False @@ -117,9 +129,11 @@ def sync_helper(to_upd, to_rem): yield to_update, to_remove -async def test_alexa_update_expose_trigger_sync(hass, cloud_prefs): +async def test_alexa_update_expose_trigger_sync(hass, cloud_prefs, cloud_stub): """Test Alexa config responds to updating exposed entities.""" - alexa_config.AlexaConfig(hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, None) + alexa_config.AlexaConfig( + hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, cloud_stub + ) with patch_sync_helper() as (to_update, to_remove): await cloud_prefs.async_update_alexa_entity_config( @@ -202,9 +216,11 @@ async def test_alexa_entity_registry_sync(hass, mock_cloud_login, cloud_prefs): assert to_remove == [] -async def test_alexa_update_report_state(hass, cloud_prefs): +async def test_alexa_update_report_state(hass, cloud_prefs, cloud_stub): """Test Alexa config responds to reporting state.""" - alexa_config.AlexaConfig(hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, None) + alexa_config.AlexaConfig( + hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, cloud_stub + ) with patch( "homeassistant.components.cloud.alexa_config.AlexaConfig.async_sync_entities", From 5d57e5c06c338fc4f8b6541bbb399bc66ed63040 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 13 Apr 2021 13:14:53 -0400 Subject: [PATCH 0253/1317] Enable the custom quirks path ZHA config option (#49143) --- homeassistant/components/zha/__init__.py | 2 ++ homeassistant/components/zha/core/const.py | 1 + 2 files changed, 3 insertions(+) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 43b95a9c2f2b8..4c8b73686bf11 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -18,6 +18,7 @@ from .core.const import ( BAUD_RATES, CONF_BAUDRATE, + CONF_CUSTOM_QUIRKS_PATH, CONF_DATABASE, CONF_DEVICE_CONFIG, CONF_ENABLE_QUIRKS, @@ -48,6 +49,7 @@ vol.Optional(CONF_ZIGPY): dict, vol.Optional(CONF_RADIO_TYPE): cv.enum(RadioType), vol.Optional(CONF_USB_PATH): cv.string, + vol.Optional(CONF_CUSTOM_QUIRKS_PATH): cv.isdir, } CONFIG_SCHEMA = vol.Schema( { diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index f43d9febc55f0..2576aa9f463ee 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -119,6 +119,7 @@ ) CONF_BAUDRATE = "baudrate" +CONF_CUSTOM_QUIRKS_PATH = "custom_quirks_path" CONF_DATABASE = "database_path" CONF_DEFAULT_LIGHT_TRANSITION = "default_light_transition" CONF_DEVICE_CONFIG = "device_config" From 5a9c3fea70ba1edb14f989211164b38bb25bce1f Mon Sep 17 00:00:00 2001 From: "Julien \"_FrnchFrgg_\" Rivaud" Date: Tue, 13 Apr 2021 21:33:46 +0200 Subject: [PATCH 0254/1317] Enable passing Amcrest/Dahua signals through as HA events (#49004) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some of the compatible hardware sends event signals that wouldn't map well to entities, e.g. NTP sync notifications, SIP registering information, or « doorbell button pressed » events with no « return to rest state » matching event to have a properly behaved binary sensor. Instead of only monitoring specific events, subscribe to all of them, and pass them through (in addition to handling them as before if they correspond to a configured binary sensor). Also bump python-amcrest to 1.7.2. Digest of the changes: * The library now passes through the event data instead of just presence of a "Start" member in in. * Connection to some devices has been fixed by not throwing the towel on minor errors. https://github.com/tchellomello/python-amcrest/compare/1.7.1...1.7.2 --- homeassistant/components/amcrest/__init__.py | 18 +++++++++++------- homeassistant/components/amcrest/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/amcrest/__init__.py b/homeassistant/components/amcrest/__init__.py index 71c277e578ca3..f6ddc21041531 100644 --- a/homeassistant/components/amcrest/__init__.py +++ b/homeassistant/components/amcrest/__init__.py @@ -197,14 +197,17 @@ def _wrap_test_online(self, now): def _monitor_events(hass, name, api, event_codes): - event_codes = ",".join(event_codes) + event_codes = set(event_codes) while True: api.available_flag.wait() try: - for code, start in api.event_actions(event_codes, retries=5): - signal = service_signal(SERVICE_EVENT, name, code) - _LOGGER.debug("Sending signal: '%s': %s", signal, start) - dispatcher_send(hass, signal, start) + for code, start in api.event_actions("All", retries=5): + event_data = {"camera": name, "event": code, "payload": start} + hass.bus.fire("amcrest", event_data) + if code in event_codes: + signal = service_signal(SERVICE_EVENT, name, code) + _LOGGER.debug("Sending signal: '%s': %s", signal, start) + dispatcher_send(hass, signal, start) except AmcrestError as error: _LOGGER.warning( "Error while processing events from %s camera: %r", name, error @@ -259,6 +262,7 @@ def setup(hass, config): discovery.load_platform(hass, CAMERA, DOMAIN, {CONF_NAME: name}, config) + event_codes = [] if binary_sensors: discovery.load_platform( hass, @@ -272,8 +276,8 @@ def setup(hass, config): for sensor_type in binary_sensors if sensor_type not in BINARY_POLLED_SENSORS ] - if event_codes: - _start_event_monitor(hass, name, api, event_codes) + + _start_event_monitor(hass, name, api, event_codes) if sensors: discovery.load_platform( diff --git a/homeassistant/components/amcrest/manifest.json b/homeassistant/components/amcrest/manifest.json index 869b65658d64f..c4d719d3166b1 100644 --- a/homeassistant/components/amcrest/manifest.json +++ b/homeassistant/components/amcrest/manifest.json @@ -2,7 +2,7 @@ "domain": "amcrest", "name": "Amcrest", "documentation": "https://www.home-assistant.io/integrations/amcrest", - "requirements": ["amcrest==1.7.1"], + "requirements": ["amcrest==1.7.2"], "dependencies": ["ffmpeg"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 95c521ec8721c..58497938086f6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -251,7 +251,7 @@ alpha_vantage==2.3.1 ambiclimate==0.2.1 # homeassistant.components.amcrest -amcrest==1.7.1 +amcrest==1.7.2 # homeassistant.components.androidtv androidtv[async]==0.0.57 From d7ac4bd65379e11461c7ce0893d3533d8d8b8cbf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 13 Apr 2021 11:03:46 -1000 Subject: [PATCH 0255/1317] Cancel sense updates on the stop event (#49187) --- homeassistant/components/sense/__init__.py | 34 +++++++++++++++------- homeassistant/components/sense/const.py | 3 ++ 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/sense/__init__.py b/homeassistant/components/sense/__init__.py index 1689d8c48341d..ee466c813f56a 100644 --- a/homeassistant/components/sense/__init__.py +++ b/homeassistant/components/sense/__init__.py @@ -11,8 +11,13 @@ import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_TIMEOUT -from homeassistant.core import HomeAssistant +from homeassistant.const import ( + CONF_EMAIL, + CONF_PASSWORD, + CONF_TIMEOUT, + EVENT_HOMEASSISTANT_STOP, +) +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -24,12 +29,14 @@ ACTIVE_UPDATE_RATE, DEFAULT_TIMEOUT, DOMAIN, + EVENT_STOP_REMOVE, SENSE_DATA, SENSE_DEVICE_UPDATE, SENSE_DEVICES_DATA, SENSE_DISCOVERED_DEVICES_DATA, SENSE_TIMEOUT_EXCEPTIONS, SENSE_TRENDS_COORDINATOR, + TRACK_TIME_REMOVE, ) _LOGGER = logging.getLogger(__name__) @@ -132,7 +139,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): # successful so we do it later. hass.loop.create_task(trends_coordinator.async_request_refresh()) - hass.data[DOMAIN][entry.entry_id] = { + data = hass.data[DOMAIN][entry.entry_id] = { SENSE_DATA: gateway, SENSE_DEVICES_DATA: sense_devices_data, SENSE_TRENDS_COORDINATOR: trends_coordinator, @@ -156,11 +163,19 @@ async def async_sense_update(_): sense_devices_data.set_devices_data(data["devices"]) async_dispatcher_send(hass, f"{SENSE_DEVICE_UPDATE}-{gateway.sense_monitor_id}") - hass.data[DOMAIN][entry.entry_id][ - "track_time_remove_callback" - ] = async_track_time_interval( + remove_update_callback = async_track_time_interval( hass, async_sense_update, timedelta(seconds=ACTIVE_UPDATE_RATE) ) + + @callback + def _remove_update_callback_at_stop(event): + remove_update_callback() + + data[TRACK_TIME_REMOVE] = remove_update_callback + data[EVENT_STOP_REMOVE] = hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, _remove_update_callback_at_stop + ) + return True @@ -174,10 +189,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): ] ) ) - track_time_remove_callback = hass.data[DOMAIN][entry.entry_id][ - "track_time_remove_callback" - ] - track_time_remove_callback() + data = hass.data[DOMAIN][entry.entry_id] + data[EVENT_STOP_REMOVE]() + data[TRACK_TIME_REMOVE]() if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/sense/const.py b/homeassistant/components/sense/const.py index 783fcb5508ae1..a6e8b88b342db 100644 --- a/homeassistant/components/sense/const.py +++ b/homeassistant/components/sense/const.py @@ -14,6 +14,9 @@ SENSE_DISCOVERED_DEVICES_DATA = "sense_discovered_devices" SENSE_TRENDS_COORDINATOR = "sense_trends_coordinator" +TRACK_TIME_REMOVE = "track_time_remove_callback" +EVENT_STOP_REMOVE = "event_stop_remove_callback" + ACTIVE_NAME = "Energy" ACTIVE_TYPE = "active" From 81e6ad07444f60d476c2548fe20f13a0f887d323 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 13 Apr 2021 11:10:58 -1000 Subject: [PATCH 0256/1317] Replace http startup logic with async_when_setup_or_start (#48784) --- homeassistant/components/http/__init__.py | 39 ++++------------- homeassistant/setup.py | 51 ++++++++++++++++++----- tests/test_setup.py | 35 ++++++++++++++++ 3 files changed, 84 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 5f57b4b77b8af..8ebb03975795b 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -6,22 +6,18 @@ import logging import os import ssl -from typing import Optional, cast +from typing import Any, Optional, cast from aiohttp import web from aiohttp.web_exceptions import HTTPMovedPermanently import voluptuous as vol -from homeassistant.const import ( - EVENT_HOMEASSISTANT_START, - EVENT_HOMEASSISTANT_STOP, - SERVER_PORT, -) +from homeassistant.const import EVENT_HOMEASSISTANT_STOP, SERVER_PORT from homeassistant.core import Event, HomeAssistant from homeassistant.helpers import storage import homeassistant.helpers.config_validation as cv from homeassistant.loader import bind_hass -from homeassistant.setup import ATTR_COMPONENT, EVENT_COMPONENT_LOADED +from homeassistant.setup import async_start_setup, async_when_setup_or_start import homeassistant.util as hass_util from homeassistant.util import ssl as ssl_util @@ -161,36 +157,17 @@ async def async_setup(hass, config): ssl_profile=ssl_profile, ) - startup_listeners = [] - async def stop_server(event: Event) -> None: """Stop the server.""" await server.stop() - async def start_server(event: Event) -> None: + async def start_server(*_: Any) -> None: """Start the server.""" + with async_start_setup(hass, ["http"]): + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_server) + await start_http_server_and_save_config(hass, dict(conf), server) - for listener in startup_listeners: - listener() - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_server) - - await start_http_server_and_save_config(hass, dict(conf), server) - - async def async_wait_frontend_load(event: Event) -> None: - """Wait for the frontend to load.""" - - if event.data[ATTR_COMPONENT] != "frontend": - return - - await start_server(event) - - startup_listeners.append( - hass.bus.async_listen(EVENT_COMPONENT_LOADED, async_wait_frontend_load) - ) - startup_listeners.append( - hass.bus.async_listen(EVENT_HOMEASSISTANT_START, start_server) - ) + async_when_setup_or_start(hass, "frontend", start_server) hass.http = server diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 6af20e21905f5..bead16c1d789e 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -10,7 +10,11 @@ from homeassistant import config as conf_util, core, loader, requirements from homeassistant.config import async_notify_setup_error -from homeassistant.const import EVENT_COMPONENT_LOADED, PLATFORM_FORMAT +from homeassistant.const import ( + EVENT_COMPONENT_LOADED, + EVENT_HOMEASSISTANT_START, + PLATFORM_FORMAT, +) from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.typing import ConfigType from homeassistant.util import dt as dt_util, ensure_unique_string @@ -379,6 +383,27 @@ def async_when_setup( when_setup_cb: Callable[[core.HomeAssistant, str], Awaitable[None]], ) -> None: """Call a method when a component is setup.""" + _async_when_setup(hass, component, when_setup_cb, False) + + +@core.callback +def async_when_setup_or_start( + hass: core.HomeAssistant, + component: str, + when_setup_cb: Callable[[core.HomeAssistant, str], Awaitable[None]], +) -> None: + """Call a method when a component is setup or state is fired.""" + _async_when_setup(hass, component, when_setup_cb, True) + + +@core.callback +def _async_when_setup( + hass: core.HomeAssistant, + component: str, + when_setup_cb: Callable[[core.HomeAssistant, str], Awaitable[None]], + start_event: bool, +) -> None: + """Call a method when a component is setup or the start event fires.""" async def when_setup() -> None: """Call the callback.""" @@ -387,22 +412,28 @@ async def when_setup() -> None: except Exception: # pylint: disable=broad-except _LOGGER.exception("Error handling when_setup callback for %s", component) - # Running it in a new task so that it always runs after if component in hass.config.components: hass.async_create_task(when_setup()) return - unsub = None + listeners: list[Callable] = [] - async def loaded_event(event: core.Event) -> None: - """Call the callback.""" - if event.data[ATTR_COMPONENT] != component: - return - - unsub() # type: ignore + async def _matched_event(event: core.Event) -> None: + """Call the callback when we matched an event.""" + for listener in listeners: + listener() await when_setup() - unsub = hass.bus.async_listen(EVENT_COMPONENT_LOADED, loaded_event) + async def _loaded_event(event: core.Event) -> None: + """Call the callback if we loaded the expected component.""" + if event.data[ATTR_COMPONENT] == component: + await _matched_event(event) + + listeners.append(hass.bus.async_listen(EVENT_COMPONENT_LOADED, _loaded_event)) + if start_event: + listeners.append( + hass.bus.async_listen(EVENT_HOMEASSISTANT_START, _matched_event) + ) @core.callback diff --git a/tests/test_setup.py b/tests/test_setup.py index 72613722ca1f1..d245c9818363b 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -556,6 +556,41 @@ async def mock_callback(hass, component): assert calls == ["test", "test"] +async def test_async_when_setup_or_start_already_loaded(hass): + """Test when setup or start.""" + calls = [] + + async def mock_callback(hass, component): + """Mock callback.""" + calls.append(component) + + setup.async_when_setup_or_start(hass, "test", mock_callback) + await hass.async_block_till_done() + assert calls == [] + + hass.config.components.add("test") + hass.bus.async_fire(EVENT_COMPONENT_LOADED, {"component": "test"}) + await hass.async_block_till_done() + assert calls == ["test"] + + # Event listener should be gone + hass.bus.async_fire(EVENT_COMPONENT_LOADED, {"component": "test"}) + await hass.async_block_till_done() + assert calls == ["test"] + + # Should be called right away + setup.async_when_setup_or_start(hass, "test", mock_callback) + await hass.async_block_till_done() + assert calls == ["test", "test"] + + setup.async_when_setup_or_start(hass, "not_loaded", mock_callback) + await hass.async_block_till_done() + assert calls == ["test", "test"] + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + assert calls == ["test", "test", "not_loaded"] + + async def test_setup_import_blows_up(hass): """Test that we handle it correctly when importing integration blows up.""" with patch( From 0b4b071c0238999f5af5c4d391478ec87e545d21 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Wed, 14 Apr 2021 00:03:17 +0000 Subject: [PATCH 0257/1317] [ci skip] Translation update --- .../accuweather/translations/de.json | 5 +- .../azure_devops/translations/de.json | 13 +++- .../components/bond/translations/de.json | 5 +- .../components/deconz/translations/es.json | 4 ++ .../components/denonavr/translations/de.json | 4 +- .../components/denonavr/translations/nl.json | 2 +- .../components/eafm/translations/de.json | 7 ++ .../components/ezviz/translations/de.json | 17 +++++ .../components/gogogate2/translations/de.json | 1 + .../google_travel_time/translations/es.json | 22 +++++++ .../components/ialarm/translations/de.json | 20 ++++++ .../meteo_france/translations/de.json | 12 +++- .../components/netatmo/translations/de.json | 3 +- .../ovo_energy/translations/de.json | 1 + .../components/script/translations/ru.json | 2 +- .../components/sensor/translations/de.json | 12 +++- .../simplisafe/translations/de.json | 5 ++ .../components/sma/translations/ca.json | 27 ++++++++ .../components/sma/translations/de.json | 23 +++++++ .../components/sma/translations/et.json | 27 ++++++++ .../components/smappee/translations/de.json | 12 +++- .../components/smarthab/translations/de.json | 5 +- .../components/spider/translations/de.json | 3 +- .../components/syncthru/translations/de.json | 6 +- .../components/tag/translations/de.json | 3 + .../components/toon/translations/de.json | 11 ++++ .../components/volumio/translations/de.json | 7 +- .../waze_travel_time/translations/es.json | 13 ++++ .../components/wolflink/translations/de.json | 6 +- .../wolflink/translations/sensor.de.json | 64 ++++++++++++++++++- .../components/zha/translations/de.json | 1 + .../components/zha/translations/es.json | 1 + 32 files changed, 323 insertions(+), 21 deletions(-) create mode 100644 homeassistant/components/google_travel_time/translations/es.json create mode 100644 homeassistant/components/ialarm/translations/de.json create mode 100644 homeassistant/components/sma/translations/ca.json create mode 100644 homeassistant/components/sma/translations/de.json create mode 100644 homeassistant/components/sma/translations/et.json create mode 100644 homeassistant/components/tag/translations/de.json create mode 100644 homeassistant/components/waze_travel_time/translations/es.json diff --git a/homeassistant/components/accuweather/translations/de.json b/homeassistant/components/accuweather/translations/de.json index 330f2850d26e8..a9b23bacf6c17 100644 --- a/homeassistant/components/accuweather/translations/de.json +++ b/homeassistant/components/accuweather/translations/de.json @@ -16,6 +16,7 @@ "longitude": "L\u00e4ngengrad", "name": "Name" }, + "description": "Wenn du Hilfe bei der Konfiguration ben\u00f6tigst, schaue hier nach: https://www.home-assistant.io/integrations/accuweather/\n\nEinige Sensoren sind standardm\u00e4\u00dfig nicht aktiviert. Du kannst sie in der Entit\u00e4tsregister nach der Integrationskonfiguration aktivieren.\nDie Wettervorhersage ist nicht standardm\u00e4\u00dfig aktiviert. Du kannst sie in den Integrationsoptionen aktivieren.", "title": "AccuWeather" } } @@ -25,7 +26,9 @@ "user": { "data": { "forecast": "Wettervorhersage" - } + }, + "description": "Aufgrund der Einschr\u00e4nkungen der kostenlosen Version des AccuWeather-API-Schl\u00fcssels werden bei aktivierter Wettervorhersage Datenaktualisierungen alle 80 Minuten statt alle 40 Minuten durchgef\u00fchrt.", + "title": "AccuWeather Optionen" } } }, diff --git a/homeassistant/components/azure_devops/translations/de.json b/homeassistant/components/azure_devops/translations/de.json index e7d9e073ec617..43a5776da2e94 100644 --- a/homeassistant/components/azure_devops/translations/de.json +++ b/homeassistant/components/azure_devops/translations/de.json @@ -6,17 +6,26 @@ }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", - "invalid_auth": "Ung\u00fcltige Authentifizierung" + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "project_error": "Konnte keine Projektinformationen erhalten." }, + "flow_title": "Azure DevOps: {project_url}", "step": { "reauth": { + "data": { + "personal_access_token": "Pers\u00f6nlicher Zugriffstoken (PAT)" + }, + "description": "Authentifizierung f\u00fcr {project_url} fehlgeschlagen. Bitte gib deine aktuellen Anmeldedaten ein.", "title": "Erneute Authentifizierung" }, "user": { "data": { "organization": "Organisation", + "personal_access_token": "Pers\u00f6nlicher Zugriffstoken (PAT)", "project": "Projekt" - } + }, + "description": "Richte eine Azure DevOps-Instanz f\u00fcr den Zugriff auf dein Projekt ein. Ein pers\u00f6nlicher Zugriffstoken ist nur f\u00fcr ein privates Projekt erforderlich.", + "title": "Azure DevOps-Projekt hinzuf\u00fcgen" } } } diff --git a/homeassistant/components/bond/translations/de.json b/homeassistant/components/bond/translations/de.json index 1c1c7375a28b0..4b7372a452645 100644 --- a/homeassistant/components/bond/translations/de.json +++ b/homeassistant/components/bond/translations/de.json @@ -6,13 +6,16 @@ "error": { "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", + "old_firmware": "Nicht unterst\u00fctzte alte Firmware auf dem Bond-Ger\u00e4t - bitte aktualisiere, bevor du fortf\u00e4hrst", "unknown": "Unerwarteter Fehler" }, + "flow_title": "Bond: {name} ({host})", "step": { "confirm": { "data": { "access_token": "Zugangstoken" - } + }, + "description": "M\u00f6chtest du {name} einrichten?" }, "user": { "data": { diff --git a/homeassistant/components/deconz/translations/es.json b/homeassistant/components/deconz/translations/es.json index b237d84fafc1e..3670caf18d08a 100644 --- a/homeassistant/components/deconz/translations/es.json +++ b/homeassistant/components/deconz/translations/es.json @@ -42,6 +42,10 @@ "button_2": "Segundo bot\u00f3n", "button_3": "Tercer bot\u00f3n", "button_4": "Cuarto bot\u00f3n", + "button_5": "Quinto bot\u00f3n", + "button_6": "Sexto bot\u00f3n", + "button_7": "S\u00e9ptimo bot\u00f3n", + "button_8": "Octavo bot\u00f3n", "close": "Cerrar", "dim_down": "Bajar la intensidad", "dim_up": "Subir la intensidad", diff --git a/homeassistant/components/denonavr/translations/de.json b/homeassistant/components/denonavr/translations/de.json index 6bd9f1613dc5f..0cf669a13b422 100644 --- a/homeassistant/components/denonavr/translations/de.json +++ b/homeassistant/components/denonavr/translations/de.json @@ -36,7 +36,9 @@ "step": { "init": { "data": { - "show_all_sources": "Alle Quellen anzeigen" + "show_all_sources": "Alle Quellen anzeigen", + "zone2": "Zone 2 einrichten", + "zone3": "Zone 3 einrichten" }, "description": "Optionale Einstellungen festlegen", "title": "Denon AVR-Netzwerk-Receiver" diff --git a/homeassistant/components/denonavr/translations/nl.json b/homeassistant/components/denonavr/translations/nl.json index 04f067c2f2a9d..2cf2ea7976877 100644 --- a/homeassistant/components/denonavr/translations/nl.json +++ b/homeassistant/components/denonavr/translations/nl.json @@ -8,7 +8,7 @@ "not_denonavr_missing": "Geen Denon AVR netwerkontvanger, zoekinformatie niet compleet" }, "error": { - "discovery_error": "Kan een Denon AVR netwerkontvanger niet vinden" + "discovery_error": "Kan geen Denon AVR netwerkontvanger vinden" }, "flow_title": "Denon AVR Network Receiver: {name}", "step": { diff --git a/homeassistant/components/eafm/translations/de.json b/homeassistant/components/eafm/translations/de.json index da1d200c2a209..874e5ff9dadab 100644 --- a/homeassistant/components/eafm/translations/de.json +++ b/homeassistant/components/eafm/translations/de.json @@ -2,6 +2,13 @@ "config": { "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "step": { + "user": { + "data": { + "station": "Station" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/ezviz/translations/de.json b/homeassistant/components/ezviz/translations/de.json index b849a7f231a9b..0286f942487c8 100644 --- a/homeassistant/components/ezviz/translations/de.json +++ b/homeassistant/components/ezviz/translations/de.json @@ -1,9 +1,25 @@ { "config": { + "abort": { + "already_configured_account": "Konto wurde bereits konfiguriert", + "unknown": "Unerwarteter Fehler" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "invalid_host": "Ung\u00fcltiger Hostname oder IP-Adresse" + }, "step": { + "confirm": { + "data": { + "password": "Passwort", + "username": "Benutzername" + } + }, "user": { "data": { "password": "Passwort", + "url": "URL", "username": "Benutzername" }, "title": "Verbinden mit Ezviz Cloud" @@ -11,6 +27,7 @@ "user_custom_url": { "data": { "password": "Passwort", + "url": "URL", "username": "Benutzername" } } diff --git a/homeassistant/components/gogogate2/translations/de.json b/homeassistant/components/gogogate2/translations/de.json index 30a1ff67b6534..5c0173a99cfe8 100644 --- a/homeassistant/components/gogogate2/translations/de.json +++ b/homeassistant/components/gogogate2/translations/de.json @@ -14,6 +14,7 @@ "password": "Passwort", "username": "Benutzername" }, + "description": "Gib die erforderlichen Informationen unten an.", "title": "GogoGate2 oder iSmartGate einrichten" } } diff --git a/homeassistant/components/google_travel_time/translations/es.json b/homeassistant/components/google_travel_time/translations/es.json new file mode 100644 index 0000000000000..8224454237516 --- /dev/null +++ b/homeassistant/components/google_travel_time/translations/es.json @@ -0,0 +1,22 @@ +{ + "config": { + "step": { + "user": { + "data": { + "destination": "Destino", + "origin": "Origen" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "language": "Idioma", + "units": "Unidades" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ialarm/translations/de.json b/homeassistant/components/ialarm/translations/de.json new file mode 100644 index 0000000000000..6577f995acc5b --- /dev/null +++ b/homeassistant/components/ialarm/translations/de.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "host": "Host", + "pin": "PIN-Code", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meteo_france/translations/de.json b/homeassistant/components/meteo_france/translations/de.json index 74637594d5ff1..e1993b466dcef 100644 --- a/homeassistant/components/meteo_france/translations/de.json +++ b/homeassistant/components/meteo_france/translations/de.json @@ -12,7 +12,8 @@ "data": { "city": "Stadt" }, - "description": "W\u00e4hle deine Stadt aus der Liste" + "description": "W\u00e4hle deine Stadt aus der Liste", + "title": "M\u00e9t\u00e9o-France" }, "user": { "data": { @@ -22,5 +23,14 @@ "title": "M\u00e9t\u00e9o-France" } } + }, + "options": { + "step": { + "init": { + "data": { + "mode": "Vorhersage Modus" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/de.json b/homeassistant/components/netatmo/translations/de.json index dccb58577487e..1037d100909df 100644 --- a/homeassistant/components/netatmo/translations/de.json +++ b/homeassistant/components/netatmo/translations/de.json @@ -48,7 +48,8 @@ "lon_sw": "L\u00e4ngengrad S\u00fcdwest-Ecke", "mode": "Berechnung", "show_on_map": "Auf Karte anzeigen" - } + }, + "title": "\u00d6ffentlicher Netatmo Wettersensor" }, "public_weather_areas": { "data": { diff --git a/homeassistant/components/ovo_energy/translations/de.json b/homeassistant/components/ovo_energy/translations/de.json index a86f39a614cf2..6fccec143332f 100644 --- a/homeassistant/components/ovo_energy/translations/de.json +++ b/homeassistant/components/ovo_energy/translations/de.json @@ -19,6 +19,7 @@ "password": "Passwort", "username": "Benutzername" }, + "description": "Richte eine OVO Energy-Instanz ein, um auf deinen Energieverbrauch zuzugreifen.", "title": "Ovo Energy Account hinzuf\u00fcgen" } } diff --git a/homeassistant/components/script/translations/ru.json b/homeassistant/components/script/translations/ru.json index 97dff767c61f8..327dc27843c5c 100644 --- a/homeassistant/components/script/translations/ru.json +++ b/homeassistant/components/script/translations/ru.json @@ -5,5 +5,5 @@ "on": "\u0412\u043a\u043b" } }, - "title": "\u0421\u0446\u0435\u043d\u0430\u0440\u0438\u0439" + "title": "\u0421\u043a\u0440\u0438\u043f\u0442" } \ No newline at end of file diff --git a/homeassistant/components/sensor/translations/de.json b/homeassistant/components/sensor/translations/de.json index bb7c197f0e82a..24c87bc15b9d2 100644 --- a/homeassistant/components/sensor/translations/de.json +++ b/homeassistant/components/sensor/translations/de.json @@ -4,27 +4,35 @@ "is_battery_level": "{entity_name} Batteriestand", "is_carbon_dioxide": "Aktuelle {entity_name} Kohlenstoffdioxid-Konzentration", "is_carbon_monoxide": "Aktuelle {entity_name} Kohlenstoffmonoxid-Konzentration", + "is_current": "Aktueller Strom von {entity_name}", + "is_energy": "Aktuelle Energie von {entity_name}", "is_humidity": "{entity_name} Feuchtigkeit", "is_illuminance": "Aktuelle {entity_name} Helligkeit", "is_power": "Aktuelle {entity_name} Leistung", + "is_power_factor": "Aktueller Leistungsfaktor f\u00fcr {entity_name}", "is_pressure": "{entity_name} Druck", "is_signal_strength": "Aktuelle {entity_name} Signalst\u00e4rke", "is_temperature": "Aktuelle {entity_name} Temperatur", "is_timestamp": "Aktueller Zeitstempel von {entity_name}", - "is_value": "Aktueller {entity_name} Wert" + "is_value": "Aktueller {entity_name} Wert", + "is_voltage": "Aktuelle Spannung von {entity_name}" }, "trigger_type": { "battery_level": "{entity_name} Batteriestatus\u00e4nderungen", "carbon_dioxide": "{entity_name} Kohlenstoffdioxid-Konzentrations\u00e4nderung", "carbon_monoxide": "{entity_name} Kohlenstoffmonoxid-Konzentrations\u00e4nderung", + "current": "{entity_name} Stromver\u00e4nderung", + "energy": "{entity_name} Energie\u00e4nderungen", "humidity": "{entity_name} Feuchtigkeits\u00e4nderungen", "illuminance": "{entity_name} Helligkeits\u00e4nderungen", "power": "{entity_name} Leistungs\u00e4nderungen", + "power_factor": "{entity_name} Leistungsfaktor\u00e4nderung", "pressure": "{entity_name} Druck\u00e4nderungen", "signal_strength": "{entity_name} Signalst\u00e4rke\u00e4nderungen", "temperature": "{entity_name} Temperatur\u00e4nderungen", "timestamp": "{entity_name} Zeitstempel\u00e4nderungen", - "value": "{entity_name} Wert\u00e4nderungen" + "value": "{entity_name} Wert\u00e4nderungen", + "voltage": "{entity_name} Spannungs\u00e4nderungen" } }, "state": { diff --git a/homeassistant/components/simplisafe/translations/de.json b/homeassistant/components/simplisafe/translations/de.json index 5914e8f680c83..046c46c01acb9 100644 --- a/homeassistant/components/simplisafe/translations/de.json +++ b/homeassistant/components/simplisafe/translations/de.json @@ -7,9 +7,14 @@ "error": { "identifier_exists": "Konto bereits registriert", "invalid_auth": "Ung\u00fcltige Authentifizierung", + "still_awaiting_mfa": "Immernoch warten auf MFA-E-Mail-Klick", "unknown": "Unerwarteter Fehler" }, "step": { + "mfa": { + "description": "Pr\u00fcfe deine E-Mail auf einen Link von SimpliSafe. Kehre nach der Verifizierung des Links hierher zur\u00fcck, um die Installation der Integration abzuschlie\u00dfen.", + "title": "SimpliSafe Multi-Faktor-Authentifizierung" + }, "reauth_confirm": { "data": { "password": "Passwort" diff --git a/homeassistant/components/sma/translations/ca.json b/homeassistant/components/sma/translations/ca.json new file mode 100644 index 0000000000000..fdc6d98f68904 --- /dev/null +++ b/homeassistant/components/sma/translations/ca.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "cannot_retrieve_device_info": "Connectat correctament per\u00f2 no s'ha pogut obtenir la informaci\u00f3 del dispositiu", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "group": "Grup", + "host": "Amfitri\u00f3", + "password": "Contrasenya", + "ssl": "Utilitza un certificat SSL", + "verify_ssl": "Verifica el certificat SSL" + }, + "description": "Introdueix la informaci\u00f3 del teu dispositiu SMA.", + "title": "Configuraci\u00f3 d'SMA Solar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sma/translations/de.json b/homeassistant/components/sma/translations/de.json new file mode 100644 index 0000000000000..807645467decb --- /dev/null +++ b/homeassistant/components/sma/translations/de.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Passwort", + "ssl": "Verwendet ein SSL-Zertifikat", + "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sma/translations/et.json b/homeassistant/components/sma/translations/et.json new file mode 100644 index 0000000000000..4e1eb29d15833 --- /dev/null +++ b/homeassistant/components/sma/translations/et.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "already_in_progress": "Seadistamine on juba k\u00e4imas" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "cannot_retrieve_device_info": "\u00dchendamine \u00f5nnestus kuid seadme teavet ei \u00f5nnestunud hankida", + "invalid_auth": "Vigane autentimine", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "group": "Grupp", + "host": "Host", + "password": "Salas\u00f5na", + "ssl": "Kasutab SSL sertifikaati", + "verify_ssl": "Kontrolli SSL sertifikaati" + }, + "description": "Sisesta oma SMA seadme teave.", + "title": "Seadista SMA Solar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smappee/translations/de.json b/homeassistant/components/smappee/translations/de.json index 15fd8d6cd22da..6491fbf2d155e 100644 --- a/homeassistant/components/smappee/translations/de.json +++ b/homeassistant/components/smappee/translations/de.json @@ -2,8 +2,10 @@ "config": { "abort": { "already_configured_device": "Ger\u00e4t ist bereits konfiguriert", + "already_configured_local_device": "Lokale(s) Ger\u00e4t(e) ist/sind bereits konfiguriert. Bitte entferne diese zuerst, bevor du ein Cloud-Ger\u00e4t konfigurierst.", "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_mdns": "Nicht unterst\u00fctztes Ger\u00e4t f\u00fcr die Smappee-Integration.", "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.", "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url})." }, @@ -12,15 +14,21 @@ "environment": { "data": { "environment": "Umgebung" - } + }, + "description": "Richte dein Smappee f\u00fcr die Integration mit dem Home Assistant ein." }, "local": { "data": { "host": "Host" - } + }, + "description": "Gib den Host ein, um die lokale Integration von Smappee zu initiieren" }, "pick_implementation": { "title": "W\u00e4hle die Authentifizierungsmethode" + }, + "zeroconf_confirm": { + "description": "M\u00f6chtest du das Smappee-Ger\u00e4t mit der Seriennummer `{serialnumber}` zum Home Assistant hinzuf\u00fcgen?", + "title": "Entdecktes Smappee-Ger\u00e4t" } } } diff --git a/homeassistant/components/smarthab/translations/de.json b/homeassistant/components/smarthab/translations/de.json index 18bb2c77047dc..ca2bf3373f2d5 100644 --- a/homeassistant/components/smarthab/translations/de.json +++ b/homeassistant/components/smarthab/translations/de.json @@ -2,6 +2,7 @@ "config": { "error": { "invalid_auth": "Ung\u00fcltige Authentifizierung", + "service": "Fehler beim Versuch, SmartHab zu erreichen. Der Dienst ist m\u00f6glicherweise nicht erreichbar. Pr\u00fcfe deine Verbindung.", "unknown": "Unerwarteter Fehler" }, "step": { @@ -9,7 +10,9 @@ "data": { "email": "E-Mail", "password": "Passwort" - } + }, + "description": "Stelle aus technischen Gr\u00fcnden sicher, dass du ein sekund\u00e4res Konto speziell f\u00fcr deine Home Assistant-Einrichtung verwendest. Du kannst ein solches Konto \u00fcber die SmartHab-Anwendung erstellen.", + "title": "SmartHab einrichten" } } } diff --git a/homeassistant/components/spider/translations/de.json b/homeassistant/components/spider/translations/de.json index c57e55e9d2ea1..81d0d0107b332 100644 --- a/homeassistant/components/spider/translations/de.json +++ b/homeassistant/components/spider/translations/de.json @@ -12,7 +12,8 @@ "data": { "password": "Passwort", "username": "Benutzername" - } + }, + "title": "Anmelden mit mijn.ithodaalderop.nl Konto" } } } diff --git a/homeassistant/components/syncthru/translations/de.json b/homeassistant/components/syncthru/translations/de.json index 8e568131e6203..450d04665971c 100644 --- a/homeassistant/components/syncthru/translations/de.json +++ b/homeassistant/components/syncthru/translations/de.json @@ -12,12 +12,14 @@ "step": { "confirm": { "data": { - "name": "Name" + "name": "Name", + "url": "Web-Interface-URL" } }, "user": { "data": { - "name": "Name" + "name": "Name", + "url": "Web-Interface-URL" } } } diff --git a/homeassistant/components/tag/translations/de.json b/homeassistant/components/tag/translations/de.json new file mode 100644 index 0000000000000..fdac700612daf --- /dev/null +++ b/homeassistant/components/tag/translations/de.json @@ -0,0 +1,3 @@ +{ + "title": "Tag" +} \ No newline at end of file diff --git a/homeassistant/components/toon/translations/de.json b/homeassistant/components/toon/translations/de.json index c04f3a5f4bb4a..58ed0d65ce112 100644 --- a/homeassistant/components/toon/translations/de.json +++ b/homeassistant/components/toon/translations/de.json @@ -1,11 +1,22 @@ { "config": { "abort": { + "already_configured": "Die ausgew\u00e4hlte Vereinbarung ist bereits konfiguriert.", + "authorize_url_fail": "Unbekannter Fehler beim Generieren einer Autorisierungs-URL.", "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.", "no_agreements": "Dieses Konto hat keine Toon-Anzeigen.", "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url}).", "unknown_authorize_url_generation": "Beim Generieren einer Authentifizierungs-URL ist ein unbekannter Fehler aufgetreten" + }, + "step": { + "agreement": { + "data": { + "agreement": "Vereinbarung" + }, + "description": "W\u00e4hlen Sie die Vereinbarungsadresse aus, die du hinzuf\u00fcgen m\u00f6chtest.", + "title": "W\u00e4hle deine Vereinbarung" + } } } } \ No newline at end of file diff --git a/homeassistant/components/volumio/translations/de.json b/homeassistant/components/volumio/translations/de.json index 45727d85ee05d..0bc75a9bc8cd8 100644 --- a/homeassistant/components/volumio/translations/de.json +++ b/homeassistant/components/volumio/translations/de.json @@ -1,13 +1,18 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "cannot_connect": "Kann keine Verbindung zu entdeckten Volumio herstellen" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", "unknown": "Unerwarteter Fehler" }, "step": { + "discovery_confirm": { + "description": "M\u00f6chtest du Volumio (`{name}`) zum Home Assistant hinzuf\u00fcgen?", + "title": "Entdeckte Volumio" + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/waze_travel_time/translations/es.json b/homeassistant/components/waze_travel_time/translations/es.json new file mode 100644 index 0000000000000..8b7235537f218 --- /dev/null +++ b/homeassistant/components/waze_travel_time/translations/es.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "destination": "Destino", + "origin": "Origen", + "region": "Regi\u00f3n" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/de.json b/homeassistant/components/wolflink/translations/de.json index 71f48a6413dca..aba055e864674 100644 --- a/homeassistant/components/wolflink/translations/de.json +++ b/homeassistant/components/wolflink/translations/de.json @@ -12,13 +12,15 @@ "device": { "data": { "device_name": "Ger\u00e4t" - } + }, + "title": "WOLF-Ger\u00e4t ausw\u00e4hlen" }, "user": { "data": { "password": "Passwort", "username": "Benutzername" - } + }, + "title": "WOLF SmartSet-Verbindung" } } } diff --git a/homeassistant/components/wolflink/translations/sensor.de.json b/homeassistant/components/wolflink/translations/sensor.de.json index 17c365e88c4c5..a83fb9856ad8c 100644 --- a/homeassistant/components/wolflink/translations/sensor.de.json +++ b/homeassistant/components/wolflink/translations/sensor.de.json @@ -1,17 +1,79 @@ { "state": { "wolflink__state": { + "1_x_warmwasser": "1 x Warmwasser", + "abgasklappe": "Abgasklappe", + "absenkbetrieb": "Absenkbetrieb", + "absenkstop": "Absenkstop", + "aktiviert": "Aktiviert", + "antilegionellenfunktion": "Anti-Legionellen-Funktion", + "at_abschaltung": "AT Abschaltung", + "at_frostschutz": "AT Frostschutz", + "aus": "Aus", + "auto": "", + "automatik_aus": "Automatik AUS", + "automatik_ein": "Automatik EIN", + "betrieb_ohne_brenner": "Betrieb ohne Brenner", + "cooling": "K\u00fchlung", + "deaktiviert": "Deaktiviert", + "eco": "Eco", + "ein": "Ein", + "estrichtrocknung": "Estrichtrocknung", + "externe_deaktivierung": "Externe Deaktivierung", + "fernschalter_ein": "Fernsteuerung aktiviert", + "frost_heizkreis": "Frost Heizkreis", + "frost_warmwasser": "Warmwasser Frost", + "frostschutz": "Frostschutz", + "gasdruck": "Gasdruck", + "gradienten_uberwachung": "Gradienten\u00fcberwachung", + "heizbetrieb": "Heizbetrieb", + "heizgerat_mit_speicher": "Heizger\u00e4t mit Speicher", + "heizung": "Heizung", + "initialisierung": "Initialisierung", + "kalibration": "Kalibrierung", + "kalibration_heizbetrieb": "Kalibrierung Heizbetrieb", + "kalibration_kombibetrieb": "Kalibrierung Kombibetrieb", + "kalibration_warmwasserbetrieb": "Kalibrierung Warmwasserbetrieb", + "kaskadenbetrieb": "Kaskadenbetrieb", + "kombibetrieb": "Kombibetrieb", + "kombigerat": "Kombiger\u00e4t", + "kombigerat_mit_solareinbindung": "Kombiger\u00e4t mit Solareinbindung", + "mindest_kombizeit": "Minimale Kombizeit", + "nachlauf_heizkreispumpe": "Nachlauf Heizkreispumpe", + "nachspulen": "Nachsp\u00fclung", + "nur_heizgerat": "Nur Heizger\u00e4t", + "parallelbetrieb": "Parallelbetrieb", "partymodus": "Party-Modus", "permanent": "Permanent", + "permanentbetrieb": "Permanentbetrieb", + "reduzierter_betrieb": "Reduzierter Betrieb", + "rt_abschaltung": "RT Abschaltung", + "rt_frostschutz": "RT Frostschutz", + "ruhekontakt": "Ruhekontakt", + "schornsteinfeger": "Emissionspr\u00fcfung", + "smart_grid": "SmartGrid", + "smart_home": "SmartHome", "solarbetrieb": "Solarmodus", "sparbetrieb": "Sparmodus", "sparen": "Sparen", + "spreizung_kf": "Spreizung KF", "stabilisierung": "Stabilisierung", "standby": "Standby", "start": "Start", "storung": "Fehler", + "taktsperre": "Taktsperre", + "telefonfernschalter": "Telefonfernschalter", "test": "Test", - "tpw": "TPW" + "tpw": "TPW", + "urlaubsmodus": "Urlaubsmodus", + "ventilprufung": "Ventilpr\u00fcfung", + "vorspulen": "Vorsp\u00fclen", + "warmwasser": "Warmwasser", + "warmwasser_schnellstart": "Warmwasser Schnellstart", + "warmwasserbetrieb": "Warmwasserbetrieb", + "warmwassernachlauf": "Warmwassernachlauf", + "warmwasservorrang": "Warmwasserpriorit\u00e4t", + "zunden": "Z\u00fcnden" } } } \ No newline at end of file diff --git a/homeassistant/components/zha/translations/de.json b/homeassistant/components/zha/translations/de.json index a0cc570a900db..f1dea3341d7fa 100644 --- a/homeassistant/components/zha/translations/de.json +++ b/homeassistant/components/zha/translations/de.json @@ -66,6 +66,7 @@ "device_dropped": "Ger\u00e4t ist gefallen", "device_flipped": "Ger\u00e4t umgedreht \"{subtype}\"", "device_knocked": "Ger\u00e4t klopfte \"{subtype}\"", + "device_offline": "Ger\u00e4t offline", "device_rotated": "Ger\u00e4t wurde gedreht \"{subtype}\"", "device_shaken": "Ger\u00e4t ersch\u00fcttert", "device_slid": "Ger\u00e4t gerutscht \"{subtype}\"", diff --git a/homeassistant/components/zha/translations/es.json b/homeassistant/components/zha/translations/es.json index 677d60d0d65c6..4fc089b26e9dd 100644 --- a/homeassistant/components/zha/translations/es.json +++ b/homeassistant/components/zha/translations/es.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "No se pudo conectar" }, + "flow_title": "ZHA: {name}", "step": { "pick_radio": { "data": { From 44beff31c26fd38a156b27903c004ccff6ab846e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 13 Apr 2021 16:16:26 -1000 Subject: [PATCH 0258/1317] Cancel config entry retry, platform retry, and polling at the stop event (#49138) --- homeassistant/config_entries.py | 25 +++++++++++--- homeassistant/helpers/entity_component.py | 16 +++++++-- homeassistant/helpers/entity_platform.py | 23 ++++++++++--- tests/helpers/test_entity_component.py | 28 +++++++++++++++- tests/helpers/test_entity_platform.py | 23 +++++++++++++ tests/test_config_entries.py | 41 +++++++++++++++++++++-- 6 files changed, 143 insertions(+), 13 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index d689d4548a9d6..34afc77e52809 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -12,7 +12,7 @@ import attr from homeassistant import data_entry_flow, loader -from homeassistant.const import EVENT_HOMEASSISTANT_STARTED +from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP from homeassistant.core import CALLBACK_TYPE, CoreState, HomeAssistant, callback from homeassistant.exceptions import ( ConfigEntryAuthFailed, @@ -331,6 +331,17 @@ async def setup_again(*_: Any) -> None: else: self.state = ENTRY_STATE_SETUP_ERROR + async def async_shutdown(self) -> None: + """Call when Home Assistant is stopping.""" + self.async_cancel_retry_setup() + + @callback + def async_cancel_retry_setup(self) -> None: + """Cancel retry setup.""" + if self._async_cancel_retry_setup is not None: + self._async_cancel_retry_setup() + self._async_cancel_retry_setup = None + async def async_unload( self, hass: HomeAssistant, *, integration: loader.Integration | None = None ) -> bool: @@ -360,9 +371,7 @@ async def async_unload( return False if self.state != ENTRY_STATE_LOADED: - if self._async_cancel_retry_setup is not None: - self._async_cancel_retry_setup() - self._async_cancel_retry_setup = None + self.async_cancel_retry_setup() self.state = ENTRY_STATE_NOT_LOADED return True @@ -778,6 +787,12 @@ async def async_remove(self, entry_id: str) -> dict[str, Any]: return {"require_restart": not unload_success} + async def _async_shutdown(self, event: Event) -> None: + """Call when Home Assistant is stopping.""" + await asyncio.gather( + *[entry.async_shutdown() for entry in self._entries.values()] + ) + async def async_initialize(self) -> None: """Initialize config entry config.""" # Migrating for config entries stored before 0.73 @@ -787,6 +802,8 @@ async def async_initialize(self) -> None: old_conf_migrate_func=_old_conf_migrator, ) + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._async_shutdown) + if config is None: self._entries = {} return diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 1713166524046..46279fcb14010 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -12,8 +12,12 @@ from homeassistant import config as conf_util from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ENTITY_NAMESPACE, CONF_SCAN_INTERVAL -from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.const import ( + CONF_ENTITY_NAMESPACE, + CONF_SCAN_INTERVAL, + EVENT_HOMEASSISTANT_STOP, +) +from homeassistant.core import Event, HomeAssistant, ServiceCall, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import ( config_per_platform, @@ -118,6 +122,8 @@ async def async_setup(self, config: ConfigType) -> None: This method must be run in the event loop. """ + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._async_shutdown) + self.config = config # Look in config for Domain, Domain 2, Domain 3 etc and load them @@ -322,3 +328,9 @@ def _async_init_entity_platform( scan_interval=scan_interval, entity_namespace=entity_namespace, ) + + async def _async_shutdown(self, event: Event) -> None: + """Call when Home Assistant is stopping.""" + await asyncio.gather( + *[platform.async_shutdown() for platform in chain(self._platforms.values())] + ) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 25996c81d9d32..ef45b8dcd97a5 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -174,6 +174,18 @@ def async_create_setup_task() -> Coroutine: await self._async_setup_platform(async_create_setup_task) + async def async_shutdown(self) -> None: + """Call when Home Assistant is stopping.""" + self.async_cancel_retry_setup() + self.async_unsub_polling() + + @callback + def async_cancel_retry_setup(self) -> None: + """Cancel retry setup.""" + if self._async_cancel_retry_setup is not None: + self._async_cancel_retry_setup() + self._async_cancel_retry_setup = None + async def async_setup_entry(self, config_entry: config_entries.ConfigEntry) -> bool: """Set up the platform from a config entry.""" # Store it so that we can save config entry ID in entity registry @@ -549,9 +561,7 @@ async def async_reset(self) -> None: This method must be run in the event loop. """ - if self._async_cancel_retry_setup is not None: - self._async_cancel_retry_setup() - self._async_cancel_retry_setup = None + self.async_cancel_retry_setup() if not self.entities: return @@ -560,10 +570,15 @@ async def async_reset(self) -> None: await asyncio.gather(*tasks) + self.async_unsub_polling() + self._setup_complete = False + + @callback + def async_unsub_polling(self) -> None: + """Stop polling.""" if self._async_unsub_polling is not None: self._async_unsub_polling() self._async_unsub_polling = None - self._setup_complete = False async def async_destroy(self) -> None: """Destroy an entity platform. diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index 8d61ec7d509ff..1d18111b0d34d 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -8,7 +8,11 @@ import pytest import voluptuous as vol -from homeassistant.const import ENTITY_MATCH_ALL, ENTITY_MATCH_NONE +from homeassistant.const import ( + ENTITY_MATCH_ALL, + ENTITY_MATCH_NONE, + EVENT_HOMEASSISTANT_STOP, +) import homeassistant.core as ha from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import discovery @@ -487,3 +491,25 @@ def appender(**kwargs): DOMAIN, "hello", {"area_id": ENTITY_MATCH_NONE, "some": "data"}, blocking=True ) assert len(calls) == 2 + + +async def test_platforms_shutdown_on_stop(hass): + """Test that we shutdown platforms on stop.""" + platform1_setup = Mock(side_effect=[PlatformNotReady, PlatformNotReady, None]) + mock_integration(hass, MockModule("mod1")) + mock_entity_platform(hass, "test_domain.mod1", MockPlatform(platform1_setup)) + + component = EntityComponent(_LOGGER, DOMAIN, hass) + + await component.async_setup({DOMAIN: {"platform": "mod1"}}) + await hass.async_block_till_done() + assert len(platform1_setup.mock_calls) == 1 + assert "test_domain.mod1" not in hass.config.components + + with patch.object( + component._platforms[DOMAIN], "async_shutdown" + ) as mock_async_shutdown: + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + + assert mock_async_shutdown.called diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index e842d5aa1ae6b..d24084ff51714 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -685,6 +685,29 @@ async def test_reset_cancels_retry_setup_when_not_started(hass): assert ent_platform._async_cancel_retry_setup is None +async def test_stop_shutdown_cancels_retry_setup_and_interval_listener(hass): + """Test that shutdown will cancel scheduled a setup retry and interval listener.""" + async_setup_entry = Mock(side_effect=PlatformNotReady) + platform = MockPlatform(async_setup_entry=async_setup_entry) + config_entry = MockConfigEntry() + ent_platform = MockEntityPlatform( + hass, platform_name=config_entry.domain, platform=platform + ) + + with patch.object(entity_platform, "async_call_later") as mock_call_later: + assert not await ent_platform.async_setup_entry(config_entry) + + assert len(mock_call_later.mock_calls) == 1 + assert len(mock_call_later.return_value.mock_calls) == 0 + assert ent_platform._async_cancel_retry_setup is not None + + await ent_platform.async_shutdown() + + assert len(mock_call_later.return_value.mock_calls) == 1 + assert ent_platform._async_unsub_polling is None + assert ent_platform._async_cancel_retry_setup is None + + async def test_not_fails_with_adding_empty_entities_(hass): """Test for not fails on empty entities list.""" component = EntityComponent(_LOGGER, DOMAIN, hass) diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index dbfe48129c1aa..20ab5e67fef6c 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -7,7 +7,7 @@ import pytest from homeassistant import config_entries, data_entry_flow, loader -from homeassistant.const import EVENT_HOMEASSISTANT_STARTED +from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP from homeassistant.core import CoreState, callback from homeassistant.exceptions import ( ConfigEntryAuthFailed, @@ -1405,7 +1405,7 @@ async def test_reload_entry_entity_registry_works(hass): assert len(mock_unload_entry.mock_calls) == 1 -async def test_unqiue_id_persisted(hass, manager): +async def test_unique_id_persisted(hass, manager): """Test that a unique ID is stored in the config entry.""" mock_setup_entry = AsyncMock(return_value=True) @@ -2667,3 +2667,40 @@ async def _async_update_data(): assert entry.state == config_entries.ENTRY_STATE_LOADED flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 + + +async def test_initialize_and_shutdown(hass): + """Test we call the shutdown function at stop.""" + manager = config_entries.ConfigEntries(hass, {}) + + with patch.object(manager, "_async_shutdown") as mock_async_shutdown: + await manager.async_initialize() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + + assert mock_async_shutdown.called + + +async def test_setup_retrying_during_shutdown(hass): + """Test if we shutdown an entry that is in retry mode.""" + entry = MockConfigEntry(domain="test") + + mock_setup_entry = AsyncMock(side_effect=ConfigEntryNotReady) + mock_integration(hass, MockModule("test", async_setup_entry=mock_setup_entry)) + mock_entity_platform(hass, "config_flow.test", None) + + with patch("homeassistant.helpers.event.async_call_later") as mock_call: + await entry.async_setup(hass) + + assert entry.state == config_entries.ENTRY_STATE_SETUP_RETRY + assert len(mock_call.return_value.mock_calls) == 0 + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + + assert len(mock_call.return_value.mock_calls) == 0 + + async_fire_time_changed(hass, dt.utcnow() + timedelta(hours=4)) + await hass.async_block_till_done() + + assert len(mock_call.return_value.mock_calls) == 0 From 1a5068f71dbef5670b562350e657e4060005858a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 14 Apr 2021 09:18:22 +0200 Subject: [PATCH 0259/1317] Use supported_color_modes in google_assistant (#49176) * Use supported_color_modes in google_assistant * Fix tests --- .../components/google_assistant/helpers.py | 5 +- .../components/google_assistant/trait.py | 56 ++++---- .../google_assistant/test_smart_home.py | 6 +- .../components/google_assistant/test_trait.py | 131 ++++++++++-------- 4 files changed, 108 insertions(+), 90 deletions(-) diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 7eb69d0872432..752f40a0ead60 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -408,7 +408,8 @@ def traits(self): state = self.state domain = state.domain - features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + attributes = state.attributes + features = attributes.get(ATTR_SUPPORTED_FEATURES, 0) if not isinstance(features, int): _LOGGER.warning( @@ -423,7 +424,7 @@ def traits(self): self._traits = [ Trait(self.hass, state, self.config) for Trait in trait.TRAITS - if Trait.supported(domain, features, device_class) + if Trait.supported(domain, features, device_class, attributes) ] return self._traits diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 384c5bfd0ae29..3bfce41ae2b74 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -212,10 +212,11 @@ class BrightnessTrait(_Trait): commands = [COMMAND_BRIGHTNESS_ABSOLUTE] @staticmethod - def supported(domain, features, device_class): + def supported(domain, features, device_class, attributes): """Test if state is supported.""" + color_modes = attributes.get(light.ATTR_SUPPORTED_COLOR_MODES, []) if domain == light.DOMAIN: - return features & light.SUPPORT_BRIGHTNESS + return any(mode in color_modes for mode in light.COLOR_MODES_BRIGHTNESS) return False @@ -267,7 +268,7 @@ class CameraStreamTrait(_Trait): stream_info = None @staticmethod - def supported(domain, features, device_class): + def supported(domain, features, device_class, _): """Test if state is supported.""" if domain == camera.DOMAIN: return features & camera.SUPPORT_STREAM @@ -308,7 +309,7 @@ class OnOffTrait(_Trait): commands = [COMMAND_ONOFF] @staticmethod - def supported(domain, features, device_class): + def supported(domain, features, device_class, _): """Test if state is supported.""" return domain in ( group.DOMAIN, @@ -362,23 +363,26 @@ class ColorSettingTrait(_Trait): commands = [COMMAND_COLOR_ABSOLUTE] @staticmethod - def supported(domain, features, device_class): + def supported(domain, features, device_class, attributes): """Test if state is supported.""" if domain != light.DOMAIN: return False - return features & light.SUPPORT_COLOR_TEMP or features & light.SUPPORT_COLOR + color_modes = attributes.get(light.ATTR_SUPPORTED_COLOR_MODES, []) + return light.COLOR_MODE_COLOR_TEMP in color_modes or any( + mode in color_modes for mode in light.COLOR_MODES_COLOR + ) def sync_attributes(self): """Return color temperature attributes for a sync request.""" attrs = self.state.attributes - features = attrs.get(ATTR_SUPPORTED_FEATURES, 0) + color_modes = attrs.get(light.ATTR_SUPPORTED_COLOR_MODES, []) response = {} - if features & light.SUPPORT_COLOR: + if any(mode in color_modes for mode in light.COLOR_MODES_COLOR): response["colorModel"] = "hsv" - if features & light.SUPPORT_COLOR_TEMP: + if light.COLOR_MODE_COLOR_TEMP in color_modes: # Max Kelvin is Min Mireds K = 1000000 / mireds # Min Kelvin is Max Mireds K = 1000000 / mireds response["colorTemperatureRange"] = { @@ -394,10 +398,10 @@ def sync_attributes(self): def query_attributes(self): """Return color temperature query attributes.""" - features = self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + color_modes = self.state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES, []) color = {} - if features & light.SUPPORT_COLOR: + if any(mode in color_modes for mode in light.COLOR_MODES_COLOR): color_hs = self.state.attributes.get(light.ATTR_HS_COLOR) brightness = self.state.attributes.get(light.ATTR_BRIGHTNESS, 1) if color_hs is not None: @@ -407,7 +411,7 @@ def query_attributes(self): "value": brightness / 255, } - if features & light.SUPPORT_COLOR_TEMP: + if light.COLOR_MODE_COLOR_TEMP in color_modes: temp = self.state.attributes.get(light.ATTR_COLOR_TEMP) # Some faulty integrations might put 0 in here, raising exception. if temp == 0: @@ -495,7 +499,7 @@ class SceneTrait(_Trait): commands = [COMMAND_ACTIVATE_SCENE] @staticmethod - def supported(domain, features, device_class): + def supported(domain, features, device_class, _): """Test if state is supported.""" return domain in (scene.DOMAIN, script.DOMAIN) @@ -531,7 +535,7 @@ class DockTrait(_Trait): commands = [COMMAND_DOCK] @staticmethod - def supported(domain, features, device_class): + def supported(domain, features, device_class, _): """Test if state is supported.""" return domain == vacuum.DOMAIN @@ -565,7 +569,7 @@ class StartStopTrait(_Trait): commands = [COMMAND_STARTSTOP, COMMAND_PAUSEUNPAUSE] @staticmethod - def supported(domain, features, device_class): + def supported(domain, features, device_class, _): """Test if state is supported.""" if domain == vacuum.DOMAIN: return True @@ -709,7 +713,7 @@ class TemperatureSettingTrait(_Trait): google_to_preset = {value: key for key, value in preset_to_google.items()} @staticmethod - def supported(domain, features, device_class): + def supported(domain, features, device_class, _): """Test if state is supported.""" if domain == climate.DOMAIN: return True @@ -976,7 +980,7 @@ class HumiditySettingTrait(_Trait): commands = [COMMAND_SET_HUMIDITY] @staticmethod - def supported(domain, features, device_class): + def supported(domain, features, device_class, _): """Test if state is supported.""" if domain == humidifier.DOMAIN: return True @@ -1059,7 +1063,7 @@ class LockUnlockTrait(_Trait): commands = [COMMAND_LOCKUNLOCK] @staticmethod - def supported(domain, features, device_class): + def supported(domain, features, device_class, _): """Test if state is supported.""" return domain == lock.DOMAIN @@ -1120,7 +1124,7 @@ class ArmDisArmTrait(_Trait): } @staticmethod - def supported(domain, features, device_class): + def supported(domain, features, device_class, _): """Test if state is supported.""" return domain == alarm_control_panel.DOMAIN @@ -1236,7 +1240,7 @@ class FanSpeedTrait(_Trait): } @staticmethod - def supported(domain, features, device_class): + def supported(domain, features, device_class, _): """Test if state is supported.""" if domain == fan.DOMAIN: return features & fan.SUPPORT_SET_SPEED @@ -1349,7 +1353,7 @@ class ModesTrait(_Trait): } @staticmethod - def supported(domain, features, device_class): + def supported(domain, features, device_class, _): """Test if state is supported.""" if domain == input_select.DOMAIN: return True @@ -1518,7 +1522,7 @@ class InputSelectorTrait(_Trait): SYNONYMS = {} @staticmethod - def supported(domain, features, device_class): + def supported(domain, features, device_class, _): """Test if state is supported.""" if domain == media_player.DOMAIN and ( features & media_player.SUPPORT_SELECT_SOURCE @@ -1591,7 +1595,7 @@ class OpenCloseTrait(_Trait): commands = [COMMAND_OPENCLOSE, COMMAND_OPENCLOSE_RELATIVE] @staticmethod - def supported(domain, features, device_class): + def supported(domain, features, device_class, _): """Test if state is supported.""" if domain == cover.DOMAIN: return True @@ -1727,7 +1731,7 @@ class VolumeTrait(_Trait): commands = [COMMAND_SET_VOLUME, COMMAND_VOLUME_RELATIVE, COMMAND_MUTE] @staticmethod - def supported(domain, features, device_class): + def supported(domain, features, device_class, _): """Test if trait is supported.""" if domain == media_player.DOMAIN: return features & ( @@ -1915,7 +1919,7 @@ class TransportControlTrait(_Trait): ] @staticmethod - def supported(domain, features, device_class): + def supported(domain, features, device_class, _): """Test if state is supported.""" if domain == media_player.DOMAIN: for feature in MEDIA_COMMAND_SUPPORT_MAPPING.values(): @@ -2034,7 +2038,7 @@ class MediaStateTrait(_Trait): } @staticmethod - def supported(domain, features, device_class): + def supported(domain, features, device_class, _): """Test if state is supported.""" return domain == media_player.DOMAIN diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 9531602ef0c51..0dfa9e2a5e908 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -1171,7 +1171,7 @@ async def test_sync_message_recovery(hass, caplog): "on", { "min_mireds": "badvalue", - "supported_features": hass.components.light.SUPPORT_COLOR_TEMP, + "supported_color_modes": ["color_temp"], }, ) @@ -1220,7 +1220,7 @@ async def test_query_recover(hass, caplog): "light.good", "on", { - "supported_features": hass.components.light.SUPPORT_BRIGHTNESS, + "supported_color_modes": ["brightness"], "brightness": 50, }, ) @@ -1228,7 +1228,7 @@ async def test_query_recover(hass, caplog): "light.bad", "on", { - "supported_features": hass.components.light.SUPPORT_BRIGHTNESS, + "supported_color_modes": ["brightness"], "brightness": "shoe", }, ) diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index fd62d225aace4..1d70027024a46 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -70,10 +70,15 @@ ) -async def test_brightness_light(hass): +@pytest.mark.parametrize( + "supported_color_modes", [["brightness"], ["hs"], ["color_temp"]] +) +async def test_brightness_light(hass, supported_color_modes): """Test brightness trait support for light domain.""" assert helpers.get_google_type(light.DOMAIN, None) is not None - assert trait.BrightnessTrait.supported(light.DOMAIN, light.SUPPORT_BRIGHTNESS, None) + assert trait.BrightnessTrait.supported( + light.DOMAIN, 0, None, {"supported_color_modes": supported_color_modes} + ) trt = trait.BrightnessTrait( hass, @@ -111,7 +116,9 @@ async def test_camera_stream(hass): {"external_url": "https://example.com"}, ) assert helpers.get_google_type(camera.DOMAIN, None) is not None - assert trait.CameraStreamTrait.supported(camera.DOMAIN, camera.SUPPORT_STREAM, None) + assert trait.CameraStreamTrait.supported( + camera.DOMAIN, camera.SUPPORT_STREAM, None, None + ) trt = trait.CameraStreamTrait( hass, State("camera.bla", camera.STATE_IDLE, {}), BASIC_CONFIG @@ -140,7 +147,7 @@ async def test_camera_stream(hass): async def test_onoff_group(hass): """Test OnOff trait support for group domain.""" assert helpers.get_google_type(group.DOMAIN, None) is not None - assert trait.OnOffTrait.supported(group.DOMAIN, 0, None) + assert trait.OnOffTrait.supported(group.DOMAIN, 0, None, None) trt_on = trait.OnOffTrait(hass, State("group.bla", STATE_ON), BASIC_CONFIG) @@ -166,7 +173,7 @@ async def test_onoff_group(hass): async def test_onoff_input_boolean(hass): """Test OnOff trait support for input_boolean domain.""" assert helpers.get_google_type(input_boolean.DOMAIN, None) is not None - assert trait.OnOffTrait.supported(input_boolean.DOMAIN, 0, None) + assert trait.OnOffTrait.supported(input_boolean.DOMAIN, 0, None, None) trt_on = trait.OnOffTrait(hass, State("input_boolean.bla", STATE_ON), BASIC_CONFIG) @@ -194,7 +201,7 @@ async def test_onoff_input_boolean(hass): async def test_onoff_switch(hass): """Test OnOff trait support for switch domain.""" assert helpers.get_google_type(switch.DOMAIN, None) is not None - assert trait.OnOffTrait.supported(switch.DOMAIN, 0, None) + assert trait.OnOffTrait.supported(switch.DOMAIN, 0, None, None) trt_on = trait.OnOffTrait(hass, State("switch.bla", STATE_ON), BASIC_CONFIG) @@ -225,7 +232,7 @@ async def test_onoff_switch(hass): async def test_onoff_fan(hass): """Test OnOff trait support for fan domain.""" assert helpers.get_google_type(fan.DOMAIN, None) is not None - assert trait.OnOffTrait.supported(fan.DOMAIN, 0, None) + assert trait.OnOffTrait.supported(fan.DOMAIN, 0, None, None) trt_on = trait.OnOffTrait(hass, State("fan.bla", STATE_ON), BASIC_CONFIG) @@ -250,7 +257,7 @@ async def test_onoff_fan(hass): async def test_onoff_light(hass): """Test OnOff trait support for light domain.""" assert helpers.get_google_type(light.DOMAIN, None) is not None - assert trait.OnOffTrait.supported(light.DOMAIN, 0, None) + assert trait.OnOffTrait.supported(light.DOMAIN, 0, None, None) trt_on = trait.OnOffTrait(hass, State("light.bla", STATE_ON), BASIC_CONFIG) @@ -276,7 +283,7 @@ async def test_onoff_light(hass): async def test_onoff_media_player(hass): """Test OnOff trait support for media_player domain.""" assert helpers.get_google_type(media_player.DOMAIN, None) is not None - assert trait.OnOffTrait.supported(media_player.DOMAIN, 0, None) + assert trait.OnOffTrait.supported(media_player.DOMAIN, 0, None, None) trt_on = trait.OnOffTrait(hass, State("media_player.bla", STATE_ON), BASIC_CONFIG) @@ -303,7 +310,7 @@ async def test_onoff_media_player(hass): async def test_onoff_humidifier(hass): """Test OnOff trait support for humidifier domain.""" assert helpers.get_google_type(humidifier.DOMAIN, None) is not None - assert trait.OnOffTrait.supported(humidifier.DOMAIN, 0, None) + assert trait.OnOffTrait.supported(humidifier.DOMAIN, 0, None, None) trt_on = trait.OnOffTrait(hass, State("humidifier.bla", STATE_ON), BASIC_CONFIG) @@ -330,7 +337,7 @@ async def test_onoff_humidifier(hass): async def test_dock_vacuum(hass): """Test dock trait support for vacuum domain.""" assert helpers.get_google_type(vacuum.DOMAIN, None) is not None - assert trait.DockTrait.supported(vacuum.DOMAIN, 0, None) + assert trait.DockTrait.supported(vacuum.DOMAIN, 0, None, None) trt = trait.DockTrait(hass, State("vacuum.bla", vacuum.STATE_IDLE), BASIC_CONFIG) @@ -347,7 +354,7 @@ async def test_dock_vacuum(hass): async def test_startstop_vacuum(hass): """Test startStop trait support for vacuum domain.""" assert helpers.get_google_type(vacuum.DOMAIN, None) is not None - assert trait.StartStopTrait.supported(vacuum.DOMAIN, 0, None) + assert trait.StartStopTrait.supported(vacuum.DOMAIN, 0, None, None) trt = trait.StartStopTrait( hass, @@ -387,7 +394,7 @@ async def test_startstop_vacuum(hass): async def test_startstop_cover(hass): """Test startStop trait support for cover domain.""" assert helpers.get_google_type(cover.DOMAIN, None) is not None - assert trait.StartStopTrait.supported(cover.DOMAIN, cover.SUPPORT_STOP, None) + assert trait.StartStopTrait.supported(cover.DOMAIN, cover.SUPPORT_STOP, None, None) state = State( "cover.bla", @@ -447,11 +454,14 @@ async def test_startstop_cover_assumed(hass): assert stop_calls[0].data == {ATTR_ENTITY_ID: "cover.bla"} -async def test_color_setting_color_light(hass): +@pytest.mark.parametrize("supported_color_modes", [["hs"], ["rgb"], ["xy"]]) +async def test_color_setting_color_light(hass, supported_color_modes): """Test ColorSpectrum trait support for light domain.""" assert helpers.get_google_type(light.DOMAIN, None) is not None - assert not trait.ColorSettingTrait.supported(light.DOMAIN, 0, None) - assert trait.ColorSettingTrait.supported(light.DOMAIN, light.SUPPORT_COLOR, None) + assert not trait.ColorSettingTrait.supported(light.DOMAIN, 0, None, {}) + assert trait.ColorSettingTrait.supported( + light.DOMAIN, 0, None, {"supported_color_modes": supported_color_modes} + ) trt = trait.ColorSettingTrait( hass, @@ -461,7 +471,7 @@ async def test_color_setting_color_light(hass): { light.ATTR_HS_COLOR: (20, 94), light.ATTR_BRIGHTNESS: 200, - ATTR_SUPPORTED_FEATURES: light.SUPPORT_COLOR, + "supported_color_modes": supported_color_modes, }, ), BASIC_CONFIG, @@ -507,9 +517,9 @@ async def test_color_setting_color_light(hass): async def test_color_setting_temperature_light(hass): """Test ColorTemperature trait support for light domain.""" assert helpers.get_google_type(light.DOMAIN, None) is not None - assert not trait.ColorSettingTrait.supported(light.DOMAIN, 0, None) + assert not trait.ColorSettingTrait.supported(light.DOMAIN, 0, None, {}) assert trait.ColorSettingTrait.supported( - light.DOMAIN, light.SUPPORT_COLOR_TEMP, None + light.DOMAIN, 0, None, {"supported_color_modes": ["color_temp"]} ) trt = trait.ColorSettingTrait( @@ -521,7 +531,7 @@ async def test_color_setting_temperature_light(hass): light.ATTR_MIN_MIREDS: 200, light.ATTR_COLOR_TEMP: 300, light.ATTR_MAX_MIREDS: 500, - ATTR_SUPPORTED_FEATURES: light.SUPPORT_COLOR_TEMP, + "supported_color_modes": ["color_temp"], }, ), BASIC_CONFIG, @@ -560,9 +570,9 @@ async def test_color_setting_temperature_light(hass): async def test_color_light_temperature_light_bad_temp(hass): """Test ColorTemperature trait support for light domain.""" assert helpers.get_google_type(light.DOMAIN, None) is not None - assert not trait.ColorSettingTrait.supported(light.DOMAIN, 0, None) + assert not trait.ColorSettingTrait.supported(light.DOMAIN, 0, None, {}) assert trait.ColorSettingTrait.supported( - light.DOMAIN, light.SUPPORT_COLOR_TEMP, None + light.DOMAIN, 0, None, {"supported_color_modes": ["color_temp"]} ) trt = trait.ColorSettingTrait( @@ -585,7 +595,7 @@ async def test_color_light_temperature_light_bad_temp(hass): async def test_light_modes(hass): """Test Light Mode trait.""" assert helpers.get_google_type(light.DOMAIN, None) is not None - assert trait.ModesTrait.supported(light.DOMAIN, light.SUPPORT_EFFECT, None) + assert trait.ModesTrait.supported(light.DOMAIN, light.SUPPORT_EFFECT, None, None) trt = trait.ModesTrait( hass, @@ -653,7 +663,7 @@ async def test_light_modes(hass): async def test_scene_scene(hass): """Test Scene trait support for scene domain.""" assert helpers.get_google_type(scene.DOMAIN, None) is not None - assert trait.SceneTrait.supported(scene.DOMAIN, 0, None) + assert trait.SceneTrait.supported(scene.DOMAIN, 0, None, None) trt = trait.SceneTrait(hass, State("scene.bla", scene.STATE), BASIC_CONFIG) assert trt.sync_attributes() == {} @@ -669,7 +679,7 @@ async def test_scene_scene(hass): async def test_scene_script(hass): """Test Scene trait support for script domain.""" assert helpers.get_google_type(script.DOMAIN, None) is not None - assert trait.SceneTrait.supported(script.DOMAIN, 0, None) + assert trait.SceneTrait.supported(script.DOMAIN, 0, None, None) trt = trait.SceneTrait(hass, State("script.bla", STATE_OFF), BASIC_CONFIG) assert trt.sync_attributes() == {} @@ -689,7 +699,7 @@ async def test_scene_script(hass): async def test_temperature_setting_climate_onoff(hass): """Test TemperatureSetting trait support for climate domain - range.""" assert helpers.get_google_type(climate.DOMAIN, None) is not None - assert trait.TemperatureSettingTrait.supported(climate.DOMAIN, 0, None) + assert trait.TemperatureSettingTrait.supported(climate.DOMAIN, 0, None, None) hass.config.units.temperature_unit = TEMP_FAHRENHEIT @@ -734,7 +744,7 @@ async def test_temperature_setting_climate_onoff(hass): async def test_temperature_setting_climate_no_modes(hass): """Test TemperatureSetting trait support for climate domain not supporting any modes.""" assert helpers.get_google_type(climate.DOMAIN, None) is not None - assert trait.TemperatureSettingTrait.supported(climate.DOMAIN, 0, None) + assert trait.TemperatureSettingTrait.supported(climate.DOMAIN, 0, None, None) hass.config.units.temperature_unit = TEMP_CELSIUS @@ -760,7 +770,7 @@ async def test_temperature_setting_climate_no_modes(hass): async def test_temperature_setting_climate_range(hass): """Test TemperatureSetting trait support for climate domain - range.""" assert helpers.get_google_type(climate.DOMAIN, None) is not None - assert trait.TemperatureSettingTrait.supported(climate.DOMAIN, 0, None) + assert trait.TemperatureSettingTrait.supported(climate.DOMAIN, 0, None, None) hass.config.units.temperature_unit = TEMP_FAHRENHEIT @@ -842,7 +852,7 @@ async def test_temperature_setting_climate_range(hass): async def test_temperature_setting_climate_setpoint(hass): """Test TemperatureSetting trait support for climate domain - setpoint.""" assert helpers.get_google_type(climate.DOMAIN, None) is not None - assert trait.TemperatureSettingTrait.supported(climate.DOMAIN, 0, None) + assert trait.TemperatureSettingTrait.supported(climate.DOMAIN, 0, None, None) hass.config.units.temperature_unit = TEMP_CELSIUS @@ -947,7 +957,7 @@ async def test_temperature_setting_climate_setpoint_auto(hass): async def test_humidity_setting_humidifier_setpoint(hass): """Test HumiditySetting trait support for humidifier domain - setpoint.""" assert helpers.get_google_type(humidifier.DOMAIN, None) is not None - assert trait.HumiditySettingTrait.supported(humidifier.DOMAIN, 0, None) + assert trait.HumiditySettingTrait.supported(humidifier.DOMAIN, 0, None, None) trt = trait.HumiditySettingTrait( hass, @@ -983,7 +993,7 @@ async def test_humidity_setting_humidifier_setpoint(hass): async def test_lock_unlock_lock(hass): """Test LockUnlock trait locking support for lock domain.""" assert helpers.get_google_type(lock.DOMAIN, None) is not None - assert trait.LockUnlockTrait.supported(lock.DOMAIN, lock.SUPPORT_OPEN, None) + assert trait.LockUnlockTrait.supported(lock.DOMAIN, lock.SUPPORT_OPEN, None, None) assert trait.LockUnlockTrait.might_2fa(lock.DOMAIN, lock.SUPPORT_OPEN, None) trt = trait.LockUnlockTrait( @@ -1007,7 +1017,7 @@ async def test_lock_unlock_lock(hass): async def test_lock_unlock_unlock(hass): """Test LockUnlock trait unlocking support for lock domain.""" assert helpers.get_google_type(lock.DOMAIN, None) is not None - assert trait.LockUnlockTrait.supported(lock.DOMAIN, lock.SUPPORT_OPEN, None) + assert trait.LockUnlockTrait.supported(lock.DOMAIN, lock.SUPPORT_OPEN, None, None) trt = trait.LockUnlockTrait( hass, State("lock.front_door", lock.STATE_LOCKED), PIN_CONFIG @@ -1067,7 +1077,7 @@ async def test_lock_unlock_unlock(hass): async def test_arm_disarm_arm_away(hass): """Test ArmDisarm trait Arming support for alarm_control_panel domain.""" assert helpers.get_google_type(alarm_control_panel.DOMAIN, None) is not None - assert trait.ArmDisArmTrait.supported(alarm_control_panel.DOMAIN, 0, None) + assert trait.ArmDisArmTrait.supported(alarm_control_panel.DOMAIN, 0, None, None) assert trait.ArmDisArmTrait.might_2fa(alarm_control_panel.DOMAIN, 0, None) trt = trait.ArmDisArmTrait( @@ -1230,7 +1240,7 @@ async def test_arm_disarm_arm_away(hass): async def test_arm_disarm_disarm(hass): """Test ArmDisarm trait Disarming support for alarm_control_panel domain.""" assert helpers.get_google_type(alarm_control_panel.DOMAIN, None) is not None - assert trait.ArmDisArmTrait.supported(alarm_control_panel.DOMAIN, 0, None) + assert trait.ArmDisArmTrait.supported(alarm_control_panel.DOMAIN, 0, None, None) assert trait.ArmDisArmTrait.might_2fa(alarm_control_panel.DOMAIN, 0, None) trt = trait.ArmDisArmTrait( @@ -1376,7 +1386,7 @@ async def test_arm_disarm_disarm(hass): async def test_fan_speed(hass): """Test FanSpeed trait speed control support for fan domain.""" assert helpers.get_google_type(fan.DOMAIN, None) is not None - assert trait.FanSpeedTrait.supported(fan.DOMAIN, fan.SUPPORT_SET_SPEED, None) + assert trait.FanSpeedTrait.supported(fan.DOMAIN, fan.SUPPORT_SET_SPEED, None, None) trt = trait.FanSpeedTrait( hass, @@ -1468,7 +1478,9 @@ async def test_fan_speed(hass): async def test_climate_fan_speed(hass): """Test FanSpeed trait speed control support for climate domain.""" assert helpers.get_google_type(climate.DOMAIN, None) is not None - assert trait.FanSpeedTrait.supported(climate.DOMAIN, climate.SUPPORT_FAN_MODE, None) + assert trait.FanSpeedTrait.supported( + climate.DOMAIN, climate.SUPPORT_FAN_MODE, None, None + ) trt = trait.FanSpeedTrait( hass, @@ -1529,7 +1541,7 @@ async def test_inputselector(hass): """Test input selector trait.""" assert helpers.get_google_type(media_player.DOMAIN, None) is not None assert trait.InputSelectorTrait.supported( - media_player.DOMAIN, media_player.SUPPORT_SELECT_SOURCE, None + media_player.DOMAIN, media_player.SUPPORT_SELECT_SOURCE, None, None ) trt = trait.InputSelectorTrait( @@ -1686,7 +1698,7 @@ async def test_inputselector_nextprev_invalid(hass, sources, source): async def test_modes_input_select(hass): """Test Input Select Mode trait.""" assert helpers.get_google_type(input_select.DOMAIN, None) is not None - assert trait.ModesTrait.supported(input_select.DOMAIN, None, None) + assert trait.ModesTrait.supported(input_select.DOMAIN, None, None, None) trt = trait.ModesTrait( hass, @@ -1762,7 +1774,9 @@ async def test_modes_input_select(hass): async def test_modes_humidifier(hass): """Test Humidifier Mode trait.""" assert helpers.get_google_type(humidifier.DOMAIN, None) is not None - assert trait.ModesTrait.supported(humidifier.DOMAIN, humidifier.SUPPORT_MODES, None) + assert trait.ModesTrait.supported( + humidifier.DOMAIN, humidifier.SUPPORT_MODES, None, None + ) trt = trait.ModesTrait( hass, @@ -1840,7 +1854,7 @@ async def test_sound_modes(hass): """Test Mode trait.""" assert helpers.get_google_type(media_player.DOMAIN, None) is not None assert trait.ModesTrait.supported( - media_player.DOMAIN, media_player.SUPPORT_SELECT_SOUND_MODE, None + media_player.DOMAIN, media_player.SUPPORT_SELECT_SOUND_MODE, None, None ) trt = trait.ModesTrait( @@ -1914,7 +1928,7 @@ async def test_openclose_cover(hass): """Test OpenClose trait support for cover domain.""" assert helpers.get_google_type(cover.DOMAIN, None) is not None assert trait.OpenCloseTrait.supported( - cover.DOMAIN, cover.SUPPORT_SET_POSITION, None + cover.DOMAIN, cover.SUPPORT_SET_POSITION, None, None ) trt = trait.OpenCloseTrait( @@ -1951,7 +1965,7 @@ async def test_openclose_cover_unknown_state(hass): """Test OpenClose trait support for cover domain with unknown state.""" assert helpers.get_google_type(cover.DOMAIN, None) is not None assert trait.OpenCloseTrait.supported( - cover.DOMAIN, cover.SUPPORT_SET_POSITION, None + cover.DOMAIN, cover.SUPPORT_SET_POSITION, None, None ) # No state @@ -1981,7 +1995,7 @@ async def test_openclose_cover_assumed_state(hass): """Test OpenClose trait support for cover domain.""" assert helpers.get_google_type(cover.DOMAIN, None) is not None assert trait.OpenCloseTrait.supported( - cover.DOMAIN, cover.SUPPORT_SET_POSITION, None + cover.DOMAIN, cover.SUPPORT_SET_POSITION, None, None ) trt = trait.OpenCloseTrait( @@ -2010,7 +2024,7 @@ async def test_openclose_cover_assumed_state(hass): async def test_openclose_cover_query_only(hass): """Test OpenClose trait support for cover domain.""" assert helpers.get_google_type(cover.DOMAIN, None) is not None - assert trait.OpenCloseTrait.supported(cover.DOMAIN, 0, None) + assert trait.OpenCloseTrait.supported(cover.DOMAIN, 0, None, None) state = State( "cover.bla", @@ -2034,7 +2048,7 @@ async def test_openclose_cover_no_position(hass): """Test OpenClose trait support for cover domain.""" assert helpers.get_google_type(cover.DOMAIN, None) is not None assert trait.OpenCloseTrait.supported( - cover.DOMAIN, cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE, None + cover.DOMAIN, cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE, None, None ) state = State( @@ -2091,7 +2105,7 @@ async def test_openclose_cover_secure(hass, device_class): """Test OpenClose trait support for cover domain.""" assert helpers.get_google_type(cover.DOMAIN, device_class) is not None assert trait.OpenCloseTrait.supported( - cover.DOMAIN, cover.SUPPORT_SET_POSITION, device_class + cover.DOMAIN, cover.SUPPORT_SET_POSITION, device_class, None ) assert trait.OpenCloseTrait.might_2fa( cover.DOMAIN, cover.SUPPORT_SET_POSITION, device_class @@ -2158,7 +2172,7 @@ async def test_openclose_cover_secure(hass, device_class): async def test_openclose_binary_sensor(hass, device_class): """Test OpenClose trait support for binary_sensor domain.""" assert helpers.get_google_type(binary_sensor.DOMAIN, device_class) is not None - assert trait.OpenCloseTrait.supported(binary_sensor.DOMAIN, 0, device_class) + assert trait.OpenCloseTrait.supported(binary_sensor.DOMAIN, 0, device_class, None) trt = trait.OpenCloseTrait( hass, @@ -2191,9 +2205,7 @@ async def test_volume_media_player(hass): """Test volume trait support for media player domain.""" assert helpers.get_google_type(media_player.DOMAIN, None) is not None assert trait.VolumeTrait.supported( - media_player.DOMAIN, - media_player.SUPPORT_VOLUME_SET, - None, + media_player.DOMAIN, media_player.SUPPORT_VOLUME_SET, None, None ) trt = trait.VolumeTrait( @@ -2244,9 +2256,7 @@ async def test_volume_media_player(hass): async def test_volume_media_player_relative(hass): """Test volume trait support for relative-volume-only media players.""" assert trait.VolumeTrait.supported( - media_player.DOMAIN, - media_player.SUPPORT_VOLUME_STEP, - None, + media_player.DOMAIN, media_player.SUPPORT_VOLUME_STEP, None, None ) trt = trait.VolumeTrait( hass, @@ -2314,6 +2324,7 @@ async def test_media_player_mute(hass): media_player.DOMAIN, media_player.SUPPORT_VOLUME_STEP | media_player.SUPPORT_VOLUME_MUTE, None, + None, ) trt = trait.VolumeTrait( hass, @@ -2376,10 +2387,10 @@ async def test_temperature_setting_sensor(hass): is not None ) assert not trait.TemperatureSettingTrait.supported( - sensor.DOMAIN, 0, sensor.DEVICE_CLASS_HUMIDITY + sensor.DOMAIN, 0, sensor.DEVICE_CLASS_HUMIDITY, None ) assert trait.TemperatureSettingTrait.supported( - sensor.DOMAIN, 0, sensor.DEVICE_CLASS_TEMPERATURE + sensor.DOMAIN, 0, sensor.DEVICE_CLASS_TEMPERATURE, None ) @@ -2422,10 +2433,10 @@ async def test_humidity_setting_sensor(hass): helpers.get_google_type(sensor.DOMAIN, sensor.DEVICE_CLASS_HUMIDITY) is not None ) assert not trait.HumiditySettingTrait.supported( - sensor.DOMAIN, 0, sensor.DEVICE_CLASS_TEMPERATURE + sensor.DOMAIN, 0, sensor.DEVICE_CLASS_TEMPERATURE, None ) assert trait.HumiditySettingTrait.supported( - sensor.DOMAIN, 0, sensor.DEVICE_CLASS_HUMIDITY + sensor.DOMAIN, 0, sensor.DEVICE_CLASS_HUMIDITY, None ) @@ -2456,7 +2467,9 @@ async def test_transport_control(hass): assert helpers.get_google_type(media_player.DOMAIN, None) is not None for feature in trait.MEDIA_COMMAND_SUPPORT_MAPPING.values(): - assert trait.TransportControlTrait.supported(media_player.DOMAIN, feature, None) + assert trait.TransportControlTrait.supported( + media_player.DOMAIN, feature, None, None + ) now = datetime(2020, 1, 1) @@ -2586,7 +2599,7 @@ async def test_media_state(hass, state): assert helpers.get_google_type(media_player.DOMAIN, None) is not None assert trait.TransportControlTrait.supported( - media_player.DOMAIN, media_player.SUPPORT_PLAY, None + media_player.DOMAIN, media_player.SUPPORT_PLAY, None, None ) trt = trait.MediaStateTrait( From e0ac12bd561f9cdaf28ab14fa944daa47a3a2343 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 14 Apr 2021 09:18:34 +0200 Subject: [PATCH 0260/1317] Use supported_color_modes in homekit (#49177) --- .../components/homekit/type_lights.py | 16 +++++---- tests/components/homekit/test_type_lights.py | 34 ++++++++++++------- 2 files changed, 30 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index 8be1580537dd4..614d9427ba66d 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -10,10 +10,11 @@ ATTR_HS_COLOR, ATTR_MAX_MIREDS, ATTR_MIN_MIREDS, + ATTR_SUPPORTED_COLOR_MODES, + COLOR_MODE_COLOR_TEMP, + COLOR_MODES_BRIGHTNESS, + COLOR_MODES_COLOR, DOMAIN, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -61,14 +62,15 @@ def __init__(self, *args): state = self.hass.states.get(self.entity_id) self._features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + self._color_modes = state.attributes.get(ATTR_SUPPORTED_COLOR_MODES, []) - if self._features & SUPPORT_BRIGHTNESS: + if any(mode in self._color_modes for mode in COLOR_MODES_BRIGHTNESS): self.chars.append(CHAR_BRIGHTNESS) - if self._features & SUPPORT_COLOR: + if any(mode in self._color_modes for mode in COLOR_MODES_COLOR): self.chars.append(CHAR_HUE) self.chars.append(CHAR_SATURATION) - elif self._features & SUPPORT_COLOR_TEMP: + elif COLOR_MODE_COLOR_TEMP in self._color_modes: # ColorTemperature and Hue characteristic should not be # exposed both. Both states are tracked separately in HomeKit, # causing "source of truth" problems. @@ -130,7 +132,7 @@ def _set_chars(self, char_values): events.append(f"color temperature at {char_values[CHAR_COLOR_TEMPERATURE]}") if ( - self._features & SUPPORT_COLOR + any(mode in self._color_modes for mode in COLOR_MODES_COLOR) and CHAR_HUE in char_values and CHAR_SATURATION in char_values ): diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index 0c81de2efe743..53d6ee02be6d6 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -1,6 +1,7 @@ """Test different accessory types: Lights.""" from pyhap.const import HAP_REPR_AID, HAP_REPR_CHARS, HAP_REPR_IID, HAP_REPR_VALUE +import pytest from homeassistant.components.homekit.const import ATTR_VALUE from homeassistant.components.homekit.type_lights import Light @@ -9,10 +10,8 @@ ATTR_BRIGHTNESS_PCT, ATTR_COLOR_TEMP, ATTR_HS_COLOR, + ATTR_SUPPORTED_COLOR_MODES, DOMAIN, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -98,14 +97,17 @@ async def test_light_basic(hass, hk_driver, events): assert events[-1].data[ATTR_VALUE] == "Set state to 0" -async def test_light_brightness(hass, hk_driver, events): +@pytest.mark.parametrize( + "supported_color_modes", [["brightness"], ["hs"], ["color_temp"]] +) +async def test_light_brightness(hass, hk_driver, events, supported_color_modes): """Test light with brightness.""" entity_id = "light.demo" hass.states.async_set( entity_id, STATE_ON, - {ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS, ATTR_BRIGHTNESS: 255}, + {ATTR_SUPPORTED_COLOR_MODES: supported_color_modes, ATTR_BRIGHTNESS: 255}, ) await hass.async_block_till_done() acc = Light(hass, hk_driver, "Light", entity_id, 1, None) @@ -223,7 +225,7 @@ async def test_light_color_temperature(hass, hk_driver, events): hass.states.async_set( entity_id, STATE_ON, - {ATTR_SUPPORTED_FEATURES: SUPPORT_COLOR_TEMP, ATTR_COLOR_TEMP: 190}, + {ATTR_SUPPORTED_COLOR_MODES: ["color_temp"], ATTR_COLOR_TEMP: 190}, ) await hass.async_block_till_done() acc = Light(hass, hk_driver, "Light", entity_id, 1, None) @@ -263,7 +265,12 @@ async def test_light_color_temperature(hass, hk_driver, events): assert events[-1].data[ATTR_VALUE] == "color temperature at 250" -async def test_light_color_temperature_and_rgb_color(hass, hk_driver, events): +@pytest.mark.parametrize( + "supported_color_modes", [["ct", "hs"], ["ct", "rgb"], ["ct", "xy"]] +) +async def test_light_color_temperature_and_rgb_color( + hass, hk_driver, events, supported_color_modes +): """Test light with color temperature and rgb color not exposing temperature.""" entity_id = "light.demo" @@ -271,7 +278,7 @@ async def test_light_color_temperature_and_rgb_color(hass, hk_driver, events): entity_id, STATE_ON, { - ATTR_SUPPORTED_FEATURES: SUPPORT_COLOR_TEMP | SUPPORT_COLOR, + ATTR_SUPPORTED_COLOR_MODES: supported_color_modes, ATTR_COLOR_TEMP: 190, ATTR_HS_COLOR: (260, 90), }, @@ -298,14 +305,15 @@ async def test_light_color_temperature_and_rgb_color(hass, hk_driver, events): assert acc.char_saturation.value == 61 -async def test_light_rgb_color(hass, hk_driver, events): +@pytest.mark.parametrize("supported_color_modes", [["hs"], ["rgb"], ["xy"]]) +async def test_light_rgb_color(hass, hk_driver, events, supported_color_modes): """Test light with rgb_color.""" entity_id = "light.demo" hass.states.async_set( entity_id, STATE_ON, - {ATTR_SUPPORTED_FEATURES: SUPPORT_COLOR, ATTR_HS_COLOR: (260, 90)}, + {ATTR_SUPPORTED_COLOR_MODES: supported_color_modes, ATTR_HS_COLOR: (260, 90)}, ) await hass.async_block_till_done() acc = Light(hass, hk_driver, "Light", entity_id, 1, None) @@ -362,7 +370,7 @@ async def test_light_restore(hass, hk_driver, events): "hue", "9012", suggested_object_id="all_info_set", - capabilities={"max": 100}, + capabilities={"supported_color_modes": ["brightness"], "max": 100}, supported_features=5, device_class="mock-device-class", ) @@ -391,7 +399,7 @@ async def test_light_set_brightness_and_color(hass, hk_driver, events): entity_id, STATE_ON, { - ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS | SUPPORT_COLOR, + ATTR_SUPPORTED_COLOR_MODES: ["hs"], ATTR_BRIGHTNESS: 255, }, ) @@ -467,7 +475,7 @@ async def test_light_set_brightness_and_color_temp(hass, hk_driver, events): entity_id, STATE_ON, { - ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP, + ATTR_SUPPORTED_COLOR_MODES: ["color_temp"], ATTR_BRIGHTNESS: 255, }, ) From 1230c46e1e5318a21f158fde5cb3cb0ca9805835 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 14 Apr 2021 09:18:49 +0200 Subject: [PATCH 0261/1317] Use supported_color_modes in alexa (#49174) --- homeassistant/components/alexa/entities.py | 8 ++--- tests/components/alexa/test_capabilities.py | 34 ++++++++++++++++----- tests/components/alexa/test_smart_home.py | 14 +++++++-- 3 files changed, 41 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index e7eaeb4a1cbe4..cbeb3a869ddf1 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -504,12 +504,12 @@ def interfaces(self): """Yield the supported interfaces.""" yield AlexaPowerController(self.entity) - supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - if supported & light.SUPPORT_BRIGHTNESS: + color_modes = self.entity.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES, []) + if any(mode in color_modes for mode in light.COLOR_MODES_BRIGHTNESS): yield AlexaBrightnessController(self.entity) - if supported & light.SUPPORT_COLOR: + if any(mode in color_modes for mode in light.COLOR_MODES_COLOR): yield AlexaColorController(self.entity) - if supported & light.SUPPORT_COLOR_TEMP: + if light.COLOR_MODE_COLOR_TEMP in color_modes: yield AlexaColorTemperatureController(self.entity) yield AlexaEndpointHealth(self.hass, self.entity) diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py index cd013ca70d9b0..020b03cc8622c 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -239,17 +239,27 @@ async def test_report_lock_state(hass): properties.assert_equal("Alexa.LockController", "lockState", "JAMMED") -async def test_report_dimmable_light_state(hass): +@pytest.mark.parametrize( + "supported_color_modes", [["brightness"], ["hs"], ["color_temp"]] +) +async def test_report_dimmable_light_state(hass, supported_color_modes): """Test BrightnessController reports brightness correctly.""" hass.states.async_set( "light.test_on", "on", - {"friendly_name": "Test light On", "brightness": 128, "supported_features": 1}, + { + "friendly_name": "Test light On", + "brightness": 128, + "supported_color_modes": supported_color_modes, + }, ) hass.states.async_set( "light.test_off", "off", - {"friendly_name": "Test light Off", "supported_features": 1}, + { + "friendly_name": "Test light Off", + "supported_color_modes": supported_color_modes, + }, ) properties = await reported_properties(hass, "light.test_on") @@ -259,7 +269,8 @@ async def test_report_dimmable_light_state(hass): properties.assert_equal("Alexa.BrightnessController", "brightness", 0) -async def test_report_colored_light_state(hass): +@pytest.mark.parametrize("supported_color_modes", [["hs"], ["rgb"], ["xy"]]) +async def test_report_colored_light_state(hass, supported_color_modes): """Test ColorController reports color correctly.""" hass.states.async_set( "light.test_on", @@ -268,13 +279,16 @@ async def test_report_colored_light_state(hass): "friendly_name": "Test light On", "hs_color": (180, 75), "brightness": 128, - "supported_features": 17, + "supported_color_modes": supported_color_modes, }, ) hass.states.async_set( "light.test_off", "off", - {"friendly_name": "Test light Off", "supported_features": 17}, + { + "friendly_name": "Test light Off", + "supported_color_modes": supported_color_modes, + }, ) properties = await reported_properties(hass, "light.test_on") @@ -295,12 +309,16 @@ async def test_report_colored_temp_light_state(hass): hass.states.async_set( "light.test_on", "on", - {"friendly_name": "Test light On", "color_temp": 240, "supported_features": 2}, + { + "friendly_name": "Test light On", + "color_temp": 240, + "supported_color_modes": ["color_temp"], + }, ) hass.states.async_set( "light.test_off", "off", - {"friendly_name": "Test light Off", "supported_features": 2}, + {"friendly_name": "Test light Off", "supported_color_modes": ["color_temp"]}, ) properties = await reported_properties(hass, "light.test_on") diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index c018e07c2648b..ab884745e958b 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -231,7 +231,11 @@ async def test_dimmable_light(hass): device = ( "light.test_2", "on", - {"brightness": 128, "friendly_name": "Test light 2", "supported_features": 1}, + { + "brightness": 128, + "friendly_name": "Test light 2", + "supported_color_modes": ["brightness"], + }, ) appliance = await discovery_test(device, hass) @@ -262,14 +266,18 @@ async def test_dimmable_light(hass): assert call.data["brightness_pct"] == 50 -async def test_color_light(hass): +@pytest.mark.parametrize( + "supported_color_modes", + [["color_temp", "hs"], ["color_temp", "rgb"], ["color_temp", "xy"]], +) +async def test_color_light(hass, supported_color_modes): """Test color light discovery.""" device = ( "light.test_3", "on", { "friendly_name": "Test light 3", - "supported_features": 19, + "supported_color_modes": supported_color_modes, "min_mireds": 142, "color_temp": "333", }, From fe1e57e76fd419a5eaed25ab6a779b2794984025 Mon Sep 17 00:00:00 2001 From: Carmen Sanchez <51202336+soundch3z@users.noreply.github.com> Date: Wed, 14 Apr 2021 11:00:32 +0200 Subject: [PATCH 0262/1317] Added Spanish US voice to Google Cloud TTS (#49200) See https://cloud.google.com/text-to-speech/docs/voices --- homeassistant/components/google_cloud/tts.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/google_cloud/tts.py b/homeassistant/components/google_cloud/tts.py index b0ae28bf5b12f..1d906ab4d2046 100644 --- a/homeassistant/components/google_cloud/tts.py +++ b/homeassistant/components/google_cloud/tts.py @@ -37,6 +37,7 @@ "en-IN", "en-US", "es-ES", + "es-US", "fi-FI", "fil-PH", "fr-CA", From 9d4ad1821e2a75d3b8848a55e6d4106c3da57737 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 14 Apr 2021 14:12:26 +0200 Subject: [PATCH 0263/1317] Fix logic of entity id extraction (#49164) Co-authored-by: Paulus Schoutsen --- homeassistant/helpers/service.py | 12 +++++++-- tests/helpers/test_service.py | 43 +++++++++++++++++++++++++++++++- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 4e484c6aaabc0..9dec919d4b5f1 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -362,8 +362,16 @@ async def async_extract_referenced_entity_ids( return selected for ent_entry in ent_reg.entities.values(): - if ent_entry.area_id in selector.area_ids or ( - not ent_entry.area_id and ent_entry.device_id in selected.referenced_devices + if ( + # when area matches the target area + ent_entry.area_id in selector.area_ids + # when device matches a referenced devices with no explicitly set area + or ( + not ent_entry.area_id + and ent_entry.device_id in selected.referenced_devices + ) + # when device matches target device + or ent_entry.device_id in selector.device_ids ): selected.indirectly_referenced.add(ent_entry.entity_id) diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 7538c0f6f2c9a..7e18547145bef 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -95,6 +95,7 @@ def area_mock(hass): device_in_area = dev_reg.DeviceEntry(area_id="test-area") device_no_area = dev_reg.DeviceEntry(id="device-no-area-id") device_diff_area = dev_reg.DeviceEntry(area_id="diff-area") + device_area_a = dev_reg.DeviceEntry(id="device-area-a-id", area_id="area-a") mock_device_registry( hass, @@ -102,6 +103,7 @@ def area_mock(hass): device_in_area.id: device_in_area, device_no_area.id: device_no_area, device_diff_area.id: device_diff_area, + device_area_a.id: device_area_a, }, ) @@ -119,7 +121,7 @@ def area_mock(hass): ) entity_in_other_area = ent_reg.RegistryEntry( entity_id="light.in_other_area", - unique_id="in-other-area-id", + unique_id="in-area-a-id", platform="test", device_id=device_in_area.id, area_id="other-area", @@ -143,6 +145,20 @@ def area_mock(hass): platform="test", device_id=device_diff_area.id, ) + entity_in_area_a = ent_reg.RegistryEntry( + entity_id="light.in_area_a", + unique_id="in-area-a-id", + platform="test", + device_id=device_area_a.id, + area_id="area-a", + ) + entity_in_area_b = ent_reg.RegistryEntry( + entity_id="light.in_area_b", + unique_id="in-area-b-id", + platform="test", + device_id=device_area_a.id, + area_id="area-b", + ) mock_registry( hass, { @@ -152,6 +168,8 @@ def area_mock(hass): entity_assigned_to_area.entity_id: entity_assigned_to_area, entity_no_area.entity_id: entity_no_area, entity_diff_area.entity_id: entity_diff_area, + entity_in_area_a.entity_id: entity_in_area_a, + entity_in_area_b.entity_id: entity_in_area_b, }, ) @@ -399,6 +417,29 @@ async def test_extract_entity_ids_from_area(hass, area_mock): ) +async def test_extract_entity_ids_from_devices(hass, area_mock): + """Test extract_entity_ids method with devices.""" + assert await service.async_extract_entity_ids( + hass, ha.ServiceCall("light", "turn_on", {"device_id": "device-no-area-id"}) + ) == { + "light.no_area", + } + + assert await service.async_extract_entity_ids( + hass, ha.ServiceCall("light", "turn_on", {"device_id": "device-area-a-id"}) + ) == { + "light.in_area_a", + "light.in_area_b", + } + + assert ( + await service.async_extract_entity_ids( + hass, ha.ServiceCall("light", "turn_on", {"device_id": "non-existing-id"}) + ) + == set() + ) + + async def test_async_get_all_descriptions(hass): """Test async_get_all_descriptions.""" group = hass.components.group From e4a7260384519633f796bd73444ab5e9b9925e56 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Wed, 14 Apr 2021 17:11:51 +0100 Subject: [PATCH 0264/1317] Bump pykmtronic to 0.3.0 (#49191) --- homeassistant/components/kmtronic/__init__.py | 5 +---- .../components/kmtronic/manifest.json | 2 +- homeassistant/components/kmtronic/switch.py | 17 +++++++++++------ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/kmtronic/test_switch.py | 18 ++++++++++++++++++ 6 files changed, 33 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/kmtronic/__init__.py b/homeassistant/components/kmtronic/__init__.py index 241e65fbe7fdd..d311940f4bca7 100644 --- a/homeassistant/components/kmtronic/__init__.py +++ b/homeassistant/components/kmtronic/__init__.py @@ -48,10 +48,7 @@ async def async_update_data(): await hub.async_update_relays() except aiohttp.client_exceptions.ClientResponseError as err: raise UpdateFailed(f"Wrong credentials: {err}") from err - except ( - asyncio.TimeoutError, - aiohttp.client_exceptions.ClientConnectorError, - ) as err: + except aiohttp.client_exceptions.ClientConnectorError as err: raise UpdateFailed(f"Error communicating with API: {err}") from err coordinator = DataUpdateCoordinator( diff --git a/homeassistant/components/kmtronic/manifest.json b/homeassistant/components/kmtronic/manifest.json index 27e9f953eb780..b7bccbe6f2d40 100644 --- a/homeassistant/components/kmtronic/manifest.json +++ b/homeassistant/components/kmtronic/manifest.json @@ -3,6 +3,6 @@ "name": "KMtronic", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/kmtronic", - "requirements": ["pykmtronic==0.0.3"], + "requirements": ["pykmtronic==0.3.0"], "codeowners": ["@dgomes"] } diff --git a/homeassistant/components/kmtronic/switch.py b/homeassistant/components/kmtronic/switch.py index d37cd54ce1a00..31b0fcb54c1b0 100644 --- a/homeassistant/components/kmtronic/switch.py +++ b/homeassistant/components/kmtronic/switch.py @@ -45,21 +45,26 @@ def unique_id(self) -> str: def is_on(self): """Return entity state.""" if self._reverse: - return not self._relay.is_on - return self._relay.is_on + return not self._relay.is_energised + return self._relay.is_energised async def async_turn_on(self, **kwargs) -> None: """Turn the switch on.""" if self._reverse: - await self._relay.turn_off() + await self._relay.de_energise() else: - await self._relay.turn_on() + await self._relay.energise() self.async_write_ha_state() async def async_turn_off(self, **kwargs) -> None: """Turn the switch off.""" if self._reverse: - await self._relay.turn_on() + await self._relay.energise() else: - await self._relay.turn_off() + await self._relay.de_energise() + self.async_write_ha_state() + + async def async_toggle(self, **kwargs) -> None: + """Toggle the switch.""" + await self._relay.toggle() self.async_write_ha_state() diff --git a/requirements_all.txt b/requirements_all.txt index 58497938086f6..ca2705f5bf6d8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1485,7 +1485,7 @@ pyitachip2ir==0.0.7 pykira==0.1.1 # homeassistant.components.kmtronic -pykmtronic==0.0.3 +pykmtronic==0.3.0 # homeassistant.components.kodi pykodi==0.2.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9d5da4349adbe..9a24bc0925d53 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -808,7 +808,7 @@ pyisy==2.1.1 pykira==0.1.1 # homeassistant.components.kmtronic -pykmtronic==0.0.3 +pykmtronic==0.3.0 # homeassistant.components.kodi pykodi==0.2.5 diff --git a/tests/components/kmtronic/test_switch.py b/tests/components/kmtronic/test_switch.py index df8ecda2c2ec9..70a298878bdd9 100644 --- a/tests/components/kmtronic/test_switch.py +++ b/tests/components/kmtronic/test_switch.py @@ -17,6 +17,10 @@ async def test_relay_on_off(hass, aioclient_mock): "http://1.1.1.1/status.xml", text="00", ) + aioclient_mock.get( + "http://1.1.1.1/relays.cgi?relay=1", + text="OK", + ) MockConfigEntry( domain=DOMAIN, data={"host": "1.1.1.1", "username": "foo", "password": "bar"} @@ -55,6 +59,20 @@ async def test_relay_on_off(hass, aioclient_mock): state = hass.states.get("switch.relay1") assert state.state == "off" + # Mocks the response for turning a relay1 on + aioclient_mock.get( + "http://1.1.1.1/FF0101", + text="", + ) + + await hass.services.async_call( + "switch", "toggle", {"entity_id": "switch.relay1"}, blocking=True + ) + + await hass.async_block_till_done() + state = hass.states.get("switch.relay1") + assert state.state == "on" + async def test_update(hass, aioclient_mock): """Tests switch refreshes status periodically.""" From 7ffd4fa83d7d7363e6d0118918ae46c08c4bafa7 Mon Sep 17 00:00:00 2001 From: Hmmbob <33529490+hmmbob@users.noreply.github.com> Date: Wed, 14 Apr 2021 18:14:24 +0200 Subject: [PATCH 0265/1317] Support all available Google Cloud TTS languages (#49208) --- homeassistant/components/google_cloud/tts.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/google_cloud/tts.py b/homeassistant/components/google_cloud/tts.py index 1d906ab4d2046..a1cbed2ee552a 100644 --- a/homeassistant/components/google_cloud/tts.py +++ b/homeassistant/components/google_cloud/tts.py @@ -23,9 +23,11 @@ CONF_TEXT_TYPE = "text_type" SUPPORTED_LANGUAGES = [ + "af-ZA", "ar-XA", + "bg-BG", "bn-IN", - "yue-HK", + "ca-ES", "cmn-CN", "cmn-TW", "cs-CZ", @@ -46,10 +48,12 @@ "hi-IN", "hu-HU", "id-ID", + "is-IS", "it-IT", "ja-JP", "kn-IN", "ko-KR", + "lv-LV", "ml-IN", "nb-NO", "nl-NL", @@ -59,6 +63,7 @@ "ro-RO", "ru-RU", "sk-SK", + "sr-RS", "sv-SE", "ta-IN", "te-IN", @@ -66,6 +71,7 @@ "tr-TR", "uk-UA", "vi-VN", + "yue-HK", ] DEFAULT_LANG = "en-US" From 81d46828ad3493dd355658e0bb992be01be6a0f5 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Wed, 14 Apr 2021 09:44:39 -0700 Subject: [PATCH 0266/1317] Bump androidtv (0.0.58) and adb-shell (0.3.1) (#49209) --- homeassistant/components/androidtv/manifest.json | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index ffcaedeb5a007..4612c220c7db4 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -3,8 +3,8 @@ "name": "Android TV", "documentation": "https://www.home-assistant.io/integrations/androidtv", "requirements": [ - "adb-shell[async]==0.2.1", - "androidtv[async]==0.0.57", + "adb-shell[async]==0.3.1", + "androidtv[async]==0.0.58", "pure-python-adb[async]==0.3.0.dev0" ], "codeowners": ["@JeffLIrion"] diff --git a/requirements_all.txt b/requirements_all.txt index ca2705f5bf6d8..02c3393d5624c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -105,7 +105,7 @@ adafruit-circuitpython-bmp280==3.1.1 adafruit-circuitpython-mcp230xx==2.2.2 # homeassistant.components.androidtv -adb-shell[async]==0.2.1 +adb-shell[async]==0.3.1 # homeassistant.components.alarmdecoder adext==0.4.1 @@ -254,7 +254,7 @@ ambiclimate==0.2.1 amcrest==1.7.2 # homeassistant.components.androidtv -androidtv[async]==0.0.57 +androidtv[async]==0.0.58 # homeassistant.components.anel_pwrctrl anel_pwrctrl-homeassistant==0.0.1.dev2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9a24bc0925d53..6c3c1673f90bd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -48,7 +48,7 @@ abodepy==1.2.0 accuweather==0.1.1 # homeassistant.components.androidtv -adb-shell[async]==0.2.1 +adb-shell[async]==0.3.1 # homeassistant.components.alarmdecoder adext==0.4.1 @@ -167,7 +167,7 @@ airly==1.1.0 ambiclimate==0.2.1 # homeassistant.components.androidtv -androidtv[async]==0.0.57 +androidtv[async]==0.0.58 # homeassistant.components.apns apns2==0.3.0 From 8ce74e598d844d0b51aeb29983a2d7f114447c50 Mon Sep 17 00:00:00 2001 From: Khole Date: Wed, 14 Apr 2021 18:26:37 +0100 Subject: [PATCH 0267/1317] Allow debugging of integration dependancies (#49211) --- .vscode/launch.json | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 3d967b25c15cd..e8bf893e0c9a3 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,11 +9,8 @@ "type": "python", "request": "launch", "module": "homeassistant", - "args": [ - "--debug", - "-c", - "config" - ] + "justMyCode": false, + "args": ["--debug", "-c", "config"] }, { // Debug by attaching to local Home Asistant server using Remote Python Debugger. @@ -28,7 +25,7 @@ "localRoot": "${workspaceFolder}", "remoteRoot": "." } - ], + ] }, { // Debug by attaching to remote Home Asistant server using Remote Python Debugger. @@ -43,7 +40,7 @@ "localRoot": "${workspaceFolder}", "remoteRoot": "/usr/src/homeassistant" } - ], + ] } ] -} \ No newline at end of file +} From aaa600e00a9940b69f120852d4a7eaf5897aa479 Mon Sep 17 00:00:00 2001 From: Unai Date: Wed, 14 Apr 2021 22:19:24 +0200 Subject: [PATCH 0268/1317] Add unique-ids to maxcube component (#49196) --- homeassistant/components/maxcube/binary_sensor.py | 5 +++++ homeassistant/components/maxcube/climate.py | 5 +++++ tests/components/maxcube/test_maxcube_binary_sensor.py | 6 ++++++ tests/components/maxcube/test_maxcube_climate.py | 10 ++++++++++ 4 files changed, 26 insertions(+) diff --git a/homeassistant/components/maxcube/binary_sensor.py b/homeassistant/components/maxcube/binary_sensor.py index 223c0e3fc9916..999d7af01c593 100644 --- a/homeassistant/components/maxcube/binary_sensor.py +++ b/homeassistant/components/maxcube/binary_sensor.py @@ -35,6 +35,11 @@ def name(self): """Return the name of the BinarySensorEntity.""" return self._name + @property + def unique_id(self): + """Return a unique ID.""" + return self._device.serial + @property def device_class(self): """Return the class of this sensor.""" diff --git a/homeassistant/components/maxcube/climate.py b/homeassistant/components/maxcube/climate.py index 75ee7ef21f096..175f44b9d0ebd 100644 --- a/homeassistant/components/maxcube/climate.py +++ b/homeassistant/components/maxcube/climate.py @@ -87,6 +87,11 @@ def name(self): """Return the name of the climate device.""" return self._name + @property + def unique_id(self): + """Return a unique ID.""" + return self._device.serial + @property def min_temp(self): """Return the minimum temperature.""" diff --git a/tests/components/maxcube/test_maxcube_binary_sensor.py b/tests/components/maxcube/test_maxcube_binary_sensor.py index db5228c5c9a4f..48d34a0df4ebb 100644 --- a/tests/components/maxcube/test_maxcube_binary_sensor.py +++ b/tests/components/maxcube/test_maxcube_binary_sensor.py @@ -11,6 +11,7 @@ STATE_OFF, STATE_ON, ) +from homeassistant.helpers import entity_registry as er from homeassistant.util import utcnow from tests.common import async_fire_time_changed @@ -20,6 +21,11 @@ async def test_window_shuttler(hass, cube: MaxCube, windowshutter: MaxWindowShutter): """Test a successful setup with a shuttler device.""" + entity_registry = er.async_get(hass) + assert entity_registry.async_is_registered(ENTITY_ID) + entity = entity_registry.async_get(ENTITY_ID) + assert entity.unique_id == "AABBCCDD03" + state = hass.states.get(ENTITY_ID) assert state is not None assert state.state == STATE_ON diff --git a/tests/components/maxcube/test_maxcube_climate.py b/tests/components/maxcube/test_maxcube_climate.py index b59e1372fde38..b234bbd130cb1 100644 --- a/tests/components/maxcube/test_maxcube_climate.py +++ b/tests/components/maxcube/test_maxcube_climate.py @@ -54,6 +54,7 @@ ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, ) +from homeassistant.helpers import entity_registry as er from homeassistant.util import utcnow from tests.common import async_fire_time_changed @@ -65,6 +66,10 @@ async def test_setup_thermostat(hass, cube: MaxCube): """Test a successful setup of a thermostat device.""" + entity_registry = er.async_get(hass) + assert entity_registry.async_is_registered(ENTITY_ID) + entity = entity_registry.async_get(ENTITY_ID) + assert entity.unique_id == "AABBCCDD01" state = hass.states.get(ENTITY_ID) assert state.state == HVAC_MODE_AUTO @@ -94,6 +99,11 @@ async def test_setup_thermostat(hass, cube: MaxCube): async def test_setup_wallthermostat(hass, cube: MaxCube): """Test a successful setup of a wall thermostat device.""" + entity_registry = er.async_get(hass) + assert entity_registry.async_is_registered(WALL_ENTITY_ID) + entity = entity_registry.async_get(WALL_ENTITY_ID) + assert entity.unique_id == "AABBCCDD02" + state = hass.states.get(WALL_ENTITY_ID) assert state.state == HVAC_MODE_OFF assert state.attributes.get(ATTR_FRIENDLY_NAME) == "TestRoom TestWallThermostat" From 403c6b9e268ac0c5b67d92a925f31fba41407a3c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 14 Apr 2021 10:23:15 -1000 Subject: [PATCH 0269/1317] Stop ssdp scans when stop event happens (#49140) --- homeassistant/components/ssdp/__init__.py | 25 ++-- tests/components/ssdp/test_init.py | 161 +++++++++++++++++----- 2 files changed, 141 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index 8cad4a74bf89b..b6e2897ade2b5 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -9,7 +9,7 @@ from defusedxml import ElementTree from netdisco import ssdp, util -from homeassistant.const import EVENT_HOMEASSISTANT_STARTED +from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback from homeassistant.helpers.event import async_track_time_interval from homeassistant.loader import async_get_ssdp @@ -43,12 +43,18 @@ async def async_setup(hass, config): """Set up the SSDP integration.""" - async def initialize(_): + async def _async_initialize(_): scanner = Scanner(hass, await async_get_ssdp(hass)) await scanner.async_scan(None) - async_track_time_interval(hass, scanner.async_scan, SCAN_INTERVAL) + cancel_scan = async_track_time_interval(hass, scanner.async_scan, SCAN_INTERVAL) - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, initialize) + @callback + def _async_stop_scans(event): + cancel_scan() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop_scans) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, _async_initialize) return True @@ -179,14 +185,13 @@ async def _fetch_description(self, xml_location): """Fetch an XML description.""" session = self.hass.helpers.aiohttp_client.async_get_clientsession() try: - resp = await session.get(xml_location, timeout=5) - xml = await resp.text(errors="replace") - - # Samsung Smart TV sometimes returns an empty document the - # first time. Retry once. - if not xml: + for _ in range(2): resp = await session.get(xml_location, timeout=5) xml = await resp.text(errors="replace") + # Samsung Smart TV sometimes returns an empty document the + # first time. Retry once. + if xml: + break except (aiohttp.ClientError, asyncio.TimeoutError) as err: _LOGGER.debug("Error fetching %s: %s", xml_location, err) return {} diff --git a/tests/components/ssdp/test_init.py b/tests/components/ssdp/test_init.py index 8ca82e93bfc71..f0f4a94e562bb 100644 --- a/tests/components/ssdp/test_init.py +++ b/tests/components/ssdp/test_init.py @@ -1,31 +1,37 @@ """Test the SSDP integration.""" import asyncio -from unittest.mock import Mock, patch +from datetime import timedelta +from unittest.mock import patch import aiohttp import pytest from homeassistant.components import ssdp +from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util -from tests.common import mock_coro +from tests.common import async_fire_time_changed, mock_coro async def test_scan_match_st(hass, caplog): """Test matching based on ST.""" scanner = ssdp.Scanner(hass, {"mock-domain": [{"st": "mock-st"}]}) - async def _inject_entry(*args, **kwargs): - scanner.async_store_entry( - Mock( - st="mock-st", - location=None, - values={"usn": "mock-usn", "server": "mock-server", "ext": ""}, - ) + async def _mock_async_scan(*args, async_callback=None, **kwargs): + await async_callback( + { + "st": "mock-st", + "location": None, + "usn": "mock-usn", + "server": "mock-server", + "ext": "", + } ) with patch( "homeassistant.components.ssdp.async_search", - side_effect=_inject_entry, + side_effect=_mock_async_scan, ), patch.object( hass.config_entries.flow, "async_init", return_value=mock_coro() ) as mock_init: @@ -61,19 +67,25 @@ async def test_scan_match_upnp_devicedesc(hass, aioclient_mock, key): ) scanner = ssdp.Scanner(hass, {"mock-domain": [{key: "Paulus"}]}) - async def _inject_entry(*args, **kwargs): - scanner.async_store_entry( - Mock(st="mock-st", location="http://1.1.1.1", values={}) - ) + async def _mock_async_scan(*args, async_callback=None, **kwargs): + for _ in range(5): + await async_callback( + { + "st": "mock-st", + "location": "http://1.1.1.1", + } + ) with patch( "homeassistant.components.ssdp.async_search", - side_effect=_inject_entry, + side_effect=_mock_async_scan, ), patch.object( hass.config_entries.flow, "async_init", return_value=mock_coro() ) as mock_init: await scanner.async_scan(None) + # If we get duplicate respones, ensure we only look it up once + assert len(aioclient_mock.mock_calls) == 1 assert len(mock_init.mock_calls) == 1 assert mock_init.mock_calls[0][1][0] == "mock-domain" assert mock_init.mock_calls[0][2]["context"] == {"source": "ssdp"} @@ -103,14 +115,17 @@ async def test_scan_not_all_present(hass, aioclient_mock): }, ) - async def _inject_entry(*args, **kwargs): - scanner.async_store_entry( - Mock(st="mock-st", location="http://1.1.1.1", values={}) + async def _mock_async_scan(*args, async_callback=None, **kwargs): + await async_callback( + { + "st": "mock-st", + "location": "http://1.1.1.1", + } ) with patch( "homeassistant.components.ssdp.async_search", - side_effect=_inject_entry, + side_effect=_mock_async_scan, ), patch.object( hass.config_entries.flow, "async_init", return_value=mock_coro() ) as mock_init: @@ -144,14 +159,17 @@ async def test_scan_not_all_match(hass, aioclient_mock): }, ) - async def _inject_entry(*args, **kwargs): - scanner.async_store_entry( - Mock(st="mock-st", location="http://1.1.1.1", values={}) + async def _mock_async_scan(*args, async_callback=None, **kwargs): + await async_callback( + { + "st": "mock-st", + "location": "http://1.1.1.1", + } ) with patch( "homeassistant.components.ssdp.async_search", - side_effect=_inject_entry, + side_effect=_mock_async_scan, ), patch.object( hass.config_entries.flow, "async_init", return_value=mock_coro() ) as mock_init: @@ -166,14 +184,17 @@ async def test_scan_description_fetch_fail(hass, aioclient_mock, exc): aioclient_mock.get("http://1.1.1.1", exc=exc) scanner = ssdp.Scanner(hass, {}) - async def _inject_entry(*args, **kwargs): - scanner.async_store_entry( - Mock(st="mock-st", location="http://1.1.1.1", values={}) + async def _mock_async_scan(*args, async_callback=None, **kwargs): + await async_callback( + { + "st": "mock-st", + "location": "http://1.1.1.1", + } ) with patch( "homeassistant.components.ssdp.async_search", - side_effect=_inject_entry, + side_effect=_mock_async_scan, ): await scanner.async_scan(None) @@ -188,14 +209,17 @@ async def test_scan_description_parse_fail(hass, aioclient_mock): ) scanner = ssdp.Scanner(hass, {}) - async def _inject_entry(*args, **kwargs): - scanner.async_store_entry( - Mock(st="mock-st", location="http://1.1.1.1", values={}) + async def _mock_async_scan(*args, async_callback=None, **kwargs): + await async_callback( + { + "st": "mock-st", + "location": "http://1.1.1.1", + } ) with patch( "homeassistant.components.ssdp.async_search", - side_effect=_inject_entry, + side_effect=_mock_async_scan, ): await scanner.async_scan(None) @@ -224,14 +248,17 @@ async def test_invalid_characters(hass, aioclient_mock): }, ) - async def _inject_entry(*args, **kwargs): - scanner.async_store_entry( - Mock(st="mock-st", location="http://1.1.1.1", values={}) + async def _mock_async_scan(*args, async_callback=None, **kwargs): + await async_callback( + { + "st": "mock-st", + "location": "http://1.1.1.1", + } ) with patch( "homeassistant.components.ssdp.async_search", - side_effect=_inject_entry, + side_effect=_mock_async_scan, ), patch.object( hass.config_entries.flow, "async_init", return_value=mock_coro() ) as mock_init: @@ -246,3 +273,67 @@ async def _inject_entry(*args, **kwargs): "deviceType": "ABC", "serialNumber": "ÿÿÿÿ", } + + +@patch("homeassistant.components.ssdp.async_search") +async def test_start_stop_scanner(async_search_mock, hass): + """Test we start and stop the scanner.""" + assert await async_setup_component(hass, ssdp.DOMAIN, {ssdp.DOMAIN: {}}) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200)) + await hass.async_block_till_done() + assert async_search_mock.call_count == 2 + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200)) + await hass.async_block_till_done() + assert async_search_mock.call_count == 2 + + +async def test_unexpected_exception_while_fetching(hass, aioclient_mock, caplog): + """Test unexpected exception while fetching.""" + aioclient_mock.get( + "http://1.1.1.1", + text=""" + + + ABC + \xff\xff\xff\xff + + + """, + ) + scanner = ssdp.Scanner( + hass, + { + "mock-domain": [ + { + ssdp.ATTR_UPNP_DEVICE_TYPE: "ABC", + } + ] + }, + ) + + async def _mock_async_scan(*args, async_callback=None, **kwargs): + await async_callback( + { + "st": "mock-st", + "location": "http://1.1.1.1", + } + ) + + with patch( + "homeassistant.components.ssdp.ElementTree.fromstring", side_effect=ValueError + ), patch( + "homeassistant.components.ssdp.async_search", + side_effect=_mock_async_scan, + ), patch.object( + hass.config_entries.flow, "async_init", return_value=mock_coro() + ) as mock_init: + await scanner.async_scan(None) + + assert len(mock_init.mock_calls) == 0 + assert "Failed to fetch ssdp data from: http://1.1.1.1" in caplog.text From ed54494b699818ec0953d0e8a833be2188837c12 Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Wed, 14 Apr 2021 23:10:35 +0200 Subject: [PATCH 0270/1317] Add binary sensor platform to Rituals Perfume Genie Integration (#49207) * Add binary sensor platform to Rituals * Sort platforms --- .coveragerc | 1 + .../rituals_perfume_genie/__init__.py | 2 +- .../rituals_perfume_genie/binary_sensor.py | 44 +++++++++++++++++++ .../components/rituals_perfume_genie/const.py | 3 ++ .../rituals_perfume_genie/entity.py | 3 +- .../rituals_perfume_genie/sensor.py | 16 +------ 6 files changed, 52 insertions(+), 17 deletions(-) create mode 100644 homeassistant/components/rituals_perfume_genie/binary_sensor.py diff --git a/.coveragerc b/.coveragerc index 2f93792a3f6b7..d3eee9c9f6031 100644 --- a/.coveragerc +++ b/.coveragerc @@ -826,6 +826,7 @@ omit = homeassistant/components/rest/switch.py homeassistant/components/ring/camera.py homeassistant/components/ripple/sensor.py + homeassistant/components/rituals_perfume_genie/binary_sensor.py homeassistant/components/rituals_perfume_genie/entity.py homeassistant/components/rituals_perfume_genie/sensor.py homeassistant/components/rituals_perfume_genie/switch.py diff --git a/homeassistant/components/rituals_perfume_genie/__init__.py b/homeassistant/components/rituals_perfume_genie/__init__.py index 610700e8fe5af..93e5619f446df 100644 --- a/homeassistant/components/rituals_perfume_genie/__init__.py +++ b/homeassistant/components/rituals_perfume_genie/__init__.py @@ -14,7 +14,7 @@ from .const import ACCOUNT_HASH, COORDINATORS, DEVICES, DOMAIN, HUB, HUBLOT -PLATFORMS = ["switch", "sensor"] +PLATFORMS = ["binary_sensor", "sensor", "switch"] EMPTY_CREDENTIALS = "" diff --git a/homeassistant/components/rituals_perfume_genie/binary_sensor.py b/homeassistant/components/rituals_perfume_genie/binary_sensor.py new file mode 100644 index 0000000000000..39c8cb8415a28 --- /dev/null +++ b/homeassistant/components/rituals_perfume_genie/binary_sensor.py @@ -0,0 +1,44 @@ +"""Support for Rituals Perfume Genie binary sensors.""" +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_BATTERY_CHARGING, + BinarySensorEntity, +) + +from .const import BATTERY, COORDINATORS, DEVICES, DOMAIN, HUB, ID +from .entity import SENSORS, DiffuserEntity + +CHARGING_SUFFIX = " Battery Charging" +BATTERY_CHARGING_ID = 21 + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the diffuser binary sensors.""" + diffusers = hass.data[DOMAIN][config_entry.entry_id][DEVICES] + coordinators = hass.data[DOMAIN][config_entry.entry_id][COORDINATORS] + entities = [] + for hublot, diffuser in diffusers.items(): + if BATTERY in diffuser.data[HUB][SENSORS]: + coordinator = coordinators[hublot] + entities.append(DiffuserBatteryChargingBinarySensor(diffuser, coordinator)) + + async_add_entities(entities) + + +class DiffuserBatteryChargingBinarySensor(DiffuserEntity, BinarySensorEntity): + """Representation of a diffuser battery charging binary sensor.""" + + def __init__(self, diffuser, coordinator): + """Initialize the battery charging binary sensor.""" + super().__init__(diffuser, coordinator, CHARGING_SUFFIX) + + @property + def is_on(self): + """Return the state of the battery charging binary sensor.""" + return bool( + self.coordinator.data[HUB][SENSORS][BATTERY][ID] == BATTERY_CHARGING_ID + ) + + @property + def device_class(self): + """Return the device class of the battery charging binary sensor.""" + return DEVICE_CLASS_BATTERY_CHARGING diff --git a/homeassistant/components/rituals_perfume_genie/const.py b/homeassistant/components/rituals_perfume_genie/const.py index 16189c8335eda..fef16b7f6f69d 100644 --- a/homeassistant/components/rituals_perfume_genie/const.py +++ b/homeassistant/components/rituals_perfume_genie/const.py @@ -6,5 +6,8 @@ ACCOUNT_HASH = "account_hash" ATTRIBUTES = "attributes" +BATTERY = "battc" HUB = "hub" HUBLOT = "hublot" +ID = "id" +SENSORS = "sensors" diff --git a/homeassistant/components/rituals_perfume_genie/entity.py b/homeassistant/components/rituals_perfume_genie/entity.py index ba8f583d04217..4f89856ad0888 100644 --- a/homeassistant/components/rituals_perfume_genie/entity.py +++ b/homeassistant/components/rituals_perfume_genie/entity.py @@ -1,12 +1,11 @@ """Base class for Rituals Perfume Genie diffuser entity.""" from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import ATTRIBUTES, DOMAIN, HUB, HUBLOT +from .const import ATTRIBUTES, DOMAIN, HUB, HUBLOT, SENSORS MANUFACTURER = "Rituals Cosmetics" MODEL = "Diffuser" -SENSORS = "sensors" ROOMNAME = "roomnamec" VERSION = "versionc" diff --git a/homeassistant/components/rituals_perfume_genie/sensor.py b/homeassistant/components/rituals_perfume_genie/sensor.py index 4a3ac34cc5872..87c2da21bc769 100644 --- a/homeassistant/components/rituals_perfume_genie/sensor.py +++ b/homeassistant/components/rituals_perfume_genie/sensor.py @@ -1,24 +1,20 @@ """Support for Rituals Perfume Genie sensors.""" from homeassistant.const import ( - ATTR_BATTERY_CHARGING, ATTR_BATTERY_LEVEL, DEVICE_CLASS_BATTERY, DEVICE_CLASS_SIGNAL_STRENGTH, PERCENTAGE, ) -from .const import COORDINATORS, DEVICES, DOMAIN, HUB -from .entity import SENSORS, DiffuserEntity +from .const import BATTERY, COORDINATORS, DEVICES, DOMAIN, HUB, ID, SENSORS +from .entity import DiffuserEntity -ID = "id" TITLE = "title" ICON = "icon" WIFI = "wific" -BATTERY = "battc" PERFUME = "rfidc" FILL = "fillc" -BATTERY_CHARGING_ID = 21 PERFUME_NO_CARTRIDGE_ID = 19 FILL_NO_CARTRIDGE_ID = 12 @@ -106,13 +102,6 @@ def state(self): "battery-low.png": 10, }[self.coordinator.data[HUB][SENSORS][BATTERY][ICON]] - @property - def _charging(self): - """Return battery charging state.""" - return bool( - self.coordinator.data[HUB][SENSORS][BATTERY][ID] == BATTERY_CHARGING_ID - ) - @property def device_class(self): """Return the class of the battery sensor.""" @@ -123,7 +112,6 @@ def extra_state_attributes(self): """Return the battery state attributes.""" return { ATTR_BATTERY_LEVEL: self.coordinator.data[HUB][SENSORS][BATTERY][TITLE], - ATTR_BATTERY_CHARGING: self._charging, } @property From 555f508b8cb5716689518250fcfc66e8e60cd815 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Wed, 14 Apr 2021 23:39:44 +0200 Subject: [PATCH 0271/1317] Reinitialize upnp device on config change (#49081) * Store coordinator at Device * Use DeviceUpdater to follow config/location changes * Cleaning up * Fix unit tests + review changes * Don't test internals --- homeassistant/components/upnp/__init__.py | 10 +++-- homeassistant/components/upnp/config_flow.py | 4 +- homeassistant/components/upnp/const.py | 1 - homeassistant/components/upnp/device.py | 45 +++++++++++++++----- homeassistant/components/upnp/sensor.py | 11 ++--- tests/components/upnp/mock_device.py | 10 +++-- tests/components/upnp/test_config_flow.py | 29 ++++++++++--- 7 files changed, 75 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index d5be0757cf3c0..439c3a8760b5e 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -21,7 +21,6 @@ DISCOVERY_UDN, DOMAIN, DOMAIN_CONFIG, - DOMAIN_COORDINATORS, DOMAIN_DEVICES, DOMAIN_LOCAL_IP, LOGGER as _LOGGER, @@ -75,7 +74,6 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): local_ip = await hass.async_add_executor_job(get_local_ip) hass.data[DOMAIN] = { DOMAIN_CONFIG: conf, - DOMAIN_COORDINATORS: {}, DOMAIN_DEVICES: {}, DOMAIN_LOCAL_IP: conf.get(CONF_LOCAL_IP, local_ip), } @@ -149,6 +147,9 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) hass.config_entries.async_forward_entry_setup(config_entry, "sensor") ) + # Start device updater. + await device.async_start() + return True @@ -160,9 +161,10 @@ async def async_unload_entry( udn = config_entry.data.get(CONFIG_ENTRY_UDN) if udn in hass.data[DOMAIN][DOMAIN_DEVICES]: + device = hass.data[DOMAIN][DOMAIN_DEVICES][udn] + await device.async_stop() + del hass.data[DOMAIN][DOMAIN_DEVICES][udn] - if udn in hass.data[DOMAIN][DOMAIN_COORDINATORS]: - del hass.data[DOMAIN][DOMAIN_COORDINATORS][udn] _LOGGER.debug("Deleting sensors") return await hass.config_entries.async_forward_entry_unload(config_entry, "sensor") diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py index 1cbaf9318572b..7a1a3d4a06c28 100644 --- a/homeassistant/components/upnp/config_flow.py +++ b/homeassistant/components/upnp/config_flow.py @@ -25,7 +25,7 @@ DISCOVERY_UNIQUE_ID, DISCOVERY_USN, DOMAIN, - DOMAIN_COORDINATORS, + DOMAIN_DEVICES, LOGGER as _LOGGER, ) from .device import Device @@ -252,7 +252,7 @@ async def async_step_init(self, user_input: Mapping = None) -> None: """Manage the options.""" if user_input is not None: udn = self.config_entry.data[CONFIG_ENTRY_UDN] - coordinator = self.hass.data[DOMAIN][DOMAIN_COORDINATORS][udn] + coordinator = self.hass.data[DOMAIN][DOMAIN_DEVICES][udn].coordinator update_interval_sec = user_input.get( CONFIG_ENTRY_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL ) diff --git a/homeassistant/components/upnp/const.py b/homeassistant/components/upnp/const.py index 6575139c4a4e0..142524ef9ca52 100644 --- a/homeassistant/components/upnp/const.py +++ b/homeassistant/components/upnp/const.py @@ -9,7 +9,6 @@ CONF_LOCAL_IP = "local_ip" DOMAIN = "upnp" DOMAIN_CONFIG = "config" -DOMAIN_COORDINATORS = "coordinators" DOMAIN_DEVICES = "devices" DOMAIN_LOCAL_IP = "local_ip" BYTES_RECEIVED = "bytes_received" diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index 034496ec02817..aafd9f5151679 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -8,10 +8,12 @@ from async_upnp_client import UpnpFactory from async_upnp_client.aiohttp import AiohttpSessionRequester +from async_upnp_client.device_updater import DeviceUpdater from async_upnp_client.profiles.igd import IgdDevice from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator import homeassistant.util.dt as dt_util from .const import ( @@ -34,23 +36,29 @@ ) +def _get_local_ip(hass: HomeAssistantType) -> IPv4Address | None: + """Get the configured local ip.""" + if DOMAIN in hass.data and DOMAIN_CONFIG in hass.data[DOMAIN]: + local_ip = hass.data[DOMAIN][DOMAIN_CONFIG].get(CONF_LOCAL_IP) + if local_ip: + return IPv4Address(local_ip) + return None + + class Device: """Home Assistant representation of a UPnP/IGD device.""" - def __init__(self, igd_device): + def __init__(self, igd_device: IgdDevice, device_updater: DeviceUpdater) -> None: """Initialize UPnP/IGD device.""" - self._igd_device: IgdDevice = igd_device + self._igd_device = igd_device + self._device_updater = device_updater + self.coordinator: DataUpdateCoordinator = None @classmethod async def async_discover(cls, hass: HomeAssistantType) -> list[Mapping]: """Discover UPnP/IGD devices.""" _LOGGER.debug("Discovering UPnP/IGD devices") - local_ip = None - if DOMAIN in hass.data and DOMAIN_CONFIG in hass.data[DOMAIN]: - local_ip = hass.data[DOMAIN][DOMAIN_CONFIG].get(CONF_LOCAL_IP) - if local_ip: - local_ip = IPv4Address(local_ip) - + local_ip = _get_local_ip(hass) discoveries = await IgdDevice.async_search(source_ip=local_ip, timeout=10) # Supplement/standardize discovery. @@ -81,17 +89,32 @@ async def async_create_device( cls, hass: HomeAssistantType, ssdp_location: str ) -> Device: """Create UPnP/IGD device.""" - # build async_upnp_client requester + # Build async_upnp_client requester. session = async_get_clientsession(hass) requester = AiohttpSessionRequester(session, True, 10) - # create async_upnp_client device + # Create async_upnp_client device. factory = UpnpFactory(requester, disable_state_variable_validation=True) upnp_device = await factory.async_create_device(ssdp_location) + # Create profile wrapper. igd_device = IgdDevice(upnp_device, None) - return cls(igd_device) + # Create updater. + local_ip = _get_local_ip(hass) + device_updater = DeviceUpdater( + device=upnp_device, factory=factory, source_ip=local_ip + ) + + return cls(igd_device, device_updater) + + async def async_start(self) -> None: + """Start the device updater.""" + await self._device_updater.async_start() + + async def async_stop(self) -> None: + """Stop the device updater.""" + await self._device_updater.async_stop() @property def udn(self) -> str: diff --git a/homeassistant/components/upnp/sensor.py b/homeassistant/components/upnp/sensor.py index 0e95b6106a399..d144bd29299a2 100644 --- a/homeassistant/components/upnp/sensor.py +++ b/homeassistant/components/upnp/sensor.py @@ -2,7 +2,7 @@ from __future__ import annotations from datetime import timedelta -from typing import Any, Mapping +from typing import Any, Callable, Mapping from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry @@ -23,7 +23,6 @@ DATA_RATE_PACKETS_PER_SECOND, DEFAULT_SCAN_INTERVAL, DOMAIN, - DOMAIN_COORDINATORS, DOMAIN_DEVICES, KIBIBYTE, LOGGER as _LOGGER, @@ -83,7 +82,7 @@ async def async_setup_platform( async def async_setup_entry( - hass, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable ) -> None: """Set up the UPnP/IGD sensors.""" udn = config_entry.data[CONFIG_ENTRY_UDN] @@ -102,8 +101,9 @@ async def async_setup_entry( update_method=device.async_get_traffic_data, update_interval=update_interval, ) + device.coordinator = coordinator + await coordinator.async_refresh() - hass.data[DOMAIN][DOMAIN_COORDINATORS][udn] = coordinator sensors = [ RawUpnpSensor(coordinator, device, SENSOR_TYPES[BYTES_RECEIVED]), @@ -126,14 +126,11 @@ def __init__( coordinator: DataUpdateCoordinator[Mapping[str, Any]], device: Device, sensor_type: Mapping[str, str], - update_multiplier: int = 2, ) -> None: """Initialize the base sensor.""" super().__init__(coordinator) self._device = device self._sensor_type = sensor_type - self._update_counter_max = update_multiplier - self._update_counter = 0 @property def icon(self) -> str: diff --git a/tests/components/upnp/mock_device.py b/tests/components/upnp/mock_device.py index d602760813735..d2ef9ad41e3bc 100644 --- a/tests/components/upnp/mock_device.py +++ b/tests/components/upnp/mock_device.py @@ -1,6 +1,7 @@ """Mock device for testing purposes.""" from typing import Mapping +from unittest.mock import AsyncMock from homeassistant.components.upnp.const import ( BYTES_RECEIVED, @@ -10,7 +11,7 @@ TIMESTAMP, ) from homeassistant.components.upnp.device import Device -import homeassistant.util.dt as dt_util +from homeassistant.util import dt class MockDevice(Device): @@ -19,8 +20,10 @@ class MockDevice(Device): def __init__(self, udn: str) -> None: """Initialize mock device.""" igd_device = object() - super().__init__(igd_device) + mock_device_updater = AsyncMock() + super().__init__(igd_device, mock_device_updater) self._udn = udn + self.times_polled = 0 @classmethod async def async_create_device(cls, hass, ssdp_location) -> "MockDevice": @@ -59,8 +62,9 @@ def hostname(self) -> str: async def async_get_traffic_data(self) -> Mapping[str, any]: """Get traffic data.""" + self.times_polled += 1 return { - TIMESTAMP: dt_util.utcnow(), + TIMESTAMP: dt.utcnow(), BYTES_RECEIVED: 0, BYTES_SENT: 0, PACKETS_RECEIVED: 0, diff --git a/tests/components/upnp/test_config_flow.py b/tests/components/upnp/test_config_flow.py index 77d04381a12c9..facc5f057012f 100644 --- a/tests/components/upnp/test_config_flow.py +++ b/tests/components/upnp/test_config_flow.py @@ -19,15 +19,15 @@ DISCOVERY_UNIQUE_ID, DISCOVERY_USN, DOMAIN, - DOMAIN_COORDINATORS, ) from homeassistant.components.upnp.device import Device from homeassistant.helpers.typing import HomeAssistantType from homeassistant.setup import async_setup_component +from homeassistant.util import dt from .mock_device import MockDevice -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed async def test_flow_ssdp_discovery(hass: HomeAssistantType): @@ -325,10 +325,12 @@ async def test_options_flow(hass: HomeAssistantType): # Initialisation of component. await async_setup_component(hass, "upnp", config) await hass.async_block_till_done() + mock_device.times_polled = 0 # Reset. - # DataUpdateCoordinator gets a default of 30 seconds for updates. - coordinator = hass.data[DOMAIN][DOMAIN_COORDINATORS][mock_device.udn] - assert coordinator.update_interval == timedelta(seconds=DEFAULT_SCAN_INTERVAL) + # Forward time, ensure single poll after 30 (default) seconds. + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=31)) + await hass.async_block_till_done() + assert mock_device.times_polled == 1 # Options flow with no input results in form. result = await hass.config_entries.options.async_init( @@ -346,5 +348,18 @@ async def test_options_flow(hass: HomeAssistantType): CONFIG_ENTRY_SCAN_INTERVAL: 60, } - # Also updates DataUpdateCoordinator. - assert coordinator.update_interval == timedelta(seconds=60) + # Forward time, ensure single poll after 60 seconds, still from original setting. + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=61)) + await hass.async_block_till_done() + assert mock_device.times_polled == 2 + + # Now the updated interval takes effect. + # Forward time, ensure single poll after 120 seconds. + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=121)) + await hass.async_block_till_done() + assert mock_device.times_polled == 3 + + # Forward time, ensure single poll after 180 seconds. + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=181)) + await hass.async_block_till_done() + assert mock_device.times_polled == 4 From 6269449507474fd2b5deb44c70ce63ba7a1f4ca1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 14 Apr 2021 23:52:10 +0200 Subject: [PATCH 0272/1317] Upgrade spotipy to 2.18.0 (#49220) --- homeassistant/components/spotify/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/spotify/manifest.json b/homeassistant/components/spotify/manifest.json index bd92217e9cf4d..d0d40291ffff9 100644 --- a/homeassistant/components/spotify/manifest.json +++ b/homeassistant/components/spotify/manifest.json @@ -2,7 +2,7 @@ "domain": "spotify", "name": "Spotify", "documentation": "https://www.home-assistant.io/integrations/spotify", - "requirements": ["spotipy==2.17.1"], + "requirements": ["spotipy==2.18.0"], "zeroconf": ["_spotify-connect._tcp.local."], "dependencies": ["http"], "codeowners": ["@frenck"], diff --git a/requirements_all.txt b/requirements_all.txt index 02c3393d5624c..e04b184b9d7a4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2126,7 +2126,7 @@ spiderpy==1.4.2 spotcrime==1.0.4 # homeassistant.components.spotify -spotipy==2.17.1 +spotipy==2.18.0 # homeassistant.components.recorder # homeassistant.components.sql diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6c3c1673f90bd..0c2656838da88 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1128,7 +1128,7 @@ speedtest-cli==2.1.3 spiderpy==1.4.2 # homeassistant.components.spotify -spotipy==2.17.1 +spotipy==2.18.0 # homeassistant.components.recorder # homeassistant.components.sql From 63fa9f7dd875caee4210942821bf8ee13ca33663 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 14 Apr 2021 23:56:32 +0200 Subject: [PATCH 0273/1317] Upgrade colorlog to 5.0.1 (#49221) --- homeassistant/scripts/check_config.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index 893351c771526..551f91b2b540d 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -20,7 +20,7 @@ # mypy: allow-untyped-calls, allow-untyped-defs -REQUIREMENTS = ("colorlog==4.8.0",) +REQUIREMENTS = ("colorlog==5.0.1",) _LOGGER = logging.getLogger(__name__) # pylint: disable=protected-access diff --git a/requirements_all.txt b/requirements_all.txt index e04b184b9d7a4..6c3df50ae46b9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -435,7 +435,7 @@ co2signal==0.4.2 coinbase==2.1.0 # homeassistant.scripts.check_config -colorlog==4.8.0 +colorlog==5.0.1 # homeassistant.components.color_extractor colorthief==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0c2656838da88..1179cf735fda0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -235,7 +235,7 @@ buienradar==1.0.4 caldav==0.7.1 # homeassistant.scripts.check_config -colorlog==4.8.0 +colorlog==5.0.1 # homeassistant.components.color_extractor colorthief==0.2.1 From 8bee25c938a123f0da7569b4e2753598d478b900 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 14 Apr 2021 12:16:59 -1000 Subject: [PATCH 0274/1317] Fix stop listener memory leak in DataUpdateCoordinator on retry (#49186) * Fix stop listener leak in DataUpdateCoordinator When an integration retries setup it will add a new stop listener * Skip scheduled refreshes when hass is stopping * Update homeassistant/helpers/update_coordinator.py * ensure manual refresh after stop --- homeassistant/helpers/update_coordinator.py | 18 ++++++++++-------- tests/helpers/test_update_coordinator.py | 9 +++++++++ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index 37e234363b8e8..d2d7612972d54 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -12,7 +12,6 @@ import requests from homeassistant import config_entries -from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import CALLBACK_TYPE, Event, HassJob, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import entity, event @@ -74,10 +73,6 @@ def __init__( self._debounced_refresh = request_refresh_debouncer - self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, self._async_stop_refresh - ) - @callback def async_add_listener(self, update_callback: CALLBACK_TYPE) -> Callable[[], None]: """Listen for data updates.""" @@ -128,7 +123,7 @@ def _schedule_refresh(self) -> None: async def _handle_refresh_interval(self, _now: datetime) -> None: """Handle a refresh interval occurrence.""" self._unsub_refresh = None - await self.async_refresh() + await self._async_refresh(log_failures=True, scheduled=True) async def async_request_refresh(self) -> None: """Request a refresh. @@ -162,7 +157,10 @@ async def async_refresh(self) -> None: await self._async_refresh(log_failures=True) async def _async_refresh( - self, log_failures: bool = True, raise_on_auth_failed: bool = False + self, + log_failures: bool = True, + raise_on_auth_failed: bool = False, + scheduled: bool = False, ) -> None: """Refresh data.""" if self._unsub_refresh: @@ -170,6 +168,10 @@ async def _async_refresh( self._unsub_refresh = None self._debounced_refresh.async_cancel() + + if scheduled and self.hass.is_stopping: + return + start = monotonic() auth_failed = False @@ -249,7 +251,7 @@ async def _async_refresh( self.name, monotonic() - start, ) - if not auth_failed and self._listeners: + if not auth_failed and self._listeners and not self.hass.is_stopping: self._schedule_refresh() for update_callback in self._listeners: diff --git a/tests/helpers/test_update_coordinator.py b/tests/helpers/test_update_coordinator.py index 391f2be38ec35..244e221f53a33 100644 --- a/tests/helpers/test_update_coordinator.py +++ b/tests/helpers/test_update_coordinator.py @@ -335,6 +335,15 @@ async def test_stop_refresh_on_ha_stop(hass, crd): await hass.async_block_till_done() assert crd.data == 1 + # Ensure we can still manually refresh after stop + await crd.async_refresh() + assert crd.data == 2 + + # ...and that the manual refresh doesn't setup another scheduled refresh + async_fire_time_changed(hass, utcnow() + update_interval) + await hass.async_block_till_done() + assert crd.data == 2 + @pytest.mark.parametrize( "err_msg", From e86aad34b9b4f1e7500419f847d3c583fd842ae4 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 15 Apr 2021 00:02:56 +0000 Subject: [PATCH 0275/1317] [ci skip] Translation update --- .../components/august/translations/id.json | 16 ++++++ .../components/cast/translations/id.json | 4 +- .../components/climacell/translations/id.json | 1 + .../components/deconz/translations/id.json | 4 ++ .../components/emonitor/translations/id.json | 23 ++++++++ .../enphase_envoy/translations/id.json | 22 ++++++++ .../components/ezviz/translations/id.json | 52 ++++++++++++++++++ .../google_travel_time/translations/id.json | 38 +++++++++++++ .../components/hive/translations/id.json | 53 +++++++++++++++++++ .../home_plus_control/translations/id.json | 21 ++++++++ .../huisbaasje/translations/id.json | 1 + .../components/hyperion/translations/id.json | 1 + .../components/ialarm/translations/id.json | 20 +++++++ .../kostal_plenticore/translations/id.json | 21 ++++++++ .../components/met/translations/id.json | 3 ++ .../met_eireann/translations/id.json | 19 +++++++ .../components/nuki/translations/id.json | 10 ++++ .../opentherm_gw/translations/id.json | 5 +- .../philips_js/translations/id.json | 7 +++ .../components/roomba/translations/id.json | 3 +- .../screenlogic/translations/id.json | 39 ++++++++++++++ .../components/sma/translations/id.json | 27 ++++++++++ .../components/sma/translations/nl.json | 27 ++++++++++ .../components/sma/translations/no.json | 27 ++++++++++ .../components/sma/translations/ru.json | 27 ++++++++++ .../components/verisure/translations/id.json | 47 ++++++++++++++++ .../water_heater/translations/id.json | 11 ++++ .../waze_travel_time/translations/id.json | 38 +++++++++++++ .../components/zha/translations/id.json | 1 + 29 files changed, 565 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/emonitor/translations/id.json create mode 100644 homeassistant/components/enphase_envoy/translations/id.json create mode 100644 homeassistant/components/ezviz/translations/id.json create mode 100644 homeassistant/components/google_travel_time/translations/id.json create mode 100644 homeassistant/components/hive/translations/id.json create mode 100644 homeassistant/components/home_plus_control/translations/id.json create mode 100644 homeassistant/components/ialarm/translations/id.json create mode 100644 homeassistant/components/kostal_plenticore/translations/id.json create mode 100644 homeassistant/components/met_eireann/translations/id.json create mode 100644 homeassistant/components/screenlogic/translations/id.json create mode 100644 homeassistant/components/sma/translations/id.json create mode 100644 homeassistant/components/sma/translations/nl.json create mode 100644 homeassistant/components/sma/translations/no.json create mode 100644 homeassistant/components/sma/translations/ru.json create mode 100644 homeassistant/components/verisure/translations/id.json create mode 100644 homeassistant/components/waze_travel_time/translations/id.json diff --git a/homeassistant/components/august/translations/id.json b/homeassistant/components/august/translations/id.json index a66c43ce05755..5408c2c0f70b0 100644 --- a/homeassistant/components/august/translations/id.json +++ b/homeassistant/components/august/translations/id.json @@ -10,6 +10,13 @@ "unknown": "Kesalahan yang tidak diharapkan" }, "step": { + "reauth_validate": { + "data": { + "password": "Kata Sandi" + }, + "description": "Masukkan sandi untuk {username}.", + "title": "Autentikasi ulang akun August" + }, "user": { "data": { "login_method": "Metode Masuk", @@ -20,6 +27,15 @@ "description": "Jika Metode Masuk adalah 'email', Nama Pengguna adalah alamat email. Jika Metode Masuk adalah 'telepon', Nama Pengguna adalah nomor telepon dalam format '+NNNNNNNNN'.", "title": "Siapkan akun August" }, + "user_validate": { + "data": { + "login_method": "Metode Masuk", + "password": "Kata Sandi", + "username": "Nama Pengguna" + }, + "description": "Jika Metode Masuk adalah 'email', Nama Pengguna adalah alamat email. Jika Metode Masuk adalah 'telepon', Nama Pengguna adalah nomor telepon dalam format '+NNNNNNNNN'.", + "title": "Siapkan akun August" + }, "validation": { "data": { "code": "Kode verifikasi" diff --git a/homeassistant/components/cast/translations/id.json b/homeassistant/components/cast/translations/id.json index 240ee85360976..d086b388252fc 100644 --- a/homeassistant/components/cast/translations/id.json +++ b/homeassistant/components/cast/translations/id.json @@ -27,7 +27,9 @@ "step": { "options": { "data": { - "known_hosts": "Daftar opsional host yang diketahui jika penemuan mDNS tidak berfungsi." + "ignore_cec": "Daftar opsional yang akan diteruskan ke pychromecast.IGNORE_CEC.", + "known_hosts": "Daftar opsional host yang diketahui jika penemuan mDNS tidak berfungsi.", + "uuid": "Daftar opsional UUID. Cast yang tidak tercantum tidak akan ditambahkan." }, "description": "Masukkan konfigurasi Google Cast." } diff --git a/homeassistant/components/climacell/translations/id.json b/homeassistant/components/climacell/translations/id.json index 132f4dcfcb7d3..b9f8c4ea9817d 100644 --- a/homeassistant/components/climacell/translations/id.json +++ b/homeassistant/components/climacell/translations/id.json @@ -10,6 +10,7 @@ "user": { "data": { "api_key": "Kunci API", + "api_version": "Versi API", "latitude": "Lintang", "longitude": "Bujur", "name": "Nama" diff --git a/homeassistant/components/deconz/translations/id.json b/homeassistant/components/deconz/translations/id.json index d7fb26f8d5208..c6d54beaec2f3 100644 --- a/homeassistant/components/deconz/translations/id.json +++ b/homeassistant/components/deconz/translations/id.json @@ -42,6 +42,10 @@ "button_2": "Tombol kedua", "button_3": "Tombol ketiga", "button_4": "Tombol keempat", + "button_5": "Tombol kelima", + "button_6": "Tombol keenam", + "button_7": "Tombol ketujuh", + "button_8": "Tombol kedelapan", "close": "Tutup", "dim_down": "Redupkan", "dim_up": "Terangkan", diff --git a/homeassistant/components/emonitor/translations/id.json b/homeassistant/components/emonitor/translations/id.json new file mode 100644 index 0000000000000..1365fed7d52dd --- /dev/null +++ b/homeassistant/components/emonitor/translations/id.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "flow_title": "SiteSage {name}", + "step": { + "confirm": { + "description": "Ingin menyiapkan {name} ({host})?", + "title": "Siapkan SiteSage Emonitor" + }, + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/id.json b/homeassistant/components/enphase_envoy/translations/id.json new file mode 100644 index 0000000000000..74e3e8a66c74b --- /dev/null +++ b/homeassistant/components/enphase_envoy/translations/id.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "flow_title": "Envoy {serial} ({host})", + "step": { + "user": { + "data": { + "host": "Host", + "password": "Kata Sandi", + "username": "Nama Pengguna" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ezviz/translations/id.json b/homeassistant/components/ezviz/translations/id.json new file mode 100644 index 0000000000000..e263b00c7dac2 --- /dev/null +++ b/homeassistant/components/ezviz/translations/id.json @@ -0,0 +1,52 @@ +{ + "config": { + "abort": { + "already_configured_account": "Akun sudah dikonfigurasi", + "ezviz_cloud_account_missing": "Akun cloud Ezviz tidak tersedia. Konfigurasi ulang akun cloud Ezviz", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "invalid_host": "Nama host atau alamat IP tidak valid" + }, + "flow_title": "{serial}", + "step": { + "confirm": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + }, + "description": "Masukkan kredensial RTSP untuk kamera Ezviz {serial} dengan IP {ip_address}", + "title": "Kamera Ezviz yang ditemukan" + }, + "user": { + "data": { + "password": "Kata Sandi", + "url": "URL", + "username": "Nama Pengguna" + }, + "title": "Hubungkan ke Ezviz Cloud" + }, + "user_custom_url": { + "data": { + "password": "Kata Sandi", + "url": "URL", + "username": "Nama Pengguna" + }, + "description": "Tentukan URL wilayah Anda secara manual", + "title": "Hubungkan ke URL Ezviz khusus" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ffmpeg_arguments": "Argumen yang diteruskan ke ffmpeg untuk kamera", + "timeout": "Tenggang Waktu Permintaan (detik)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_travel_time/translations/id.json b/homeassistant/components/google_travel_time/translations/id.json new file mode 100644 index 0000000000000..3973d673f8ea6 --- /dev/null +++ b/homeassistant/components/google_travel_time/translations/id.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "Lokasi sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "step": { + "user": { + "data": { + "api_key": "Kunci API", + "destination": "Tujuan", + "origin": "Asal" + }, + "description": "Saat menentukan asal dan tujuan, Anda dapat menyediakan satu atau beberapa lokasi yang dipisahkan oleh karakter pipe, dalam bentuk alamat, koordinat lintang/bujur, atau ID tempat Google. Saat menentukan lokasi menggunakan ID tempat Google, ID harus diawali dengan \"place_id:'." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "avoid": "Hindari", + "language": "Bahasa", + "mode": "Mode Perjalanan", + "time": "Waktu", + "time_type": "Jenis Waktu", + "transit_mode": "Mode Transit", + "transit_routing_preference": "Preferensi Perutean Transit", + "units": "Unit" + }, + "description": "Anda dapat menentukan Waktu Keberangkatan atau Waktu Kedatangan secara opsional. Jika menentukan waktu keberangkatan, Anda dapat memasukkan 'sekarang', stempel waktu Unix, atau string waktu 24 jam seperti 08:00:00`. Jika menentukan waktu kedatangan, Anda dapat menggunakan stempel waktu Unix atau string waktu 24 jam seperti 08:00:00`" + } + } + }, + "title": "Waktu Perjalanan Google Maps" +} \ No newline at end of file diff --git a/homeassistant/components/hive/translations/id.json b/homeassistant/components/hive/translations/id.json new file mode 100644 index 0000000000000..e092515e91e54 --- /dev/null +++ b/homeassistant/components/hive/translations/id.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil", + "unknown_entry": "Tidak dapat menemukan entri yang sudah ada." + }, + "error": { + "invalid_code": "Gagal masuk ke Hive. Kode autentikasi dua faktor Anda salah.", + "invalid_password": "Gagal masuk ke Hive. Sandinya sa\u00f6ah, coba kembali.", + "invalid_username": "Gagal masuk ke Hive. Alamat email Anda tidak dikenali.", + "no_internet_available": "Koneksi internet diperlukan untuk terhubung ke Hive.", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "2fa": { + "data": { + "2fa": "Kode dua faktor" + }, + "description": "Masukkan kode autentikasi Hive Anda. \n \nMasukkan kode 0000 untuk meminta kode lain.", + "title": "Autentikasi Dua Faktor Hive." + }, + "reauth": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + }, + "description": "Masukkan kembali informasi masuk Hive Anda.", + "title": "Info Masuk Hive" + }, + "user": { + "data": { + "password": "Kata Sandi", + "scan_interval": "Interval Pindai (detik)", + "username": "Nama Pengguna" + }, + "description": "Masukkan informasi masuk dan konfigurasi Hive Anda.", + "title": "Info Masuk Hive" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "scan_interval": "Interval Pindai (detik)" + }, + "description": "Perbarui interval pemindaian untuk meminta data lebih sering.", + "title": "Opsi untuk Hive" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/home_plus_control/translations/id.json b/homeassistant/components/home_plus_control/translations/id.json new file mode 100644 index 0000000000000..2ef7efe3d871b --- /dev/null +++ b/homeassistant/components/home_plus_control/translations/id.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "authorize_url_timeout": "Tenggang waktu pembuatan URL otorisasi habis.", + "missing_configuration": "Komponen tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi.", + "no_url_available": "Tidak ada URL yang tersedia. Untuk informasi tentang kesalahan ini, [lihat bagian bantuan]({docs_url})", + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "create_entry": { + "default": "Berhasil diautentikasi" + }, + "step": { + "pick_implementation": { + "title": "Pilih Metode Autentikasi" + } + } + }, + "title": "Legrand Home+ Control" +} \ No newline at end of file diff --git a/homeassistant/components/huisbaasje/translations/id.json b/homeassistant/components/huisbaasje/translations/id.json index 76e8805524e8a..c83d53b384924 100644 --- a/homeassistant/components/huisbaasje/translations/id.json +++ b/homeassistant/components/huisbaasje/translations/id.json @@ -4,6 +4,7 @@ "already_configured": "Perangkat sudah dikonfigurasi" }, "error": { + "cannot_connect": "Gagal terhubung", "connection_exception": "Gagal terhubung", "invalid_auth": "Autentikasi tidak valid", "unauthenticated_exception": "Autentikasi tidak valid", diff --git a/homeassistant/components/hyperion/translations/id.json b/homeassistant/components/hyperion/translations/id.json index c1c2a62e0d933..fd1bb12711da9 100644 --- a/homeassistant/components/hyperion/translations/id.json +++ b/homeassistant/components/hyperion/translations/id.json @@ -45,6 +45,7 @@ "step": { "init": { "data": { + "effect_show_list": "Efek hyperion untuk ditampilkan", "priority": "Prioritas hyperion digunakan untuk warna dan efek" } } diff --git a/homeassistant/components/ialarm/translations/id.json b/homeassistant/components/ialarm/translations/id.json new file mode 100644 index 0000000000000..4f299f816f1dc --- /dev/null +++ b/homeassistant/components/ialarm/translations/id.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "host": "Host", + "pin": "Kode PIN", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kostal_plenticore/translations/id.json b/homeassistant/components/kostal_plenticore/translations/id.json new file mode 100644 index 0000000000000..c249355f8ca12 --- /dev/null +++ b/homeassistant/components/kostal_plenticore/translations/id.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Kata Sandi" + } + } + } + }, + "title": "Solar Inverter Kostal Plenticore" +} \ No newline at end of file diff --git a/homeassistant/components/met/translations/id.json b/homeassistant/components/met/translations/id.json index 639ed5086ce23..cb60165d6c4aa 100644 --- a/homeassistant/components/met/translations/id.json +++ b/homeassistant/components/met/translations/id.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_home": "Tidak ada koordinat rumah yang disetel dalam konfigurasi Home Assistant" + }, "error": { "already_configured": "Layanan sudah dikonfigurasi" }, diff --git a/homeassistant/components/met_eireann/translations/id.json b/homeassistant/components/met_eireann/translations/id.json new file mode 100644 index 0000000000000..68028a77dfc4a --- /dev/null +++ b/homeassistant/components/met_eireann/translations/id.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "already_configured": "Layanan sudah dikonfigurasi" + }, + "step": { + "user": { + "data": { + "elevation": "Ketinggian", + "latitude": "Lintang", + "longitude": "Bujur", + "name": "Nama" + }, + "description": "Masukkan lokasi Anda untuk menggunakan data cuaca dari Met \u00c9ireann Public Weather Forecast API", + "title": "Lokasi" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuki/translations/id.json b/homeassistant/components/nuki/translations/id.json index d9e5e1de2c31a..1294b18b460ca 100644 --- a/homeassistant/components/nuki/translations/id.json +++ b/homeassistant/components/nuki/translations/id.json @@ -1,11 +1,21 @@ { "config": { + "abort": { + "reauth_successful": "Autentikasi ulang berhasil" + }, "error": { "cannot_connect": "Gagal terhubung", "invalid_auth": "Autentikasi tidak valid", "unknown": "Kesalahan yang tidak diharapkan" }, "step": { + "reauth_confirm": { + "data": { + "token": "Token Akses" + }, + "description": "Integrasi Nuki perlu mengautentikasi ulang dengan bridge Anda.", + "title": "Autentikasi Ulang Integrasi" + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/opentherm_gw/translations/id.json b/homeassistant/components/opentherm_gw/translations/id.json index 7c7624c3dfe07..c0fc97d9c8f36 100644 --- a/homeassistant/components/opentherm_gw/translations/id.json +++ b/homeassistant/components/opentherm_gw/translations/id.json @@ -21,7 +21,10 @@ "init": { "data": { "floor_temperature": "Suhu Lantai", - "precision": "Tingkat Presisi" + "precision": "Tingkat Presisi", + "read_precision": "Tingkat Presisi Baca", + "set_precision": "Atur Presisi", + "temporary_override_mode": "Mode Penimpaan Setpoint Sementara" }, "description": "Pilihan untuk Gateway OpenTherm" } diff --git a/homeassistant/components/philips_js/translations/id.json b/homeassistant/components/philips_js/translations/id.json index 633cfdd633e69..b9a1b948a9156 100644 --- a/homeassistant/components/philips_js/translations/id.json +++ b/homeassistant/components/philips_js/translations/id.json @@ -10,6 +10,13 @@ "unknown": "Kesalahan yang tidak diharapkan" }, "step": { + "pair": { + "data": { + "pin": "Kode PIN" + }, + "description": "Masukkan PIN yang ditampilkan di TV Anda", + "title": "Pasangkan" + }, "user": { "data": { "api_version": "Versi API", diff --git a/homeassistant/components/roomba/translations/id.json b/homeassistant/components/roomba/translations/id.json index 3afe75ae09da8..aaffac267aa47 100644 --- a/homeassistant/components/roomba/translations/id.json +++ b/homeassistant/components/roomba/translations/id.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "Perangkat sudah dikonfigurasi", "cannot_connect": "Gagal terhubung", - "not_irobot_device": "Perangkat yang ditemukan bukan perangkat iRobot" + "not_irobot_device": "Perangkat yang ditemukan bukan perangkat iRobot", + "short_blid": "BLID terpotong" }, "error": { "cannot_connect": "Gagal terhubung" diff --git a/homeassistant/components/screenlogic/translations/id.json b/homeassistant/components/screenlogic/translations/id.json new file mode 100644 index 0000000000000..5af1cfbe5ef98 --- /dev/null +++ b/homeassistant/components/screenlogic/translations/id.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "flow_title": "ScreenLogic {name}", + "step": { + "gateway_entry": { + "data": { + "ip_address": "Alamat IP", + "port": "Port" + }, + "description": "Masukkan informasi ScreenLogic Gateway Anda.", + "title": "ScreenLogic" + }, + "gateway_select": { + "data": { + "selected_gateway": "Gateway" + }, + "description": "Gateway ScreenLogic berikut ini ditemukan. Pilih satu untuk dikonfigurasi, atau pilih untuk mengonfigurasi gateway ScreenLogic secara manual.", + "title": "ScreenLogic" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Interval pemindaian dalam detik" + }, + "description": "Tentukan pengaturan untuk {gateway_name}", + "title": "ScreenLogic" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sma/translations/id.json b/homeassistant/components/sma/translations/id.json new file mode 100644 index 0000000000000..8f8ec5bda24a4 --- /dev/null +++ b/homeassistant/components/sma/translations/id.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "cannot_retrieve_device_info": "Berhasil tersambung, tetapi tidak dapat mengambil informasi perangkat", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "group": "Grup", + "host": "Host", + "password": "Kata Sandi", + "ssl": "Menggunakan sertifikat SSL", + "verify_ssl": "Verifikasi sertifikat SSL" + }, + "description": "Masukkan informasi perangkat SMA Anda.", + "title": "Siapkan SMA Solar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sma/translations/nl.json b/homeassistant/components/sma/translations/nl.json new file mode 100644 index 0000000000000..d860518a18c7a --- /dev/null +++ b/homeassistant/components/sma/translations/nl.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "already_in_progress": "De configuratiestroom is al aan de gang" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "cannot_retrieve_device_info": "Succesvol verbonden, maar kan geen apparaatinformatie ophalen", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "group": "Groep", + "host": "Host", + "password": "Wachtwoord", + "ssl": "Gebruik een SSL-certificaat", + "verify_ssl": "SSL-certificaat verifi\u00ebren" + }, + "description": "Voer uw SMA-apparaatgegevens in.", + "title": "SMA Solar instellen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sma/translations/no.json b/homeassistant/components/sma/translations/no.json new file mode 100644 index 0000000000000..7c56ed722b6f9 --- /dev/null +++ b/homeassistant/components/sma/translations/no.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "cannot_retrieve_device_info": "Koblet til, men kan ikke hente enhetsinformasjonen", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "group": "Gruppe", + "host": "Vert", + "password": "Passord", + "ssl": "Bruker et SSL-sertifikat", + "verify_ssl": "Verifisere SSL-sertifikat" + }, + "description": "Skriv inn SMA-enhetsinformasjonen din.", + "title": "Sett opp SMA Solar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sma/translations/ru.json b/homeassistant/components/sma/translations/ru.json new file mode 100644 index 0000000000000..ab1b7635bc32b --- /dev/null +++ b/homeassistant/components/sma/translations/ru.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "cannot_retrieve_device_info": "\u0423\u0441\u043f\u0435\u0448\u043d\u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e, \u043d\u043e \u043d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e\u0431 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0435.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "group": "\u0413\u0440\u0443\u043f\u043f\u0430", + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL", + "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c SMA.", + "title": "SMA Solar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/verisure/translations/id.json b/homeassistant/components/verisure/translations/id.json new file mode 100644 index 0000000000000..5c9badda34183 --- /dev/null +++ b/homeassistant/components/verisure/translations/id.json @@ -0,0 +1,47 @@ +{ + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" + }, + "error": { + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "installation": { + "data": { + "giid": "Instalasi" + }, + "description": "Home Assistant menemukan beberapa instalasi Verisure di akun My Pages. Pilih instalasi untuk ditambahkan ke Home Assistant." + }, + "reauth_confirm": { + "data": { + "description": "Autentikasi ulang dengan akun Verisure My Pages Anda.", + "email": "Email", + "password": "Kata Sandi" + } + }, + "user": { + "data": { + "description": "Masuk dengan akun Verisure My Pages Anda.", + "email": "Email", + "password": "Kata Sandi" + } + } + } + }, + "options": { + "error": { + "code_format_mismatch": "Kode PIN default tidak cocok dengan jumlah digit yang diperlukan" + }, + "step": { + "init": { + "data": { + "lock_code_digits": "Jumlah digit dalam kode PIN untuk kunci", + "lock_default_code": "Kode PIN default untuk kunci, digunakan jika tidak ada yang diberikan" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/water_heater/translations/id.json b/homeassistant/components/water_heater/translations/id.json index 591f96ffc4f4e..0f29290a50343 100644 --- a/homeassistant/components/water_heater/translations/id.json +++ b/homeassistant/components/water_heater/translations/id.json @@ -4,5 +4,16 @@ "turn_off": "Matikan {entity_name}", "turn_on": "Nyalakan {entity_name}" } + }, + "state": { + "_": { + "eco": "Eco", + "electric": "Listrik", + "gas": "Gas", + "heat_pump": "Pompa Pemanas", + "high_demand": "Permintaan Tinggi", + "off": "Mati", + "performance": "Kinerja" + } } } \ No newline at end of file diff --git a/homeassistant/components/waze_travel_time/translations/id.json b/homeassistant/components/waze_travel_time/translations/id.json new file mode 100644 index 0000000000000..587e959fe7eee --- /dev/null +++ b/homeassistant/components/waze_travel_time/translations/id.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "Lokasi sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "step": { + "user": { + "data": { + "destination": "Tujuan", + "origin": "Asal", + "region": "Wilayah" + }, + "description": "Untuk Asal dan Tujuan, masukkan alamat atau koordinat GPS lokasi (koordinat GPS harus dipisahkan dengan koma). Anda juga dapat memasukkan ID entitas yang menyediakan informasi ini dalam statusnya, ID entitas dengan atribut garis lintang dan garis bujur, atau nama ramah zona." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "avoid_ferries": "Hindari Feri?", + "avoid_subscription_roads": "Hindari Jalan Berbayar?", + "avoid_toll_roads": "Hindari Jalan Tol?", + "excl_filter": "Substring TIDAK dalam Deskripsi Rute yang Dipilih", + "incl_filter": "Substring dalam Deskripsi Rute yang Dipilih", + "realtime": "Waktu Perjalanan Waktu Nyata?", + "units": "Unit", + "vehicle_type": "Jenis Kendaraan" + }, + "description": "Input `substring` akan memungkinkan Anda untuk memaksa integrasi untuk menggunakan rute tertentu atau menghindari rute tertentu dalam perhitungan waktu perjalanan." + } + } + }, + "title": "Waktu Perjalanan Waze" +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/id.json b/homeassistant/components/zha/translations/id.json index 5baf04e13149e..aaa563ffddf3a 100644 --- a/homeassistant/components/zha/translations/id.json +++ b/homeassistant/components/zha/translations/id.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "Gagal terhubung" }, + "flow_title": "ZHA: {name}", "step": { "pick_radio": { "data": { From 54322f84c50fb1e57b0d035cb7d2b58c1e80decc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 14 Apr 2021 20:49:28 -1000 Subject: [PATCH 0276/1317] Do not schedule future ping device tracker updates once hass is stopping (#49236) --- homeassistant/components/ping/device_tracker.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ping/device_tracker.py b/homeassistant/components/ping/device_tracker.py index 256023263bab6..e40b8168938b0 100644 --- a/homeassistant/components/ping/device_tracker.py +++ b/homeassistant/components/ping/device_tracker.py @@ -141,9 +141,10 @@ async def _async_update_interval(now): try: await async_update(now) finally: - async_track_point_in_utc_time( - hass, _async_update_interval, util.dt.utcnow() + interval - ) + if not hass.is_stopping: + async_track_point_in_utc_time( + hass, _async_update_interval, util.dt.utcnow() + interval + ) await _async_update_interval(None) return True From e234fc6e7e33f356e209307ae83e351244502d04 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 14 Apr 2021 21:47:15 -1000 Subject: [PATCH 0277/1317] Disconnect homekit_controller devices on the stop event (#49244) --- .../components/homekit_controller/__init__.py | 12 ++++++++ .../homekit_controller/test_init.py | 29 +++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 tests/components/homekit_controller/test_init.py diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index d7b28036426d8..3db6c1800c9e4 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -1,6 +1,7 @@ """Support for Homekit device discovery.""" from __future__ import annotations +import asyncio from typing import Any import aiohomekit @@ -13,6 +14,7 @@ from aiohomekit.model.services import Service, ServicesTypes from homeassistant.components import zeroconf +from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.entity import Entity @@ -228,6 +230,16 @@ async def async_setup(hass, config): hass.data[KNOWN_DEVICES] = {} hass.data[TRIGGERS] = {} + async def _async_stop_homekit_controller(event): + await asyncio.gather( + *[ + connection.async_unload() + for connection in hass.data[KNOWN_DEVICES].values() + ] + ) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop_homekit_controller) + return True diff --git a/tests/components/homekit_controller/test_init.py b/tests/components/homekit_controller/test_init.py new file mode 100644 index 0000000000000..cd5662d73c92d --- /dev/null +++ b/tests/components/homekit_controller/test_init.py @@ -0,0 +1,29 @@ +"""Tests for homekit_controller init.""" + +from unittest.mock import patch + +from aiohomekit.model.characteristics import CharacteristicsTypes +from aiohomekit.model.services import ServicesTypes + +from homeassistant.const import EVENT_HOMEASSISTANT_STOP + +from tests.components.homekit_controller.common import setup_test_component + + +def create_motion_sensor_service(accessory): + """Define motion characteristics as per page 225 of HAP spec.""" + service = accessory.add_service(ServicesTypes.MOTION_SENSOR) + cur_state = service.add_char(CharacteristicsTypes.MOTION_DETECTED) + cur_state.value = 0 + + +async def test_unload_on_stop(hass, utcnow): + """Test async_unload is called on stop.""" + await setup_test_component(hass, create_motion_sensor_service) + with patch( + "homeassistant.components.homekit_controller.HKDevice.async_unload" + ) as async_unlock_mock: + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + + assert async_unlock_mock.called From 985b4a581af9b7a8d28867a6bed1bf88c5c7bdd3 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Thu, 15 Apr 2021 09:47:43 +0200 Subject: [PATCH 0278/1317] Create KNX switch entity directly from config (#49238) --- homeassistant/components/knx/__init__.py | 10 +++++++- homeassistant/components/knx/factory.py | 20 +++------------- homeassistant/components/knx/switch.py | 29 +++++++++++++++++++----- 3 files changed, 35 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 0fe3e133b6eb1..5caa284cc48f7 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -235,7 +235,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # We need to wait until all entities are loaded into the device list since they could also be created from other platforms for platform in SupportedPlatforms: hass.async_create_task( - discovery.async_load_platform(hass, platform.value, DOMAIN, {}, config) + discovery.async_load_platform( + hass, + platform.value, + DOMAIN, + { + "platform_config": config[DOMAIN].get(platform.value), + }, + config, + ) ) hass.services.async_register( diff --git a/homeassistant/components/knx/factory.py b/homeassistant/components/knx/factory.py index 827ec83a8e18f..ebc9bfc0ce284 100644 --- a/homeassistant/components/knx/factory.py +++ b/homeassistant/components/knx/factory.py @@ -13,7 +13,6 @@ Notification as XknxNotification, Scene as XknxScene, Sensor as XknxSensor, - Switch as XknxSwitch, Weather as XknxWeather, ) @@ -29,7 +28,6 @@ LightSchema, SceneSchema, SensorSchema, - SwitchSchema, WeatherSchema, ) @@ -38,7 +36,7 @@ def create_knx_device( platform: SupportedPlatforms, knx_module: XKNX, config: ConfigType, -) -> XknxDevice: +) -> XknxDevice | None: """Return the requested XKNX device.""" if platform is SupportedPlatforms.LIGHT: return _create_light(knx_module, config) @@ -49,9 +47,6 @@ def create_knx_device( if platform is SupportedPlatforms.CLIMATE: return _create_climate(knx_module, config) - if platform is SupportedPlatforms.SWITCH: - return _create_switch(knx_module, config) - if platform is SupportedPlatforms.SENSOR: return _create_sensor(knx_module, config) @@ -70,6 +65,8 @@ def create_knx_device( if platform is SupportedPlatforms.FAN: return _create_fan(knx_module, config) + return None + def _create_cover(knx_module: XKNX, config: ConfigType) -> XknxCover: """Return a KNX Cover device to be used within XKNX.""" @@ -270,17 +267,6 @@ def _create_climate(knx_module: XKNX, config: ConfigType) -> XknxClimate: ) -def _create_switch(knx_module: XKNX, config: ConfigType) -> XknxSwitch: - """Return a KNX switch to be used within XKNX.""" - return XknxSwitch( - knx_module, - name=config[CONF_NAME], - group_address=config[KNX_ADDRESS], - group_address_state=config.get(SwitchSchema.CONF_STATE_ADDRESS), - invert=config[SwitchSchema.CONF_INVERT], - ) - - def _create_sensor(knx_module: XKNX, config: ConfigType) -> XknxSensor: """Return a KNX sensor to be used within XKNX.""" return XknxSensor( diff --git a/homeassistant/components/knx/switch.py b/homeassistant/components/knx/switch.py index 82fe2f40be379..c52beaea2ef64 100644 --- a/homeassistant/components/knx/switch.py +++ b/homeassistant/components/knx/switch.py @@ -3,15 +3,18 @@ from typing import Any, Callable, Iterable +from xknx import XKNX from xknx.devices import Switch as XknxSwitch from homeassistant.components.switch import SwitchEntity +from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .const import DOMAIN +from .const import DOMAIN, KNX_ADDRESS from .knx_entity import KnxEntity +from .schema import SwitchSchema async def async_setup_platform( @@ -21,20 +24,34 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up switch(es) for KNX platform.""" + if not discovery_info or not discovery_info["platform_config"]: + return + + platform_config = discovery_info["platform_config"] + xknx: XKNX = hass.data[DOMAIN].xknx + entities = [] - for device in hass.data[DOMAIN].xknx.devices: - if isinstance(device, XknxSwitch): - entities.append(KNXSwitch(device)) + for entity_config in platform_config: + entities.append(KNXSwitch(xknx, entity_config)) + async_add_entities(entities) class KNXSwitch(KnxEntity, SwitchEntity): """Representation of a KNX switch.""" - def __init__(self, device: XknxSwitch) -> None: + def __init__(self, xknx: XKNX, config: ConfigType) -> None: """Initialize of KNX switch.""" self._device: XknxSwitch - super().__init__(device) + super().__init__( + device=XknxSwitch( + xknx, + name=config[CONF_NAME], + group_address=config[KNX_ADDRESS], + group_address_state=config.get(SwitchSchema.CONF_STATE_ADDRESS), + invert=config[SwitchSchema.CONF_INVERT], + ) + ) @property def is_on(self) -> bool: From 055cdc64c028512ba37cb3a4ef87301b56c8d2d9 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 15 Apr 2021 10:21:38 +0200 Subject: [PATCH 0279/1317] Add support for IoT class in manifest (#46935) --- homeassistant/components/abode/manifest.json | 3 +- .../components/accuweather/manifest.json | 3 +- .../components/acer_projector/manifest.json | 3 +- homeassistant/components/acmeda/manifest.json | 7 +- .../components/actiontec/manifest.json | 3 +- .../components/adguard/manifest.json | 3 +- homeassistant/components/ads/manifest.json | 3 +- .../components/advantage_air/manifest.json | 3 +- homeassistant/components/aemet/manifest.json | 3 +- .../components/aftership/manifest.json | 3 +- .../components/agent_dvr/manifest.json | 3 +- homeassistant/components/airly/manifest.json | 3 +- homeassistant/components/airnow/manifest.json | 9 +- .../components/airvisual/manifest.json | 3 +- .../components/aladdin_connect/manifest.json | 3 +- .../components/alarmdecoder/manifest.json | 3 +- homeassistant/components/alert/manifest.json | 3 +- homeassistant/components/alexa/manifest.json | 14 +-- homeassistant/components/almond/manifest.json | 3 +- .../components/alpha_vantage/manifest.json | 3 +- .../components/amazon_polly/manifest.json | 3 +- .../components/ambiclimate/manifest.json | 3 +- .../components/ambient_station/manifest.json | 3 +- .../components/amcrest/manifest.json | 3 +- homeassistant/components/ampio/manifest.json | 3 +- .../components/analytics/manifest.json | 3 +- .../android_ip_webcam/manifest.json | 3 +- .../components/androidtv/manifest.json | 3 +- .../components/anel_pwrctrl/manifest.json | 3 +- .../components/anthemav/manifest.json | 3 +- .../components/apache_kafka/manifest.json | 3 +- .../components/apcupsd/manifest.json | 3 +- homeassistant/components/apns/manifest.json | 3 +- .../components/apple_tv/manifest.json | 14 +-- .../components/apprise/manifest.json | 3 +- homeassistant/components/aprs/manifest.json | 3 +- .../components/aqualogic/manifest.json | 3 +- .../components/aquostv/manifest.json | 3 +- .../components/arcam_fmj/manifest.json | 3 +- .../components/arduino/manifest.json | 3 +- homeassistant/components/arest/manifest.json | 3 +- homeassistant/components/arlo/manifest.json | 3 +- .../components/arris_tg2492lg/manifest.json | 9 +- homeassistant/components/aruba/manifest.json | 3 +- homeassistant/components/arwn/manifest.json | 3 +- .../components/asterisk_cdr/manifest.json | 3 +- .../components/asterisk_mbox/manifest.json | 3 +- .../components/asuswrt/manifest.json | 3 +- homeassistant/components/atag/manifest.json | 3 +- .../components/aten_pe/manifest.json | 3 +- homeassistant/components/atome/manifest.json | 3 +- homeassistant/components/august/manifest.json | 18 ++- homeassistant/components/aurora/manifest.json | 3 +- .../aurora_abb_powerone/manifest.json | 3 +- .../components/automation/manifest.json | 9 +- homeassistant/components/avea/manifest.json | 3 +- homeassistant/components/avion/manifest.json | 3 +- homeassistant/components/awair/manifest.json | 3 +- homeassistant/components/aws/manifest.json | 3 +- homeassistant/components/axis/manifest.json | 33 ++++-- .../components/azure_devops/manifest.json | 3 +- .../components/azure_event_hub/manifest.json | 3 +- .../azure_service_bus/manifest.json | 3 +- homeassistant/components/baidu/manifest.json | 3 +- .../components/bayesian/manifest.json | 3 +- .../components/bbb_gpio/manifest.json | 3 +- homeassistant/components/bbox/manifest.json | 3 +- .../components/beewi_smartclim/manifest.json | 3 +- homeassistant/components/bh1750/manifest.json | 3 +- .../components/bitcoin/manifest.json | 3 +- .../components/bizkaibus/manifest.json | 3 +- .../components/blackbird/manifest.json | 3 +- homeassistant/components/blebox/manifest.json | 3 +- homeassistant/components/blink/manifest.json | 10 +- .../components/blinksticklight/manifest.json | 3 +- homeassistant/components/blinkt/manifest.json | 3 +- .../components/blockchain/manifest.json | 3 +- .../components/bloomsky/manifest.json | 3 +- .../components/blueprint/manifest.json | 4 +- .../components/bluesound/manifest.json | 3 +- .../bluetooth_le_tracker/manifest.json | 3 +- .../bluetooth_tracker/manifest.json | 3 +- homeassistant/components/bme280/manifest.json | 3 +- homeassistant/components/bme680/manifest.json | 3 +- homeassistant/components/bmp280/manifest.json | 3 +- .../bmw_connected_drive/manifest.json | 3 +- homeassistant/components/bond/manifest.json | 3 +- .../components/braviatv/manifest.json | 3 +- .../components/broadlink/manifest.json | 19 +++- .../components/brother/manifest.json | 10 +- .../brottsplatskartan/manifest.json | 3 +- .../components/browser/manifest.json | 3 +- homeassistant/components/brunt/manifest.json | 3 +- homeassistant/components/bsblan/manifest.json | 3 +- .../components/bt_home_hub_5/manifest.json | 3 +- .../components/bt_smarthub/manifest.json | 3 +- .../components/buienradar/manifest.json | 3 +- homeassistant/components/caldav/manifest.json | 3 +- homeassistant/components/canary/manifest.json | 3 +- homeassistant/components/cast/manifest.json | 12 +- .../components/cert_expiry/manifest.json | 3 +- .../components/channels/manifest.json | 3 +- .../components/circuit/manifest.json | 3 +- .../components/cisco_ios/manifest.json | 3 +- .../cisco_mobility_express/manifest.json | 3 +- .../cisco_webex_teams/manifest.json | 3 +- .../components/citybikes/manifest.json | 3 +- .../components/clementine/manifest.json | 3 +- .../components/clickatell/manifest.json | 3 +- .../components/clicksend/manifest.json | 3 +- .../components/clicksend_tts/manifest.json | 3 +- .../components/climacell/manifest.json | 3 +- homeassistant/components/cloud/manifest.json | 3 +- .../components/cloudflare/manifest.json | 3 +- homeassistant/components/cmus/manifest.json | 3 +- .../components/co2signal/manifest.json | 3 +- .../components/coinbase/manifest.json | 3 +- .../comed_hourly_pricing/manifest.json | 3 +- .../components/comfoconnect/manifest.json | 3 +- .../components/command_line/manifest.json | 3 +- .../components/compensation/manifest.json | 3 +- .../components/concord232/manifest.json | 3 +- .../components/control4/manifest.json | 3 +- .../components/conversation/manifest.json | 3 +- .../components/coolmaster/manifest.json | 3 +- .../components/coronavirus/manifest.json | 9 +- .../components/cppm_tracker/manifest.json | 3 +- .../components/cpuspeed/manifest.json | 3 +- homeassistant/components/cups/manifest.json | 3 +- .../components/currencylayer/manifest.json | 3 +- homeassistant/components/daikin/manifest.json | 3 +- .../components/danfoss_air/manifest.json | 3 +- .../components/darksky/manifest.json | 3 +- .../components/datadog/manifest.json | 3 +- homeassistant/components/ddwrt/manifest.json | 3 +- .../components/debugpy/manifest.json | 3 +- homeassistant/components/deconz/manifest.json | 3 +- homeassistant/components/decora/manifest.json | 3 +- .../components/decora_wifi/manifest.json | 3 +- homeassistant/components/delijn/manifest.json | 3 +- homeassistant/components/deluge/manifest.json | 3 +- homeassistant/components/demo/manifest.json | 3 +- homeassistant/components/denon/manifest.json | 3 +- .../components/denonavr/manifest.json | 3 +- .../components/derivative/manifest.json | 3 +- .../components/deutsche_bahn/manifest.json | 3 +- .../device_sun_light_trigger/manifest.json | 3 +- .../devolo_home_control/manifest.json | 3 +- homeassistant/components/dexcom/manifest.json | 5 +- homeassistant/components/dhcp/manifest.json | 11 +- homeassistant/components/dht/manifest.json | 3 +- .../components/dialogflow/manifest.json | 3 +- .../components/digital_ocean/manifest.json | 3 +- .../components/digitalloggers/manifest.json | 3 +- .../components/directv/manifest.json | 3 +- .../components/discogs/manifest.json | 3 +- .../components/discord/manifest.json | 3 +- .../components/dlib_face_detect/manifest.json | 3 +- .../dlib_face_identify/manifest.json | 3 +- homeassistant/components/dlink/manifest.json | 3 +- .../components/dlna_dmr/manifest.json | 3 +- homeassistant/components/dnsip/manifest.json | 3 +- .../components/dominos/manifest.json | 3 +- homeassistant/components/doods/manifest.json | 3 +- .../components/doorbird/manifest.json | 10 +- homeassistant/components/dovado/manifest.json | 3 +- homeassistant/components/dsmr/manifest.json | 3 +- .../components/dsmr_reader/manifest.json | 3 +- .../dte_energy_bridge/manifest.json | 3 +- .../dublin_bus_transport/manifest.json | 3 +- .../components/duckdns/manifest.json | 3 +- homeassistant/components/dunehd/manifest.json | 3 +- .../dwd_weather_warnings/manifest.json | 3 +- homeassistant/components/dweet/manifest.json | 3 +- .../components/dynalite/manifest.json | 3 +- homeassistant/components/dyson/manifest.json | 3 +- homeassistant/components/eafm/manifest.json | 3 +- homeassistant/components/ebox/manifest.json | 3 +- homeassistant/components/ebusd/manifest.json | 3 +- .../components/ecoal_boiler/manifest.json | 3 +- homeassistant/components/ecobee/manifest.json | 3 +- homeassistant/components/econet/manifest.json | 6 +- .../components/ecovacs/manifest.json | 3 +- .../eddystone_temperature/manifest.json | 3 +- homeassistant/components/edimax/manifest.json | 3 +- homeassistant/components/edl21/manifest.json | 3 +- .../components/ee_brightbox/manifest.json | 3 +- homeassistant/components/efergy/manifest.json | 3 +- .../components/egardia/manifest.json | 3 +- .../components/eight_sleep/manifest.json | 3 +- homeassistant/components/elgato/manifest.json | 3 +- .../components/eliqonline/manifest.json | 3 +- homeassistant/components/elkm1/manifest.json | 3 +- homeassistant/components/elv/manifest.json | 3 +- homeassistant/components/emby/manifest.json | 3 +- .../components/emoncms/manifest.json | 3 +- .../components/emoncms_history/manifest.json | 3 +- .../components/emonitor/manifest.json | 13 +-- .../components/emulated_hue/manifest.json | 3 +- .../components/emulated_kasa/manifest.json | 3 +- .../components/emulated_roku/manifest.json | 3 +- .../components/enigma2/manifest.json | 3 +- .../components/enocean/manifest.json | 11 +- .../components/enphase_envoy/manifest.json | 15 +-- .../entur_public_transport/manifest.json | 3 +- .../environment_canada/manifest.json | 3 +- .../components/envirophat/manifest.json | 3 +- .../components/envisalink/manifest.json | 3 +- .../components/ephember/manifest.json | 3 +- homeassistant/components/epson/manifest.json | 5 +- .../components/epsonworkforce/manifest.json | 3 +- .../components/eq3btsmart/manifest.json | 3 +- .../components/esphome/manifest.json | 3 +- homeassistant/components/essent/manifest.json | 3 +- .../components/etherscan/manifest.json | 3 +- homeassistant/components/eufy/manifest.json | 3 +- .../components/everlights/manifest.json | 3 +- .../components/evohome/manifest.json | 3 +- homeassistant/components/ezviz/manifest.json | 3 +- .../components/faa_delays/manifest.json | 3 +- .../components/facebook/manifest.json | 3 +- .../components/facebox/manifest.json | 3 +- .../components/fail2ban/manifest.json | 3 +- .../components/familyhub/manifest.json | 3 +- .../components/fastdotcom/manifest.json | 3 +- .../components/feedreader/manifest.json | 3 +- .../components/ffmpeg_motion/manifest.json | 3 +- .../components/ffmpeg_noise/manifest.json | 3 +- homeassistant/components/fibaro/manifest.json | 3 +- homeassistant/components/fido/manifest.json | 3 +- homeassistant/components/file/manifest.json | 3 +- .../components/filesize/manifest.json | 3 +- homeassistant/components/filter/manifest.json | 3 +- homeassistant/components/fints/manifest.json | 3 +- .../components/fireservicerota/manifest.json | 3 +- .../components/firmata/manifest.json | 11 +- homeassistant/components/fitbit/manifest.json | 3 +- homeassistant/components/fixer/manifest.json | 3 +- .../components/fleetgo/manifest.json | 3 +- homeassistant/components/flexit/manifest.json | 3 +- homeassistant/components/flic/manifest.json | 3 +- .../components/flick_electric/manifest.json | 11 +- homeassistant/components/flo/manifest.json | 3 +- homeassistant/components/flock/manifest.json | 3 +- homeassistant/components/flume/manifest.json | 13 ++- .../components/flunearyou/manifest.json | 3 +- homeassistant/components/flux/manifest.json | 3 +- .../components/flux_led/manifest.json | 3 +- homeassistant/components/folder/manifest.json | 3 +- .../components/folder_watcher/manifest.json | 3 +- homeassistant/components/foobot/manifest.json | 3 +- .../components/forked_daapd/manifest.json | 3 +- .../components/fortios/manifest.json | 3 +- homeassistant/components/foscam/manifest.json | 3 +- .../components/foursquare/manifest.json | 3 +- .../components/free_mobile/manifest.json | 3 +- .../components/freebox/manifest.json | 3 +- .../components/freedns/manifest.json | 3 +- homeassistant/components/fritz/manifest.json | 3 +- .../components/fritzbox/manifest.json | 3 +- .../fritzbox_callmonitor/manifest.json | 3 +- .../fritzbox_netmonitor/manifest.json | 3 +- .../components/fronius/manifest.json | 3 +- .../components/frontend/manifest.json | 10 +- .../components/frontier_silicon/manifest.json | 3 +- .../components/futurenow/manifest.json | 3 +- .../components/garadget/manifest.json | 3 +- .../components/garmin_connect/manifest.json | 3 +- homeassistant/components/gc100/manifest.json | 5 +- homeassistant/components/gdacs/manifest.json | 3 +- .../components/generic/manifest.json | 3 +- .../generic_thermostat/manifest.json | 3 +- .../components/geniushub/manifest.json | 3 +- .../components/geo_json_events/manifest.json | 3 +- .../components/geo_rss_events/manifest.json | 3 +- .../components/geofency/manifest.json | 3 +- .../components/geonetnz_quakes/manifest.json | 3 +- .../components/geonetnz_volcano/manifest.json | 3 +- homeassistant/components/gios/manifest.json | 5 +- homeassistant/components/github/manifest.json | 3 +- .../components/gitlab_ci/manifest.json | 3 +- homeassistant/components/gitter/manifest.json | 3 +- .../components/glances/manifest.json | 3 +- homeassistant/components/gntp/manifest.json | 3 +- .../components/goalfeed/manifest.json | 3 +- .../components/goalzero/manifest.json | 3 +- .../components/gogogate2/manifest.json | 7 +- homeassistant/components/google/manifest.json | 3 +- .../components/google_assistant/manifest.json | 3 +- .../components/google_cloud/manifest.json | 3 +- .../components/google_domains/manifest.json | 3 +- .../components/google_maps/manifest.json | 3 +- .../components/google_pubsub/manifest.json | 3 +- .../components/google_translate/manifest.json | 3 +- .../google_travel_time/manifest.json | 9 +- .../components/google_wifi/manifest.json | 3 +- homeassistant/components/gpmdp/manifest.json | 3 +- homeassistant/components/gpsd/manifest.json | 3 +- .../components/gpslogger/manifest.json | 3 +- .../components/graphite/manifest.json | 3 +- homeassistant/components/gree/manifest.json | 5 +- .../components/greeneye_monitor/manifest.json | 3 +- .../components/greenwave/manifest.json | 3 +- homeassistant/components/group/manifest.json | 3 +- .../components/growatt_server/manifest.json | 3 +- .../components/gstreamer/manifest.json | 3 +- homeassistant/components/gtfs/manifest.json | 3 +- .../components/guardian/manifest.json | 13 +-- .../components/habitica/manifest.json | 13 ++- .../components/hangouts/manifest.json | 7 +- .../harman_kardon_avr/manifest.json | 3 +- .../components/harmony/manifest.json | 3 +- homeassistant/components/hassio/manifest.json | 3 +- .../components/haveibeenpwned/manifest.json | 3 +- .../components/hddtemp/manifest.json | 3 +- .../components/hdmi_cec/manifest.json | 3 +- .../components/heatmiser/manifest.json | 3 +- homeassistant/components/heos/manifest.json | 3 +- .../components/here_travel_time/manifest.json | 3 +- .../components/hikvision/manifest.json | 3 +- .../components/hikvisioncam/manifest.json | 3 +- .../components/hisense_aehw4a1/manifest.json | 3 +- .../components/history_stats/manifest.json | 3 +- .../components/hitron_coda/manifest.json | 3 +- homeassistant/components/hive/manifest.json | 12 +- .../components/hlk_sw16/manifest.json | 13 +-- .../components/home_connect/manifest.json | 3 +- .../home_plus_control/manifest.json | 13 +-- .../components/homekit/manifest.json | 17 +-- .../homekit_controller/manifest.json | 17 +-- .../components/homematic/manifest.json | 3 +- .../homematicip_cloud/manifest.json | 3 +- .../components/homeworks/manifest.json | 3 +- .../components/honeywell/manifest.json | 3 +- .../components/horizon/manifest.json | 3 +- homeassistant/components/hp_ilo/manifest.json | 3 +- homeassistant/components/html5/manifest.json | 3 +- homeassistant/components/http/manifest.json | 3 +- homeassistant/components/htu21d/manifest.json | 3 +- .../components/huawei_lte/manifest.json | 3 +- .../components/huawei_router/manifest.json | 3 +- homeassistant/components/hue/manifest.json | 3 +- .../components/huisbaasje/manifest.json | 7 +- .../hunterdouglas_powerview/manifest.json | 9 +- .../components/hvv_departures/manifest.json | 3 +- .../components/hydrawise/manifest.json | 3 +- .../components/hyperion/manifest.json | 3 +- homeassistant/components/ialarm/manifest.json | 11 +- .../components/iammeter/manifest.json | 3 +- .../components/iaqualink/manifest.json | 3 +- homeassistant/components/icloud/manifest.json | 3 +- .../components/idteck_prox/manifest.json | 3 +- homeassistant/components/ifttt/manifest.json | 3 +- homeassistant/components/iglo/manifest.json | 3 +- .../components/ign_sismologia/manifest.json | 5 +- homeassistant/components/ihc/manifest.json | 3 +- homeassistant/components/imap/manifest.json | 3 +- .../imap_email_content/manifest.json | 3 +- .../components/incomfort/manifest.json | 3 +- .../components/influxdb/manifest.json | 3 +- .../components/insteon/manifest.json | 5 +- .../components/integration/manifest.json | 3 +- .../components/intesishome/manifest.json | 3 +- homeassistant/components/ios/manifest.json | 3 +- homeassistant/components/iota/manifest.json | 3 +- homeassistant/components/iperf3/manifest.json | 3 +- homeassistant/components/ipma/manifest.json | 5 +- homeassistant/components/ipp/manifest.json | 3 +- homeassistant/components/iqvia/manifest.json | 3 +- .../irish_rail_transport/manifest.json | 3 +- .../islamic_prayer_times/manifest.json | 3 +- homeassistant/components/iss/manifest.json | 3 +- homeassistant/components/isy994/manifest.json | 3 +- homeassistant/components/itach/manifest.json | 5 +- homeassistant/components/itunes/manifest.json | 3 +- homeassistant/components/izone/manifest.json | 7 +- .../components/jewish_calendar/manifest.json | 3 +- .../components/joaoapps_join/manifest.json | 3 +- .../components/juicenet/manifest.json | 3 +- .../components/kaiterra/manifest.json | 3 +- homeassistant/components/kankun/manifest.json | 3 +- homeassistant/components/keba/manifest.json | 3 +- .../components/keenetic_ndms2/manifest.json | 3 +- homeassistant/components/kef/manifest.json | 3 +- .../components/keyboard/manifest.json | 3 +- .../components/keyboard_remote/manifest.json | 3 +- homeassistant/components/kira/manifest.json | 3 +- homeassistant/components/kiwi/manifest.json | 3 +- .../components/kmtronic/manifest.json | 13 ++- homeassistant/components/knx/manifest.json | 3 +- homeassistant/components/kodi/manifest.json | 18 +-- .../components/konnected/manifest.json | 3 +- .../kostal_plenticore/manifest.json | 7 +- .../components/kulersky/manifest.json | 9 +- homeassistant/components/kwb/manifest.json | 3 +- .../components/lacrosse/manifest.json | 3 +- .../components/lametric/manifest.json | 3 +- .../components/lannouncer/manifest.json | 3 +- homeassistant/components/lastfm/manifest.json | 3 +- .../components/launch_library/manifest.json | 3 +- homeassistant/components/lcn/manifest.json | 9 +- .../components/lg_netcast/manifest.json | 3 +- .../components/lg_soundbar/manifest.json | 3 +- .../components/life360/manifest.json | 3 +- homeassistant/components/lifx/manifest.json | 3 +- .../components/lifx_cloud/manifest.json | 3 +- .../components/lifx_legacy/manifest.json | 3 +- .../components/lightwave/manifest.json | 3 +- .../components/limitlessled/manifest.json | 3 +- .../components/linksys_smart/manifest.json | 3 +- homeassistant/components/linode/manifest.json | 3 +- .../components/linux_battery/manifest.json | 3 +- homeassistant/components/lirc/manifest.json | 3 +- .../components/litejet/manifest.json | 3 +- .../components/litterrobot/manifest.json | 3 +- .../llamalab_automate/manifest.json | 3 +- .../components/local_file/manifest.json | 3 +- .../components/local_ip/manifest.json | 3 +- .../components/locative/manifest.json | 3 +- .../components/logentries/manifest.json | 3 +- .../components/logi_circle/manifest.json | 3 +- .../components/london_air/manifest.json | 3 +- .../london_underground/manifest.json | 3 +- .../components/loopenergy/manifest.json | 5 +- homeassistant/components/luci/manifest.json | 3 +- .../components/luftdaten/manifest.json | 3 +- .../components/lupusec/manifest.json | 3 +- homeassistant/components/lutron/manifest.json | 3 +- .../components/lutron_caseta/manifest.json | 9 +- .../components/lw12wifi/manifest.json | 3 +- homeassistant/components/lyft/manifest.json | 3 +- homeassistant/components/lyric/manifest.json | 3 +- .../components/magicseaweed/manifest.json | 3 +- .../components/mailgun/manifest.json | 3 +- homeassistant/components/manual/manifest.json | 3 +- .../components/manual_mqtt/manifest.json | 3 +- .../components/marytts/manifest.json | 3 +- .../components/mastodon/manifest.json | 3 +- homeassistant/components/matrix/manifest.json | 3 +- .../components/maxcube/manifest.json | 3 +- homeassistant/components/mazda/manifest.json | 5 +- .../components/mcp23017/manifest.json | 3 +- .../components/media_extractor/manifest.json | 3 +- .../components/mediaroom/manifest.json | 3 +- .../components/melcloud/manifest.json | 3 +- .../components/melissa/manifest.json | 3 +- homeassistant/components/meraki/manifest.json | 3 +- .../components/message_bird/manifest.json | 3 +- homeassistant/components/met/manifest.json | 3 +- .../components/met_eireann/manifest.json | 13 ++- .../components/meteo_france/manifest.json | 15 +-- .../components/meteoalarm/manifest.json | 3 +- .../components/metoffice/manifest.json | 3 +- homeassistant/components/mfi/manifest.json | 3 +- homeassistant/components/mhz19/manifest.json | 3 +- .../components/microsoft/manifest.json | 3 +- .../components/microsoft_face/manifest.json | 3 +- .../microsoft_face_detect/manifest.json | 3 +- .../microsoft_face_identify/manifest.json | 3 +- .../components/miflora/manifest.json | 3 +- .../components/mikrotik/manifest.json | 3 +- homeassistant/components/mill/manifest.json | 3 +- .../components/min_max/manifest.json | 3 +- .../components/minecraft_server/manifest.json | 3 +- homeassistant/components/minio/manifest.json | 3 +- .../components/mitemp_bt/manifest.json | 3 +- homeassistant/components/mjpeg/manifest.json | 3 +- .../components/mobile_app/manifest.json | 3 +- homeassistant/components/mochad/manifest.json | 3 +- homeassistant/components/modbus/manifest.json | 3 +- .../components/modem_callerid/manifest.json | 3 +- .../components/mold_indicator/manifest.json | 3 +- .../components/monoprice/manifest.json | 3 +- homeassistant/components/moon/manifest.json | 3 +- .../components/motion_blinds/manifest.json | 3 +- homeassistant/components/mpchc/manifest.json | 3 +- homeassistant/components/mpd/manifest.json | 3 +- homeassistant/components/mqtt/manifest.json | 3 +- .../components/mqtt_eventstream/manifest.json | 3 +- .../components/mqtt_json/manifest.json | 3 +- .../components/mqtt_room/manifest.json | 3 +- .../components/mqtt_statestream/manifest.json | 3 +- .../components/msteams/manifest.json | 3 +- .../components/mullvad/manifest.json | 9 +- .../components/mvglive/manifest.json | 3 +- .../components/mychevy/manifest.json | 3 +- .../components/mycroft/manifest.json | 3 +- homeassistant/components/myq/manifest.json | 3 +- .../components/mysensors/manifest.json | 3 +- .../components/mystrom/manifest.json | 3 +- .../components/mythicbeastsdns/manifest.json | 3 +- homeassistant/components/n26/manifest.json | 3 +- homeassistant/components/nad/manifest.json | 3 +- .../components/namecheapdns/manifest.json | 3 +- .../components/nanoleaf/manifest.json | 3 +- homeassistant/components/neato/manifest.json | 16 +-- .../nederlandse_spoorwegen/manifest.json | 3 +- homeassistant/components/nello/manifest.json | 3 +- .../components/ness_alarm/manifest.json | 3 +- homeassistant/components/nest/manifest.json | 7 +- .../components/netatmo/manifest.json | 29 ++--- .../components/netdata/manifest.json | 3 +- .../components/netgear/manifest.json | 3 +- .../components/netgear_lte/manifest.json | 3 +- homeassistant/components/netio/manifest.json | 3 +- .../components/neurio_energy/manifest.json | 3 +- homeassistant/components/nexia/manifest.json | 8 +- .../components/nextbus/manifest.json | 3 +- .../components/nextcloud/manifest.json | 3 +- .../components/nfandroidtv/manifest.json | 3 +- .../components/nightscout/manifest.json | 13 +-- .../niko_home_control/manifest.json | 3 +- homeassistant/components/nilu/manifest.json | 3 +- .../components/nissan_leaf/manifest.json | 3 +- .../components/nmap_tracker/manifest.json | 3 +- homeassistant/components/nmbs/manifest.json | 3 +- homeassistant/components/no_ip/manifest.json | 3 +- .../components/noaa_tides/manifest.json | 3 +- .../components/norway_air/manifest.json | 3 +- .../components/notify_events/manifest.json | 3 +- homeassistant/components/notion/manifest.json | 3 +- .../components/nsw_fuel_station/manifest.json | 3 +- .../nsw_rural_fire_service_feed/manifest.json | 3 +- homeassistant/components/nuheat/manifest.json | 8 +- homeassistant/components/nuki/manifest.json | 21 ++-- homeassistant/components/numato/manifest.json | 3 +- homeassistant/components/nut/manifest.json | 3 +- homeassistant/components/nws/manifest.json | 3 +- homeassistant/components/nx584/manifest.json | 3 +- homeassistant/components/nzbget/manifest.json | 3 +- .../components/oasa_telematics/manifest.json | 3 +- homeassistant/components/obihai/manifest.json | 3 +- .../components/octoprint/manifest.json | 3 +- homeassistant/components/oem/manifest.json | 3 +- .../components/ohmconnect/manifest.json | 3 +- homeassistant/components/ombi/manifest.json | 3 +- .../components/omnilogic/manifest.json | 3 +- .../components/onboarding/manifest.json | 15 +-- .../components/ondilo_ico/manifest.json | 15 +-- .../components/onewire/manifest.json | 3 +- homeassistant/components/onkyo/manifest.json | 3 +- homeassistant/components/onvif/manifest.json | 3 +- .../components/openalpr_cloud/manifest.json | 3 +- .../components/openalpr_local/manifest.json | 3 +- homeassistant/components/opencv/manifest.json | 3 +- .../components/openerz/manifest.json | 3 +- .../components/openevse/manifest.json | 3 +- .../openexchangerates/manifest.json | 3 +- .../components/opengarage/manifest.json | 7 +- .../openhardwaremonitor/manifest.json | 3 +- .../components/openhome/manifest.json | 3 +- .../components/opensensemap/manifest.json | 3 +- .../components/opensky/manifest.json | 3 +- .../components/opentherm_gw/manifest.json | 3 +- homeassistant/components/openuv/manifest.json | 3 +- .../components/openweathermap/manifest.json | 3 +- .../components/opnsense/manifest.json | 3 +- homeassistant/components/opple/manifest.json | 3 +- .../components/orangepi_gpio/manifest.json | 3 +- homeassistant/components/oru/manifest.json | 3 +- homeassistant/components/orvibo/manifest.json | 3 +- .../components/osramlightify/manifest.json | 3 +- homeassistant/components/otp/manifest.json | 3 +- .../components/ovo_energy/manifest.json | 3 +- .../components/owntracks/manifest.json | 3 +- homeassistant/components/ozw/manifest.json | 15 +-- .../components/panasonic_bluray/manifest.json | 3 +- .../components/panasonic_viera/manifest.json | 3 +- .../components/pandora/manifest.json | 3 +- .../components/pcal9535a/manifest.json | 3 +- homeassistant/components/pencom/manifest.json | 3 +- .../persistent_notification/manifest.json | 3 +- homeassistant/components/person/manifest.json | 3 +- .../components/philips_js/manifest.json | 13 +-- .../components/pi4ioe5v9xxxx/manifest.json | 11 +- .../components/pi_hole/manifest.json | 3 +- .../components/picotts/manifest.json | 3 +- homeassistant/components/piglow/manifest.json | 3 +- .../components/pilight/manifest.json | 3 +- homeassistant/components/ping/manifest.json | 3 +- .../components/pioneer/manifest.json | 3 +- homeassistant/components/pjlink/manifest.json | 3 +- homeassistant/components/plaato/manifest.json | 3 +- homeassistant/components/plex/manifest.json | 9 +- .../components/plugwise/manifest.json | 3 +- .../components/plum_lightpad/manifest.json | 12 +- .../components/pocketcasts/manifest.json | 3 +- homeassistant/components/point/manifest.json | 3 +- .../components/poolsense/manifest.json | 9 +- .../components/powerwall/manifest.json | 13 ++- .../components/progettihwsw/manifest.json | 13 +-- .../components/proliphix/manifest.json | 3 +- .../components/prometheus/manifest.json | 3 +- homeassistant/components/prowl/manifest.json | 3 +- .../components/proximity/manifest.json | 3 +- .../components/proxmoxve/manifest.json | 3 +- homeassistant/components/ps4/manifest.json | 3 +- .../pulseaudio_loopback/manifest.json | 3 +- homeassistant/components/push/manifest.json | 3 +- .../components/pushbullet/manifest.json | 3 +- .../components/pushover/manifest.json | 3 +- .../components/pushsafer/manifest.json | 3 +- .../components/pvoutput/manifest.json | 3 +- .../pvpc_hourly_pricing/manifest.json | 3 +- homeassistant/components/pyload/manifest.json | 3 +- .../components/qbittorrent/manifest.json | 3 +- .../components/qld_bushfire/manifest.json | 3 +- homeassistant/components/qnap/manifest.json | 3 +- homeassistant/components/qrcode/manifest.json | 3 +- .../components/quantum_gateway/manifest.json | 3 +- .../components/qvr_pro/manifest.json | 3 +- .../components/qwikswitch/manifest.json | 3 +- homeassistant/components/rachio/manifest.json | 29 ++--- homeassistant/components/radarr/manifest.json | 3 +- .../components/radiotherm/manifest.json | 3 +- .../components/rainbird/manifest.json | 3 +- .../components/raincloud/manifest.json | 3 +- .../components/rainforest_eagle/manifest.json | 3 +- .../components/rainmachine/manifest.json | 3 +- homeassistant/components/random/manifest.json | 3 +- .../components/raspihats/manifest.json | 3 +- .../components/raspyrfm/manifest.json | 3 +- .../components/recollect_waste/manifest.json | 9 +- .../components/recorder/manifest.json | 3 +- .../components/recswitch/manifest.json | 3 +- homeassistant/components/reddit/manifest.json | 3 +- .../components/rejseplanen/manifest.json | 3 +- .../remember_the_milk/manifest.json | 3 +- .../components/remote_rpi_gpio/manifest.json | 3 +- .../components/repetier/manifest.json | 3 +- homeassistant/components/rest/manifest.json | 3 +- .../components/rest_command/manifest.json | 3 +- homeassistant/components/rflink/manifest.json | 5 +- homeassistant/components/rfxtrx/manifest.json | 3 +- homeassistant/components/ring/manifest.json | 8 +- homeassistant/components/ripple/manifest.json | 3 +- homeassistant/components/risco/manifest.json | 13 +-- .../rituals_perfume_genie/manifest.json | 9 +- .../components/rmvtransport/manifest.json | 11 +- .../components/rocketchat/manifest.json | 3 +- homeassistant/components/roku/manifest.json | 11 +- homeassistant/components/roomba/manifest.json | 20 ++-- homeassistant/components/roon/manifest.json | 9 +- .../components/route53/manifest.json | 3 +- homeassistant/components/rova/manifest.json | 3 +- .../components/rpi_camera/manifest.json | 3 +- .../components/rpi_gpio/manifest.json | 3 +- .../components/rpi_gpio_pwm/manifest.json | 3 +- .../components/rpi_pfio/manifest.json | 3 +- .../components/rpi_power/manifest.json | 12 +- homeassistant/components/rpi_rf/manifest.json | 3 +- .../rss_feed_template/manifest.json | 3 +- .../components/rtorrent/manifest.json | 3 +- .../components/ruckus_unleashed/manifest.json | 9 +- .../components/russound_rio/manifest.json | 3 +- .../components/russound_rnet/manifest.json | 3 +- .../components/sabnzbd/manifest.json | 3 +- homeassistant/components/saj/manifest.json | 3 +- .../components/samsungtv/manifest.json | 12 +- .../components/satel_integra/manifest.json | 3 +- .../components/schluter/manifest.json | 3 +- homeassistant/components/scrape/manifest.json | 3 +- .../components/screenlogic/manifest.json | 12 +- homeassistant/components/script/manifest.json | 4 +- .../components/scsgate/manifest.json | 3 +- homeassistant/components/season/manifest.json | 3 +- .../components/sendgrid/manifest.json | 3 +- homeassistant/components/sense/manifest.json | 12 +- .../components/sensehat/manifest.json | 3 +- .../components/sensibo/manifest.json | 3 +- homeassistant/components/sentry/manifest.json | 3 +- homeassistant/components/serial/manifest.json | 3 +- .../components/serial_pm/manifest.json | 3 +- homeassistant/components/sesame/manifest.json | 3 +- .../components/seven_segments/manifest.json | 3 +- .../components/seventeentrack/manifest.json | 3 +- .../components/sharkiq/manifest.json | 3 +- .../components/shell_command/manifest.json | 3 +- homeassistant/components/shelly/manifest.json | 10 +- homeassistant/components/shiftr/manifest.json | 3 +- homeassistant/components/shodan/manifest.json | 3 +- .../components/shopping_list/manifest.json | 3 +- homeassistant/components/sht31/manifest.json | 3 +- homeassistant/components/sigfox/manifest.json | 3 +- .../components/sighthound/manifest.json | 3 +- .../components/signal_messenger/manifest.json | 3 +- .../components/simplepush/manifest.json | 3 +- .../components/simplisafe/manifest.json | 3 +- .../components/simulated/manifest.json | 3 +- homeassistant/components/sinch/manifest.json | 3 +- .../components/sisyphus/manifest.json | 11 +- .../components/sky_hub/manifest.json | 3 +- .../components/skybeacon/manifest.json | 3 +- .../components/skybell/manifest.json | 3 +- homeassistant/components/slack/manifest.json | 3 +- .../components/sleepiq/manifest.json | 3 +- homeassistant/components/slide/manifest.json | 3 +- homeassistant/components/sma/manifest.json | 3 +- .../components/smappee/manifest.json | 21 ++-- .../smart_meter_texas/manifest.json | 3 +- .../components/smarthab/manifest.json | 3 +- .../components/smartthings/manifest.json | 3 +- .../components/smarttub/manifest.json | 7 +- homeassistant/components/smarty/manifest.json | 3 +- homeassistant/components/smhi/manifest.json | 3 +- homeassistant/components/sms/manifest.json | 3 +- homeassistant/components/smtp/manifest.json | 3 +- .../components/snapcast/manifest.json | 3 +- homeassistant/components/snips/manifest.json | 3 +- homeassistant/components/snmp/manifest.json | 3 +- .../components/sochain/manifest.json | 3 +- .../components/solaredge/manifest.json | 8 +- .../components/solaredge_local/manifest.json | 3 +- .../components/solarlog/manifest.json | 3 +- homeassistant/components/solax/manifest.json | 3 +- homeassistant/components/soma/manifest.json | 3 +- homeassistant/components/somfy/manifest.json | 8 +- .../components/somfy_mylink/manifest.json | 14 ++- homeassistant/components/sonarr/manifest.json | 3 +- .../components/songpal/manifest.json | 3 +- homeassistant/components/sonos/manifest.json | 5 +- .../components/sony_projector/manifest.json | 3 +- .../components/soundtouch/manifest.json | 3 +- .../components/spaceapi/manifest.json | 3 +- homeassistant/components/spc/manifest.json | 3 +- .../components/speedtestdotnet/manifest.json | 7 +- homeassistant/components/spider/manifest.json | 11 +- homeassistant/components/splunk/manifest.json | 11 +- .../components/spotcrime/manifest.json | 3 +- .../components/spotify/manifest.json | 3 +- homeassistant/components/sql/manifest.json | 3 +- .../components/squeezebox/manifest.json | 16 +-- .../components/srp_energy/manifest.json | 11 +- homeassistant/components/ssdp/manifest.json | 9 +- .../components/starline/manifest.json | 3 +- .../components/starlingbank/manifest.json | 3 +- .../components/startca/manifest.json | 3 +- .../components/statistics/manifest.json | 3 +- homeassistant/components/statsd/manifest.json | 3 +- .../components/steam_online/manifest.json | 3 +- .../components/stiebel_eltron/manifest.json | 3 +- .../components/stookalert/manifest.json | 3 +- homeassistant/components/stream/manifest.json | 3 +- .../components/streamlabswater/manifest.json | 3 +- homeassistant/components/subaru/manifest.json | 3 +- .../components/suez_water/manifest.json | 11 +- homeassistant/components/sun/manifest.json | 3 +- .../components/supervisord/manifest.json | 3 +- homeassistant/components/supla/manifest.json | 3 +- .../components/surepetcare/manifest.json | 3 +- .../swiss_hydrological_data/manifest.json | 3 +- .../swiss_public_transport/manifest.json | 3 +- .../components/swisscom/manifest.json | 3 +- .../components/switchbot/manifest.json | 3 +- .../components/switcher_kis/manifest.json | 3 +- .../components/switchmate/manifest.json | 3 +- .../components/syncthru/manifest.json | 3 +- .../components/synology_chat/manifest.json | 3 +- .../components/synology_dsm/manifest.json | 3 +- .../components/synology_srm/manifest.json | 3 +- homeassistant/components/syslog/manifest.json | 3 +- .../components/systemmonitor/manifest.json | 3 +- homeassistant/components/tado/manifest.json | 3 +- homeassistant/components/tahoma/manifest.json | 3 +- .../components/tank_utility/manifest.json | 3 +- .../components/tankerkoenig/manifest.json | 3 +- .../components/tapsaff/manifest.json | 3 +- .../components/tasmota/manifest.json | 3 +- .../components/tautulli/manifest.json | 3 +- homeassistant/components/tcp/manifest.json | 3 +- .../components/ted5000/manifest.json | 3 +- .../components/telegram/manifest.json | 3 +- .../components/telegram_bot/manifest.json | 3 +- .../components/tellduslive/manifest.json | 3 +- .../components/tellstick/manifest.json | 3 +- homeassistant/components/telnet/manifest.json | 3 +- homeassistant/components/temper/manifest.json | 3 +- .../components/template/manifest.json | 3 +- .../components/tensorflow/manifest.json | 3 +- homeassistant/components/tesla/manifest.json | 18 ++- homeassistant/components/tfiac/manifest.json | 3 +- .../thermoworks_smoke/manifest.json | 3 +- .../components/thethingsnetwork/manifest.json | 3 +- .../components/thingspeak/manifest.json | 3 +- .../components/thinkingcleaner/manifest.json | 3 +- .../components/thomson/manifest.json | 3 +- .../components/threshold/manifest.json | 3 +- homeassistant/components/tibber/manifest.json | 3 +- .../components/tikteck/manifest.json | 3 +- homeassistant/components/tile/manifest.json | 3 +- .../components/time_date/manifest.json | 3 +- homeassistant/components/tmb/manifest.json | 3 +- homeassistant/components/tod/manifest.json | 3 +- .../components/todoist/manifest.json | 3 +- homeassistant/components/tof/manifest.json | 3 +- homeassistant/components/tomato/manifest.json | 3 +- homeassistant/components/toon/manifest.json | 8 +- homeassistant/components/torque/manifest.json | 3 +- .../components/totalconnect/manifest.json | 3 +- .../components/touchline/manifest.json | 3 +- homeassistant/components/tplink/manifest.json | 10 +- .../components/tplink_lte/manifest.json | 3 +- .../components/traccar/manifest.json | 3 +- homeassistant/components/trackr/manifest.json | 3 +- .../components/tradfri/manifest.json | 5 +- .../trafikverket_train/manifest.json | 3 +- .../trafikverket_weatherstation/manifest.json | 3 +- .../components/transmission/manifest.json | 3 +- .../components/transport_nsw/manifest.json | 3 +- .../components/travisci/manifest.json | 3 +- homeassistant/components/trend/manifest.json | 3 +- homeassistant/components/tuya/manifest.json | 3 +- .../components/twentemilieu/manifest.json | 3 +- homeassistant/components/twilio/manifest.json | 3 +- .../components/twilio_call/manifest.json | 3 +- .../components/twilio_sms/manifest.json | 3 +- .../components/twinkly/manifest.json | 3 +- homeassistant/components/twitch/manifest.json | 3 +- .../components/twitter/manifest.json | 3 +- homeassistant/components/ubus/manifest.json | 3 +- .../components/ue_smart_radio/manifest.json | 3 +- .../components/uk_transport/manifest.json | 3 +- homeassistant/components/unifi/manifest.json | 3 +- .../components/unifi_direct/manifest.json | 3 +- .../components/unifiled/manifest.json | 3 +- .../components/universal/manifest.json | 3 +- homeassistant/components/upb/manifest.json | 3 +- .../components/upc_connect/manifest.json | 3 +- .../components/upcloud/manifest.json | 3 +- .../components/updater/manifest.json | 3 +- homeassistant/components/upnp/manifest.json | 3 +- homeassistant/components/uptime/manifest.json | 3 +- .../components/uptimerobot/manifest.json | 3 +- homeassistant/components/uscis/manifest.json | 3 +- .../usgs_earthquakes_feed/manifest.json | 3 +- .../components/utility_meter/manifest.json | 3 +- homeassistant/components/uvc/manifest.json | 3 +- homeassistant/components/vallox/manifest.json | 3 +- .../components/vasttrafik/manifest.json | 5 +- homeassistant/components/velbus/manifest.json | 3 +- homeassistant/components/velux/manifest.json | 3 +- .../components/venstar/manifest.json | 3 +- homeassistant/components/vera/manifest.json | 3 +- .../components/verisure/manifest.json | 7 +- .../components/versasense/manifest.json | 3 +- .../components/version/manifest.json | 3 +- homeassistant/components/vesync/manifest.json | 13 +-- .../components/viaggiatreno/manifest.json | 3 +- homeassistant/components/vicare/manifest.json | 3 +- homeassistant/components/vilfo/manifest.json | 3 +- .../components/vivotek/manifest.json | 3 +- homeassistant/components/vizio/manifest.json | 3 +- homeassistant/components/vlc/manifest.json | 3 +- .../components/vlc_telnet/manifest.json | 3 +- .../components/voicerss/manifest.json | 3 +- .../components/volkszaehler/manifest.json | 3 +- .../components/volumio/manifest.json | 5 +- .../components/volvooncall/manifest.json | 3 +- homeassistant/components/vultr/manifest.json | 3 +- .../components/w800rf32/manifest.json | 3 +- .../components/wake_on_lan/manifest.json | 3 +- homeassistant/components/waqi/manifest.json | 3 +- .../components/waterfurnace/manifest.json | 3 +- .../components/watson_iot/manifest.json | 3 +- .../components/watson_tts/manifest.json | 3 +- .../components/waze_travel_time/manifest.json | 7 +- .../components/webostv/manifest.json | 3 +- homeassistant/components/wemo/manifest.json | 3 +- homeassistant/components/whois/manifest.json | 3 +- homeassistant/components/wiffi/manifest.json | 5 +- .../components/wilight/manifest.json | 3 +- homeassistant/components/wink/manifest.json | 3 +- .../components/wirelesstag/manifest.json | 3 +- .../components/withings/manifest.json | 3 +- homeassistant/components/wled/manifest.json | 3 +- .../components/wolflink/manifest.json | 3 +- .../components/workday/manifest.json | 3 +- .../components/worldclock/manifest.json | 3 +- .../components/worldtidesinfo/manifest.json | 3 +- .../components/worxlandroid/manifest.json | 3 +- homeassistant/components/wsdot/manifest.json | 3 +- .../components/wunderground/manifest.json | 3 +- homeassistant/components/x10/manifest.json | 3 +- homeassistant/components/xbee/manifest.json | 3 +- homeassistant/components/xbox/manifest.json | 3 +- .../components/xbox_live/manifest.json | 3 +- homeassistant/components/xeoma/manifest.json | 3 +- homeassistant/components/xiaomi/manifest.json | 3 +- .../components/xiaomi_aqara/manifest.json | 3 +- .../components/xiaomi_miio/manifest.json | 3 +- .../components/xiaomi_tv/manifest.json | 3 +- homeassistant/components/xmpp/manifest.json | 3 +- homeassistant/components/xs1/manifest.json | 3 +- .../components/yale_smart_alarm/manifest.json | 3 +- homeassistant/components/yamaha/manifest.json | 3 +- .../components/yamaha_musiccast/manifest.json | 3 +- .../components/yandex_transport/manifest.json | 3 +- .../components/yandextts/manifest.json | 3 +- .../components/yeelight/manifest.json | 15 +-- .../yeelightsunflower/manifest.json | 3 +- homeassistant/components/yi/manifest.json | 3 +- homeassistant/components/zabbix/manifest.json | 3 +- homeassistant/components/zamg/manifest.json | 5 +- homeassistant/components/zengge/manifest.json | 3 +- .../components/zeroconf/manifest.json | 3 +- .../components/zerproc/manifest.json | 9 +- .../components/zestimate/manifest.json | 3 +- homeassistant/components/zha/manifest.json | 10 +- .../components/zhong_hong/manifest.json | 3 +- .../ziggo_mediabox_xl/manifest.json | 3 +- homeassistant/components/zodiac/manifest.json | 3 +- .../components/zoneminder/manifest.json | 3 +- homeassistant/components/zwave/manifest.json | 3 +- .../components/zwave_js/manifest.json | 3 +- homeassistant/loader.py | 6 + script/hassfest/manifest.py | 106 +++++++++++++++++- script/hassfest/model.py | 2 +- 917 files changed, 2327 insertions(+), 1467 deletions(-) diff --git a/homeassistant/components/abode/manifest.json b/homeassistant/components/abode/manifest.json index b7c962dac380a..c9353c31bab32 100644 --- a/homeassistant/components/abode/manifest.json +++ b/homeassistant/components/abode/manifest.json @@ -7,5 +7,6 @@ "codeowners": ["@shred86"], "homekit": { "models": ["Abode", "Iota"] - } + }, + "iot_class": "cloud_push" } diff --git a/homeassistant/components/accuweather/manifest.json b/homeassistant/components/accuweather/manifest.json index fd91f62ae33a6..068b0fc83a9a2 100644 --- a/homeassistant/components/accuweather/manifest.json +++ b/homeassistant/components/accuweather/manifest.json @@ -5,5 +5,6 @@ "requirements": ["accuweather==0.1.1"], "codeowners": ["@bieniu"], "config_flow": true, - "quality_scale": "platinum" + "quality_scale": "platinum", + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/acer_projector/manifest.json b/homeassistant/components/acer_projector/manifest.json index 096d2c6e24dfc..1120b5c93d071 100644 --- a/homeassistant/components/acer_projector/manifest.json +++ b/homeassistant/components/acer_projector/manifest.json @@ -3,5 +3,6 @@ "name": "Acer Projector", "documentation": "https://www.home-assistant.io/integrations/acer_projector", "requirements": ["pyserial==3.5"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/acmeda/manifest.json b/homeassistant/components/acmeda/manifest.json index f1858f9fd5a12..ae72df5a32329 100644 --- a/homeassistant/components/acmeda/manifest.json +++ b/homeassistant/components/acmeda/manifest.json @@ -4,7 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/acmeda", "requirements": ["aiopulse==0.4.2"], - "codeowners": [ - "@atmurray" - ] -} \ No newline at end of file + "codeowners": ["@atmurray"], + "iot_class": "local_push" +} diff --git a/homeassistant/components/actiontec/manifest.json b/homeassistant/components/actiontec/manifest.json index 8a3f2f3f96a83..a257391962989 100644 --- a/homeassistant/components/actiontec/manifest.json +++ b/homeassistant/components/actiontec/manifest.json @@ -2,5 +2,6 @@ "domain": "actiontec", "name": "Actiontec", "documentation": "https://www.home-assistant.io/integrations/actiontec", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/adguard/manifest.json b/homeassistant/components/adguard/manifest.json index dd23e56136403..bd311dd3d3574 100644 --- a/homeassistant/components/adguard/manifest.json +++ b/homeassistant/components/adguard/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/adguard", "requirements": ["adguardhome==0.5.0"], - "codeowners": ["@frenck"] + "codeowners": ["@frenck"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/ads/manifest.json b/homeassistant/components/ads/manifest.json index cee2419b4fe1c..9e4f838440460 100644 --- a/homeassistant/components/ads/manifest.json +++ b/homeassistant/components/ads/manifest.json @@ -3,5 +3,6 @@ "name": "ADS", "documentation": "https://www.home-assistant.io/integrations/ads", "requirements": ["pyads==3.2.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/advantage_air/manifest.json b/homeassistant/components/advantage_air/manifest.json index 87655d61be4d4..750d5457e17ab 100644 --- a/homeassistant/components/advantage_air/manifest.json +++ b/homeassistant/components/advantage_air/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/advantage_air", "codeowners": ["@Bre77"], "requirements": ["advantage_air==0.2.1"], - "quality_scale": "platinum" + "quality_scale": "platinum", + "iot_class": "local_polling" } diff --git a/homeassistant/components/aemet/manifest.json b/homeassistant/components/aemet/manifest.json index eb5dc295f297e..26f9139aa9e57 100644 --- a/homeassistant/components/aemet/manifest.json +++ b/homeassistant/components/aemet/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/aemet", "requirements": ["AEMET-OpenData==0.1.8"], - "codeowners": ["@noltari"] + "codeowners": ["@noltari"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/aftership/manifest.json b/homeassistant/components/aftership/manifest.json index 335befa937b24..5308d08be50b9 100644 --- a/homeassistant/components/aftership/manifest.json +++ b/homeassistant/components/aftership/manifest.json @@ -3,5 +3,6 @@ "name": "AfterShip", "documentation": "https://www.home-assistant.io/integrations/aftership", "requirements": ["pyaftership==0.1.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/agent_dvr/manifest.json b/homeassistant/components/agent_dvr/manifest.json index 0690dfedec375..7d740bbe7310d 100644 --- a/homeassistant/components/agent_dvr/manifest.json +++ b/homeassistant/components/agent_dvr/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/agent_dvr/", "requirements": ["agent-py==0.0.23"], "config_flow": true, - "codeowners": ["@ispysoftware"] + "codeowners": ["@ispysoftware"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/airly/manifest.json b/homeassistant/components/airly/manifest.json index a5ff485d1d0aa..430e51c6e9e07 100644 --- a/homeassistant/components/airly/manifest.json +++ b/homeassistant/components/airly/manifest.json @@ -5,5 +5,6 @@ "codeowners": ["@bieniu"], "requirements": ["airly==1.1.0"], "config_flow": true, - "quality_scale": "platinum" + "quality_scale": "platinum", + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/airnow/manifest.json b/homeassistant/components/airnow/manifest.json index fee89ae4fff20..d4e7bc71937b9 100644 --- a/homeassistant/components/airnow/manifest.json +++ b/homeassistant/components/airnow/manifest.json @@ -3,10 +3,7 @@ "name": "AirNow", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/airnow", - "requirements": [ - "pyairnow==1.1.0" - ], - "codeowners": [ - "@asymworks" - ] + "requirements": ["pyairnow==1.1.0"], + "codeowners": ["@asymworks"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/airvisual/manifest.json b/homeassistant/components/airvisual/manifest.json index 351c72511022b..db77716bf418e 100644 --- a/homeassistant/components/airvisual/manifest.json +++ b/homeassistant/components/airvisual/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/airvisual", "requirements": ["pyairvisual==5.0.4"], - "codeowners": ["@bachya"] + "codeowners": ["@bachya"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index 2eb72f6bd35d4..b2cc5f6d32c73 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -3,5 +3,6 @@ "name": "Aladdin Connect", "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", "requirements": ["aladdin_connect==0.3"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/alarmdecoder/manifest.json b/homeassistant/components/alarmdecoder/manifest.json index c3e72e407c262..fa2bcca389f82 100644 --- a/homeassistant/components/alarmdecoder/manifest.json +++ b/homeassistant/components/alarmdecoder/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/alarmdecoder", "requirements": ["adext==0.4.1"], "codeowners": ["@ajschmidt8"], - "config_flow": true + "config_flow": true, + "iot_class": "local_push" } diff --git a/homeassistant/components/alert/manifest.json b/homeassistant/components/alert/manifest.json index ff1faf3982780..f5d3e08f2fe68 100644 --- a/homeassistant/components/alert/manifest.json +++ b/homeassistant/components/alert/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/alert", "after_dependencies": ["notify"], "codeowners": [], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_push" } diff --git a/homeassistant/components/alexa/manifest.json b/homeassistant/components/alexa/manifest.json index 1ed91866cdc3a..486079b03134e 100644 --- a/homeassistant/components/alexa/manifest.json +++ b/homeassistant/components/alexa/manifest.json @@ -2,14 +2,8 @@ "domain": "alexa", "name": "Amazon Alexa", "documentation": "https://www.home-assistant.io/integrations/alexa", - "dependencies": [ - "http" - ], - "after_dependencies": [ - "camera" - ], - "codeowners": [ - "@home-assistant/cloud", - "@ochlocracy" - ] + "dependencies": ["http"], + "after_dependencies": ["camera"], + "codeowners": ["@home-assistant/cloud", "@ochlocracy"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/almond/manifest.json b/homeassistant/components/almond/manifest.json index 44404b504f6a0..cd045f25715c1 100644 --- a/homeassistant/components/almond/manifest.json +++ b/homeassistant/components/almond/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/almond", "dependencies": ["http", "conversation"], "codeowners": ["@gcampax", "@balloob"], - "requirements": ["pyalmond==0.0.2"] + "requirements": ["pyalmond==0.0.2"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/alpha_vantage/manifest.json b/homeassistant/components/alpha_vantage/manifest.json index 5ff3122668d05..bfa41b3eeb15c 100644 --- a/homeassistant/components/alpha_vantage/manifest.json +++ b/homeassistant/components/alpha_vantage/manifest.json @@ -3,5 +3,6 @@ "name": "Alpha Vantage", "documentation": "https://www.home-assistant.io/integrations/alpha_vantage", "requirements": ["alpha_vantage==2.3.1"], - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/amazon_polly/manifest.json b/homeassistant/components/amazon_polly/manifest.json index 6b8a1894f5028..779e320b0ab57 100644 --- a/homeassistant/components/amazon_polly/manifest.json +++ b/homeassistant/components/amazon_polly/manifest.json @@ -3,5 +3,6 @@ "name": "Amazon Polly", "documentation": "https://www.home-assistant.io/integrations/amazon_polly", "requirements": ["boto3==1.16.52"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/ambiclimate/manifest.json b/homeassistant/components/ambiclimate/manifest.json index 151b761dff865..9441cdb86bc30 100644 --- a/homeassistant/components/ambiclimate/manifest.json +++ b/homeassistant/components/ambiclimate/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/ambiclimate", "requirements": ["ambiclimate==0.2.1"], "dependencies": ["http"], - "codeowners": ["@danielhiversen"] + "codeowners": ["@danielhiversen"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/ambient_station/manifest.json b/homeassistant/components/ambient_station/manifest.json index 51f6703ba5ccd..6d4c40d260dc7 100644 --- a/homeassistant/components/ambient_station/manifest.json +++ b/homeassistant/components/ambient_station/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ambient_station", "requirements": ["aioambient==1.2.4"], - "codeowners": ["@bachya"] + "codeowners": ["@bachya"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/amcrest/manifest.json b/homeassistant/components/amcrest/manifest.json index c4d719d3166b1..702e6a614874e 100644 --- a/homeassistant/components/amcrest/manifest.json +++ b/homeassistant/components/amcrest/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/amcrest", "requirements": ["amcrest==1.7.2"], "dependencies": ["ffmpeg"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/ampio/manifest.json b/homeassistant/components/ampio/manifest.json index c92837d2417a5..b47f84f2fe516 100644 --- a/homeassistant/components/ampio/manifest.json +++ b/homeassistant/components/ampio/manifest.json @@ -3,5 +3,6 @@ "name": "Ampio Smart Smog System", "documentation": "https://www.home-assistant.io/integrations/ampio", "requirements": ["asmog==0.0.6"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/analytics/manifest.json b/homeassistant/components/analytics/manifest.json index db795501fa666..49edf1bcf8c3c 100644 --- a/homeassistant/components/analytics/manifest.json +++ b/homeassistant/components/analytics/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/analytics", "codeowners": ["@home-assistant/core", "@ludeeus"], "dependencies": ["api", "websocket_api"], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "cloud_push" } diff --git a/homeassistant/components/android_ip_webcam/manifest.json b/homeassistant/components/android_ip_webcam/manifest.json index 60fe72040341f..637a773ac33bf 100644 --- a/homeassistant/components/android_ip_webcam/manifest.json +++ b/homeassistant/components/android_ip_webcam/manifest.json @@ -3,5 +3,6 @@ "name": "Android IP Webcam", "documentation": "https://www.home-assistant.io/integrations/android_ip_webcam", "requirements": ["pydroid-ipcam==0.8"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index 4612c220c7db4..b86a6d9e40a89 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -7,5 +7,6 @@ "androidtv[async]==0.0.58", "pure-python-adb[async]==0.3.0.dev0" ], - "codeowners": ["@JeffLIrion"] + "codeowners": ["@JeffLIrion"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/anel_pwrctrl/manifest.json b/homeassistant/components/anel_pwrctrl/manifest.json index 891b485bd97f4..926549f768d82 100644 --- a/homeassistant/components/anel_pwrctrl/manifest.json +++ b/homeassistant/components/anel_pwrctrl/manifest.json @@ -3,5 +3,6 @@ "name": "Anel NET-PwrCtrl", "documentation": "https://www.home-assistant.io/integrations/anel_pwrctrl", "requirements": ["anel_pwrctrl-homeassistant==0.0.1.dev2"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/anthemav/manifest.json b/homeassistant/components/anthemav/manifest.json index db9d8c7d3b9e4..3e11675fa1f86 100644 --- a/homeassistant/components/anthemav/manifest.json +++ b/homeassistant/components/anthemav/manifest.json @@ -3,5 +3,6 @@ "name": "Anthem A/V Receivers", "documentation": "https://www.home-assistant.io/integrations/anthemav", "requirements": ["anthemav==1.1.10"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/apache_kafka/manifest.json b/homeassistant/components/apache_kafka/manifest.json index 259082c84c785..688c7c9fb3d7a 100644 --- a/homeassistant/components/apache_kafka/manifest.json +++ b/homeassistant/components/apache_kafka/manifest.json @@ -3,5 +3,6 @@ "name": "Apache Kafka", "documentation": "https://www.home-assistant.io/integrations/apache_kafka", "requirements": ["aiokafka==0.6.0"], - "codeowners": ["@bachya"] + "codeowners": ["@bachya"], + "iot_class": "local_push" } diff --git a/homeassistant/components/apcupsd/manifest.json b/homeassistant/components/apcupsd/manifest.json index 643f42b420116..ac9352bae449b 100644 --- a/homeassistant/components/apcupsd/manifest.json +++ b/homeassistant/components/apcupsd/manifest.json @@ -3,5 +3,6 @@ "name": "apcupsd", "documentation": "https://www.home-assistant.io/integrations/apcupsd", "requirements": ["apcaccess==0.0.13"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/apns/manifest.json b/homeassistant/components/apns/manifest.json index 0d3639040f72a..73136a2ff2982 100644 --- a/homeassistant/components/apns/manifest.json +++ b/homeassistant/components/apns/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/apns", "requirements": ["apns2==0.3.0"], "after_dependencies": ["device_tracker"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/apple_tv/manifest.json b/homeassistant/components/apple_tv/manifest.json index a60c5db3a0630..963cbb9be33fa 100644 --- a/homeassistant/components/apple_tv/manifest.json +++ b/homeassistant/components/apple_tv/manifest.json @@ -3,15 +3,9 @@ "name": "Apple TV", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/apple_tv", - "requirements": [ - "pyatv==0.7.7" - ], - "zeroconf": [ - "_mediaremotetv._tcp.local.", - "_touch-able._tcp.local." - ], + "requirements": ["pyatv==0.7.7"], + "zeroconf": ["_mediaremotetv._tcp.local.", "_touch-able._tcp.local."], "after_dependencies": ["discovery"], - "codeowners": [ - "@postlund" - ] + "codeowners": ["@postlund"], + "iot_class": "local_push" } diff --git a/homeassistant/components/apprise/manifest.json b/homeassistant/components/apprise/manifest.json index 34061120322db..f9e6305678a89 100644 --- a/homeassistant/components/apprise/manifest.json +++ b/homeassistant/components/apprise/manifest.json @@ -3,5 +3,6 @@ "name": "Apprise", "documentation": "https://www.home-assistant.io/integrations/apprise", "requirements": ["apprise==0.8.9"], - "codeowners": ["@caronc"] + "codeowners": ["@caronc"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/aprs/manifest.json b/homeassistant/components/aprs/manifest.json index c2f4fe52fa129..5879c12235652 100644 --- a/homeassistant/components/aprs/manifest.json +++ b/homeassistant/components/aprs/manifest.json @@ -3,5 +3,6 @@ "name": "APRS", "documentation": "https://www.home-assistant.io/integrations/aprs", "codeowners": ["@PhilRW"], - "requirements": ["aprslib==0.6.46", "geopy==1.21.0"] + "requirements": ["aprslib==0.6.46", "geopy==1.21.0"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/aqualogic/manifest.json b/homeassistant/components/aqualogic/manifest.json index 5a753342b2bcb..acae105b54d6f 100644 --- a/homeassistant/components/aqualogic/manifest.json +++ b/homeassistant/components/aqualogic/manifest.json @@ -3,5 +3,6 @@ "name": "AquaLogic", "documentation": "https://www.home-assistant.io/integrations/aqualogic", "requirements": ["aqualogic==2.6"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/aquostv/manifest.json b/homeassistant/components/aquostv/manifest.json index cd402b3db90db..a28c852d8dbcb 100644 --- a/homeassistant/components/aquostv/manifest.json +++ b/homeassistant/components/aquostv/manifest.json @@ -3,5 +3,6 @@ "name": "Sharp Aquos TV", "documentation": "https://www.home-assistant.io/integrations/aquostv", "requirements": ["sharp_aquos_rc==0.3.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/arcam_fmj/manifest.json b/homeassistant/components/arcam_fmj/manifest.json index 5f8b8bb69a2fb..d38ceceba7305 100644 --- a/homeassistant/components/arcam_fmj/manifest.json +++ b/homeassistant/components/arcam_fmj/manifest.json @@ -10,5 +10,6 @@ "manufacturer": "ARCAM" } ], - "codeowners": ["@elupus"] + "codeowners": ["@elupus"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/arduino/manifest.json b/homeassistant/components/arduino/manifest.json index 4266d55926b66..95764ebb913fe 100644 --- a/homeassistant/components/arduino/manifest.json +++ b/homeassistant/components/arduino/manifest.json @@ -3,5 +3,6 @@ "name": "Arduino", "documentation": "https://www.home-assistant.io/integrations/arduino", "requirements": ["PyMata==2.20"], - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/arest/manifest.json b/homeassistant/components/arest/manifest.json index 9ed57d2d982f4..8a3b676c51803 100644 --- a/homeassistant/components/arest/manifest.json +++ b/homeassistant/components/arest/manifest.json @@ -2,5 +2,6 @@ "domain": "arest", "name": "aREST", "documentation": "https://www.home-assistant.io/integrations/arest", - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/arlo/manifest.json b/homeassistant/components/arlo/manifest.json index f046f84f94d99..7b4978b56c1cd 100644 --- a/homeassistant/components/arlo/manifest.json +++ b/homeassistant/components/arlo/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/arlo", "requirements": ["pyarlo==0.2.4"], "dependencies": ["ffmpeg"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/arris_tg2492lg/manifest.json b/homeassistant/components/arris_tg2492lg/manifest.json index 2d27824ba639e..8ed5c39882edf 100644 --- a/homeassistant/components/arris_tg2492lg/manifest.json +++ b/homeassistant/components/arris_tg2492lg/manifest.json @@ -2,10 +2,7 @@ "domain": "arris_tg2492lg", "name": "Arris TG2492LG", "documentation": "https://www.home-assistant.io/integrations/arris_tg2492lg", - "requirements": [ - "arris-tg2492lg==1.1.0" - ], - "codeowners": [ - "@vanbalken" - ] + "requirements": ["arris-tg2492lg==1.1.0"], + "codeowners": ["@vanbalken"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/aruba/manifest.json b/homeassistant/components/aruba/manifest.json index aa55cdba35567..660ba9f06f13c 100644 --- a/homeassistant/components/aruba/manifest.json +++ b/homeassistant/components/aruba/manifest.json @@ -3,5 +3,6 @@ "name": "Aruba", "documentation": "https://www.home-assistant.io/integrations/aruba", "requirements": ["pexpect==4.6.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/arwn/manifest.json b/homeassistant/components/arwn/manifest.json index 36ec1c79e585e..b9781fd6aa76f 100644 --- a/homeassistant/components/arwn/manifest.json +++ b/homeassistant/components/arwn/manifest.json @@ -3,5 +3,6 @@ "name": "Ambient Radio Weather Network", "documentation": "https://www.home-assistant.io/integrations/arwn", "dependencies": ["mqtt"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/asterisk_cdr/manifest.json b/homeassistant/components/asterisk_cdr/manifest.json index 8681c308ba3e5..c92d415fbee73 100644 --- a/homeassistant/components/asterisk_cdr/manifest.json +++ b/homeassistant/components/asterisk_cdr/manifest.json @@ -3,5 +3,6 @@ "name": "Asterisk Call Detail Records", "documentation": "https://www.home-assistant.io/integrations/asterisk_cdr", "dependencies": ["asterisk_mbox"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/asterisk_mbox/manifest.json b/homeassistant/components/asterisk_mbox/manifest.json index f02e964fb614b..068da7d64f44c 100644 --- a/homeassistant/components/asterisk_mbox/manifest.json +++ b/homeassistant/components/asterisk_mbox/manifest.json @@ -3,5 +3,6 @@ "name": "Asterisk Voicemail", "documentation": "https://www.home-assistant.io/integrations/asterisk_mbox", "requirements": ["asterisk_mbox==0.5.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/asuswrt/manifest.json b/homeassistant/components/asuswrt/manifest.json index ab739f1c7ec47..fef0c7a14cbca 100644 --- a/homeassistant/components/asuswrt/manifest.json +++ b/homeassistant/components/asuswrt/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/asuswrt", "requirements": ["aioasuswrt==1.3.1"], - "codeowners": ["@kennedyshead", "@ollo69"] + "codeowners": ["@kennedyshead", "@ollo69"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/atag/manifest.json b/homeassistant/components/atag/manifest.json index 1154a120f9137..eb9dc54ecd289 100644 --- a/homeassistant/components/atag/manifest.json +++ b/homeassistant/components/atag/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/atag/", "requirements": ["pyatag==0.3.5.3"], - "codeowners": ["@MatsNL"] + "codeowners": ["@MatsNL"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/aten_pe/manifest.json b/homeassistant/components/aten_pe/manifest.json index fdfcb4de0475b..b5a35345086d8 100644 --- a/homeassistant/components/aten_pe/manifest.json +++ b/homeassistant/components/aten_pe/manifest.json @@ -3,5 +3,6 @@ "name": "ATEN Rack PDU", "documentation": "https://www.home-assistant.io/integrations/aten_pe", "requirements": ["atenpdu==0.3.0"], - "codeowners": ["@mtdcr"] + "codeowners": ["@mtdcr"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/atome/manifest.json b/homeassistant/components/atome/manifest.json index 9479f76c7d82d..975e7f1ac3165 100644 --- a/homeassistant/components/atome/manifest.json +++ b/homeassistant/components/atome/manifest.json @@ -3,5 +3,6 @@ "name": "Atome Linky", "documentation": "https://www.home-assistant.io/integrations/atome", "codeowners": ["@baqs"], - "requirements": ["pyatome==0.1.1"] + "requirements": ["pyatome==0.1.1"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index fb4ff1a3484da..810e4d056388b 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -5,9 +5,19 @@ "requirements": ["yalexs==1.1.10"], "codeowners": ["@bdraco"], "dhcp": [ - {"hostname":"connect","macaddress":"D86162*"}, - {"hostname":"connect","macaddress":"B8B7F1*"}, - {"hostname":"august*","macaddress":"E076D0*"} + { + "hostname": "connect", + "macaddress": "D86162*" + }, + { + "hostname": "connect", + "macaddress": "B8B7F1*" + }, + { + "hostname": "august*", + "macaddress": "E076D0*" + } ], - "config_flow": true + "config_flow": true, + "iot_class": "cloud_push" } diff --git a/homeassistant/components/aurora/manifest.json b/homeassistant/components/aurora/manifest.json index 8d7d856e50c3d..466bf938cb511 100644 --- a/homeassistant/components/aurora/manifest.json +++ b/homeassistant/components/aurora/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/aurora", "config_flow": true, "codeowners": ["@djtimca"], - "requirements": ["auroranoaa==0.0.2"] + "requirements": ["auroranoaa==0.0.2"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/aurora_abb_powerone/manifest.json b/homeassistant/components/aurora_abb_powerone/manifest.json index 55d700c649629..69798ce49061a 100644 --- a/homeassistant/components/aurora_abb_powerone/manifest.json +++ b/homeassistant/components/aurora_abb_powerone/manifest.json @@ -3,5 +3,6 @@ "name": "Aurora ABB Solar PV", "documentation": "https://www.home-assistant.io/integrations/aurora_abb_powerone/", "codeowners": ["@davet2001"], - "requirements": ["aurorapy==0.2.6"] + "requirements": ["aurorapy==0.2.6"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/automation/manifest.json b/homeassistant/components/automation/manifest.json index 2483f57de8eec..9dd0130ee2f10 100644 --- a/homeassistant/components/automation/manifest.json +++ b/homeassistant/components/automation/manifest.json @@ -3,12 +3,7 @@ "name": "Automation", "documentation": "https://www.home-assistant.io/integrations/automation", "dependencies": ["blueprint", "trace"], - "after_dependencies": [ - "device_automation", - "webhook" - ], - "codeowners": [ - "@home-assistant/core" - ], + "after_dependencies": ["device_automation", "webhook"], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } diff --git a/homeassistant/components/avea/manifest.json b/homeassistant/components/avea/manifest.json index bf2b1a6a6ec29..223ceba7685f2 100644 --- a/homeassistant/components/avea/manifest.json +++ b/homeassistant/components/avea/manifest.json @@ -3,5 +3,6 @@ "name": "Elgato Avea", "documentation": "https://www.home-assistant.io/integrations/avea", "codeowners": ["@pattyland"], - "requirements": ["avea==1.5.1"] + "requirements": ["avea==1.5.1"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/avion/manifest.json b/homeassistant/components/avion/manifest.json index bd72cb8c06c66..7ee6af89347d9 100644 --- a/homeassistant/components/avion/manifest.json +++ b/homeassistant/components/avion/manifest.json @@ -3,5 +3,6 @@ "name": "Avi-on", "documentation": "https://www.home-assistant.io/integrations/avion", "requirements": ["avion==0.10"], - "codeowners": [] + "codeowners": [], + "iot_class": "assumed_state" } diff --git a/homeassistant/components/awair/manifest.json b/homeassistant/components/awair/manifest.json index f95e1c19d423f..c1a3fbd59a761 100644 --- a/homeassistant/components/awair/manifest.json +++ b/homeassistant/components/awair/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/awair", "requirements": ["python_awair==0.2.1"], "codeowners": ["@ahayworth", "@danielsjf"], - "config_flow": true + "config_flow": true, + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/aws/manifest.json b/homeassistant/components/aws/manifest.json index a1a307dda9428..57f5558f0b1eb 100644 --- a/homeassistant/components/aws/manifest.json +++ b/homeassistant/components/aws/manifest.json @@ -3,5 +3,6 @@ "name": "Amazon Web Services (AWS)", "documentation": "https://www.home-assistant.io/integrations/aws", "requirements": ["aiobotocore==1.2.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/axis/manifest.json b/homeassistant/components/axis/manifest.json index b709ac35da2b3..52e0c99044b24 100644 --- a/homeassistant/components/axis/manifest.json +++ b/homeassistant/components/axis/manifest.json @@ -5,9 +5,18 @@ "documentation": "https://www.home-assistant.io/integrations/axis", "requirements": ["axis==44"], "dhcp": [ - { "hostname": "axis-00408c*", "macaddress": "00408C*" }, - { "hostname": "axis-accc8e*", "macaddress": "ACCC8E*" }, - { "hostname": "axis-b8a44f*", "macaddress": "B8A44F*" } + { + "hostname": "axis-00408c*", + "macaddress": "00408C*" + }, + { + "hostname": "axis-accc8e*", + "macaddress": "ACCC8E*" + }, + { + "hostname": "axis-b8a44f*", + "macaddress": "B8A44F*" + } ], "ssdp": [ { @@ -15,11 +24,21 @@ } ], "zeroconf": [ - { "type": "_axis-video._tcp.local.", "macaddress": "00408C*" }, - { "type": "_axis-video._tcp.local.", "macaddress": "ACCC8E*" }, - { "type": "_axis-video._tcp.local.", "macaddress": "B8A44F*" } + { + "type": "_axis-video._tcp.local.", + "macaddress": "00408C*" + }, + { + "type": "_axis-video._tcp.local.", + "macaddress": "ACCC8E*" + }, + { + "type": "_axis-video._tcp.local.", + "macaddress": "B8A44F*" + } ], "after_dependencies": ["mqtt"], "codeowners": ["@Kane610"], - "quality_scale": "platinum" + "quality_scale": "platinum", + "iot_class": "local_push" } diff --git a/homeassistant/components/azure_devops/manifest.json b/homeassistant/components/azure_devops/manifest.json index 17338f5a29fd9..1dd0475329396 100644 --- a/homeassistant/components/azure_devops/manifest.json +++ b/homeassistant/components/azure_devops/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/azure_devops", "requirements": ["aioazuredevops==1.3.5"], - "codeowners": ["@timmo001"] + "codeowners": ["@timmo001"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/azure_event_hub/manifest.json b/homeassistant/components/azure_event_hub/manifest.json index 08bae34d731ef..b570f11e28f87 100644 --- a/homeassistant/components/azure_event_hub/manifest.json +++ b/homeassistant/components/azure_event_hub/manifest.json @@ -3,5 +3,6 @@ "name": "Azure Event Hub", "documentation": "https://www.home-assistant.io/integrations/azure_event_hub", "requirements": ["azure-eventhub==5.1.0"], - "codeowners": ["@eavanvalkenburg"] + "codeowners": ["@eavanvalkenburg"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/azure_service_bus/manifest.json b/homeassistant/components/azure_service_bus/manifest.json index d7a232d8d1ac8..5de15056b0874 100644 --- a/homeassistant/components/azure_service_bus/manifest.json +++ b/homeassistant/components/azure_service_bus/manifest.json @@ -3,5 +3,6 @@ "name": "Azure Service Bus", "documentation": "https://www.home-assistant.io/integrations/azure_service_bus", "requirements": ["azure-servicebus==0.50.3"], - "codeowners": ["@hfurubotten"] + "codeowners": ["@hfurubotten"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/baidu/manifest.json b/homeassistant/components/baidu/manifest.json index 88443e8672224..e808da427281e 100644 --- a/homeassistant/components/baidu/manifest.json +++ b/homeassistant/components/baidu/manifest.json @@ -3,5 +3,6 @@ "name": "Baidu", "documentation": "https://www.home-assistant.io/integrations/baidu", "requirements": ["baidu-aip==1.6.6"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/bayesian/manifest.json b/homeassistant/components/bayesian/manifest.json index ca62e91f09ee5..6a84beb1df68f 100644 --- a/homeassistant/components/bayesian/manifest.json +++ b/homeassistant/components/bayesian/manifest.json @@ -3,5 +3,6 @@ "name": "Bayesian", "documentation": "https://www.home-assistant.io/integrations/bayesian", "codeowners": [], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_polling" } diff --git a/homeassistant/components/bbb_gpio/manifest.json b/homeassistant/components/bbb_gpio/manifest.json index 201c01fa70994..add067ab0ccfb 100644 --- a/homeassistant/components/bbb_gpio/manifest.json +++ b/homeassistant/components/bbb_gpio/manifest.json @@ -3,5 +3,6 @@ "name": "BeagleBone Black GPIO", "documentation": "https://www.home-assistant.io/integrations/bbb_gpio", "requirements": ["Adafruit_BBIO==1.1.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/bbox/manifest.json b/homeassistant/components/bbox/manifest.json index bdace6c35f5a0..a59023bb3f524 100644 --- a/homeassistant/components/bbox/manifest.json +++ b/homeassistant/components/bbox/manifest.json @@ -3,5 +3,6 @@ "name": "Bbox", "documentation": "https://www.home-assistant.io/integrations/bbox", "requirements": ["pybbox==0.0.5-alpha"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/beewi_smartclim/manifest.json b/homeassistant/components/beewi_smartclim/manifest.json index 29f70b1135250..941faf1b598c0 100644 --- a/homeassistant/components/beewi_smartclim/manifest.json +++ b/homeassistant/components/beewi_smartclim/manifest.json @@ -3,5 +3,6 @@ "name": "BeeWi SmartClim BLE sensor", "documentation": "https://www.home-assistant.io/integrations/beewi_smartclim", "requirements": ["beewi_smartclim==0.0.10"], - "codeowners": ["@alemuro"] + "codeowners": ["@alemuro"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/bh1750/manifest.json b/homeassistant/components/bh1750/manifest.json index e8473910abdf2..f784b029a01d8 100644 --- a/homeassistant/components/bh1750/manifest.json +++ b/homeassistant/components/bh1750/manifest.json @@ -3,5 +3,6 @@ "name": "BH1750", "documentation": "https://www.home-assistant.io/integrations/bh1750", "requirements": ["i2csense==0.0.4", "smbus-cffi==0.5.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/bitcoin/manifest.json b/homeassistant/components/bitcoin/manifest.json index e198813dbee78..0a8abfa6500b4 100644 --- a/homeassistant/components/bitcoin/manifest.json +++ b/homeassistant/components/bitcoin/manifest.json @@ -3,5 +3,6 @@ "name": "Bitcoin", "documentation": "https://www.home-assistant.io/integrations/bitcoin", "requirements": ["blockchain==1.4.4"], - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/bizkaibus/manifest.json b/homeassistant/components/bizkaibus/manifest.json index d403d96ce6fbc..c8923f3d541fe 100644 --- a/homeassistant/components/bizkaibus/manifest.json +++ b/homeassistant/components/bizkaibus/manifest.json @@ -3,5 +3,6 @@ "name": "Bizkaibus", "documentation": "https://www.home-assistant.io/integrations/bizkaibus", "codeowners": ["@UgaitzEtxebarria"], - "requirements": ["bizkaibus==0.1.1"] + "requirements": ["bizkaibus==0.1.1"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/blackbird/manifest.json b/homeassistant/components/blackbird/manifest.json index f094109ba8492..04bde4b4617fe 100644 --- a/homeassistant/components/blackbird/manifest.json +++ b/homeassistant/components/blackbird/manifest.json @@ -3,5 +3,6 @@ "name": "Monoprice Blackbird Matrix Switch", "documentation": "https://www.home-assistant.io/integrations/blackbird", "requirements": ["pyblackbird==0.5"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/blebox/manifest.json b/homeassistant/components/blebox/manifest.json index 703d9042270d4..00b4b61c507ed 100644 --- a/homeassistant/components/blebox/manifest.json +++ b/homeassistant/components/blebox/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/blebox", "requirements": ["blebox_uniapi==1.3.2"], - "codeowners": [ "@gadgetmobile" ] + "codeowners": ["@gadgetmobile"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/blink/manifest.json b/homeassistant/components/blink/manifest.json index c88e13cdde717..7172406d6711a 100644 --- a/homeassistant/components/blink/manifest.json +++ b/homeassistant/components/blink/manifest.json @@ -4,6 +4,12 @@ "documentation": "https://www.home-assistant.io/integrations/blink", "requirements": ["blinkpy==0.17.0"], "codeowners": ["@fronzbot"], - "dhcp": [{"hostname":"blink*","macaddress":"B85F98*"}], - "config_flow": true + "dhcp": [ + { + "hostname": "blink*", + "macaddress": "B85F98*" + } + ], + "config_flow": true, + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/blinksticklight/manifest.json b/homeassistant/components/blinksticklight/manifest.json index 07726bc8cb08b..2520d2b1fcc03 100644 --- a/homeassistant/components/blinksticklight/manifest.json +++ b/homeassistant/components/blinksticklight/manifest.json @@ -3,5 +3,6 @@ "name": "BlinkStick", "documentation": "https://www.home-assistant.io/integrations/blinksticklight", "requirements": ["blinkstick==1.1.8"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/blinkt/manifest.json b/homeassistant/components/blinkt/manifest.json index 4759a356d9d66..ac659f78e11e5 100644 --- a/homeassistant/components/blinkt/manifest.json +++ b/homeassistant/components/blinkt/manifest.json @@ -3,5 +3,6 @@ "name": "Blinkt!", "documentation": "https://www.home-assistant.io/integrations/blinkt", "requirements": ["blinkt==0.1.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/blockchain/manifest.json b/homeassistant/components/blockchain/manifest.json index f30f7d041a0fc..c7c37c9bd0dd8 100644 --- a/homeassistant/components/blockchain/manifest.json +++ b/homeassistant/components/blockchain/manifest.json @@ -3,5 +3,6 @@ "name": "Blockchain.com", "documentation": "https://www.home-assistant.io/integrations/blockchain", "requirements": ["python-blockchain-api==0.0.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/bloomsky/manifest.json b/homeassistant/components/bloomsky/manifest.json index 8dda93b16b90d..f2b69f96dacf5 100644 --- a/homeassistant/components/bloomsky/manifest.json +++ b/homeassistant/components/bloomsky/manifest.json @@ -2,5 +2,6 @@ "domain": "bloomsky", "name": "BloomSky", "documentation": "https://www.home-assistant.io/integrations/bloomsky", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/blueprint/manifest.json b/homeassistant/components/blueprint/manifest.json index 215d788ee6bf4..c00b92b1e3c0d 100644 --- a/homeassistant/components/blueprint/manifest.json +++ b/homeassistant/components/blueprint/manifest.json @@ -2,8 +2,6 @@ "domain": "blueprint", "name": "Blueprint", "documentation": "https://www.home-assistant.io/integrations/blueprint", - "codeowners": [ - "@home-assistant/core" - ], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } diff --git a/homeassistant/components/bluesound/manifest.json b/homeassistant/components/bluesound/manifest.json index 9ea32a9e5df06..648ff2a180943 100644 --- a/homeassistant/components/bluesound/manifest.json +++ b/homeassistant/components/bluesound/manifest.json @@ -3,5 +3,6 @@ "name": "Bluesound", "documentation": "https://www.home-assistant.io/integrations/bluesound", "requirements": ["xmltodict==0.12.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/bluetooth_le_tracker/manifest.json b/homeassistant/components/bluetooth_le_tracker/manifest.json index ca4a44c55c669..564aef45f8442 100644 --- a/homeassistant/components/bluetooth_le_tracker/manifest.json +++ b/homeassistant/components/bluetooth_le_tracker/manifest.json @@ -3,5 +3,6 @@ "name": "Bluetooth LE Tracker", "documentation": "https://www.home-assistant.io/integrations/bluetooth_le_tracker", "requirements": ["pygatt[GATTTOOL]==4.0.5"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/bluetooth_tracker/manifest.json b/homeassistant/components/bluetooth_tracker/manifest.json index 9ef6fddcb0d06..a41720c2c4f7e 100644 --- a/homeassistant/components/bluetooth_tracker/manifest.json +++ b/homeassistant/components/bluetooth_tracker/manifest.json @@ -3,5 +3,6 @@ "name": "Bluetooth Tracker", "documentation": "https://www.home-assistant.io/integrations/bluetooth_tracker", "requirements": ["bt_proximity==0.2", "pybluez==0.22"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/bme280/manifest.json b/homeassistant/components/bme280/manifest.json index 2402c41402e3d..515e9e460d3b6 100644 --- a/homeassistant/components/bme280/manifest.json +++ b/homeassistant/components/bme280/manifest.json @@ -3,5 +3,6 @@ "name": "Bosch BME280 Environmental Sensor", "documentation": "https://www.home-assistant.io/integrations/bme280", "requirements": ["i2csense==0.0.4", "smbus-cffi==0.5.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/bme680/manifest.json b/homeassistant/components/bme680/manifest.json index be59b2fbaf9f6..16e841b942f5c 100644 --- a/homeassistant/components/bme680/manifest.json +++ b/homeassistant/components/bme680/manifest.json @@ -3,5 +3,6 @@ "name": "Bosch BME680 Environmental Sensor", "documentation": "https://www.home-assistant.io/integrations/bme680", "requirements": ["bme680==1.0.5", "smbus-cffi==0.5.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/bmp280/manifest.json b/homeassistant/components/bmp280/manifest.json index e22c275ed76ac..5347c93f4fa40 100644 --- a/homeassistant/components/bmp280/manifest.json +++ b/homeassistant/components/bmp280/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/bmp280", "codeowners": ["@belidzs"], "requirements": ["adafruit-circuitpython-bmp280==3.1.1", "RPi.GPIO==0.7.1a4"], - "quality_scale": "silver" + "quality_scale": "silver", + "iot_class": "local_polling" } diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index bbff139187e8b..aff9e4fd647bf 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", "requirements": ["bimmer_connected==0.7.15"], "codeowners": ["@gerard33", "@rikroe"], - "config_flow": true + "config_flow": true, + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/bond/manifest.json b/homeassistant/components/bond/manifest.json index 7204ac7e91df1..3995ecf5024c3 100644 --- a/homeassistant/components/bond/manifest.json +++ b/homeassistant/components/bond/manifest.json @@ -6,5 +6,6 @@ "requirements": ["bond-api==0.1.12"], "zeroconf": ["_bond._tcp.local."], "codeowners": ["@prystupa"], - "quality_scale": "platinum" + "quality_scale": "platinum", + "iot_class": "local_push" } diff --git a/homeassistant/components/braviatv/manifest.json b/homeassistant/components/braviatv/manifest.json index bdc4822d1d016..c3fcf218e9a6a 100644 --- a/homeassistant/components/braviatv/manifest.json +++ b/homeassistant/components/braviatv/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/braviatv", "requirements": ["bravia-tv==1.0.8"], "codeowners": ["@bieniu"], - "config_flow": true + "config_flow": true, + "iot_class": "local_polling" } diff --git a/homeassistant/components/broadlink/manifest.json b/homeassistant/components/broadlink/manifest.json index a1437521cb645..c27b9276ec408 100644 --- a/homeassistant/components/broadlink/manifest.json +++ b/homeassistant/components/broadlink/manifest.json @@ -6,9 +6,18 @@ "codeowners": ["@danielhiversen", "@felipediel"], "config_flow": true, "dhcp": [ - {"macaddress": "34EA34*"}, - {"macaddress": "24DFA7*"}, - {"macaddress": "A043B0*"}, - {"macaddress": "B4430D*"} - ] + { + "macaddress": "34EA34*" + }, + { + "macaddress": "24DFA7*" + }, + { + "macaddress": "A043B0*" + }, + { + "macaddress": "B4430D*" + } + ], + "iot_class": "local_polling" } diff --git a/homeassistant/components/brother/manifest.json b/homeassistant/components/brother/manifest.json index 13933b7bf6046..dd33046a065d1 100644 --- a/homeassistant/components/brother/manifest.json +++ b/homeassistant/components/brother/manifest.json @@ -4,7 +4,13 @@ "documentation": "https://www.home-assistant.io/integrations/brother", "codeowners": ["@bieniu"], "requirements": ["brother==0.2.2"], - "zeroconf": [{ "type": "_printer._tcp.local.", "name": "brother*" }], + "zeroconf": [ + { + "type": "_printer._tcp.local.", + "name": "brother*" + } + ], "config_flow": true, - "quality_scale": "platinum" + "quality_scale": "platinum", + "iot_class": "local_polling" } diff --git a/homeassistant/components/brottsplatskartan/manifest.json b/homeassistant/components/brottsplatskartan/manifest.json index 0737e506785c2..cb91446e4762f 100644 --- a/homeassistant/components/brottsplatskartan/manifest.json +++ b/homeassistant/components/brottsplatskartan/manifest.json @@ -3,5 +3,6 @@ "name": "Brottsplatskartan", "documentation": "https://www.home-assistant.io/integrations/brottsplatskartan", "requirements": ["brottsplatskartan==0.0.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/browser/manifest.json b/homeassistant/components/browser/manifest.json index 448e3af1d24e2..262635b7e27dd 100644 --- a/homeassistant/components/browser/manifest.json +++ b/homeassistant/components/browser/manifest.json @@ -3,5 +3,6 @@ "name": "Browser", "documentation": "https://www.home-assistant.io/integrations/browser", "codeowners": [], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_push" } diff --git a/homeassistant/components/brunt/manifest.json b/homeassistant/components/brunt/manifest.json index 68f0cf9e461d1..ba7d1ba117df7 100644 --- a/homeassistant/components/brunt/manifest.json +++ b/homeassistant/components/brunt/manifest.json @@ -3,5 +3,6 @@ "name": "Brunt Blind Engine", "documentation": "https://www.home-assistant.io/integrations/brunt", "requirements": ["brunt==0.1.3"], - "codeowners": ["@eavanvalkenburg"] + "codeowners": ["@eavanvalkenburg"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/bsblan/manifest.json b/homeassistant/components/bsblan/manifest.json index 0348cf3eeb424..1813b9ee04e4c 100644 --- a/homeassistant/components/bsblan/manifest.json +++ b/homeassistant/components/bsblan/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/bsblan", "requirements": ["bsblan==0.4.0"], - "codeowners": ["@liudger"] + "codeowners": ["@liudger"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/bt_home_hub_5/manifest.json b/homeassistant/components/bt_home_hub_5/manifest.json index adf3e74c7a60c..dfd61b1b9a849 100644 --- a/homeassistant/components/bt_home_hub_5/manifest.json +++ b/homeassistant/components/bt_home_hub_5/manifest.json @@ -3,5 +3,6 @@ "name": "BT Home Hub 5", "documentation": "https://www.home-assistant.io/integrations/bt_home_hub_5", "requirements": ["bthomehub5-devicelist==0.1.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/bt_smarthub/manifest.json b/homeassistant/components/bt_smarthub/manifest.json index 81f7098e65348..33fab430453a2 100644 --- a/homeassistant/components/bt_smarthub/manifest.json +++ b/homeassistant/components/bt_smarthub/manifest.json @@ -3,5 +3,6 @@ "name": "BT Smart Hub", "documentation": "https://www.home-assistant.io/integrations/bt_smarthub", "requirements": ["btsmarthub_devicelist==0.2.0"], - "codeowners": ["@jxwolstenholme"] + "codeowners": ["@jxwolstenholme"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/buienradar/manifest.json b/homeassistant/components/buienradar/manifest.json index 359cb471adad0..bdaa4e166ee62 100644 --- a/homeassistant/components/buienradar/manifest.json +++ b/homeassistant/components/buienradar/manifest.json @@ -3,5 +3,6 @@ "name": "Buienradar", "documentation": "https://www.home-assistant.io/integrations/buienradar", "requirements": ["buienradar==1.0.4"], - "codeowners": ["@mjj4791", "@ties"] + "codeowners": ["@mjj4791", "@ties"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/caldav/manifest.json b/homeassistant/components/caldav/manifest.json index 992b79f0d3beb..dadb3ac4bc8a3 100644 --- a/homeassistant/components/caldav/manifest.json +++ b/homeassistant/components/caldav/manifest.json @@ -3,5 +3,6 @@ "name": "CalDAV", "documentation": "https://www.home-assistant.io/integrations/caldav", "requirements": ["caldav==0.7.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/canary/manifest.json b/homeassistant/components/canary/manifest.json index af6b0ce54ba7b..c9a75b063f611 100644 --- a/homeassistant/components/canary/manifest.json +++ b/homeassistant/components/canary/manifest.json @@ -5,5 +5,6 @@ "requirements": ["py-canary==0.5.1"], "dependencies": ["ffmpeg"], "codeowners": [], - "config_flow": true + "config_flow": true, + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index 3f30bc450fdd4..c104ff7a12e18 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -4,7 +4,15 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/cast", "requirements": ["pychromecast==9.1.2"], - "after_dependencies": ["cloud", "http", "media_source", "plex", "tts", "zeroconf"], + "after_dependencies": [ + "cloud", + "http", + "media_source", + "plex", + "tts", + "zeroconf" + ], "zeroconf": ["_googlecast._tcp.local."], - "codeowners": ["@emontnemery"] + "codeowners": ["@emontnemery"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/cert_expiry/manifest.json b/homeassistant/components/cert_expiry/manifest.json index 62216290b805b..b0ed3f9d385e4 100644 --- a/homeassistant/components/cert_expiry/manifest.json +++ b/homeassistant/components/cert_expiry/manifest.json @@ -3,5 +3,6 @@ "name": "Certificate Expiry", "documentation": "https://www.home-assistant.io/integrations/cert_expiry", "config_flow": true, - "codeowners": ["@Cereal2nd", "@jjlawren"] + "codeowners": ["@Cereal2nd", "@jjlawren"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/channels/manifest.json b/homeassistant/components/channels/manifest.json index 45248bf1e7d25..1113699cdcac9 100644 --- a/homeassistant/components/channels/manifest.json +++ b/homeassistant/components/channels/manifest.json @@ -3,5 +3,6 @@ "name": "Channels", "documentation": "https://www.home-assistant.io/integrations/channels", "requirements": ["pychannels==1.0.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/circuit/manifest.json b/homeassistant/components/circuit/manifest.json index d6c43e186779a..6c10e7ff29980 100644 --- a/homeassistant/components/circuit/manifest.json +++ b/homeassistant/components/circuit/manifest.json @@ -3,5 +3,6 @@ "name": "Unify Circuit", "documentation": "https://www.home-assistant.io/integrations/circuit", "codeowners": ["@braam"], - "requirements": ["circuit-webhook==1.0.1"] + "requirements": ["circuit-webhook==1.0.1"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/cisco_ios/manifest.json b/homeassistant/components/cisco_ios/manifest.json index b485cf831b17b..25e07086efe92 100644 --- a/homeassistant/components/cisco_ios/manifest.json +++ b/homeassistant/components/cisco_ios/manifest.json @@ -3,5 +3,6 @@ "name": "Cisco IOS", "documentation": "https://www.home-assistant.io/integrations/cisco_ios", "requirements": ["pexpect==4.6.0"], - "codeowners": ["@fbradyirl"] + "codeowners": ["@fbradyirl"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/cisco_mobility_express/manifest.json b/homeassistant/components/cisco_mobility_express/manifest.json index b34daaa6d17d9..e1bdaeb314490 100644 --- a/homeassistant/components/cisco_mobility_express/manifest.json +++ b/homeassistant/components/cisco_mobility_express/manifest.json @@ -3,5 +3,6 @@ "name": "Cisco Mobility Express", "documentation": "https://www.home-assistant.io/integrations/cisco_mobility_express", "requirements": ["ciscomobilityexpress==0.3.9"], - "codeowners": ["@fbradyirl"] + "codeowners": ["@fbradyirl"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/cisco_webex_teams/manifest.json b/homeassistant/components/cisco_webex_teams/manifest.json index d10f9641846ef..ba20014fdcffd 100644 --- a/homeassistant/components/cisco_webex_teams/manifest.json +++ b/homeassistant/components/cisco_webex_teams/manifest.json @@ -3,5 +3,6 @@ "name": "Cisco Webex Teams", "documentation": "https://www.home-assistant.io/integrations/cisco_webex_teams", "requirements": ["webexteamssdk==1.1.1"], - "codeowners": ["@fbradyirl"] + "codeowners": ["@fbradyirl"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/citybikes/manifest.json b/homeassistant/components/citybikes/manifest.json index 1470832e899e0..eb76782ca9c7e 100644 --- a/homeassistant/components/citybikes/manifest.json +++ b/homeassistant/components/citybikes/manifest.json @@ -2,5 +2,6 @@ "domain": "citybikes", "name": "CityBikes", "documentation": "https://www.home-assistant.io/integrations/citybikes", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/clementine/manifest.json b/homeassistant/components/clementine/manifest.json index 53ae0cbe5332b..4f0b72a2be84e 100644 --- a/homeassistant/components/clementine/manifest.json +++ b/homeassistant/components/clementine/manifest.json @@ -3,5 +3,6 @@ "name": "Clementine Music Player", "documentation": "https://www.home-assistant.io/integrations/clementine", "requirements": ["python-clementine-remote==1.0.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/clickatell/manifest.json b/homeassistant/components/clickatell/manifest.json index 520fce157cda4..aa266bb811e05 100644 --- a/homeassistant/components/clickatell/manifest.json +++ b/homeassistant/components/clickatell/manifest.json @@ -2,5 +2,6 @@ "domain": "clickatell", "name": "Clickatell", "documentation": "https://www.home-assistant.io/integrations/clickatell", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/clicksend/manifest.json b/homeassistant/components/clicksend/manifest.json index ee72e056b30c3..59cdf7e036a16 100644 --- a/homeassistant/components/clicksend/manifest.json +++ b/homeassistant/components/clicksend/manifest.json @@ -2,5 +2,6 @@ "domain": "clicksend", "name": "ClickSend SMS", "documentation": "https://www.home-assistant.io/integrations/clicksend", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/clicksend_tts/manifest.json b/homeassistant/components/clicksend_tts/manifest.json index f5d3390d00576..e64bdafdf19fe 100644 --- a/homeassistant/components/clicksend_tts/manifest.json +++ b/homeassistant/components/clicksend_tts/manifest.json @@ -2,5 +2,6 @@ "domain": "clicksend_tts", "name": "ClickSend TTS", "documentation": "https://www.home-assistant.io/integrations/clicksend_tts", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/climacell/manifest.json b/homeassistant/components/climacell/manifest.json index 1df0b3613bba5..89f6d7bf846a1 100644 --- a/homeassistant/components/climacell/manifest.json +++ b/homeassistant/components/climacell/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/climacell", "requirements": ["pyclimacell==0.18.0"], - "codeowners": ["@raman325"] + "codeowners": ["@raman325"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index e51451be39709..d0d7ae0950570 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -5,5 +5,6 @@ "requirements": ["hass-nabucasa==0.43.0"], "dependencies": ["http", "webhook"], "after_dependencies": ["google_assistant", "alexa"], - "codeowners": ["@home-assistant/cloud"] + "codeowners": ["@home-assistant/cloud"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/cloudflare/manifest.json b/homeassistant/components/cloudflare/manifest.json index e2f55b13a7fef..c831dbeb34d92 100644 --- a/homeassistant/components/cloudflare/manifest.json +++ b/homeassistant/components/cloudflare/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/cloudflare", "requirements": ["pycfdns==1.2.1"], "codeowners": ["@ludeeus", "@ctalkington"], - "config_flow": true + "config_flow": true, + "iot_class": "cloud_push" } diff --git a/homeassistant/components/cmus/manifest.json b/homeassistant/components/cmus/manifest.json index 5a062996ab97a..7e785af57c165 100644 --- a/homeassistant/components/cmus/manifest.json +++ b/homeassistant/components/cmus/manifest.json @@ -3,5 +3,6 @@ "name": "cmus", "documentation": "https://www.home-assistant.io/integrations/cmus", "requirements": ["pycmus==0.1.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/co2signal/manifest.json b/homeassistant/components/co2signal/manifest.json index 9b7aa80e2ccd7..50ed7f6203815 100644 --- a/homeassistant/components/co2signal/manifest.json +++ b/homeassistant/components/co2signal/manifest.json @@ -3,5 +3,6 @@ "name": "CO2 Signal", "documentation": "https://www.home-assistant.io/integrations/co2signal", "requirements": ["co2signal==0.4.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/coinbase/manifest.json b/homeassistant/components/coinbase/manifest.json index 8d134792bbd0f..4579aecdd5bc9 100644 --- a/homeassistant/components/coinbase/manifest.json +++ b/homeassistant/components/coinbase/manifest.json @@ -3,5 +3,6 @@ "name": "Coinbase", "documentation": "https://www.home-assistant.io/integrations/coinbase", "requirements": ["coinbase==2.1.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/comed_hourly_pricing/manifest.json b/homeassistant/components/comed_hourly_pricing/manifest.json index e0d2b2bd3b44f..ecccc57686b45 100644 --- a/homeassistant/components/comed_hourly_pricing/manifest.json +++ b/homeassistant/components/comed_hourly_pricing/manifest.json @@ -2,5 +2,6 @@ "domain": "comed_hourly_pricing", "name": "ComEd Hourly Pricing", "documentation": "https://www.home-assistant.io/integrations/comed_hourly_pricing", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/comfoconnect/manifest.json b/homeassistant/components/comfoconnect/manifest.json index 8488ef58f1f6b..d02c10682e174 100644 --- a/homeassistant/components/comfoconnect/manifest.json +++ b/homeassistant/components/comfoconnect/manifest.json @@ -3,5 +3,6 @@ "name": "Zehnder ComfoAir Q", "documentation": "https://www.home-assistant.io/integrations/comfoconnect", "requirements": ["pycomfoconnect==0.4"], - "codeowners": ["@michaelarnauts"] + "codeowners": ["@michaelarnauts"], + "iot_class": "local_push" } diff --git a/homeassistant/components/command_line/manifest.json b/homeassistant/components/command_line/manifest.json index ffb1a33ed7bfb..3495c43ecc4e5 100644 --- a/homeassistant/components/command_line/manifest.json +++ b/homeassistant/components/command_line/manifest.json @@ -2,5 +2,6 @@ "domain": "command_line", "name": "Command Line", "documentation": "https://www.home-assistant.io/integrations/command_line", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/compensation/manifest.json b/homeassistant/components/compensation/manifest.json index 86efbce72c87b..9c4cd3449a920 100644 --- a/homeassistant/components/compensation/manifest.json +++ b/homeassistant/components/compensation/manifest.json @@ -3,5 +3,6 @@ "name": "Compensation", "documentation": "https://www.home-assistant.io/integrations/compensation", "requirements": ["numpy==1.20.2"], - "codeowners": ["@Petro31"] + "codeowners": ["@Petro31"], + "iot_class": "calculated" } diff --git a/homeassistant/components/concord232/manifest.json b/homeassistant/components/concord232/manifest.json index 97ae62bc3b06f..cfcd7fe8d6857 100644 --- a/homeassistant/components/concord232/manifest.json +++ b/homeassistant/components/concord232/manifest.json @@ -3,5 +3,6 @@ "name": "Concord232", "documentation": "https://www.home-assistant.io/integrations/concord232", "requirements": ["concord232==0.15"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/control4/manifest.json b/homeassistant/components/control4/manifest.json index 0d61b080745d3..656dd5bc93cf6 100644 --- a/homeassistant/components/control4/manifest.json +++ b/homeassistant/components/control4/manifest.json @@ -9,5 +9,6 @@ "st": "c4:director" } ], - "codeowners": ["@lawtancool"] + "codeowners": ["@lawtancool"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index 4f7a8f489bf43..1d2e089306556 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/conversation", "dependencies": ["http"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_push" } diff --git a/homeassistant/components/coolmaster/manifest.json b/homeassistant/components/coolmaster/manifest.json index 85bd3b1893feb..c032c2620ce6a 100644 --- a/homeassistant/components/coolmaster/manifest.json +++ b/homeassistant/components/coolmaster/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/coolmaster", "requirements": ["pycoolmasternet-async==0.1.2"], - "codeowners": ["@OnFreund"] + "codeowners": ["@OnFreund"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/coronavirus/manifest.json b/homeassistant/components/coronavirus/manifest.json index ae5083a5f9871..08a88d1b8269e 100644 --- a/homeassistant/components/coronavirus/manifest.json +++ b/homeassistant/components/coronavirus/manifest.json @@ -3,10 +3,7 @@ "name": "Coronavirus (COVID-19)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/coronavirus", - "requirements": [ - "coronavirus==1.1.1" - ], - "codeowners": [ - "@home_assistant/core" - ] + "requirements": ["coronavirus==1.1.1"], + "codeowners": ["@home_assistant/core"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/cppm_tracker/manifest.json b/homeassistant/components/cppm_tracker/manifest.json index 053e0ea0ba113..41794c06d9648 100644 --- a/homeassistant/components/cppm_tracker/manifest.json +++ b/homeassistant/components/cppm_tracker/manifest.json @@ -3,5 +3,6 @@ "name": "Aruba ClearPass", "documentation": "https://www.home-assistant.io/integrations/cppm_tracker", "requirements": ["clearpasspy==1.0.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/cpuspeed/manifest.json b/homeassistant/components/cpuspeed/manifest.json index ced8344ee5554..19973b4e8d2ef 100644 --- a/homeassistant/components/cpuspeed/manifest.json +++ b/homeassistant/components/cpuspeed/manifest.json @@ -3,5 +3,6 @@ "name": "CPU Speed", "documentation": "https://www.home-assistant.io/integrations/cpuspeed", "requirements": ["py-cpuinfo==7.0.0"], - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "local_push" } diff --git a/homeassistant/components/cups/manifest.json b/homeassistant/components/cups/manifest.json index 5f63e7c6a5066..7491dc1b42903 100644 --- a/homeassistant/components/cups/manifest.json +++ b/homeassistant/components/cups/manifest.json @@ -3,5 +3,6 @@ "name": "CUPS", "documentation": "https://www.home-assistant.io/integrations/cups", "requirements": ["pycups==1.9.73"], - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/currencylayer/manifest.json b/homeassistant/components/currencylayer/manifest.json index 508483732fca8..4dd46f74b00b1 100644 --- a/homeassistant/components/currencylayer/manifest.json +++ b/homeassistant/components/currencylayer/manifest.json @@ -2,5 +2,6 @@ "domain": "currencylayer", "name": "currencylayer", "documentation": "https://www.home-assistant.io/integrations/currencylayer", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/daikin/manifest.json b/homeassistant/components/daikin/manifest.json index 245f10a0e836a..2db81e8f167d7 100644 --- a/homeassistant/components/daikin/manifest.json +++ b/homeassistant/components/daikin/manifest.json @@ -6,5 +6,6 @@ "requirements": ["pydaikin==2.4.1"], "codeowners": ["@fredrike"], "zeroconf": ["_dkapi._tcp.local."], - "quality_scale": "platinum" + "quality_scale": "platinum", + "iot_class": "local_polling" } diff --git a/homeassistant/components/danfoss_air/manifest.json b/homeassistant/components/danfoss_air/manifest.json index bbecccf2a919d..6468eea0a273f 100644 --- a/homeassistant/components/danfoss_air/manifest.json +++ b/homeassistant/components/danfoss_air/manifest.json @@ -3,5 +3,6 @@ "name": "Danfoss Air", "documentation": "https://www.home-assistant.io/integrations/danfoss_air", "requirements": ["pydanfossair==0.1.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/darksky/manifest.json b/homeassistant/components/darksky/manifest.json index 53f0538881776..deefcaeb906e7 100644 --- a/homeassistant/components/darksky/manifest.json +++ b/homeassistant/components/darksky/manifest.json @@ -3,5 +3,6 @@ "name": "Dark Sky", "documentation": "https://www.home-assistant.io/integrations/darksky", "requirements": ["python-forecastio==1.4.0"], - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/datadog/manifest.json b/homeassistant/components/datadog/manifest.json index 7394c60804af8..bd2349798fda3 100644 --- a/homeassistant/components/datadog/manifest.json +++ b/homeassistant/components/datadog/manifest.json @@ -3,5 +3,6 @@ "name": "Datadog", "documentation": "https://www.home-assistant.io/integrations/datadog", "requirements": ["datadog==0.15.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/ddwrt/manifest.json b/homeassistant/components/ddwrt/manifest.json index 4c716929a86e5..0dcf709e82cee 100644 --- a/homeassistant/components/ddwrt/manifest.json +++ b/homeassistant/components/ddwrt/manifest.json @@ -2,5 +2,6 @@ "domain": "ddwrt", "name": "DD-WRT", "documentation": "https://www.home-assistant.io/integrations/ddwrt", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/debugpy/manifest.json b/homeassistant/components/debugpy/manifest.json index 67af8fc553b24..5820887c0c0d8 100644 --- a/homeassistant/components/debugpy/manifest.json +++ b/homeassistant/components/debugpy/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/debugpy", "requirements": ["debugpy==1.2.1"], "codeowners": ["@frenck"], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_push" } diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index 5cce88589104b..97dbc9a4854d1 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -10,5 +10,6 @@ } ], "codeowners": ["@Kane610"], - "quality_scale": "platinum" + "quality_scale": "platinum", + "iot_class": "local_push" } diff --git a/homeassistant/components/decora/manifest.json b/homeassistant/components/decora/manifest.json index 247422bee73c3..b631467e5e31e 100644 --- a/homeassistant/components/decora/manifest.json +++ b/homeassistant/components/decora/manifest.json @@ -3,5 +3,6 @@ "name": "Leviton Decora", "documentation": "https://www.home-assistant.io/integrations/decora", "requirements": ["bluepy==1.3.0", "decora==0.6"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/decora_wifi/manifest.json b/homeassistant/components/decora_wifi/manifest.json index c2a7dc63e00e8..1fd2b1737ad53 100644 --- a/homeassistant/components/decora_wifi/manifest.json +++ b/homeassistant/components/decora_wifi/manifest.json @@ -3,5 +3,6 @@ "name": "Leviton Decora Wi-Fi", "documentation": "https://www.home-assistant.io/integrations/decora_wifi", "requirements": ["decora_wifi==1.4"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/delijn/manifest.json b/homeassistant/components/delijn/manifest.json index 1de62e8df0f5f..317ee21a9b04c 100644 --- a/homeassistant/components/delijn/manifest.json +++ b/homeassistant/components/delijn/manifest.json @@ -3,5 +3,6 @@ "name": "De Lijn", "documentation": "https://www.home-assistant.io/integrations/delijn", "codeowners": ["@bollewolle", "@Emilv2"], - "requirements": ["pydelijn==0.6.1"] + "requirements": ["pydelijn==0.6.1"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/deluge/manifest.json b/homeassistant/components/deluge/manifest.json index 53210a17f17a9..8539a69e560d4 100644 --- a/homeassistant/components/deluge/manifest.json +++ b/homeassistant/components/deluge/manifest.json @@ -3,5 +3,6 @@ "name": "Deluge", "documentation": "https://www.home-assistant.io/integrations/deluge", "requirements": ["deluge-client==1.7.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/demo/manifest.json b/homeassistant/components/demo/manifest.json index 697e6520d7d0b..0997868fbfd9f 100644 --- a/homeassistant/components/demo/manifest.json +++ b/homeassistant/components/demo/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/demo", "dependencies": ["conversation", "zone", "group"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "calculated" } diff --git a/homeassistant/components/denon/manifest.json b/homeassistant/components/denon/manifest.json index e1f8f309e60c4..3073dd6e66107 100644 --- a/homeassistant/components/denon/manifest.json +++ b/homeassistant/components/denon/manifest.json @@ -2,5 +2,6 @@ "domain": "denon", "name": "Denon Network Receivers", "documentation": "https://www.home-assistant.io/integrations/denon", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/denonavr/manifest.json b/homeassistant/components/denonavr/manifest.json index e4cdaa0372403..b3f45330c9457 100644 --- a/homeassistant/components/denonavr/manifest.json +++ b/homeassistant/components/denonavr/manifest.json @@ -54,5 +54,6 @@ "manufacturer": "Marantz", "deviceType": "urn:schemas-denon-com:device:AiosDevice:1" } - ] + ], + "iot_class": "local_polling" } diff --git a/homeassistant/components/derivative/manifest.json b/homeassistant/components/derivative/manifest.json index 15f5b71d5cbc5..2b86c07cfe441 100644 --- a/homeassistant/components/derivative/manifest.json +++ b/homeassistant/components/derivative/manifest.json @@ -2,5 +2,6 @@ "domain": "derivative", "name": "Derivative", "documentation": "https://www.home-assistant.io/integrations/derivative", - "codeowners": ["@afaucogney"] + "codeowners": ["@afaucogney"], + "iot_class": "calculated" } diff --git a/homeassistant/components/deutsche_bahn/manifest.json b/homeassistant/components/deutsche_bahn/manifest.json index fa382b1b6a5c8..c8cbc5ba11e4c 100644 --- a/homeassistant/components/deutsche_bahn/manifest.json +++ b/homeassistant/components/deutsche_bahn/manifest.json @@ -3,5 +3,6 @@ "name": "Deutsche Bahn", "documentation": "https://www.home-assistant.io/integrations/deutsche_bahn", "requirements": ["schiene==0.23"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/device_sun_light_trigger/manifest.json b/homeassistant/components/device_sun_light_trigger/manifest.json index 777e8c5181ed3..7bd85771357bc 100644 --- a/homeassistant/components/device_sun_light_trigger/manifest.json +++ b/homeassistant/components/device_sun_light_trigger/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/device_sun_light_trigger", "after_dependencies": ["device_tracker", "group", "light", "person"], "codeowners": [], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "calculated" } diff --git a/homeassistant/components/devolo_home_control/manifest.json b/homeassistant/components/devolo_home_control/manifest.json index e53e715ffb168..832eb8025bc83 100644 --- a/homeassistant/components/devolo_home_control/manifest.json +++ b/homeassistant/components/devolo_home_control/manifest.json @@ -6,5 +6,6 @@ "after_dependencies": ["zeroconf"], "config_flow": true, "codeowners": ["@2Fake", "@Shutgun"], - "quality_scale": "silver" + "quality_scale": "silver", + "iot_class": "local_push" } diff --git a/homeassistant/components/dexcom/manifest.json b/homeassistant/components/dexcom/manifest.json index 3afe225e91bd1..1321f38a0d72d 100644 --- a/homeassistant/components/dexcom/manifest.json +++ b/homeassistant/components/dexcom/manifest.json @@ -4,7 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dexcom", "requirements": ["pydexcom==0.2.0"], - "codeowners": [ - "@gagebenne" - ] + "codeowners": ["@gagebenne"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/dhcp/manifest.json b/homeassistant/components/dhcp/manifest.json index 6ab395e7d829f..e6f181401c32c 100644 --- a/homeassistant/components/dhcp/manifest.json +++ b/homeassistant/components/dhcp/manifest.json @@ -2,11 +2,8 @@ "domain": "dhcp", "name": "DHCP Discovery", "documentation": "https://www.home-assistant.io/integrations/dhcp", - "requirements": [ - "scapy==2.4.4", "aiodiscover==1.3.4" - ], - "codeowners": [ - "@bdraco" - ], - "quality_scale": "internal" + "requirements": ["scapy==2.4.4", "aiodiscover==1.3.4"], + "codeowners": ["@bdraco"], + "quality_scale": "internal", + "iot_class": "local_push" } diff --git a/homeassistant/components/dht/manifest.json b/homeassistant/components/dht/manifest.json index 5e747d9473272..583a6e332d5b7 100644 --- a/homeassistant/components/dht/manifest.json +++ b/homeassistant/components/dht/manifest.json @@ -3,5 +3,6 @@ "name": "DHT Sensor", "documentation": "https://www.home-assistant.io/integrations/dht", "requirements": ["Adafruit-DHT==1.4.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/dialogflow/manifest.json b/homeassistant/components/dialogflow/manifest.json index 53aed42afaae5..40bbfae2a308f 100644 --- a/homeassistant/components/dialogflow/manifest.json +++ b/homeassistant/components/dialogflow/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dialogflow", "dependencies": ["webhook"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/digital_ocean/manifest.json b/homeassistant/components/digital_ocean/manifest.json index 217803ef19572..eba3626a95085 100644 --- a/homeassistant/components/digital_ocean/manifest.json +++ b/homeassistant/components/digital_ocean/manifest.json @@ -3,5 +3,6 @@ "name": "Digital Ocean", "documentation": "https://www.home-assistant.io/integrations/digital_ocean", "requirements": ["python-digitalocean==1.13.2"], - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/digitalloggers/manifest.json b/homeassistant/components/digitalloggers/manifest.json index 9e6bd5b7e5f93..35cc1413bdfaa 100644 --- a/homeassistant/components/digitalloggers/manifest.json +++ b/homeassistant/components/digitalloggers/manifest.json @@ -3,5 +3,6 @@ "name": "Digital Loggers", "documentation": "https://www.home-assistant.io/integrations/digitalloggers", "requirements": ["dlipower==0.7.165"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/directv/manifest.json b/homeassistant/components/directv/manifest.json index 916855535961b..6d69ba2fd5aae 100644 --- a/homeassistant/components/directv/manifest.json +++ b/homeassistant/components/directv/manifest.json @@ -11,5 +11,6 @@ "manufacturer": "DIRECTV", "deviceType": "urn:schemas-upnp-org:device:MediaServer:1" } - ] + ], + "iot_class": "local_polling" } diff --git a/homeassistant/components/discogs/manifest.json b/homeassistant/components/discogs/manifest.json index 2d8e308a42b0b..5cc2d900229f7 100644 --- a/homeassistant/components/discogs/manifest.json +++ b/homeassistant/components/discogs/manifest.json @@ -3,5 +3,6 @@ "name": "Discogs", "documentation": "https://www.home-assistant.io/integrations/discogs", "requirements": ["discogs_client==2.3.0"], - "codeowners": ["@thibmaek"] + "codeowners": ["@thibmaek"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/discord/manifest.json b/homeassistant/components/discord/manifest.json index 474705913c033..508ddd126a3d1 100644 --- a/homeassistant/components/discord/manifest.json +++ b/homeassistant/components/discord/manifest.json @@ -3,5 +3,6 @@ "name": "Discord", "documentation": "https://www.home-assistant.io/integrations/discord", "requirements": ["discord.py==1.6.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/dlib_face_detect/manifest.json b/homeassistant/components/dlib_face_detect/manifest.json index e7bd53560bf61..792486c7a875d 100644 --- a/homeassistant/components/dlib_face_detect/manifest.json +++ b/homeassistant/components/dlib_face_detect/manifest.json @@ -3,5 +3,6 @@ "name": "Dlib Face Detect", "documentation": "https://www.home-assistant.io/integrations/dlib_face_detect", "requirements": ["face_recognition==1.2.3"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/dlib_face_identify/manifest.json b/homeassistant/components/dlib_face_identify/manifest.json index a1e47f967c028..b8ac5bce5fa81 100644 --- a/homeassistant/components/dlib_face_identify/manifest.json +++ b/homeassistant/components/dlib_face_identify/manifest.json @@ -3,5 +3,6 @@ "name": "Dlib Face Identify", "documentation": "https://www.home-assistant.io/integrations/dlib_face_identify", "requirements": ["face_recognition==1.2.3"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/dlink/manifest.json b/homeassistant/components/dlink/manifest.json index 81a89c8e397e6..48a36a908c3c9 100644 --- a/homeassistant/components/dlink/manifest.json +++ b/homeassistant/components/dlink/manifest.json @@ -3,5 +3,6 @@ "name": "D-Link Wi-Fi Smart Plugs", "documentation": "https://www.home-assistant.io/integrations/dlink", "requirements": ["pyW215==0.7.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index 094a9adc43a76..928df4b1ecc23 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -3,5 +3,6 @@ "name": "DLNA Digital Media Renderer", "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", "requirements": ["async-upnp-client==0.16.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/dnsip/manifest.json b/homeassistant/components/dnsip/manifest.json index 6aeac70b4f39d..2254314804bfe 100644 --- a/homeassistant/components/dnsip/manifest.json +++ b/homeassistant/components/dnsip/manifest.json @@ -3,5 +3,6 @@ "name": "DNS IP", "documentation": "https://www.home-assistant.io/integrations/dnsip", "requirements": ["aiodns==2.0.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/dominos/manifest.json b/homeassistant/components/dominos/manifest.json index 0137cafc169b6..d7d366befd4b4 100644 --- a/homeassistant/components/dominos/manifest.json +++ b/homeassistant/components/dominos/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/dominos", "requirements": ["pizzapi==0.0.3"], "dependencies": ["http"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/doods/manifest.json b/homeassistant/components/doods/manifest.json index 6f6fcb0d6b3b5..4e31ca03371b3 100644 --- a/homeassistant/components/doods/manifest.json +++ b/homeassistant/components/doods/manifest.json @@ -3,5 +3,6 @@ "name": "DOODS - Dedicated Open Object Detection Service", "documentation": "https://www.home-assistant.io/integrations/doods", "requirements": ["pydoods==1.0.2", "pillow==8.1.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/doorbird/manifest.json b/homeassistant/components/doorbird/manifest.json index c5805b15eac89..5dd9ecbd0db81 100644 --- a/homeassistant/components/doorbird/manifest.json +++ b/homeassistant/components/doorbird/manifest.json @@ -4,7 +4,13 @@ "documentation": "https://www.home-assistant.io/integrations/doorbird", "requirements": ["doorbirdpy==2.1.0"], "dependencies": ["http"], - "zeroconf": [{"type":"_axis-video._tcp.local.","macaddress":"1CCAE3*"}], + "zeroconf": [ + { + "type": "_axis-video._tcp.local.", + "macaddress": "1CCAE3*" + } + ], "codeowners": ["@oblogic7", "@bdraco"], - "config_flow": true + "config_flow": true, + "iot_class": "local_push" } diff --git a/homeassistant/components/dovado/manifest.json b/homeassistant/components/dovado/manifest.json index 0a2a52cb21d44..e4c2a48c2d44c 100644 --- a/homeassistant/components/dovado/manifest.json +++ b/homeassistant/components/dovado/manifest.json @@ -3,5 +3,6 @@ "name": "Dovado", "documentation": "https://www.home-assistant.io/integrations/dovado", "requirements": ["dovado==0.4.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/dsmr/manifest.json b/homeassistant/components/dsmr/manifest.json index c442130bb9fa8..de81d14f2480c 100644 --- a/homeassistant/components/dsmr/manifest.json +++ b/homeassistant/components/dsmr/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/dsmr", "requirements": ["dsmr_parser==0.28"], "codeowners": ["@Robbie1221"], - "config_flow": false + "config_flow": false, + "iot_class": "local_push" } diff --git a/homeassistant/components/dsmr_reader/manifest.json b/homeassistant/components/dsmr_reader/manifest.json index 59096d626e32d..daa6cb2332f8b 100644 --- a/homeassistant/components/dsmr_reader/manifest.json +++ b/homeassistant/components/dsmr_reader/manifest.json @@ -3,5 +3,6 @@ "name": "DSMR Reader", "documentation": "https://www.home-assistant.io/integrations/dsmr_reader", "dependencies": ["mqtt"], - "codeowners": ["@depl0y"] + "codeowners": ["@depl0y"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/dte_energy_bridge/manifest.json b/homeassistant/components/dte_energy_bridge/manifest.json index a63831498889e..f2154c20c10ee 100644 --- a/homeassistant/components/dte_energy_bridge/manifest.json +++ b/homeassistant/components/dte_energy_bridge/manifest.json @@ -2,5 +2,6 @@ "domain": "dte_energy_bridge", "name": "DTE Energy Bridge", "documentation": "https://www.home-assistant.io/integrations/dte_energy_bridge", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/dublin_bus_transport/manifest.json b/homeassistant/components/dublin_bus_transport/manifest.json index a8ed951b1d9f1..f7df307653a70 100644 --- a/homeassistant/components/dublin_bus_transport/manifest.json +++ b/homeassistant/components/dublin_bus_transport/manifest.json @@ -2,5 +2,6 @@ "domain": "dublin_bus_transport", "name": "Dublin Bus", "documentation": "https://www.home-assistant.io/integrations/dublin_bus_transport", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/duckdns/manifest.json b/homeassistant/components/duckdns/manifest.json index bfa692c80f320..dbd1e8b0939ec 100644 --- a/homeassistant/components/duckdns/manifest.json +++ b/homeassistant/components/duckdns/manifest.json @@ -2,5 +2,6 @@ "domain": "duckdns", "name": "Duck DNS", "documentation": "https://www.home-assistant.io/integrations/duckdns", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/dunehd/manifest.json b/homeassistant/components/dunehd/manifest.json index 96a497f1f96a8..bf5fd34788806 100644 --- a/homeassistant/components/dunehd/manifest.json +++ b/homeassistant/components/dunehd/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/dunehd", "requirements": ["pdunehd==1.3.2"], "codeowners": ["@bieniu"], - "config_flow": true + "config_flow": true, + "iot_class": "local_polling" } diff --git a/homeassistant/components/dwd_weather_warnings/manifest.json b/homeassistant/components/dwd_weather_warnings/manifest.json index df4c412cc6284..1550d9262a4a2 100644 --- a/homeassistant/components/dwd_weather_warnings/manifest.json +++ b/homeassistant/components/dwd_weather_warnings/manifest.json @@ -3,5 +3,6 @@ "name": "Deutscher Wetterdienst (DWD) Weather Warnings", "documentation": "https://www.home-assistant.io/integrations/dwd_weather_warnings", "codeowners": ["@runningman84", "@stephan192", "@Hummel95"], - "requirements": ["dwdwfsapi==1.0.3"] + "requirements": ["dwdwfsapi==1.0.3"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/dweet/manifest.json b/homeassistant/components/dweet/manifest.json index 7849b2b33460f..46edd2bacfa5f 100644 --- a/homeassistant/components/dweet/manifest.json +++ b/homeassistant/components/dweet/manifest.json @@ -3,5 +3,6 @@ "name": "dweet.io", "documentation": "https://www.home-assistant.io/integrations/dweet", "requirements": ["dweepy==0.3.0"], - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/dynalite/manifest.json b/homeassistant/components/dynalite/manifest.json index 387e69a1fbddf..1ae50233b1a3c 100644 --- a/homeassistant/components/dynalite/manifest.json +++ b/homeassistant/components/dynalite/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dynalite", "codeowners": ["@ziv1234"], - "requirements": ["dynalite_devices==0.1.46"] + "requirements": ["dynalite_devices==0.1.46"], + "iot_class": "local_push" } diff --git a/homeassistant/components/dyson/manifest.json b/homeassistant/components/dyson/manifest.json index 4678b1ad5982e..0f5da0691c4f1 100644 --- a/homeassistant/components/dyson/manifest.json +++ b/homeassistant/components/dyson/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/dyson", "requirements": ["libpurecool==0.6.4"], "after_dependencies": ["zeroconf"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/eafm/manifest.json b/homeassistant/components/eafm/manifest.json index 66813d3303613..a4250e33a60b6 100644 --- a/homeassistant/components/eafm/manifest.json +++ b/homeassistant/components/eafm/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/eafm", "config_flow": true, "codeowners": ["@Jc2k"], - "requirements": ["aioeafm==0.1.2"] + "requirements": ["aioeafm==0.1.2"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/ebox/manifest.json b/homeassistant/components/ebox/manifest.json index 18f26436981b3..6e4aca44ad6af 100644 --- a/homeassistant/components/ebox/manifest.json +++ b/homeassistant/components/ebox/manifest.json @@ -3,5 +3,6 @@ "name": "EBox", "documentation": "https://www.home-assistant.io/integrations/ebox", "requirements": ["pyebox==1.1.4"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/ebusd/manifest.json b/homeassistant/components/ebusd/manifest.json index 482b691851850..347fee0bc8557 100644 --- a/homeassistant/components/ebusd/manifest.json +++ b/homeassistant/components/ebusd/manifest.json @@ -3,5 +3,6 @@ "name": "ebusd", "documentation": "https://www.home-assistant.io/integrations/ebusd", "requirements": ["ebusdpy==0.0.16"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/ecoal_boiler/manifest.json b/homeassistant/components/ecoal_boiler/manifest.json index c51f737cfd816..83a9e7dbf6bdd 100644 --- a/homeassistant/components/ecoal_boiler/manifest.json +++ b/homeassistant/components/ecoal_boiler/manifest.json @@ -3,5 +3,6 @@ "name": "eSterownik eCoal.pl Boiler", "documentation": "https://www.home-assistant.io/integrations/ecoal_boiler", "requirements": ["ecoaliface==0.4.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/ecobee/manifest.json b/homeassistant/components/ecobee/manifest.json index de7a7d325b3d2..f27cb8e425eea 100644 --- a/homeassistant/components/ecobee/manifest.json +++ b/homeassistant/components/ecobee/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ecobee", "requirements": ["python-ecobee-api==0.2.10"], - "codeowners": ["@marthoc"] + "codeowners": ["@marthoc"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/econet/manifest.json b/homeassistant/components/econet/manifest.json index 379fd8953590a..99a021de73a39 100644 --- a/homeassistant/components/econet/manifest.json +++ b/homeassistant/components/econet/manifest.json @@ -1,9 +1,9 @@ - { "domain": "econet", "name": "Rheem EcoNet Products", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/econet", "requirements": ["pyeconet==0.1.14"], - "codeowners": ["@vangorra", "@w1ll1am23"] -} \ No newline at end of file + "codeowners": ["@vangorra", "@w1ll1am23"], + "iot_class": "cloud_push" +} diff --git a/homeassistant/components/ecovacs/manifest.json b/homeassistant/components/ecovacs/manifest.json index aa67be422c5c1..ad442b0621a72 100644 --- a/homeassistant/components/ecovacs/manifest.json +++ b/homeassistant/components/ecovacs/manifest.json @@ -3,5 +3,6 @@ "name": "Ecovacs", "documentation": "https://www.home-assistant.io/integrations/ecovacs", "requirements": ["sucks==0.9.4"], - "codeowners": ["@OverloadUT"] + "codeowners": ["@OverloadUT"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/eddystone_temperature/manifest.json b/homeassistant/components/eddystone_temperature/manifest.json index e6ff0a17ea30c..92ab636b87f9f 100644 --- a/homeassistant/components/eddystone_temperature/manifest.json +++ b/homeassistant/components/eddystone_temperature/manifest.json @@ -3,5 +3,6 @@ "name": "Eddystone", "documentation": "https://www.home-assistant.io/integrations/eddystone_temperature", "requirements": ["beacontools[scan]==1.2.3", "construct==2.10.56"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/edimax/manifest.json b/homeassistant/components/edimax/manifest.json index 20d72b30a6a60..6226968b5d313 100644 --- a/homeassistant/components/edimax/manifest.json +++ b/homeassistant/components/edimax/manifest.json @@ -3,5 +3,6 @@ "name": "Edimax", "documentation": "https://www.home-assistant.io/integrations/edimax", "requirements": ["pyedimax==0.2.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/edl21/manifest.json b/homeassistant/components/edl21/manifest.json index ea960de6b4960..77c0cdebf206f 100644 --- a/homeassistant/components/edl21/manifest.json +++ b/homeassistant/components/edl21/manifest.json @@ -3,5 +3,6 @@ "name": "EDL21", "documentation": "https://www.home-assistant.io/integrations/edl21", "requirements": ["pysml==0.0.5"], - "codeowners": ["@mtdcr"] + "codeowners": ["@mtdcr"], + "iot_class": "local_push" } diff --git a/homeassistant/components/ee_brightbox/manifest.json b/homeassistant/components/ee_brightbox/manifest.json index 361df9575df4d..c477b9fb33986 100644 --- a/homeassistant/components/ee_brightbox/manifest.json +++ b/homeassistant/components/ee_brightbox/manifest.json @@ -3,5 +3,6 @@ "name": "EE Bright Box", "documentation": "https://www.home-assistant.io/integrations/ee_brightbox", "requirements": ["eebrightbox==0.0.4"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/efergy/manifest.json b/homeassistant/components/efergy/manifest.json index cb9cfb17ac551..fe9ea7e60477f 100644 --- a/homeassistant/components/efergy/manifest.json +++ b/homeassistant/components/efergy/manifest.json @@ -2,5 +2,6 @@ "domain": "efergy", "name": "Efergy", "documentation": "https://www.home-assistant.io/integrations/efergy", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/egardia/manifest.json b/homeassistant/components/egardia/manifest.json index 94953a773c29f..78e32a4d74965 100644 --- a/homeassistant/components/egardia/manifest.json +++ b/homeassistant/components/egardia/manifest.json @@ -3,5 +3,6 @@ "name": "Egardia", "documentation": "https://www.home-assistant.io/integrations/egardia", "requirements": ["pythonegardia==1.0.40"], - "codeowners": ["@jeroenterheerdt"] + "codeowners": ["@jeroenterheerdt"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/eight_sleep/manifest.json b/homeassistant/components/eight_sleep/manifest.json index 1de572d14104a..d0f86d5a5e450 100644 --- a/homeassistant/components/eight_sleep/manifest.json +++ b/homeassistant/components/eight_sleep/manifest.json @@ -3,5 +3,6 @@ "name": "Eight Sleep", "documentation": "https://www.home-assistant.io/integrations/eight_sleep", "requirements": ["pyeight==0.1.5"], - "codeowners": ["@mezz64"] + "codeowners": ["@mezz64"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/elgato/manifest.json b/homeassistant/components/elgato/manifest.json index 9a166b86b8e7d..f2493befcbdd1 100644 --- a/homeassistant/components/elgato/manifest.json +++ b/homeassistant/components/elgato/manifest.json @@ -6,5 +6,6 @@ "requirements": ["elgato==2.0.1"], "zeroconf": ["_elg._tcp.local."], "codeowners": ["@frenck"], - "quality_scale": "platinum" + "quality_scale": "platinum", + "iot_class": "local_polling" } diff --git a/homeassistant/components/eliqonline/manifest.json b/homeassistant/components/eliqonline/manifest.json index 6860ff003c4d5..20456c5b5ec7d 100644 --- a/homeassistant/components/eliqonline/manifest.json +++ b/homeassistant/components/eliqonline/manifest.json @@ -3,5 +3,6 @@ "name": "Eliqonline", "documentation": "https://www.home-assistant.io/integrations/eliqonline", "requirements": ["eliqonline==1.2.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/elkm1/manifest.json b/homeassistant/components/elkm1/manifest.json index 2077890d3d278..3f72ecfd7a722 100644 --- a/homeassistant/components/elkm1/manifest.json +++ b/homeassistant/components/elkm1/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/elkm1", "requirements": ["elkm1-lib==0.8.10"], "codeowners": ["@gwww", "@bdraco"], - "config_flow": true + "config_flow": true, + "iot_class": "local_push" } diff --git a/homeassistant/components/elv/manifest.json b/homeassistant/components/elv/manifest.json index 89b3751685a93..a5eb96e137681 100644 --- a/homeassistant/components/elv/manifest.json +++ b/homeassistant/components/elv/manifest.json @@ -3,5 +3,6 @@ "name": "ELV PCA", "documentation": "https://www.home-assistant.io/integrations/pca", "codeowners": ["@majuss"], - "requirements": ["pypca==0.0.7"] + "requirements": ["pypca==0.0.7"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/emby/manifest.json b/homeassistant/components/emby/manifest.json index 88f5f57e390e1..7c1295b0e5827 100644 --- a/homeassistant/components/emby/manifest.json +++ b/homeassistant/components/emby/manifest.json @@ -3,5 +3,6 @@ "name": "Emby", "documentation": "https://www.home-assistant.io/integrations/emby", "requirements": ["pyemby==1.7"], - "codeowners": ["@mezz64"] + "codeowners": ["@mezz64"], + "iot_class": "local_push" } diff --git a/homeassistant/components/emoncms/manifest.json b/homeassistant/components/emoncms/manifest.json index 6ea57cf370492..040e29c846b45 100644 --- a/homeassistant/components/emoncms/manifest.json +++ b/homeassistant/components/emoncms/manifest.json @@ -2,5 +2,6 @@ "domain": "emoncms", "name": "Emoncms", "documentation": "https://www.home-assistant.io/integrations/emoncms", - "codeowners": ["@borpin"] + "codeowners": ["@borpin"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/emoncms_history/manifest.json b/homeassistant/components/emoncms_history/manifest.json index 9c3066db215e1..ab1610db1fe86 100644 --- a/homeassistant/components/emoncms_history/manifest.json +++ b/homeassistant/components/emoncms_history/manifest.json @@ -2,5 +2,6 @@ "domain": "emoncms_history", "name": "Emoncms History", "documentation": "https://www.home-assistant.io/integrations/emoncms_history", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/emonitor/manifest.json b/homeassistant/components/emonitor/manifest.json index b6cf3526bd8ed..331597225f005 100644 --- a/homeassistant/components/emonitor/manifest.json +++ b/homeassistant/components/emonitor/manifest.json @@ -3,11 +3,8 @@ "name": "SiteSage Emonitor", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/emonitor", - "requirements": [ - "aioemonitor==1.0.5" - ], - "dhcp": [{"hostname":"emonitor*","macaddress":"0090C2*"}], - "codeowners": [ - "@bdraco" - ] -} \ No newline at end of file + "requirements": ["aioemonitor==1.0.5"], + "dhcp": [{ "hostname": "emonitor*", "macaddress": "0090C2*" }], + "codeowners": ["@bdraco"], + "iot_class": "local_polling" +} diff --git a/homeassistant/components/emulated_hue/manifest.json b/homeassistant/components/emulated_hue/manifest.json index fdff91630f3b3..406451639f20c 100644 --- a/homeassistant/components/emulated_hue/manifest.json +++ b/homeassistant/components/emulated_hue/manifest.json @@ -5,5 +5,6 @@ "requirements": ["aiohttp_cors==0.7.0"], "after_dependencies": ["http"], "codeowners": [], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_push" } diff --git a/homeassistant/components/emulated_kasa/manifest.json b/homeassistant/components/emulated_kasa/manifest.json index bb292b2e7b588..419a34db98c44 100644 --- a/homeassistant/components/emulated_kasa/manifest.json +++ b/homeassistant/components/emulated_kasa/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/emulated_kasa", "requirements": ["sense_energy==0.9.0"], "codeowners": ["@kbickar"], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_push" } diff --git a/homeassistant/components/emulated_roku/manifest.json b/homeassistant/components/emulated_roku/manifest.json index 78dfa78802f62..6ef54d1d1ccfb 100644 --- a/homeassistant/components/emulated_roku/manifest.json +++ b/homeassistant/components/emulated_roku/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/emulated_roku", "requirements": ["emulated_roku==0.2.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/enigma2/manifest.json b/homeassistant/components/enigma2/manifest.json index da6765368ae50..37ed8a5c6bbc2 100644 --- a/homeassistant/components/enigma2/manifest.json +++ b/homeassistant/components/enigma2/manifest.json @@ -3,5 +3,6 @@ "name": "Enigma2 (OpenWebif)", "documentation": "https://www.home-assistant.io/integrations/enigma2", "requirements": ["openwebifpy==3.2.7"], - "codeowners": ["@fbradyirl"] + "codeowners": ["@fbradyirl"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/enocean/manifest.json b/homeassistant/components/enocean/manifest.json index 390b48342fde8..86db950ccc5d6 100644 --- a/homeassistant/components/enocean/manifest.json +++ b/homeassistant/components/enocean/manifest.json @@ -2,11 +2,8 @@ "domain": "enocean", "name": "EnOcean", "documentation": "https://www.home-assistant.io/integrations/enocean", - "requirements": [ - "enocean==0.50" - ], - "codeowners": [ - "@bdurrer" - ], - "config_flow": true + "requirements": ["enocean==0.50"], + "codeowners": ["@bdurrer"], + "config_flow": true, + "iot_class": "local_push" } diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index 9b8f01f254702..3e31ac5dc63c5 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -2,12 +2,13 @@ "domain": "enphase_envoy", "name": "Enphase Envoy", "documentation": "https://www.home-assistant.io/integrations/enphase_envoy", - "requirements": [ - "envoy_reader==0.18.4" - ], - "codeowners": [ - "@gtdiehl" - ], + "requirements": ["envoy_reader==0.18.4"], + "codeowners": ["@gtdiehl"], "config_flow": true, - "zeroconf": [{ "type": "_enphase-envoy._tcp.local."}] + "zeroconf": [ + { + "type": "_enphase-envoy._tcp.local." + } + ], + "iot_class": "local_polling" } diff --git a/homeassistant/components/entur_public_transport/manifest.json b/homeassistant/components/entur_public_transport/manifest.json index db5c68d2a4c45..ad522be932152 100644 --- a/homeassistant/components/entur_public_transport/manifest.json +++ b/homeassistant/components/entur_public_transport/manifest.json @@ -3,5 +3,6 @@ "name": "Entur", "documentation": "https://www.home-assistant.io/integrations/entur_public_transport", "requirements": ["enturclient==0.2.1"], - "codeowners": ["@hfurubotten"] + "codeowners": ["@hfurubotten"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index 02a60049f0787..62c3e935d69a9 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -3,5 +3,6 @@ "name": "Environment Canada", "documentation": "https://www.home-assistant.io/integrations/environment_canada", "requirements": ["env_canada==0.2.5"], - "codeowners": ["@michaeldavie"] + "codeowners": ["@michaeldavie"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/envirophat/manifest.json b/homeassistant/components/envirophat/manifest.json index 911e7a2fc3582..9bb90facbf389 100644 --- a/homeassistant/components/envirophat/manifest.json +++ b/homeassistant/components/envirophat/manifest.json @@ -3,5 +3,6 @@ "name": "Enviro pHAT", "documentation": "https://www.home-assistant.io/integrations/envirophat", "requirements": ["envirophat==0.0.6", "smbus-cffi==0.5.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/envisalink/manifest.json b/homeassistant/components/envisalink/manifest.json index e45f8140df62a..7ec8628be09f4 100644 --- a/homeassistant/components/envisalink/manifest.json +++ b/homeassistant/components/envisalink/manifest.json @@ -3,5 +3,6 @@ "name": "Envisalink", "documentation": "https://www.home-assistant.io/integrations/envisalink", "requirements": ["pyenvisalink==4.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/ephember/manifest.json b/homeassistant/components/ephember/manifest.json index c03a45a580452..5abbc7b252a98 100644 --- a/homeassistant/components/ephember/manifest.json +++ b/homeassistant/components/ephember/manifest.json @@ -3,5 +3,6 @@ "name": "EPH Controls", "documentation": "https://www.home-assistant.io/integrations/ephember", "requirements": ["pyephember==0.3.1"], - "codeowners": ["@ttroy50"] + "codeowners": ["@ttroy50"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/epson/manifest.json b/homeassistant/components/epson/manifest.json index 809bcf1d65107..b02ef0dddd3ba 100644 --- a/homeassistant/components/epson/manifest.json +++ b/homeassistant/components/epson/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/epson", "requirements": ["epson-projector==0.2.3"], - "codeowners": ["@pszafer"] -} \ No newline at end of file + "codeowners": ["@pszafer"], + "iot_class": "local_polling" +} diff --git a/homeassistant/components/epsonworkforce/manifest.json b/homeassistant/components/epsonworkforce/manifest.json index cd989b9c69023..3fb7f1d598712 100644 --- a/homeassistant/components/epsonworkforce/manifest.json +++ b/homeassistant/components/epsonworkforce/manifest.json @@ -3,5 +3,6 @@ "name": "Epson Workforce", "documentation": "https://www.home-assistant.io/integrations/epsonworkforce", "codeowners": ["@ThaStealth"], - "requirements": ["epsonprinter==0.0.9"] + "requirements": ["epsonprinter==0.0.9"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/eq3btsmart/manifest.json b/homeassistant/components/eq3btsmart/manifest.json index 5f5fefe25ead8..a644ff394e035 100644 --- a/homeassistant/components/eq3btsmart/manifest.json +++ b/homeassistant/components/eq3btsmart/manifest.json @@ -3,5 +3,6 @@ "name": "EQ3 Bluetooth Smart Thermostats", "documentation": "https://www.home-assistant.io/integrations/eq3btsmart", "requirements": ["construct==2.10.56", "python-eq3bt==0.1.11"], - "codeowners": ["@rytilahti"] + "codeowners": ["@rytilahti"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index e3c609c9fad52..2f60c84a828ef 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -6,5 +6,6 @@ "requirements": ["aioesphomeapi==2.6.6"], "zeroconf": ["_esphomelib._tcp.local."], "codeowners": ["@OttoWinter"], - "after_dependencies": ["zeroconf", "tag"] + "after_dependencies": ["zeroconf", "tag"], + "iot_class": "local_push" } diff --git a/homeassistant/components/essent/manifest.json b/homeassistant/components/essent/manifest.json index c90ce5ba6648a..d136cae43a9a3 100644 --- a/homeassistant/components/essent/manifest.json +++ b/homeassistant/components/essent/manifest.json @@ -3,5 +3,6 @@ "name": "Essent", "documentation": "https://www.home-assistant.io/integrations/essent", "requirements": ["PyEssent==0.14"], - "codeowners": ["@TheLastProject"] + "codeowners": ["@TheLastProject"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/etherscan/manifest.json b/homeassistant/components/etherscan/manifest.json index b21f7d0e3fb67..7df8bb8d4f3a4 100644 --- a/homeassistant/components/etherscan/manifest.json +++ b/homeassistant/components/etherscan/manifest.json @@ -3,5 +3,6 @@ "name": "Etherscan", "documentation": "https://www.home-assistant.io/integrations/etherscan", "requirements": ["python-etherscan-api==0.0.3"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/eufy/manifest.json b/homeassistant/components/eufy/manifest.json index 49956b9f0b2c0..525283359c9f1 100644 --- a/homeassistant/components/eufy/manifest.json +++ b/homeassistant/components/eufy/manifest.json @@ -3,5 +3,6 @@ "name": "eufy", "documentation": "https://www.home-assistant.io/integrations/eufy", "requirements": ["lakeside==0.12"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/everlights/manifest.json b/homeassistant/components/everlights/manifest.json index 83cb166296df4..bbb5e09c446ab 100644 --- a/homeassistant/components/everlights/manifest.json +++ b/homeassistant/components/everlights/manifest.json @@ -3,5 +3,6 @@ "name": "EverLights", "documentation": "https://www.home-assistant.io/integrations/everlights", "requirements": ["pyeverlights==0.1.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/evohome/manifest.json b/homeassistant/components/evohome/manifest.json index e707387ce4f00..b9f93c295d665 100644 --- a/homeassistant/components/evohome/manifest.json +++ b/homeassistant/components/evohome/manifest.json @@ -3,5 +3,6 @@ "name": "Honeywell Total Connect Comfort (Europe)", "documentation": "https://www.home-assistant.io/integrations/evohome", "requirements": ["evohome-async==0.3.8"], - "codeowners": ["@zxdavb"] + "codeowners": ["@zxdavb"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/ezviz/manifest.json b/homeassistant/components/ezviz/manifest.json index 32742de203578..46abf8bc99a4c 100644 --- a/homeassistant/components/ezviz/manifest.json +++ b/homeassistant/components/ezviz/manifest.json @@ -5,5 +5,6 @@ "dependencies": ["ffmpeg"], "codeowners": ["@RenierM26", "@baqs"], "requirements": ["pyezviz==0.1.8.7"], - "config_flow": true + "config_flow": true, + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/faa_delays/manifest.json b/homeassistant/components/faa_delays/manifest.json index 7ffe7898b603e..c829ac5b1719e 100644 --- a/homeassistant/components/faa_delays/manifest.json +++ b/homeassistant/components/faa_delays/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/faa_delays", "requirements": ["faadelays==0.0.6"], - "codeowners": ["@ntilley905"] + "codeowners": ["@ntilley905"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/facebook/manifest.json b/homeassistant/components/facebook/manifest.json index 5d44ccc40ce72..6f8412d6b256f 100644 --- a/homeassistant/components/facebook/manifest.json +++ b/homeassistant/components/facebook/manifest.json @@ -2,5 +2,6 @@ "domain": "facebook", "name": "Facebook Messenger", "documentation": "https://www.home-assistant.io/integrations/facebook", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/facebox/manifest.json b/homeassistant/components/facebox/manifest.json index d8a8fb457ea89..359ef95f55e92 100644 --- a/homeassistant/components/facebox/manifest.json +++ b/homeassistant/components/facebox/manifest.json @@ -2,5 +2,6 @@ "domain": "facebox", "name": "Facebox", "documentation": "https://www.home-assistant.io/integrations/facebox", - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/fail2ban/manifest.json b/homeassistant/components/fail2ban/manifest.json index 4d8e50d507b1c..235bebf914a3e 100644 --- a/homeassistant/components/fail2ban/manifest.json +++ b/homeassistant/components/fail2ban/manifest.json @@ -2,5 +2,6 @@ "domain": "fail2ban", "name": "Fail2Ban", "documentation": "https://www.home-assistant.io/integrations/fail2ban", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/familyhub/manifest.json b/homeassistant/components/familyhub/manifest.json index 06acb922eeedc..ecdafb22b56b3 100644 --- a/homeassistant/components/familyhub/manifest.json +++ b/homeassistant/components/familyhub/manifest.json @@ -3,5 +3,6 @@ "name": "Samsung Family Hub", "documentation": "https://www.home-assistant.io/integrations/familyhub", "requirements": ["python-family-hub-local==0.0.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/fastdotcom/manifest.json b/homeassistant/components/fastdotcom/manifest.json index ca7a720668bf1..af68bbf29932e 100644 --- a/homeassistant/components/fastdotcom/manifest.json +++ b/homeassistant/components/fastdotcom/manifest.json @@ -3,5 +3,6 @@ "name": "Fast.com", "documentation": "https://www.home-assistant.io/integrations/fastdotcom", "requirements": ["fastdotcom==0.0.3"], - "codeowners": ["@rohankapoorcom"] + "codeowners": ["@rohankapoorcom"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/feedreader/manifest.json b/homeassistant/components/feedreader/manifest.json index d1bc9cdb52401..66874f760ffb2 100644 --- a/homeassistant/components/feedreader/manifest.json +++ b/homeassistant/components/feedreader/manifest.json @@ -3,5 +3,6 @@ "name": "Feedreader", "documentation": "https://www.home-assistant.io/integrations/feedreader", "requirements": ["feedparser==6.0.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/ffmpeg_motion/manifest.json b/homeassistant/components/ffmpeg_motion/manifest.json index 854bca7f9bdd9..a368107999bc5 100644 --- a/homeassistant/components/ffmpeg_motion/manifest.json +++ b/homeassistant/components/ffmpeg_motion/manifest.json @@ -3,5 +3,6 @@ "name": "FFmpeg Motion", "documentation": "https://www.home-assistant.io/integrations/ffmpeg_motion", "dependencies": ["ffmpeg"], - "codeowners": [] + "codeowners": [], + "iot_class": "calculated" } diff --git a/homeassistant/components/ffmpeg_noise/manifest.json b/homeassistant/components/ffmpeg_noise/manifest.json index b2b4148a02291..f35319b4fd4c4 100644 --- a/homeassistant/components/ffmpeg_noise/manifest.json +++ b/homeassistant/components/ffmpeg_noise/manifest.json @@ -3,5 +3,6 @@ "name": "FFmpeg Noise", "documentation": "https://www.home-assistant.io/integrations/ffmpeg_noise", "dependencies": ["ffmpeg"], - "codeowners": [] + "codeowners": [], + "iot_class": "calculated" } diff --git a/homeassistant/components/fibaro/manifest.json b/homeassistant/components/fibaro/manifest.json index ff6d881009dfd..81eb184549b7c 100644 --- a/homeassistant/components/fibaro/manifest.json +++ b/homeassistant/components/fibaro/manifest.json @@ -3,5 +3,6 @@ "name": "Fibaro", "documentation": "https://www.home-assistant.io/integrations/fibaro", "requirements": ["fiblary3==0.1.7"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/fido/manifest.json b/homeassistant/components/fido/manifest.json index 9c150d479158f..7de047114faaa 100644 --- a/homeassistant/components/fido/manifest.json +++ b/homeassistant/components/fido/manifest.json @@ -3,5 +3,6 @@ "name": "Fido", "documentation": "https://www.home-assistant.io/integrations/fido", "requirements": ["pyfido==2.1.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/file/manifest.json b/homeassistant/components/file/manifest.json index cac7fc98fb1d1..8688ed7939c77 100644 --- a/homeassistant/components/file/manifest.json +++ b/homeassistant/components/file/manifest.json @@ -2,5 +2,6 @@ "domain": "file", "name": "File", "documentation": "https://www.home-assistant.io/integrations/file", - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/filesize/manifest.json b/homeassistant/components/filesize/manifest.json index 6ef52457eaaf1..1db5009b7e4e9 100644 --- a/homeassistant/components/filesize/manifest.json +++ b/homeassistant/components/filesize/manifest.json @@ -2,5 +2,6 @@ "domain": "filesize", "name": "File Size", "documentation": "https://www.home-assistant.io/integrations/filesize", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/filter/manifest.json b/homeassistant/components/filter/manifest.json index 7b474c2b53a6a..d8ca603c5a96a 100644 --- a/homeassistant/components/filter/manifest.json +++ b/homeassistant/components/filter/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/filter", "dependencies": ["history"], "codeowners": ["@dgomes"], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_push" } diff --git a/homeassistant/components/fints/manifest.json b/homeassistant/components/fints/manifest.json index 4a1a7b8f89d66..854f3a2f195f1 100644 --- a/homeassistant/components/fints/manifest.json +++ b/homeassistant/components/fints/manifest.json @@ -3,5 +3,6 @@ "name": "FinTS", "documentation": "https://www.home-assistant.io/integrations/fints", "requirements": ["fints==1.0.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/fireservicerota/manifest.json b/homeassistant/components/fireservicerota/manifest.json index 6485d155f50e4..0e2259b6b5e7e 100644 --- a/homeassistant/components/fireservicerota/manifest.json +++ b/homeassistant/components/fireservicerota/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/fireservicerota", "requirements": ["pyfireservicerota==0.0.40"], - "codeowners": ["@cyberjunky"] + "codeowners": ["@cyberjunky"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/firmata/manifest.json b/homeassistant/components/firmata/manifest.json index 8b283c4f81df9..7af4624669bb0 100644 --- a/homeassistant/components/firmata/manifest.json +++ b/homeassistant/components/firmata/manifest.json @@ -3,10 +3,7 @@ "name": "Firmata", "config_flow": false, "documentation": "https://www.home-assistant.io/integrations/firmata", - "requirements": [ - "pymata-express==1.19" - ], - "codeowners": [ - "@DaAwesomeP" - ] -} \ No newline at end of file + "requirements": ["pymata-express==1.19"], + "codeowners": ["@DaAwesomeP"], + "iot_class": "local_push" +} diff --git a/homeassistant/components/fitbit/manifest.json b/homeassistant/components/fitbit/manifest.json index 1213a29020b34..b848a344f1f3c 100644 --- a/homeassistant/components/fitbit/manifest.json +++ b/homeassistant/components/fitbit/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/fitbit", "requirements": ["fitbit==0.3.1"], "dependencies": ["configurator", "http"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/fixer/manifest.json b/homeassistant/components/fixer/manifest.json index 6dbeae949f210..fa85a0283d86a 100644 --- a/homeassistant/components/fixer/manifest.json +++ b/homeassistant/components/fixer/manifest.json @@ -3,5 +3,6 @@ "name": "Fixer", "documentation": "https://www.home-assistant.io/integrations/fixer", "requirements": ["fixerio==1.0.0a0"], - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/fleetgo/manifest.json b/homeassistant/components/fleetgo/manifest.json index 148d79f45c236..4e4d1200e56fb 100644 --- a/homeassistant/components/fleetgo/manifest.json +++ b/homeassistant/components/fleetgo/manifest.json @@ -3,5 +3,6 @@ "name": "FleetGO", "documentation": "https://www.home-assistant.io/integrations/fleetgo", "requirements": ["ritassist==0.9.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/flexit/manifest.json b/homeassistant/components/flexit/manifest.json index 6c98925ababdf..96ed5b55904f1 100644 --- a/homeassistant/components/flexit/manifest.json +++ b/homeassistant/components/flexit/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/flexit", "requirements": ["pyflexit==0.3"], "dependencies": ["modbus"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/flic/manifest.json b/homeassistant/components/flic/manifest.json index f638908a80f61..c7018199d91bd 100644 --- a/homeassistant/components/flic/manifest.json +++ b/homeassistant/components/flic/manifest.json @@ -3,5 +3,6 @@ "name": "Flic", "documentation": "https://www.home-assistant.io/integrations/flic", "requirements": ["pyflic-homeassistant==0.4.dev0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/flick_electric/manifest.json b/homeassistant/components/flick_electric/manifest.json index 6eb5a2e58f96c..75511aba4a1d9 100644 --- a/homeassistant/components/flick_electric/manifest.json +++ b/homeassistant/components/flick_electric/manifest.json @@ -3,10 +3,7 @@ "name": "Flick Electric", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flick_electric/", - "requirements": [ - "PyFlick==0.0.2" - ], - "codeowners": [ - "@ZephireNZ" - ] -} \ No newline at end of file + "requirements": ["PyFlick==0.0.2"], + "codeowners": ["@ZephireNZ"], + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/flo/manifest.json b/homeassistant/components/flo/manifest.json index 81505ed8d14fc..11972f5056b8d 100644 --- a/homeassistant/components/flo/manifest.json +++ b/homeassistant/components/flo/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flo", "requirements": ["aioflo==0.4.1"], - "codeowners": ["@dmulcahey"] + "codeowners": ["@dmulcahey"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/flock/manifest.json b/homeassistant/components/flock/manifest.json index 29328cfd1f6c6..ddbb2bb201cc9 100644 --- a/homeassistant/components/flock/manifest.json +++ b/homeassistant/components/flock/manifest.json @@ -2,5 +2,6 @@ "domain": "flock", "name": "Flock", "documentation": "https://www.home-assistant.io/integrations/flock", - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/flume/manifest.json b/homeassistant/components/flume/manifest.json index 813b8788ed5e1..1f6d7a38a47cd 100644 --- a/homeassistant/components/flume/manifest.json +++ b/homeassistant/components/flume/manifest.json @@ -6,7 +6,14 @@ "codeowners": ["@ChrisMandich", "@bdraco"], "config_flow": true, "dhcp": [ - {"hostname":"flume-gw-*","macaddress":"ECFABC*"}, - {"hostname":"flume-gw-*","macaddress":"B4E62D*"} - ] + { + "hostname": "flume-gw-*", + "macaddress": "ECFABC*" + }, + { + "hostname": "flume-gw-*", + "macaddress": "B4E62D*" + } + ], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/flunearyou/manifest.json b/homeassistant/components/flunearyou/manifest.json index f6cc6714a38a1..71f0b49771e82 100644 --- a/homeassistant/components/flunearyou/manifest.json +++ b/homeassistant/components/flunearyou/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flunearyou", "requirements": ["pyflunearyou==1.0.7"], - "codeowners": ["@bachya"] + "codeowners": ["@bachya"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/flux/manifest.json b/homeassistant/components/flux/manifest.json index 400331f9f5fa5..be136f044120a 100644 --- a/homeassistant/components/flux/manifest.json +++ b/homeassistant/components/flux/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/flux", "after_dependencies": ["light"], "codeowners": [], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "calculated" } diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 378860229eebc..0c6d8ae8db161 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,5 +3,6 @@ "name": "Flux LED/MagicLight", "documentation": "https://www.home-assistant.io/integrations/flux_led", "requirements": ["flux_led==0.22"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/folder/manifest.json b/homeassistant/components/folder/manifest.json index 810a26bc1e054..5ee65f17d0f6a 100644 --- a/homeassistant/components/folder/manifest.json +++ b/homeassistant/components/folder/manifest.json @@ -2,5 +2,6 @@ "domain": "folder", "name": "Folder", "documentation": "https://www.home-assistant.io/integrations/folder", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/folder_watcher/manifest.json b/homeassistant/components/folder_watcher/manifest.json index ebb0ab947f5fe..6263a0495b749 100644 --- a/homeassistant/components/folder_watcher/manifest.json +++ b/homeassistant/components/folder_watcher/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/folder_watcher", "requirements": ["watchdog==2.0.2"], "codeowners": [], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_polling" } diff --git a/homeassistant/components/foobot/manifest.json b/homeassistant/components/foobot/manifest.json index 09458a18d9161..b32ff6b4c8aab 100644 --- a/homeassistant/components/foobot/manifest.json +++ b/homeassistant/components/foobot/manifest.json @@ -3,5 +3,6 @@ "name": "Foobot", "documentation": "https://www.home-assistant.io/integrations/foobot", "requirements": ["foobot_async==1.0.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/forked_daapd/manifest.json b/homeassistant/components/forked_daapd/manifest.json index b9f78875a2d65..b802eac13c8d2 100644 --- a/homeassistant/components/forked_daapd/manifest.json +++ b/homeassistant/components/forked_daapd/manifest.json @@ -5,5 +5,6 @@ "codeowners": ["@uvjustin"], "requirements": ["pyforked-daapd==0.1.11", "pylibrespot-java==0.1.0"], "config_flow": true, - "zeroconf": ["_daap._tcp.local."] + "zeroconf": ["_daap._tcp.local."], + "iot_class": "local_push" } diff --git a/homeassistant/components/fortios/manifest.json b/homeassistant/components/fortios/manifest.json index e0ca2671b1971..251cb900adc65 100644 --- a/homeassistant/components/fortios/manifest.json +++ b/homeassistant/components/fortios/manifest.json @@ -3,5 +3,6 @@ "name": "FortiOS", "documentation": "https://www.home-assistant.io/integrations/fortios/", "requirements": ["fortiosapi==0.10.8"], - "codeowners": ["@kimfrellsen"] + "codeowners": ["@kimfrellsen"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/foscam/manifest.json b/homeassistant/components/foscam/manifest.json index fdd050d513345..e2d9e5e501daa 100644 --- a/homeassistant/components/foscam/manifest.json +++ b/homeassistant/components/foscam/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/foscam", "requirements": ["libpyfoscam==1.0"], - "codeowners": ["@skgsergio"] + "codeowners": ["@skgsergio"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/foursquare/manifest.json b/homeassistant/components/foursquare/manifest.json index 98ce65b5f636f..c76481a289f15 100644 --- a/homeassistant/components/foursquare/manifest.json +++ b/homeassistant/components/foursquare/manifest.json @@ -3,5 +3,6 @@ "name": "Foursquare", "documentation": "https://www.home-assistant.io/integrations/foursquare", "dependencies": ["http"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/free_mobile/manifest.json b/homeassistant/components/free_mobile/manifest.json index 1cdef3d1162aa..ea6ea921a38a8 100644 --- a/homeassistant/components/free_mobile/manifest.json +++ b/homeassistant/components/free_mobile/manifest.json @@ -3,5 +3,6 @@ "name": "Free Mobile", "documentation": "https://www.home-assistant.io/integrations/free_mobile", "requirements": ["freesms==0.1.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/freebox/manifest.json b/homeassistant/components/freebox/manifest.json index 2d55553511bd0..254be7b685766 100644 --- a/homeassistant/components/freebox/manifest.json +++ b/homeassistant/components/freebox/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/freebox", "requirements": ["freebox-api==0.0.10"], "zeroconf": ["_fbx-api._tcp.local."], - "codeowners": ["@hacf-fr", "@Quentame"] + "codeowners": ["@hacf-fr", "@Quentame"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/freedns/manifest.json b/homeassistant/components/freedns/manifest.json index 58e8e9fdaf822..0f7e27ae24e8b 100644 --- a/homeassistant/components/freedns/manifest.json +++ b/homeassistant/components/freedns/manifest.json @@ -2,5 +2,6 @@ "domain": "freedns", "name": "FreeDNS", "documentation": "https://www.home-assistant.io/integrations/freedns", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/fritz/manifest.json b/homeassistant/components/fritz/manifest.json index 45b73cf58ee07..0b9a2a8302d1c 100644 --- a/homeassistant/components/fritz/manifest.json +++ b/homeassistant/components/fritz/manifest.json @@ -3,5 +3,6 @@ "name": "AVM FRITZ!Box", "documentation": "https://www.home-assistant.io/integrations/fritz", "requirements": ["fritzconnection==1.4.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/fritzbox/manifest.json b/homeassistant/components/fritzbox/manifest.json index 6b1bbdc4af57e..4a56d68e1700e 100644 --- a/homeassistant/components/fritzbox/manifest.json +++ b/homeassistant/components/fritzbox/manifest.json @@ -9,5 +9,6 @@ } ], "codeowners": [], - "config_flow": true + "config_flow": true, + "iot_class": "local_polling" } diff --git a/homeassistant/components/fritzbox_callmonitor/manifest.json b/homeassistant/components/fritzbox_callmonitor/manifest.json index 256292c88f7b2..6c92cfab458aa 100644 --- a/homeassistant/components/fritzbox_callmonitor/manifest.json +++ b/homeassistant/components/fritzbox_callmonitor/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/fritzbox_callmonitor", "requirements": ["fritzconnection==1.4.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/fritzbox_netmonitor/manifest.json b/homeassistant/components/fritzbox_netmonitor/manifest.json index d2fe23a8112db..d0406c99dfab5 100644 --- a/homeassistant/components/fritzbox_netmonitor/manifest.json +++ b/homeassistant/components/fritzbox_netmonitor/manifest.json @@ -3,5 +3,6 @@ "name": "AVM FRITZ!Box Net Monitor", "documentation": "https://www.home-assistant.io/integrations/fritzbox_netmonitor", "requirements": ["fritzconnection==1.4.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/fronius/manifest.json b/homeassistant/components/fronius/manifest.json index 8f94e816505c7..4f48bc1aecc05 100644 --- a/homeassistant/components/fronius/manifest.json +++ b/homeassistant/components/fronius/manifest.json @@ -3,5 +3,6 @@ "name": "Fronius", "documentation": "https://www.home-assistant.io/integrations/fronius", "requirements": ["pyfronius==0.4.6"], - "codeowners": ["@nielstron"] + "codeowners": ["@nielstron"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index b69ee769d6689..2a90a867ce3b9 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,9 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": [ - "home-assistant-frontend==20210407.3" - ], + "requirements": ["home-assistant-frontend==20210407.3"], "dependencies": [ "api", "auth", @@ -17,8 +15,6 @@ "system_log", "websocket_api" ], - "codeowners": [ - "@home-assistant/frontend" - ], + "codeowners": ["@home-assistant/frontend"], "quality_scale": "internal" -} \ No newline at end of file +} diff --git a/homeassistant/components/frontier_silicon/manifest.json b/homeassistant/components/frontier_silicon/manifest.json index 4e52eee995495..3eb982e8118ca 100644 --- a/homeassistant/components/frontier_silicon/manifest.json +++ b/homeassistant/components/frontier_silicon/manifest.json @@ -3,5 +3,6 @@ "name": "Frontier Silicon", "documentation": "https://www.home-assistant.io/integrations/frontier_silicon", "requirements": ["afsapi==0.0.4"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/futurenow/manifest.json b/homeassistant/components/futurenow/manifest.json index c8f07a106e2de..853849b27337f 100644 --- a/homeassistant/components/futurenow/manifest.json +++ b/homeassistant/components/futurenow/manifest.json @@ -3,5 +3,6 @@ "name": "P5 FutureNow", "documentation": "https://www.home-assistant.io/integrations/futurenow", "requirements": ["pyfnip==0.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/garadget/manifest.json b/homeassistant/components/garadget/manifest.json index 21d33405c843e..7dd6e418eafd1 100644 --- a/homeassistant/components/garadget/manifest.json +++ b/homeassistant/components/garadget/manifest.json @@ -2,5 +2,6 @@ "domain": "garadget", "name": "Garadget", "documentation": "https://www.home-assistant.io/integrations/garadget", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/garmin_connect/manifest.json b/homeassistant/components/garmin_connect/manifest.json index 59597750ce812..913e85de95494 100644 --- a/homeassistant/components/garmin_connect/manifest.json +++ b/homeassistant/components/garmin_connect/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/garmin_connect", "requirements": ["garminconnect==0.1.19"], "codeowners": ["@cyberjunky"], - "config_flow": true + "config_flow": true, + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/gc100/manifest.json b/homeassistant/components/gc100/manifest.json index e2dffb1e09000..55ea7d946822a 100644 --- a/homeassistant/components/gc100/manifest.json +++ b/homeassistant/components/gc100/manifest.json @@ -1,7 +1,8 @@ { "domain": "gc100", - "name": "Global Caché GC-100", + "name": "Global Cach\u00e9 GC-100", "documentation": "https://www.home-assistant.io/integrations/gc100", "requirements": ["python-gc100==1.0.3a"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/gdacs/manifest.json b/homeassistant/components/gdacs/manifest.json index 1b6356d21e8f0..26743a69d682a 100644 --- a/homeassistant/components/gdacs/manifest.json +++ b/homeassistant/components/gdacs/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/gdacs", "requirements": ["aio_georss_gdacs==0.4"], "codeowners": ["@exxamalte"], - "quality_scale": "platinum" + "quality_scale": "platinum", + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/generic/manifest.json b/homeassistant/components/generic/manifest.json index a066333679d0c..8ab7bec48ac70 100644 --- a/homeassistant/components/generic/manifest.json +++ b/homeassistant/components/generic/manifest.json @@ -2,5 +2,6 @@ "domain": "generic", "name": "Generic", "documentation": "https://www.home-assistant.io/integrations/generic", - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/generic_thermostat/manifest.json b/homeassistant/components/generic_thermostat/manifest.json index 011c3f595927c..82800a196ddfe 100644 --- a/homeassistant/components/generic_thermostat/manifest.json +++ b/homeassistant/components/generic_thermostat/manifest.json @@ -3,5 +3,6 @@ "name": "Generic Thermostat", "documentation": "https://www.home-assistant.io/integrations/generic_thermostat", "dependencies": ["sensor", "switch"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/geniushub/manifest.json b/homeassistant/components/geniushub/manifest.json index b4a72d8831598..698da72c3f461 100644 --- a/homeassistant/components/geniushub/manifest.json +++ b/homeassistant/components/geniushub/manifest.json @@ -3,5 +3,6 @@ "name": "Genius Hub", "documentation": "https://www.home-assistant.io/integrations/geniushub", "requirements": ["geniushub-client==0.6.30"], - "codeowners": ["@zxdavb"] + "codeowners": ["@zxdavb"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/geo_json_events/manifest.json b/homeassistant/components/geo_json_events/manifest.json index 4cf99155b37ed..5d898ee99d501 100644 --- a/homeassistant/components/geo_json_events/manifest.json +++ b/homeassistant/components/geo_json_events/manifest.json @@ -3,5 +3,6 @@ "name": "GeoJSON", "documentation": "https://www.home-assistant.io/integrations/geo_json_events", "requirements": ["geojson_client==0.4"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/geo_rss_events/manifest.json b/homeassistant/components/geo_rss_events/manifest.json index 4a434aed8d765..e7ac2948237b1 100644 --- a/homeassistant/components/geo_rss_events/manifest.json +++ b/homeassistant/components/geo_rss_events/manifest.json @@ -3,5 +3,6 @@ "name": "GeoRSS", "documentation": "https://www.home-assistant.io/integrations/geo_rss_events", "requirements": ["georss_generic_client==0.4"], - "codeowners": ["@exxamalte"] + "codeowners": ["@exxamalte"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/geofency/manifest.json b/homeassistant/components/geofency/manifest.json index 0fbc30444557f..40cf9a7f07f4f 100644 --- a/homeassistant/components/geofency/manifest.json +++ b/homeassistant/components/geofency/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/geofency", "dependencies": ["webhook"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/geonetnz_quakes/manifest.json b/homeassistant/components/geonetnz_quakes/manifest.json index 1e61d52604729..64a78c02d25de 100644 --- a/homeassistant/components/geonetnz_quakes/manifest.json +++ b/homeassistant/components/geonetnz_quakes/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/geonetnz_quakes", "requirements": ["aio_geojson_geonetnz_quakes==0.12"], "codeowners": ["@exxamalte"], - "quality_scale": "platinum" + "quality_scale": "platinum", + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/geonetnz_volcano/manifest.json b/homeassistant/components/geonetnz_volcano/manifest.json index 13e1e9baf3e92..ed0ebccf6201d 100644 --- a/homeassistant/components/geonetnz_volcano/manifest.json +++ b/homeassistant/components/geonetnz_volcano/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/geonetnz_volcano", "requirements": ["aio_geojson_geonetnz_volcano==0.5"], - "codeowners": ["@exxamalte"] + "codeowners": ["@exxamalte"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/gios/manifest.json b/homeassistant/components/gios/manifest.json index 3f520525a5a0f..f0d5422de24fe 100644 --- a/homeassistant/components/gios/manifest.json +++ b/homeassistant/components/gios/manifest.json @@ -1,9 +1,10 @@ { "domain": "gios", - "name": "GIOŚ", + "name": "GIO\u015a", "documentation": "https://www.home-assistant.io/integrations/gios", "codeowners": ["@bieniu"], "requirements": ["gios==0.2.1"], "config_flow": true, - "quality_scale": "platinum" + "quality_scale": "platinum", + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/github/manifest.json b/homeassistant/components/github/manifest.json index 1a9cd620b0e8f..d4405196b7ac7 100644 --- a/homeassistant/components/github/manifest.json +++ b/homeassistant/components/github/manifest.json @@ -3,5 +3,6 @@ "name": "GitHub", "documentation": "https://www.home-assistant.io/integrations/github", "requirements": ["PyGithub==1.43.8"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/gitlab_ci/manifest.json b/homeassistant/components/gitlab_ci/manifest.json index 5061d35c18903..77852e6d9828d 100644 --- a/homeassistant/components/gitlab_ci/manifest.json +++ b/homeassistant/components/gitlab_ci/manifest.json @@ -3,5 +3,6 @@ "name": "GitLab-CI", "documentation": "https://www.home-assistant.io/integrations/gitlab_ci", "requirements": ["python-gitlab==1.6.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/gitter/manifest.json b/homeassistant/components/gitter/manifest.json index c1c13af792a1d..bbf02d1ec9eac 100644 --- a/homeassistant/components/gitter/manifest.json +++ b/homeassistant/components/gitter/manifest.json @@ -3,5 +3,6 @@ "name": "Gitter", "documentation": "https://www.home-assistant.io/integrations/gitter", "requirements": ["gitterpy==0.1.7"], - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/glances/manifest.json b/homeassistant/components/glances/manifest.json index b50601ae835e5..71e861cc69eb9 100644 --- a/homeassistant/components/glances/manifest.json +++ b/homeassistant/components/glances/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/glances", "requirements": ["glances_api==0.2.0"], - "codeowners": ["@fabaff", "@engrbm87"] + "codeowners": ["@fabaff", "@engrbm87"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/gntp/manifest.json b/homeassistant/components/gntp/manifest.json index 5785c6337491b..ebef78f9e7fd1 100644 --- a/homeassistant/components/gntp/manifest.json +++ b/homeassistant/components/gntp/manifest.json @@ -3,5 +3,6 @@ "name": "Growl (GnGNTP)", "documentation": "https://www.home-assistant.io/integrations/gntp", "requirements": ["gntp==1.0.3"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/goalfeed/manifest.json b/homeassistant/components/goalfeed/manifest.json index d07c7c2df7efd..5b064551cf922 100644 --- a/homeassistant/components/goalfeed/manifest.json +++ b/homeassistant/components/goalfeed/manifest.json @@ -3,5 +3,6 @@ "name": "Goalfeed", "documentation": "https://www.home-assistant.io/integrations/goalfeed", "requirements": ["pysher==1.0.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/goalzero/manifest.json b/homeassistant/components/goalzero/manifest.json index 803b8f7eaae01..405fbaf734227 100644 --- a/homeassistant/components/goalzero/manifest.json +++ b/homeassistant/components/goalzero/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/goalzero", "requirements": ["goalzero==0.1.4"], - "codeowners": ["@tkdrob"] + "codeowners": ["@tkdrob"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/gogogate2/manifest.json b/homeassistant/components/gogogate2/manifest.json index b21eeace46670..519291c40d1ad 100644 --- a/homeassistant/components/gogogate2/manifest.json +++ b/homeassistant/components/gogogate2/manifest.json @@ -6,8 +6,7 @@ "requirements": ["gogogate2-api==3.0.0"], "codeowners": ["@vangorra"], "homekit": { - "models": [ - "iSmartGate" - ] - } + "models": ["iSmartGate"] + }, + "iot_class": "local_polling" } diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index 859f1b332960b..9b6f7d77f2698 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -7,5 +7,6 @@ "httplib2==0.19.0", "oauth2client==4.0.0" ], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/google_assistant/manifest.json b/homeassistant/components/google_assistant/manifest.json index eef58106bd0d3..fcd7c983937eb 100644 --- a/homeassistant/components/google_assistant/manifest.json +++ b/homeassistant/components/google_assistant/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/google_assistant", "dependencies": ["http"], "after_dependencies": ["camera"], - "codeowners": ["@home-assistant/cloud"] + "codeowners": ["@home-assistant/cloud"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/google_cloud/manifest.json b/homeassistant/components/google_cloud/manifest.json index 12d761786d3a4..90c5eebaeb237 100644 --- a/homeassistant/components/google_cloud/manifest.json +++ b/homeassistant/components/google_cloud/manifest.json @@ -3,5 +3,6 @@ "name": "Google Cloud Platform", "documentation": "https://www.home-assistant.io/integrations/google_cloud", "requirements": ["google-cloud-texttospeech==0.4.0"], - "codeowners": ["@lufton"] + "codeowners": ["@lufton"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/google_domains/manifest.json b/homeassistant/components/google_domains/manifest.json index 3372bb3f97df5..296b07b08afbf 100644 --- a/homeassistant/components/google_domains/manifest.json +++ b/homeassistant/components/google_domains/manifest.json @@ -2,5 +2,6 @@ "domain": "google_domains", "name": "Google Domains", "documentation": "https://www.home-assistant.io/integrations/google_domains", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/google_maps/manifest.json b/homeassistant/components/google_maps/manifest.json index 435e01fb0268d..f0f403912a630 100644 --- a/homeassistant/components/google_maps/manifest.json +++ b/homeassistant/components/google_maps/manifest.json @@ -3,5 +3,6 @@ "name": "Google Maps", "documentation": "https://www.home-assistant.io/integrations/google_maps", "requirements": ["locationsharinglib==4.1.5"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/google_pubsub/manifest.json b/homeassistant/components/google_pubsub/manifest.json index 717a52dd62382..1a289e04bed17 100644 --- a/homeassistant/components/google_pubsub/manifest.json +++ b/homeassistant/components/google_pubsub/manifest.json @@ -3,5 +3,6 @@ "name": "Google Pub/Sub", "documentation": "https://www.home-assistant.io/integrations/google_pubsub", "requirements": ["google-cloud-pubsub==2.1.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/google_translate/manifest.json b/homeassistant/components/google_translate/manifest.json index 64d19bed27722..890479f9ffdc1 100644 --- a/homeassistant/components/google_translate/manifest.json +++ b/homeassistant/components/google_translate/manifest.json @@ -3,5 +3,6 @@ "name": "Google Translate Text-to-Speech", "documentation": "https://www.home-assistant.io/integrations/google_translate", "requirements": ["gTTS==2.2.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/google_travel_time/manifest.json b/homeassistant/components/google_travel_time/manifest.json index d8981fe4283d4..8800b4ef4b8a4 100644 --- a/homeassistant/components/google_travel_time/manifest.json +++ b/homeassistant/components/google_travel_time/manifest.json @@ -2,9 +2,8 @@ "domain": "google_travel_time", "name": "Google Maps Travel Time", "documentation": "https://www.home-assistant.io/integrations/google_travel_time", - "requirements": [ - "googlemaps==2.5.1" - ], + "requirements": ["googlemaps==2.5.1"], "codeowners": [], - "config_flow": true -} \ No newline at end of file + "config_flow": true, + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/google_wifi/manifest.json b/homeassistant/components/google_wifi/manifest.json index 285152239d38c..8566e51f77180 100644 --- a/homeassistant/components/google_wifi/manifest.json +++ b/homeassistant/components/google_wifi/manifest.json @@ -2,5 +2,6 @@ "domain": "google_wifi", "name": "Google Wifi", "documentation": "https://www.home-assistant.io/integrations/google_wifi", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/gpmdp/manifest.json b/homeassistant/components/gpmdp/manifest.json index c2128b27eeba2..2b65226b0c141 100644 --- a/homeassistant/components/gpmdp/manifest.json +++ b/homeassistant/components/gpmdp/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/gpmdp", "requirements": ["websocket-client==0.54.0"], "dependencies": ["configurator"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/gpsd/manifest.json b/homeassistant/components/gpsd/manifest.json index 2a2bf0ffd3661..9053bb7ddfcdc 100644 --- a/homeassistant/components/gpsd/manifest.json +++ b/homeassistant/components/gpsd/manifest.json @@ -3,5 +3,6 @@ "name": "GPSD", "documentation": "https://www.home-assistant.io/integrations/gpsd", "requirements": ["gps3==0.33.3"], - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/gpslogger/manifest.json b/homeassistant/components/gpslogger/manifest.json index 9afbed0d684bb..41f3caa07e577 100644 --- a/homeassistant/components/gpslogger/manifest.json +++ b/homeassistant/components/gpslogger/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/gpslogger", "dependencies": ["webhook"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/graphite/manifest.json b/homeassistant/components/graphite/manifest.json index 4fed461907780..66d148c3cc47d 100644 --- a/homeassistant/components/graphite/manifest.json +++ b/homeassistant/components/graphite/manifest.json @@ -2,5 +2,6 @@ "domain": "graphite", "name": "Graphite", "documentation": "https://www.home-assistant.io/integrations/graphite", - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/gree/manifest.json b/homeassistant/components/gree/manifest.json index c163fc152fdd5..58ddb62216b5e 100644 --- a/homeassistant/components/gree/manifest.json +++ b/homeassistant/components/gree/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/gree", "requirements": ["greeclimate==0.11.4"], - "codeowners": ["@cmroche"] -} \ No newline at end of file + "codeowners": ["@cmroche"], + "iot_class": "local_polling" +} diff --git a/homeassistant/components/greeneye_monitor/manifest.json b/homeassistant/components/greeneye_monitor/manifest.json index ddced4d168be7..628a91774f4fd 100644 --- a/homeassistant/components/greeneye_monitor/manifest.json +++ b/homeassistant/components/greeneye_monitor/manifest.json @@ -3,5 +3,6 @@ "name": "GreenEye Monitor (GEM)", "documentation": "https://www.home-assistant.io/integrations/greeneye_monitor", "requirements": ["greeneye_monitor==2.1"], - "codeowners": ["@jkeljo"] + "codeowners": ["@jkeljo"], + "iot_class": "local_push" } diff --git a/homeassistant/components/greenwave/manifest.json b/homeassistant/components/greenwave/manifest.json index b0076058833f8..3d9aca1a0f90d 100644 --- a/homeassistant/components/greenwave/manifest.json +++ b/homeassistant/components/greenwave/manifest.json @@ -3,5 +3,6 @@ "name": "Greenwave Reality", "documentation": "https://www.home-assistant.io/integrations/greenwave", "requirements": ["greenwavereality==0.5.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/group/manifest.json b/homeassistant/components/group/manifest.json index 692267817f99c..6d8fd446c27a1 100644 --- a/homeassistant/components/group/manifest.json +++ b/homeassistant/components/group/manifest.json @@ -3,5 +3,6 @@ "name": "Group", "documentation": "https://www.home-assistant.io/integrations/group", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "calculated" } diff --git a/homeassistant/components/growatt_server/manifest.json b/homeassistant/components/growatt_server/manifest.json index 8da456aa76a69..f3376ba4ae2d6 100644 --- a/homeassistant/components/growatt_server/manifest.json +++ b/homeassistant/components/growatt_server/manifest.json @@ -3,5 +3,6 @@ "name": "Growatt", "documentation": "https://www.home-assistant.io/integrations/growatt_server/", "requirements": ["growattServer==1.0.0"], - "codeowners": ["@indykoning", "@muppet3000"] + "codeowners": ["@indykoning", "@muppet3000"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/gstreamer/manifest.json b/homeassistant/components/gstreamer/manifest.json index 691d26ce009cf..9957e4602bd23 100644 --- a/homeassistant/components/gstreamer/manifest.json +++ b/homeassistant/components/gstreamer/manifest.json @@ -3,5 +3,6 @@ "name": "GStreamer", "documentation": "https://www.home-assistant.io/integrations/gstreamer", "requirements": ["gstreamer-player==1.1.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/gtfs/manifest.json b/homeassistant/components/gtfs/manifest.json index 2544e8cc7d9b7..d987899463f79 100644 --- a/homeassistant/components/gtfs/manifest.json +++ b/homeassistant/components/gtfs/manifest.json @@ -3,5 +3,6 @@ "name": "General Transit Feed Specification (GTFS)", "documentation": "https://www.home-assistant.io/integrations/gtfs", "requirements": ["pygtfs==0.1.5"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/guardian/manifest.json b/homeassistant/components/guardian/manifest.json index f1fa9c73e5d73..4bc889f4ab053 100644 --- a/homeassistant/components/guardian/manifest.json +++ b/homeassistant/components/guardian/manifest.json @@ -3,14 +3,9 @@ "name": "Elexa Guardian", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/guardian", - "requirements": [ - "aioguardian==1.0.4" - ], - "zeroconf": [ - "_api._udp.local." - ], + "requirements": ["aioguardian==1.0.4"], + "zeroconf": ["_api._udp.local."], "homekit": {}, - "codeowners": [ - "@bachya" - ] + "codeowners": ["@bachya"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/habitica/manifest.json b/homeassistant/components/habitica/manifest.json index 0779a2d324864..4967a6e87ba81 100644 --- a/homeassistant/components/habitica/manifest.json +++ b/homeassistant/components/habitica/manifest.json @@ -1,8 +1,9 @@ { - "domain": "habitica", - "name": "Habitica", - "config_flow": true, - "documentation": "https://www.home-assistant.io/integrations/habitica", - "requirements": ["habitipy==0.2.0"], - "codeowners": ["@ASMfreaK", "@leikoilja"] + "domain": "habitica", + "name": "Habitica", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/habitica", + "requirements": ["habitipy==0.2.0"], + "codeowners": ["@ASMfreaK", "@leikoilja"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/hangouts/manifest.json b/homeassistant/components/hangouts/manifest.json index a2605124dc40f..69cfa515c02b0 100644 --- a/homeassistant/components/hangouts/manifest.json +++ b/homeassistant/components/hangouts/manifest.json @@ -3,8 +3,7 @@ "name": "Google Hangouts", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hangouts", - "requirements": [ - "hangups==0.4.11" - ], - "codeowners": [] + "requirements": ["hangups==0.4.11"], + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/harman_kardon_avr/manifest.json b/homeassistant/components/harman_kardon_avr/manifest.json index 906b8ab266246..a7f4fffa4d682 100644 --- a/homeassistant/components/harman_kardon_avr/manifest.json +++ b/homeassistant/components/harman_kardon_avr/manifest.json @@ -3,5 +3,6 @@ "name": "Harman Kardon AVR", "documentation": "https://www.home-assistant.io/integrations/harman_kardon_avr", "requirements": ["hkavr==0.0.5"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/harmony/manifest.json b/homeassistant/components/harmony/manifest.json index eb7a99fffa8ea..e28d525539b30 100644 --- a/homeassistant/components/harmony/manifest.json +++ b/homeassistant/components/harmony/manifest.json @@ -11,5 +11,6 @@ } ], "dependencies": ["remote", "switch"], - "config_flow": true + "config_flow": true, + "iot_class": "local_push" } diff --git a/homeassistant/components/hassio/manifest.json b/homeassistant/components/hassio/manifest.json index ba969a4af3ac6..aaa5b3669ade8 100644 --- a/homeassistant/components/hassio/manifest.json +++ b/homeassistant/components/hassio/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/hassio", "dependencies": ["http"], "after_dependencies": ["panel_custom"], - "codeowners": ["@home-assistant/supervisor"] + "codeowners": ["@home-assistant/supervisor"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/haveibeenpwned/manifest.json b/homeassistant/components/haveibeenpwned/manifest.json index 255124eb133a3..12344b759d152 100644 --- a/homeassistant/components/haveibeenpwned/manifest.json +++ b/homeassistant/components/haveibeenpwned/manifest.json @@ -2,5 +2,6 @@ "domain": "haveibeenpwned", "name": "HaveIBeenPwned", "documentation": "https://www.home-assistant.io/integrations/haveibeenpwned", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/hddtemp/manifest.json b/homeassistant/components/hddtemp/manifest.json index d72103f202644..32e0ab8604bfc 100644 --- a/homeassistant/components/hddtemp/manifest.json +++ b/homeassistant/components/hddtemp/manifest.json @@ -2,5 +2,6 @@ "domain": "hddtemp", "name": "hddtemp", "documentation": "https://www.home-assistant.io/integrations/hddtemp", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/hdmi_cec/manifest.json b/homeassistant/components/hdmi_cec/manifest.json index 4f6975f52df83..08797541eed58 100644 --- a/homeassistant/components/hdmi_cec/manifest.json +++ b/homeassistant/components/hdmi_cec/manifest.json @@ -3,5 +3,6 @@ "name": "HDMI-CEC", "documentation": "https://www.home-assistant.io/integrations/hdmi_cec", "requirements": ["pyCEC==0.5.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/heatmiser/manifest.json b/homeassistant/components/heatmiser/manifest.json index 065cfc9f6a2e6..772171660529b 100644 --- a/homeassistant/components/heatmiser/manifest.json +++ b/homeassistant/components/heatmiser/manifest.json @@ -3,5 +3,6 @@ "name": "Heatmiser", "documentation": "https://www.home-assistant.io/integrations/heatmiser", "requirements": ["heatmiserV3==1.1.18"], - "codeowners": ["@andylockran"] + "codeowners": ["@andylockran"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/heos/manifest.json b/homeassistant/components/heos/manifest.json index 6505a56456006..94794bf536d84 100644 --- a/homeassistant/components/heos/manifest.json +++ b/homeassistant/components/heos/manifest.json @@ -9,5 +9,6 @@ "st": "urn:schemas-denon-com:device:ACT-Denon:1" } ], - "codeowners": ["@andrewsayre"] + "codeowners": ["@andrewsayre"], + "iot_class": "local_push" } diff --git a/homeassistant/components/here_travel_time/manifest.json b/homeassistant/components/here_travel_time/manifest.json index 151211eef795f..9a3e8bd482717 100644 --- a/homeassistant/components/here_travel_time/manifest.json +++ b/homeassistant/components/here_travel_time/manifest.json @@ -3,5 +3,6 @@ "name": "HERE Travel Time", "documentation": "https://www.home-assistant.io/integrations/here_travel_time", "requirements": ["herepy==2.0.0"], - "codeowners": ["@eifinger"] + "codeowners": ["@eifinger"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/hikvision/manifest.json b/homeassistant/components/hikvision/manifest.json index 8abe4519166a0..9676870ecc48e 100644 --- a/homeassistant/components/hikvision/manifest.json +++ b/homeassistant/components/hikvision/manifest.json @@ -3,5 +3,6 @@ "name": "Hikvision", "documentation": "https://www.home-assistant.io/integrations/hikvision", "requirements": ["pyhik==0.2.8"], - "codeowners": ["@mezz64"] + "codeowners": ["@mezz64"], + "iot_class": "local_push" } diff --git a/homeassistant/components/hikvisioncam/manifest.json b/homeassistant/components/hikvisioncam/manifest.json index 1a08487fa3a1d..61c629655cecf 100644 --- a/homeassistant/components/hikvisioncam/manifest.json +++ b/homeassistant/components/hikvisioncam/manifest.json @@ -3,5 +3,6 @@ "name": "Hikvision", "documentation": "https://www.home-assistant.io/integrations/hikvisioncam", "requirements": ["hikvision==0.4"], - "codeowners": ["@fbradyirl"] + "codeowners": ["@fbradyirl"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/hisense_aehw4a1/manifest.json b/homeassistant/components/hisense_aehw4a1/manifest.json index 00afa0d1de24b..514ee712710b8 100644 --- a/homeassistant/components/hisense_aehw4a1/manifest.json +++ b/homeassistant/components/hisense_aehw4a1/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hisense_aehw4a1", "requirements": ["pyaehw4a1==0.3.9"], - "codeowners": ["@bannhead"] + "codeowners": ["@bannhead"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/history_stats/manifest.json b/homeassistant/components/history_stats/manifest.json index dad7cfa6a5a0b..1f6e8822e6489 100644 --- a/homeassistant/components/history_stats/manifest.json +++ b/homeassistant/components/history_stats/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/history_stats", "dependencies": ["history"], "codeowners": [], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_polling" } diff --git a/homeassistant/components/hitron_coda/manifest.json b/homeassistant/components/hitron_coda/manifest.json index 609e217128060..41f9b5209ebe8 100644 --- a/homeassistant/components/hitron_coda/manifest.json +++ b/homeassistant/components/hitron_coda/manifest.json @@ -2,5 +2,6 @@ "domain": "hitron_coda", "name": "Rogers Hitron CODA", "documentation": "https://www.home-assistant.io/integrations/hitron_coda", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/hive/manifest.json b/homeassistant/components/hive/manifest.json index a1d74c023f1bb..e09e06c867664 100644 --- a/homeassistant/components/hive/manifest.json +++ b/homeassistant/components/hive/manifest.json @@ -3,11 +3,7 @@ "name": "Hive", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hive", - "requirements": [ - "pyhiveapi==0.4.1" - ], - "codeowners": [ - "@Rendili", - "@KJonline" - ] -} \ No newline at end of file + "requirements": ["pyhiveapi==0.4.1"], + "codeowners": ["@Rendili", "@KJonline"], + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/hlk_sw16/manifest.json b/homeassistant/components/hlk_sw16/manifest.json index 112172d715ce2..1bd0a73b7ab97 100644 --- a/homeassistant/components/hlk_sw16/manifest.json +++ b/homeassistant/components/hlk_sw16/manifest.json @@ -2,11 +2,8 @@ "domain": "hlk_sw16", "name": "Hi-Link HLK-SW16", "documentation": "https://www.home-assistant.io/integrations/hlk_sw16", - "requirements": [ - "hlk-sw16==0.0.9" - ], - "codeowners": [ - "@jameshilliard" - ], - "config_flow": true -} \ No newline at end of file + "requirements": ["hlk-sw16==0.0.9"], + "codeowners": ["@jameshilliard"], + "config_flow": true, + "iot_class": "local_push" +} diff --git a/homeassistant/components/home_connect/manifest.json b/homeassistant/components/home_connect/manifest.json index 11cf7e3e0cd10..b9a4f8e6ddb63 100644 --- a/homeassistant/components/home_connect/manifest.json +++ b/homeassistant/components/home_connect/manifest.json @@ -5,5 +5,6 @@ "dependencies": ["http"], "codeowners": ["@DavidMStraub"], "requirements": ["homeconnect==0.6.3"], - "config_flow": true + "config_flow": true, + "iot_class": "cloud_push" } diff --git a/homeassistant/components/home_plus_control/manifest.json b/homeassistant/components/home_plus_control/manifest.json index 1eb143ca3c26c..edbf0147e145a 100644 --- a/homeassistant/components/home_plus_control/manifest.json +++ b/homeassistant/components/home_plus_control/manifest.json @@ -3,13 +3,8 @@ "name": "Legrand Home+ Control", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/home_plus_control", - "requirements": [ - "homepluscontrol==0.0.5" - ], - "dependencies": [ - "http" - ], - "codeowners": [ - "@chemaaa" - ] + "requirements": ["homepluscontrol==0.0.5"], + "dependencies": ["http"], + "codeowners": ["@chemaaa"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json index 53438138e43c0..0a23d52f17a38 100644 --- a/homeassistant/components/homekit/manifest.json +++ b/homeassistant/components/homekit/manifest.json @@ -9,17 +9,10 @@ "base36==0.1.1", "PyTurboJPEG==1.4.0" ], - "dependencies": [ - "http", - "camera", - "ffmpeg" - ], - "after_dependencies": [ - "zeroconf" - ], - "codeowners": [ - "@bdraco" - ], + "dependencies": ["http", "camera", "ffmpeg"], + "after_dependencies": ["zeroconf"], + "codeowners": ["@bdraco"], "zeroconf": ["_homekit._tcp.local."], - "config_flow": true + "config_flow": true, + "iot_class": "local_push" } diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index d4e7eb83ee3bc..cb248fcaa5f07 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,16 +3,9 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": [ - "aiohomekit==0.2.61" - ], - "zeroconf": [ - "_hap._tcp.local." - ], - "after_dependencies": [ - "zeroconf" - ], - "codeowners": [ - "@Jc2k" - ] + "requirements": ["aiohomekit==0.2.61"], + "zeroconf": ["_hap._tcp.local."], + "after_dependencies": ["zeroconf"], + "codeowners": ["@Jc2k"], + "iot_class": "local_push" } diff --git a/homeassistant/components/homematic/manifest.json b/homeassistant/components/homematic/manifest.json index d81dc97cdb766..ce192bc38084a 100644 --- a/homeassistant/components/homematic/manifest.json +++ b/homeassistant/components/homematic/manifest.json @@ -3,5 +3,6 @@ "name": "Homematic", "documentation": "https://www.home-assistant.io/integrations/homematic", "requirements": ["pyhomematic==0.1.72"], - "codeowners": ["@pvizeli", "@danielperna84"] + "codeowners": ["@pvizeli", "@danielperna84"], + "iot_class": "local_push" } diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index f247a58f364ec..f82e2c199962c 100644 --- a/homeassistant/components/homematicip_cloud/manifest.json +++ b/homeassistant/components/homematicip_cloud/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/homematicip_cloud", "requirements": ["homematicip==0.13.1"], "codeowners": [], - "quality_scale": "platinum" + "quality_scale": "platinum", + "iot_class": "cloud_push" } diff --git a/homeassistant/components/homeworks/manifest.json b/homeassistant/components/homeworks/manifest.json index 9432e80d04e70..7dc7c602b9804 100644 --- a/homeassistant/components/homeworks/manifest.json +++ b/homeassistant/components/homeworks/manifest.json @@ -3,5 +3,6 @@ "name": "Lutron Homeworks", "documentation": "https://www.home-assistant.io/integrations/homeworks", "requirements": ["pyhomeworks==0.0.6"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/honeywell/manifest.json b/homeassistant/components/honeywell/manifest.json index 1fbaff72426e0..bd0c5dfca6de9 100644 --- a/homeassistant/components/honeywell/manifest.json +++ b/homeassistant/components/honeywell/manifest.json @@ -3,5 +3,6 @@ "name": "Honeywell Total Connect Comfort (US)", "documentation": "https://www.home-assistant.io/integrations/honeywell", "requirements": ["somecomfort==0.5.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/horizon/manifest.json b/homeassistant/components/horizon/manifest.json index 0d89adb51093b..09e6066e57372 100644 --- a/homeassistant/components/horizon/manifest.json +++ b/homeassistant/components/horizon/manifest.json @@ -3,5 +3,6 @@ "name": "Unitymedia Horizon HD Recorder", "documentation": "https://www.home-assistant.io/integrations/horizon", "requirements": ["horimote==0.4.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/hp_ilo/manifest.json b/homeassistant/components/hp_ilo/manifest.json index ea922edd59e10..041d59eb67039 100644 --- a/homeassistant/components/hp_ilo/manifest.json +++ b/homeassistant/components/hp_ilo/manifest.json @@ -3,5 +3,6 @@ "name": "HP Integrated Lights-Out (ILO)", "documentation": "https://www.home-assistant.io/integrations/hp_ilo", "requirements": ["python-hpilo==4.3"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/html5/manifest.json b/homeassistant/components/html5/manifest.json index 7e65ea4f2b580..49f44634bcb01 100644 --- a/homeassistant/components/html5/manifest.json +++ b/homeassistant/components/html5/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/html5", "requirements": ["pywebpush==1.9.2"], "dependencies": ["http"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/http/manifest.json b/homeassistant/components/http/manifest.json index 2fd0be87a8b8e..4391fd1acaf89 100644 --- a/homeassistant/components/http/manifest.json +++ b/homeassistant/components/http/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/http", "requirements": ["aiohttp_cors==0.7.0"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_push" } diff --git a/homeassistant/components/htu21d/manifest.json b/homeassistant/components/htu21d/manifest.json index 18109aa40e461..6f7ff77efb78c 100644 --- a/homeassistant/components/htu21d/manifest.json +++ b/homeassistant/components/htu21d/manifest.json @@ -3,5 +3,6 @@ "name": "HTU21D(F) Sensor", "documentation": "https://www.home-assistant.io/integrations/htu21d", "requirements": ["i2csense==0.0.4", "smbus-cffi==0.5.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/huawei_lte/manifest.json b/homeassistant/components/huawei_lte/manifest.json index b0cd7bb8b8d4f..f48206a48020d 100644 --- a/homeassistant/components/huawei_lte/manifest.json +++ b/homeassistant/components/huawei_lte/manifest.json @@ -15,5 +15,6 @@ "manufacturer": "Huawei" } ], - "codeowners": ["@scop", "@fphammerle"] + "codeowners": ["@scop", "@fphammerle"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/huawei_router/manifest.json b/homeassistant/components/huawei_router/manifest.json index 56aafe8c3f04e..94e7fde3b945c 100644 --- a/homeassistant/components/huawei_router/manifest.json +++ b/homeassistant/components/huawei_router/manifest.json @@ -2,5 +2,6 @@ "domain": "huawei_router", "name": "Huawei Router", "documentation": "https://www.home-assistant.io/integrations/huawei_router", - "codeowners": ["@abmantis"] + "codeowners": ["@abmantis"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index caa008de40810..b86bcd6179067 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -22,5 +22,6 @@ "models": ["BSB002"] }, "codeowners": ["@balloob", "@frenck"], - "quality_scale": "platinum" + "quality_scale": "platinum", + "iot_class": "local_polling" } diff --git a/homeassistant/components/huisbaasje/manifest.json b/homeassistant/components/huisbaasje/manifest.json index 975adb52a226b..d018273375033 100644 --- a/homeassistant/components/huisbaasje/manifest.json +++ b/homeassistant/components/huisbaasje/manifest.json @@ -3,8 +3,7 @@ "name": "Huisbaasje", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/huisbaasje", - "requirements": [ - "huisbaasje-client==0.1.0" - ], - "codeowners": ["@denniss17"] + "requirements": ["huisbaasje-client==0.1.0"], + "codeowners": ["@denniss17"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/hunterdouglas_powerview/manifest.json b/homeassistant/components/hunterdouglas_powerview/manifest.json index b68ec02d3f689..183f4b454725f 100644 --- a/homeassistant/components/hunterdouglas_powerview/manifest.json +++ b/homeassistant/components/hunterdouglas_powerview/manifest.json @@ -2,12 +2,11 @@ "domain": "hunterdouglas_powerview", "name": "Hunter Douglas PowerView", "documentation": "https://www.home-assistant.io/integrations/hunterdouglas_powerview", - "requirements": [ - "aiopvapi==1.6.14" - ], + "requirements": ["aiopvapi==1.6.14"], "codeowners": ["@bdraco"], "config_flow": true, "homekit": { "models": ["PowerView"] - } -} \ No newline at end of file + }, + "iot_class": "local_polling" +} diff --git a/homeassistant/components/hvv_departures/manifest.json b/homeassistant/components/hvv_departures/manifest.json index a07181c4a951c..71a6abdfbdd54 100644 --- a/homeassistant/components/hvv_departures/manifest.json +++ b/homeassistant/components/hvv_departures/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hvv_departures", "requirements": ["pygti==0.9.2"], - "codeowners": ["@vigonotion"] + "codeowners": ["@vigonotion"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/hydrawise/manifest.json b/homeassistant/components/hydrawise/manifest.json index d5a18620eddaa..e9656b69eb81b 100644 --- a/homeassistant/components/hydrawise/manifest.json +++ b/homeassistant/components/hydrawise/manifest.json @@ -3,5 +3,6 @@ "name": "Hunter Hydrawise", "documentation": "https://www.home-assistant.io/integrations/hydrawise", "requirements": ["hydrawiser==0.2"], - "codeowners": ["@ptcryan"] + "codeowners": ["@ptcryan"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/hyperion/manifest.json b/homeassistant/components/hyperion/manifest.json index d2983e756300b..0c5e46b83e25e 100644 --- a/homeassistant/components/hyperion/manifest.json +++ b/homeassistant/components/hyperion/manifest.json @@ -11,5 +11,6 @@ "manufacturer": "Hyperion Open Source Ambient Lighting", "st": "urn:hyperion-project.org:device:basic:1" } - ] + ], + "iot_class": "local_push" } diff --git a/homeassistant/components/ialarm/manifest.json b/homeassistant/components/ialarm/manifest.json index 1e4c0383922e0..5cdc0ead3ea92 100644 --- a/homeassistant/components/ialarm/manifest.json +++ b/homeassistant/components/ialarm/manifest.json @@ -2,11 +2,8 @@ "domain": "ialarm", "name": "Antifurto365 iAlarm", "documentation": "https://www.home-assistant.io/integrations/ialarm", - "requirements": [ - "pyialarm==1.5" - ], - "codeowners": [ - "@RyuzakiKK" - ], - "config_flow": true + "requirements": ["pyialarm==1.5"], + "codeowners": ["@RyuzakiKK"], + "config_flow": true, + "iot_class": "local_polling" } diff --git a/homeassistant/components/iammeter/manifest.json b/homeassistant/components/iammeter/manifest.json index a5893c54f5a8d..e0e0b68bcf454 100644 --- a/homeassistant/components/iammeter/manifest.json +++ b/homeassistant/components/iammeter/manifest.json @@ -3,5 +3,6 @@ "name": "IamMeter", "documentation": "https://www.home-assistant.io/integrations/iammeter", "codeowners": ["@lewei50"], - "requirements": ["iammeter==0.1.7"] + "requirements": ["iammeter==0.1.7"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/iaqualink/manifest.json b/homeassistant/components/iaqualink/manifest.json index d0d9b7ed7f29b..b3aa257a9b285 100644 --- a/homeassistant/components/iaqualink/manifest.json +++ b/homeassistant/components/iaqualink/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/iaqualink/", "codeowners": ["@flz"], - "requirements": ["iaqualink==0.3.4"] + "requirements": ["iaqualink==0.3.4"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/icloud/manifest.json b/homeassistant/components/icloud/manifest.json index 4d96f42b8cbfa..6c40ef6bf03b2 100644 --- a/homeassistant/components/icloud/manifest.json +++ b/homeassistant/components/icloud/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/icloud", "requirements": ["pyicloud==0.10.2"], - "codeowners": ["@Quentame", "@nzapponi"] + "codeowners": ["@Quentame", "@nzapponi"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/idteck_prox/manifest.json b/homeassistant/components/idteck_prox/manifest.json index 8eb95f2d083ce..aa18ead9b6e41 100644 --- a/homeassistant/components/idteck_prox/manifest.json +++ b/homeassistant/components/idteck_prox/manifest.json @@ -3,5 +3,6 @@ "name": "IDTECK Proximity Reader", "documentation": "https://www.home-assistant.io/integrations/idteck_prox", "requirements": ["rfk101py==0.0.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/ifttt/manifest.json b/homeassistant/components/ifttt/manifest.json index 5dff164d640f6..a4699853b0174 100644 --- a/homeassistant/components/ifttt/manifest.json +++ b/homeassistant/components/ifttt/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/ifttt", "requirements": ["pyfttt==0.3"], "dependencies": ["webhook"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/iglo/manifest.json b/homeassistant/components/iglo/manifest.json index 98a1f8c4ee04f..b96769af932a0 100644 --- a/homeassistant/components/iglo/manifest.json +++ b/homeassistant/components/iglo/manifest.json @@ -3,5 +3,6 @@ "name": "iGlo", "documentation": "https://www.home-assistant.io/integrations/iglo", "requirements": ["iglo==1.2.7"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/ign_sismologia/manifest.json b/homeassistant/components/ign_sismologia/manifest.json index ba70cbcddf125..ce472e66449d0 100644 --- a/homeassistant/components/ign_sismologia/manifest.json +++ b/homeassistant/components/ign_sismologia/manifest.json @@ -1,7 +1,8 @@ { "domain": "ign_sismologia", - "name": "IGN Sismología", + "name": "IGN Sismolog\u00eda", "documentation": "https://www.home-assistant.io/integrations/ign_sismologia", "requirements": ["georss_ign_sismologia_client==0.2"], - "codeowners": ["@exxamalte"] + "codeowners": ["@exxamalte"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/ihc/manifest.json b/homeassistant/components/ihc/manifest.json index fe54117e56abc..3aaa8f2fb7799 100644 --- a/homeassistant/components/ihc/manifest.json +++ b/homeassistant/components/ihc/manifest.json @@ -3,5 +3,6 @@ "name": "IHC Controller", "documentation": "https://www.home-assistant.io/integrations/ihc", "requirements": ["defusedxml==0.6.0", "ihcsdk==2.7.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/imap/manifest.json b/homeassistant/components/imap/manifest.json index b2064742a9243..5bb1efa0ca167 100644 --- a/homeassistant/components/imap/manifest.json +++ b/homeassistant/components/imap/manifest.json @@ -3,5 +3,6 @@ "name": "IMAP", "documentation": "https://www.home-assistant.io/integrations/imap", "requirements": ["aioimaplib==0.7.15"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/imap_email_content/manifest.json b/homeassistant/components/imap_email_content/manifest.json index 869d465b1b7ab..bf523f23b2f4d 100644 --- a/homeassistant/components/imap_email_content/manifest.json +++ b/homeassistant/components/imap_email_content/manifest.json @@ -2,5 +2,6 @@ "domain": "imap_email_content", "name": "IMAP Email Content", "documentation": "https://www.home-assistant.io/integrations/imap_email_content", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/incomfort/manifest.json b/homeassistant/components/incomfort/manifest.json index 891cbb20be453..7e8a00aee72a4 100644 --- a/homeassistant/components/incomfort/manifest.json +++ b/homeassistant/components/incomfort/manifest.json @@ -3,5 +3,6 @@ "name": "Intergas InComfort/Intouch Lan2RF gateway", "documentation": "https://www.home-assistant.io/integrations/incomfort", "requirements": ["incomfort-client==0.4.4"], - "codeowners": ["@zxdavb"] + "codeowners": ["@zxdavb"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/influxdb/manifest.json b/homeassistant/components/influxdb/manifest.json index c2d6f77e7c175..ea1df45158727 100644 --- a/homeassistant/components/influxdb/manifest.json +++ b/homeassistant/components/influxdb/manifest.json @@ -3,5 +3,6 @@ "name": "InfluxDB", "documentation": "https://www.home-assistant.io/integrations/influxdb", "requirements": ["influxdb==5.2.3", "influxdb-client==1.14.0"], - "codeowners": ["@fabaff", "@mdegat01"] + "codeowners": ["@fabaff", "@mdegat01"], + "iot_class": "local_push" } diff --git a/homeassistant/components/insteon/manifest.json b/homeassistant/components/insteon/manifest.json index 57c750c44292c..dc564ae0d70e7 100644 --- a/homeassistant/components/insteon/manifest.json +++ b/homeassistant/components/insteon/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/insteon", "requirements": ["pyinsteon==1.0.9"], "codeowners": ["@teharris1"], - "config_flow": true -} \ No newline at end of file + "config_flow": true, + "iot_class": "local_push" +} diff --git a/homeassistant/components/integration/manifest.json b/homeassistant/components/integration/manifest.json index 8d70a26ff7e8c..afec4dbe9ec61 100644 --- a/homeassistant/components/integration/manifest.json +++ b/homeassistant/components/integration/manifest.json @@ -3,5 +3,6 @@ "name": "Integration - Riemann sum integral", "documentation": "https://www.home-assistant.io/integrations/integration", "codeowners": ["@dgomes"], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_push" } diff --git a/homeassistant/components/intesishome/manifest.json b/homeassistant/components/intesishome/manifest.json index d17014cdf0d6a..44d4d4ca58249 100644 --- a/homeassistant/components/intesishome/manifest.json +++ b/homeassistant/components/intesishome/manifest.json @@ -3,5 +3,6 @@ "name": "IntesisHome", "documentation": "https://www.home-assistant.io/integrations/intesishome", "codeowners": ["@jnimmo"], - "requirements": ["pyintesishome==1.7.6"] + "requirements": ["pyintesishome==1.7.6"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/ios/manifest.json b/homeassistant/components/ios/manifest.json index 3ab8573edc8ec..f184e7bad46c5 100644 --- a/homeassistant/components/ios/manifest.json +++ b/homeassistant/components/ios/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ios", "dependencies": ["device_tracker", "http", "zeroconf"], - "codeowners": ["@robbiet480"] + "codeowners": ["@robbiet480"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/iota/manifest.json b/homeassistant/components/iota/manifest.json index 456f77a3690d4..36e9a79d8d420 100644 --- a/homeassistant/components/iota/manifest.json +++ b/homeassistant/components/iota/manifest.json @@ -3,5 +3,6 @@ "name": "IOTA", "documentation": "https://www.home-assistant.io/integrations/iota", "requirements": ["pyota==2.0.5"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/iperf3/manifest.json b/homeassistant/components/iperf3/manifest.json index 6820953dc5d4b..6cebb34bc6347 100644 --- a/homeassistant/components/iperf3/manifest.json +++ b/homeassistant/components/iperf3/manifest.json @@ -3,5 +3,6 @@ "name": "Iperf3", "documentation": "https://www.home-assistant.io/integrations/iperf3", "requirements": ["iperf3==0.1.11"], - "codeowners": ["@rohankapoorcom"] + "codeowners": ["@rohankapoorcom"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/ipma/manifest.json b/homeassistant/components/ipma/manifest.json index 3358bbe45e9d7..06079bf0b5c48 100644 --- a/homeassistant/components/ipma/manifest.json +++ b/homeassistant/components/ipma/manifest.json @@ -1,8 +1,9 @@ { "domain": "ipma", - "name": "Instituto Português do Mar e Atmosfera (IPMA)", + "name": "Instituto Portugu\u00eas do Mar e Atmosfera (IPMA)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ipma", "requirements": ["pyipma==2.0.5"], - "codeowners": ["@dgomes", "@abmantis"] + "codeowners": ["@dgomes", "@abmantis"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/ipp/manifest.json b/homeassistant/components/ipp/manifest.json index d4e3669b795a3..18bfc3abc5480 100644 --- a/homeassistant/components/ipp/manifest.json +++ b/homeassistant/components/ipp/manifest.json @@ -6,5 +6,6 @@ "codeowners": ["@ctalkington"], "config_flow": true, "quality_scale": "platinum", - "zeroconf": ["_ipps._tcp.local.", "_ipp._tcp.local."] + "zeroconf": ["_ipps._tcp.local.", "_ipp._tcp.local."], + "iot_class": "local_polling" } diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json index 145972e287555..85131bebded42 100644 --- a/homeassistant/components/iqvia/manifest.json +++ b/homeassistant/components/iqvia/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/iqvia", "requirements": ["numpy==1.20.2", "pyiqvia==0.3.1"], - "codeowners": ["@bachya"] + "codeowners": ["@bachya"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/irish_rail_transport/manifest.json b/homeassistant/components/irish_rail_transport/manifest.json index a6c9554d6069d..4263d5288ff1e 100644 --- a/homeassistant/components/irish_rail_transport/manifest.json +++ b/homeassistant/components/irish_rail_transport/manifest.json @@ -3,5 +3,6 @@ "name": "Irish Rail Transport", "documentation": "https://www.home-assistant.io/integrations/irish_rail_transport", "requirements": ["pyirishrail==0.0.2"], - "codeowners": ["@ttroy50"] + "codeowners": ["@ttroy50"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/islamic_prayer_times/manifest.json b/homeassistant/components/islamic_prayer_times/manifest.json index 536e728e845b1..af6d09d0302b8 100644 --- a/homeassistant/components/islamic_prayer_times/manifest.json +++ b/homeassistant/components/islamic_prayer_times/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/islamic_prayer_times", "requirements": ["prayer_times_calculator==0.0.3"], "codeowners": ["@engrbm87"], - "config_flow": true + "config_flow": true, + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/iss/manifest.json b/homeassistant/components/iss/manifest.json index 7fd98ebcdde0e..be34babeeae5a 100644 --- a/homeassistant/components/iss/manifest.json +++ b/homeassistant/components/iss/manifest.json @@ -3,5 +3,6 @@ "name": "International Space Station (ISS)", "documentation": "https://www.home-assistant.io/integrations/iss", "requirements": ["pyiss==1.0.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/isy994/manifest.json b/homeassistant/components/isy994/manifest.json index 3769cc328db36..8758a9d828b27 100644 --- a/homeassistant/components/isy994/manifest.json +++ b/homeassistant/components/isy994/manifest.json @@ -10,5 +10,6 @@ "manufacturer": "Universal Devices Inc.", "deviceType": "urn:udi-com:device:X_Insteon_Lighting_Device:1" } - ] + ], + "iot_class": "local_push" } diff --git a/homeassistant/components/itach/manifest.json b/homeassistant/components/itach/manifest.json index 90d69a9a9b15f..0c2ea3eac8b82 100644 --- a/homeassistant/components/itach/manifest.json +++ b/homeassistant/components/itach/manifest.json @@ -1,7 +1,8 @@ { "domain": "itach", - "name": "Global Caché iTach TCP/IP to IR", + "name": "Global Cach\u00e9 iTach TCP/IP to IR", "documentation": "https://www.home-assistant.io/integrations/itach", "requirements": ["pyitachip2ir==0.0.7"], - "codeowners": [] + "codeowners": [], + "iot_class": "assumed_state" } diff --git a/homeassistant/components/itunes/manifest.json b/homeassistant/components/itunes/manifest.json index 206f6e0a1d2e7..8f9de6f60277f 100644 --- a/homeassistant/components/itunes/manifest.json +++ b/homeassistant/components/itunes/manifest.json @@ -2,5 +2,6 @@ "domain": "itunes", "name": "Apple iTunes", "documentation": "https://www.home-assistant.io/integrations/itunes", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/izone/manifest.json b/homeassistant/components/izone/manifest.json index bed7654b7e836..0a2b8f82fe51e 100644 --- a/homeassistant/components/izone/manifest.json +++ b/homeassistant/components/izone/manifest.json @@ -6,8 +6,7 @@ "codeowners": ["@Swamp-Ig"], "config_flow": true, "homekit": { - "models": [ - "iZone" - ] - } + "models": ["iZone"] + }, + "iot_class": "local_push" } diff --git a/homeassistant/components/jewish_calendar/manifest.json b/homeassistant/components/jewish_calendar/manifest.json index bd45335797dff..9bec8fce5b061 100644 --- a/homeassistant/components/jewish_calendar/manifest.json +++ b/homeassistant/components/jewish_calendar/manifest.json @@ -3,5 +3,6 @@ "name": "Jewish Calendar", "documentation": "https://www.home-assistant.io/integrations/jewish_calendar", "requirements": ["hdate==0.10.2"], - "codeowners": ["@tsvi"] + "codeowners": ["@tsvi"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/joaoapps_join/manifest.json b/homeassistant/components/joaoapps_join/manifest.json index 3d74d03c7bb62..a9d67e915fa49 100644 --- a/homeassistant/components/joaoapps_join/manifest.json +++ b/homeassistant/components/joaoapps_join/manifest.json @@ -3,5 +3,6 @@ "name": "Joaoapps Join", "documentation": "https://www.home-assistant.io/integrations/joaoapps_join", "requirements": ["python-join-api==0.0.6"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/juicenet/manifest.json b/homeassistant/components/juicenet/manifest.json index 66b7912028e79..4b0c946c53afc 100644 --- a/homeassistant/components/juicenet/manifest.json +++ b/homeassistant/components/juicenet/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/juicenet", "requirements": ["python-juicenet==1.0.1"], "codeowners": ["@jesserockz"], - "config_flow": true + "config_flow": true, + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/kaiterra/manifest.json b/homeassistant/components/kaiterra/manifest.json index 33fc1266d8301..1bdcd7670e64e 100644 --- a/homeassistant/components/kaiterra/manifest.json +++ b/homeassistant/components/kaiterra/manifest.json @@ -3,5 +3,6 @@ "name": "Kaiterra", "documentation": "https://www.home-assistant.io/integrations/kaiterra", "requirements": ["kaiterra-async-client==0.0.2"], - "codeowners": ["@Michsior14"] + "codeowners": ["@Michsior14"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/kankun/manifest.json b/homeassistant/components/kankun/manifest.json index 933111ebccac1..f16ed40e1bc0e 100644 --- a/homeassistant/components/kankun/manifest.json +++ b/homeassistant/components/kankun/manifest.json @@ -2,5 +2,6 @@ "domain": "kankun", "name": "Kankun", "documentation": "https://www.home-assistant.io/integrations/kankun", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/keba/manifest.json b/homeassistant/components/keba/manifest.json index 29c4ec86c49b1..7e148be103b0a 100644 --- a/homeassistant/components/keba/manifest.json +++ b/homeassistant/components/keba/manifest.json @@ -3,5 +3,6 @@ "name": "Keba Charging Station", "documentation": "https://www.home-assistant.io/integrations/keba", "requirements": ["keba-kecontact==1.1.0"], - "codeowners": ["@dannerph"] + "codeowners": ["@dannerph"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/keenetic_ndms2/manifest.json b/homeassistant/components/keenetic_ndms2/manifest.json index da8321a8bdc56..7e1e7166da9d4 100644 --- a/homeassistant/components/keenetic_ndms2/manifest.json +++ b/homeassistant/components/keenetic_ndms2/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/keenetic_ndms2", "requirements": ["ndms2_client==0.1.1"], - "codeowners": ["@foxel"] + "codeowners": ["@foxel"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/kef/manifest.json b/homeassistant/components/kef/manifest.json index 7441b59906335..1b0c0b190e603 100644 --- a/homeassistant/components/kef/manifest.json +++ b/homeassistant/components/kef/manifest.json @@ -3,5 +3,6 @@ "name": "KEF", "documentation": "https://www.home-assistant.io/integrations/kef", "codeowners": ["@basnijholt"], - "requirements": ["aiokef==0.2.16", "getmac==0.8.2"] + "requirements": ["aiokef==0.2.16", "getmac==0.8.2"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/keyboard/manifest.json b/homeassistant/components/keyboard/manifest.json index c6379fac4a1ce..b53d44ff18838 100644 --- a/homeassistant/components/keyboard/manifest.json +++ b/homeassistant/components/keyboard/manifest.json @@ -3,5 +3,6 @@ "name": "Keyboard", "documentation": "https://www.home-assistant.io/integrations/keyboard", "requirements": ["pyuserinput==0.1.11"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/keyboard_remote/manifest.json b/homeassistant/components/keyboard_remote/manifest.json index 5a803f95bb3f2..7e7525f6664a0 100644 --- a/homeassistant/components/keyboard_remote/manifest.json +++ b/homeassistant/components/keyboard_remote/manifest.json @@ -3,5 +3,6 @@ "name": "Keyboard Remote", "documentation": "https://www.home-assistant.io/integrations/keyboard_remote", "requirements": ["evdev==1.1.2", "aionotify==0.2.0"], - "codeowners": ["@bendavid"] + "codeowners": ["@bendavid"], + "iot_class": "local_push" } diff --git a/homeassistant/components/kira/manifest.json b/homeassistant/components/kira/manifest.json index 04c6598adb7ef..09514d01cb51b 100644 --- a/homeassistant/components/kira/manifest.json +++ b/homeassistant/components/kira/manifest.json @@ -3,5 +3,6 @@ "name": "Kira", "documentation": "https://www.home-assistant.io/integrations/kira", "requirements": ["pykira==0.1.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/kiwi/manifest.json b/homeassistant/components/kiwi/manifest.json index a80e279f97473..7b5093eb86b5d 100644 --- a/homeassistant/components/kiwi/manifest.json +++ b/homeassistant/components/kiwi/manifest.json @@ -3,5 +3,6 @@ "name": "KIWI", "documentation": "https://www.home-assistant.io/integrations/kiwi", "requirements": ["kiwiki-client==0.1.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/kmtronic/manifest.json b/homeassistant/components/kmtronic/manifest.json index b7bccbe6f2d40..1c17ee0fd3cfd 100644 --- a/homeassistant/components/kmtronic/manifest.json +++ b/homeassistant/components/kmtronic/manifest.json @@ -1,8 +1,9 @@ { - "domain": "kmtronic", - "name": "KMtronic", - "config_flow": true, - "documentation": "https://www.home-assistant.io/integrations/kmtronic", - "requirements": ["pykmtronic==0.3.0"], - "codeowners": ["@dgomes"] + "domain": "kmtronic", + "name": "KMtronic", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/kmtronic", + "requirements": ["pykmtronic==0.3.0"], + "codeowners": ["@dgomes"], + "iot_class": "local_push" } diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index abb7fff37e027..5f8711141e38b 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/knx", "requirements": ["xknx==0.18.0"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], - "quality_scale": "silver" + "quality_scale": "silver", + "iot_class": "local_push" } diff --git a/homeassistant/components/kodi/manifest.json b/homeassistant/components/kodi/manifest.json index 9ab510507048a..78d0c6e59982e 100644 --- a/homeassistant/components/kodi/manifest.json +++ b/homeassistant/components/kodi/manifest.json @@ -2,15 +2,9 @@ "domain": "kodi", "name": "Kodi", "documentation": "https://www.home-assistant.io/integrations/kodi", - "requirements": [ - "pykodi==0.2.5" - ], - "codeowners": [ - "@OnFreund", - "@cgtobi" - ], - "zeroconf": [ - "_xbmc-jsonrpc-h._tcp.local." - ], - "config_flow": true -} \ No newline at end of file + "requirements": ["pykodi==0.2.5"], + "codeowners": ["@OnFreund", "@cgtobi"], + "zeroconf": ["_xbmc-jsonrpc-h._tcp.local."], + "config_flow": true, + "iot_class": "local_push" +} diff --git a/homeassistant/components/konnected/manifest.json b/homeassistant/components/konnected/manifest.json index b6c1c8117fb70..4838e1ab1e497 100644 --- a/homeassistant/components/konnected/manifest.json +++ b/homeassistant/components/konnected/manifest.json @@ -10,5 +10,6 @@ } ], "dependencies": ["http"], - "codeowners": ["@heythisisnate", "@kit-klein"] + "codeowners": ["@heythisisnate", "@kit-klein"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/kostal_plenticore/manifest.json b/homeassistant/components/kostal_plenticore/manifest.json index 427c730833cf4..9e6d4353259fb 100644 --- a/homeassistant/components/kostal_plenticore/manifest.json +++ b/homeassistant/components/kostal_plenticore/manifest.json @@ -4,7 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/kostal_plenticore", "requirements": ["kostal_plenticore==0.2.0"], - "codeowners": [ - "@stegm" - ] -} \ No newline at end of file + "codeowners": ["@stegm"], + "iot_class": "local_polling" +} diff --git a/homeassistant/components/kulersky/manifest.json b/homeassistant/components/kulersky/manifest.json index b690d94e8d4a5..24091ec65c809 100644 --- a/homeassistant/components/kulersky/manifest.json +++ b/homeassistant/components/kulersky/manifest.json @@ -3,10 +3,7 @@ "name": "Kuler Sky", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/kulersky", - "requirements": [ - "pykulersky==0.5.2" - ], - "codeowners": [ - "@emlove" - ] + "requirements": ["pykulersky==0.5.2"], + "codeowners": ["@emlove"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/kwb/manifest.json b/homeassistant/components/kwb/manifest.json index 2f816345a8647..b84d36131e5f3 100644 --- a/homeassistant/components/kwb/manifest.json +++ b/homeassistant/components/kwb/manifest.json @@ -3,5 +3,6 @@ "name": "KWB Easyfire", "documentation": "https://www.home-assistant.io/integrations/kwb", "requirements": ["pykwb==0.0.8"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/lacrosse/manifest.json b/homeassistant/components/lacrosse/manifest.json index a6517a2768b44..922c0e9d17361 100644 --- a/homeassistant/components/lacrosse/manifest.json +++ b/homeassistant/components/lacrosse/manifest.json @@ -3,5 +3,6 @@ "name": "LaCrosse", "documentation": "https://www.home-assistant.io/integrations/lacrosse", "requirements": ["pylacrosse==0.4"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/lametric/manifest.json b/homeassistant/components/lametric/manifest.json index 4edcef1a14756..4c49055a6ea4d 100644 --- a/homeassistant/components/lametric/manifest.json +++ b/homeassistant/components/lametric/manifest.json @@ -3,5 +3,6 @@ "name": "LaMetric", "documentation": "https://www.home-assistant.io/integrations/lametric", "requirements": ["lmnotify==0.0.4"], - "codeowners": ["@robbiet480"] + "codeowners": ["@robbiet480"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/lannouncer/manifest.json b/homeassistant/components/lannouncer/manifest.json index 3c46672776d9b..41cb6fb498e4b 100644 --- a/homeassistant/components/lannouncer/manifest.json +++ b/homeassistant/components/lannouncer/manifest.json @@ -2,5 +2,6 @@ "domain": "lannouncer", "name": "LANnouncer", "documentation": "https://www.home-assistant.io/integrations/lannouncer", - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/lastfm/manifest.json b/homeassistant/components/lastfm/manifest.json index e732b5d700093..9b4b0e5cdfcb8 100644 --- a/homeassistant/components/lastfm/manifest.json +++ b/homeassistant/components/lastfm/manifest.json @@ -3,5 +3,6 @@ "name": "Last.fm", "documentation": "https://www.home-assistant.io/integrations/lastfm", "requirements": ["pylast==4.2.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/launch_library/manifest.json b/homeassistant/components/launch_library/manifest.json index 023e15fea1434..f7820a1d408ee 100644 --- a/homeassistant/components/launch_library/manifest.json +++ b/homeassistant/components/launch_library/manifest.json @@ -3,5 +3,6 @@ "name": "Launch Library", "documentation": "https://www.home-assistant.io/integrations/launch_library", "requirements": ["pylaunches==1.0.0"], - "codeowners": ["@ludeeus"] + "codeowners": ["@ludeeus"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/lcn/manifest.json b/homeassistant/components/lcn/manifest.json index 5c8be5829e01b..092e07eb5d223 100644 --- a/homeassistant/components/lcn/manifest.json +++ b/homeassistant/components/lcn/manifest.json @@ -3,10 +3,7 @@ "name": "LCN", "config_flow": false, "documentation": "https://www.home-assistant.io/integrations/lcn", - "requirements": [ - "pypck==0.7.9" - ], - "codeowners": [ - "@alengwenus" - ] + "requirements": ["pypck==0.7.9"], + "codeowners": ["@alengwenus"], + "iot_class": "local_push" } diff --git a/homeassistant/components/lg_netcast/manifest.json b/homeassistant/components/lg_netcast/manifest.json index 78cccdda3be0f..d214cebc636eb 100644 --- a/homeassistant/components/lg_netcast/manifest.json +++ b/homeassistant/components/lg_netcast/manifest.json @@ -3,5 +3,6 @@ "name": "LG Netcast", "documentation": "https://www.home-assistant.io/integrations/lg_netcast", "requirements": ["pylgnetcast-homeassistant==0.2.0.dev0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/lg_soundbar/manifest.json b/homeassistant/components/lg_soundbar/manifest.json index d7bc310253dc7..671b1d2ca5735 100644 --- a/homeassistant/components/lg_soundbar/manifest.json +++ b/homeassistant/components/lg_soundbar/manifest.json @@ -3,5 +3,6 @@ "name": "LG Soundbars", "documentation": "https://www.home-assistant.io/integrations/lg_soundbar", "requirements": ["temescal==0.3"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/life360/manifest.json b/homeassistant/components/life360/manifest.json index c7a832f78e7cc..54919088262b7 100644 --- a/homeassistant/components/life360/manifest.json +++ b/homeassistant/components/life360/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/life360", "codeowners": ["@pnbruckner"], - "requirements": ["life360==4.1.1"] + "requirements": ["life360==4.1.1"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/lifx/manifest.json b/homeassistant/components/lifx/manifest.json index d76f18c695f98..9e1a4fc26893d 100644 --- a/homeassistant/components/lifx/manifest.json +++ b/homeassistant/components/lifx/manifest.json @@ -7,5 +7,6 @@ "homekit": { "models": ["LIFX"] }, - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/lifx_cloud/manifest.json b/homeassistant/components/lifx_cloud/manifest.json index 038282390cafe..54459963466fa 100644 --- a/homeassistant/components/lifx_cloud/manifest.json +++ b/homeassistant/components/lifx_cloud/manifest.json @@ -2,5 +2,6 @@ "domain": "lifx_cloud", "name": "LIFX Cloud", "documentation": "https://www.home-assistant.io/integrations/lifx_cloud", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/lifx_legacy/manifest.json b/homeassistant/components/lifx_legacy/manifest.json index 4a42f44f48270..8bd5a471bf656 100644 --- a/homeassistant/components/lifx_legacy/manifest.json +++ b/homeassistant/components/lifx_legacy/manifest.json @@ -3,5 +3,6 @@ "name": "LIFX Legacy", "documentation": "https://www.home-assistant.io/integrations/lifx_legacy", "requirements": ["liffylights==0.9.4"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/lightwave/manifest.json b/homeassistant/components/lightwave/manifest.json index ffe2ca065feac..72138bf34f9a3 100644 --- a/homeassistant/components/lightwave/manifest.json +++ b/homeassistant/components/lightwave/manifest.json @@ -3,5 +3,6 @@ "name": "Lightwave", "documentation": "https://www.home-assistant.io/integrations/lightwave", "requirements": ["lightwave==0.19"], - "codeowners": [] + "codeowners": [], + "iot_class": "assumed_state" } diff --git a/homeassistant/components/limitlessled/manifest.json b/homeassistant/components/limitlessled/manifest.json index 3187b795e8868..f0a8888214a7a 100644 --- a/homeassistant/components/limitlessled/manifest.json +++ b/homeassistant/components/limitlessled/manifest.json @@ -3,5 +3,6 @@ "name": "LimitlessLED", "documentation": "https://www.home-assistant.io/integrations/limitlessled", "requirements": ["limitlessled==1.1.3"], - "codeowners": [] + "codeowners": [], + "iot_class": "assumed_state" } diff --git a/homeassistant/components/linksys_smart/manifest.json b/homeassistant/components/linksys_smart/manifest.json index e0fafcdce2515..e4b64ed67222b 100644 --- a/homeassistant/components/linksys_smart/manifest.json +++ b/homeassistant/components/linksys_smart/manifest.json @@ -2,5 +2,6 @@ "domain": "linksys_smart", "name": "Linksys Smart Wi-Fi", "documentation": "https://www.home-assistant.io/integrations/linksys_smart", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/linode/manifest.json b/homeassistant/components/linode/manifest.json index dbc1a6fb8aa93..2732535455364 100644 --- a/homeassistant/components/linode/manifest.json +++ b/homeassistant/components/linode/manifest.json @@ -3,5 +3,6 @@ "name": "Linode", "documentation": "https://www.home-assistant.io/integrations/linode", "requirements": ["linode-api==4.1.9b1"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/linux_battery/manifest.json b/homeassistant/components/linux_battery/manifest.json index 1f242dd791b2c..4502bd039f407 100644 --- a/homeassistant/components/linux_battery/manifest.json +++ b/homeassistant/components/linux_battery/manifest.json @@ -3,5 +3,6 @@ "name": "Linux Battery", "documentation": "https://www.home-assistant.io/integrations/linux_battery", "requirements": ["batinfo==0.4.2"], - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/lirc/manifest.json b/homeassistant/components/lirc/manifest.json index 16f2445d840e7..3e688bdef6fd0 100644 --- a/homeassistant/components/lirc/manifest.json +++ b/homeassistant/components/lirc/manifest.json @@ -3,5 +3,6 @@ "name": "LIRC", "documentation": "https://www.home-assistant.io/integrations/lirc", "requirements": ["python-lirc==1.2.3"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/litejet/manifest.json b/homeassistant/components/litejet/manifest.json index e23e5ac2964cc..7481cabb65539 100644 --- a/homeassistant/components/litejet/manifest.json +++ b/homeassistant/components/litejet/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/litejet", "requirements": ["pylitejet==0.3.0"], "codeowners": ["@joncar"], - "config_flow": true + "config_flow": true, + "iot_class": "local_push" } diff --git a/homeassistant/components/litterrobot/manifest.json b/homeassistant/components/litterrobot/manifest.json index 1e440fabe1a1b..346bb5e07610e 100644 --- a/homeassistant/components/litterrobot/manifest.json +++ b/homeassistant/components/litterrobot/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/litterrobot", "requirements": ["pylitterbot==2021.3.1"], - "codeowners": ["@natekspencer"] + "codeowners": ["@natekspencer"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/llamalab_automate/manifest.json b/homeassistant/components/llamalab_automate/manifest.json index 777696f5c756e..360415049b847 100644 --- a/homeassistant/components/llamalab_automate/manifest.json +++ b/homeassistant/components/llamalab_automate/manifest.json @@ -2,5 +2,6 @@ "domain": "llamalab_automate", "name": "LlamaLab Automate", "documentation": "https://www.home-assistant.io/integrations/llamalab_automate", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/local_file/manifest.json b/homeassistant/components/local_file/manifest.json index d7ec128018695..945c05f65ea07 100644 --- a/homeassistant/components/local_file/manifest.json +++ b/homeassistant/components/local_file/manifest.json @@ -2,5 +2,6 @@ "domain": "local_file", "name": "Local File", "documentation": "https://www.home-assistant.io/integrations/local_file", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/local_ip/manifest.json b/homeassistant/components/local_ip/manifest.json index 62c862e33c86b..f7e245aac05cc 100644 --- a/homeassistant/components/local_ip/manifest.json +++ b/homeassistant/components/local_ip/manifest.json @@ -3,5 +3,6 @@ "name": "Local IP Address", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/local_ip", - "codeowners": ["@issacg"] + "codeowners": ["@issacg"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/locative/manifest.json b/homeassistant/components/locative/manifest.json index 653b27ce4d6e6..8566de1b5118d 100644 --- a/homeassistant/components/locative/manifest.json +++ b/homeassistant/components/locative/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/locative", "dependencies": ["webhook"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/logentries/manifest.json b/homeassistant/components/logentries/manifest.json index 23500d66dd623..46c0cd6462353 100644 --- a/homeassistant/components/logentries/manifest.json +++ b/homeassistant/components/logentries/manifest.json @@ -2,5 +2,6 @@ "domain": "logentries", "name": "Logentries", "documentation": "https://www.home-assistant.io/integrations/logentries", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/logi_circle/manifest.json b/homeassistant/components/logi_circle/manifest.json index bd6dc8a8d27bc..b89950061694d 100644 --- a/homeassistant/components/logi_circle/manifest.json +++ b/homeassistant/components/logi_circle/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/logi_circle", "requirements": ["logi_circle==0.2.2"], "dependencies": ["ffmpeg", "http"], - "codeowners": ["@evanjd"] + "codeowners": ["@evanjd"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/london_air/manifest.json b/homeassistant/components/london_air/manifest.json index 48ba49bee237e..2480b461660b4 100644 --- a/homeassistant/components/london_air/manifest.json +++ b/homeassistant/components/london_air/manifest.json @@ -2,5 +2,6 @@ "domain": "london_air", "name": "London Air", "documentation": "https://www.home-assistant.io/integrations/london_air", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/london_underground/manifest.json b/homeassistant/components/london_underground/manifest.json index 5dbccea27b100..329c9fa504d91 100644 --- a/homeassistant/components/london_underground/manifest.json +++ b/homeassistant/components/london_underground/manifest.json @@ -3,5 +3,6 @@ "name": "London Underground", "documentation": "https://www.home-assistant.io/integrations/london_underground", "requirements": ["london-tube-status==0.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/loopenergy/manifest.json b/homeassistant/components/loopenergy/manifest.json index 9b421083d1061..01a18dc01db6d 100644 --- a/homeassistant/components/loopenergy/manifest.json +++ b/homeassistant/components/loopenergy/manifest.json @@ -3,7 +3,6 @@ "name": "Loop Energy", "documentation": "https://www.home-assistant.io/integrations/loopenergy", "requirements": ["pyloopenergy==0.2.1"], - "codeowners": [ - "@pavoni" - ] + "codeowners": ["@pavoni"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/luci/manifest.json b/homeassistant/components/luci/manifest.json index 95fd6fc35ad84..6feac638637ff 100644 --- a/homeassistant/components/luci/manifest.json +++ b/homeassistant/components/luci/manifest.json @@ -3,5 +3,6 @@ "name": "OpenWRT (luci)", "documentation": "https://www.home-assistant.io/integrations/luci", "requirements": ["openwrt-luci-rpc==1.1.8"], - "codeowners": ["@mzdrale"] + "codeowners": ["@mzdrale"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/luftdaten/manifest.json b/homeassistant/components/luftdaten/manifest.json index e4670680b16c3..dad6a1a693472 100644 --- a/homeassistant/components/luftdaten/manifest.json +++ b/homeassistant/components/luftdaten/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/luftdaten", "requirements": ["luftdaten==0.6.4"], "codeowners": ["@fabaff"], - "quality_scale": "gold" + "quality_scale": "gold", + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/lupusec/manifest.json b/homeassistant/components/lupusec/manifest.json index fb9cf64545a1f..163789d19bd7f 100644 --- a/homeassistant/components/lupusec/manifest.json +++ b/homeassistant/components/lupusec/manifest.json @@ -3,5 +3,6 @@ "name": "Lupus Electronics LUPUSEC", "documentation": "https://www.home-assistant.io/integrations/lupusec", "requirements": ["lupupy==0.0.18"], - "codeowners": ["@majuss"] + "codeowners": ["@majuss"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/lutron/manifest.json b/homeassistant/components/lutron/manifest.json index fdd47d9005d62..db1c9090ce87f 100644 --- a/homeassistant/components/lutron/manifest.json +++ b/homeassistant/components/lutron/manifest.json @@ -3,5 +3,6 @@ "name": "Lutron", "documentation": "https://www.home-assistant.io/integrations/lutron", "requirements": ["pylutron==0.2.7"], - "codeowners": ["@JonGilmore"] + "codeowners": ["@JonGilmore"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/lutron_caseta/manifest.json b/homeassistant/components/lutron_caseta/manifest.json index 88c6eddd0bf29..de32b839153e5 100644 --- a/homeassistant/components/lutron_caseta/manifest.json +++ b/homeassistant/components/lutron_caseta/manifest.json @@ -1,14 +1,13 @@ { "domain": "lutron_caseta", - "name": "Lutron Caséta", + "name": "Lutron Cas\u00e9ta", "documentation": "https://www.home-assistant.io/integrations/lutron_caseta", - "requirements": [ - "pylutron-caseta==0.9.0", "aiolip==1.1.4" - ], + "requirements": ["pylutron-caseta==0.9.0", "aiolip==1.1.4"], "config_flow": true, "zeroconf": ["_leap._tcp.local."], "homekit": { "models": ["Smart Bridge"] }, - "codeowners": ["@swails", "@bdraco"] + "codeowners": ["@swails", "@bdraco"], + "iot_class": "local_push" } diff --git a/homeassistant/components/lw12wifi/manifest.json b/homeassistant/components/lw12wifi/manifest.json index 27523ccb7c2b7..ae585a335f27c 100644 --- a/homeassistant/components/lw12wifi/manifest.json +++ b/homeassistant/components/lw12wifi/manifest.json @@ -3,5 +3,6 @@ "name": "LAGUTE LW-12", "documentation": "https://www.home-assistant.io/integrations/lw12wifi", "requirements": ["lw12==0.9.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/lyft/manifest.json b/homeassistant/components/lyft/manifest.json index 7b5ad8df07cd1..784ffa30d6e68 100644 --- a/homeassistant/components/lyft/manifest.json +++ b/homeassistant/components/lyft/manifest.json @@ -3,5 +3,6 @@ "name": "Lyft", "documentation": "https://www.home-assistant.io/integrations/lyft", "requirements": ["lyft_rides==0.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/lyric/manifest.json b/homeassistant/components/lyric/manifest.json index 6aa028e26366a..71976fa2ac13b 100644 --- a/homeassistant/components/lyric/manifest.json +++ b/homeassistant/components/lyric/manifest.json @@ -20,5 +20,6 @@ "hostname": "lyric-*", "macaddress": "00D02D" } - ] + ], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/magicseaweed/manifest.json b/homeassistant/components/magicseaweed/manifest.json index 2edac84c7f54e..84a2addc3e144 100644 --- a/homeassistant/components/magicseaweed/manifest.json +++ b/homeassistant/components/magicseaweed/manifest.json @@ -3,5 +3,6 @@ "name": "Magicseaweed", "documentation": "https://www.home-assistant.io/integrations/magicseaweed", "requirements": ["magicseaweed==1.0.3"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/mailgun/manifest.json b/homeassistant/components/mailgun/manifest.json index 45e809bac1ad6..d8d5182816b8d 100644 --- a/homeassistant/components/mailgun/manifest.json +++ b/homeassistant/components/mailgun/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/mailgun", "requirements": ["pymailgunner==1.4"], "dependencies": ["webhook"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/manual/manifest.json b/homeassistant/components/manual/manifest.json index 813dbf4e5705f..832631878ebeb 100644 --- a/homeassistant/components/manual/manifest.json +++ b/homeassistant/components/manual/manifest.json @@ -3,5 +3,6 @@ "name": "Manual", "documentation": "https://www.home-assistant.io/integrations/manual", "codeowners": [], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "calculated" } diff --git a/homeassistant/components/manual_mqtt/manifest.json b/homeassistant/components/manual_mqtt/manifest.json index 8189b167f934a..56b13ce90a7fc 100644 --- a/homeassistant/components/manual_mqtt/manifest.json +++ b/homeassistant/components/manual_mqtt/manifest.json @@ -3,5 +3,6 @@ "name": "Manual MQTT", "documentation": "https://www.home-assistant.io/integrations/manual_mqtt", "dependencies": ["mqtt"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/marytts/manifest.json b/homeassistant/components/marytts/manifest.json index 5152e838fb942..f53e0deecd70b 100644 --- a/homeassistant/components/marytts/manifest.json +++ b/homeassistant/components/marytts/manifest.json @@ -3,5 +3,6 @@ "name": "MaryTTS", "documentation": "https://www.home-assistant.io/integrations/marytts", "requirements": ["speak2mary==1.4.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/mastodon/manifest.json b/homeassistant/components/mastodon/manifest.json index 8c29ba1da35c9..cd393002e1de3 100644 --- a/homeassistant/components/mastodon/manifest.json +++ b/homeassistant/components/mastodon/manifest.json @@ -3,5 +3,6 @@ "name": "Mastodon", "documentation": "https://www.home-assistant.io/integrations/mastodon", "requirements": ["Mastodon.py==1.5.1"], - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/matrix/manifest.json b/homeassistant/components/matrix/manifest.json index 90571d239f62f..c28d20196e9b2 100644 --- a/homeassistant/components/matrix/manifest.json +++ b/homeassistant/components/matrix/manifest.json @@ -3,5 +3,6 @@ "name": "Matrix", "documentation": "https://www.home-assistant.io/integrations/matrix", "requirements": ["matrix-client==0.3.2"], - "codeowners": ["@tinloaf"] + "codeowners": ["@tinloaf"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/maxcube/manifest.json b/homeassistant/components/maxcube/manifest.json index 75b5a5fcb6df4..ba263b5e0d990 100644 --- a/homeassistant/components/maxcube/manifest.json +++ b/homeassistant/components/maxcube/manifest.json @@ -3,5 +3,6 @@ "name": "eQ-3 MAX!", "documentation": "https://www.home-assistant.io/integrations/maxcube", "requirements": ["maxcube-api==0.4.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/mazda/manifest.json b/homeassistant/components/mazda/manifest.json index c3a05a351c304..9c5fb2c6b4677 100644 --- a/homeassistant/components/mazda/manifest.json +++ b/homeassistant/components/mazda/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/mazda", "requirements": ["pymazda==0.0.9"], "codeowners": ["@bdr99"], - "quality_scale": "platinum" -} \ No newline at end of file + "quality_scale": "platinum", + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/mcp23017/manifest.json b/homeassistant/components/mcp23017/manifest.json index 7460529f8fee0..2fad5acc0ce69 100644 --- a/homeassistant/components/mcp23017/manifest.json +++ b/homeassistant/components/mcp23017/manifest.json @@ -6,5 +6,6 @@ "RPi.GPIO==0.7.1a4", "adafruit-circuitpython-mcp230xx==2.2.2" ], - "codeowners": ["@jardiamj"] + "codeowners": ["@jardiamj"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index 35a5b0981843b..5872e0bd84191 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -5,5 +5,6 @@ "requirements": ["youtube_dl==2021.03.14"], "dependencies": ["media_player"], "codeowners": [], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "calculated" } diff --git a/homeassistant/components/mediaroom/manifest.json b/homeassistant/components/mediaroom/manifest.json index c3a59e3404f0a..4171322400a68 100644 --- a/homeassistant/components/mediaroom/manifest.json +++ b/homeassistant/components/mediaroom/manifest.json @@ -3,5 +3,6 @@ "name": "Mediaroom", "documentation": "https://www.home-assistant.io/integrations/mediaroom", "requirements": ["pymediaroom==0.6.4.1"], - "codeowners": ["@dgomes"] + "codeowners": ["@dgomes"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/melcloud/manifest.json b/homeassistant/components/melcloud/manifest.json index aac8db678f969..641a4df583e89 100644 --- a/homeassistant/components/melcloud/manifest.json +++ b/homeassistant/components/melcloud/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/melcloud", "requirements": ["pymelcloud==2.5.2"], - "codeowners": ["@vilppuvuorinen"] + "codeowners": ["@vilppuvuorinen"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/melissa/manifest.json b/homeassistant/components/melissa/manifest.json index b4e1881c4d040..d3b4f95a82ebd 100644 --- a/homeassistant/components/melissa/manifest.json +++ b/homeassistant/components/melissa/manifest.json @@ -3,5 +3,6 @@ "name": "Melissa", "documentation": "https://www.home-assistant.io/integrations/melissa", "requirements": ["py-melissa-climate==2.1.4"], - "codeowners": ["@kennedyshead"] + "codeowners": ["@kennedyshead"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/meraki/manifest.json b/homeassistant/components/meraki/manifest.json index f0de1aa7c1dec..40b8d12472ecd 100644 --- a/homeassistant/components/meraki/manifest.json +++ b/homeassistant/components/meraki/manifest.json @@ -3,5 +3,6 @@ "name": "Meraki", "documentation": "https://www.home-assistant.io/integrations/meraki", "dependencies": ["http"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/message_bird/manifest.json b/homeassistant/components/message_bird/manifest.json index 91018849449ec..9e38e9d724e96 100644 --- a/homeassistant/components/message_bird/manifest.json +++ b/homeassistant/components/message_bird/manifest.json @@ -3,5 +3,6 @@ "name": "MessageBird", "documentation": "https://www.home-assistant.io/integrations/message_bird", "requirements": ["messagebird==1.2.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/met/manifest.json b/homeassistant/components/met/manifest.json index 38b77a0afd2df..950251958098d 100644 --- a/homeassistant/components/met/manifest.json +++ b/homeassistant/components/met/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/met", "requirements": ["pyMetno==0.8.1"], - "codeowners": ["@danielhiversen", "@thimic"] + "codeowners": ["@danielhiversen", "@thimic"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/met_eireann/manifest.json b/homeassistant/components/met_eireann/manifest.json index 5fe6ec5104592..9d2e1857689d9 100644 --- a/homeassistant/components/met_eireann/manifest.json +++ b/homeassistant/components/met_eireann/manifest.json @@ -1,8 +1,9 @@ { - "domain": "met_eireann", - "name": "Met Éireann", - "config_flow": true, - "documentation": "https://www.home-assistant.io/integrations/met_eireann", - "requirements": ["pyMetEireann==0.2"], - "codeowners": ["@DylanGore"] + "domain": "met_eireann", + "name": "Met Éireann", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/met_eireann", + "requirements": ["pyMetEireann==0.2"], + "codeowners": ["@DylanGore"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/meteo_france/manifest.json b/homeassistant/components/meteo_france/manifest.json index 6ffcda2922997..e7d1c4bd64a52 100644 --- a/homeassistant/components/meteo_france/manifest.json +++ b/homeassistant/components/meteo_france/manifest.json @@ -1,14 +1,9 @@ { "domain": "meteo_france", - "name": "Météo-France", + "name": "M\u00e9t\u00e9o-France", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/meteo_france", - "requirements": [ - "meteofrance-api==1.0.2" - ], - "codeowners": [ - "@hacf-fr", - "@oncleben31", - "@Quentame" - ] -} \ No newline at end of file + "requirements": ["meteofrance-api==1.0.2"], + "codeowners": ["@hacf-fr", "@oncleben31", "@Quentame"], + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/meteoalarm/manifest.json b/homeassistant/components/meteoalarm/manifest.json index 116bbdcac6df0..0888a8fa06381 100644 --- a/homeassistant/components/meteoalarm/manifest.json +++ b/homeassistant/components/meteoalarm/manifest.json @@ -3,5 +3,6 @@ "name": "MeteoAlarm", "documentation": "https://www.home-assistant.io/integrations/meteoalarm", "requirements": ["meteoalertapi==0.1.6"], - "codeowners": ["@rolfberkenbosch"] + "codeowners": ["@rolfberkenbosch"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/metoffice/manifest.json b/homeassistant/components/metoffice/manifest.json index 0c5d4e1d6251d..31a768eee8d2d 100644 --- a/homeassistant/components/metoffice/manifest.json +++ b/homeassistant/components/metoffice/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/metoffice", "requirements": ["datapoint==0.9.5"], "codeowners": ["@MrHarcombe"], - "config_flow": true + "config_flow": true, + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/mfi/manifest.json b/homeassistant/components/mfi/manifest.json index 29b9bb1ac697b..8ac5f3876351d 100644 --- a/homeassistant/components/mfi/manifest.json +++ b/homeassistant/components/mfi/manifest.json @@ -3,5 +3,6 @@ "name": "Ubiquiti mFi mPort", "documentation": "https://www.home-assistant.io/integrations/mfi", "requirements": ["mficlient==0.3.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/mhz19/manifest.json b/homeassistant/components/mhz19/manifest.json index ea16ac697f1eb..aa2271f2dd4d5 100644 --- a/homeassistant/components/mhz19/manifest.json +++ b/homeassistant/components/mhz19/manifest.json @@ -3,5 +3,6 @@ "name": "MH-Z19 CO2 Sensor", "documentation": "https://www.home-assistant.io/integrations/mhz19", "requirements": ["pmsensor==0.4"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/microsoft/manifest.json b/homeassistant/components/microsoft/manifest.json index 5b936bc7dedad..299209e9b9760 100644 --- a/homeassistant/components/microsoft/manifest.json +++ b/homeassistant/components/microsoft/manifest.json @@ -3,5 +3,6 @@ "name": "Microsoft Text-to-Speech (TTS)", "documentation": "https://www.home-assistant.io/integrations/microsoft", "requirements": ["pycsspeechtts==1.0.4"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/microsoft_face/manifest.json b/homeassistant/components/microsoft_face/manifest.json index 7677cc989b64a..2eb1b8df2a42c 100644 --- a/homeassistant/components/microsoft_face/manifest.json +++ b/homeassistant/components/microsoft_face/manifest.json @@ -3,5 +3,6 @@ "name": "Microsoft Face", "documentation": "https://www.home-assistant.io/integrations/microsoft_face", "dependencies": ["camera"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/microsoft_face_detect/manifest.json b/homeassistant/components/microsoft_face_detect/manifest.json index ea57b2bb134e4..1d087ab8bb433 100644 --- a/homeassistant/components/microsoft_face_detect/manifest.json +++ b/homeassistant/components/microsoft_face_detect/manifest.json @@ -3,5 +3,6 @@ "name": "Microsoft Face Detect", "documentation": "https://www.home-assistant.io/integrations/microsoft_face_detect", "dependencies": ["microsoft_face"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/microsoft_face_identify/manifest.json b/homeassistant/components/microsoft_face_identify/manifest.json index 866abde36736f..5d6f3c91f7ddf 100644 --- a/homeassistant/components/microsoft_face_identify/manifest.json +++ b/homeassistant/components/microsoft_face_identify/manifest.json @@ -3,5 +3,6 @@ "name": "Microsoft Face Identify", "documentation": "https://www.home-assistant.io/integrations/microsoft_face_identify", "dependencies": ["microsoft_face"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/miflora/manifest.json b/homeassistant/components/miflora/manifest.json index eb8a9c1c38fde..3a56a1b72fdd5 100644 --- a/homeassistant/components/miflora/manifest.json +++ b/homeassistant/components/miflora/manifest.json @@ -3,5 +3,6 @@ "name": "Mi Flora", "documentation": "https://www.home-assistant.io/integrations/miflora", "requirements": ["bluepy==1.3.0", "miflora==0.7.0"], - "codeowners": ["@danielhiversen", "@basnijholt"] + "codeowners": ["@danielhiversen", "@basnijholt"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/mikrotik/manifest.json b/homeassistant/components/mikrotik/manifest.json index 41223f97a8e8e..fdb6774f4b647 100644 --- a/homeassistant/components/mikrotik/manifest.json +++ b/homeassistant/components/mikrotik/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/mikrotik", "requirements": ["librouteros==3.0.0"], - "codeowners": ["@engrbm87"] + "codeowners": ["@engrbm87"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/mill/manifest.json b/homeassistant/components/mill/manifest.json index d0faa1e2ed52a..495ee960588d8 100644 --- a/homeassistant/components/mill/manifest.json +++ b/homeassistant/components/mill/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/mill", "requirements": ["millheater==0.4.0"], "codeowners": ["@danielhiversen"], - "config_flow": true + "config_flow": true, + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/min_max/manifest.json b/homeassistant/components/min_max/manifest.json index d4eb6554405cf..525d6c0ac1af8 100644 --- a/homeassistant/components/min_max/manifest.json +++ b/homeassistant/components/min_max/manifest.json @@ -3,5 +3,6 @@ "name": "Min/Max", "documentation": "https://www.home-assistant.io/integrations/min_max", "codeowners": ["@fabaff"], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_polling" } diff --git a/homeassistant/components/minecraft_server/manifest.json b/homeassistant/components/minecraft_server/manifest.json index 2c4a2ae4b8eac..61860fb163ade 100644 --- a/homeassistant/components/minecraft_server/manifest.json +++ b/homeassistant/components/minecraft_server/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/minecraft_server", "requirements": ["aiodns==2.0.0", "getmac==0.8.2", "mcstatus==5.1.1"], "codeowners": ["@elmurato"], - "quality_scale": "silver" + "quality_scale": "silver", + "iot_class": "local_polling" } diff --git a/homeassistant/components/minio/manifest.json b/homeassistant/components/minio/manifest.json index ba31bbcb2de72..45ba422c33101 100644 --- a/homeassistant/components/minio/manifest.json +++ b/homeassistant/components/minio/manifest.json @@ -3,5 +3,6 @@ "name": "Minio", "documentation": "https://www.home-assistant.io/integrations/minio", "requirements": ["minio==4.0.9"], - "codeowners": ["@tkislan"] + "codeowners": ["@tkislan"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/mitemp_bt/manifest.json b/homeassistant/components/mitemp_bt/manifest.json index d35e50a8657c8..8c5906ae43907 100644 --- a/homeassistant/components/mitemp_bt/manifest.json +++ b/homeassistant/components/mitemp_bt/manifest.json @@ -3,5 +3,6 @@ "name": "Xiaomi Mijia BLE Temperature and Humidity Sensor", "documentation": "https://www.home-assistant.io/integrations/mitemp_bt", "requirements": ["mitemp_bt==0.0.3"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/mjpeg/manifest.json b/homeassistant/components/mjpeg/manifest.json index 1e2bb33a24cbb..88e4cdba35628 100644 --- a/homeassistant/components/mjpeg/manifest.json +++ b/homeassistant/components/mjpeg/manifest.json @@ -2,5 +2,6 @@ "domain": "mjpeg", "name": "MJPEG IP Camera", "documentation": "https://www.home-assistant.io/integrations/mjpeg", - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/mobile_app/manifest.json b/homeassistant/components/mobile_app/manifest.json index bd8ed7713484f..2372ee0c51572 100644 --- a/homeassistant/components/mobile_app/manifest.json +++ b/homeassistant/components/mobile_app/manifest.json @@ -7,5 +7,6 @@ "dependencies": ["http", "webhook", "person", "tag"], "after_dependencies": ["cloud", "camera", "notify"], "codeowners": ["@robbiet480"], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_push" } diff --git a/homeassistant/components/mochad/manifest.json b/homeassistant/components/mochad/manifest.json index 63bd7405e0071..35a92dbb51b0a 100644 --- a/homeassistant/components/mochad/manifest.json +++ b/homeassistant/components/mochad/manifest.json @@ -3,5 +3,6 @@ "name": "Mochad", "documentation": "https://www.home-assistant.io/integrations/mochad", "requirements": ["pymochad==0.2.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/modbus/manifest.json b/homeassistant/components/modbus/manifest.json index 05e9c39c4b5b8..8d033968e2ff2 100644 --- a/homeassistant/components/modbus/manifest.json +++ b/homeassistant/components/modbus/manifest.json @@ -3,5 +3,6 @@ "name": "Modbus", "documentation": "https://www.home-assistant.io/integrations/modbus", "requirements": ["pymodbus==2.3.0"], - "codeowners": ["@adamchengtkc", "@janiversen", "@vzahradnik"] + "codeowners": ["@adamchengtkc", "@janiversen", "@vzahradnik"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/modem_callerid/manifest.json b/homeassistant/components/modem_callerid/manifest.json index 21e9c94943d25..a3bb7b676f0ea 100644 --- a/homeassistant/components/modem_callerid/manifest.json +++ b/homeassistant/components/modem_callerid/manifest.json @@ -3,5 +3,6 @@ "name": "Modem Caller ID", "documentation": "https://www.home-assistant.io/integrations/modem_callerid", "requirements": ["basicmodem==0.7"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/mold_indicator/manifest.json b/homeassistant/components/mold_indicator/manifest.json index 764faf6e79a85..ce10c8e3692ec 100644 --- a/homeassistant/components/mold_indicator/manifest.json +++ b/homeassistant/components/mold_indicator/manifest.json @@ -3,5 +3,6 @@ "name": "Mold Indicator", "documentation": "https://www.home-assistant.io/integrations/mold_indicator", "codeowners": [], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_polling" } diff --git a/homeassistant/components/monoprice/manifest.json b/homeassistant/components/monoprice/manifest.json index 93cebc9d88546..2001531a396eb 100644 --- a/homeassistant/components/monoprice/manifest.json +++ b/homeassistant/components/monoprice/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/monoprice", "requirements": ["pymonoprice==0.3"], "codeowners": ["@etsinko", "@OnFreund"], - "config_flow": true + "config_flow": true, + "iot_class": "local_polling" } diff --git a/homeassistant/components/moon/manifest.json b/homeassistant/components/moon/manifest.json index 8af5f40630c06..19fb952f59f01 100644 --- a/homeassistant/components/moon/manifest.json +++ b/homeassistant/components/moon/manifest.json @@ -3,5 +3,6 @@ "name": "Moon", "documentation": "https://www.home-assistant.io/integrations/moon", "codeowners": ["@fabaff"], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_polling" } diff --git a/homeassistant/components/motion_blinds/manifest.json b/homeassistant/components/motion_blinds/manifest.json index c144dc99bc5b3..83007cf562c26 100644 --- a/homeassistant/components/motion_blinds/manifest.json +++ b/homeassistant/components/motion_blinds/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/motion_blinds", "requirements": ["motionblinds==0.4.10"], - "codeowners": ["@starkillerOG"] + "codeowners": ["@starkillerOG"], + "iot_class": "local_push" } diff --git a/homeassistant/components/mpchc/manifest.json b/homeassistant/components/mpchc/manifest.json index 2ff6793151856..a1a9e769be617 100644 --- a/homeassistant/components/mpchc/manifest.json +++ b/homeassistant/components/mpchc/manifest.json @@ -2,5 +2,6 @@ "domain": "mpchc", "name": "Media Player Classic Home Cinema (MPC-HC)", "documentation": "https://www.home-assistant.io/integrations/mpchc", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/mpd/manifest.json b/homeassistant/components/mpd/manifest.json index a11b9fedd801c..39b4e45196b76 100644 --- a/homeassistant/components/mpd/manifest.json +++ b/homeassistant/components/mpd/manifest.json @@ -3,5 +3,6 @@ "name": "Music Player Daemon (MPD)", "documentation": "https://www.home-assistant.io/integrations/mpd", "requirements": ["python-mpd2==3.0.4"], - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/mqtt/manifest.json b/homeassistant/components/mqtt/manifest.json index 9de3b07184487..c5d9ad21ed614 100644 --- a/homeassistant/components/mqtt/manifest.json +++ b/homeassistant/components/mqtt/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/mqtt", "requirements": ["paho-mqtt==1.5.1"], "dependencies": ["http"], - "codeowners": ["@emontnemery"] + "codeowners": ["@emontnemery"], + "iot_class": "local_push" } diff --git a/homeassistant/components/mqtt_eventstream/manifest.json b/homeassistant/components/mqtt_eventstream/manifest.json index 87eb6bee31e2b..ec1fa9d2a5c64 100644 --- a/homeassistant/components/mqtt_eventstream/manifest.json +++ b/homeassistant/components/mqtt_eventstream/manifest.json @@ -3,5 +3,6 @@ "name": "MQTT Eventstream", "documentation": "https://www.home-assistant.io/integrations/mqtt_eventstream", "dependencies": ["mqtt"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/mqtt_json/manifest.json b/homeassistant/components/mqtt_json/manifest.json index 353ca20d5d77b..8a603f3539c50 100644 --- a/homeassistant/components/mqtt_json/manifest.json +++ b/homeassistant/components/mqtt_json/manifest.json @@ -3,5 +3,6 @@ "name": "MQTT JSON", "documentation": "https://www.home-assistant.io/integrations/mqtt_json", "dependencies": ["mqtt"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/mqtt_room/manifest.json b/homeassistant/components/mqtt_room/manifest.json index 814435ea8355c..5a5197550ad2c 100644 --- a/homeassistant/components/mqtt_room/manifest.json +++ b/homeassistant/components/mqtt_room/manifest.json @@ -3,5 +3,6 @@ "name": "MQTT Room Presence", "documentation": "https://www.home-assistant.io/integrations/mqtt_room", "dependencies": ["mqtt"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/mqtt_statestream/manifest.json b/homeassistant/components/mqtt_statestream/manifest.json index eb8556d8d9ffb..dec6d4d09d2b9 100644 --- a/homeassistant/components/mqtt_statestream/manifest.json +++ b/homeassistant/components/mqtt_statestream/manifest.json @@ -3,5 +3,6 @@ "name": "MQTT Statestream", "documentation": "https://www.home-assistant.io/integrations/mqtt_statestream", "dependencies": ["mqtt"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/msteams/manifest.json b/homeassistant/components/msteams/manifest.json index 184e50915a55d..3024bfb310ba3 100644 --- a/homeassistant/components/msteams/manifest.json +++ b/homeassistant/components/msteams/manifest.json @@ -3,5 +3,6 @@ "name": "Microsoft Teams", "documentation": "https://www.home-assistant.io/integrations/msteams", "requirements": ["pymsteams==0.1.12"], - "codeowners": ["@peroyvind"] + "codeowners": ["@peroyvind"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/mullvad/manifest.json b/homeassistant/components/mullvad/manifest.json index 1a440240d7e1c..6a9bf2017ab9f 100644 --- a/homeassistant/components/mullvad/manifest.json +++ b/homeassistant/components/mullvad/manifest.json @@ -3,10 +3,7 @@ "name": "Mullvad VPN", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/mullvad", - "requirements": [ - "mullvad-api==1.0.0" - ], - "codeowners": [ - "@meichthys" - ] + "requirements": ["mullvad-api==1.0.0"], + "codeowners": ["@meichthys"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/mvglive/manifest.json b/homeassistant/components/mvglive/manifest.json index e676cb0438cf8..90c4b5a9ec08d 100644 --- a/homeassistant/components/mvglive/manifest.json +++ b/homeassistant/components/mvglive/manifest.json @@ -3,5 +3,6 @@ "name": "MVG", "documentation": "https://www.home-assistant.io/integrations/mvglive", "requirements": ["PyMVGLive==1.1.4"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/mychevy/manifest.json b/homeassistant/components/mychevy/manifest.json index 5c34290f425ad..e726d49bb6482 100644 --- a/homeassistant/components/mychevy/manifest.json +++ b/homeassistant/components/mychevy/manifest.json @@ -3,5 +3,6 @@ "name": "myChevrolet", "documentation": "https://www.home-assistant.io/integrations/mychevy", "requirements": ["mychevy==2.1.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/mycroft/manifest.json b/homeassistant/components/mycroft/manifest.json index 33fafacaa8884..21fc51fa9eeed 100644 --- a/homeassistant/components/mycroft/manifest.json +++ b/homeassistant/components/mycroft/manifest.json @@ -3,5 +3,6 @@ "name": "Mycroft", "documentation": "https://www.home-assistant.io/integrations/mycroft", "requirements": ["mycroftapi==2.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/myq/manifest.json b/homeassistant/components/myq/manifest.json index 2098480af523b..350ba24c7c0e8 100644 --- a/homeassistant/components/myq/manifest.json +++ b/homeassistant/components/myq/manifest.json @@ -7,5 +7,6 @@ "config_flow": true, "homekit": { "models": ["819LMB"] - } + }, + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/mysensors/manifest.json b/homeassistant/components/mysensors/manifest.json index c7d439dedc44d..3b7695146ba09 100644 --- a/homeassistant/components/mysensors/manifest.json +++ b/homeassistant/components/mysensors/manifest.json @@ -5,5 +5,6 @@ "requirements": ["pymysensors==0.21.0"], "after_dependencies": ["mqtt"], "codeowners": ["@MartinHjelmare", "@functionpointer"], - "config_flow": true + "config_flow": true, + "iot_class": "local_push" } diff --git a/homeassistant/components/mystrom/manifest.json b/homeassistant/components/mystrom/manifest.json index 71a719be92ad6..5becef7fff26a 100644 --- a/homeassistant/components/mystrom/manifest.json +++ b/homeassistant/components/mystrom/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/mystrom", "requirements": ["python-mystrom==1.1.2"], "dependencies": ["http"], - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/mythicbeastsdns/manifest.json b/homeassistant/components/mythicbeastsdns/manifest.json index b710cd05c1305..50841f21f3a6e 100644 --- a/homeassistant/components/mythicbeastsdns/manifest.json +++ b/homeassistant/components/mythicbeastsdns/manifest.json @@ -3,5 +3,6 @@ "name": "Mythic Beasts DNS", "documentation": "https://www.home-assistant.io/integrations/mythicbeastsdns", "requirements": ["mbddns==0.1.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/n26/manifest.json b/homeassistant/components/n26/manifest.json index 2dec0e6ba2de7..a73f4742fae17 100644 --- a/homeassistant/components/n26/manifest.json +++ b/homeassistant/components/n26/manifest.json @@ -3,5 +3,6 @@ "name": "N26", "documentation": "https://www.home-assistant.io/integrations/n26", "requirements": ["n26==0.2.7"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/nad/manifest.json b/homeassistant/components/nad/manifest.json index 97dce35063bf1..063ceca0fd7d2 100644 --- a/homeassistant/components/nad/manifest.json +++ b/homeassistant/components/nad/manifest.json @@ -3,5 +3,6 @@ "name": "NAD", "documentation": "https://www.home-assistant.io/integrations/nad", "requirements": ["nad_receiver==0.0.12"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/namecheapdns/manifest.json b/homeassistant/components/namecheapdns/manifest.json index 9015f2dc8470a..7b94b09885dac 100644 --- a/homeassistant/components/namecheapdns/manifest.json +++ b/homeassistant/components/namecheapdns/manifest.json @@ -3,5 +3,6 @@ "name": "Namecheap FreeDNS", "documentation": "https://www.home-assistant.io/integrations/namecheapdns", "requirements": ["defusedxml==0.6.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/nanoleaf/manifest.json b/homeassistant/components/nanoleaf/manifest.json index 1f0fbf80983c6..0984962fb7300 100644 --- a/homeassistant/components/nanoleaf/manifest.json +++ b/homeassistant/components/nanoleaf/manifest.json @@ -3,5 +3,6 @@ "name": "Nanoleaf", "documentation": "https://www.home-assistant.io/integrations/nanoleaf", "requirements": ["pynanoleaf==0.1.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/neato/manifest.json b/homeassistant/components/neato/manifest.json index 5cd6a7558b112..7632360d13c68 100644 --- a/homeassistant/components/neato/manifest.json +++ b/homeassistant/components/neato/manifest.json @@ -3,14 +3,8 @@ "name": "Neato Botvac", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/neato", - "requirements": [ - "pybotvac==0.0.20" - ], - "codeowners": [ - "@dshokouhi", - "@Santobert" - ], - "dependencies": [ - "http" - ] -} \ No newline at end of file + "requirements": ["pybotvac==0.0.20"], + "codeowners": ["@dshokouhi", "@Santobert"], + "dependencies": ["http"], + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/nederlandse_spoorwegen/manifest.json b/homeassistant/components/nederlandse_spoorwegen/manifest.json index 01372e744fbc7..92de680c17af9 100644 --- a/homeassistant/components/nederlandse_spoorwegen/manifest.json +++ b/homeassistant/components/nederlandse_spoorwegen/manifest.json @@ -3,5 +3,6 @@ "name": "Nederlandse Spoorwegen (NS)", "documentation": "https://www.home-assistant.io/integrations/nederlandse_spoorwegen", "requirements": ["nsapi==3.0.4"], - "codeowners": ["@YarmoM"] + "codeowners": ["@YarmoM"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/nello/manifest.json b/homeassistant/components/nello/manifest.json index c8324022b6316..790b861054386 100644 --- a/homeassistant/components/nello/manifest.json +++ b/homeassistant/components/nello/manifest.json @@ -3,5 +3,6 @@ "name": "Nello", "documentation": "https://www.home-assistant.io/integrations/nello", "requirements": ["pynello==2.0.3"], - "codeowners": ["@pschmitt"] + "codeowners": ["@pschmitt"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/ness_alarm/manifest.json b/homeassistant/components/ness_alarm/manifest.json index 1977328c33a19..57c89e52ee86d 100644 --- a/homeassistant/components/ness_alarm/manifest.json +++ b/homeassistant/components/ness_alarm/manifest.json @@ -3,5 +3,6 @@ "name": "Ness Alarm", "documentation": "https://www.home-assistant.io/integrations/ness_alarm", "requirements": ["nessclient==0.9.15"], - "codeowners": ["@nickw444"] + "codeowners": ["@nickw444"], + "iot_class": "local_push" } diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 734261d9b0861..201ae40583e90 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -7,5 +7,10 @@ "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.2.12"], "codeowners": ["@allenporter"], "quality_scale": "platinum", - "dhcp": [{"macaddress":"18B430*"}] + "dhcp": [ + { + "macaddress": "18B430*" + } + ], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index 34307f2311d3a..bd33efb6ea14c 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -2,26 +2,13 @@ "domain": "netatmo", "name": "Netatmo", "documentation": "https://www.home-assistant.io/integrations/netatmo", - "requirements": [ - "pyatmo==4.2.2" - ], - "after_dependencies": [ - "cloud", - "media_source" - ], - "dependencies": [ - "webhook" - ], - "codeowners": [ - "@cgtobi" - ], + "requirements": ["pyatmo==4.2.2"], + "after_dependencies": ["cloud", "media_source"], + "dependencies": ["webhook"], + "codeowners": ["@cgtobi"], "config_flow": true, "homekit": { - "models": [ - "Healty Home Coach", - "Netatmo Relay", - "Presence", - "Welcome" - ] - } -} \ No newline at end of file + "models": ["Healty Home Coach", "Netatmo Relay", "Presence", "Welcome"] + }, + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/netdata/manifest.json b/homeassistant/components/netdata/manifest.json index 02a5bbddacd37..9d79f54450c9e 100644 --- a/homeassistant/components/netdata/manifest.json +++ b/homeassistant/components/netdata/manifest.json @@ -3,5 +3,6 @@ "name": "Netdata", "documentation": "https://www.home-assistant.io/integrations/netdata", "requirements": ["netdata==0.2.0"], - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/netgear/manifest.json b/homeassistant/components/netgear/manifest.json index 1126bbe558f31..713101f657f38 100644 --- a/homeassistant/components/netgear/manifest.json +++ b/homeassistant/components/netgear/manifest.json @@ -3,5 +3,6 @@ "name": "NETGEAR", "documentation": "https://www.home-assistant.io/integrations/netgear", "requirements": ["pynetgear==0.6.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/netgear_lte/manifest.json b/homeassistant/components/netgear_lte/manifest.json index e910132e78424..c02393e0f54cf 100644 --- a/homeassistant/components/netgear_lte/manifest.json +++ b/homeassistant/components/netgear_lte/manifest.json @@ -3,5 +3,6 @@ "name": "NETGEAR LTE", "documentation": "https://www.home-assistant.io/integrations/netgear_lte", "requirements": ["eternalegypt==0.0.12"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/netio/manifest.json b/homeassistant/components/netio/manifest.json index ef3d4a9519fec..3a246404c9152 100644 --- a/homeassistant/components/netio/manifest.json +++ b/homeassistant/components/netio/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/netio", "requirements": ["pynetio==0.1.9.1"], "dependencies": ["http"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/neurio_energy/manifest.json b/homeassistant/components/neurio_energy/manifest.json index bba814966dfd6..a46acb46dc623 100644 --- a/homeassistant/components/neurio_energy/manifest.json +++ b/homeassistant/components/neurio_energy/manifest.json @@ -3,5 +3,6 @@ "name": "Neurio energy", "documentation": "https://www.home-assistant.io/integrations/neurio_energy", "requirements": ["neurio==0.3.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/nexia/manifest.json b/homeassistant/components/nexia/manifest.json index 253400c886d15..5411723d2e230 100644 --- a/homeassistant/components/nexia/manifest.json +++ b/homeassistant/components/nexia/manifest.json @@ -5,5 +5,11 @@ "codeowners": ["@bdraco"], "documentation": "https://www.home-assistant.io/integrations/nexia", "config_flow": true, - "dhcp": [{"hostname":"xl857-*","macaddress":"000231*"}] + "dhcp": [ + { + "hostname": "xl857-*", + "macaddress": "000231*" + } + ], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/nextbus/manifest.json b/homeassistant/components/nextbus/manifest.json index 0f32505536a7a..71001bfc52c6e 100644 --- a/homeassistant/components/nextbus/manifest.json +++ b/homeassistant/components/nextbus/manifest.json @@ -3,5 +3,6 @@ "name": "NextBus", "documentation": "https://www.home-assistant.io/integrations/nextbus", "codeowners": ["@vividboarder"], - "requirements": ["py_nextbusnext==0.1.4"] + "requirements": ["py_nextbusnext==0.1.4"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/nextcloud/manifest.json b/homeassistant/components/nextcloud/manifest.json index 73ec2a138b3ba..03b1f429fea35 100644 --- a/homeassistant/components/nextcloud/manifest.json +++ b/homeassistant/components/nextcloud/manifest.json @@ -3,5 +3,6 @@ "name": "Nextcloud", "documentation": "https://www.home-assistant.io/integrations/nextcloud", "requirements": ["nextcloudmonitor==1.1.0"], - "codeowners": ["@meichthys"] + "codeowners": ["@meichthys"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/nfandroidtv/manifest.json b/homeassistant/components/nfandroidtv/manifest.json index e727c47b1e329..6f29d4d410e13 100644 --- a/homeassistant/components/nfandroidtv/manifest.json +++ b/homeassistant/components/nfandroidtv/manifest.json @@ -2,5 +2,6 @@ "domain": "nfandroidtv", "name": "Notifications for Android TV / FireTV", "documentation": "https://www.home-assistant.io/integrations/nfandroidtv", - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/nightscout/manifest.json b/homeassistant/components/nightscout/manifest.json index ecc44258e907f..49cb077dc7956 100644 --- a/homeassistant/components/nightscout/manifest.json +++ b/homeassistant/components/nightscout/manifest.json @@ -3,11 +3,8 @@ "name": "Nightscout", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/nightscout", - "requirements": [ - "py-nightscout==1.2.2" - ], - "codeowners": [ - "@marciogranzotto" - ], - "quality_scale": "platinum" -} \ No newline at end of file + "requirements": ["py-nightscout==1.2.2"], + "codeowners": ["@marciogranzotto"], + "quality_scale": "platinum", + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/niko_home_control/manifest.json b/homeassistant/components/niko_home_control/manifest.json index f9e3cf8573b96..bb015a059b9d4 100644 --- a/homeassistant/components/niko_home_control/manifest.json +++ b/homeassistant/components/niko_home_control/manifest.json @@ -3,5 +3,6 @@ "name": "Niko Home Control", "documentation": "https://www.home-assistant.io/integrations/niko_home_control", "requirements": ["niko-home-control==0.2.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/nilu/manifest.json b/homeassistant/components/nilu/manifest.json index 1eb9464290270..bdc9220994798 100644 --- a/homeassistant/components/nilu/manifest.json +++ b/homeassistant/components/nilu/manifest.json @@ -3,5 +3,6 @@ "name": "Norwegian Institute for Air Research (NILU)", "documentation": "https://www.home-assistant.io/integrations/nilu", "requirements": ["niluclient==0.1.2"], - "codeowners": ["@hfurubotten"] + "codeowners": ["@hfurubotten"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/nissan_leaf/manifest.json b/homeassistant/components/nissan_leaf/manifest.json index db78e5ce0e980..298343d2d8d25 100644 --- a/homeassistant/components/nissan_leaf/manifest.json +++ b/homeassistant/components/nissan_leaf/manifest.json @@ -3,5 +3,6 @@ "name": "Nissan Leaf", "documentation": "https://www.home-assistant.io/integrations/nissan_leaf", "requirements": ["pycarwings2==2.10"], - "codeowners": ["@filcole"] + "codeowners": ["@filcole"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/nmap_tracker/manifest.json b/homeassistant/components/nmap_tracker/manifest.json index 1b049b54a07b6..9f81c0facaf70 100644 --- a/homeassistant/components/nmap_tracker/manifest.json +++ b/homeassistant/components/nmap_tracker/manifest.json @@ -3,5 +3,6 @@ "name": "Nmap Tracker", "documentation": "https://www.home-assistant.io/integrations/nmap_tracker", "requirements": ["python-nmap==0.6.1", "getmac==0.8.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/nmbs/manifest.json b/homeassistant/components/nmbs/manifest.json index e9b1d1ecbf759..82723f9792433 100644 --- a/homeassistant/components/nmbs/manifest.json +++ b/homeassistant/components/nmbs/manifest.json @@ -3,5 +3,6 @@ "name": "NMBS", "documentation": "https://www.home-assistant.io/integrations/nmbs", "requirements": ["pyrail==0.0.3"], - "codeowners": ["@thibmaek"] + "codeowners": ["@thibmaek"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/no_ip/manifest.json b/homeassistant/components/no_ip/manifest.json index 8294ba650721a..565ef8a78407d 100644 --- a/homeassistant/components/no_ip/manifest.json +++ b/homeassistant/components/no_ip/manifest.json @@ -2,5 +2,6 @@ "domain": "no_ip", "name": "No-IP.com", "documentation": "https://www.home-assistant.io/integrations/no_ip", - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/noaa_tides/manifest.json b/homeassistant/components/noaa_tides/manifest.json index f0343d88c843f..8ad99c8a5c22e 100644 --- a/homeassistant/components/noaa_tides/manifest.json +++ b/homeassistant/components/noaa_tides/manifest.json @@ -3,5 +3,6 @@ "name": "NOAA Tides", "documentation": "https://www.home-assistant.io/integrations/noaa_tides", "requirements": ["noaa-coops==0.1.8"], - "codeowners": ["@jdelaney72"] + "codeowners": ["@jdelaney72"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/norway_air/manifest.json b/homeassistant/components/norway_air/manifest.json index 193d96e2a189b..db4415932a5bb 100644 --- a/homeassistant/components/norway_air/manifest.json +++ b/homeassistant/components/norway_air/manifest.json @@ -3,5 +3,6 @@ "name": "Om Luftkvalitet i Norge (Norway Air)", "documentation": "https://www.home-assistant.io/integrations/norway_air", "requirements": ["pyMetno==0.8.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/notify_events/manifest.json b/homeassistant/components/notify_events/manifest.json index 9f0055e01642a..96eda381506ac 100644 --- a/homeassistant/components/notify_events/manifest.json +++ b/homeassistant/components/notify_events/manifest.json @@ -3,5 +3,6 @@ "name": "Notify.Events", "documentation": "https://www.home-assistant.io/integrations/notify_events", "codeowners": ["@matrozov", "@papajojo"], - "requirements": ["notify-events==1.0.4"] + "requirements": ["notify-events==1.0.4"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/notion/manifest.json b/homeassistant/components/notion/manifest.json index 94d123ed17f0a..191f66ee59d7d 100644 --- a/homeassistant/components/notion/manifest.json +++ b/homeassistant/components/notion/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/notion", "requirements": ["aionotion==1.1.0"], - "codeowners": ["@bachya"] + "codeowners": ["@bachya"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/nsw_fuel_station/manifest.json b/homeassistant/components/nsw_fuel_station/manifest.json index bdc9847c14fc5..4dca09e77eaf7 100644 --- a/homeassistant/components/nsw_fuel_station/manifest.json +++ b/homeassistant/components/nsw_fuel_station/manifest.json @@ -3,5 +3,6 @@ "name": "NSW Fuel Station Price", "documentation": "https://www.home-assistant.io/integrations/nsw_fuel_station", "requirements": ["nsw-fuel-api-client==1.0.10"], - "codeowners": ["@nickw444"] + "codeowners": ["@nickw444"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/nsw_rural_fire_service_feed/manifest.json b/homeassistant/components/nsw_rural_fire_service_feed/manifest.json index aa8275ad0842b..debc255ec7f2b 100644 --- a/homeassistant/components/nsw_rural_fire_service_feed/manifest.json +++ b/homeassistant/components/nsw_rural_fire_service_feed/manifest.json @@ -3,5 +3,6 @@ "name": "NSW Rural Fire Service Incidents", "documentation": "https://www.home-assistant.io/integrations/nsw_rural_fire_service_feed", "requirements": ["aio_geojson_nsw_rfs_incidents==0.3"], - "codeowners": ["@exxamalte"] + "codeowners": ["@exxamalte"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/nuheat/manifest.json b/homeassistant/components/nuheat/manifest.json index 92527f5066086..64f7c0e43e455 100644 --- a/homeassistant/components/nuheat/manifest.json +++ b/homeassistant/components/nuheat/manifest.json @@ -5,5 +5,11 @@ "requirements": ["nuheat==0.3.0"], "codeowners": ["@bdraco"], "config_flow": true, - "dhcp": [{"hostname":"nuheat","macaddress":"002338*"}] + "dhcp": [ + { + "hostname": "nuheat", + "macaddress": "002338*" + } + ], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/nuki/manifest.json b/homeassistant/components/nuki/manifest.json index 8500a3c90aa06..4cc2599900d04 100644 --- a/homeassistant/components/nuki/manifest.json +++ b/homeassistant/components/nuki/manifest.json @@ -1,9 +1,14 @@ { - "domain": "nuki", - "name": "Nuki", - "documentation": "https://www.home-assistant.io/integrations/nuki", - "requirements": ["pynuki==1.4.1"], - "codeowners": ["@pschmitt", "@pvizeli", "@pree"], - "config_flow": true, - "dhcp": [{ "hostname": "nuki_bridge_*" }] -} \ No newline at end of file + "domain": "nuki", + "name": "Nuki", + "documentation": "https://www.home-assistant.io/integrations/nuki", + "requirements": ["pynuki==1.4.1"], + "codeowners": ["@pschmitt", "@pvizeli", "@pree"], + "config_flow": true, + "dhcp": [ + { + "hostname": "nuki_bridge_*" + } + ], + "iot_class": "local_polling" +} diff --git a/homeassistant/components/numato/manifest.json b/homeassistant/components/numato/manifest.json index 6138f401ec264..a65c4998554c6 100644 --- a/homeassistant/components/numato/manifest.json +++ b/homeassistant/components/numato/manifest.json @@ -3,5 +3,6 @@ "name": "Numato USB GPIO Expander", "documentation": "https://www.home-assistant.io/integrations/numato", "requirements": ["numato-gpio==0.10.0"], - "codeowners": ["@clssn"] + "codeowners": ["@clssn"], + "iot_class": "local_push" } diff --git a/homeassistant/components/nut/manifest.json b/homeassistant/components/nut/manifest.json index 693b225c6dda1..388858b93f0be 100644 --- a/homeassistant/components/nut/manifest.json +++ b/homeassistant/components/nut/manifest.json @@ -5,5 +5,6 @@ "requirements": ["pynut2==2.1.2"], "codeowners": ["@bdraco"], "config_flow": true, - "zeroconf": ["_nut._tcp.local."] + "zeroconf": ["_nut._tcp.local."], + "iot_class": "local_polling" } diff --git a/homeassistant/components/nws/manifest.json b/homeassistant/components/nws/manifest.json index ef0a35b846a68..d1e7158ab20c9 100644 --- a/homeassistant/components/nws/manifest.json +++ b/homeassistant/components/nws/manifest.json @@ -5,5 +5,6 @@ "codeowners": ["@MatthewFlamm"], "requirements": ["pynws==1.3.0"], "quality_scale": "platinum", - "config_flow": true + "config_flow": true, + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/nx584/manifest.json b/homeassistant/components/nx584/manifest.json index 57676870ce765..2aa3df8d167f2 100644 --- a/homeassistant/components/nx584/manifest.json +++ b/homeassistant/components/nx584/manifest.json @@ -3,5 +3,6 @@ "name": "NX584", "documentation": "https://www.home-assistant.io/integrations/nx584", "requirements": ["pynx584==0.5"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/nzbget/manifest.json b/homeassistant/components/nzbget/manifest.json index 7c5e9cf5e8d4c..951d5237736be 100644 --- a/homeassistant/components/nzbget/manifest.json +++ b/homeassistant/components/nzbget/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/nzbget", "requirements": ["pynzbgetapi==0.2.0"], "codeowners": ["@chriscla"], - "config_flow": true + "config_flow": true, + "iot_class": "local_polling" } diff --git a/homeassistant/components/oasa_telematics/manifest.json b/homeassistant/components/oasa_telematics/manifest.json index 84f5e78fec26c..a1d672ba59518 100644 --- a/homeassistant/components/oasa_telematics/manifest.json +++ b/homeassistant/components/oasa_telematics/manifest.json @@ -3,5 +3,6 @@ "name": "OASA Telematics", "documentation": "https://www.home-assistant.io/integrations/oasa_telematics/", "requirements": ["oasatelematics==0.3"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/obihai/manifest.json b/homeassistant/components/obihai/manifest.json index 78123cc07f507..05121c81ac73c 100644 --- a/homeassistant/components/obihai/manifest.json +++ b/homeassistant/components/obihai/manifest.json @@ -3,5 +3,6 @@ "name": "Obihai", "documentation": "https://www.home-assistant.io/integrations/obihai", "requirements": ["pyobihai==1.3.1"], - "codeowners": ["@dshokouhi"] + "codeowners": ["@dshokouhi"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/octoprint/manifest.json b/homeassistant/components/octoprint/manifest.json index 28e09cc7be957..85436f9617618 100644 --- a/homeassistant/components/octoprint/manifest.json +++ b/homeassistant/components/octoprint/manifest.json @@ -3,5 +3,6 @@ "name": "OctoPrint", "documentation": "https://www.home-assistant.io/integrations/octoprint", "after_dependencies": ["discovery"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/oem/manifest.json b/homeassistant/components/oem/manifest.json index 7ebacb9fa4e2e..29c2b1e7fa465 100644 --- a/homeassistant/components/oem/manifest.json +++ b/homeassistant/components/oem/manifest.json @@ -3,5 +3,6 @@ "name": "OpenEnergyMonitor WiFi Thermostat", "documentation": "https://www.home-assistant.io/integrations/oem", "requirements": ["oemthermostat==1.1.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/ohmconnect/manifest.json b/homeassistant/components/ohmconnect/manifest.json index 3eb0d4758af19..d2ee9bc70cd79 100644 --- a/homeassistant/components/ohmconnect/manifest.json +++ b/homeassistant/components/ohmconnect/manifest.json @@ -3,5 +3,6 @@ "name": "OhmConnect", "documentation": "https://www.home-assistant.io/integrations/ohmconnect", "requirements": ["defusedxml==0.6.0"], - "codeowners": ["@robbiet480"] + "codeowners": ["@robbiet480"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/ombi/manifest.json b/homeassistant/components/ombi/manifest.json index f61555495c325..2c9e40d830f86 100644 --- a/homeassistant/components/ombi/manifest.json +++ b/homeassistant/components/ombi/manifest.json @@ -3,5 +3,6 @@ "name": "Ombi", "documentation": "https://www.home-assistant.io/integrations/ombi/", "codeowners": ["@larssont"], - "requirements": ["pyombi==0.1.10"] + "requirements": ["pyombi==0.1.10"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/omnilogic/manifest.json b/homeassistant/components/omnilogic/manifest.json index 2b2a4a9fe3d31..c6de70d0b33cd 100644 --- a/homeassistant/components/omnilogic/manifest.json +++ b/homeassistant/components/omnilogic/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/omnilogic", "requirements": ["omnilogic==0.4.3"], - "codeowners": ["@oliver84","@djtimca","@gentoosu"] + "codeowners": ["@oliver84", "@djtimca", "@gentoosu"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/onboarding/manifest.json b/homeassistant/components/onboarding/manifest.json index 06c9946b5c9b6..fe65d82f626f9 100644 --- a/homeassistant/components/onboarding/manifest.json +++ b/homeassistant/components/onboarding/manifest.json @@ -2,17 +2,8 @@ "domain": "onboarding", "name": "Home Assistant Onboarding", "documentation": "https://www.home-assistant.io/integrations/onboarding", - "after_dependencies": [ - "hassio" - ], - "dependencies": [ - "analytics", - "auth", - "http", - "person" - ], - "codeowners": [ - "@home-assistant/core" - ], + "after_dependencies": ["hassio"], + "dependencies": ["analytics", "auth", "http", "person"], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } diff --git a/homeassistant/components/ondilo_ico/manifest.json b/homeassistant/components/ondilo_ico/manifest.json index ee1afd315d649..4c3ee64779a35 100644 --- a/homeassistant/components/ondilo_ico/manifest.json +++ b/homeassistant/components/ondilo_ico/manifest.json @@ -3,13 +3,8 @@ "name": "Ondilo ICO", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ondilo_ico", - "requirements": [ - "ondilo==0.2.0" - ], - "dependencies": [ - "http" - ], - "codeowners": [ - "@JeromeHXP" - ] -} \ No newline at end of file + "requirements": ["ondilo==0.2.0"], + "dependencies": ["http"], + "codeowners": ["@JeromeHXP"], + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/onewire/manifest.json b/homeassistant/components/onewire/manifest.json index 47ab6ad24046f..f48236c7f37ea 100644 --- a/homeassistant/components/onewire/manifest.json +++ b/homeassistant/components/onewire/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/onewire", "config_flow": true, "requirements": ["pyownet==0.10.0.post1", "pi1wire==0.1.0"], - "codeowners": ["@garbled1", "@epenet"] + "codeowners": ["@garbled1", "@epenet"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/onkyo/manifest.json b/homeassistant/components/onkyo/manifest.json index a1a7659bae50a..39c1686d03e61 100644 --- a/homeassistant/components/onkyo/manifest.json +++ b/homeassistant/components/onkyo/manifest.json @@ -3,5 +3,6 @@ "name": "Onkyo", "documentation": "https://www.home-assistant.io/integrations/onkyo", "requirements": ["onkyo-eiscp==1.2.7"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/onvif/manifest.json b/homeassistant/components/onvif/manifest.json index 7329f629affa4..641497f52047c 100644 --- a/homeassistant/components/onvif/manifest.json +++ b/homeassistant/components/onvif/manifest.json @@ -9,5 +9,6 @@ ], "dependencies": ["ffmpeg"], "codeowners": ["@hunterjm"], - "config_flow": true + "config_flow": true, + "iot_class": "local_push" } diff --git a/homeassistant/components/openalpr_cloud/manifest.json b/homeassistant/components/openalpr_cloud/manifest.json index dbb8253ff96d0..74b593bd1acfe 100644 --- a/homeassistant/components/openalpr_cloud/manifest.json +++ b/homeassistant/components/openalpr_cloud/manifest.json @@ -2,5 +2,6 @@ "domain": "openalpr_cloud", "name": "OpenALPR Cloud", "documentation": "https://www.home-assistant.io/integrations/openalpr_cloud", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/openalpr_local/manifest.json b/homeassistant/components/openalpr_local/manifest.json index 29b9c3a07d801..8837d79369d3e 100644 --- a/homeassistant/components/openalpr_local/manifest.json +++ b/homeassistant/components/openalpr_local/manifest.json @@ -2,5 +2,6 @@ "domain": "openalpr_local", "name": "OpenALPR Local", "documentation": "https://www.home-assistant.io/integrations/openalpr_local", - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/opencv/manifest.json b/homeassistant/components/opencv/manifest.json index a0294a7aa49ce..b2fecaf814417 100644 --- a/homeassistant/components/opencv/manifest.json +++ b/homeassistant/components/opencv/manifest.json @@ -3,5 +3,6 @@ "name": "OpenCV", "documentation": "https://www.home-assistant.io/integrations/opencv", "requirements": ["numpy==1.20.2", "opencv-python-headless==4.3.0.36"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/openerz/manifest.json b/homeassistant/components/openerz/manifest.json index 9fa696a873a0e..b1e3b0597b5a2 100644 --- a/homeassistant/components/openerz/manifest.json +++ b/homeassistant/components/openerz/manifest.json @@ -3,5 +3,6 @@ "name": "Open ERZ", "documentation": "https://www.home-assistant.io/integrations/openerz", "codeowners": ["@misialq"], - "requirements": ["openerz-api==0.1.0"] + "requirements": ["openerz-api==0.1.0"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/openevse/manifest.json b/homeassistant/components/openevse/manifest.json index 9cf38cbdd0d53..c4e5a5b7711f6 100644 --- a/homeassistant/components/openevse/manifest.json +++ b/homeassistant/components/openevse/manifest.json @@ -3,5 +3,6 @@ "name": "OpenEVSE", "documentation": "https://www.home-assistant.io/integrations/openevse", "requirements": ["openevsewifi==1.1.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/openexchangerates/manifest.json b/homeassistant/components/openexchangerates/manifest.json index 60484aca77c73..43c45b6b66530 100644 --- a/homeassistant/components/openexchangerates/manifest.json +++ b/homeassistant/components/openexchangerates/manifest.json @@ -2,5 +2,6 @@ "domain": "openexchangerates", "name": "Open Exchange Rates", "documentation": "https://www.home-assistant.io/integrations/openexchangerates", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/opengarage/manifest.json b/homeassistant/components/opengarage/manifest.json index 8bbf8c76c42c3..a14fb232eac57 100644 --- a/homeassistant/components/opengarage/manifest.json +++ b/homeassistant/components/opengarage/manifest.json @@ -2,8 +2,7 @@ "domain": "opengarage", "name": "OpenGarage", "documentation": "https://www.home-assistant.io/integrations/opengarage", - "codeowners": [ - "@danielhiversen" - ], - "requirements": ["open-garage==0.1.4"] + "codeowners": ["@danielhiversen"], + "requirements": ["open-garage==0.1.4"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/openhardwaremonitor/manifest.json b/homeassistant/components/openhardwaremonitor/manifest.json index 242b00175d852..faf98c11a6df1 100644 --- a/homeassistant/components/openhardwaremonitor/manifest.json +++ b/homeassistant/components/openhardwaremonitor/manifest.json @@ -2,5 +2,6 @@ "domain": "openhardwaremonitor", "name": "Open Hardware Monitor", "documentation": "https://www.home-assistant.io/integrations/openhardwaremonitor", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/openhome/manifest.json b/homeassistant/components/openhome/manifest.json index 98fbf2d961a81..f45d6d31cef17 100644 --- a/homeassistant/components/openhome/manifest.json +++ b/homeassistant/components/openhome/manifest.json @@ -3,5 +3,6 @@ "name": "Linn / OpenHome", "documentation": "https://www.home-assistant.io/integrations/openhome", "requirements": ["openhomedevice==0.7.2"], - "codeowners": ["@bazwilliams"] + "codeowners": ["@bazwilliams"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/opensensemap/manifest.json b/homeassistant/components/opensensemap/manifest.json index 780f5f59020d3..df750156d1de9 100644 --- a/homeassistant/components/opensensemap/manifest.json +++ b/homeassistant/components/opensensemap/manifest.json @@ -3,5 +3,6 @@ "name": "openSenseMap", "documentation": "https://www.home-assistant.io/integrations/opensensemap", "requirements": ["opensensemap-api==0.1.5"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/opensky/manifest.json b/homeassistant/components/opensky/manifest.json index 17479b70de787..38877042d59a7 100644 --- a/homeassistant/components/opensky/manifest.json +++ b/homeassistant/components/opensky/manifest.json @@ -2,5 +2,6 @@ "domain": "opensky", "name": "OpenSky Network", "documentation": "https://www.home-assistant.io/integrations/opensky", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/opentherm_gw/manifest.json b/homeassistant/components/opentherm_gw/manifest.json index baa02dc3f4604..463a0aa105289 100644 --- a/homeassistant/components/opentherm_gw/manifest.json +++ b/homeassistant/components/opentherm_gw/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/opentherm_gw", "requirements": ["pyotgw==1.1b1"], "codeowners": ["@mvn23"], - "config_flow": true + "config_flow": true, + "iot_class": "local_push" } diff --git a/homeassistant/components/openuv/manifest.json b/homeassistant/components/openuv/manifest.json index f55ca587679b7..81e38d251f124 100644 --- a/homeassistant/components/openuv/manifest.json +++ b/homeassistant/components/openuv/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/openuv", "requirements": ["pyopenuv==1.0.9"], - "codeowners": ["@bachya"] + "codeowners": ["@bachya"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/openweathermap/manifest.json b/homeassistant/components/openweathermap/manifest.json index 27cda9fb26dcd..0b0114328acb6 100644 --- a/homeassistant/components/openweathermap/manifest.json +++ b/homeassistant/components/openweathermap/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/openweathermap", "requirements": ["pyowm==3.2.0"], - "codeowners": ["@fabaff", "@freekode", "@nzapponi"] + "codeowners": ["@fabaff", "@freekode", "@nzapponi"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/opnsense/manifest.json b/homeassistant/components/opnsense/manifest.json index 129ca0108a5a7..ed390278969cf 100644 --- a/homeassistant/components/opnsense/manifest.json +++ b/homeassistant/components/opnsense/manifest.json @@ -3,5 +3,6 @@ "name": "OPNSense", "documentation": "https://www.home-assistant.io/integrations/opnsense", "requirements": ["pyopnsense==0.2.0"], - "codeowners": ["@mtreinish"] + "codeowners": ["@mtreinish"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/opple/manifest.json b/homeassistant/components/opple/manifest.json index bb6596c47ef96..1f0360e265a36 100644 --- a/homeassistant/components/opple/manifest.json +++ b/homeassistant/components/opple/manifest.json @@ -3,5 +3,6 @@ "name": "Opple", "documentation": "https://www.home-assistant.io/integrations/opple", "requirements": ["pyoppleio==1.0.5"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/orangepi_gpio/manifest.json b/homeassistant/components/orangepi_gpio/manifest.json index 904ff29cb1d09..7d96756a8d1b0 100644 --- a/homeassistant/components/orangepi_gpio/manifest.json +++ b/homeassistant/components/orangepi_gpio/manifest.json @@ -3,5 +3,6 @@ "name": "Orange Pi GPIO", "documentation": "https://www.home-assistant.io/integrations/orangepi_gpio", "requirements": ["OPi.GPIO==0.4.0"], - "codeowners": ["@pascallj"] + "codeowners": ["@pascallj"], + "iot_class": "local_push" } diff --git a/homeassistant/components/oru/manifest.json b/homeassistant/components/oru/manifest.json index 1be40a72d1c55..0d023a96ad5b8 100644 --- a/homeassistant/components/oru/manifest.json +++ b/homeassistant/components/oru/manifest.json @@ -3,5 +3,6 @@ "name": "Orange and Rockland Utility (ORU)", "documentation": "https://www.home-assistant.io/integrations/oru", "codeowners": ["@bvlaicu"], - "requirements": ["oru==0.1.11"] + "requirements": ["oru==0.1.11"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/orvibo/manifest.json b/homeassistant/components/orvibo/manifest.json index 83b5d644898ff..94c7391b6492c 100644 --- a/homeassistant/components/orvibo/manifest.json +++ b/homeassistant/components/orvibo/manifest.json @@ -3,5 +3,6 @@ "name": "Orvibo", "documentation": "https://www.home-assistant.io/integrations/orvibo", "requirements": ["orvibo==1.1.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/osramlightify/manifest.json b/homeassistant/components/osramlightify/manifest.json index 80cfeff6e12bd..0596d4073eba5 100644 --- a/homeassistant/components/osramlightify/manifest.json +++ b/homeassistant/components/osramlightify/manifest.json @@ -3,5 +3,6 @@ "name": "Osramlightify", "documentation": "https://www.home-assistant.io/integrations/osramlightify", "requirements": ["lightify==1.0.7.3"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/otp/manifest.json b/homeassistant/components/otp/manifest.json index cfd84eb206939..9b8b4527b2c4a 100644 --- a/homeassistant/components/otp/manifest.json +++ b/homeassistant/components/otp/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/otp", "requirements": ["pyotp==2.3.0"], "codeowners": [], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_polling" } diff --git a/homeassistant/components/ovo_energy/manifest.json b/homeassistant/components/ovo_energy/manifest.json index 6ec03eb19a5a1..37950df84ccb5 100644 --- a/homeassistant/components/ovo_energy/manifest.json +++ b/homeassistant/components/ovo_energy/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ovo_energy", "requirements": ["ovoenergy==1.1.11"], - "codeowners": ["@timmo001"] + "codeowners": ["@timmo001"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/owntracks/manifest.json b/homeassistant/components/owntracks/manifest.json index 0fcca8953c70f..9e83e5b4ec46b 100644 --- a/homeassistant/components/owntracks/manifest.json +++ b/homeassistant/components/owntracks/manifest.json @@ -6,5 +6,6 @@ "requirements": ["PyNaCl==1.3.0"], "dependencies": ["webhook"], "after_dependencies": ["mqtt", "cloud"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/ozw/manifest.json b/homeassistant/components/ozw/manifest.json index a1409fd79a81e..e2adce13339cb 100644 --- a/homeassistant/components/ozw/manifest.json +++ b/homeassistant/components/ozw/manifest.json @@ -3,15 +3,8 @@ "name": "OpenZWave (beta)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ozw", - "requirements": [ - "python-openzwave-mqtt[mqtt-client]==1.4.0" - ], - "after_dependencies": [ - "mqtt" - ], - "codeowners": [ - "@cgarwood", - "@marcelveldt", - "@MartinHjelmare" - ] + "requirements": ["python-openzwave-mqtt[mqtt-client]==1.4.0"], + "after_dependencies": ["mqtt"], + "codeowners": ["@cgarwood", "@marcelveldt", "@MartinHjelmare"], + "iot_class": "local_push" } diff --git a/homeassistant/components/panasonic_bluray/manifest.json b/homeassistant/components/panasonic_bluray/manifest.json index c7e50c1c91a46..a9d6a4ebf76cb 100644 --- a/homeassistant/components/panasonic_bluray/manifest.json +++ b/homeassistant/components/panasonic_bluray/manifest.json @@ -3,5 +3,6 @@ "name": "Panasonic Blu-Ray Player", "documentation": "https://www.home-assistant.io/integrations/panasonic_bluray", "requirements": ["panacotta==0.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/panasonic_viera/manifest.json b/homeassistant/components/panasonic_viera/manifest.json index 7b9a3d7d4e0bc..fe365f85f2cea 100644 --- a/homeassistant/components/panasonic_viera/manifest.json +++ b/homeassistant/components/panasonic_viera/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/panasonic_viera", "requirements": ["panasonic_viera==0.3.6"], "codeowners": [], - "config_flow": true + "config_flow": true, + "iot_class": "local_polling" } diff --git a/homeassistant/components/pandora/manifest.json b/homeassistant/components/pandora/manifest.json index 9ecb5b4b29d2c..45f87b36ec16f 100644 --- a/homeassistant/components/pandora/manifest.json +++ b/homeassistant/components/pandora/manifest.json @@ -3,5 +3,6 @@ "name": "Pandora", "documentation": "https://www.home-assistant.io/integrations/pandora", "requirements": ["pexpect==4.6.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/pcal9535a/manifest.json b/homeassistant/components/pcal9535a/manifest.json index 81802af1084a3..2e685a8625c51 100644 --- a/homeassistant/components/pcal9535a/manifest.json +++ b/homeassistant/components/pcal9535a/manifest.json @@ -3,5 +3,6 @@ "name": "PCAL9535A I/O Expander", "documentation": "https://www.home-assistant.io/integrations/pcal9535a", "requirements": ["pcal9535a==0.7"], - "codeowners": ["@Shulyaka"] + "codeowners": ["@Shulyaka"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/pencom/manifest.json b/homeassistant/components/pencom/manifest.json index 0637c18b64749..e8b44173fe91d 100644 --- a/homeassistant/components/pencom/manifest.json +++ b/homeassistant/components/pencom/manifest.json @@ -3,5 +3,6 @@ "name": "Pencom", "documentation": "https://www.home-assistant.io/integrations/pencom", "requirements": ["pencompy==0.0.3"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/persistent_notification/manifest.json b/homeassistant/components/persistent_notification/manifest.json index ff3ef06d97c89..c21e8150d8a81 100644 --- a/homeassistant/components/persistent_notification/manifest.json +++ b/homeassistant/components/persistent_notification/manifest.json @@ -3,5 +3,6 @@ "name": "Persistent Notification", "documentation": "https://www.home-assistant.io/integrations/persistent_notification", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_push" } diff --git a/homeassistant/components/person/manifest.json b/homeassistant/components/person/manifest.json index 7aec7df7c9a69..09b74bf34eba0 100644 --- a/homeassistant/components/person/manifest.json +++ b/homeassistant/components/person/manifest.json @@ -5,5 +5,6 @@ "dependencies": ["image"], "after_dependencies": ["device_tracker"], "codeowners": [], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "calculated" } diff --git a/homeassistant/components/philips_js/manifest.json b/homeassistant/components/philips_js/manifest.json index 36e01d8f3c8a6..d41ac0881bafe 100644 --- a/homeassistant/components/philips_js/manifest.json +++ b/homeassistant/components/philips_js/manifest.json @@ -2,11 +2,8 @@ "domain": "philips_js", "name": "Philips TV", "documentation": "https://www.home-assistant.io/integrations/philips_js", - "requirements": [ - "ha-philipsjs==2.7.0" - ], - "codeowners": [ - "@elupus" - ], - "config_flow": true -} \ No newline at end of file + "requirements": ["ha-philipsjs==2.7.0"], + "codeowners": ["@elupus"], + "config_flow": true, + "iot_class": "local_polling" +} diff --git a/homeassistant/components/pi4ioe5v9xxxx/manifest.json b/homeassistant/components/pi4ioe5v9xxxx/manifest.json index f399c52859df0..4e12fcd009cae 100644 --- a/homeassistant/components/pi4ioe5v9xxxx/manifest.json +++ b/homeassistant/components/pi4ioe5v9xxxx/manifest.json @@ -1,7 +1,8 @@ { - "domain": "pi4ioe5v9xxxx", - "name": "pi4ioe5v9xxxx IO Expander", - "documentation": "https://www.home-assistant.io/integrations/pi4ioe5v9xxxx", - "requirements": ["pi4ioe5v9xxxx==0.0.2"], - "codeowners": ["@antonverburg"] + "domain": "pi4ioe5v9xxxx", + "name": "pi4ioe5v9xxxx IO Expander", + "documentation": "https://www.home-assistant.io/integrations/pi4ioe5v9xxxx", + "requirements": ["pi4ioe5v9xxxx==0.0.2"], + "codeowners": ["@antonverburg"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/pi_hole/manifest.json b/homeassistant/components/pi_hole/manifest.json index efe90bbf7e8b7..a96cae8b22b20 100644 --- a/homeassistant/components/pi_hole/manifest.json +++ b/homeassistant/components/pi_hole/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/pi_hole", "requirements": ["hole==0.5.1"], "codeowners": ["@fabaff", "@johnluetke", "@shenxn"], - "config_flow": true + "config_flow": true, + "iot_class": "local_polling" } diff --git a/homeassistant/components/picotts/manifest.json b/homeassistant/components/picotts/manifest.json index 6f7a80be970db..cba95eb75b6aa 100644 --- a/homeassistant/components/picotts/manifest.json +++ b/homeassistant/components/picotts/manifest.json @@ -2,5 +2,6 @@ "domain": "picotts", "name": "Pico TTS", "documentation": "https://www.home-assistant.io/integrations/picotts", - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/piglow/manifest.json b/homeassistant/components/piglow/manifest.json index 14d25b1dc92a7..f4b869aacf861 100644 --- a/homeassistant/components/piglow/manifest.json +++ b/homeassistant/components/piglow/manifest.json @@ -3,5 +3,6 @@ "name": "Piglow", "documentation": "https://www.home-assistant.io/integrations/piglow", "requirements": ["piglow==1.2.4"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/pilight/manifest.json b/homeassistant/components/pilight/manifest.json index 8afafcd68b31b..e7173df21d9f2 100644 --- a/homeassistant/components/pilight/manifest.json +++ b/homeassistant/components/pilight/manifest.json @@ -3,5 +3,6 @@ "name": "Pilight", "documentation": "https://www.home-assistant.io/integrations/pilight", "requirements": ["pilight==0.1.1"], - "codeowners": ["@trekky12"] + "codeowners": ["@trekky12"], + "iot_class": "local_push" } diff --git a/homeassistant/components/ping/manifest.json b/homeassistant/components/ping/manifest.json index 0995478760866..639a30a4fa02c 100644 --- a/homeassistant/components/ping/manifest.json +++ b/homeassistant/components/ping/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/ping", "codeowners": [], "requirements": ["icmplib==2.1.1"], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_polling" } diff --git a/homeassistant/components/pioneer/manifest.json b/homeassistant/components/pioneer/manifest.json index 524f276441475..d19ecfb1f3615 100644 --- a/homeassistant/components/pioneer/manifest.json +++ b/homeassistant/components/pioneer/manifest.json @@ -2,5 +2,6 @@ "domain": "pioneer", "name": "Pioneer", "documentation": "https://www.home-assistant.io/integrations/pioneer", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/pjlink/manifest.json b/homeassistant/components/pjlink/manifest.json index 6b2dd94c0bd7b..ea07cc5d85a0b 100644 --- a/homeassistant/components/pjlink/manifest.json +++ b/homeassistant/components/pjlink/manifest.json @@ -3,5 +3,6 @@ "name": "PJLink", "documentation": "https://www.home-assistant.io/integrations/pjlink", "requirements": ["pypjlink2==1.2.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/plaato/manifest.json b/homeassistant/components/plaato/manifest.json index e3291e5a229d2..99453f21d459a 100644 --- a/homeassistant/components/plaato/manifest.json +++ b/homeassistant/components/plaato/manifest.json @@ -6,5 +6,6 @@ "dependencies": ["webhook"], "after_dependencies": ["cloud"], "codeowners": ["@JohNan"], - "requirements": ["pyplaato==0.0.15"] + "requirements": ["pyplaato==0.0.15"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index e0e62d7150bf0..5d6ffd19550e1 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -4,10 +4,11 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/plex", "requirements": [ - "plexapi==4.5.1", - "plexauth==0.0.6", - "plexwebsocket==0.0.13" + "plexapi==4.5.1", + "plexauth==0.0.6", + "plexwebsocket==0.0.13" ], "dependencies": ["http"], - "codeowners": ["@jjlawren"] + "codeowners": ["@jjlawren"], + "iot_class": "local_push" } diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index 998b84fe5d449..f81c240284621 100644 --- a/homeassistant/components/plugwise/manifest.json +++ b/homeassistant/components/plugwise/manifest.json @@ -5,5 +5,6 @@ "requirements": ["plugwise==0.8.5"], "codeowners": ["@CoMPaTech", "@bouwew", "@brefra"], "zeroconf": ["_plugwise._tcp.local."], - "config_flow": true + "config_flow": true, + "iot_class": "local_polling" } diff --git a/homeassistant/components/plum_lightpad/manifest.json b/homeassistant/components/plum_lightpad/manifest.json index ed9bb9c2eb447..366f770ca3b9e 100644 --- a/homeassistant/components/plum_lightpad/manifest.json +++ b/homeassistant/components/plum_lightpad/manifest.json @@ -2,12 +2,8 @@ "domain": "plum_lightpad", "name": "Plum Lightpad", "documentation": "https://www.home-assistant.io/integrations/plum_lightpad", - "requirements": [ - "plumlightpad==0.0.11" - ], - "codeowners": [ - "@ColinHarrington", - "@prystupa" - ], - "config_flow": true + "requirements": ["plumlightpad==0.0.11"], + "codeowners": ["@ColinHarrington", "@prystupa"], + "config_flow": true, + "iot_class": "local_push" } diff --git a/homeassistant/components/pocketcasts/manifest.json b/homeassistant/components/pocketcasts/manifest.json index ad95609bd9f56..a2070daedd7c1 100644 --- a/homeassistant/components/pocketcasts/manifest.json +++ b/homeassistant/components/pocketcasts/manifest.json @@ -3,5 +3,6 @@ "name": "Pocket Casts", "documentation": "https://www.home-assistant.io/integrations/pocketcasts", "requirements": ["pycketcasts==1.0.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/point/manifest.json b/homeassistant/components/point/manifest.json index 899e5615b4061..fffb1b07f25e0 100644 --- a/homeassistant/components/point/manifest.json +++ b/homeassistant/components/point/manifest.json @@ -6,5 +6,6 @@ "requirements": ["pypoint==2.1.0"], "dependencies": ["webhook", "http"], "codeowners": ["@fredrike"], - "quality_scale": "gold" + "quality_scale": "gold", + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/poolsense/manifest.json b/homeassistant/components/poolsense/manifest.json index 9eebadf2da066..697afd541063a 100644 --- a/homeassistant/components/poolsense/manifest.json +++ b/homeassistant/components/poolsense/manifest.json @@ -3,10 +3,7 @@ "name": "PoolSense", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/poolsense", - "requirements": [ - "poolsense==0.0.8" - ], - "codeowners": [ - "@haemishkyd" - ] + "requirements": ["poolsense==0.0.8"], + "codeowners": ["@haemishkyd"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/powerwall/manifest.json b/homeassistant/components/powerwall/manifest.json index 40d0a6c50fe11..d9f821df90555 100644 --- a/homeassistant/components/powerwall/manifest.json +++ b/homeassistant/components/powerwall/manifest.json @@ -6,7 +6,14 @@ "requirements": ["tesla-powerwall==0.3.5"], "codeowners": ["@bdraco", "@jrester"], "dhcp": [ - {"hostname":"1118431-*","macaddress":"88DA1A*"}, - {"hostname":"1118431-*","macaddress":"000145*"} - ] + { + "hostname": "1118431-*", + "macaddress": "88DA1A*" + }, + { + "hostname": "1118431-*", + "macaddress": "000145*" + } + ], + "iot_class": "local_polling" } diff --git a/homeassistant/components/progettihwsw/manifest.json b/homeassistant/components/progettihwsw/manifest.json index 15987837fb5b0..d1dbb30f2fcb2 100644 --- a/homeassistant/components/progettihwsw/manifest.json +++ b/homeassistant/components/progettihwsw/manifest.json @@ -2,11 +2,8 @@ "domain": "progettihwsw", "name": "ProgettiHWSW Automation", "documentation": "https://www.home-assistant.io/integrations/progettihwsw", - "codeowners": [ - "@ardaseremet" - ], - "requirements": [ - "progettihwsw==0.1.1" - ], - "config_flow": true -} \ No newline at end of file + "codeowners": ["@ardaseremet"], + "requirements": ["progettihwsw==0.1.1"], + "config_flow": true, + "iot_class": "local_polling" +} diff --git a/homeassistant/components/proliphix/manifest.json b/homeassistant/components/proliphix/manifest.json index eb0b6e1b85764..e5f2fc056dc3a 100644 --- a/homeassistant/components/proliphix/manifest.json +++ b/homeassistant/components/proliphix/manifest.json @@ -3,5 +3,6 @@ "name": "Proliphix", "documentation": "https://www.home-assistant.io/integrations/proliphix", "requirements": ["proliphix==0.4.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/prometheus/manifest.json b/homeassistant/components/prometheus/manifest.json index 9b4df619fb5af..9315bf308b7a8 100644 --- a/homeassistant/components/prometheus/manifest.json +++ b/homeassistant/components/prometheus/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/prometheus", "requirements": ["prometheus_client==0.7.1"], "dependencies": ["http"], - "codeowners": ["@knyar"] + "codeowners": ["@knyar"], + "iot_class": "assumed_state" } diff --git a/homeassistant/components/prowl/manifest.json b/homeassistant/components/prowl/manifest.json index 10bb7f8948e10..223d6f28865a7 100644 --- a/homeassistant/components/prowl/manifest.json +++ b/homeassistant/components/prowl/manifest.json @@ -2,5 +2,6 @@ "domain": "prowl", "name": "Prowl", "documentation": "https://www.home-assistant.io/integrations/prowl", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/proximity/manifest.json b/homeassistant/components/proximity/manifest.json index a93da5f72d01c..edc1f15254143 100644 --- a/homeassistant/components/proximity/manifest.json +++ b/homeassistant/components/proximity/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/proximity", "dependencies": ["device_tracker", "zone"], "codeowners": [], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "calculated" } diff --git a/homeassistant/components/proxmoxve/manifest.json b/homeassistant/components/proxmoxve/manifest.json index 0f0029dff3239..bfea03e890210 100644 --- a/homeassistant/components/proxmoxve/manifest.json +++ b/homeassistant/components/proxmoxve/manifest.json @@ -3,5 +3,6 @@ "name": "Proxmox VE", "documentation": "https://www.home-assistant.io/integrations/proxmoxve", "codeowners": ["@k4ds3", "@jhollowe", "@Corbeno"], - "requirements": ["proxmoxer==1.1.1"] + "requirements": ["proxmoxer==1.1.1"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/ps4/manifest.json b/homeassistant/components/ps4/manifest.json index 500c243b8c9e5..609b749774411 100644 --- a/homeassistant/components/ps4/manifest.json +++ b/homeassistant/components/ps4/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ps4", "requirements": ["pyps4-2ndscreen==1.2.0"], - "codeowners": ["@ktnrg45"] + "codeowners": ["@ktnrg45"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/pulseaudio_loopback/manifest.json b/homeassistant/components/pulseaudio_loopback/manifest.json index bc38d8c2594d6..4d7bfbf1e2984 100644 --- a/homeassistant/components/pulseaudio_loopback/manifest.json +++ b/homeassistant/components/pulseaudio_loopback/manifest.json @@ -3,5 +3,6 @@ "name": "PulseAudio Loopback", "documentation": "https://www.home-assistant.io/integrations/pulseaudio_loopback", "requirements": ["pulsectl==20.2.4"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/push/manifest.json b/homeassistant/components/push/manifest.json index c4a419bcfd3af..bafae78c23b5c 100644 --- a/homeassistant/components/push/manifest.json +++ b/homeassistant/components/push/manifest.json @@ -3,5 +3,6 @@ "name": "Push", "documentation": "https://www.home-assistant.io/integrations/push", "dependencies": ["webhook"], - "codeowners": ["@dgomes"] + "codeowners": ["@dgomes"], + "iot_class": "local_push" } diff --git a/homeassistant/components/pushbullet/manifest.json b/homeassistant/components/pushbullet/manifest.json index 1453f9ffe7355..34356e74a5648 100644 --- a/homeassistant/components/pushbullet/manifest.json +++ b/homeassistant/components/pushbullet/manifest.json @@ -3,5 +3,6 @@ "name": "Pushbullet", "documentation": "https://www.home-assistant.io/integrations/pushbullet", "requirements": ["pushbullet.py==0.11.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/pushover/manifest.json b/homeassistant/components/pushover/manifest.json index 222e7a22fdf41..56bfac0185919 100644 --- a/homeassistant/components/pushover/manifest.json +++ b/homeassistant/components/pushover/manifest.json @@ -3,5 +3,6 @@ "name": "Pushover", "documentation": "https://www.home-assistant.io/integrations/pushover", "requirements": ["pushover_complete==1.1.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/pushsafer/manifest.json b/homeassistant/components/pushsafer/manifest.json index 8932de99b5d60..a38f6f45f04d5 100644 --- a/homeassistant/components/pushsafer/manifest.json +++ b/homeassistant/components/pushsafer/manifest.json @@ -2,5 +2,6 @@ "domain": "pushsafer", "name": "Pushsafer", "documentation": "https://www.home-assistant.io/integrations/pushsafer", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/pvoutput/manifest.json b/homeassistant/components/pvoutput/manifest.json index 93f9b45c62ada..af40cf7eca457 100644 --- a/homeassistant/components/pvoutput/manifest.json +++ b/homeassistant/components/pvoutput/manifest.json @@ -3,5 +3,6 @@ "name": "PVOutput", "documentation": "https://www.home-assistant.io/integrations/pvoutput", "after_dependencies": ["rest"], - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/pvpc_hourly_pricing/manifest.json b/homeassistant/components/pvpc_hourly_pricing/manifest.json index 3f2dd00d832f4..578dfc73619f9 100644 --- a/homeassistant/components/pvpc_hourly_pricing/manifest.json +++ b/homeassistant/components/pvpc_hourly_pricing/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/pvpc_hourly_pricing", "requirements": ["aiopvpc==2.0.2"], "codeowners": ["@azogue"], - "quality_scale": "platinum" + "quality_scale": "platinum", + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/pyload/manifest.json b/homeassistant/components/pyload/manifest.json index 8a446a032f84b..15cf837c90e6f 100644 --- a/homeassistant/components/pyload/manifest.json +++ b/homeassistant/components/pyload/manifest.json @@ -2,5 +2,6 @@ "domain": "pyload", "name": "pyLoad", "documentation": "https://www.home-assistant.io/integrations/pyload", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/qbittorrent/manifest.json b/homeassistant/components/qbittorrent/manifest.json index 2f3e8cf4f1a00..241b9a5cff9cf 100644 --- a/homeassistant/components/qbittorrent/manifest.json +++ b/homeassistant/components/qbittorrent/manifest.json @@ -3,5 +3,6 @@ "name": "qBittorrent", "documentation": "https://www.home-assistant.io/integrations/qbittorrent", "requirements": ["python-qbittorrent==0.4.2"], - "codeowners": ["@geoffreylagaisse"] + "codeowners": ["@geoffreylagaisse"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/qld_bushfire/manifest.json b/homeassistant/components/qld_bushfire/manifest.json index db98e2f7338ce..aeddc8cbeb086 100644 --- a/homeassistant/components/qld_bushfire/manifest.json +++ b/homeassistant/components/qld_bushfire/manifest.json @@ -3,5 +3,6 @@ "name": "Queensland Bushfire Alert", "documentation": "https://www.home-assistant.io/integrations/qld_bushfire", "requirements": ["georss_qld_bushfire_alert_client==0.3"], - "codeowners": ["@exxamalte"] + "codeowners": ["@exxamalte"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/qnap/manifest.json b/homeassistant/components/qnap/manifest.json index 29750683abf05..abd5d6f5a4a6d 100644 --- a/homeassistant/components/qnap/manifest.json +++ b/homeassistant/components/qnap/manifest.json @@ -3,5 +3,6 @@ "name": "QNAP", "documentation": "https://www.home-assistant.io/integrations/qnap", "requirements": ["qnapstats==0.3.1"], - "codeowners": ["@colinodell"] + "codeowners": ["@colinodell"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/qrcode/manifest.json b/homeassistant/components/qrcode/manifest.json index bd574af0297d4..18bf2d7db6dfc 100644 --- a/homeassistant/components/qrcode/manifest.json +++ b/homeassistant/components/qrcode/manifest.json @@ -3,5 +3,6 @@ "name": "QR Code", "documentation": "https://www.home-assistant.io/integrations/qrcode", "requirements": ["pillow==8.1.2", "pyzbar==0.1.7"], - "codeowners": [] + "codeowners": [], + "iot_class": "calculated" } diff --git a/homeassistant/components/quantum_gateway/manifest.json b/homeassistant/components/quantum_gateway/manifest.json index 1c4a7a13923eb..b734be8508e9c 100644 --- a/homeassistant/components/quantum_gateway/manifest.json +++ b/homeassistant/components/quantum_gateway/manifest.json @@ -3,5 +3,6 @@ "name": "Quantum Gateway", "documentation": "https://www.home-assistant.io/integrations/quantum_gateway", "requirements": ["quantum-gateway==0.0.5"], - "codeowners": ["@cisasteelersfan"] + "codeowners": ["@cisasteelersfan"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/qvr_pro/manifest.json b/homeassistant/components/qvr_pro/manifest.json index d6365afd213a1..eb08be180c6c4 100644 --- a/homeassistant/components/qvr_pro/manifest.json +++ b/homeassistant/components/qvr_pro/manifest.json @@ -3,5 +3,6 @@ "name": "QVR Pro", "documentation": "https://www.home-assistant.io/integrations/qvr_pro", "requirements": ["pyqvrpro==0.52"], - "codeowners": ["@oblogic7"] + "codeowners": ["@oblogic7"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/qwikswitch/manifest.json b/homeassistant/components/qwikswitch/manifest.json index 31e84fccf9a24..851e93dc67d95 100644 --- a/homeassistant/components/qwikswitch/manifest.json +++ b/homeassistant/components/qwikswitch/manifest.json @@ -3,5 +3,6 @@ "name": "QwikSwitch QSUSB", "documentation": "https://www.home-assistant.io/integrations/qwikswitch", "requirements": ["pyqwikswitch==0.93"], - "codeowners": ["@kellerza"] + "codeowners": ["@kellerza"], + "iot_class": "local_push" } diff --git a/homeassistant/components/rachio/manifest.json b/homeassistant/components/rachio/manifest.json index ba81b65b37f36..67cdf2496ee03 100644 --- a/homeassistant/components/rachio/manifest.json +++ b/homeassistant/components/rachio/manifest.json @@ -7,19 +7,22 @@ "after_dependencies": ["cloud"], "codeowners": ["@bdraco"], "config_flow": true, - "dhcp": [{ - "hostname": "rachio-*", - "macaddress": "009D6B*" - }, - { - "hostname": "rachio-*", - "macaddress": "F0038C*" - }, - { - "hostname": "rachio-*", - "macaddress": "74C63B*" - }], + "dhcp": [ + { + "hostname": "rachio-*", + "macaddress": "009D6B*" + }, + { + "hostname": "rachio-*", + "macaddress": "F0038C*" + }, + { + "hostname": "rachio-*", + "macaddress": "74C63B*" + } + ], "homekit": { "models": ["Rachio"] - } + }, + "iot_class": "cloud_push" } diff --git a/homeassistant/components/radarr/manifest.json b/homeassistant/components/radarr/manifest.json index 8f752f0350077..611b4a33f3be8 100644 --- a/homeassistant/components/radarr/manifest.json +++ b/homeassistant/components/radarr/manifest.json @@ -2,5 +2,6 @@ "domain": "radarr", "name": "Radarr", "documentation": "https://www.home-assistant.io/integrations/radarr", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/radiotherm/manifest.json b/homeassistant/components/radiotherm/manifest.json index 0220c2338419d..b051ba65b3b0c 100644 --- a/homeassistant/components/radiotherm/manifest.json +++ b/homeassistant/components/radiotherm/manifest.json @@ -3,5 +3,6 @@ "name": "Radio Thermostat", "documentation": "https://www.home-assistant.io/integrations/radiotherm", "requirements": ["radiotherm==2.1.0"], - "codeowners": ["@vinnyfuria"] + "codeowners": ["@vinnyfuria"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/rainbird/manifest.json b/homeassistant/components/rainbird/manifest.json index 89ca65fd44bd5..120e38e8058e7 100644 --- a/homeassistant/components/rainbird/manifest.json +++ b/homeassistant/components/rainbird/manifest.json @@ -3,5 +3,6 @@ "name": "Rain Bird", "documentation": "https://www.home-assistant.io/integrations/rainbird", "requirements": ["pyrainbird==0.4.2"], - "codeowners": ["@konikvranik"] + "codeowners": ["@konikvranik"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/raincloud/manifest.json b/homeassistant/components/raincloud/manifest.json index a0edaa87825f4..309dc6bdb5199 100644 --- a/homeassistant/components/raincloud/manifest.json +++ b/homeassistant/components/raincloud/manifest.json @@ -3,5 +3,6 @@ "name": "Melnor RainCloud", "documentation": "https://www.home-assistant.io/integrations/raincloud", "requirements": ["raincloudy==0.0.7"], - "codeowners": ["@vanstinator"] + "codeowners": ["@vanstinator"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/rainforest_eagle/manifest.json b/homeassistant/components/rainforest_eagle/manifest.json index 4fbce5d04cec4..fd28e5b09944d 100644 --- a/homeassistant/components/rainforest_eagle/manifest.json +++ b/homeassistant/components/rainforest_eagle/manifest.json @@ -3,5 +3,6 @@ "name": "Rainforest Eagle-200", "documentation": "https://www.home-assistant.io/integrations/rainforest_eagle", "requirements": ["eagle200_reader==0.2.4", "uEagle==0.0.2"], - "codeowners": ["@gtdiehl", "@jcalbert"] + "codeowners": ["@gtdiehl", "@jcalbert"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/rainmachine/manifest.json b/homeassistant/components/rainmachine/manifest.json index 5d03155deac62..17429a74d405f 100644 --- a/homeassistant/components/rainmachine/manifest.json +++ b/homeassistant/components/rainmachine/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rainmachine", "requirements": ["regenmaschine==3.0.0"], - "codeowners": ["@bachya"] + "codeowners": ["@bachya"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/random/manifest.json b/homeassistant/components/random/manifest.json index 5e73fbd442105..ae135c9de40a6 100644 --- a/homeassistant/components/random/manifest.json +++ b/homeassistant/components/random/manifest.json @@ -3,5 +3,6 @@ "name": "Random", "documentation": "https://www.home-assistant.io/integrations/random", "codeowners": ["@fabaff"], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_polling" } diff --git a/homeassistant/components/raspihats/manifest.json b/homeassistant/components/raspihats/manifest.json index 400cd275dc12f..984f440e0646d 100644 --- a/homeassistant/components/raspihats/manifest.json +++ b/homeassistant/components/raspihats/manifest.json @@ -3,5 +3,6 @@ "name": "Raspihats", "documentation": "https://www.home-assistant.io/integrations/raspihats", "requirements": ["raspihats==2.2.3", "smbus-cffi==0.5.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/raspyrfm/manifest.json b/homeassistant/components/raspyrfm/manifest.json index ed840c708244d..6fd4b13dee07b 100644 --- a/homeassistant/components/raspyrfm/manifest.json +++ b/homeassistant/components/raspyrfm/manifest.json @@ -3,5 +3,6 @@ "name": "RaspyRFM", "documentation": "https://www.home-assistant.io/integrations/raspyrfm", "requirements": ["raspyrfm-client==1.2.8"], - "codeowners": [] + "codeowners": [], + "iot_class": "assumed_state" } diff --git a/homeassistant/components/recollect_waste/manifest.json b/homeassistant/components/recollect_waste/manifest.json index 4e7568a3fff30..e33edcc2ab5fc 100644 --- a/homeassistant/components/recollect_waste/manifest.json +++ b/homeassistant/components/recollect_waste/manifest.json @@ -3,10 +3,7 @@ "name": "ReCollect Waste", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/recollect_waste", - "requirements": [ - "aiorecollect==1.0.4" - ], - "codeowners": [ - "@bachya" - ] + "requirements": ["aiorecollect==1.0.4"], + "codeowners": ["@bachya"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json index a7e5eb0814d79..e943e61d5c0b4 100644 --- a/homeassistant/components/recorder/manifest.json +++ b/homeassistant/components/recorder/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/recorder", "requirements": ["sqlalchemy==1.3.23"], "codeowners": [], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_push" } diff --git a/homeassistant/components/recswitch/manifest.json b/homeassistant/components/recswitch/manifest.json index 4d155b6ec02d3..c8a724471883e 100644 --- a/homeassistant/components/recswitch/manifest.json +++ b/homeassistant/components/recswitch/manifest.json @@ -3,5 +3,6 @@ "name": "Ankuoo REC Switch", "documentation": "https://www.home-assistant.io/integrations/recswitch", "requirements": ["pyrecswitch==1.0.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/reddit/manifest.json b/homeassistant/components/reddit/manifest.json index 252052ac5c221..a9ffe490019ad 100644 --- a/homeassistant/components/reddit/manifest.json +++ b/homeassistant/components/reddit/manifest.json @@ -3,5 +3,6 @@ "name": "Reddit", "documentation": "https://www.home-assistant.io/integrations/reddit", "requirements": ["praw==7.1.4"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/rejseplanen/manifest.json b/homeassistant/components/rejseplanen/manifest.json index 6f91e2a9abe18..58594f1757745 100644 --- a/homeassistant/components/rejseplanen/manifest.json +++ b/homeassistant/components/rejseplanen/manifest.json @@ -3,5 +3,6 @@ "name": "Rejseplanen", "documentation": "https://www.home-assistant.io/integrations/rejseplanen", "requirements": ["rjpl==0.3.6"], - "codeowners": ["@DarkFox"] + "codeowners": ["@DarkFox"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/remember_the_milk/manifest.json b/homeassistant/components/remember_the_milk/manifest.json index 8ce8cb98e5bfa..c19cc701afce5 100644 --- a/homeassistant/components/remember_the_milk/manifest.json +++ b/homeassistant/components/remember_the_milk/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/remember_the_milk", "requirements": ["RtmAPI==0.7.2", "httplib2==0.19.0"], "dependencies": ["configurator"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/remote_rpi_gpio/manifest.json b/homeassistant/components/remote_rpi_gpio/manifest.json index c69a9c92fde5c..b2ed060bffaba 100644 --- a/homeassistant/components/remote_rpi_gpio/manifest.json +++ b/homeassistant/components/remote_rpi_gpio/manifest.json @@ -3,5 +3,6 @@ "name": "remote_rpi_gpio", "documentation": "https://www.home-assistant.io/integrations/remote_rpi_gpio", "requirements": ["gpiozero==1.5.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/repetier/manifest.json b/homeassistant/components/repetier/manifest.json index b6d48aded2f8f..0fd3d9049875f 100644 --- a/homeassistant/components/repetier/manifest.json +++ b/homeassistant/components/repetier/manifest.json @@ -3,5 +3,6 @@ "name": "Repetier-Server", "documentation": "https://www.home-assistant.io/integrations/repetier", "requirements": ["pyrepetier==3.0.5"], - "codeowners": ["@MTrab"] + "codeowners": ["@MTrab"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/rest/manifest.json b/homeassistant/components/rest/manifest.json index 3ab926a3b1329..c81656d82b401 100644 --- a/homeassistant/components/rest/manifest.json +++ b/homeassistant/components/rest/manifest.json @@ -3,5 +3,6 @@ "name": "RESTful", "documentation": "https://www.home-assistant.io/integrations/rest", "requirements": ["jsonpath==0.82", "xmltodict==0.12.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/rest_command/manifest.json b/homeassistant/components/rest_command/manifest.json index a4441a7afa000..ced35e88293f4 100644 --- a/homeassistant/components/rest_command/manifest.json +++ b/homeassistant/components/rest_command/manifest.json @@ -2,5 +2,6 @@ "domain": "rest_command", "name": "RESTful Command", "documentation": "https://www.home-assistant.io/integrations/rest_command", - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/rflink/manifest.json b/homeassistant/components/rflink/manifest.json index ebd1fb5afdca5..93afa8f5df43c 100644 --- a/homeassistant/components/rflink/manifest.json +++ b/homeassistant/components/rflink/manifest.json @@ -3,7 +3,6 @@ "name": "RFLink", "documentation": "https://www.home-assistant.io/integrations/rflink", "requirements": ["rflink==0.0.58"], - "codeowners": [ - "@javicalle" - ] + "codeowners": ["@javicalle"], + "iot_class": "assumed_state" } diff --git a/homeassistant/components/rfxtrx/manifest.json b/homeassistant/components/rfxtrx/manifest.json index 19e834d11d614..34c31c72a0df6 100644 --- a/homeassistant/components/rfxtrx/manifest.json +++ b/homeassistant/components/rfxtrx/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/rfxtrx", "requirements": ["pyRFXtrx==0.26.1"], "codeowners": ["@danielhiversen", "@elupus", "@RobBie1221"], - "config_flow": true + "config_flow": true, + "iot_class": "local_push" } diff --git a/homeassistant/components/ring/manifest.json b/homeassistant/components/ring/manifest.json index 3808383031106..ecb64c99fd764 100644 --- a/homeassistant/components/ring/manifest.json +++ b/homeassistant/components/ring/manifest.json @@ -6,5 +6,11 @@ "dependencies": ["ffmpeg"], "codeowners": ["@balloob"], "config_flow": true, - "dhcp": [{"hostname":"ring*","macaddress":"0CAE7D*"}] + "dhcp": [ + { + "hostname": "ring*", + "macaddress": "0CAE7D*" + } + ], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/ripple/manifest.json b/homeassistant/components/ripple/manifest.json index d730093ed0f40..68adda3edeae1 100644 --- a/homeassistant/components/ripple/manifest.json +++ b/homeassistant/components/ripple/manifest.json @@ -3,5 +3,6 @@ "name": "Ripple", "documentation": "https://www.home-assistant.io/integrations/ripple", "requirements": ["python-ripple-api==0.0.3"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/risco/manifest.json b/homeassistant/components/risco/manifest.json index 7f13af252f3d4..2da0a5254a485 100644 --- a/homeassistant/components/risco/manifest.json +++ b/homeassistant/components/risco/manifest.json @@ -3,11 +3,8 @@ "name": "Risco", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/risco", - "requirements": [ - "pyrisco==0.3.1" - ], - "codeowners": [ - "@OnFreund" - ], - "quality_scale": "platinum" -} \ No newline at end of file + "requirements": ["pyrisco==0.3.1"], + "codeowners": ["@OnFreund"], + "quality_scale": "platinum", + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/rituals_perfume_genie/manifest.json b/homeassistant/components/rituals_perfume_genie/manifest.json index 8be7e98b93989..8ec7b0c8df3e0 100644 --- a/homeassistant/components/rituals_perfume_genie/manifest.json +++ b/homeassistant/components/rituals_perfume_genie/manifest.json @@ -3,10 +3,7 @@ "name": "Rituals Perfume Genie", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rituals_perfume_genie", - "requirements": [ - "pyrituals==0.0.2" - ], - "codeowners": [ - "@milanmeu" - ] + "requirements": ["pyrituals==0.0.2"], + "codeowners": ["@milanmeu"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/rmvtransport/manifest.json b/homeassistant/components/rmvtransport/manifest.json index 68f895cb2b885..a2e91b9a01c2c 100644 --- a/homeassistant/components/rmvtransport/manifest.json +++ b/homeassistant/components/rmvtransport/manifest.json @@ -2,10 +2,7 @@ "domain": "rmvtransport", "name": "RMV", "documentation": "https://www.home-assistant.io/integrations/rmvtransport", - "requirements": [ - "PyRMVtransport==0.3.1" - ], - "codeowners": [ - "@cgtobi" - ] -} \ No newline at end of file + "requirements": ["PyRMVtransport==0.3.1"], + "codeowners": ["@cgtobi"], + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/rocketchat/manifest.json b/homeassistant/components/rocketchat/manifest.json index 23798ff5df135..13e6a7bb745a1 100644 --- a/homeassistant/components/rocketchat/manifest.json +++ b/homeassistant/components/rocketchat/manifest.json @@ -3,5 +3,6 @@ "name": "Rocket.Chat", "documentation": "https://www.home-assistant.io/integrations/rocketchat", "requirements": ["rocketchat-API==0.6.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/roku/manifest.json b/homeassistant/components/roku/manifest.json index 981a9b080777d..81e3af86bb5bd 100644 --- a/homeassistant/components/roku/manifest.json +++ b/homeassistant/components/roku/manifest.json @@ -4,13 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/roku", "requirements": ["rokuecp==0.8.1"], "homekit": { - "models": [ - "3810X", - "4660X", - "7820X", - "C105X", - "C135X" - ] + "models": ["3810X", "4660X", "7820X", "C105X", "C135X"] }, "ssdp": [ { @@ -21,5 +15,6 @@ ], "codeowners": ["@ctalkington"], "quality_scale": "silver", - "config_flow": true + "config_flow": true, + "iot_class": "local_polling" } diff --git a/homeassistant/components/roomba/manifest.json b/homeassistant/components/roomba/manifest.json index d1858a46fdc9d..ce17cf8c2c2cb 100644 --- a/homeassistant/components/roomba/manifest.json +++ b/homeassistant/components/roomba/manifest.json @@ -6,14 +6,14 @@ "requirements": ["roombapy==1.6.2"], "codeowners": ["@pschmitt", "@cyr-ius", "@shenxn"], "dhcp": [ - { - "hostname" : "irobot-*", - "macaddress" : "501479*" - }, - { - "hostname" : "roomba-*", - "macaddress" : "80A589*" - } - ] + { + "hostname": "irobot-*", + "macaddress": "501479*" + }, + { + "hostname": "roomba-*", + "macaddress": "80A589*" + } + ], + "iot_class": "local_push" } - diff --git a/homeassistant/components/roon/manifest.json b/homeassistant/components/roon/manifest.json index e4c4a25dcb540..875294310d9c0 100644 --- a/homeassistant/components/roon/manifest.json +++ b/homeassistant/components/roon/manifest.json @@ -3,10 +3,7 @@ "name": "RoonLabs music player", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/roon", - "requirements": [ - "roonapi==0.0.32" - ], - "codeowners": [ - "@pavoni" - ] + "requirements": ["roonapi==0.0.32"], + "codeowners": ["@pavoni"], + "iot_class": "local_push" } diff --git a/homeassistant/components/route53/manifest.json b/homeassistant/components/route53/manifest.json index 61fb7d34ced05..1611fdad6fc3b 100644 --- a/homeassistant/components/route53/manifest.json +++ b/homeassistant/components/route53/manifest.json @@ -3,5 +3,6 @@ "name": "AWS Route53", "documentation": "https://www.home-assistant.io/integrations/route53", "requirements": ["boto3==1.16.52"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/rova/manifest.json b/homeassistant/components/rova/manifest.json index b3635b39f38c3..27421b2093695 100644 --- a/homeassistant/components/rova/manifest.json +++ b/homeassistant/components/rova/manifest.json @@ -3,5 +3,6 @@ "name": "ROVA", "documentation": "https://www.home-assistant.io/integrations/rova", "requirements": ["rova==0.2.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/rpi_camera/manifest.json b/homeassistant/components/rpi_camera/manifest.json index 5f42be58ffe71..cc4cbbace88cb 100644 --- a/homeassistant/components/rpi_camera/manifest.json +++ b/homeassistant/components/rpi_camera/manifest.json @@ -2,5 +2,6 @@ "domain": "rpi_camera", "name": "Raspberry Pi Camera", "documentation": "https://www.home-assistant.io/integrations/rpi_camera", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/rpi_gpio/manifest.json b/homeassistant/components/rpi_gpio/manifest.json index 1a73c736d04c3..d09c21779fe80 100644 --- a/homeassistant/components/rpi_gpio/manifest.json +++ b/homeassistant/components/rpi_gpio/manifest.json @@ -3,5 +3,6 @@ "name": "Raspberry Pi GPIO", "documentation": "https://www.home-assistant.io/integrations/rpi_gpio", "requirements": ["RPi.GPIO==0.7.1a4"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/rpi_gpio_pwm/manifest.json b/homeassistant/components/rpi_gpio_pwm/manifest.json index 35d09ea92bf02..ea0bdbcb0f357 100644 --- a/homeassistant/components/rpi_gpio_pwm/manifest.json +++ b/homeassistant/components/rpi_gpio_pwm/manifest.json @@ -3,5 +3,6 @@ "name": "pigpio Daemon PWM LED", "documentation": "https://www.home-assistant.io/integrations/rpi_gpio_pwm", "requirements": ["pwmled==1.6.7"], - "codeowners": ["@soldag"] + "codeowners": ["@soldag"], + "iot_class": "local_push" } diff --git a/homeassistant/components/rpi_pfio/manifest.json b/homeassistant/components/rpi_pfio/manifest.json index f40c34a11a4f1..9e8f0a30e87f7 100644 --- a/homeassistant/components/rpi_pfio/manifest.json +++ b/homeassistant/components/rpi_pfio/manifest.json @@ -3,5 +3,6 @@ "name": "PiFace Digital I/O (PFIO)", "documentation": "https://www.home-assistant.io/integrations/rpi_pfio", "requirements": ["pifacecommon==4.2.2", "pifacedigitalio==3.0.5"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/rpi_power/manifest.json b/homeassistant/components/rpi_power/manifest.json index 1b355711535fd..34e249ccfc33b 100644 --- a/homeassistant/components/rpi_power/manifest.json +++ b/homeassistant/components/rpi_power/manifest.json @@ -2,12 +2,8 @@ "domain": "rpi_power", "name": "Raspberry Pi Power Supply Checker", "documentation": "https://www.home-assistant.io/integrations/rpi_power", - "codeowners": [ - "@shenxn", - "@swetoast" - ], - "requirements": [ - "rpi-bad-power==0.1.0" - ], - "config_flow": true + "codeowners": ["@shenxn", "@swetoast"], + "requirements": ["rpi-bad-power==0.1.0"], + "config_flow": true, + "iot_class": "local_polling" } diff --git a/homeassistant/components/rpi_rf/manifest.json b/homeassistant/components/rpi_rf/manifest.json index 0a2cc42b63351..e880671072467 100644 --- a/homeassistant/components/rpi_rf/manifest.json +++ b/homeassistant/components/rpi_rf/manifest.json @@ -3,5 +3,6 @@ "name": "Raspberry Pi RF", "documentation": "https://www.home-assistant.io/integrations/rpi_rf", "requirements": ["rpi-rf==0.9.7"], - "codeowners": [] + "codeowners": [], + "iot_class": "assumed_state" } diff --git a/homeassistant/components/rss_feed_template/manifest.json b/homeassistant/components/rss_feed_template/manifest.json index 1ae8fe58d7b0c..46b449b03dd17 100644 --- a/homeassistant/components/rss_feed_template/manifest.json +++ b/homeassistant/components/rss_feed_template/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/rss_feed_template", "dependencies": ["http"], "codeowners": [], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_push" } diff --git a/homeassistant/components/rtorrent/manifest.json b/homeassistant/components/rtorrent/manifest.json index 137a77b12942d..549c2406b2fb3 100644 --- a/homeassistant/components/rtorrent/manifest.json +++ b/homeassistant/components/rtorrent/manifest.json @@ -2,5 +2,6 @@ "domain": "rtorrent", "name": "rTorrent", "documentation": "https://www.home-assistant.io/integrations/rtorrent", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/ruckus_unleashed/manifest.json b/homeassistant/components/ruckus_unleashed/manifest.json index b8bc14a108adf..b8b2ef6e46a80 100644 --- a/homeassistant/components/ruckus_unleashed/manifest.json +++ b/homeassistant/components/ruckus_unleashed/manifest.json @@ -3,10 +3,7 @@ "name": "Ruckus Unleashed", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ruckus_unleashed", - "requirements": [ - "pyruckus==0.12" - ], - "codeowners": [ - "@gabe565" - ] + "requirements": ["pyruckus==0.12"], + "codeowners": ["@gabe565"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/russound_rio/manifest.json b/homeassistant/components/russound_rio/manifest.json index 2fd9f039d53b4..a12d149550b13 100644 --- a/homeassistant/components/russound_rio/manifest.json +++ b/homeassistant/components/russound_rio/manifest.json @@ -3,5 +3,6 @@ "name": "Russound RIO", "documentation": "https://www.home-assistant.io/integrations/russound_rio", "requirements": ["russound_rio==0.1.7"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/russound_rnet/manifest.json b/homeassistant/components/russound_rnet/manifest.json index 6379dd021f2a2..0e7928fb23b05 100644 --- a/homeassistant/components/russound_rnet/manifest.json +++ b/homeassistant/components/russound_rnet/manifest.json @@ -3,5 +3,6 @@ "name": "Russound RNET", "documentation": "https://www.home-assistant.io/integrations/russound_rnet", "requirements": ["russound==0.1.9"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/sabnzbd/manifest.json b/homeassistant/components/sabnzbd/manifest.json index 6fec5c008b3d0..25dfe6788009c 100644 --- a/homeassistant/components/sabnzbd/manifest.json +++ b/homeassistant/components/sabnzbd/manifest.json @@ -5,5 +5,6 @@ "requirements": ["pysabnzbd==1.1.0"], "dependencies": ["configurator"], "after_dependencies": ["discovery"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/saj/manifest.json b/homeassistant/components/saj/manifest.json index fdd999ac68461..79067e47c731c 100644 --- a/homeassistant/components/saj/manifest.json +++ b/homeassistant/components/saj/manifest.json @@ -3,5 +3,6 @@ "name": "SAJ Solar Inverter", "documentation": "https://www.home-assistant.io/integrations/saj", "requirements": ["pysaj==0.0.16"], - "codeowners": ["@fredericvl"] + "codeowners": ["@fredericvl"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index 08dc4d0c04974..81e08ddeaa671 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -2,17 +2,13 @@ "domain": "samsungtv", "name": "Samsung Smart TV", "documentation": "https://www.home-assistant.io/integrations/samsungtv", - "requirements": [ - "samsungctl[websocket]==0.7.1", - "samsungtvws==1.6.0" - ], + "requirements": ["samsungctl[websocket]==0.7.1", "samsungtvws==1.6.0"], "ssdp": [ { "st": "urn:samsung.com:device:RemoteControlReceiver:1" } ], - "codeowners": [ - "@escoand" - ], - "config_flow": true + "codeowners": ["@escoand"], + "config_flow": true, + "iot_class": "local_polling" } diff --git a/homeassistant/components/satel_integra/manifest.json b/homeassistant/components/satel_integra/manifest.json index 0a157cd4debaf..6aacb3015e1a6 100644 --- a/homeassistant/components/satel_integra/manifest.json +++ b/homeassistant/components/satel_integra/manifest.json @@ -3,5 +3,6 @@ "name": "Satel Integra", "documentation": "https://www.home-assistant.io/integrations/satel_integra", "requirements": ["satel_integra==0.3.4"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/schluter/manifest.json b/homeassistant/components/schluter/manifest.json index 46eb2449e3d67..86f0974b6d12f 100644 --- a/homeassistant/components/schluter/manifest.json +++ b/homeassistant/components/schluter/manifest.json @@ -3,5 +3,6 @@ "name": "Schluter", "documentation": "https://www.home-assistant.io/integrations/schluter", "requirements": ["py-schluter==0.1.7"], - "codeowners": ["@prairieapps"] + "codeowners": ["@prairieapps"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/scrape/manifest.json b/homeassistant/components/scrape/manifest.json index daa5a269dcf6d..c57dd14e37ddb 100644 --- a/homeassistant/components/scrape/manifest.json +++ b/homeassistant/components/scrape/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/scrape", "requirements": ["beautifulsoup4==4.9.3"], "after_dependencies": ["rest"], - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/screenlogic/manifest.json b/homeassistant/components/screenlogic/manifest.json index ab3d08a0702d4..e62c5ba1f8ae7 100644 --- a/homeassistant/components/screenlogic/manifest.json +++ b/homeassistant/components/screenlogic/manifest.json @@ -4,8 +4,12 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/screenlogic", "requirements": ["screenlogicpy==0.2.1"], - "codeowners": [ - "@dieselrabbit" + "codeowners": ["@dieselrabbit"], + "dhcp": [ + { + "hostname": "pentair: *", + "macaddress": "00C033*" + } ], - "dhcp": [{"hostname":"pentair: *","macaddress":"00C033*"}] -} \ No newline at end of file + "iot_class": "local_polling" +} diff --git a/homeassistant/components/script/manifest.json b/homeassistant/components/script/manifest.json index ab14889a60cf1..a7e56d5118a88 100644 --- a/homeassistant/components/script/manifest.json +++ b/homeassistant/components/script/manifest.json @@ -3,8 +3,6 @@ "name": "Scripts", "documentation": "https://www.home-assistant.io/integrations/script", "dependencies": ["trace"], - "codeowners": [ - "@home-assistant/core" - ], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } diff --git a/homeassistant/components/scsgate/manifest.json b/homeassistant/components/scsgate/manifest.json index 88b55bd6b330d..8720dfac8797b 100644 --- a/homeassistant/components/scsgate/manifest.json +++ b/homeassistant/components/scsgate/manifest.json @@ -3,5 +3,6 @@ "name": "SCSGate", "documentation": "https://www.home-assistant.io/integrations/scsgate", "requirements": ["scsgate==0.1.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/season/manifest.json b/homeassistant/components/season/manifest.json index e30c5684d2d93..b48a148034bd6 100644 --- a/homeassistant/components/season/manifest.json +++ b/homeassistant/components/season/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/season", "requirements": ["ephem==3.7.7.0"], "codeowners": [], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_polling" } diff --git a/homeassistant/components/sendgrid/manifest.json b/homeassistant/components/sendgrid/manifest.json index 21ebcd828c2f6..318bd87689faa 100644 --- a/homeassistant/components/sendgrid/manifest.json +++ b/homeassistant/components/sendgrid/manifest.json @@ -3,5 +3,6 @@ "name": "SendGrid", "documentation": "https://www.home-assistant.io/integrations/sendgrid", "requirements": ["sendgrid==6.6.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/sense/manifest.json b/homeassistant/components/sense/manifest.json index 57028ccb395a6..0bde2f7a7a7d5 100644 --- a/homeassistant/components/sense/manifest.json +++ b/homeassistant/components/sense/manifest.json @@ -5,5 +5,15 @@ "requirements": ["sense_energy==0.9.0"], "codeowners": ["@kbickar"], "config_flow": true, - "dhcp": [{"hostname":"sense-*","macaddress":"009D6B*"}, {"hostname":"sense-*","macaddress":"DCEFCA*"}] + "dhcp": [ + { + "hostname": "sense-*", + "macaddress": "009D6B*" + }, + { + "hostname": "sense-*", + "macaddress": "DCEFCA*" + } + ], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/sensehat/manifest.json b/homeassistant/components/sensehat/manifest.json index 3ce37884cd0b5..d8e607ec816c8 100644 --- a/homeassistant/components/sensehat/manifest.json +++ b/homeassistant/components/sensehat/manifest.json @@ -3,5 +3,6 @@ "name": "Sense HAT", "documentation": "https://www.home-assistant.io/integrations/sensehat", "requirements": ["sense-hat==2.2.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "assumed_state" } diff --git a/homeassistant/components/sensibo/manifest.json b/homeassistant/components/sensibo/manifest.json index 9d2e3e9e18787..3cea31c5d5e72 100644 --- a/homeassistant/components/sensibo/manifest.json +++ b/homeassistant/components/sensibo/manifest.json @@ -3,5 +3,6 @@ "name": "Sensibo", "documentation": "https://www.home-assistant.io/integrations/sensibo", "requirements": ["pysensibo==1.0.3"], - "codeowners": ["@andrey-git"] + "codeowners": ["@andrey-git"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/sentry/manifest.json b/homeassistant/components/sentry/manifest.json index 04735d9868749..776a19673c231 100644 --- a/homeassistant/components/sentry/manifest.json +++ b/homeassistant/components/sentry/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sentry", "requirements": ["sentry-sdk==1.0.0"], - "codeowners": ["@dcramer", "@frenck"] + "codeowners": ["@dcramer", "@frenck"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/serial/manifest.json b/homeassistant/components/serial/manifest.json index ce85d07d08634..c87221cce547b 100644 --- a/homeassistant/components/serial/manifest.json +++ b/homeassistant/components/serial/manifest.json @@ -3,5 +3,6 @@ "name": "Serial", "documentation": "https://www.home-assistant.io/integrations/serial", "requirements": ["pyserial-asyncio==0.5"], - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/serial_pm/manifest.json b/homeassistant/components/serial_pm/manifest.json index b40090ca49761..3812a5de072d7 100644 --- a/homeassistant/components/serial_pm/manifest.json +++ b/homeassistant/components/serial_pm/manifest.json @@ -3,5 +3,6 @@ "name": "Serial Particulate Matter", "documentation": "https://www.home-assistant.io/integrations/serial_pm", "requirements": ["pmsensor==0.4"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/sesame/manifest.json b/homeassistant/components/sesame/manifest.json index 0ba0fa8c8ebec..c4a3e3775ae95 100644 --- a/homeassistant/components/sesame/manifest.json +++ b/homeassistant/components/sesame/manifest.json @@ -3,5 +3,6 @@ "name": "Sesame Smart Lock", "documentation": "https://www.home-assistant.io/integrations/sesame", "requirements": ["pysesame2==1.0.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/seven_segments/manifest.json b/homeassistant/components/seven_segments/manifest.json index 13f3cf22506b9..7c4ea22497c1e 100644 --- a/homeassistant/components/seven_segments/manifest.json +++ b/homeassistant/components/seven_segments/manifest.json @@ -3,5 +3,6 @@ "name": "Seven Segments OCR", "documentation": "https://www.home-assistant.io/integrations/seven_segments", "requirements": ["pillow==8.1.2"], - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/seventeentrack/manifest.json b/homeassistant/components/seventeentrack/manifest.json index 427882de91a55..6f0ed4c8a9dd8 100644 --- a/homeassistant/components/seventeentrack/manifest.json +++ b/homeassistant/components/seventeentrack/manifest.json @@ -3,5 +3,6 @@ "name": "17TRACK", "documentation": "https://www.home-assistant.io/integrations/seventeentrack", "requirements": ["py17track==3.2.1"], - "codeowners": ["@bachya"] + "codeowners": ["@bachya"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/sharkiq/manifest.json b/homeassistant/components/sharkiq/manifest.json index ee98ccfe32e96..3299e05222790 100644 --- a/homeassistant/components/sharkiq/manifest.json +++ b/homeassistant/components/sharkiq/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sharkiq", "requirements": ["sharkiqpy==0.1.8"], - "codeowners": ["@ajmarks"] + "codeowners": ["@ajmarks"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/shell_command/manifest.json b/homeassistant/components/shell_command/manifest.json index bdef9467d8550..ec5fc864ccf1e 100644 --- a/homeassistant/components/shell_command/manifest.json +++ b/homeassistant/components/shell_command/manifest.json @@ -3,5 +3,6 @@ "name": "Shell Command", "documentation": "https://www.home-assistant.io/integrations/shell_command", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_push" } diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index 1ae274d6dfd85..222d5b8b11f43 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -4,6 +4,12 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/shelly", "requirements": ["aioshelly==0.6.2"], - "zeroconf": [{ "type": "_http._tcp.local.", "name": "shelly*" }], - "codeowners": ["@balloob", "@bieniu", "@thecode", "@chemelli74"] + "zeroconf": [ + { + "type": "_http._tcp.local.", + "name": "shelly*" + } + ], + "codeowners": ["@balloob", "@bieniu", "@thecode", "@chemelli74"], + "iot_class": "local_push" } diff --git a/homeassistant/components/shiftr/manifest.json b/homeassistant/components/shiftr/manifest.json index 21977c286d08b..f7f04eb5a8663 100644 --- a/homeassistant/components/shiftr/manifest.json +++ b/homeassistant/components/shiftr/manifest.json @@ -3,5 +3,6 @@ "name": "shiftr.io", "documentation": "https://www.home-assistant.io/integrations/shiftr", "requirements": ["paho-mqtt==1.5.1"], - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/shodan/manifest.json b/homeassistant/components/shodan/manifest.json index 17f4dc1bf79b1..c2d9d3dd265ae 100644 --- a/homeassistant/components/shodan/manifest.json +++ b/homeassistant/components/shodan/manifest.json @@ -3,5 +3,6 @@ "name": "Shodan", "documentation": "https://www.home-assistant.io/integrations/shodan", "requirements": ["shodan==1.25.0"], - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/shopping_list/manifest.json b/homeassistant/components/shopping_list/manifest.json index 38829d80f0ab1..72576ef47cff7 100644 --- a/homeassistant/components/shopping_list/manifest.json +++ b/homeassistant/components/shopping_list/manifest.json @@ -5,5 +5,6 @@ "dependencies": ["http"], "codeowners": [], "config_flow": true, - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_push" } diff --git a/homeassistant/components/sht31/manifest.json b/homeassistant/components/sht31/manifest.json index 899215ffe71a7..c91d6a6276892 100644 --- a/homeassistant/components/sht31/manifest.json +++ b/homeassistant/components/sht31/manifest.json @@ -3,5 +3,6 @@ "name": "Sensirion SHT31", "documentation": "https://www.home-assistant.io/integrations/sht31", "requirements": ["Adafruit-GPIO==1.0.3", "Adafruit-SHT31==1.0.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/sigfox/manifest.json b/homeassistant/components/sigfox/manifest.json index b3ad57f3727a9..f139a75fa7895 100644 --- a/homeassistant/components/sigfox/manifest.json +++ b/homeassistant/components/sigfox/manifest.json @@ -2,5 +2,6 @@ "domain": "sigfox", "name": "Sigfox", "documentation": "https://www.home-assistant.io/integrations/sigfox", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/sighthound/manifest.json b/homeassistant/components/sighthound/manifest.json index 0cab5b45b848f..e372c995b5e03 100644 --- a/homeassistant/components/sighthound/manifest.json +++ b/homeassistant/components/sighthound/manifest.json @@ -3,5 +3,6 @@ "name": "Sighthound", "documentation": "https://www.home-assistant.io/integrations/sighthound", "requirements": ["pillow==8.1.2", "simplehound==0.3"], - "codeowners": ["@robmarkcole"] + "codeowners": ["@robmarkcole"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/signal_messenger/manifest.json b/homeassistant/components/signal_messenger/manifest.json index dcbf41307c457..9c1c40880784b 100644 --- a/homeassistant/components/signal_messenger/manifest.json +++ b/homeassistant/components/signal_messenger/manifest.json @@ -3,5 +3,6 @@ "name": "Signal Messenger", "documentation": "https://www.home-assistant.io/integrations/signal_messenger", "codeowners": ["@bbernhard"], - "requirements": ["pysignalclirestapi==0.3.4"] + "requirements": ["pysignalclirestapi==0.3.4"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/simplepush/manifest.json b/homeassistant/components/simplepush/manifest.json index 70c4f1b4580ee..dc711df0e8d3a 100644 --- a/homeassistant/components/simplepush/manifest.json +++ b/homeassistant/components/simplepush/manifest.json @@ -3,5 +3,6 @@ "name": "Simplepush", "documentation": "https://www.home-assistant.io/integrations/simplepush", "requirements": ["simplepush==1.1.4"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index 45deb938b59c2..0a46e1d52809a 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/simplisafe", "requirements": ["simplisafe-python==9.6.9"], - "codeowners": ["@bachya"] + "codeowners": ["@bachya"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/simulated/manifest.json b/homeassistant/components/simulated/manifest.json index 72514c80f9724..f7584e9b8afdd 100644 --- a/homeassistant/components/simulated/manifest.json +++ b/homeassistant/components/simulated/manifest.json @@ -3,5 +3,6 @@ "name": "Simulated", "documentation": "https://www.home-assistant.io/integrations/simulated", "codeowners": [], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_polling" } diff --git a/homeassistant/components/sinch/manifest.json b/homeassistant/components/sinch/manifest.json index c1968cff445cf..c33babf4913f8 100644 --- a/homeassistant/components/sinch/manifest.json +++ b/homeassistant/components/sinch/manifest.json @@ -3,5 +3,6 @@ "name": "Sinch SMS", "documentation": "https://www.home-assistant.io/integrations/sinch", "codeowners": ["@bendikrb"], - "requirements": ["clx-sdk-xms==1.0.0"] + "requirements": ["clx-sdk-xms==1.0.0"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/sisyphus/manifest.json b/homeassistant/components/sisyphus/manifest.json index 24dd3345f80ca..d8a0392ab558a 100644 --- a/homeassistant/components/sisyphus/manifest.json +++ b/homeassistant/components/sisyphus/manifest.json @@ -2,10 +2,7 @@ "domain": "sisyphus", "name": "Sisyphus", "documentation": "https://www.home-assistant.io/integrations/sisyphus", - "requirements": [ - "sisyphus-control==3.0" - ], - "codeowners": [ - "@jkeljo" - ] -} \ No newline at end of file + "requirements": ["sisyphus-control==3.0"], + "codeowners": ["@jkeljo"], + "iot_class": "local_push" +} diff --git a/homeassistant/components/sky_hub/manifest.json b/homeassistant/components/sky_hub/manifest.json index 965c2af515916..ba47b3fc1471c 100644 --- a/homeassistant/components/sky_hub/manifest.json +++ b/homeassistant/components/sky_hub/manifest.json @@ -3,5 +3,6 @@ "name": "Sky Hub", "documentation": "https://www.home-assistant.io/integrations/sky_hub", "requirements": ["pyskyqhub==0.1.3"], - "codeowners": ["@rogerselwyn"] + "codeowners": ["@rogerselwyn"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/skybeacon/manifest.json b/homeassistant/components/skybeacon/manifest.json index 2ce19afc6c534..da7ee08ff5989 100644 --- a/homeassistant/components/skybeacon/manifest.json +++ b/homeassistant/components/skybeacon/manifest.json @@ -3,5 +3,6 @@ "name": "Skybeacon", "documentation": "https://www.home-assistant.io/integrations/skybeacon", "requirements": ["pygatt[GATTTOOL]==4.0.5"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/skybell/manifest.json b/homeassistant/components/skybell/manifest.json index 4d621d18fa6e9..8b939d1d522eb 100644 --- a/homeassistant/components/skybell/manifest.json +++ b/homeassistant/components/skybell/manifest.json @@ -3,5 +3,6 @@ "name": "SkyBell", "documentation": "https://www.home-assistant.io/integrations/skybell", "requirements": ["skybellpy==0.6.3"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/slack/manifest.json b/homeassistant/components/slack/manifest.json index e183dd455f1a9..2605ffd2914fe 100644 --- a/homeassistant/components/slack/manifest.json +++ b/homeassistant/components/slack/manifest.json @@ -3,5 +3,6 @@ "name": "Slack", "documentation": "https://www.home-assistant.io/integrations/slack", "requirements": ["slackclient==2.5.0"], - "codeowners": ["@bachya"] + "codeowners": ["@bachya"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/sleepiq/manifest.json b/homeassistant/components/sleepiq/manifest.json index 0f5064f3264de..f6d4404884d86 100644 --- a/homeassistant/components/sleepiq/manifest.json +++ b/homeassistant/components/sleepiq/manifest.json @@ -3,5 +3,6 @@ "name": "SleepIQ", "documentation": "https://www.home-assistant.io/integrations/sleepiq", "requirements": ["sleepyq==0.8.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/slide/manifest.json b/homeassistant/components/slide/manifest.json index d5567b0d3474f..a360bb7491a65 100644 --- a/homeassistant/components/slide/manifest.json +++ b/homeassistant/components/slide/manifest.json @@ -3,5 +3,6 @@ "name": "Slide", "documentation": "https://www.home-assistant.io/integrations/slide", "requirements": ["goslide-api==0.5.1"], - "codeowners": ["@ualex73"] + "codeowners": ["@ualex73"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/sma/manifest.json b/homeassistant/components/sma/manifest.json index f38038d8eb1b0..8add6f830e8bd 100644 --- a/homeassistant/components/sma/manifest.json +++ b/homeassistant/components/sma/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sma", "requirements": ["pysma==0.4.3"], - "codeowners": ["@kellerza", "@rklomp"] + "codeowners": ["@kellerza", "@rklomp"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/smappee/manifest.json b/homeassistant/components/smappee/manifest.json index a6dda75ac723d..cf693b8061c68 100644 --- a/homeassistant/components/smappee/manifest.json +++ b/homeassistant/components/smappee/manifest.json @@ -4,14 +4,17 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/smappee", "dependencies": ["http"], - "requirements": [ - "pysmappee==0.2.17" - ], - "codeowners": [ - "@bsmappee" - ], + "requirements": ["pysmappee==0.2.17"], + "codeowners": ["@bsmappee"], "zeroconf": [ - {"type":"_ssh._tcp.local.", "name":"smappee1*"}, - {"type":"_ssh._tcp.local.", "name":"smappee2*"} - ] + { + "type": "_ssh._tcp.local.", + "name": "smappee1*" + }, + { + "type": "_ssh._tcp.local.", + "name": "smappee2*" + } + ], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/smart_meter_texas/manifest.json b/homeassistant/components/smart_meter_texas/manifest.json index be1ef6b11a881..0e8a6b91236ab 100644 --- a/homeassistant/components/smart_meter_texas/manifest.json +++ b/homeassistant/components/smart_meter_texas/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/smart_meter_texas", "requirements": ["smart-meter-texas==0.4.0"], - "codeowners": ["@grahamwetzler"] + "codeowners": ["@grahamwetzler"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/smarthab/manifest.json b/homeassistant/components/smarthab/manifest.json index 5c601cc9e2192..054aaca2d7678 100644 --- a/homeassistant/components/smarthab/manifest.json +++ b/homeassistant/components/smarthab/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/smarthab", "config_flow": true, "requirements": ["smarthab==0.21"], - "codeowners": ["@outadoc"] + "codeowners": ["@outadoc"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/smartthings/manifest.json b/homeassistant/components/smartthings/manifest.json index 88ed85306dbe5..7d8bc17d430c6 100644 --- a/homeassistant/components/smartthings/manifest.json +++ b/homeassistant/components/smartthings/manifest.json @@ -6,5 +6,6 @@ "requirements": ["pysmartapp==0.3.3", "pysmartthings==0.7.6"], "dependencies": ["webhook"], "after_dependencies": ["cloud"], - "codeowners": ["@andrewsayre"] + "codeowners": ["@andrewsayre"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/smarttub/manifest.json b/homeassistant/components/smarttub/manifest.json index 5505ba69a6d33..291c700e108a2 100644 --- a/homeassistant/components/smarttub/manifest.json +++ b/homeassistant/components/smarttub/manifest.json @@ -5,8 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/smarttub", "dependencies": [], "codeowners": ["@mdz"], - "requirements": [ - "python-smarttub==0.0.23" - ], - "quality_scale": "platinum" + "requirements": ["python-smarttub==0.0.23"], + "quality_scale": "platinum", + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/smarty/manifest.json b/homeassistant/components/smarty/manifest.json index b55f3f11c3e5e..cfae1d98a5b96 100644 --- a/homeassistant/components/smarty/manifest.json +++ b/homeassistant/components/smarty/manifest.json @@ -3,5 +3,6 @@ "name": "Salda Smarty", "documentation": "https://www.home-assistant.io/integrations/smarty", "requirements": ["pysmarty==0.8"], - "codeowners": ["@z0mbieprocess"] + "codeowners": ["@z0mbieprocess"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/smhi/manifest.json b/homeassistant/components/smhi/manifest.json index 2e21f62a599cd..9d762df831d0c 100644 --- a/homeassistant/components/smhi/manifest.json +++ b/homeassistant/components/smhi/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/smhi", "requirements": ["smhi-pkg==1.0.13"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/sms/manifest.json b/homeassistant/components/sms/manifest.json index 1c24777bd4ac3..9a466236758d8 100644 --- a/homeassistant/components/sms/manifest.json +++ b/homeassistant/components/sms/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sms", "requirements": ["python-gammu==3.1"], - "codeowners": ["@ocalvo"] + "codeowners": ["@ocalvo"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/smtp/manifest.json b/homeassistant/components/smtp/manifest.json index 334687a804788..f7a3373ce3088 100644 --- a/homeassistant/components/smtp/manifest.json +++ b/homeassistant/components/smtp/manifest.json @@ -2,5 +2,6 @@ "domain": "smtp", "name": "SMTP", "documentation": "https://www.home-assistant.io/integrations/smtp", - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/snapcast/manifest.json b/homeassistant/components/snapcast/manifest.json index 43fbbeb8808fa..32162c062dd9a 100644 --- a/homeassistant/components/snapcast/manifest.json +++ b/homeassistant/components/snapcast/manifest.json @@ -3,5 +3,6 @@ "name": "Snapcast", "documentation": "https://www.home-assistant.io/integrations/snapcast", "requirements": ["snapcast==2.1.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/snips/manifest.json b/homeassistant/components/snips/manifest.json index c704164c17fff..2b7319af14ced 100644 --- a/homeassistant/components/snips/manifest.json +++ b/homeassistant/components/snips/manifest.json @@ -3,5 +3,6 @@ "name": "Snips", "documentation": "https://www.home-assistant.io/integrations/snips", "dependencies": ["mqtt"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/snmp/manifest.json b/homeassistant/components/snmp/manifest.json index 1dfdc36a0cb73..19cd258ce6ffc 100644 --- a/homeassistant/components/snmp/manifest.json +++ b/homeassistant/components/snmp/manifest.json @@ -3,5 +3,6 @@ "name": "SNMP", "documentation": "https://www.home-assistant.io/integrations/snmp", "requirements": ["pysnmp==4.4.12"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/sochain/manifest.json b/homeassistant/components/sochain/manifest.json index db89dfc219e32..e270e81012230 100644 --- a/homeassistant/components/sochain/manifest.json +++ b/homeassistant/components/sochain/manifest.json @@ -3,5 +3,6 @@ "name": "SoChain", "documentation": "https://www.home-assistant.io/integrations/sochain", "requirements": ["python-sochain-api==0.0.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/solaredge/manifest.json b/homeassistant/components/solaredge/manifest.json index 5cfe773d98c0b..84b1e6b9445bd 100644 --- a/homeassistant/components/solaredge/manifest.json +++ b/homeassistant/components/solaredge/manifest.json @@ -5,5 +5,11 @@ "requirements": ["solaredge==0.0.2", "stringcase==1.2.0"], "config_flow": true, "codeowners": ["@frenck"], - "dhcp": [{ "hostname": "target", "macaddress": "002702*" }] + "dhcp": [ + { + "hostname": "target", + "macaddress": "002702*" + } + ], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/solaredge_local/manifest.json b/homeassistant/components/solaredge_local/manifest.json index 8f8b80c2c659d..56e722174b4f3 100644 --- a/homeassistant/components/solaredge_local/manifest.json +++ b/homeassistant/components/solaredge_local/manifest.json @@ -3,5 +3,6 @@ "name": "SolarEdge Local", "documentation": "https://www.home-assistant.io/integrations/solaredge_local", "requirements": ["solaredge-local==0.2.0"], - "codeowners": ["@drobtravels", "@scheric"] + "codeowners": ["@drobtravels", "@scheric"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/solarlog/manifest.json b/homeassistant/components/solarlog/manifest.json index f24f9b9473c7b..5535da860f0bb 100644 --- a/homeassistant/components/solarlog/manifest.json +++ b/homeassistant/components/solarlog/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/solarlog", "codeowners": ["@Ernst79"], - "requirements": ["sunwatcher==0.2.1"] + "requirements": ["sunwatcher==0.2.1"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/solax/manifest.json b/homeassistant/components/solax/manifest.json index 90bfd8e61840d..d14cfea2501a7 100644 --- a/homeassistant/components/solax/manifest.json +++ b/homeassistant/components/solax/manifest.json @@ -3,5 +3,6 @@ "name": "SolaX Power", "documentation": "https://www.home-assistant.io/integrations/solax", "requirements": ["solax==0.2.6"], - "codeowners": ["@squishykid"] + "codeowners": ["@squishykid"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/soma/manifest.json b/homeassistant/components/soma/manifest.json index 3c96ef2efddc7..fe7cb8d89eb47 100644 --- a/homeassistant/components/soma/manifest.json +++ b/homeassistant/components/soma/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/soma", "codeowners": ["@ratsept"], - "requirements": ["pysoma==0.0.10"] + "requirements": ["pysoma==0.0.10"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/somfy/manifest.json b/homeassistant/components/somfy/manifest.json index a236bc40085fe..8dad4abd6cc53 100644 --- a/homeassistant/components/somfy/manifest.json +++ b/homeassistant/components/somfy/manifest.json @@ -7,6 +7,10 @@ "codeowners": ["@tetienne"], "requirements": ["pymfy==0.9.3"], "zeroconf": [ - {"type": "_kizbox._tcp.local.", "name": "gateway*"} - ] + { + "type": "_kizbox._tcp.local.", + "name": "gateway*" + } + ], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/somfy_mylink/manifest.json b/homeassistant/components/somfy_mylink/manifest.json index a71661f57f445..a376654ede4ae 100644 --- a/homeassistant/components/somfy_mylink/manifest.json +++ b/homeassistant/components/somfy_mylink/manifest.json @@ -2,12 +2,14 @@ "domain": "somfy_mylink", "name": "Somfy MyLink", "documentation": "https://www.home-assistant.io/integrations/somfy_mylink", - "requirements": [ - "somfy-mylink-synergy==1.0.6" - ], + "requirements": ["somfy-mylink-synergy==1.0.6"], "codeowners": [], "config_flow": true, - "dhcp": [{ - "hostname":"somfy_*", "macaddress":"B8B7F1*" - }] + "dhcp": [ + { + "hostname": "somfy_*", + "macaddress": "B8B7F1*" + } + ], + "iot_class": "assumed_state" } diff --git a/homeassistant/components/sonarr/manifest.json b/homeassistant/components/sonarr/manifest.json index 65146b90759ac..50de11d8209a0 100644 --- a/homeassistant/components/sonarr/manifest.json +++ b/homeassistant/components/sonarr/manifest.json @@ -5,5 +5,6 @@ "codeowners": ["@ctalkington"], "requirements": ["sonarr==0.3.0"], "config_flow": true, - "quality_scale": "silver" + "quality_scale": "silver", + "iot_class": "local_polling" } diff --git a/homeassistant/components/songpal/manifest.json b/homeassistant/components/songpal/manifest.json index 40df684df79b4..4d417aec1a2f0 100644 --- a/homeassistant/components/songpal/manifest.json +++ b/homeassistant/components/songpal/manifest.json @@ -11,5 +11,6 @@ "manufacturer": "Sony Corporation" } ], - "quality_scale": "gold" + "quality_scale": "gold", + "iot_class": "local_push" } diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index f66e25e3d2718..5875baf0fb96c 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -10,7 +10,6 @@ "st": "urn:schemas-upnp-org:device:ZonePlayer:1" } ], - "codeowners": [ - "@cgtobi" - ] + "codeowners": ["@cgtobi"], + "iot_class": "local_push" } diff --git a/homeassistant/components/sony_projector/manifest.json b/homeassistant/components/sony_projector/manifest.json index 3e86eae6b809e..07819b7b63941 100644 --- a/homeassistant/components/sony_projector/manifest.json +++ b/homeassistant/components/sony_projector/manifest.json @@ -3,5 +3,6 @@ "name": "Sony Projector", "documentation": "https://www.home-assistant.io/integrations/sony_projector", "requirements": ["pysdcp==1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/soundtouch/manifest.json b/homeassistant/components/soundtouch/manifest.json index 58bdab1a2d7aa..2b8c2fb5477a9 100644 --- a/homeassistant/components/soundtouch/manifest.json +++ b/homeassistant/components/soundtouch/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/soundtouch", "requirements": ["libsoundtouch==0.8"], "after_dependencies": ["zeroconf"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/spaceapi/manifest.json b/homeassistant/components/spaceapi/manifest.json index 598ea05ace6f3..6b6292851b6ac 100644 --- a/homeassistant/components/spaceapi/manifest.json +++ b/homeassistant/components/spaceapi/manifest.json @@ -3,5 +3,6 @@ "name": "Space API", "documentation": "https://www.home-assistant.io/integrations/spaceapi", "dependencies": ["http"], - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/spc/manifest.json b/homeassistant/components/spc/manifest.json index 63fb359371f84..9906a4025a562 100644 --- a/homeassistant/components/spc/manifest.json +++ b/homeassistant/components/spc/manifest.json @@ -3,5 +3,6 @@ "name": "Vanderbilt SPC", "documentation": "https://www.home-assistant.io/integrations/spc", "requirements": ["pyspcwebgw==0.4.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/speedtestdotnet/manifest.json b/homeassistant/components/speedtestdotnet/manifest.json index f2e2a2196c98b..1df9d6c236a63 100644 --- a/homeassistant/components/speedtestdotnet/manifest.json +++ b/homeassistant/components/speedtestdotnet/manifest.json @@ -3,8 +3,7 @@ "name": "Speedtest.net", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/speedtestdotnet", - "requirements": [ - "speedtest-cli==2.1.3" - ], - "codeowners": ["@rohankapoorcom", "@engrbm87"] + "requirements": ["speedtest-cli==2.1.3"], + "codeowners": ["@rohankapoorcom", "@engrbm87"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/spider/manifest.json b/homeassistant/components/spider/manifest.json index 32567e6d13412..ced19db39c72b 100644 --- a/homeassistant/components/spider/manifest.json +++ b/homeassistant/components/spider/manifest.json @@ -2,11 +2,8 @@ "domain": "spider", "name": "Itho Daalderop Spider", "documentation": "https://www.home-assistant.io/integrations/spider", - "requirements": [ - "spiderpy==1.4.2" - ], - "codeowners": [ - "@peternijssen" - ], - "config_flow": true + "requirements": ["spiderpy==1.4.2"], + "codeowners": ["@peternijssen"], + "config_flow": true, + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/splunk/manifest.json b/homeassistant/components/splunk/manifest.json index d51d6c712de79..09a128c9b72fa 100644 --- a/homeassistant/components/splunk/manifest.json +++ b/homeassistant/components/splunk/manifest.json @@ -2,10 +2,7 @@ "domain": "splunk", "name": "Splunk", "documentation": "https://www.home-assistant.io/integrations/splunk", - "requirements": [ - "hass_splunk==0.1.1" - ], - "codeowners": [ - "@Bre77" - ] -} \ No newline at end of file + "requirements": ["hass_splunk==0.1.1"], + "codeowners": ["@Bre77"], + "iot_class": "local_push" +} diff --git a/homeassistant/components/spotcrime/manifest.json b/homeassistant/components/spotcrime/manifest.json index fd0184f1b21c7..a668454469db7 100644 --- a/homeassistant/components/spotcrime/manifest.json +++ b/homeassistant/components/spotcrime/manifest.json @@ -3,5 +3,6 @@ "name": "Spot Crime", "documentation": "https://www.home-assistant.io/integrations/spotcrime", "requirements": ["spotcrime==1.0.4"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/spotify/manifest.json b/homeassistant/components/spotify/manifest.json index d0d40291ffff9..4a4a904fe9e09 100644 --- a/homeassistant/components/spotify/manifest.json +++ b/homeassistant/components/spotify/manifest.json @@ -7,5 +7,6 @@ "dependencies": ["http"], "codeowners": ["@frenck"], "config_flow": true, - "quality_scale": "silver" + "quality_scale": "silver", + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/sql/manifest.json b/homeassistant/components/sql/manifest.json index 7418eb095da78..3eb1308c7f6fa 100644 --- a/homeassistant/components/sql/manifest.json +++ b/homeassistant/components/sql/manifest.json @@ -3,5 +3,6 @@ "name": "SQL", "documentation": "https://www.home-assistant.io/integrations/sql", "requirements": ["sqlalchemy==1.3.23"], - "codeowners": ["@dgomes"] + "codeowners": ["@dgomes"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/squeezebox/manifest.json b/homeassistant/components/squeezebox/manifest.json index c31d80e1acf18..ec3089dc4bef8 100644 --- a/homeassistant/components/squeezebox/manifest.json +++ b/homeassistant/components/squeezebox/manifest.json @@ -2,14 +2,14 @@ "domain": "squeezebox", "name": "Logitech Squeezebox", "documentation": "https://www.home-assistant.io/integrations/squeezebox", - "codeowners": [ - "@rajlaud" - ], - "requirements": [ - "pysqueezebox==0.5.5" - ], + "codeowners": ["@rajlaud"], + "requirements": ["pysqueezebox==0.5.5"], "config_flow": true, "dhcp": [ - {"hostname":"squeezebox*","macaddress":"000420*"} - ] + { + "hostname": "squeezebox*", + "macaddress": "000420*" + } + ], + "iot_class": "local_polling" } diff --git a/homeassistant/components/srp_energy/manifest.json b/homeassistant/components/srp_energy/manifest.json index fb051fc7b2f7c..eb9aa7d12c404 100644 --- a/homeassistant/components/srp_energy/manifest.json +++ b/homeassistant/components/srp_energy/manifest.json @@ -3,14 +3,11 @@ "name": "SRP Energy", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/srp_energy", - "requirements": [ - "srpenergy==1.3.2" - ], + "requirements": ["srpenergy==1.3.2"], "ssdp": [], "zeroconf": [], "homekit": {}, "dependencies": [], - "codeowners": [ - "@briglx" - ] -} \ No newline at end of file + "codeowners": ["@briglx"], + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index 5fd635db3f190..c2ad7921ac24f 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -2,8 +2,13 @@ "domain": "ssdp", "name": "Simple Service Discovery Protocol (SSDP)", "documentation": "https://www.home-assistant.io/integrations/ssdp", - "requirements": ["defusedxml==0.6.0", "netdisco==2.8.2", "async-upnp-client==0.16.0"], + "requirements": [ + "defusedxml==0.6.0", + "netdisco==2.8.2", + "async-upnp-client==0.16.0" + ], "after_dependencies": ["zeroconf"], "codeowners": [], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_push" } diff --git a/homeassistant/components/starline/manifest.json b/homeassistant/components/starline/manifest.json index 79b163ee11526..e487d8d63f0f7 100644 --- a/homeassistant/components/starline/manifest.json +++ b/homeassistant/components/starline/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/starline", "requirements": ["starline==0.1.5"], - "codeowners": ["@anonym-tsk"] + "codeowners": ["@anonym-tsk"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/starlingbank/manifest.json b/homeassistant/components/starlingbank/manifest.json index cb0ecc63d6980..8de4b4c24dc11 100644 --- a/homeassistant/components/starlingbank/manifest.json +++ b/homeassistant/components/starlingbank/manifest.json @@ -3,5 +3,6 @@ "name": "Starling Bank", "documentation": "https://www.home-assistant.io/integrations/starlingbank", "requirements": ["starlingbank==3.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/startca/manifest.json b/homeassistant/components/startca/manifest.json index 68ac1aeb65bc1..d08f276e770ff 100644 --- a/homeassistant/components/startca/manifest.json +++ b/homeassistant/components/startca/manifest.json @@ -3,5 +3,6 @@ "name": "Start.ca", "documentation": "https://www.home-assistant.io/integrations/startca", "requirements": ["xmltodict==0.12.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/statistics/manifest.json b/homeassistant/components/statistics/manifest.json index bf0de54aa8296..936f8b608490e 100644 --- a/homeassistant/components/statistics/manifest.json +++ b/homeassistant/components/statistics/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/statistics", "after_dependencies": ["recorder"], "codeowners": ["@fabaff"], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_polling" } diff --git a/homeassistant/components/statsd/manifest.json b/homeassistant/components/statsd/manifest.json index c2e5f0bc33f95..5e4db0b6770f0 100644 --- a/homeassistant/components/statsd/manifest.json +++ b/homeassistant/components/statsd/manifest.json @@ -3,5 +3,6 @@ "name": "StatsD", "documentation": "https://www.home-assistant.io/integrations/statsd", "requirements": ["statsd==3.2.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/steam_online/manifest.json b/homeassistant/components/steam_online/manifest.json index 99015e54a4ce0..ca5e4f1da53f6 100644 --- a/homeassistant/components/steam_online/manifest.json +++ b/homeassistant/components/steam_online/manifest.json @@ -3,5 +3,6 @@ "name": "Steam", "documentation": "https://www.home-assistant.io/integrations/steam_online", "requirements": ["steamodd==4.21"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/stiebel_eltron/manifest.json b/homeassistant/components/stiebel_eltron/manifest.json index 769d63328a726..3f83c35ffa9d4 100644 --- a/homeassistant/components/stiebel_eltron/manifest.json +++ b/homeassistant/components/stiebel_eltron/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/stiebel_eltron", "requirements": ["pystiebeleltron==0.0.1.dev2"], "dependencies": ["modbus"], - "codeowners": ["@fucm"] + "codeowners": ["@fucm"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/stookalert/manifest.json b/homeassistant/components/stookalert/manifest.json index dc12512920e2e..094f4c45670cc 100644 --- a/homeassistant/components/stookalert/manifest.json +++ b/homeassistant/components/stookalert/manifest.json @@ -3,5 +3,6 @@ "name": "RIVM Stookalert", "documentation": "https://www.home-assistant.io/integrations/stookalert", "codeowners": ["@fwestenberg"], - "requirements": ["stookalert==0.1.4"] + "requirements": ["stookalert==0.1.4"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/stream/manifest.json b/homeassistant/components/stream/manifest.json index 400b50eae0459..47ba33c44d504 100644 --- a/homeassistant/components/stream/manifest.json +++ b/homeassistant/components/stream/manifest.json @@ -5,5 +5,6 @@ "requirements": ["av==8.0.3"], "dependencies": ["http"], "codeowners": ["@hunterjm", "@uvjustin", "@allenporter"], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_push" } diff --git a/homeassistant/components/streamlabswater/manifest.json b/homeassistant/components/streamlabswater/manifest.json index d1c01cb66b503..cb42752d966d6 100644 --- a/homeassistant/components/streamlabswater/manifest.json +++ b/homeassistant/components/streamlabswater/manifest.json @@ -3,5 +3,6 @@ "name": "StreamLabs", "documentation": "https://www.home-assistant.io/integrations/streamlabswater", "requirements": ["streamlabswater==1.0.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/subaru/manifest.json b/homeassistant/components/subaru/manifest.json index 7a918c59f7472..2b7af28a91606 100644 --- a/homeassistant/components/subaru/manifest.json +++ b/homeassistant/components/subaru/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/subaru", "requirements": ["subarulink==0.3.12"], - "codeowners": ["@G-Two"] + "codeowners": ["@G-Two"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/suez_water/manifest.json b/homeassistant/components/suez_water/manifest.json index 632915d7e5f50..20c8ba1dfed76 100644 --- a/homeassistant/components/suez_water/manifest.json +++ b/homeassistant/components/suez_water/manifest.json @@ -1,7 +1,8 @@ { - "domain": "suez_water", - "name": "Suez Water", - "documentation": "https://www.home-assistant.io/integrations/suez_water", - "codeowners": ["@ooii"], - "requirements": ["pysuez==0.1.19"] + "domain": "suez_water", + "name": "Suez Water", + "documentation": "https://www.home-assistant.io/integrations/suez_water", + "codeowners": ["@ooii"], + "requirements": ["pysuez==0.1.19"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/sun/manifest.json b/homeassistant/components/sun/manifest.json index c406a339a5f9f..93fb76629ccb7 100644 --- a/homeassistant/components/sun/manifest.json +++ b/homeassistant/components/sun/manifest.json @@ -3,5 +3,6 @@ "name": "Sun", "documentation": "https://www.home-assistant.io/integrations/sun", "codeowners": ["@Swamp-Ig"], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "calculated" } diff --git a/homeassistant/components/supervisord/manifest.json b/homeassistant/components/supervisord/manifest.json index 82f4027d359a1..23b4e24c652bf 100644 --- a/homeassistant/components/supervisord/manifest.json +++ b/homeassistant/components/supervisord/manifest.json @@ -2,5 +2,6 @@ "domain": "supervisord", "name": "Supervisord", "documentation": "https://www.home-assistant.io/integrations/supervisord", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/supla/manifest.json b/homeassistant/components/supla/manifest.json index 1a2dcf3cbc51e..6420e39538e9e 100644 --- a/homeassistant/components/supla/manifest.json +++ b/homeassistant/components/supla/manifest.json @@ -3,5 +3,6 @@ "name": "Supla", "documentation": "https://www.home-assistant.io/integrations/supla", "requirements": ["asyncpysupla==0.0.5"], - "codeowners": ["@mwegrzynek"] + "codeowners": ["@mwegrzynek"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/surepetcare/manifest.json b/homeassistant/components/surepetcare/manifest.json index 99b52a68c8dee..6c5b0616be755 100644 --- a/homeassistant/components/surepetcare/manifest.json +++ b/homeassistant/components/surepetcare/manifest.json @@ -3,5 +3,6 @@ "name": "Sure Petcare", "documentation": "https://www.home-assistant.io/integrations/surepetcare", "codeowners": ["@benleb"], - "requirements": ["surepy==0.4.0"] + "requirements": ["surepy==0.4.0"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/swiss_hydrological_data/manifest.json b/homeassistant/components/swiss_hydrological_data/manifest.json index b293e5c2e1de5..faceb69c3e129 100644 --- a/homeassistant/components/swiss_hydrological_data/manifest.json +++ b/homeassistant/components/swiss_hydrological_data/manifest.json @@ -3,5 +3,6 @@ "name": "Swiss Hydrological Data", "documentation": "https://www.home-assistant.io/integrations/swiss_hydrological_data", "requirements": ["swisshydrodata==0.0.3"], - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/swiss_public_transport/manifest.json b/homeassistant/components/swiss_public_transport/manifest.json index ae7601ebc8e0c..2d99d6ef9f44f 100644 --- a/homeassistant/components/swiss_public_transport/manifest.json +++ b/homeassistant/components/swiss_public_transport/manifest.json @@ -3,5 +3,6 @@ "name": "Swiss public transport", "documentation": "https://www.home-assistant.io/integrations/swiss_public_transport", "requirements": ["python_opendata_transport==0.2.1"], - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/swisscom/manifest.json b/homeassistant/components/swisscom/manifest.json index f9f023e8e3cf3..319c1578e827f 100644 --- a/homeassistant/components/swisscom/manifest.json +++ b/homeassistant/components/swisscom/manifest.json @@ -2,5 +2,6 @@ "domain": "swisscom", "name": "Swisscom Internet-Box", "documentation": "https://www.home-assistant.io/integrations/swisscom", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 2bbca5ae50aeb..365f4ce475c0a 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -3,5 +3,6 @@ "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", "requirements": ["PySwitchbot==0.8.0"], - "codeowners": ["@danielhiversen"] + "codeowners": ["@danielhiversen"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/switcher_kis/manifest.json b/homeassistant/components/switcher_kis/manifest.json index c0cf7f18de6af..7344e2d05c014 100644 --- a/homeassistant/components/switcher_kis/manifest.json +++ b/homeassistant/components/switcher_kis/manifest.json @@ -3,5 +3,6 @@ "name": "Switcher", "documentation": "https://www.home-assistant.io/integrations/switcher_kis/", "codeowners": ["@tomerfi"], - "requirements": ["aioswitcher==1.2.1"] + "requirements": ["aioswitcher==1.2.1"], + "iot_class": "local_push" } diff --git a/homeassistant/components/switchmate/manifest.json b/homeassistant/components/switchmate/manifest.json index 30dc08d1dce73..042ccd93091fe 100644 --- a/homeassistant/components/switchmate/manifest.json +++ b/homeassistant/components/switchmate/manifest.json @@ -3,5 +3,6 @@ "name": "Switchmate SimplySmart Home", "documentation": "https://www.home-assistant.io/integrations/switchmate", "requirements": ["pySwitchmate==0.4.6"], - "codeowners": ["@danielhiversen"] + "codeowners": ["@danielhiversen"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/syncthru/manifest.json b/homeassistant/components/syncthru/manifest.json index f70afa5a695db..e84a52b514ea9 100644 --- a/homeassistant/components/syncthru/manifest.json +++ b/homeassistant/components/syncthru/manifest.json @@ -10,5 +10,6 @@ "manufacturer": "Samsung Electronics" } ], - "codeowners": ["@nielstron"] + "codeowners": ["@nielstron"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/synology_chat/manifest.json b/homeassistant/components/synology_chat/manifest.json index e11e7911488c6..6b8f57ab789f2 100644 --- a/homeassistant/components/synology_chat/manifest.json +++ b/homeassistant/components/synology_chat/manifest.json @@ -2,5 +2,6 @@ "domain": "synology_chat", "name": "Synology Chat", "documentation": "https://www.home-assistant.io/integrations/synology_chat", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/synology_dsm/manifest.json b/homeassistant/components/synology_dsm/manifest.json index 4da44942b4ff5..afa8e2674de5f 100644 --- a/homeassistant/components/synology_dsm/manifest.json +++ b/homeassistant/components/synology_dsm/manifest.json @@ -10,5 +10,6 @@ "manufacturer": "Synology", "deviceType": "urn:schemas-upnp-org:device:Basic:1" } - ] + ], + "iot_class": "local_polling" } diff --git a/homeassistant/components/synology_srm/manifest.json b/homeassistant/components/synology_srm/manifest.json index 798d7e7ef82cb..b4d96f6f9b1bc 100644 --- a/homeassistant/components/synology_srm/manifest.json +++ b/homeassistant/components/synology_srm/manifest.json @@ -3,5 +3,6 @@ "name": "Synology SRM", "documentation": "https://www.home-assistant.io/integrations/synology_srm", "requirements": ["synology-srm==0.2.0"], - "codeowners": ["@aerialls"] + "codeowners": ["@aerialls"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/syslog/manifest.json b/homeassistant/components/syslog/manifest.json index 07a74b663646e..35e039f9dd32f 100644 --- a/homeassistant/components/syslog/manifest.json +++ b/homeassistant/components/syslog/manifest.json @@ -2,5 +2,6 @@ "domain": "syslog", "name": "Syslog", "documentation": "https://www.home-assistant.io/integrations/syslog", - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "local_push" } diff --git a/homeassistant/components/systemmonitor/manifest.json b/homeassistant/components/systemmonitor/manifest.json index 9ea39b63888d0..cc79ed12e1e13 100644 --- a/homeassistant/components/systemmonitor/manifest.json +++ b/homeassistant/components/systemmonitor/manifest.json @@ -3,5 +3,6 @@ "name": "System Monitor", "documentation": "https://www.home-assistant.io/integrations/systemmonitor", "requirements": ["psutil==5.8.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/tado/manifest.json b/homeassistant/components/tado/manifest.json index 27c7ecff4116d..7b488487afea9 100644 --- a/homeassistant/components/tado/manifest.json +++ b/homeassistant/components/tado/manifest.json @@ -7,5 +7,6 @@ "config_flow": true, "homekit": { "models": ["tado", "AC02"] - } + }, + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/tahoma/manifest.json b/homeassistant/components/tahoma/manifest.json index 12f1eb7d0a1c5..44eb2ca7575d1 100644 --- a/homeassistant/components/tahoma/manifest.json +++ b/homeassistant/components/tahoma/manifest.json @@ -3,5 +3,6 @@ "name": "Tahoma", "documentation": "https://www.home-assistant.io/integrations/tahoma", "requirements": ["tahoma-api==0.0.16"], - "codeowners": ["@philklei"] + "codeowners": ["@philklei"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/tank_utility/manifest.json b/homeassistant/components/tank_utility/manifest.json index dafe90193f6f8..62a667af5b14e 100644 --- a/homeassistant/components/tank_utility/manifest.json +++ b/homeassistant/components/tank_utility/manifest.json @@ -3,5 +3,6 @@ "name": "Tank Utility", "documentation": "https://www.home-assistant.io/integrations/tank_utility", "requirements": ["tank_utility==1.4.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/tankerkoenig/manifest.json b/homeassistant/components/tankerkoenig/manifest.json index d9a63037a8fe2..d49ee6a125527 100644 --- a/homeassistant/components/tankerkoenig/manifest.json +++ b/homeassistant/components/tankerkoenig/manifest.json @@ -3,5 +3,6 @@ "name": "Tankerkoenig", "documentation": "https://www.home-assistant.io/integrations/tankerkoenig", "requirements": ["pytankerkoenig==0.0.6"], - "codeowners": ["@guillempages"] + "codeowners": ["@guillempages"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/tapsaff/manifest.json b/homeassistant/components/tapsaff/manifest.json index 30b9a2066cd84..f8c4dff154522 100644 --- a/homeassistant/components/tapsaff/manifest.json +++ b/homeassistant/components/tapsaff/manifest.json @@ -3,5 +3,6 @@ "name": "Taps Aff", "documentation": "https://www.home-assistant.io/integrations/tapsaff", "requirements": ["tapsaff==0.2.1"], - "codeowners": ["@bazwilliams"] + "codeowners": ["@bazwilliams"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index 17e72a57ce65d..c6a77d40c83c7 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -6,5 +6,6 @@ "requirements": ["hatasmota==0.2.9"], "dependencies": ["mqtt"], "mqtt": ["tasmota/discovery/#"], - "codeowners": ["@emontnemery"] + "codeowners": ["@emontnemery"], + "iot_class": "local_push" } diff --git a/homeassistant/components/tautulli/manifest.json b/homeassistant/components/tautulli/manifest.json index c821fb498535d..cb2e38ebd6d79 100644 --- a/homeassistant/components/tautulli/manifest.json +++ b/homeassistant/components/tautulli/manifest.json @@ -3,5 +3,6 @@ "name": "Tautulli", "documentation": "https://www.home-assistant.io/integrations/tautulli", "requirements": ["pytautulli==0.5.0"], - "codeowners": ["@ludeeus"] + "codeowners": ["@ludeeus"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/tcp/manifest.json b/homeassistant/components/tcp/manifest.json index b05a3ff58fbd4..d2326f12c4d02 100644 --- a/homeassistant/components/tcp/manifest.json +++ b/homeassistant/components/tcp/manifest.json @@ -2,5 +2,6 @@ "domain": "tcp", "name": "TCP", "documentation": "https://www.home-assistant.io/integrations/tcp", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/ted5000/manifest.json b/homeassistant/components/ted5000/manifest.json index d328d42b019fe..1ab57418af55b 100644 --- a/homeassistant/components/ted5000/manifest.json +++ b/homeassistant/components/ted5000/manifest.json @@ -3,5 +3,6 @@ "name": "The Energy Detective TED5000", "documentation": "https://www.home-assistant.io/integrations/ted5000", "requirements": ["xmltodict==0.12.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/telegram/manifest.json b/homeassistant/components/telegram/manifest.json index 6f661ba574118..e9b5aa76f56b4 100644 --- a/homeassistant/components/telegram/manifest.json +++ b/homeassistant/components/telegram/manifest.json @@ -3,5 +3,6 @@ "name": "Telegram", "documentation": "https://www.home-assistant.io/integrations/telegram", "dependencies": ["telegram_bot"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/telegram_bot/manifest.json b/homeassistant/components/telegram_bot/manifest.json index 80d9b50932ed8..048762903e1cb 100644 --- a/homeassistant/components/telegram_bot/manifest.json +++ b/homeassistant/components/telegram_bot/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/telegram_bot", "requirements": ["python-telegram-bot==13.1", "PySocks==1.7.1"], "dependencies": ["http"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/tellduslive/manifest.json b/homeassistant/components/tellduslive/manifest.json index 7ad65b4abd44e..cebae0c6cf561 100644 --- a/homeassistant/components/tellduslive/manifest.json +++ b/homeassistant/components/tellduslive/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/tellduslive", "requirements": ["tellduslive==0.10.11"], "codeowners": ["@fredrike"], - "quality_scale": "gold" + "quality_scale": "gold", + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/tellstick/manifest.json b/homeassistant/components/tellstick/manifest.json index 4a5a3dd15c697..5d8029ddcf529 100644 --- a/homeassistant/components/tellstick/manifest.json +++ b/homeassistant/components/tellstick/manifest.json @@ -3,5 +3,6 @@ "name": "TellStick", "documentation": "https://www.home-assistant.io/integrations/tellstick", "requirements": ["tellcore-net==0.4", "tellcore-py==1.1.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "assumed_state" } diff --git a/homeassistant/components/telnet/manifest.json b/homeassistant/components/telnet/manifest.json index d4f070519932b..1eeccb50f7c3d 100644 --- a/homeassistant/components/telnet/manifest.json +++ b/homeassistant/components/telnet/manifest.json @@ -2,5 +2,6 @@ "domain": "telnet", "name": "Telnet", "documentation": "https://www.home-assistant.io/integrations/telnet", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/temper/manifest.json b/homeassistant/components/temper/manifest.json index e88cd1fb0432d..d80c44f8a87dc 100644 --- a/homeassistant/components/temper/manifest.json +++ b/homeassistant/components/temper/manifest.json @@ -3,5 +3,6 @@ "name": "TEMPer", "documentation": "https://www.home-assistant.io/integrations/temper", "requirements": ["temperusb==1.5.3"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/template/manifest.json b/homeassistant/components/template/manifest.json index dd2f8d1e0c668..fe9edb21ea160 100644 --- a/homeassistant/components/template/manifest.json +++ b/homeassistant/components/template/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/template", "codeowners": ["@PhracturedBlue", "@tetienne"], "quality_scale": "internal", - "after_dependencies": ["group"] + "after_dependencies": ["group"], + "iot_class": "local_push" } diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index 84619680490c5..c4036e3cb3bf2 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -9,5 +9,6 @@ "numpy==1.20.2", "pillow==8.1.2" ], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/tesla/manifest.json b/homeassistant/components/tesla/manifest.json index 9236aae7fb6ee..6befca8a5f28c 100644 --- a/homeassistant/components/tesla/manifest.json +++ b/homeassistant/components/tesla/manifest.json @@ -6,8 +6,18 @@ "requirements": ["teslajsonpy==0.11.5"], "codeowners": ["@zabuldon", "@alandtse"], "dhcp": [ - { "hostname": "tesla_*", "macaddress": "4CFCAA*" }, - { "hostname": "tesla_*", "macaddress": "044EAF*" }, - { "hostname": "tesla_*", "macaddress": "98ED5C*" } - ] + { + "hostname": "tesla_*", + "macaddress": "4CFCAA*" + }, + { + "hostname": "tesla_*", + "macaddress": "044EAF*" + }, + { + "hostname": "tesla_*", + "macaddress": "98ED5C*" + } + ], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/tfiac/manifest.json b/homeassistant/components/tfiac/manifest.json index 1e86e6a0218c1..9e7ef7ebe0e17 100644 --- a/homeassistant/components/tfiac/manifest.json +++ b/homeassistant/components/tfiac/manifest.json @@ -3,5 +3,6 @@ "name": "Tfiac", "documentation": "https://www.home-assistant.io/integrations/tfiac", "requirements": ["pytfiac==0.4"], - "codeowners": ["@fredrike", "@mellado"] + "codeowners": ["@fredrike", "@mellado"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/thermoworks_smoke/manifest.json b/homeassistant/components/thermoworks_smoke/manifest.json index e69b1d40874bd..aa9a87413907b 100644 --- a/homeassistant/components/thermoworks_smoke/manifest.json +++ b/homeassistant/components/thermoworks_smoke/manifest.json @@ -3,5 +3,6 @@ "name": "ThermoWorks Smoke", "documentation": "https://www.home-assistant.io/integrations/thermoworks_smoke", "requirements": ["stringcase==1.2.0", "thermoworks_smoke==0.1.8"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/thethingsnetwork/manifest.json b/homeassistant/components/thethingsnetwork/manifest.json index ffd2291e158da..5958cbd4dd71a 100644 --- a/homeassistant/components/thethingsnetwork/manifest.json +++ b/homeassistant/components/thethingsnetwork/manifest.json @@ -2,5 +2,6 @@ "domain": "thethingsnetwork", "name": "The Things Network", "documentation": "https://www.home-assistant.io/integrations/thethingsnetwork", - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "local_push" } diff --git a/homeassistant/components/thingspeak/manifest.json b/homeassistant/components/thingspeak/manifest.json index e22dfeb91661b..3ac2e7e4b2523 100644 --- a/homeassistant/components/thingspeak/manifest.json +++ b/homeassistant/components/thingspeak/manifest.json @@ -3,5 +3,6 @@ "name": "ThingSpeak", "documentation": "https://www.home-assistant.io/integrations/thingspeak", "requirements": ["thingspeak==1.0.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/thinkingcleaner/manifest.json b/homeassistant/components/thinkingcleaner/manifest.json index 4515f7f4ed302..cb87c1ea8a375 100644 --- a/homeassistant/components/thinkingcleaner/manifest.json +++ b/homeassistant/components/thinkingcleaner/manifest.json @@ -3,5 +3,6 @@ "name": "Thinking Cleaner", "documentation": "https://www.home-assistant.io/integrations/thinkingcleaner", "requirements": ["pythinkingcleaner==0.0.3"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/thomson/manifest.json b/homeassistant/components/thomson/manifest.json index cca5b05854be8..bdb4592923c62 100644 --- a/homeassistant/components/thomson/manifest.json +++ b/homeassistant/components/thomson/manifest.json @@ -2,5 +2,6 @@ "domain": "thomson", "name": "Thomson", "documentation": "https://www.home-assistant.io/integrations/thomson", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/threshold/manifest.json b/homeassistant/components/threshold/manifest.json index 6cf871ee8a51a..c4eabcfe6a587 100644 --- a/homeassistant/components/threshold/manifest.json +++ b/homeassistant/components/threshold/manifest.json @@ -3,5 +3,6 @@ "name": "Threshold", "documentation": "https://www.home-assistant.io/integrations/threshold", "codeowners": ["@fabaff"], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_polling" } diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json index 108f05d562515..01a20011befbf 100644 --- a/homeassistant/components/tibber/manifest.json +++ b/homeassistant/components/tibber/manifest.json @@ -5,5 +5,6 @@ "requirements": ["pyTibber==0.16.2"], "codeowners": ["@danielhiversen"], "quality_scale": "silver", - "config_flow": true + "config_flow": true, + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/tikteck/manifest.json b/homeassistant/components/tikteck/manifest.json index 4b64d3852134d..8e332df8f625f 100644 --- a/homeassistant/components/tikteck/manifest.json +++ b/homeassistant/components/tikteck/manifest.json @@ -3,5 +3,6 @@ "name": "Tikteck", "documentation": "https://www.home-assistant.io/integrations/tikteck", "requirements": ["tikteck==0.4"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/tile/manifest.json b/homeassistant/components/tile/manifest.json index 194fc49418ac4..a17c099509eb2 100644 --- a/homeassistant/components/tile/manifest.json +++ b/homeassistant/components/tile/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tile", "requirements": ["pytile==5.2.0"], - "codeowners": ["@bachya"] + "codeowners": ["@bachya"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/time_date/manifest.json b/homeassistant/components/time_date/manifest.json index e3f5c6d3cf4c6..9d4cf0eb2eb29 100644 --- a/homeassistant/components/time_date/manifest.json +++ b/homeassistant/components/time_date/manifest.json @@ -3,5 +3,6 @@ "name": "Time & Date", "documentation": "https://www.home-assistant.io/integrations/time_date", "codeowners": ["@fabaff"], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_push" } diff --git a/homeassistant/components/tmb/manifest.json b/homeassistant/components/tmb/manifest.json index fb4270f641da4..4032b7e27d6c9 100644 --- a/homeassistant/components/tmb/manifest.json +++ b/homeassistant/components/tmb/manifest.json @@ -3,5 +3,6 @@ "name": "Transports Metropolitans de Barcelona", "documentation": "https://www.home-assistant.io/integrations/tmb", "requirements": ["tmb==0.0.4"], - "codeowners": ["@alemuro"] + "codeowners": ["@alemuro"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/tod/manifest.json b/homeassistant/components/tod/manifest.json index d5f62562f8385..b74465e05c369 100644 --- a/homeassistant/components/tod/manifest.json +++ b/homeassistant/components/tod/manifest.json @@ -3,5 +3,6 @@ "name": "Times of the Day", "documentation": "https://www.home-assistant.io/integrations/tod", "codeowners": [], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_push" } diff --git a/homeassistant/components/todoist/manifest.json b/homeassistant/components/todoist/manifest.json index eac7f761c50bf..09cd080b4d770 100644 --- a/homeassistant/components/todoist/manifest.json +++ b/homeassistant/components/todoist/manifest.json @@ -3,5 +3,6 @@ "name": "Todoist", "documentation": "https://www.home-assistant.io/integrations/todoist", "requirements": ["todoist-python==8.0.0"], - "codeowners": ["@boralyl"] + "codeowners": ["@boralyl"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/tof/manifest.json b/homeassistant/components/tof/manifest.json index 8edae0026de85..83a0ba6fbe342 100644 --- a/homeassistant/components/tof/manifest.json +++ b/homeassistant/components/tof/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/tof", "requirements": ["VL53L1X2==0.1.5"], "dependencies": ["rpi_gpio"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/tomato/manifest.json b/homeassistant/components/tomato/manifest.json index 54dd37a63dbc2..9f24187d91dba 100644 --- a/homeassistant/components/tomato/manifest.json +++ b/homeassistant/components/tomato/manifest.json @@ -2,5 +2,6 @@ "domain": "tomato", "name": "Tomato", "documentation": "https://www.home-assistant.io/integrations/tomato", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/toon/manifest.json b/homeassistant/components/toon/manifest.json index f8f9fc1101207..2df5cfa2e905f 100644 --- a/homeassistant/components/toon/manifest.json +++ b/homeassistant/components/toon/manifest.json @@ -7,5 +7,11 @@ "dependencies": ["http"], "after_dependencies": ["cloud"], "codeowners": ["@frenck"], - "dhcp": [{ "hostname": "eneco-*", "macaddress": "74C63B*" }] + "dhcp": [ + { + "hostname": "eneco-*", + "macaddress": "74C63B*" + } + ], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/torque/manifest.json b/homeassistant/components/torque/manifest.json index 5350ae95f2d37..39b01ba712e58 100644 --- a/homeassistant/components/torque/manifest.json +++ b/homeassistant/components/torque/manifest.json @@ -3,5 +3,6 @@ "name": "Torque", "documentation": "https://www.home-assistant.io/integrations/torque", "dependencies": ["http"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/totalconnect/manifest.json b/homeassistant/components/totalconnect/manifest.json index 8a42ca99f035d..3bfba56f92ce5 100644 --- a/homeassistant/components/totalconnect/manifest.json +++ b/homeassistant/components/totalconnect/manifest.json @@ -5,5 +5,6 @@ "requirements": ["total_connect_client==0.57"], "dependencies": [], "codeowners": ["@austinmroczek"], - "config_flow": true + "config_flow": true, + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/touchline/manifest.json b/homeassistant/components/touchline/manifest.json index cbfb7d8583976..1ea02f29ae284 100644 --- a/homeassistant/components/touchline/manifest.json +++ b/homeassistant/components/touchline/manifest.json @@ -3,5 +3,6 @@ "name": "Roth Touchline", "documentation": "https://www.home-assistant.io/integrations/touchline", "requirements": ["pytouchline==0.7"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index 5b49d8ef1b4eb..2cb4b5f369fbb 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -3,11 +3,7 @@ "name": "TP-Link Kasa Smart", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tplink", - "requirements": [ - "pyHS100==0.3.5.2" - ], - "codeowners": [ - "@rytilahti", - "@thegardenmonkey" - ] + "requirements": ["pyHS100==0.3.5.2"], + "codeowners": ["@rytilahti", "@thegardenmonkey"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/tplink_lte/manifest.json b/homeassistant/components/tplink_lte/manifest.json index a2602527b31d6..c18ccbb61067a 100644 --- a/homeassistant/components/tplink_lte/manifest.json +++ b/homeassistant/components/tplink_lte/manifest.json @@ -3,5 +3,6 @@ "name": "TP-Link LTE", "documentation": "https://www.home-assistant.io/integrations/tplink_lte", "requirements": ["tp-connected==0.0.4"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/traccar/manifest.json b/homeassistant/components/traccar/manifest.json index 898113d1b76e9..fd8908a32642f 100644 --- a/homeassistant/components/traccar/manifest.json +++ b/homeassistant/components/traccar/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/traccar", "requirements": ["pytraccar==0.9.0", "stringcase==1.2.0"], "dependencies": ["webhook"], - "codeowners": ["@ludeeus"] + "codeowners": ["@ludeeus"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/trackr/manifest.json b/homeassistant/components/trackr/manifest.json index d59d13102e225..04a629d49c6e4 100644 --- a/homeassistant/components/trackr/manifest.json +++ b/homeassistant/components/trackr/manifest.json @@ -3,5 +3,6 @@ "name": "TrackR", "documentation": "https://www.home-assistant.io/integrations/trackr", "requirements": ["pytrackr==0.0.5"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/tradfri/manifest.json b/homeassistant/components/tradfri/manifest.json index 99b9dff6d2237..3e13cdc015a67 100644 --- a/homeassistant/components/tradfri/manifest.json +++ b/homeassistant/components/tradfri/manifest.json @@ -1,11 +1,12 @@ { "domain": "tradfri", - "name": "IKEA TRÅDFRI", + "name": "IKEA TR\u00c5DFRI", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tradfri", "requirements": ["pytradfri[async]==7.0.6"], "homekit": { "models": ["TRADFRI"] }, - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/trafikverket_train/manifest.json b/homeassistant/components/trafikverket_train/manifest.json index 6104305f66c40..b640d2e59e130 100644 --- a/homeassistant/components/trafikverket_train/manifest.json +++ b/homeassistant/components/trafikverket_train/manifest.json @@ -3,5 +3,6 @@ "name": "Trafikverket Train", "documentation": "https://www.home-assistant.io/integrations/trafikverket_train", "requirements": ["pytrafikverket==0.1.6.2"], - "codeowners": ["@endor-force"] + "codeowners": ["@endor-force"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/trafikverket_weatherstation/manifest.json b/homeassistant/components/trafikverket_weatherstation/manifest.json index 1b3b7ea497a1a..6e123983e8b27 100644 --- a/homeassistant/components/trafikverket_weatherstation/manifest.json +++ b/homeassistant/components/trafikverket_weatherstation/manifest.json @@ -3,5 +3,6 @@ "name": "Trafikverket Weather Station", "documentation": "https://www.home-assistant.io/integrations/trafikverket_weatherstation", "requirements": ["pytrafikverket==0.1.6.2"], - "codeowners": ["@endor-force"] + "codeowners": ["@endor-force"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/transmission/manifest.json b/homeassistant/components/transmission/manifest.json index d0861baafb58c..1f5843e5e6c88 100644 --- a/homeassistant/components/transmission/manifest.json +++ b/homeassistant/components/transmission/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/transmission", "requirements": ["transmissionrpc==0.11"], - "codeowners": ["@engrbm87", "@JPHutchins"] + "codeowners": ["@engrbm87", "@JPHutchins"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/transport_nsw/manifest.json b/homeassistant/components/transport_nsw/manifest.json index 452bad9be8a33..e6670b0e4f6dd 100644 --- a/homeassistant/components/transport_nsw/manifest.json +++ b/homeassistant/components/transport_nsw/manifest.json @@ -3,5 +3,6 @@ "name": "Transport NSW", "documentation": "https://www.home-assistant.io/integrations/transport_nsw", "requirements": ["PyTransportNSW==0.1.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/travisci/manifest.json b/homeassistant/components/travisci/manifest.json index c5f05fb6dae05..c991eecebb2e2 100644 --- a/homeassistant/components/travisci/manifest.json +++ b/homeassistant/components/travisci/manifest.json @@ -3,5 +3,6 @@ "name": "Travis-CI", "documentation": "https://www.home-assistant.io/integrations/travisci", "requirements": ["TravisPy==0.3.5"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/trend/manifest.json b/homeassistant/components/trend/manifest.json index 2bb3719fe9502..594a327f26643 100644 --- a/homeassistant/components/trend/manifest.json +++ b/homeassistant/components/trend/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/trend", "requirements": ["numpy==1.20.2"], "codeowners": [], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_push" } diff --git a/homeassistant/components/tuya/manifest.json b/homeassistant/components/tuya/manifest.json index e72c7c63112cc..52b616a0e8319 100644 --- a/homeassistant/components/tuya/manifest.json +++ b/homeassistant/components/tuya/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/tuya", "requirements": ["tuyaha==0.0.10"], "codeowners": ["@ollo69"], - "config_flow": true + "config_flow": true, + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/twentemilieu/manifest.json b/homeassistant/components/twentemilieu/manifest.json index da4dc0742622c..a56154cba7175 100644 --- a/homeassistant/components/twentemilieu/manifest.json +++ b/homeassistant/components/twentemilieu/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/twentemilieu", "requirements": ["twentemilieu==0.3.0"], - "codeowners": ["@frenck"] + "codeowners": ["@frenck"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/twilio/manifest.json b/homeassistant/components/twilio/manifest.json index c0b4499528116..f34dc5684c3f9 100644 --- a/homeassistant/components/twilio/manifest.json +++ b/homeassistant/components/twilio/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/twilio", "requirements": ["twilio==6.32.0"], "dependencies": ["webhook"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/twilio_call/manifest.json b/homeassistant/components/twilio_call/manifest.json index 133979b18bddd..1317bd9a55868 100644 --- a/homeassistant/components/twilio_call/manifest.json +++ b/homeassistant/components/twilio_call/manifest.json @@ -3,5 +3,6 @@ "name": "Twilio Call", "documentation": "https://www.home-assistant.io/integrations/twilio_call", "dependencies": ["twilio"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/twilio_sms/manifest.json b/homeassistant/components/twilio_sms/manifest.json index d4cde77a80ffc..d8ebdfafef2a4 100644 --- a/homeassistant/components/twilio_sms/manifest.json +++ b/homeassistant/components/twilio_sms/manifest.json @@ -3,5 +3,6 @@ "name": "Twilio SMS", "documentation": "https://www.home-assistant.io/integrations/twilio_sms", "dependencies": ["twilio"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/twinkly/manifest.json b/homeassistant/components/twinkly/manifest.json index c87394ba3bb6c..58c2d9b763b83 100644 --- a/homeassistant/components/twinkly/manifest.json +++ b/homeassistant/components/twinkly/manifest.json @@ -5,5 +5,6 @@ "requirements": ["twinkly-client==0.0.2"], "dependencies": [], "codeowners": ["@dr1rrb"], - "config_flow": true + "config_flow": true, + "iot_class": "local_polling" } diff --git a/homeassistant/components/twitch/manifest.json b/homeassistant/components/twitch/manifest.json index 2fc29fc9be84f..706f2d7ab2cc8 100644 --- a/homeassistant/components/twitch/manifest.json +++ b/homeassistant/components/twitch/manifest.json @@ -3,5 +3,6 @@ "name": "Twitch", "documentation": "https://www.home-assistant.io/integrations/twitch", "requirements": ["python-twitch-client==0.6.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/twitter/manifest.json b/homeassistant/components/twitter/manifest.json index 297f990e9df2c..79d3b58b2bd3d 100644 --- a/homeassistant/components/twitter/manifest.json +++ b/homeassistant/components/twitter/manifest.json @@ -3,5 +3,6 @@ "name": "Twitter", "documentation": "https://www.home-assistant.io/integrations/twitter", "requirements": ["TwitterAPI==2.6.8"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/ubus/manifest.json b/homeassistant/components/ubus/manifest.json index 68452f98f7d72..1c5ca3f5ae12c 100644 --- a/homeassistant/components/ubus/manifest.json +++ b/homeassistant/components/ubus/manifest.json @@ -3,5 +3,6 @@ "name": "OpenWrt (ubus)", "documentation": "https://www.home-assistant.io/integrations/ubus", "requirements": ["openwrt-ubus-rpc==0.0.2"], - "codeowners": ["@noltari"] + "codeowners": ["@noltari"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/ue_smart_radio/manifest.json b/homeassistant/components/ue_smart_radio/manifest.json index 365bb9b822d97..127b6ff76ba8b 100644 --- a/homeassistant/components/ue_smart_radio/manifest.json +++ b/homeassistant/components/ue_smart_radio/manifest.json @@ -2,5 +2,6 @@ "domain": "ue_smart_radio", "name": "Logitech UE Smart Radio", "documentation": "https://www.home-assistant.io/integrations/ue_smart_radio", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/uk_transport/manifest.json b/homeassistant/components/uk_transport/manifest.json index b7200a3599404..6b17a1f4bf67f 100644 --- a/homeassistant/components/uk_transport/manifest.json +++ b/homeassistant/components/uk_transport/manifest.json @@ -2,5 +2,6 @@ "domain": "uk_transport", "name": "UK Transport", "documentation": "https://www.home-assistant.io/integrations/uk_transport", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index cec2d0f859b45..7f70d4c9f372a 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -17,5 +17,6 @@ "deviceType": "urn:schemas-upnp-org:device:InternetGatewayDevice:1", "modelDescription": "UniFi Dream Machine Pro" } - ] + ], + "iot_class": "local_push" } diff --git a/homeassistant/components/unifi_direct/manifest.json b/homeassistant/components/unifi_direct/manifest.json index 206cf39f14970..e901d66acbf24 100644 --- a/homeassistant/components/unifi_direct/manifest.json +++ b/homeassistant/components/unifi_direct/manifest.json @@ -3,5 +3,6 @@ "name": "Ubiquiti UniFi AP", "documentation": "https://www.home-assistant.io/integrations/unifi_direct", "requirements": ["pexpect==4.6.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/unifiled/manifest.json b/homeassistant/components/unifiled/manifest.json index ebbc825578b3f..46656e4cb3d10 100644 --- a/homeassistant/components/unifiled/manifest.json +++ b/homeassistant/components/unifiled/manifest.json @@ -3,5 +3,6 @@ "name": "Ubiquiti UniFi LED", "documentation": "https://www.home-assistant.io/integrations/unifiled", "codeowners": ["@florisvdk"], - "requirements": ["unifiled==0.11"] + "requirements": ["unifiled==0.11"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/universal/manifest.json b/homeassistant/components/universal/manifest.json index ab11e1e0b0771..748f67d7e073d 100644 --- a/homeassistant/components/universal/manifest.json +++ b/homeassistant/components/universal/manifest.json @@ -3,5 +3,6 @@ "name": "Universal Media Player", "documentation": "https://www.home-assistant.io/integrations/universal", "codeowners": [], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "calculated" } diff --git a/homeassistant/components/upb/manifest.json b/homeassistant/components/upb/manifest.json index 9ad43117225f9..75b64806dffd0 100644 --- a/homeassistant/components/upb/manifest.json +++ b/homeassistant/components/upb/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/upb", "requirements": ["upb_lib==0.4.12"], "codeowners": ["@gwww"], - "config_flow": true + "config_flow": true, + "iot_class": "local_push" } diff --git a/homeassistant/components/upc_connect/manifest.json b/homeassistant/components/upc_connect/manifest.json index f34061e276a57..8d5d2c16fbb48 100644 --- a/homeassistant/components/upc_connect/manifest.json +++ b/homeassistant/components/upc_connect/manifest.json @@ -3,5 +3,6 @@ "name": "UPC Connect Box", "documentation": "https://www.home-assistant.io/integrations/upc_connect", "requirements": ["connect-box==0.2.8"], - "codeowners": ["@pvizeli", "@fabaff"] + "codeowners": ["@pvizeli", "@fabaff"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/upcloud/manifest.json b/homeassistant/components/upcloud/manifest.json index f161e273bc361..064cfa224e1b3 100644 --- a/homeassistant/components/upcloud/manifest.json +++ b/homeassistant/components/upcloud/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/upcloud", "requirements": ["upcloud-api==1.0.1"], - "codeowners": ["@scop"] + "codeowners": ["@scop"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/updater/manifest.json b/homeassistant/components/updater/manifest.json index 76a6d8f64f464..9996d2bb1f001 100644 --- a/homeassistant/components/updater/manifest.json +++ b/homeassistant/components/updater/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/updater", "requirements": ["distro==1.5.0"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index feecdb00b1870..50046802e4712 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -12,5 +12,6 @@ { "st": "urn:schemas-upnp-org:device:InternetGatewayDevice:2" } - ] + ], + "iot_class": "local_polling" } diff --git a/homeassistant/components/uptime/manifest.json b/homeassistant/components/uptime/manifest.json index e3d30345dc439..cf2dd1a6ea1fc 100644 --- a/homeassistant/components/uptime/manifest.json +++ b/homeassistant/components/uptime/manifest.json @@ -3,5 +3,6 @@ "name": "Uptime", "documentation": "https://www.home-assistant.io/integrations/uptime", "codeowners": [], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_push" } diff --git a/homeassistant/components/uptimerobot/manifest.json b/homeassistant/components/uptimerobot/manifest.json index 88cbc8ad57fed..414defd55714e 100644 --- a/homeassistant/components/uptimerobot/manifest.json +++ b/homeassistant/components/uptimerobot/manifest.json @@ -3,5 +3,6 @@ "name": "Uptime Robot", "documentation": "https://www.home-assistant.io/integrations/uptimerobot", "requirements": ["pyuptimerobot==0.0.5"], - "codeowners": ["@ludeeus"] + "codeowners": ["@ludeeus"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/uscis/manifest.json b/homeassistant/components/uscis/manifest.json index aabcf344685c0..6ae41e340ab13 100644 --- a/homeassistant/components/uscis/manifest.json +++ b/homeassistant/components/uscis/manifest.json @@ -3,5 +3,6 @@ "name": "U.S. Citizenship and Immigration Services (USCIS)", "documentation": "https://www.home-assistant.io/integrations/uscis", "requirements": ["uscisstatus==0.1.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/usgs_earthquakes_feed/manifest.json b/homeassistant/components/usgs_earthquakes_feed/manifest.json index 4e30ac470d473..ef6fa7a982fbd 100644 --- a/homeassistant/components/usgs_earthquakes_feed/manifest.json +++ b/homeassistant/components/usgs_earthquakes_feed/manifest.json @@ -3,5 +3,6 @@ "name": "U.S. Geological Survey Earthquake Hazards (USGS)", "documentation": "https://www.home-assistant.io/integrations/usgs_earthquakes_feed", "requirements": ["geojson_client==0.4"], - "codeowners": ["@exxamalte"] + "codeowners": ["@exxamalte"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/utility_meter/manifest.json b/homeassistant/components/utility_meter/manifest.json index ff3ce025f0eb4..06f2b60297b00 100644 --- a/homeassistant/components/utility_meter/manifest.json +++ b/homeassistant/components/utility_meter/manifest.json @@ -3,5 +3,6 @@ "name": "Utility Meter", "documentation": "https://www.home-assistant.io/integrations/utility_meter", "codeowners": ["@dgomes"], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_push" } diff --git a/homeassistant/components/uvc/manifest.json b/homeassistant/components/uvc/manifest.json index b44cdd274b403..507ee518454a3 100644 --- a/homeassistant/components/uvc/manifest.json +++ b/homeassistant/components/uvc/manifest.json @@ -3,5 +3,6 @@ "name": "Ubiquiti UniFi Video", "documentation": "https://www.home-assistant.io/integrations/uvc", "requirements": ["uvcclient==0.11.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/vallox/manifest.json b/homeassistant/components/vallox/manifest.json index 7a95965452581..14845f97c1c8d 100644 --- a/homeassistant/components/vallox/manifest.json +++ b/homeassistant/components/vallox/manifest.json @@ -3,5 +3,6 @@ "name": "Vallox", "documentation": "https://www.home-assistant.io/integrations/vallox", "requirements": ["vallox-websocket-api==2.4.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/vasttrafik/manifest.json b/homeassistant/components/vasttrafik/manifest.json index 59e655c94f22f..965e84435db7d 100644 --- a/homeassistant/components/vasttrafik/manifest.json +++ b/homeassistant/components/vasttrafik/manifest.json @@ -1,7 +1,8 @@ { "domain": "vasttrafik", - "name": "Västtrafik", + "name": "V\u00e4sttrafik", "documentation": "https://www.home-assistant.io/integrations/vasttrafik", "requirements": ["vtjp==0.1.14"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json index 2e1612554b543..ba99415944d14 100644 --- a/homeassistant/components/velbus/manifest.json +++ b/homeassistant/components/velbus/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/velbus", "requirements": ["python-velbus==2.1.2"], "config_flow": true, - "codeowners": ["@Cereal2nd", "@brefra"] + "codeowners": ["@Cereal2nd", "@brefra"], + "iot_class": "local_push" } diff --git a/homeassistant/components/velux/manifest.json b/homeassistant/components/velux/manifest.json index a0893b49e4480..43be9b424a846 100644 --- a/homeassistant/components/velux/manifest.json +++ b/homeassistant/components/velux/manifest.json @@ -3,5 +3,6 @@ "name": "Velux", "documentation": "https://www.home-assistant.io/integrations/velux", "requirements": ["pyvlx==0.2.18"], - "codeowners": ["@Julius2342"] + "codeowners": ["@Julius2342"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/venstar/manifest.json b/homeassistant/components/venstar/manifest.json index 68f762a54fcd6..0baa1e56cfa52 100644 --- a/homeassistant/components/venstar/manifest.json +++ b/homeassistant/components/venstar/manifest.json @@ -3,5 +3,6 @@ "name": "Venstar", "documentation": "https://www.home-assistant.io/integrations/venstar", "requirements": ["venstarcolortouch==0.13"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/vera/manifest.json b/homeassistant/components/vera/manifest.json index 76d6bda5c7b30..84cf9eac007c2 100644 --- a/homeassistant/components/vera/manifest.json +++ b/homeassistant/components/vera/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/vera", "requirements": ["pyvera==0.3.13"], - "codeowners": ["@pavoni"] + "codeowners": ["@pavoni"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/verisure/manifest.json b/homeassistant/components/verisure/manifest.json index 074ef4f955ccf..0bd04961ec72c 100644 --- a/homeassistant/components/verisure/manifest.json +++ b/homeassistant/components/verisure/manifest.json @@ -5,5 +5,10 @@ "requirements": ["vsure==1.7.3"], "codeowners": ["@frenck"], "config_flow": true, - "dhcp": [{ "macaddress": "0023C1*" }] + "dhcp": [ + { + "macaddress": "0023C1*" + } + ], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/versasense/manifest.json b/homeassistant/components/versasense/manifest.json index bd409b5977f3b..470177997d040 100644 --- a/homeassistant/components/versasense/manifest.json +++ b/homeassistant/components/versasense/manifest.json @@ -3,5 +3,6 @@ "name": "VersaSense", "documentation": "https://www.home-assistant.io/integrations/versasense", "codeowners": ["@flamm3blemuff1n"], - "requirements": ["pyversasense==0.0.6"] + "requirements": ["pyversasense==0.0.6"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/version/manifest.json b/homeassistant/components/version/manifest.json index 7f55273383dc1..880b000bc43f2 100644 --- a/homeassistant/components/version/manifest.json +++ b/homeassistant/components/version/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/version", "requirements": ["pyhaversion==21.3.0"], "codeowners": ["@fabaff", "@ludeeus"], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_push" } diff --git a/homeassistant/components/vesync/manifest.json b/homeassistant/components/vesync/manifest.json index 6aa7a5774fd88..f09a58e469680 100644 --- a/homeassistant/components/vesync/manifest.json +++ b/homeassistant/components/vesync/manifest.json @@ -2,13 +2,8 @@ "domain": "vesync", "name": "VeSync", "documentation": "https://www.home-assistant.io/integrations/vesync", - "codeowners": [ - "@markperdue", - "@webdjoe", - "@thegardenmonkey" - ], - "requirements": [ - "pyvesync==1.3.1" - ], - "config_flow": true + "codeowners": ["@markperdue", "@webdjoe", "@thegardenmonkey"], + "requirements": ["pyvesync==1.3.1"], + "config_flow": true, + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/viaggiatreno/manifest.json b/homeassistant/components/viaggiatreno/manifest.json index b4eb145f315b0..40059770af2cf 100644 --- a/homeassistant/components/viaggiatreno/manifest.json +++ b/homeassistant/components/viaggiatreno/manifest.json @@ -2,5 +2,6 @@ "domain": "viaggiatreno", "name": "Trenitalia ViaggiaTreno", "documentation": "https://www.home-assistant.io/integrations/viaggiatreno", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/vicare/manifest.json b/homeassistant/components/vicare/manifest.json index 2eb40645e5869..400618c3e8568 100644 --- a/homeassistant/components/vicare/manifest.json +++ b/homeassistant/components/vicare/manifest.json @@ -3,5 +3,6 @@ "name": "Viessmann ViCare", "documentation": "https://www.home-assistant.io/integrations/vicare", "codeowners": ["@oischinger"], - "requirements": ["PyViCare==0.2.5"] + "requirements": ["PyViCare==0.2.5"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/vilfo/manifest.json b/homeassistant/components/vilfo/manifest.json index 4dba1a5687e41..568db1afdc04a 100644 --- a/homeassistant/components/vilfo/manifest.json +++ b/homeassistant/components/vilfo/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/vilfo", "requirements": ["vilfo-api-client==0.3.2"], - "codeowners": ["@ManneW"] + "codeowners": ["@ManneW"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/vivotek/manifest.json b/homeassistant/components/vivotek/manifest.json index 5d1b8cedd7bbf..c3a48b304021d 100644 --- a/homeassistant/components/vivotek/manifest.json +++ b/homeassistant/components/vivotek/manifest.json @@ -3,5 +3,6 @@ "name": "VIVOTEK", "documentation": "https://www.home-assistant.io/integrations/vivotek", "requirements": ["libpyvivotek==0.4.0"], - "codeowners": ["@HarlemSquirrel"] + "codeowners": ["@HarlemSquirrel"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/vizio/manifest.json b/homeassistant/components/vizio/manifest.json index 9e4bd712e0f41..f686a6ac1fcee 100644 --- a/homeassistant/components/vizio/manifest.json +++ b/homeassistant/components/vizio/manifest.json @@ -6,5 +6,6 @@ "codeowners": ["@raman325"], "config_flow": true, "zeroconf": ["_viziocast._tcp.local."], - "quality_scale": "platinum" + "quality_scale": "platinum", + "iot_class": "local_polling" } diff --git a/homeassistant/components/vlc/manifest.json b/homeassistant/components/vlc/manifest.json index 6a79e542be267..a228bb2353570 100644 --- a/homeassistant/components/vlc/manifest.json +++ b/homeassistant/components/vlc/manifest.json @@ -3,5 +3,6 @@ "name": "VLC media player", "documentation": "https://www.home-assistant.io/integrations/vlc", "requirements": ["python-vlc==1.1.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/vlc_telnet/manifest.json b/homeassistant/components/vlc_telnet/manifest.json index 37941e1545857..1aa41fb9bb9d1 100644 --- a/homeassistant/components/vlc_telnet/manifest.json +++ b/homeassistant/components/vlc_telnet/manifest.json @@ -3,5 +3,6 @@ "name": "VLC media player Telnet", "documentation": "https://www.home-assistant.io/integrations/vlc-telnet", "requirements": ["python-telnet-vlc==2.0.1"], - "codeowners": ["@rodripf", "@dmcc"] + "codeowners": ["@rodripf", "@dmcc"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/voicerss/manifest.json b/homeassistant/components/voicerss/manifest.json index ff9d194a270c9..d2772f2aacfe1 100644 --- a/homeassistant/components/voicerss/manifest.json +++ b/homeassistant/components/voicerss/manifest.json @@ -2,5 +2,6 @@ "domain": "voicerss", "name": "VoiceRSS", "documentation": "https://www.home-assistant.io/integrations/voicerss", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/volkszaehler/manifest.json b/homeassistant/components/volkszaehler/manifest.json index 937c589bdf4b2..11624da7f5329 100644 --- a/homeassistant/components/volkszaehler/manifest.json +++ b/homeassistant/components/volkszaehler/manifest.json @@ -3,5 +3,6 @@ "name": "Volkszaehler", "documentation": "https://www.home-assistant.io/integrations/volkszaehler", "requirements": ["volkszaehler==0.2.1"], - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/volumio/manifest.json b/homeassistant/components/volumio/manifest.json index a12b96e7bcae4..0daffe1cc0a20 100644 --- a/homeassistant/components/volumio/manifest.json +++ b/homeassistant/components/volumio/manifest.json @@ -5,5 +5,6 @@ "codeowners": ["@OnFreund"], "config_flow": true, "zeroconf": ["_Volumio._tcp.local."], - "requirements": ["pyvolumio==0.1.3"] -} \ No newline at end of file + "requirements": ["pyvolumio==0.1.3"], + "iot_class": "local_polling" +} diff --git a/homeassistant/components/volvooncall/manifest.json b/homeassistant/components/volvooncall/manifest.json index 822e7eef5a818..5201614ab8bd5 100644 --- a/homeassistant/components/volvooncall/manifest.json +++ b/homeassistant/components/volvooncall/manifest.json @@ -3,5 +3,6 @@ "name": "Volvo On Call", "documentation": "https://www.home-assistant.io/integrations/volvooncall", "requirements": ["volvooncall==0.8.12"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/vultr/manifest.json b/homeassistant/components/vultr/manifest.json index 596e37c3545a2..0fbd4e2ebe4da 100644 --- a/homeassistant/components/vultr/manifest.json +++ b/homeassistant/components/vultr/manifest.json @@ -3,5 +3,6 @@ "name": "Vultr", "documentation": "https://www.home-assistant.io/integrations/vultr", "requirements": ["vultr==0.1.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/w800rf32/manifest.json b/homeassistant/components/w800rf32/manifest.json index c93d25dcf4601..6089c00be489a 100644 --- a/homeassistant/components/w800rf32/manifest.json +++ b/homeassistant/components/w800rf32/manifest.json @@ -3,5 +3,6 @@ "name": "WGL Designs W800RF32", "documentation": "https://www.home-assistant.io/integrations/w800rf32", "requirements": ["pyW800rf32==0.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/wake_on_lan/manifest.json b/homeassistant/components/wake_on_lan/manifest.json index 8ca0389bea0cd..e959f4b33f3ee 100644 --- a/homeassistant/components/wake_on_lan/manifest.json +++ b/homeassistant/components/wake_on_lan/manifest.json @@ -3,5 +3,6 @@ "name": "Wake on LAN", "documentation": "https://www.home-assistant.io/integrations/wake_on_lan", "requirements": ["wakeonlan==2.0.1"], - "codeowners": ["@ntilley905"] + "codeowners": ["@ntilley905"], + "iot_class": "local_push" } diff --git a/homeassistant/components/waqi/manifest.json b/homeassistant/components/waqi/manifest.json index 947d0089f4b35..48f812f447a9e 100644 --- a/homeassistant/components/waqi/manifest.json +++ b/homeassistant/components/waqi/manifest.json @@ -3,5 +3,6 @@ "name": "World Air Quality Index (WAQI)", "documentation": "https://www.home-assistant.io/integrations/waqi", "requirements": ["waqiasync==1.0.0"], - "codeowners": ["@andrey-git"] + "codeowners": ["@andrey-git"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/waterfurnace/manifest.json b/homeassistant/components/waterfurnace/manifest.json index 6ccd2382db9e4..82f60abbd64e9 100644 --- a/homeassistant/components/waterfurnace/manifest.json +++ b/homeassistant/components/waterfurnace/manifest.json @@ -3,5 +3,6 @@ "name": "WaterFurnace", "documentation": "https://www.home-assistant.io/integrations/waterfurnace", "requirements": ["waterfurnace==1.1.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/watson_iot/manifest.json b/homeassistant/components/watson_iot/manifest.json index f735b4007e10b..95f5b3c7d0a97 100644 --- a/homeassistant/components/watson_iot/manifest.json +++ b/homeassistant/components/watson_iot/manifest.json @@ -3,5 +3,6 @@ "name": "IBM Watson IoT Platform", "documentation": "https://www.home-assistant.io/integrations/watson_iot", "requirements": ["ibmiotf==0.3.4"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/watson_tts/manifest.json b/homeassistant/components/watson_tts/manifest.json index 78d5613e16db2..e833ac02638b4 100644 --- a/homeassistant/components/watson_tts/manifest.json +++ b/homeassistant/components/watson_tts/manifest.json @@ -3,5 +3,6 @@ "name": "IBM Watson TTS", "documentation": "https://www.home-assistant.io/integrations/watson_tts", "requirements": ["ibm-watson==4.0.1"], - "codeowners": ["@rutkai"] + "codeowners": ["@rutkai"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/waze_travel_time/manifest.json b/homeassistant/components/waze_travel_time/manifest.json index d3058b7b783f7..24927ac9ae3f0 100644 --- a/homeassistant/components/waze_travel_time/manifest.json +++ b/homeassistant/components/waze_travel_time/manifest.json @@ -2,9 +2,8 @@ "domain": "waze_travel_time", "name": "Waze Travel Time", "documentation": "https://www.home-assistant.io/integrations/waze_travel_time", - "requirements": [ - "WazeRouteCalculator==0.12" - ], + "requirements": ["WazeRouteCalculator==0.12"], "codeowners": [], - "config_flow": true + "config_flow": true, + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/webostv/manifest.json b/homeassistant/components/webostv/manifest.json index 7773e9c496332..b14fd793cabda 100644 --- a/homeassistant/components/webostv/manifest.json +++ b/homeassistant/components/webostv/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/webostv", "requirements": ["aiopylgtv==0.4.0"], "dependencies": ["configurator"], - "codeowners": ["@bendavid"] + "codeowners": ["@bendavid"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/wemo/manifest.json b/homeassistant/components/wemo/manifest.json index 9d91ab7ef967d..bd153294282d9 100644 --- a/homeassistant/components/wemo/manifest.json +++ b/homeassistant/components/wemo/manifest.json @@ -12,5 +12,6 @@ "homekit": { "models": ["Socket", "Wemo"] }, - "codeowners": ["@esev"] + "codeowners": ["@esev"], + "iot_class": "local_push" } diff --git a/homeassistant/components/whois/manifest.json b/homeassistant/components/whois/manifest.json index 39cc1c194c86c..f591d7bb47824 100644 --- a/homeassistant/components/whois/manifest.json +++ b/homeassistant/components/whois/manifest.json @@ -3,5 +3,6 @@ "name": "Whois", "documentation": "https://www.home-assistant.io/integrations/whois", "requirements": ["python-whois==0.7.3"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/wiffi/manifest.json b/homeassistant/components/wiffi/manifest.json index 2259b1a620e38..803c5f7e52038 100644 --- a/homeassistant/components/wiffi/manifest.json +++ b/homeassistant/components/wiffi/manifest.json @@ -4,7 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wiffi", "requirements": ["wiffi==1.0.1"], - "codeowners": [ - "@mampfes" - ] + "codeowners": ["@mampfes"], + "iot_class": "local_push" } diff --git a/homeassistant/components/wilight/manifest.json b/homeassistant/components/wilight/manifest.json index 5b8a93c60392f..689a37f3c915b 100644 --- a/homeassistant/components/wilight/manifest.json +++ b/homeassistant/components/wilight/manifest.json @@ -10,5 +10,6 @@ } ], "codeowners": ["@leofig-rj"], - "quality_scale": "silver" + "quality_scale": "silver", + "iot_class": "local_polling" } diff --git a/homeassistant/components/wink/manifest.json b/homeassistant/components/wink/manifest.json index 7d357d88e553a..e4da7b9c03acb 100644 --- a/homeassistant/components/wink/manifest.json +++ b/homeassistant/components/wink/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/wink", "requirements": ["pubnubsub-handler==1.0.9", "python-wink==1.10.5"], "dependencies": ["configurator", "http"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/wirelesstag/manifest.json b/homeassistant/components/wirelesstag/manifest.json index 97205e6fc9d41..fd18235c9940b 100644 --- a/homeassistant/components/wirelesstag/manifest.json +++ b/homeassistant/components/wirelesstag/manifest.json @@ -3,5 +3,6 @@ "name": "Wireless Sensor Tags", "documentation": "https://www.home-assistant.io/integrations/wirelesstag", "requirements": ["wirelesstagpy==0.4.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/withings/manifest.json b/homeassistant/components/withings/manifest.json index 6b2918722ba01..d1c867cd4e645 100644 --- a/homeassistant/components/withings/manifest.json +++ b/homeassistant/components/withings/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/withings", "requirements": ["withings-api==2.3.2"], "dependencies": ["http", "webhook"], - "codeowners": ["@vangorra"] + "codeowners": ["@vangorra"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/wled/manifest.json b/homeassistant/components/wled/manifest.json index a646c41d832da..b076889707698 100644 --- a/homeassistant/components/wled/manifest.json +++ b/homeassistant/components/wled/manifest.json @@ -6,5 +6,6 @@ "requirements": ["wled==0.4.4"], "zeroconf": ["_wled._tcp.local."], "codeowners": ["@frenck"], - "quality_scale": "platinum" + "quality_scale": "platinum", + "iot_class": "local_polling" } diff --git a/homeassistant/components/wolflink/manifest.json b/homeassistant/components/wolflink/manifest.json index 6d038d4fb29e4..504419ef0f429 100644 --- a/homeassistant/components/wolflink/manifest.json +++ b/homeassistant/components/wolflink/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wolflink", "requirements": ["wolf_smartset==0.1.8"], - "codeowners": ["@adamkrol93"] + "codeowners": ["@adamkrol93"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index b87704cde679c..6fc8d2328a19a 100644 --- a/homeassistant/components/workday/manifest.json +++ b/homeassistant/components/workday/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/workday", "requirements": ["holidays==0.11.1"], "codeowners": ["@fabaff"], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_polling" } diff --git a/homeassistant/components/worldclock/manifest.json b/homeassistant/components/worldclock/manifest.json index 4f13e8fba905f..cc58003dadc04 100644 --- a/homeassistant/components/worldclock/manifest.json +++ b/homeassistant/components/worldclock/manifest.json @@ -3,5 +3,6 @@ "name": "Worldclock", "documentation": "https://www.home-assistant.io/integrations/worldclock", "codeowners": ["@fabaff"], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_push" } diff --git a/homeassistant/components/worldtidesinfo/manifest.json b/homeassistant/components/worldtidesinfo/manifest.json index b4c3d9509d4bf..b2b95af105a7b 100644 --- a/homeassistant/components/worldtidesinfo/manifest.json +++ b/homeassistant/components/worldtidesinfo/manifest.json @@ -2,5 +2,6 @@ "domain": "worldtidesinfo", "name": "World Tides", "documentation": "https://www.home-assistant.io/integrations/worldtidesinfo", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/worxlandroid/manifest.json b/homeassistant/components/worxlandroid/manifest.json index a8a722ff93e34..82a16fd92ca6e 100644 --- a/homeassistant/components/worxlandroid/manifest.json +++ b/homeassistant/components/worxlandroid/manifest.json @@ -2,5 +2,6 @@ "domain": "worxlandroid", "name": "Worx Landroid", "documentation": "https://www.home-assistant.io/integrations/worxlandroid", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/wsdot/manifest.json b/homeassistant/components/wsdot/manifest.json index 386b14a3a6a35..f731c3a7d526b 100644 --- a/homeassistant/components/wsdot/manifest.json +++ b/homeassistant/components/wsdot/manifest.json @@ -2,5 +2,6 @@ "domain": "wsdot", "name": "Washington State Department of Transportation (WSDOT)", "documentation": "https://www.home-assistant.io/integrations/wsdot", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/wunderground/manifest.json b/homeassistant/components/wunderground/manifest.json index 85f3be460293b..b932d9ac403fc 100644 --- a/homeassistant/components/wunderground/manifest.json +++ b/homeassistant/components/wunderground/manifest.json @@ -2,5 +2,6 @@ "domain": "wunderground", "name": "Weather Underground (WUnderground)", "documentation": "https://www.home-assistant.io/integrations/wunderground", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/x10/manifest.json b/homeassistant/components/x10/manifest.json index ce51fcac0cac4..249d42e4d8074 100644 --- a/homeassistant/components/x10/manifest.json +++ b/homeassistant/components/x10/manifest.json @@ -2,5 +2,6 @@ "domain": "x10", "name": "Heyu X10", "documentation": "https://www.home-assistant.io/integrations/x10", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/xbee/manifest.json b/homeassistant/components/xbee/manifest.json index 9d70751e230e8..fbf9cc925baf4 100644 --- a/homeassistant/components/xbee/manifest.json +++ b/homeassistant/components/xbee/manifest.json @@ -3,5 +3,6 @@ "name": "XBee", "documentation": "https://www.home-assistant.io/integrations/xbee", "requirements": ["xbee-helper==0.0.7"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/xbox/manifest.json b/homeassistant/components/xbox/manifest.json index b410c32465c2b..64cda6055c0b0 100644 --- a/homeassistant/components/xbox/manifest.json +++ b/homeassistant/components/xbox/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/xbox", "requirements": ["xbox-webapi==2.0.8"], "dependencies": ["http"], - "codeowners": ["@hunterjm"] + "codeowners": ["@hunterjm"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/xbox_live/manifest.json b/homeassistant/components/xbox_live/manifest.json index 937f33bd00942..94ebef9f24177 100644 --- a/homeassistant/components/xbox_live/manifest.json +++ b/homeassistant/components/xbox_live/manifest.json @@ -3,5 +3,6 @@ "name": "Xbox Live", "documentation": "https://www.home-assistant.io/integrations/xbox_live", "requirements": ["xboxapi==2.0.1"], - "codeowners": ["@MartinHjelmare"] + "codeowners": ["@MartinHjelmare"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/xeoma/manifest.json b/homeassistant/components/xeoma/manifest.json index 9fb6cb8b598f3..e235d35237f0f 100644 --- a/homeassistant/components/xeoma/manifest.json +++ b/homeassistant/components/xeoma/manifest.json @@ -3,5 +3,6 @@ "name": "Xeoma", "documentation": "https://www.home-assistant.io/integrations/xeoma", "requirements": ["pyxeoma==1.4.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/xiaomi/manifest.json b/homeassistant/components/xiaomi/manifest.json index 407406228a5d0..37f9488d5c87f 100644 --- a/homeassistant/components/xiaomi/manifest.json +++ b/homeassistant/components/xiaomi/manifest.json @@ -3,5 +3,6 @@ "name": "Xiaomi", "documentation": "https://www.home-assistant.io/integrations/xiaomi", "dependencies": ["ffmpeg"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/xiaomi_aqara/manifest.json b/homeassistant/components/xiaomi_aqara/manifest.json index eb115b6471dad..13444c6ad69b1 100644 --- a/homeassistant/components/xiaomi_aqara/manifest.json +++ b/homeassistant/components/xiaomi_aqara/manifest.json @@ -6,5 +6,6 @@ "requirements": ["PyXiaomiGateway==0.13.4"], "after_dependencies": ["discovery"], "codeowners": ["@danielhiversen", "@syssi"], - "zeroconf": ["_miio._udp.local."] + "zeroconf": ["_miio._udp.local."], + "iot_class": "local_push" } diff --git a/homeassistant/components/xiaomi_miio/manifest.json b/homeassistant/components/xiaomi_miio/manifest.json index 6f8069be681b8..6566270041a1e 100644 --- a/homeassistant/components/xiaomi_miio/manifest.json +++ b/homeassistant/components/xiaomi_miio/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/xiaomi_miio", "requirements": ["construct==2.10.56", "python-miio==0.5.5"], "codeowners": ["@rytilahti", "@syssi", "@starkillerOG"], - "zeroconf": ["_miio._udp.local."] + "zeroconf": ["_miio._udp.local."], + "iot_class": "local_polling" } diff --git a/homeassistant/components/xiaomi_tv/manifest.json b/homeassistant/components/xiaomi_tv/manifest.json index 3c901ca753a08..85fbbef7928a3 100644 --- a/homeassistant/components/xiaomi_tv/manifest.json +++ b/homeassistant/components/xiaomi_tv/manifest.json @@ -3,5 +3,6 @@ "name": "Xiaomi TV", "documentation": "https://www.home-assistant.io/integrations/xiaomi_tv", "requirements": ["pymitv==1.4.3"], - "codeowners": ["@simse"] + "codeowners": ["@simse"], + "iot_class": "assumed_state" } diff --git a/homeassistant/components/xmpp/manifest.json b/homeassistant/components/xmpp/manifest.json index ced8bd19e4090..46acec9e5674d 100644 --- a/homeassistant/components/xmpp/manifest.json +++ b/homeassistant/components/xmpp/manifest.json @@ -3,5 +3,6 @@ "name": "Jabber (XMPP)", "documentation": "https://www.home-assistant.io/integrations/xmpp", "requirements": ["slixmpp==1.7.0"], - "codeowners": ["@fabaff", "@flowolf"] + "codeowners": ["@fabaff", "@flowolf"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/xs1/manifest.json b/homeassistant/components/xs1/manifest.json index 164f571fade2a..4cb5770bed7c0 100644 --- a/homeassistant/components/xs1/manifest.json +++ b/homeassistant/components/xs1/manifest.json @@ -3,5 +3,6 @@ "name": "EZcontrol XS1", "documentation": "https://www.home-assistant.io/integrations/xs1", "requirements": ["xs1-api-client==3.0.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/yale_smart_alarm/manifest.json b/homeassistant/components/yale_smart_alarm/manifest.json index b465125508cd7..fd1fa3bee2361 100644 --- a/homeassistant/components/yale_smart_alarm/manifest.json +++ b/homeassistant/components/yale_smart_alarm/manifest.json @@ -3,5 +3,6 @@ "name": "Yale Smart Living", "documentation": "https://www.home-assistant.io/integrations/yale_smart_alarm", "requirements": ["yalesmartalarmclient==0.1.6"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/yamaha/manifest.json b/homeassistant/components/yamaha/manifest.json index e2f2ed9878305..46752fee69909 100644 --- a/homeassistant/components/yamaha/manifest.json +++ b/homeassistant/components/yamaha/manifest.json @@ -3,5 +3,6 @@ "name": "Yamaha Network Receivers", "documentation": "https://www.home-assistant.io/integrations/yamaha", "requirements": ["rxv==0.6.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/yamaha_musiccast/manifest.json b/homeassistant/components/yamaha_musiccast/manifest.json index 4c3a35c15dcbf..4a0294f444cb1 100644 --- a/homeassistant/components/yamaha_musiccast/manifest.json +++ b/homeassistant/components/yamaha_musiccast/manifest.json @@ -3,5 +3,6 @@ "name": "Yamaha MusicCast", "documentation": "https://www.home-assistant.io/integrations/yamaha_musiccast", "requirements": ["pymusiccast==0.1.6"], - "codeowners": ["@jalmeroth"] + "codeowners": ["@jalmeroth"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/yandex_transport/manifest.json b/homeassistant/components/yandex_transport/manifest.json index b8afe738a0743..79818f8e63eea 100644 --- a/homeassistant/components/yandex_transport/manifest.json +++ b/homeassistant/components/yandex_transport/manifest.json @@ -3,5 +3,6 @@ "name": "Yandex Transport", "documentation": "https://www.home-assistant.io/integrations/yandex_transport", "requirements": ["aioymaps==1.1.0"], - "codeowners": ["@rishatik92", "@devbis"] + "codeowners": ["@rishatik92", "@devbis"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/yandextts/manifest.json b/homeassistant/components/yandextts/manifest.json index 2769b5fc177a1..d6e3cb60b3709 100644 --- a/homeassistant/components/yandextts/manifest.json +++ b/homeassistant/components/yandextts/manifest.json @@ -2,5 +2,6 @@ "domain": "yandextts", "name": "Yandex TTS", "documentation": "https://www.home-assistant.io/integrations/yandextts", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 25909c74443b9..845d9314bda46 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -2,13 +2,8 @@ "domain": "yeelight", "name": "Yeelight", "documentation": "https://www.home-assistant.io/integrations/yeelight", - "requirements": [ - "yeelight==0.6.0" - ], - "codeowners": [ - "@rytilahti", - "@zewelor", - "@shenxn" - ], - "config_flow": true -} \ No newline at end of file + "requirements": ["yeelight==0.6.0"], + "codeowners": ["@rytilahti", "@zewelor", "@shenxn"], + "config_flow": true, + "iot_class": "local_polling" +} diff --git a/homeassistant/components/yeelightsunflower/manifest.json b/homeassistant/components/yeelightsunflower/manifest.json index 4c21e8e6f2691..17156ae3490ac 100644 --- a/homeassistant/components/yeelightsunflower/manifest.json +++ b/homeassistant/components/yeelightsunflower/manifest.json @@ -3,5 +3,6 @@ "name": "Yeelight Sunflower", "documentation": "https://www.home-assistant.io/integrations/yeelightsunflower", "requirements": ["yeelightsunflower==0.0.10"], - "codeowners": ["@lindsaymarkward"] + "codeowners": ["@lindsaymarkward"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/yi/manifest.json b/homeassistant/components/yi/manifest.json index f14d7ad742b09..140b1cf3132ea 100644 --- a/homeassistant/components/yi/manifest.json +++ b/homeassistant/components/yi/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/yi", "requirements": ["aioftp==0.12.0"], "dependencies": ["ffmpeg"], - "codeowners": ["@bachya"] + "codeowners": ["@bachya"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/zabbix/manifest.json b/homeassistant/components/zabbix/manifest.json index 5ed2e7c163d0c..39f8ebae4aeae 100644 --- a/homeassistant/components/zabbix/manifest.json +++ b/homeassistant/components/zabbix/manifest.json @@ -3,5 +3,6 @@ "name": "Zabbix", "documentation": "https://www.home-assistant.io/integrations/zabbix", "requirements": ["py-zabbix==1.1.7"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/zamg/manifest.json b/homeassistant/components/zamg/manifest.json index c2c03145f6008..fc4345141896d 100644 --- a/homeassistant/components/zamg/manifest.json +++ b/homeassistant/components/zamg/manifest.json @@ -1,6 +1,7 @@ { "domain": "zamg", - "name": "Zentralanstalt für Meteorologie und Geodynamik (ZAMG)", + "name": "Zentralanstalt f\u00fcr Meteorologie und Geodynamik (ZAMG)", "documentation": "https://www.home-assistant.io/integrations/zamg", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/zengge/manifest.json b/homeassistant/components/zengge/manifest.json index fc76517086030..45cf866f51f25 100644 --- a/homeassistant/components/zengge/manifest.json +++ b/homeassistant/components/zengge/manifest.json @@ -3,5 +3,6 @@ "name": "Zengge", "documentation": "https://www.home-assistant.io/integrations/zengge", "requirements": ["zengge==0.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index d407acece5746..149033c4acb6c 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -5,5 +5,6 @@ "requirements": ["zeroconf==0.29.0"], "dependencies": ["api"], "codeowners": ["@bdraco"], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_push" } diff --git a/homeassistant/components/zerproc/manifest.json b/homeassistant/components/zerproc/manifest.json index d2d00987ab7a3..dfaf6587d3b18 100644 --- a/homeassistant/components/zerproc/manifest.json +++ b/homeassistant/components/zerproc/manifest.json @@ -3,10 +3,7 @@ "name": "Zerproc", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zerproc", - "requirements": [ - "pyzerproc==0.4.8" - ], - "codeowners": [ - "@emlove" - ] + "requirements": ["pyzerproc==0.4.8"], + "codeowners": ["@emlove"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/zestimate/manifest.json b/homeassistant/components/zestimate/manifest.json index 9df1c3f7b91f7..4fee44ffcac88 100644 --- a/homeassistant/components/zestimate/manifest.json +++ b/homeassistant/components/zestimate/manifest.json @@ -3,5 +3,6 @@ "name": "Zestimate", "documentation": "https://www.home-assistant.io/integrations/zestimate", "requirements": ["xmltodict==0.12.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 5cd57e2627413..3e99f971e8885 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -16,6 +16,12 @@ "zigpy-znp==0.4.0" ], "codeowners": ["@dmulcahey", "@adminiuga"], - "zeroconf": [{ "type": "_esphomelib._tcp.local.", "name": "tube*" }], - "after_dependencies": ["zeroconf"] + "zeroconf": [ + { + "type": "_esphomelib._tcp.local.", + "name": "tube*" + } + ], + "after_dependencies": ["zeroconf"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/zhong_hong/manifest.json b/homeassistant/components/zhong_hong/manifest.json index f2caf269258e2..c57e23507c97e 100644 --- a/homeassistant/components/zhong_hong/manifest.json +++ b/homeassistant/components/zhong_hong/manifest.json @@ -3,5 +3,6 @@ "name": "ZhongHong", "documentation": "https://www.home-assistant.io/integrations/zhong_hong", "requirements": ["zhong_hong_hvac==1.0.9"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/ziggo_mediabox_xl/manifest.json b/homeassistant/components/ziggo_mediabox_xl/manifest.json index ccc5e260eaf71..e2a0dc94d5503 100644 --- a/homeassistant/components/ziggo_mediabox_xl/manifest.json +++ b/homeassistant/components/ziggo_mediabox_xl/manifest.json @@ -3,5 +3,6 @@ "name": "Ziggo Mediabox XL", "documentation": "https://www.home-assistant.io/integrations/ziggo_mediabox_xl", "requirements": ["ziggo-mediabox-xl==1.1.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/zodiac/manifest.json b/homeassistant/components/zodiac/manifest.json index 9d38c2cff3913..45fcb762ed223 100644 --- a/homeassistant/components/zodiac/manifest.json +++ b/homeassistant/components/zodiac/manifest.json @@ -3,5 +3,6 @@ "name": "Zodiac", "documentation": "https://www.home-assistant.io/integrations/zodiac", "codeowners": ["@JulienTant"], - "quality_scale": "silver" + "quality_scale": "silver", + "iot_class": "local_polling" } diff --git a/homeassistant/components/zoneminder/manifest.json b/homeassistant/components/zoneminder/manifest.json index 039513f100e37..92324f338b552 100644 --- a/homeassistant/components/zoneminder/manifest.json +++ b/homeassistant/components/zoneminder/manifest.json @@ -3,5 +3,6 @@ "name": "ZoneMinder", "documentation": "https://www.home-assistant.io/integrations/zoneminder", "requirements": ["zm-py==0.5.2"], - "codeowners": ["@rohankapoorcom"] + "codeowners": ["@rohankapoorcom"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/zwave/manifest.json b/homeassistant/components/zwave/manifest.json index 6623036d2fea9..f65dbb557dbe4 100644 --- a/homeassistant/components/zwave/manifest.json +++ b/homeassistant/components/zwave/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/zwave", "requirements": ["homeassistant-pyozw==0.1.10", "pydispatcher==2.0.5"], "after_dependencies": ["ozw"], - "codeowners": ["@home-assistant/z-wave"] + "codeowners": ["@home-assistant/z-wave"], + "iot_class": "local_push" } diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index e6b4ed7c2a830..0b780ef7da40c 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/zwave_js", "requirements": ["zwave-js-server-python==0.23.1"], "codeowners": ["@home-assistant/z-wave"], - "dependencies": ["http", "websocket_api"] + "dependencies": ["http", "websocket_api"], + "iot_class": "local_push" } diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 44823720ea5f1..492233d8bcaf6 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -81,6 +81,7 @@ class Manifest(TypedDict, total=False): documentation: str issue_tracker: str quality_scale: str + iot_class: str mqtt: list[str] ssdp: list[dict[str, str]] zeroconf: list[str | dict[str, str]] @@ -390,6 +391,11 @@ def quality_scale(self) -> str | None: """Return Integration Quality Scale.""" return self.manifest.get("quality_scale") + @property + def iot_class(self) -> str | None: + """Return the integration IoT Class.""" + return self.manifest.get("iot_class") + @property def mqtt(self) -> list[str] | None: """Return Integration MQTT entries.""" diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index d8f6350911dff..ac9ab516dd1bd 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -16,6 +16,93 @@ DOCUMENTATION_URL_EXCEPTIONS = {"https://www.home-assistant.io/hassio"} SUPPORTED_QUALITY_SCALES = ["gold", "internal", "platinum", "silver"] +SUPPORTED_IOT_CLASSES = [ + "assumed_state", + "calculated", + "cloud_polling", + "cloud_push", + "local_polling", + "local_push", +] + +# List of integrations that are supposed to have no IoT class +NO_IOT_CLASS = [ + "air_quality", + "alarm_control_panel", + "api", + "auth", + "automation", + "binary_sensor", + "blueprint", + "calendar", + "camera", + "climate", + "color_extractor", + "config", + "configurator", + "counter", + "cover", + "default_config", + "device_automation", + "device_tracker", + "discovery", + "downloader", + "fan", + "ffmpeg", + "frontend", + "geo_location", + "history", + "homeassistant", + "humidifier", + "image_processing", + "image", + "input_boolean", + "input_datetime", + "input_number", + "input_select", + "input_text", + "intent_script", + "intent", + "light", + "lock", + "logbook", + "logger", + "lovelace", + "mailbox", + "map", + "media_player", + "media_source", + "my", + "notify", + "number", + "onboarding", + "panel_custom", + "panel_iframe", + "plant", + "profiler", + "proxy", + "python_script", + "remote", + "safe_mode", + "scene", + "script", + "search", + "sensor", + "stt", + "switch", + "system_health", + "system_log", + "tag", + "timer", + "trace", + "tts", + "vacuum", + "water_heater", + "weather", + "webhook", + "websocket_api", + "zone", +] def documentation_url(value: str) -> str: @@ -104,6 +191,7 @@ def verify_version(value: str): vol.Optional("after_dependencies"): [str], vol.Required("codeowners"): [str], vol.Optional("disabled"): str, + vol.Optional("iot_class"): vol.In(SUPPORTED_IOT_CLASSES), } ) @@ -130,6 +218,9 @@ def validate_version(integration: Integration): def validate_manifest(integration: Integration): """Validate manifest.""" + if not integration.manifest: + return + try: if integration.core: MANIFEST_SCHEMA(integration.manifest) @@ -143,6 +234,18 @@ def validate_manifest(integration: Integration): if integration.manifest["domain"] != integration.path.name: integration.add_error("manifest", "Domain does not match dir name") + if ( + integration.manifest["domain"] in NO_IOT_CLASS + and "iot_class" in integration.manifest + ): + integration.add_error("manifest", "Domain should not have an IoT Class") + + if ( + integration.manifest["domain"] not in NO_IOT_CLASS + and "iot_class" not in integration.manifest + ): + integration.add_error("manifest", "Domain is missing an IoT Class") + if not integration.core: validate_version(integration) @@ -150,5 +253,4 @@ def validate_manifest(integration: Integration): def validate(integrations: dict[str, Integration], config): """Handle all integrations manifests.""" for integration in integrations.values(): - if integration.manifest: - validate_manifest(integration) + validate_manifest(integration) diff --git a/script/hassfest/model.py b/script/hassfest/model.py index c5b8dbff61875..3bb46d4c230bb 100644 --- a/script/hassfest/model.py +++ b/script/hassfest/model.py @@ -67,7 +67,7 @@ def load_dir(cls, path: pathlib.Path): return integrations path: pathlib.Path = attr.ib() - manifest: dict | None = attr.ib(default=None) + manifest: dict[str, Any] | None = attr.ib(default=None) errors: list[Error] = attr.ib(factory=list) warnings: list[Error] = attr.ib(factory=list) From d71f913a12e244397144a706547f19861e563e65 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 14 Apr 2021 22:53:48 -1000 Subject: [PATCH 0280/1317] Ensure original log handlers are restored at close (#49230) Error messages after close were not being logged --- homeassistant/util/logging.py | 3 +++ tests/util/test_logging.py | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/homeassistant/util/logging.py b/homeassistant/util/logging.py index 816af95718d6b..1c0ff3de5d7bd 100644 --- a/homeassistant/util/logging.py +++ b/homeassistant/util/logging.py @@ -77,6 +77,9 @@ def async_activate_log_queue_handler(hass: HomeAssistant) -> None: @callback def _async_stop_queue_handler(_: Any) -> None: """Cleanup handler.""" + # Ensure any messages that happen after close still get logged + for original_handler in migrated_handlers: + logging.root.addHandler(original_handler) logging.root.removeHandler(queue_handler) listener.stop() diff --git a/tests/util/test_logging.py b/tests/util/test_logging.py index 9277d92f3686f..a1fd84409713b 100644 --- a/tests/util/test_logging.py +++ b/tests/util/test_logging.py @@ -7,6 +7,7 @@ import pytest +from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE from homeassistant.core import callback, is_callback import homeassistant.util.logging as logging_util @@ -65,11 +66,18 @@ async def test_logging_with_queue_handler(): async def test_migrate_log_handler(hass): """Test migrating log handlers.""" + original_handlers = logging.root.handlers + logging_util.async_activate_log_queue_handler(hass) assert len(logging.root.handlers) == 1 assert isinstance(logging.root.handlers[0], logging_util.HomeAssistantQueueHandler) + hass.bus.async_fire(EVENT_HOMEASSISTANT_CLOSE) + await hass.async_block_till_done() + + assert logging.root.handlers == original_handlers + @pytest.mark.no_fail_on_log_exception async def test_async_create_catching_coro(hass, caplog): From 2887eeb32f00d22dd6dc67decb1c80151de8c603 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 14 Apr 2021 23:17:32 -1000 Subject: [PATCH 0281/1317] Only enable envoy inverters when the user has access (#49234) --- .../components/enphase_envoy/__init__.py | 20 +++++----- .../components/enphase_envoy/config_flow.py | 21 +++++++++- .../components/enphase_envoy/strings.json | 3 +- .../enphase_envoy/translations/en.json | 3 +- .../enphase_envoy/test_config_flow.py | 40 +++++++++++++++++++ 5 files changed, 72 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/enphase_envoy/__init__.py b/homeassistant/components/enphase_envoy/__init__.py index faa9247b4e71f..26318faa7f9b5 100644 --- a/homeassistant/components/enphase_envoy/__init__.py +++ b/homeassistant/components/enphase_envoy/__init__.py @@ -12,7 +12,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.httpx_client import get_async_client from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -33,23 +33,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: config[CONF_HOST], config[CONF_USERNAME], config[CONF_PASSWORD], + inverters=True, async_client=get_async_client(hass), ) - try: - await envoy_reader.getData() - except httpx.HTTPStatusError as err: - _LOGGER.error("Authentication failure during setup: %s", err) - return - except (RuntimeError, httpx.HTTPError) as err: - raise ConfigEntryNotReady from err - async def async_update_data(): """Fetch data from API endpoint.""" data = {} async with async_timeout.timeout(30): try: await envoy_reader.getData() + except httpx.HTTPStatusError as err: + raise ConfigEntryAuthFailed from err except httpx.HTTPError as err: raise UpdateFailed(f"Error communicating with API: {err}") from err @@ -73,8 +68,11 @@ async def async_update_data(): update_interval=SCAN_INTERVAL, ) - envoy_reader.get_inverters = True - await coordinator.async_config_entry_first_refresh() + try: + await coordinator.async_config_entry_first_refresh() + except ConfigEntryAuthFailed: + envoy_reader.get_inverters = False + await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { COORDINATOR: coordinator, diff --git a/homeassistant/components/enphase_envoy/config_flow.py b/homeassistant/components/enphase_envoy/config_flow.py index 934b02be31104..a47a095fde77e 100644 --- a/homeassistant/components/enphase_envoy/config_flow.py +++ b/homeassistant/components/enphase_envoy/config_flow.py @@ -35,7 +35,7 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, data[CONF_HOST], data[CONF_USERNAME], data[CONF_PASSWORD], - inverters=True, + inverters=False, async_client=get_async_client(hass), ) @@ -59,6 +59,7 @@ def __init__(self): self.name = None self.username = None self.serial = None + self._reauth_entry = None @callback def _async_generate_schema(self): @@ -121,6 +122,13 @@ async def async_step_zeroconf(self, discovery_info): return await self.async_step_user() + async def async_step_reauth(self, user_input): + """Handle configuration by re-auth.""" + self._reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + return await self.async_step_user() + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> dict[str, Any]: @@ -128,7 +136,10 @@ async def async_step_user( errors = {} if user_input is not None: - if user_input[CONF_HOST] in self._async_current_hosts(): + if ( + not self._reauth_entry + and user_input[CONF_HOST] in self._async_current_hosts() + ): return self.async_abort(reason="already_configured") try: await validate_input(self.hass, user_input) @@ -145,6 +156,12 @@ async def async_step_user( data[CONF_NAME] = f"{ENVOY} {self.serial}" else: data[CONF_NAME] = self.name or ENVOY + if self._reauth_entry: + self.hass.config_entries.async_update_entry( + self._reauth_entry, + data=data, + ) + return self.async_abort(reason="reauth_successful") return self.async_create_entry(title=data[CONF_NAME], data=data) if self.serial: diff --git a/homeassistant/components/enphase_envoy/strings.json b/homeassistant/components/enphase_envoy/strings.json index 399358659d70e..1af58a32fa745 100644 --- a/homeassistant/components/enphase_envoy/strings.json +++ b/homeassistant/components/enphase_envoy/strings.json @@ -16,7 +16,8 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } } } \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/en.json b/homeassistant/components/enphase_envoy/translations/en.json index 7c138727cd72d..58c69e90eef9b 100644 --- a/homeassistant/components/enphase_envoy/translations/en.json +++ b/homeassistant/components/enphase_envoy/translations/en.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Device is already configured" + "already_configured": "Device is already configured", + "reauth_successful": "Re-authentication was successful" }, "error": { "cannot_connect": "Failed to connect", diff --git a/tests/components/enphase_envoy/test_config_flow.py b/tests/components/enphase_envoy/test_config_flow.py index 99efca883c8ce..0f48067ec6d35 100644 --- a/tests/components/enphase_envoy/test_config_flow.py +++ b/tests/components/enphase_envoy/test_config_flow.py @@ -302,3 +302,43 @@ async def test_zeroconf_host_already_exists(hass: HomeAssistant) -> None: assert config_entry.unique_id == "1234" assert config_entry.title == "Envoy 1234" assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_reauth(hass: HomeAssistant) -> None: + """Test we reauth auth.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + "host": "1.1.1.1", + "name": "Envoy", + "username": "test-username", + "password": "test-password", + }, + title="Envoy", + ) + config_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": config_entry.unique_id, + "entry_id": config_entry.entry_id, + }, + ) + + with patch( + "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.getData", + return_value=True, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == "abort" + assert result2["reason"] == "reauth_successful" From 7a40d0f1c224b0319c48a322af1911019c375616 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 14 Apr 2021 23:24:43 -1000 Subject: [PATCH 0282/1317] Disconnect roomba on stop event (#49235) --- .coveragerc | 1 + homeassistant/components/roomba/__init__.py | 40 +++++++++++++++------ homeassistant/components/roomba/const.py | 1 + tests/components/roomba/test_config_flow.py | 27 -------------- 4 files changed, 31 insertions(+), 38 deletions(-) diff --git a/.coveragerc b/.coveragerc index d3eee9c9f6031..40daa9ce2307c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -832,6 +832,7 @@ omit = homeassistant/components/rituals_perfume_genie/switch.py homeassistant/components/rituals_perfume_genie/__init__.py homeassistant/components/rocketchat/notify.py + homeassistant/components/roomba/__init__.py homeassistant/components/roomba/binary_sensor.py homeassistant/components/roomba/braava.py homeassistant/components/roomba/irobot_base.py diff --git a/homeassistant/components/roomba/__init__.py b/homeassistant/components/roomba/__init__.py index 6de775e1d997f..ae1fc05ad5365 100644 --- a/homeassistant/components/roomba/__init__.py +++ b/homeassistant/components/roomba/__init__.py @@ -6,19 +6,27 @@ from roombapy import Roomba, RoombaConnectionError from homeassistant import exceptions -from homeassistant.const import CONF_DELAY, CONF_HOST, CONF_NAME, CONF_PASSWORD - -from .const import BLID, CONF_BLID, CONF_CONTINUOUS, DOMAIN, PLATFORMS, ROOMBA_SESSION +from homeassistant.const import ( + CONF_DELAY, + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + EVENT_HOMEASSISTANT_STOP, +) + +from .const import ( + BLID, + CANCEL_STOP, + CONF_BLID, + CONF_CONTINUOUS, + DOMAIN, + PLATFORMS, + ROOMBA_SESSION, +) _LOGGER = logging.getLogger(__name__) -async def async_setup(hass, config): - """Set up the roomba environment.""" - hass.data.setdefault(DOMAIN, {}) - return True - - async def async_setup_entry(hass, config_entry): """Set the config entry up.""" # Set up roomba platforms with config entry @@ -46,9 +54,18 @@ async def async_setup_entry(hass, config_entry): except CannotConnect as err: raise exceptions.ConfigEntryNotReady from err + async def _async_disconnect_roomba(event): + await async_disconnect_or_timeout(hass, roomba) + + cancel_stop = hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, _async_disconnect_roomba + ) + + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][config_entry.entry_id] = { ROOMBA_SESSION: roomba, BLID: config_entry.data[CONF_BLID], + CANCEL_STOP: cancel_stop, } for platform in PLATFORMS: @@ -76,12 +93,12 @@ async def async_connect_or_timeout(hass, roomba): break await asyncio.sleep(1) except RoombaConnectionError as err: - _LOGGER.error("Error to connect to vacuum") + _LOGGER.debug("Error to connect to vacuum: %s", err) raise CannotConnect from err except asyncio.TimeoutError as err: # api looping if user or password incorrect and roomba exist await async_disconnect_or_timeout(hass, roomba) - _LOGGER.error("Timeout expired") + _LOGGER.debug("Timeout expired: %s", err) raise CannotConnect from err return {ROOMBA_SESSION: roomba, CONF_NAME: name} @@ -112,6 +129,7 @@ async def async_unload_entry(hass, config_entry): ) if unload_ok: domain_data = hass.data[DOMAIN][config_entry.entry_id] + domain_data[CANCEL_STOP]() await async_disconnect_or_timeout(hass, roomba=domain_data[ROOMBA_SESSION]) hass.data[DOMAIN].pop(config_entry.entry_id) diff --git a/homeassistant/components/roomba/const.py b/homeassistant/components/roomba/const.py index 0509cd9211627..2e59279cfdb0b 100644 --- a/homeassistant/components/roomba/const.py +++ b/homeassistant/components/roomba/const.py @@ -9,3 +9,4 @@ DEFAULT_DELAY = 1 ROOMBA_SESSION = "roomba_session" BLID = "blid_key" +CANCEL_STOP = "cancel_stop" diff --git a/tests/components/roomba/test_config_flow.py b/tests/components/roomba/test_config_flow.py index ee3b7d4b49752..e125e9bd5ba17 100644 --- a/tests/components/roomba/test_config_flow.py +++ b/tests/components/roomba/test_config_flow.py @@ -150,8 +150,6 @@ async def test_form_user_discovery_and_password_fetch(hass): "homeassistant.components.roomba.config_flow.RoombaPassword", _mocked_getpassword, ), patch( - "homeassistant.components.roomba.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.roomba.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -171,7 +169,6 @@ async def test_form_user_discovery_and_password_fetch(hass): CONF_HOST: MOCK_IP, CONF_PASSWORD: "password", } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -269,8 +266,6 @@ async def test_form_user_discovery_manual_and_auto_password_fetch(hass): "homeassistant.components.roomba.config_flow.RoombaPassword", _mocked_getpassword, ), patch( - "homeassistant.components.roomba.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.roomba.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -290,7 +285,6 @@ async def test_form_user_discovery_manual_and_auto_password_fetch(hass): CONF_HOST: MOCK_IP, CONF_PASSWORD: "password", } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -371,8 +365,6 @@ async def test_form_user_discovery_manual_and_auto_password_fetch_but_cannot_con "homeassistant.components.roomba.config_flow.RoombaPassword", _mocked_getpassword, ), patch( - "homeassistant.components.roomba.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.roomba.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -384,7 +376,6 @@ async def test_form_user_discovery_manual_and_auto_password_fetch_but_cannot_con assert result4["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result4["reason"] == "cannot_connect" - assert len(mock_setup.mock_calls) == 0 assert len(mock_setup_entry.mock_calls) == 0 @@ -425,8 +416,6 @@ async def test_form_user_discovery_no_devices_found_and_auto_password_fetch(hass "homeassistant.components.roomba.config_flow.RoombaPassword", _mocked_getpassword, ), patch( - "homeassistant.components.roomba.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.roomba.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -446,7 +435,6 @@ async def test_form_user_discovery_no_devices_found_and_auto_password_fetch(hass CONF_HOST: MOCK_IP, CONF_PASSWORD: "password", } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -494,8 +482,6 @@ async def test_form_user_discovery_no_devices_found_and_password_fetch_fails(has "homeassistant.components.roomba.config_flow.Roomba", return_value=mocked_roomba, ), patch( - "homeassistant.components.roomba.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.roomba.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -515,7 +501,6 @@ async def test_form_user_discovery_no_devices_found_and_password_fetch_fails(has CONF_HOST: MOCK_IP, CONF_PASSWORD: "password", } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -566,8 +551,6 @@ async def test_form_user_discovery_not_devices_found_and_password_fetch_fails_an "homeassistant.components.roomba.config_flow.Roomba", return_value=mocked_roomba, ), patch( - "homeassistant.components.roomba.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.roomba.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -579,7 +562,6 @@ async def test_form_user_discovery_not_devices_found_and_password_fetch_fails_an assert result4["type"] == data_entry_flow.RESULT_TYPE_FORM assert result4["errors"] == {"base": "cannot_connect"} - assert len(mock_setup.mock_calls) == 0 assert len(mock_setup_entry.mock_calls) == 0 @@ -627,8 +609,6 @@ async def test_form_user_discovery_and_password_fetch_gets_connection_refused(ha "homeassistant.components.roomba.config_flow.Roomba", return_value=mocked_roomba, ), patch( - "homeassistant.components.roomba.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.roomba.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -648,7 +628,6 @@ async def test_form_user_discovery_and_password_fetch_gets_connection_refused(ha CONF_HOST: MOCK_IP, CONF_PASSWORD: "password", } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -684,8 +663,6 @@ async def test_dhcp_discovery_and_roomba_discovery_finds(hass, discovery_data): "homeassistant.components.roomba.config_flow.RoombaPassword", _mocked_getpassword, ), patch( - "homeassistant.components.roomba.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.roomba.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -705,7 +682,6 @@ async def test_dhcp_discovery_and_roomba_discovery_finds(hass, discovery_data): CONF_HOST: MOCK_IP, CONF_PASSWORD: "password", } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -757,8 +733,6 @@ async def test_dhcp_discovery_falls_back_to_manual(hass, discovery_data): "homeassistant.components.roomba.config_flow.RoombaPassword", _mocked_getpassword, ), patch( - "homeassistant.components.roomba.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.roomba.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -778,7 +752,6 @@ async def test_dhcp_discovery_falls_back_to_manual(hass, discovery_data): CONF_HOST: MOCK_IP, CONF_PASSWORD: "password", } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 From 4f5c7454929f76d6415b418fc2f83fa9862f7395 Mon Sep 17 00:00:00 2001 From: bouni Date: Thu, 15 Apr 2021 12:40:23 +0200 Subject: [PATCH 0283/1317] Fix broken swiss_hydrological_data integration (#49119) * update requirements_all.txt * :ambulance: Fix broken JSON Co-authored-by: Franck Nijhof Co-authored-by: Franck Nijhof --- .../components/swiss_hydrological_data/manifest.json | 2 +- .../components/swiss_hydrological_data/sensor.py | 10 ---------- requirements_all.txt | 2 +- 3 files changed, 2 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/swiss_hydrological_data/manifest.json b/homeassistant/components/swiss_hydrological_data/manifest.json index faceb69c3e129..7d7280ecc5fa8 100644 --- a/homeassistant/components/swiss_hydrological_data/manifest.json +++ b/homeassistant/components/swiss_hydrological_data/manifest.json @@ -2,7 +2,7 @@ "domain": "swiss_hydrological_data", "name": "Swiss Hydrological Data", "documentation": "https://www.home-assistant.io/integrations/swiss_hydrological_data", - "requirements": ["swisshydrodata==0.0.3"], + "requirements": ["swisshydrodata==0.1.0"], "codeowners": ["@fabaff"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/swiss_hydrological_data/sensor.py b/homeassistant/components/swiss_hydrological_data/sensor.py index 47a8d3e55899d..1d77410f03148 100644 --- a/homeassistant/components/swiss_hydrological_data/sensor.py +++ b/homeassistant/components/swiss_hydrological_data/sensor.py @@ -14,14 +14,9 @@ ATTRIBUTION = "Data provided by the Swiss Federal Office for the Environment FOEN" -ATTR_DELTA_24H = "delta-24h" -ATTR_MAX_1H = "max-1h" ATTR_MAX_24H = "max-24h" -ATTR_MEAN_1H = "mean-1h" ATTR_MEAN_24H = "mean-24h" -ATTR_MIN_1H = "min-1h" ATTR_MIN_24H = "min-24h" -ATTR_PREVIOUS_24H = "previous-24h" ATTR_STATION = "station" ATTR_STATION_UPDATE = "station_update" ATTR_WATER_BODY = "water_body" @@ -42,14 +37,9 @@ } CONDITION_DETAILS = [ - ATTR_DELTA_24H, - ATTR_MAX_1H, ATTR_MAX_24H, - ATTR_MEAN_1H, ATTR_MEAN_24H, - ATTR_MIN_1H, ATTR_MIN_24H, - ATTR_PREVIOUS_24H, ] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( diff --git a/requirements_all.txt b/requirements_all.txt index 6c3df50ae46b9..35b248b8d2103 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2172,7 +2172,7 @@ sunwatcher==0.2.1 surepy==0.4.0 # homeassistant.components.swiss_hydrological_data -swisshydrodata==0.0.3 +swisshydrodata==0.1.0 # homeassistant.components.synology_srm synology-srm==0.2.0 From 1b5148a3af58ea7e6ab497091996ab0805a16a51 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 15 Apr 2021 16:12:49 +0200 Subject: [PATCH 0284/1317] Fix mysensors sensor protocol version check (#49257) --- homeassistant/components/mysensors/sensor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mysensors/sensor.py b/homeassistant/components/mysensors/sensor.py index a62318aea53ec..1a5f7330ddffa 100644 --- a/homeassistant/components/mysensors/sensor.py +++ b/homeassistant/components/mysensors/sensor.py @@ -1,6 +1,8 @@ """Support for MySensors sensors.""" from typing import Callable +from awesomeversion import AwesomeVersion + from homeassistant.components import mysensors from homeassistant.components.mysensors import on_unload from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY @@ -115,7 +117,7 @@ def unit_of_measurement(self): """Return the unit of measurement of this entity.""" set_req = self.gateway.const.SetReq if ( - float(self.gateway.protocol_version) >= 1.5 + AwesomeVersion(self.gateway.protocol_version) >= AwesomeVersion("1.5") and set_req.V_UNIT_PREFIX in self._values ): return self._values[set_req.V_UNIT_PREFIX] From ec56ae2cbc30324cd3b776809f4723536906c649 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 15 Apr 2021 17:24:21 +0200 Subject: [PATCH 0285/1317] Set deprecated supported_features for MQTT JSON light (#49167) * Set deprecated supported_features for MQTT json light * Update homeassistant/components/light/__init__.py Co-authored-by: Franck Nijhof Co-authored-by: Franck Nijhof --- homeassistant/components/light/__init__.py | 17 +++++ .../components/mqtt/light/schema_json.py | 5 +- tests/components/mqtt/test_light_json.py | 71 +++++++++++++++---- 3 files changed, 80 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index fe9a38d12b4f7..bfdb723e159d4 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -751,3 +751,20 @@ def __init_subclass__(cls, **kwargs): "Light is deprecated, modify %s to extend LightEntity", cls.__name__, ) + + +def legacy_supported_features( + supported_features: int, supported_color_modes: list[str] | None +) -> int: + """Calculate supported features with backwards compatibility.""" + # Backwards compatibility for supported_color_modes added in 2021.4 + if supported_color_modes is None: + return supported_features + if any(mode in supported_color_modes for mode in COLOR_MODES_COLOR): + supported_features |= SUPPORT_COLOR + if any(mode in supported_color_modes for mode in COLOR_MODES_BRIGHTNESS): + supported_features |= SUPPORT_BRIGHTNESS + if COLOR_MODE_COLOR_TEMP in supported_color_modes: + supported_features |= SUPPORT_COLOR_TEMP + + return supported_features diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index aaf12f3362f42..9940d646a35bc 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -35,6 +35,7 @@ SUPPORT_WHITE_VALUE, VALID_COLOR_MODES, LightEntity, + legacy_supported_features, valid_supported_color_modes, ) from homeassistant.const import ( @@ -458,7 +459,9 @@ def supported_color_modes(self): @property def supported_features(self): """Flag supported features.""" - return self._supported_features + return legacy_supported_features( + self._supported_features, self._config.get(CONF_SUPPORTED_COLOR_MODES) + ) def _set_flash_and_transition(self, message, **kwargs): if ATTR_TRANSITION in kwargs: diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index 6c9c7ae903a80..77e5936c7b49c 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -234,10 +234,10 @@ async def test_rgb_light(hass, mqtt_mock): state = hass.states.get("light.test") expected_features = ( - light.SUPPORT_TRANSITION + light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR | light.SUPPORT_FLASH - | light.SUPPORT_BRIGHTNESS + | light.SUPPORT_TRANSITION ) assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features @@ -261,7 +261,8 @@ async def test_no_color_brightness_color_temp_white_val_if_no_topics(hass, mqtt_ state = hass.states.get("light.test") assert state.state == STATE_OFF - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 40 + expected_features = light.SUPPORT_FLASH | light.SUPPORT_TRANSITION + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features assert state.attributes.get("rgb_color") is None assert state.attributes.get("brightness") is None assert state.attributes.get("color_temp") is None @@ -310,7 +311,16 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): state = hass.states.get("light.test") assert state.state == STATE_OFF - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 191 + expected_features = ( + light.SUPPORT_BRIGHTNESS + | light.SUPPORT_COLOR + | light.SUPPORT_COLOR_TEMP + | light.SUPPORT_EFFECT + | light.SUPPORT_FLASH + | light.SUPPORT_TRANSITION + | light.SUPPORT_WHITE_VALUE + ) + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features assert state.attributes.get("rgb_color") is None assert state.attributes.get("brightness") is None assert state.attributes.get("color_temp") is None @@ -429,7 +439,15 @@ async def test_controlling_state_via_topic2(hass, mqtt_mock, caplog): state = hass.states.get("light.test") assert state.state == STATE_OFF - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 44 + expected_features = ( + light.SUPPORT_BRIGHTNESS + | light.SUPPORT_COLOR + | light.SUPPORT_COLOR_TEMP + | light.SUPPORT_EFFECT + | light.SUPPORT_FLASH + | light.SUPPORT_TRANSITION + ) + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features assert state.attributes.get("brightness") is None assert state.attributes.get("color_mode") is None assert state.attributes.get("color_temp") is None @@ -610,7 +628,16 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): assert state.attributes.get("effect") == "random" assert state.attributes.get("color_temp") == 100 assert state.attributes.get("white_value") == 50 - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 191 + expected_features = ( + light.SUPPORT_BRIGHTNESS + | light.SUPPORT_COLOR + | light.SUPPORT_COLOR_TEMP + | light.SUPPORT_EFFECT + | light.SUPPORT_FLASH + | light.SUPPORT_TRANSITION + | light.SUPPORT_WHITE_VALUE + ) + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features assert state.attributes.get(ATTR_ASSUMED_STATE) await common.async_turn_on(hass, "light.test") @@ -738,7 +765,15 @@ async def test_sending_mqtt_commands_and_optimistic2(hass, mqtt_mock): state = hass.states.get("light.test") assert state.state == STATE_ON - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 44 + expected_features = ( + light.SUPPORT_BRIGHTNESS + | light.SUPPORT_COLOR + | light.SUPPORT_COLOR_TEMP + | light.SUPPORT_EFFECT + | light.SUPPORT_FLASH + | light.SUPPORT_TRANSITION + ) + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features assert state.attributes.get("brightness") == 95 assert state.attributes.get("color_mode") == "rgb" assert state.attributes.get("color_temp") is None @@ -1313,7 +1348,10 @@ async def test_effect(hass, mqtt_mock): state = hass.states.get("light.test") assert state.state == STATE_OFF - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 44 + expected_features = ( + light.SUPPORT_EFFECT | light.SUPPORT_FLASH | light.SUPPORT_TRANSITION + ) + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features await common.async_turn_on(hass, "light.test") @@ -1373,7 +1411,8 @@ async def test_flash_short_and_long(hass, mqtt_mock): state = hass.states.get("light.test") assert state.state == STATE_OFF - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 40 + expected_features = light.SUPPORT_FLASH | light.SUPPORT_TRANSITION + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features await common.async_turn_on(hass, "light.test", flash="short") @@ -1431,8 +1470,8 @@ async def test_transition(hass, mqtt_mock): state = hass.states.get("light.test") assert state.state == STATE_OFF - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 40 - + expected_features = light.SUPPORT_FLASH | light.SUPPORT_TRANSITION + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features await common.async_turn_on(hass, "light.test", transition=15) mqtt_mock.async_publish.assert_called_once_with( @@ -1523,7 +1562,15 @@ async def test_invalid_values(hass, mqtt_mock): state = hass.states.get("light.test") assert state.state == STATE_OFF - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 187 + expected_features = ( + light.SUPPORT_BRIGHTNESS + | light.SUPPORT_COLOR + | light.SUPPORT_COLOR_TEMP + | light.SUPPORT_FLASH + | light.SUPPORT_TRANSITION + | light.SUPPORT_WHITE_VALUE + ) + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features assert state.attributes.get("rgb_color") is None assert state.attributes.get("brightness") is None assert state.attributes.get("white_value") is None From a529a1274567a65a00956616901531d37dd31d2b Mon Sep 17 00:00:00 2001 From: Angeliki Papadopoulou <56366807+apapadopoulou@users.noreply.github.com> Date: Thu, 15 Apr 2021 19:05:07 +0300 Subject: [PATCH 0286/1317] Remove redundant text from documentation (#49262) I found an extra "when" in the documentation text. --- homeassistant/util/percentage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/util/percentage.py b/homeassistant/util/percentage.py index ec05a2dc2ec60..42beeeb5523e5 100644 --- a/homeassistant/util/percentage.py +++ b/homeassistant/util/percentage.py @@ -8,7 +8,7 @@ def ordered_list_item_to_percentage(ordered_list: list[str], item: str) -> int: When using this utility for fan speeds, do not include "off" Given the list: ["low", "medium", "high", "very_high"], this - function will return the following when when the item is passed + function will return the following when the item is passed in: low: 25 From dafc7a072caac1625506dbaaf55659cc9394dda9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 15 Apr 2021 07:13:42 -1000 Subject: [PATCH 0287/1317] Cancel discovery flows that are initializing at shutdown (#49241) --- homeassistant/config_entries.py | 1 + homeassistant/data_entry_flow.py | 42 +++++++++++++++++++++++--------- tests/test_data_entry_flow.py | 28 +++++++++++++++++++++ 3 files changed, 59 insertions(+), 12 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 34afc77e52809..9df6dff8316c4 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -792,6 +792,7 @@ async def _async_shutdown(self, event: Event) -> None: await asyncio.gather( *[entry.async_shutdown() for entry in self._entries.values()] ) + await self.flow.async_shutdown() async def async_initialize(self) -> None: """Initialize config entry config.""" diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 46ec967bd94e5..a9a78337b17c1 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -61,6 +61,7 @@ def __init__( """Initialize the flow manager.""" self.hass = hass self._initializing: dict[str, list[asyncio.Future]] = {} + self._initialize_tasks: dict[str, list[asyncio.Task]] = {} self._progress: dict[str, Any] = {} async def async_wait_init_flow_finish(self, handler: str) -> None: @@ -118,21 +119,13 @@ async def async_init( init_done: asyncio.Future = asyncio.Future() self._initializing.setdefault(handler, []).append(init_done) - flow = await self.async_create_flow(handler, context=context, data=data) - if not flow: - self._initializing[handler].remove(init_done) - raise UnknownFlow("Flow was not created") - flow.hass = self.hass - flow.handler = handler - flow.flow_id = uuid.uuid4().hex - flow.context = context - self._progress[flow.flow_id] = flow + task = asyncio.create_task(self._async_init(init_done, handler, context, data)) + self._initialize_tasks.setdefault(handler, []).append(task) try: - result = await self._async_handle_step( - flow, flow.init_step, data, init_done - ) + flow, result = await task finally: + self._initialize_tasks[handler].remove(task) self._initializing[handler].remove(init_done) if result["type"] != RESULT_TYPE_ABORT: @@ -140,6 +133,31 @@ async def async_init( return result + async def _async_init( + self, + init_done: asyncio.Future, + handler: str, + context: dict, + data: Any, + ) -> tuple[FlowHandler, Any]: + """Run the init in a task to allow it to be canceled at shutdown.""" + flow = await self.async_create_flow(handler, context=context, data=data) + if not flow: + raise UnknownFlow("Flow was not created") + flow.hass = self.hass + flow.handler = handler + flow.flow_id = uuid.uuid4().hex + flow.context = context + self._progress[flow.flow_id] = flow + result = await self._async_handle_step(flow, flow.init_step, data, init_done) + return flow, result + + async def async_shutdown(self) -> None: + """Cancel any initializing flows.""" + for task_list in self._initialize_tasks.values(): + for task in task_list: + task.cancel() + async def async_configure( self, flow_id: str, user_input: dict | None = None ) -> Any: diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index b2fd9c8e34b43..47b217936561b 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -1,4 +1,7 @@ """Test the flow classes.""" +import asyncio +from unittest.mock import patch + import pytest import voluptuous as vol @@ -367,3 +370,28 @@ async def async_step_init(self, user_input=None): assert form["type"] == "abort" assert form["reason"] == "mock-reason" assert form["description_placeholders"] == {"placeholder": "yo"} + + +async def test_initializing_flows_canceled_on_shutdown(hass, manager): + """Test that initializing flows are canceled on shutdown.""" + + @manager.mock_reg_handler("test") + class TestFlow(data_entry_flow.FlowHandler): + async def async_step_init(self, user_input=None): + await asyncio.sleep(1) + + task = asyncio.create_task(manager.async_init("test")) + await hass.async_block_till_done() + await manager.async_shutdown() + + with pytest.raises(asyncio.exceptions.CancelledError): + await task + + +async def test_init_unknown_flow(manager): + """Test that UnknownFlow is raised when async_create_flow returns None.""" + + with pytest.raises(data_entry_flow.UnknownFlow), patch.object( + manager, "async_create_flow", return_value=None + ): + await manager.async_init("test") From 80f66f301bab86a90d945d5cde5de1078cca5183 Mon Sep 17 00:00:00 2001 From: Ruslan Sayfutdinov Date: Thu, 15 Apr 2021 18:17:07 +0100 Subject: [PATCH 0288/1317] Define data flow result type (#49260) * Define data flow result type * Revert explicit definitions * Fix tests * Specific mypy ignore --- homeassistant/auth/__init__.py | 9 +-- homeassistant/auth/mfa_modules/__init__.py | 3 +- homeassistant/auth/mfa_modules/notify.py | 5 +- homeassistant/auth/mfa_modules/totp.py | 3 +- homeassistant/auth/providers/__init__.py | 12 ++-- homeassistant/auth/providers/command_line.py | 6 +- homeassistant/auth/providers/homeassistant.py | 6 +- .../auth/providers/insecure_example.py | 8 ++- .../auth/providers/legacy_api_password.py | 8 ++- .../auth/providers/trusted_networks.py | 6 +- homeassistant/components/bond/config_flow.py | 9 ++- homeassistant/components/hassio/__init__.py | 2 +- .../components/huawei_lte/config_flow.py | 16 +++-- .../components/hyperion/config_flow.py | 25 ++++--- .../components/zwave_js/config_flow.py | 30 ++++---- homeassistant/config_entries.py | 39 ++++++----- homeassistant/data_entry_flow.py | 69 +++++++++++++------ homeassistant/helpers/config_entry_flow.py | 14 ++-- .../helpers/config_entry_oauth2_flow.py | 9 +-- homeassistant/helpers/data_entry_flow.py | 4 +- 20 files changed, 169 insertions(+), 114 deletions(-) diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index 3830419c53754..89a05e20eb22c 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -4,13 +4,14 @@ import asyncio from collections import OrderedDict from datetime import timedelta -from typing import Any, Dict, Optional, Tuple, cast +from typing import Any, Dict, Mapping, Optional, Tuple, cast import jwt from homeassistant import data_entry_flow from homeassistant.auth.const import ACCESS_TOKEN_EXPIRATION from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResultDict from homeassistant.util import dt as dt_util from . import auth_store, models @@ -97,8 +98,8 @@ async def async_create_flow( return await auth_provider.async_login_flow(context) async def async_finish_flow( - self, flow: data_entry_flow.FlowHandler, result: dict[str, Any] - ) -> dict[str, Any]: + self, flow: data_entry_flow.FlowHandler, result: FlowResultDict + ) -> FlowResultDict: """Return a user as result of login flow.""" flow = cast(LoginFlow, flow) @@ -115,7 +116,7 @@ async def async_finish_flow( raise KeyError(f"Unknown auth provider {result['handler']}") credentials = await auth_provider.async_get_or_create_credentials( - result["data"] + cast(Mapping[str, str], result["data"]), ) if flow.context.get("credential_only"): diff --git a/homeassistant/auth/mfa_modules/__init__.py b/homeassistant/auth/mfa_modules/__init__.py index d6989b6416fc9..80e0a0d834a3f 100644 --- a/homeassistant/auth/mfa_modules/__init__.py +++ b/homeassistant/auth/mfa_modules/__init__.py @@ -12,6 +12,7 @@ from homeassistant import data_entry_flow, requirements from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultDict from homeassistant.exceptions import HomeAssistantError from homeassistant.util.decorator import Registry @@ -105,7 +106,7 @@ def __init__( async def async_step_init( self, user_input: dict[str, str] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle the first step of setup flow. Return self.async_show_form(step_id='init') if user_input is None. diff --git a/homeassistant/auth/mfa_modules/notify.py b/homeassistant/auth/mfa_modules/notify.py index 76a5676d562c8..c590b6195e46e 100644 --- a/homeassistant/auth/mfa_modules/notify.py +++ b/homeassistant/auth/mfa_modules/notify.py @@ -14,6 +14,7 @@ from homeassistant.const import CONF_EXCLUDE, CONF_INCLUDE from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResultDict from homeassistant.exceptions import ServiceNotFound from homeassistant.helpers import config_validation as cv @@ -292,7 +293,7 @@ def __init__( async def async_step_init( self, user_input: dict[str, str] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Let user select available notify services.""" errors: dict[str, str] = {} @@ -318,7 +319,7 @@ async def async_step_init( async def async_step_setup( self, user_input: dict[str, str] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Verify user can receive one-time password.""" errors: dict[str, str] = {} diff --git a/homeassistant/auth/mfa_modules/totp.py b/homeassistant/auth/mfa_modules/totp.py index d20c84655463a..cb9ff95f808a9 100644 --- a/homeassistant/auth/mfa_modules/totp.py +++ b/homeassistant/auth/mfa_modules/totp.py @@ -9,6 +9,7 @@ from homeassistant.auth.models import User from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultDict from . import ( MULTI_FACTOR_AUTH_MODULE_SCHEMA, @@ -189,7 +190,7 @@ def __init__( async def async_step_init( self, user_input: dict[str, str] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle the first step of setup flow. Return self.async_show_form(step_id='init') if user_input is None. diff --git a/homeassistant/auth/providers/__init__.py b/homeassistant/auth/providers/__init__.py index 6e188be1ffc94..cdd5029f1d983 100644 --- a/homeassistant/auth/providers/__init__.py +++ b/homeassistant/auth/providers/__init__.py @@ -1,6 +1,7 @@ """Auth providers for Home Assistant.""" from __future__ import annotations +from collections.abc import Mapping import importlib import logging import types @@ -12,6 +13,7 @@ from homeassistant import data_entry_flow, requirements from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResultDict from homeassistant.exceptions import HomeAssistantError from homeassistant.util import dt as dt_util from homeassistant.util.decorator import Registry @@ -102,7 +104,7 @@ async def async_login_flow(self, context: dict | None) -> LoginFlow: raise NotImplementedError async def async_get_or_create_credentials( - self, flow_result: dict[str, str] + self, flow_result: Mapping[str, str] ) -> Credentials: """Get credentials based on the flow result.""" raise NotImplementedError @@ -198,7 +200,7 @@ def __init__(self, auth_provider: AuthProvider) -> None: async def async_step_init( self, user_input: dict[str, str] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle the first step of login flow. Return self.async_show_form(step_id='init') if user_input is None. @@ -208,7 +210,7 @@ async def async_step_init( async def async_step_select_mfa_module( self, user_input: dict[str, str] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle the step of select mfa module.""" errors = {} @@ -233,7 +235,7 @@ async def async_step_select_mfa_module( async def async_step_mfa( self, user_input: dict[str, str] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle the step of mfa validation.""" assert self.credential assert self.user @@ -285,6 +287,6 @@ async def async_step_mfa( errors=errors, ) - async def async_finish(self, flow_result: Any) -> dict: + async def async_finish(self, flow_result: Any) -> FlowResultDict: """Handle the pass of login flow.""" return self.async_create_entry(title=self._auth_provider.name, data=flow_result) diff --git a/homeassistant/auth/providers/command_line.py b/homeassistant/auth/providers/command_line.py index 47a56d87097c4..9413072fd4b63 100644 --- a/homeassistant/auth/providers/command_line.py +++ b/homeassistant/auth/providers/command_line.py @@ -3,6 +3,7 @@ import asyncio.subprocess import collections +from collections.abc import Mapping import logging import os from typing import Any, cast @@ -10,6 +11,7 @@ import voluptuous as vol from homeassistant.const import CONF_COMMAND +from homeassistant.data_entry_flow import FlowResultDict from homeassistant.exceptions import HomeAssistantError from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow @@ -100,7 +102,7 @@ async def async_validate_login(self, username: str, password: str) -> None: self._user_meta[username] = meta async def async_get_or_create_credentials( - self, flow_result: dict[str, str] + self, flow_result: Mapping[str, str] ) -> Credentials: """Get credentials based on the flow result.""" username = flow_result["username"] @@ -127,7 +129,7 @@ class CommandLineLoginFlow(LoginFlow): async def async_step_init( self, user_input: dict[str, str] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle the step of the form.""" errors = {} diff --git a/homeassistant/auth/providers/homeassistant.py b/homeassistant/auth/providers/homeassistant.py index 54d82013a7580..7544ae9aa1485 100644 --- a/homeassistant/auth/providers/homeassistant.py +++ b/homeassistant/auth/providers/homeassistant.py @@ -4,6 +4,7 @@ import asyncio import base64 from collections import OrderedDict +from collections.abc import Mapping import logging from typing import Any, cast @@ -12,6 +13,7 @@ from homeassistant.const import CONF_ID from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResultDict from homeassistant.exceptions import HomeAssistantError from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow @@ -277,7 +279,7 @@ async def async_change_password(self, username: str, new_password: str) -> None: await self.data.async_save() async def async_get_or_create_credentials( - self, flow_result: dict[str, str] + self, flow_result: Mapping[str, str] ) -> Credentials: """Get credentials based on the flow result.""" if self.data is None: @@ -319,7 +321,7 @@ class HassLoginFlow(LoginFlow): async def async_step_init( self, user_input: dict[str, str] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle the step of the form.""" errors = {} diff --git a/homeassistant/auth/providers/insecure_example.py b/homeassistant/auth/providers/insecure_example.py index c938a6fac8153..ac6171a346c5c 100644 --- a/homeassistant/auth/providers/insecure_example.py +++ b/homeassistant/auth/providers/insecure_example.py @@ -2,12 +2,14 @@ from __future__ import annotations from collections import OrderedDict +from collections.abc import Mapping import hmac -from typing import Any, cast +from typing import cast import voluptuous as vol from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResultDict from homeassistant.exceptions import HomeAssistantError from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow @@ -62,7 +64,7 @@ def async_validate_login(self, username: str, password: str) -> None: raise InvalidAuthError async def async_get_or_create_credentials( - self, flow_result: dict[str, str] + self, flow_result: Mapping[str, str] ) -> Credentials: """Get credentials based on the flow result.""" username = flow_result["username"] @@ -97,7 +99,7 @@ class ExampleLoginFlow(LoginFlow): async def async_step_init( self, user_input: dict[str, str] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle the step of the form.""" errors = {} diff --git a/homeassistant/auth/providers/legacy_api_password.py b/homeassistant/auth/providers/legacy_api_password.py index 522751c70d698..5ffb59638dbb8 100644 --- a/homeassistant/auth/providers/legacy_api_password.py +++ b/homeassistant/auth/providers/legacy_api_password.py @@ -5,12 +5,14 @@ """ from __future__ import annotations +from collections.abc import Mapping import hmac -from typing import Any, cast +from typing import cast import voluptuous as vol from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResultDict from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv @@ -57,7 +59,7 @@ def async_validate_login(self, password: str) -> None: raise InvalidAuthError async def async_get_or_create_credentials( - self, flow_result: dict[str, str] + self, flow_result: Mapping[str, str] ) -> Credentials: """Return credentials for this login.""" credentials = await self.async_credentials() @@ -82,7 +84,7 @@ class LegacyLoginFlow(LoginFlow): async def async_step_init( self, user_input: dict[str, str] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle the step of the form.""" errors = {} diff --git a/homeassistant/auth/providers/trusted_networks.py b/homeassistant/auth/providers/trusted_networks.py index 85b43d89f3fb4..a6a5cfb94f08f 100644 --- a/homeassistant/auth/providers/trusted_networks.py +++ b/homeassistant/auth/providers/trusted_networks.py @@ -5,6 +5,7 @@ """ from __future__ import annotations +from collections.abc import Mapping from ipaddress import ( IPv4Address, IPv4Network, @@ -18,6 +19,7 @@ import voluptuous as vol from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResultDict from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv @@ -127,7 +129,7 @@ async def async_login_flow(self, context: dict | None) -> LoginFlow: ) async def async_get_or_create_credentials( - self, flow_result: dict[str, str] + self, flow_result: Mapping[str, str] ) -> Credentials: """Get credentials based on the flow result.""" user_id = flow_result["user"] @@ -199,7 +201,7 @@ def __init__( async def async_step_init( self, user_input: dict[str, str] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle the step of the form.""" try: cast( diff --git a/homeassistant/components/bond/config_flow.py b/homeassistant/components/bond/config_flow.py index 2e1f106193efe..d4bf0275ad9e3 100644 --- a/homeassistant/components/bond/config_flow.py +++ b/homeassistant/components/bond/config_flow.py @@ -16,6 +16,7 @@ HTTP_UNAUTHORIZED, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultDict from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import DiscoveryInfoType @@ -91,7 +92,9 @@ async def _async_try_automatic_configure(self) -> None: _, hub_name = await _validate_input(self.hass, self._discovered) self._discovered[CONF_NAME] = hub_name - async def async_step_zeroconf(self, discovery_info: DiscoveryInfoType) -> dict[str, Any]: # type: ignore + async def async_step_zeroconf( # type: ignore[override] + self, discovery_info: DiscoveryInfoType + ) -> FlowResultDict: """Handle a flow initialized by zeroconf discovery.""" name: str = discovery_info[CONF_NAME] host: str = discovery_info[CONF_HOST] @@ -115,7 +118,7 @@ async def async_step_zeroconf(self, discovery_info: DiscoveryInfoType) -> dict[s async def async_step_confirm( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle confirmation flow for discovered bond hub.""" errors = {} if user_input is not None: @@ -156,7 +159,7 @@ async def async_step_confirm( async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle a flow initialized by the user.""" errors = {} if user_input is not None: diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index a5a2a1886d744..6dd2a067c8906 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -323,7 +323,7 @@ def get_core_info(hass): @callback @bind_hass -def is_hassio(hass): +def is_hassio(hass: HomeAssistant) -> bool: """Return true if Hass.io is loaded. Async friendly. diff --git a/homeassistant/components/huawei_lte/config_flow.py b/homeassistant/components/huawei_lte/config_flow.py index fc455f865fd30..5f1cdf9325281 100644 --- a/homeassistant/components/huawei_lte/config_flow.py +++ b/homeassistant/components/huawei_lte/config_flow.py @@ -29,6 +29,8 @@ CONF_USERNAME, ) from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResultDict +from homeassistant.helpers.typing import DiscoveryInfoType from .const import ( CONNECTION_TIMEOUT, @@ -58,7 +60,7 @@ async def _async_show_user_form( self, user_input: dict[str, Any] | None = None, errors: dict[str, str] | None = None, - ) -> dict[str, Any]: + ) -> FlowResultDict: if user_input is None: user_input = {} return self.async_show_form( @@ -85,7 +87,7 @@ async def _async_show_user_form( async def async_step_import( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle import initiated config flow.""" return await self.async_step_user(user_input) @@ -99,7 +101,7 @@ def _already_configured(self, user_input: dict[str, Any]) -> bool: async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle user initiated config flow.""" if user_input is None: return await self._async_show_user_form() @@ -211,9 +213,9 @@ def get_router_title(conn: Connection) -> str: return self.async_create_entry(title=title, data=user_input) - async def async_step_ssdp( # type: ignore # mypy says signature incompatible with supertype, but it's the same? - self, discovery_info: dict[str, Any] - ) -> dict[str, Any]: + async def async_step_ssdp( # type: ignore[override] + self, discovery_info: DiscoveryInfoType + ) -> FlowResultDict: """Handle SSDP initiated config flow.""" await self.async_set_unique_id(discovery_info[ssdp.ATTR_UPNP_UDN]) self._abort_if_unique_id_configured() @@ -254,7 +256,7 @@ def __init__(self, config_entry: config_entries.ConfigEntry): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle options flow.""" # Recipients are persisted as a list, but handled as comma separated string in UI diff --git a/homeassistant/components/hyperion/config_flow.py b/homeassistant/components/hyperion/config_flow.py index 7ceedcbf00544..1a087460151fd 100644 --- a/homeassistant/components/hyperion/config_flow.py +++ b/homeassistant/components/hyperion/config_flow.py @@ -27,6 +27,7 @@ CONF_TOKEN, ) from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResultDict import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType @@ -130,7 +131,7 @@ def _create_client(self, raw_connection: bool = False) -> client.HyperionClient: async def _advance_to_auth_step_if_necessary( self, hyperion_client: client.HyperionClient - ) -> dict[str, Any]: + ) -> FlowResultDict: """Determine if auth is required.""" auth_resp = await hyperion_client.async_is_auth_required() @@ -145,7 +146,7 @@ async def _advance_to_auth_step_if_necessary( async def async_step_reauth( self, config_data: ConfigType, - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle a reauthentication flow.""" self._data = dict(config_data) async with self._create_client(raw_connection=True) as hyperion_client: @@ -153,9 +154,7 @@ async def async_step_reauth( return self.async_abort(reason="cannot_connect") return await self._advance_to_auth_step_if_necessary(hyperion_client) - async def async_step_ssdp( # type: ignore[override] - self, discovery_info: dict[str, Any] - ) -> dict[str, Any]: + async def async_step_ssdp(self, discovery_info: dict[str, Any]) -> FlowResultDict: # type: ignore[override] """Handle a flow initiated by SSDP.""" # Sample data provided by SSDP: { # 'ssdp_location': 'http://192.168.0.1:8090/description.xml', @@ -226,7 +225,7 @@ async def async_step_ssdp( # type: ignore[override] async def async_step_user( self, user_input: ConfigType | None = None, - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle a flow initiated by the user.""" errors = {} if user_input: @@ -297,7 +296,7 @@ async def _can_login(self) -> bool | None: async def async_step_auth( self, user_input: ConfigType | None = None, - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle the auth step of a flow.""" errors = {} if user_input: @@ -326,7 +325,7 @@ async def async_step_auth( async def async_step_create_token( self, user_input: ConfigType | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Send a request for a new token.""" if user_input is None: self._auth_id = client.generate_random_auth_id() @@ -352,7 +351,7 @@ async def async_step_create_token( async def async_step_create_token_external( self, auth_resp: ConfigType | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle completion of the request for a new token.""" if auth_resp is not None and client.ResponseOK(auth_resp): token = auth_resp.get(const.KEY_INFO, {}).get(const.KEY_TOKEN) @@ -365,7 +364,7 @@ async def async_step_create_token_external( async def async_step_create_token_success( self, _: ConfigType | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Create an entry after successful token creation.""" # Clean-up the request task. await self._cancel_request_token_task() @@ -381,7 +380,7 @@ async def async_step_create_token_success( async def async_step_create_token_fail( self, _: ConfigType | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Show an error on the auth form.""" # Clean-up the request task. await self._cancel_request_token_task() @@ -389,7 +388,7 @@ async def async_step_create_token_fail( async def async_step_confirm( self, user_input: ConfigType | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Get final confirmation before entry creation.""" if user_input is None and self._require_confirm: return self.async_show_form( @@ -449,7 +448,7 @@ def _create_client(self) -> client.HyperionClient: async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Manage the options.""" effects = {source: source for source in const.KEY_COMPONENTID_EXTERNAL_SOURCES} diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index 313f4e146a51e..a2429a25c1b12 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -14,7 +14,7 @@ from homeassistant.components.hassio import is_hassio from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import AbortFlow +from homeassistant.data_entry_flow import AbortFlow, FlowResultDict from homeassistant.helpers.aiohttp_client import async_get_clientsession from .addon import AddonError, AddonManager, get_addon_manager @@ -89,16 +89,16 @@ def __init__(self) -> None: async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle the initial step.""" - if is_hassio(self.hass): # type: ignore # no-untyped-call + if is_hassio(self.hass): return await self.async_step_on_supervisor() return await self.async_step_manual() async def async_step_manual( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle a manual configuration.""" if user_input is None: return self.async_show_form( @@ -134,9 +134,7 @@ async def async_step_manual( step_id="manual", data_schema=STEP_USER_DATA_SCHEMA, errors=errors ) - async def async_step_hassio( # type: ignore # override - self, discovery_info: dict[str, Any] - ) -> dict[str, Any]: + async def async_step_hassio(self, discovery_info: dict[str, Any]) -> FlowResultDict: # type: ignore[override] """Receive configuration from add-on discovery info. This flow is triggered by the Z-Wave JS add-on. @@ -154,7 +152,7 @@ async def async_step_hassio( # type: ignore # override async def async_step_hassio_confirm( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Confirm the add-on discovery.""" if user_input is not None: return await self.async_step_on_supervisor( @@ -164,7 +162,7 @@ async def async_step_hassio_confirm( return self.async_show_form(step_id="hassio_confirm") @callback - def _async_create_entry_from_vars(self) -> dict[str, Any]: + def _async_create_entry_from_vars(self) -> FlowResultDict: """Return a config entry for the flow.""" return self.async_create_entry( title=TITLE, @@ -179,7 +177,7 @@ def _async_create_entry_from_vars(self) -> dict[str, Any]: async def async_step_on_supervisor( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle logic when on Supervisor host.""" if user_input is None: return self.async_show_form( @@ -203,7 +201,7 @@ async def async_step_on_supervisor( async def async_step_install_addon( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Install Z-Wave JS add-on.""" if not self.install_task: self.install_task = self.hass.async_create_task(self._async_install_addon()) @@ -223,13 +221,13 @@ async def async_step_install_addon( async def async_step_install_failed( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Add-on installation failed.""" return self.async_abort(reason="addon_install_failed") async def async_step_configure_addon( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Ask for config for Z-Wave JS add-on.""" addon_config = await self._async_get_addon_config() @@ -265,7 +263,7 @@ async def async_step_configure_addon( async def async_step_start_addon( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Start Z-Wave JS add-on.""" if not self.start_task: self.start_task = self.hass.async_create_task(self._async_start_addon()) @@ -283,7 +281,7 @@ async def async_step_start_addon( async def async_step_start_failed( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Add-on start failed.""" return self.async_abort(reason="addon_start_failed") @@ -320,7 +318,7 @@ async def _async_start_addon(self) -> None: async def async_step_finish_addon_setup( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Prepare info needed to complete the config entry. Get add-on discovery info and server version info. diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 9df6dff8316c4..c69cd0c9d5b48 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from collections.abc import Mapping from contextvars import ContextVar import functools import logging @@ -21,7 +22,7 @@ ) from homeassistant.helpers import device_registry, entity_registry from homeassistant.helpers.event import Event -from homeassistant.helpers.typing import UNDEFINED, UndefinedType +from homeassistant.helpers.typing import UNDEFINED, DiscoveryInfoType, UndefinedType from homeassistant.setup import async_process_deps_reqs, async_setup_component from homeassistant.util.decorator import Registry import homeassistant.util.uuid as uuid_util @@ -146,7 +147,7 @@ def __init__( version: int, domain: str, title: str, - data: dict, + data: Mapping[str, Any], source: str, connection_class: str, system_options: dict, @@ -559,8 +560,8 @@ def __init__( self._hass_config = hass_config async def async_finish_flow( - self, flow: data_entry_flow.FlowHandler, result: dict[str, Any] - ) -> dict[str, Any]: + self, flow: data_entry_flow.FlowHandler, result: data_entry_flow.FlowResultDict + ) -> data_entry_flow.FlowResultDict: """Finish a config flow and add an entry.""" flow = cast(ConfigFlow, flow) @@ -668,7 +669,7 @@ async def async_create_flow( return flow async def async_post_init( - self, flow: data_entry_flow.FlowHandler, result: dict + self, flow: data_entry_flow.FlowHandler, result: data_entry_flow.FlowResultDict ) -> None: """After a flow is initialised trigger new flow notifications.""" source = flow.context["source"] @@ -931,7 +932,7 @@ def async_update_entry( unique_id: str | dict | None | UndefinedType = UNDEFINED, title: str | dict | UndefinedType = UNDEFINED, data: dict | UndefinedType = UNDEFINED, - options: dict | UndefinedType = UNDEFINED, + options: Mapping[str, Any] | UndefinedType = UNDEFINED, system_options: dict | UndefinedType = UNDEFINED, ) -> bool: """Update a config entry. @@ -956,7 +957,7 @@ def async_update_entry( changed = True entry.data = MappingProxyType(data) - if options is not UNDEFINED and entry.options != options: # type: ignore + if options is not UNDEFINED and entry.options != options: changed = True entry.options = MappingProxyType(options) @@ -1147,7 +1148,9 @@ def _async_current_ids(self, include_ignore: bool = True) -> set[str | None]: } @callback - def _async_in_progress(self, include_uninitialized: bool = False) -> list[dict]: + def _async_in_progress( + self, include_uninitialized: bool = False + ) -> list[data_entry_flow.FlowResultDict]: """Return other in progress flows for current domain.""" return [ flw @@ -1157,18 +1160,22 @@ def _async_in_progress(self, include_uninitialized: bool = False) -> list[dict]: if flw["handler"] == self.handler and flw["flow_id"] != self.flow_id ] - async def async_step_ignore(self, user_input: dict[str, Any]) -> dict[str, Any]: + async def async_step_ignore( + self, user_input: dict[str, Any] + ) -> data_entry_flow.FlowResultDict: """Ignore this config flow.""" await self.async_set_unique_id(user_input["unique_id"], raise_on_progress=False) return self.async_create_entry(title=user_input["title"], data={}) - async def async_step_unignore(self, user_input: dict[str, Any]) -> dict[str, Any]: + async def async_step_unignore( + self, user_input: dict[str, Any] + ) -> data_entry_flow.FlowResultDict: """Rediscover a config entry by it's unique_id.""" return self.async_abort(reason="not_implemented") async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> data_entry_flow.FlowResultDict: """Handle a flow initiated by the user.""" return self.async_abort(reason="not_implemented") @@ -1197,8 +1204,8 @@ async def _async_handle_discovery_without_unique_id(self) -> None: raise data_entry_flow.AbortFlow("already_in_progress") async def async_step_discovery( - self, discovery_info: dict[str, Any] - ) -> dict[str, Any]: + self, discovery_info: DiscoveryInfoType + ) -> data_entry_flow.FlowResultDict: """Handle a flow initialized by discovery.""" await self._async_handle_discovery_without_unique_id() return await self.async_step_user() @@ -1206,7 +1213,7 @@ async def async_step_discovery( @callback def async_abort( self, *, reason: str, description_placeholders: dict | None = None - ) -> dict[str, Any]: + ) -> data_entry_flow.FlowResultDict: """Abort the config flow.""" # Remove reauth notification if no reauth flows are in progress if self.source == SOURCE_REAUTH and not any( @@ -1254,8 +1261,8 @@ async def async_create_flow( return cast(OptionsFlow, HANDLERS[entry.domain].async_get_options_flow(entry)) async def async_finish_flow( - self, flow: data_entry_flow.FlowHandler, result: dict[str, Any] - ) -> dict[str, Any]: + self, flow: data_entry_flow.FlowHandler, result: data_entry_flow.FlowResultDict + ) -> data_entry_flow.FlowResultDict: """Finish an options flow and update options for configuration entry. Flow.handler and entry_id is the same thing to map flow with entry. diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index a9a78337b17c1..3a38cd0da7120 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -5,7 +5,7 @@ import asyncio from collections.abc import Mapping from types import MappingProxyType -from typing import Any +from typing import Any, TypedDict import uuid import voluptuous as vol @@ -51,6 +51,29 @@ def __init__(self, reason: str, description_placeholders: dict | None = None): self.description_placeholders = description_placeholders +class FlowResultDict(TypedDict, total=False): + """Typed result dict.""" + + version: int + type: str + flow_id: str + handler: str + title: str + data: Mapping[str, Any] + step_id: str + data_schema: vol.Schema + extra: str + required: bool + errors: dict[str, str] | None + description: str | None + description_placeholders: dict[str, Any] | None + progress_action: str + url: str + reason: str + context: dict[str, Any] + result: Any + + class FlowManager(abc.ABC): """Manage all the flows that are in progress.""" @@ -88,15 +111,17 @@ async def async_create_flow( @abc.abstractmethod async def async_finish_flow( - self, flow: FlowHandler, result: dict[str, Any] - ) -> dict[str, Any]: + self, flow: FlowHandler, result: FlowResultDict + ) -> FlowResultDict: """Finish a config flow and add an entry.""" - async def async_post_init(self, flow: FlowHandler, result: dict[str, Any]) -> None: + async def async_post_init(self, flow: FlowHandler, result: FlowResultDict) -> None: """Entry has finished executing its first step asynchronously.""" @callback - def async_progress(self, include_uninitialized: bool = False) -> list[dict]: + def async_progress( + self, include_uninitialized: bool = False + ) -> list[FlowResultDict]: """Return the flows in progress.""" return [ { @@ -110,8 +135,8 @@ def async_progress(self, include_uninitialized: bool = False) -> list[dict]: ] async def async_init( - self, handler: str, *, context: dict | None = None, data: Any = None - ) -> Any: + self, handler: str, *, context: dict[str, Any] | None = None, data: Any = None + ) -> FlowResultDict: """Start a configuration flow.""" if context is None: context = {} @@ -160,7 +185,7 @@ async def async_shutdown(self) -> None: async def async_configure( self, flow_id: str, user_input: dict | None = None - ) -> Any: + ) -> FlowResultDict: """Continue a configuration flow.""" flow = self._progress.get(flow_id) @@ -217,7 +242,7 @@ async def _async_handle_step( step_id: str, user_input: dict | None, step_done: asyncio.Future | None = None, - ) -> dict: + ) -> FlowResultDict: """Handle a step of a flow.""" method = f"async_step_{step_id}" @@ -230,7 +255,7 @@ async def _async_handle_step( ) try: - result: dict = await getattr(flow, method)(user_input) + result: FlowResultDict = await getattr(flow, method)(user_input) except AbortFlow as err: result = _create_abort_data( flow.flow_id, flow.handler, err.reason, err.description_placeholders @@ -265,7 +290,7 @@ async def _async_handle_step( return result # We pass a copy of the result because we're mutating our version - result = await self.async_finish_flow(flow, dict(result)) + result = await self.async_finish_flow(flow, result.copy()) # _async_finish_flow may change result type, check it again if result["type"] == RESULT_TYPE_FORM: @@ -288,7 +313,7 @@ class FlowHandler: hass: HomeAssistant = None # type: ignore handler: str = None # type: ignore # Ensure the attribute has a subscriptable, but immutable, default value. - context: dict = MappingProxyType({}) # type: ignore + context: dict[str, Any] = MappingProxyType({}) # type: ignore # Set by _async_create_flow callback init_step = "init" @@ -318,9 +343,9 @@ def async_show_form( *, step_id: str, data_schema: vol.Schema = None, - errors: dict | None = None, - description_placeholders: dict | None = None, - ) -> dict[str, Any]: + errors: dict[str, str] | None = None, + description_placeholders: dict[str, Any] | None = None, + ) -> FlowResultDict: """Return the definition of a form to gather user input.""" return { "type": RESULT_TYPE_FORM, @@ -340,7 +365,7 @@ def async_create_entry( data: Mapping[str, Any], description: str | None = None, description_placeholders: dict | None = None, - ) -> dict[str, Any]: + ) -> FlowResultDict: """Finish config flow and create a config entry.""" return { "version": self.VERSION, @@ -356,7 +381,7 @@ def async_create_entry( @callback def async_abort( self, *, reason: str, description_placeholders: dict | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Abort the config flow.""" return _create_abort_data( self.flow_id, self.handler, reason, description_placeholders @@ -365,7 +390,7 @@ def async_abort( @callback def async_external_step( self, *, step_id: str, url: str, description_placeholders: dict | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Return the definition of an external step for the user to take.""" return { "type": RESULT_TYPE_EXTERNAL_STEP, @@ -377,7 +402,7 @@ def async_external_step( } @callback - def async_external_step_done(self, *, next_step_id: str) -> dict[str, Any]: + def async_external_step_done(self, *, next_step_id: str) -> FlowResultDict: """Return the definition of an external step for the user to take.""" return { "type": RESULT_TYPE_EXTERNAL_STEP_DONE, @@ -393,7 +418,7 @@ def async_show_progress( step_id: str, progress_action: str, description_placeholders: dict | None = None, - ) -> dict[str, Any]: + ) -> FlowResultDict: """Show a progress message to the user, without user input allowed.""" return { "type": RESULT_TYPE_SHOW_PROGRESS, @@ -405,7 +430,7 @@ def async_show_progress( } @callback - def async_show_progress_done(self, *, next_step_id: str) -> dict[str, Any]: + def async_show_progress_done(self, *, next_step_id: str) -> FlowResultDict: """Mark the progress done.""" return { "type": RESULT_TYPE_SHOW_PROGRESS_DONE, @@ -421,7 +446,7 @@ def _create_abort_data( handler: str, reason: str, description_placeholders: dict | None = None, -) -> dict[str, Any]: +) -> FlowResultDict: """Return the definition of an external step for the user to take.""" return { "type": RESULT_TYPE_ABORT, diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index 6abcf0ece56bf..c9ac765ecbbe1 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -5,6 +5,8 @@ from homeassistant import config_entries from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultDict +from homeassistant.helpers.typing import DiscoveryInfoType DiscoveryFunctionType = Callable[[], Union[Awaitable[bool], bool]] @@ -29,7 +31,7 @@ def __init__( async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle a flow initialized by the user.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -40,7 +42,7 @@ async def async_step_user( async def async_step_confirm( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Confirm setup.""" if user_input is None: self._set_confirm_only() @@ -69,8 +71,8 @@ async def async_step_confirm( return self.async_create_entry(title=self._title, data={}) async def async_step_discovery( - self, discovery_info: dict[str, Any] - ) -> dict[str, Any]: + self, discovery_info: DiscoveryInfoType + ) -> FlowResultDict: """Handle a flow initialized by discovery.""" if self._async_in_progress() or self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -85,7 +87,7 @@ async def async_step_discovery( async_step_homekit = async_step_discovery async_step_dhcp = async_step_discovery - async def async_step_import(self, _: dict[str, Any] | None) -> dict[str, Any]: + async def async_step_import(self, _: dict[str, Any] | None) -> FlowResultDict: """Handle a flow initialized by import.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -135,7 +137,7 @@ def __init__( async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle a user initiated set up flow to create a webhook.""" if not self._allow_multiple and self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index 795c08dd1c98f..891d6c7d28c4c 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -23,6 +23,7 @@ from homeassistant import config_entries from homeassistant.components import http from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResultDict from homeassistant.helpers.network import NoURLAvailableError from .aiohttp_client import async_get_clientsession @@ -234,7 +235,7 @@ def extra_authorize_data(self) -> dict: async def async_step_pick_implementation( self, user_input: dict | None = None - ) -> dict: + ) -> FlowResultDict: """Handle a flow start.""" implementations = await async_get_implementations(self.hass, self.DOMAIN) @@ -265,7 +266,7 @@ async def async_step_pick_implementation( async def async_step_auth( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Create an entry for auth.""" # Flow has been triggered by external data if user_input: @@ -291,7 +292,7 @@ async def async_step_auth( async def async_step_creation( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Create config entry from external data.""" token = await self.flow_impl.async_resolve_external_data(self.external_data) # Force int for non-compliant oauth2 providers @@ -308,7 +309,7 @@ async def async_step_creation( {"auth_implementation": self.flow_impl.domain, "token": token} ) - async def async_oauth_create_entry(self, data: dict) -> dict: + async def async_oauth_create_entry(self, data: dict) -> FlowResultDict: """Create an entry for the flow. Ok to override if you want to fetch extra info or even add another step. diff --git a/homeassistant/helpers/data_entry_flow.py b/homeassistant/helpers/data_entry_flow.py index 00d12d3ab907d..af0ea22d50359 100644 --- a/homeassistant/helpers/data_entry_flow.py +++ b/homeassistant/helpers/data_entry_flow.py @@ -21,7 +21,9 @@ def __init__(self, flow_mgr: data_entry_flow.FlowManager) -> None: self._flow_mgr = flow_mgr # pylint: disable=no-self-use - def _prepare_result_json(self, result: dict[str, Any]) -> dict[str, Any]: + def _prepare_result_json( + self, result: data_entry_flow.FlowResultDict + ) -> data_entry_flow.FlowResultDict: """Convert result to JSON.""" if result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY: data = result.copy() From 31c519b26dff10d52f0372e0c772b08a4dcf2b00 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Thu, 15 Apr 2021 20:52:06 +0300 Subject: [PATCH 0289/1317] Fix shelly RSSI sensor unit (#49265) --- homeassistant/components/shelly/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index b6d3bc2dbff3e..a7d2e1e72ce9c 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -9,7 +9,7 @@ LIGHT_LUX, PERCENTAGE, POWER_WATT, - SIGNAL_STRENGTH_DECIBELS, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, VOLT, ) @@ -178,7 +178,7 @@ REST_SENSORS = { "rssi": RestAttributeDescription( name="RSSI", - unit=SIGNAL_STRENGTH_DECIBELS, + unit=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, value=lambda status, _: status["wifi_sta"]["rssi"], device_class=sensor.DEVICE_CLASS_SIGNAL_STRENGTH, default_enabled=False, From 38f0c201c243963af67bd9e33f75a0145626d795 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Thu, 15 Apr 2021 20:53:03 +0300 Subject: [PATCH 0290/1317] Fix Tasmota Wifi Signal Strength unit (#49263) --- homeassistant/components/tasmota/manifest.json | 2 +- homeassistant/components/tasmota/sensor.py | 2 ++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index c6a77d40c83c7..c1c668a762ebc 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -3,7 +3,7 @@ "name": "Tasmota", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tasmota", - "requirements": ["hatasmota==0.2.9"], + "requirements": ["hatasmota==0.2.10"], "dependencies": ["mqtt"], "mqtt": ["tasmota/discovery/#"], "codeowners": ["@emontnemery"], diff --git a/homeassistant/components/tasmota/sensor.py b/homeassistant/components/tasmota/sensor.py index 432fc2266f3ad..02a044671946c 100644 --- a/homeassistant/components/tasmota/sensor.py +++ b/homeassistant/components/tasmota/sensor.py @@ -29,6 +29,7 @@ POWER_WATT, PRESSURE_HPA, SIGNAL_STRENGTH_DECIBELS, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, SPEED_KILOMETERS_PER_HOUR, SPEED_METERS_PER_SECOND, SPEED_MILES_PER_HOUR, @@ -113,6 +114,7 @@ hc.POWER_WATT: POWER_WATT, hc.PRESSURE_HPA: PRESSURE_HPA, hc.SIGNAL_STRENGTH_DECIBELS: SIGNAL_STRENGTH_DECIBELS, + hc.SIGNAL_STRENGTH_DECIBELS_MILLIWATT: SIGNAL_STRENGTH_DECIBELS_MILLIWATT, hc.SPEED_KILOMETERS_PER_HOUR: SPEED_KILOMETERS_PER_HOUR, hc.SPEED_METERS_PER_SECOND: SPEED_METERS_PER_SECOND, hc.SPEED_MILES_PER_HOUR: SPEED_MILES_PER_HOUR, diff --git a/requirements_all.txt b/requirements_all.txt index 35b248b8d2103..c06ed6a03cfc9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -735,7 +735,7 @@ hass-nabucasa==0.43.0 hass_splunk==0.1.1 # homeassistant.components.tasmota -hatasmota==0.2.9 +hatasmota==0.2.10 # homeassistant.components.jewish_calendar hdate==0.10.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1179cf735fda0..0419b9b3c5f97 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -405,7 +405,7 @@ hangups==0.4.11 hass-nabucasa==0.43.0 # homeassistant.components.tasmota -hatasmota==0.2.9 +hatasmota==0.2.10 # homeassistant.components.jewish_calendar hdate==0.10.2 From 236d274351fd14da72898819660e742c2b5267a8 Mon Sep 17 00:00:00 2001 From: Mike Degatano Date: Thu, 15 Apr 2021 14:13:27 -0400 Subject: [PATCH 0291/1317] Add `search` and `match` as Jinja tests (#49229) --- homeassistant/helpers/template.py | 2 ++ tests/helpers/test_template.py | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 7909572bede99..b024c8f265612 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -1459,6 +1459,8 @@ def __init__(self, hass, limited=False, strict=False): self.globals["urlencode"] = urlencode self.globals["max"] = max self.globals["min"] = min + self.tests["match"] = regex_match + self.tests["search"] = regex_search if hass is None: return diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 0e8b2f76843a7..46098917b0e6e 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -1003,6 +1003,17 @@ def test_regex_match(hass): assert tpl.async_render() is True +def test_match_test(hass): + """Test match test.""" + tpl = template.Template( + r""" +{{ '123-456-7890' is match('(\\d{3})-(\\d{3})-(\\d{4})') }} + """, + hass, + ) + assert tpl.async_render() is True + + def test_regex_search(hass): """Test regex_search method.""" tpl = template.Template( @@ -1038,6 +1049,17 @@ def test_regex_search(hass): assert tpl.async_render() is True +def test_search_test(hass): + """Test search test.""" + tpl = template.Template( + r""" +{{ '123-456-7890' is search('(\\d{3})-(\\d{3})-(\\d{4})') }} + """, + hass, + ) + assert tpl.async_render() is True + + def test_regex_replace(hass): """Test regex_replace method.""" tpl = template.Template( From 3d90d6073ec2dbd25ffdacca71ccacf095407b73 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 15 Apr 2021 20:32:27 +0200 Subject: [PATCH 0292/1317] Add common light helpers to test for feature support (#49199) --- homeassistant/components/alexa/entities.py | 8 +++---- .../components/google_assistant/trait.py | 22 +++++++++---------- .../components/homekit/type_lights.py | 16 +++++++------- homeassistant/components/light/__init__.py | 21 ++++++++++++++++++ 4 files changed, 44 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index cbeb3a869ddf1..c6ae05e9d6f8f 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -504,12 +504,12 @@ def interfaces(self): """Yield the supported interfaces.""" yield AlexaPowerController(self.entity) - color_modes = self.entity.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES, []) - if any(mode in color_modes for mode in light.COLOR_MODES_BRIGHTNESS): + color_modes = self.entity.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) + if light.brightness_supported(color_modes): yield AlexaBrightnessController(self.entity) - if any(mode in color_modes for mode in light.COLOR_MODES_COLOR): + if light.color_supported(color_modes): yield AlexaColorController(self.entity) - if light.COLOR_MODE_COLOR_TEMP in color_modes: + if light.color_temp_supported(color_modes): yield AlexaColorTemperatureController(self.entity) yield AlexaEndpointHealth(self.hass, self.entity) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 3bfce41ae2b74..25013dad1713a 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -214,9 +214,9 @@ class BrightnessTrait(_Trait): @staticmethod def supported(domain, features, device_class, attributes): """Test if state is supported.""" - color_modes = attributes.get(light.ATTR_SUPPORTED_COLOR_MODES, []) if domain == light.DOMAIN: - return any(mode in color_modes for mode in light.COLOR_MODES_BRIGHTNESS) + color_modes = attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) + return light.brightness_supported(color_modes) return False @@ -368,21 +368,21 @@ def supported(domain, features, device_class, attributes): if domain != light.DOMAIN: return False - color_modes = attributes.get(light.ATTR_SUPPORTED_COLOR_MODES, []) - return light.COLOR_MODE_COLOR_TEMP in color_modes or any( - mode in color_modes for mode in light.COLOR_MODES_COLOR + color_modes = attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) + return light.color_temp_supported(color_modes) or light.color_supported( + color_modes ) def sync_attributes(self): """Return color temperature attributes for a sync request.""" attrs = self.state.attributes - color_modes = attrs.get(light.ATTR_SUPPORTED_COLOR_MODES, []) + color_modes = attrs.get(light.ATTR_SUPPORTED_COLOR_MODES) response = {} - if any(mode in color_modes for mode in light.COLOR_MODES_COLOR): + if light.color_supported(color_modes): response["colorModel"] = "hsv" - if light.COLOR_MODE_COLOR_TEMP in color_modes: + if light.color_temp_supported(color_modes): # Max Kelvin is Min Mireds K = 1000000 / mireds # Min Kelvin is Max Mireds K = 1000000 / mireds response["colorTemperatureRange"] = { @@ -398,10 +398,10 @@ def sync_attributes(self): def query_attributes(self): """Return color temperature query attributes.""" - color_modes = self.state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES, []) + color_modes = self.state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) color = {} - if any(mode in color_modes for mode in light.COLOR_MODES_COLOR): + if light.color_supported(color_modes): color_hs = self.state.attributes.get(light.ATTR_HS_COLOR) brightness = self.state.attributes.get(light.ATTR_BRIGHTNESS, 1) if color_hs is not None: @@ -411,7 +411,7 @@ def query_attributes(self): "value": brightness / 255, } - if light.COLOR_MODE_COLOR_TEMP in color_modes: + if light.color_temp_supported(color_modes): temp = self.state.attributes.get(light.ATTR_COLOR_TEMP) # Some faulty integrations might put 0 in here, raising exception. if temp == 0: diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index 614d9427ba66d..cb3c97fadb4fb 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -11,10 +11,10 @@ ATTR_MAX_MIREDS, ATTR_MIN_MIREDS, ATTR_SUPPORTED_COLOR_MODES, - COLOR_MODE_COLOR_TEMP, - COLOR_MODES_BRIGHTNESS, - COLOR_MODES_COLOR, DOMAIN, + brightness_supported, + color_supported, + color_temp_supported, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -62,15 +62,15 @@ def __init__(self, *args): state = self.hass.states.get(self.entity_id) self._features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - self._color_modes = state.attributes.get(ATTR_SUPPORTED_COLOR_MODES, []) + self._color_modes = state.attributes.get(ATTR_SUPPORTED_COLOR_MODES) - if any(mode in self._color_modes for mode in COLOR_MODES_BRIGHTNESS): + if brightness_supported(self._color_modes): self.chars.append(CHAR_BRIGHTNESS) - if any(mode in self._color_modes for mode in COLOR_MODES_COLOR): + if color_supported(self._color_modes): self.chars.append(CHAR_HUE) self.chars.append(CHAR_SATURATION) - elif COLOR_MODE_COLOR_TEMP in self._color_modes: + elif color_temp_supported(self._color_modes): # ColorTemperature and Hue characteristic should not be # exposed both. Both states are tracked separately in HomeKit, # causing "source of truth" problems. @@ -132,7 +132,7 @@ def _set_chars(self, char_values): events.append(f"color temperature at {char_values[CHAR_COLOR_TEMPERATURE]}") if ( - any(mode in self._color_modes for mode in COLOR_MODES_COLOR) + color_supported(self._color_modes) and CHAR_HUE in char_values and CHAR_SATURATION in char_values ): diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index bfdb723e159d4..1b55aa51c4539 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -87,6 +87,27 @@ def valid_supported_color_modes(color_modes): return color_modes +def brightness_supported(color_modes): + """Test if brightness is supported.""" + if not color_modes: + return False + return any(mode in COLOR_MODES_BRIGHTNESS for mode in color_modes) + + +def color_supported(color_modes): + """Test if color is supported.""" + if not color_modes: + return False + return any(mode in COLOR_MODES_COLOR for mode in color_modes) + + +def color_temp_supported(color_modes): + """Test if color temperature is supported.""" + if not color_modes: + return False + return COLOR_MODE_COLOR_TEMP in color_modes + + # Float that represents transition time in seconds to make change. ATTR_TRANSITION = "transition" From 5a01addd67cb3fad27b4b99712850bc4005436ed Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Thu, 15 Apr 2021 21:32:52 +0200 Subject: [PATCH 0293/1317] Add support for multiple AdGuard instances (#49116) --- homeassistant/components/adguard/__init__.py | 24 +++++--- .../components/adguard/config_flow.py | 11 +++- homeassistant/components/adguard/const.py | 2 +- homeassistant/components/adguard/sensor.py | 55 ++++++++++++------- homeassistant/components/adguard/switch.py | 46 +++++++++------- tests/components/adguard/test_config_flow.py | 24 ++++---- 6 files changed, 98 insertions(+), 64 deletions(-) diff --git a/homeassistant/components/adguard/__init__.py b/homeassistant/components/adguard/__init__.py index 0f10d20ec593f..be465b1e1a752 100644 --- a/homeassistant/components/adguard/__init__.py +++ b/homeassistant/components/adguard/__init__.py @@ -10,7 +10,7 @@ from homeassistant.components.adguard.const import ( CONF_FORCE, DATA_ADGUARD_CLIENT, - DATA_ADGUARD_VERION, + DATA_ADGUARD_VERSION, DOMAIN, SERVICE_ADD_URL, SERVICE_DISABLE_URL, @@ -61,16 +61,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: session=session, ) - hass.data.setdefault(DOMAIN, {})[DATA_ADGUARD_CLIENT] = adguard + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {DATA_ADGUARD_CLIENT: adguard} try: await adguard.version() except AdGuardHomeConnectionError as exception: raise ConfigEntryNotReady from exception - for platform in PLATFORMS: + for component in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) + hass.config_entries.async_forward_entry_setup(entry, component) ) async def add_url(call) -> None: @@ -126,8 +126,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.services.async_remove(DOMAIN, SERVICE_DISABLE_URL) hass.services.async_remove(DOMAIN, SERVICE_REFRESH) - for platform in PLATFORMS: - await hass.config_entries.async_forward_entry_unload(entry, platform) + for component in PLATFORMS: + await hass.config_entries.async_forward_entry_unload(entry, component) del hass.data[DOMAIN] @@ -138,13 +138,19 @@ class AdGuardHomeEntity(Entity): """Defines a base AdGuard Home entity.""" def __init__( - self, adguard, name: str, icon: str, enabled_default: bool = True + self, + adguard: AdGuardHome, + entry: ConfigEntry, + name: str, + icon: str, + enabled_default: bool = True, ) -> None: """Initialize the AdGuard Home entity.""" self._available = True self._enabled_default = enabled_default self._icon = icon self._name = name + self._entry = entry self.adguard = adguard @property @@ -200,6 +206,8 @@ def device_info(self) -> dict[str, Any]: }, "name": "AdGuard Home", "manufacturer": "AdGuard Team", - "sw_version": self.hass.data[DOMAIN].get(DATA_ADGUARD_VERION), + "sw_version": self.hass.data[DOMAIN][self._entry.entry_id].get( + DATA_ADGUARD_VERSION + ), "entry_type": "service", } diff --git a/homeassistant/components/adguard/config_flow.py b/homeassistant/components/adguard/config_flow.py index d5ec79d788f4a..d8e657dfc763b 100644 --- a/homeassistant/components/adguard/config_flow.py +++ b/homeassistant/components/adguard/config_flow.py @@ -63,12 +63,17 @@ async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> dict[str, Any]: """Handle a flow initiated by the user.""" - if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") - if user_input is None: return await self._show_setup_form(user_input) + entries = self._async_current_entries() + for entry in entries: + if ( + entry.data[CONF_HOST] == user_input[CONF_HOST] + and entry.data[CONF_PORT] == user_input[CONF_PORT] + ): + return self.async_abort(reason="single_instance_allowed") + errors = {} session = async_get_clientsession(self.hass, user_input[CONF_VERIFY_SSL]) diff --git a/homeassistant/components/adguard/const.py b/homeassistant/components/adguard/const.py index c77d76a70cfa3..8bfa5b49fc678 100644 --- a/homeassistant/components/adguard/const.py +++ b/homeassistant/components/adguard/const.py @@ -3,7 +3,7 @@ DOMAIN = "adguard" DATA_ADGUARD_CLIENT = "adguard_client" -DATA_ADGUARD_VERION = "adguard_version" +DATA_ADGUARD_VERSION = "adguard_version" CONF_FORCE = "force" diff --git a/homeassistant/components/adguard/sensor.py b/homeassistant/components/adguard/sensor.py index dd0400b659260..012df197684ff 100644 --- a/homeassistant/components/adguard/sensor.py +++ b/homeassistant/components/adguard/sensor.py @@ -14,7 +14,7 @@ from homeassistant.helpers.entity import Entity from . import AdGuardHomeDeviceEntity -from .const import DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERION, DOMAIN +from .const import DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERSION, DOMAIN SCAN_INTERVAL = timedelta(seconds=300) PARALLEL_UPDATES = 4 @@ -26,24 +26,24 @@ async def async_setup_entry( async_add_entities: Callable[[list[Entity], bool], None], ) -> None: """Set up AdGuard Home sensor based on a config entry.""" - adguard = hass.data[DOMAIN][DATA_ADGUARD_CLIENT] + adguard = hass.data[DOMAIN][entry.entry_id][DATA_ADGUARD_CLIENT] try: version = await adguard.version() except AdGuardHomeConnectionError as exception: raise PlatformNotReady from exception - hass.data[DOMAIN][DATA_ADGUARD_VERION] = version + hass.data[DOMAIN][entry.entry_id][DATA_ADGUARD_VERSION] = version sensors = [ - AdGuardHomeDNSQueriesSensor(adguard), - AdGuardHomeBlockedFilteringSensor(adguard), - AdGuardHomePercentageBlockedSensor(adguard), - AdGuardHomeReplacedParentalSensor(adguard), - AdGuardHomeReplacedSafeBrowsingSensor(adguard), - AdGuardHomeReplacedSafeSearchSensor(adguard), - AdGuardHomeAverageProcessingTimeSensor(adguard), - AdGuardHomeRulesCountSensor(adguard), + AdGuardHomeDNSQueriesSensor(adguard, entry), + AdGuardHomeBlockedFilteringSensor(adguard, entry), + AdGuardHomePercentageBlockedSensor(adguard, entry), + AdGuardHomeReplacedParentalSensor(adguard, entry), + AdGuardHomeReplacedSafeBrowsingSensor(adguard, entry), + AdGuardHomeReplacedSafeSearchSensor(adguard, entry), + AdGuardHomeAverageProcessingTimeSensor(adguard, entry), + AdGuardHomeRulesCountSensor(adguard, entry), ] async_add_entities(sensors, True) @@ -55,6 +55,7 @@ class AdGuardHomeSensor(AdGuardHomeDeviceEntity, SensorEntity): def __init__( self, adguard: AdGuardHome, + entry: ConfigEntry, name: str, icon: str, measurement: str, @@ -66,7 +67,7 @@ def __init__( self._unit_of_measurement = unit_of_measurement self.measurement = measurement - super().__init__(adguard, name, icon, enabled_default) + super().__init__(adguard, entry, name, icon, enabled_default) @property def unique_id(self) -> str: @@ -95,10 +96,15 @@ def unit_of_measurement(self) -> str | None: class AdGuardHomeDNSQueriesSensor(AdGuardHomeSensor): """Defines a AdGuard Home DNS Queries sensor.""" - def __init__(self, adguard: AdGuardHome) -> None: + def __init__(self, adguard: AdGuardHome, entry: ConfigEntry): """Initialize AdGuard Home sensor.""" super().__init__( - adguard, "AdGuard DNS Queries", "mdi:magnify", "dns_queries", "queries" + adguard, + entry, + "AdGuard DNS Queries", + "mdi:magnify", + "dns_queries", + "queries", ) async def _adguard_update(self) -> None: @@ -109,10 +115,11 @@ async def _adguard_update(self) -> None: class AdGuardHomeBlockedFilteringSensor(AdGuardHomeSensor): """Defines a AdGuard Home blocked by filtering sensor.""" - def __init__(self, adguard: AdGuardHome) -> None: + def __init__(self, adguard: AdGuardHome, entry: ConfigEntry): """Initialize AdGuard Home sensor.""" super().__init__( adguard, + entry, "AdGuard DNS Queries Blocked", "mdi:magnify-close", "blocked_filtering", @@ -128,10 +135,11 @@ async def _adguard_update(self) -> None: class AdGuardHomePercentageBlockedSensor(AdGuardHomeSensor): """Defines a AdGuard Home blocked percentage sensor.""" - def __init__(self, adguard: AdGuardHome) -> None: + def __init__(self, adguard: AdGuardHome, entry: ConfigEntry): """Initialize AdGuard Home sensor.""" super().__init__( adguard, + entry, "AdGuard DNS Queries Blocked Ratio", "mdi:magnify-close", "blocked_percentage", @@ -147,10 +155,11 @@ async def _adguard_update(self) -> None: class AdGuardHomeReplacedParentalSensor(AdGuardHomeSensor): """Defines a AdGuard Home replaced by parental control sensor.""" - def __init__(self, adguard: AdGuardHome) -> None: + def __init__(self, adguard: AdGuardHome, entry: ConfigEntry): """Initialize AdGuard Home sensor.""" super().__init__( adguard, + entry, "AdGuard Parental Control Blocked", "mdi:human-male-girl", "blocked_parental", @@ -165,10 +174,11 @@ async def _adguard_update(self) -> None: class AdGuardHomeReplacedSafeBrowsingSensor(AdGuardHomeSensor): """Defines a AdGuard Home replaced by safe browsing sensor.""" - def __init__(self, adguard: AdGuardHome) -> None: + def __init__(self, adguard: AdGuardHome, entry: ConfigEntry): """Initialize AdGuard Home sensor.""" super().__init__( adguard, + entry, "AdGuard Safe Browsing Blocked", "mdi:shield-half-full", "blocked_safebrowsing", @@ -183,10 +193,11 @@ async def _adguard_update(self) -> None: class AdGuardHomeReplacedSafeSearchSensor(AdGuardHomeSensor): """Defines a AdGuard Home replaced by safe search sensor.""" - def __init__(self, adguard: AdGuardHome) -> None: + def __init__(self, adguard: AdGuardHome, entry: ConfigEntry): """Initialize AdGuard Home sensor.""" super().__init__( adguard, + entry, "AdGuard Safe Searches Enforced", "mdi:shield-search", "enforced_safesearch", @@ -201,10 +212,11 @@ async def _adguard_update(self) -> None: class AdGuardHomeAverageProcessingTimeSensor(AdGuardHomeSensor): """Defines a AdGuard Home average processing time sensor.""" - def __init__(self, adguard: AdGuardHome) -> None: + def __init__(self, adguard: AdGuardHome, entry: ConfigEntry): """Initialize AdGuard Home sensor.""" super().__init__( adguard, + entry, "AdGuard Average Processing Speed", "mdi:speedometer", "average_speed", @@ -220,10 +232,11 @@ async def _adguard_update(self) -> None: class AdGuardHomeRulesCountSensor(AdGuardHomeSensor): """Defines a AdGuard Home rules count sensor.""" - def __init__(self, adguard: AdGuardHome) -> None: + def __init__(self, adguard: AdGuardHome, entry: ConfigEntry): """Initialize AdGuard Home sensor.""" super().__init__( adguard, + entry, "AdGuard Rules Count", "mdi:counter", "rules_count", diff --git a/homeassistant/components/adguard/switch.py b/homeassistant/components/adguard/switch.py index 0b127a280cfad..22b4e8319f359 100644 --- a/homeassistant/components/adguard/switch.py +++ b/homeassistant/components/adguard/switch.py @@ -14,7 +14,7 @@ from homeassistant.helpers.entity import Entity from . import AdGuardHomeDeviceEntity -from .const import DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERION, DOMAIN +from .const import DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERSION, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -28,22 +28,22 @@ async def async_setup_entry( async_add_entities: Callable[[list[Entity], bool], None], ) -> None: """Set up AdGuard Home switch based on a config entry.""" - adguard = hass.data[DOMAIN][DATA_ADGUARD_CLIENT] + adguard = hass.data[DOMAIN][entry.entry_id][DATA_ADGUARD_CLIENT] try: version = await adguard.version() except AdGuardHomeConnectionError as exception: raise PlatformNotReady from exception - hass.data[DOMAIN][DATA_ADGUARD_VERION] = version + hass.data[DOMAIN][entry.entry_id][DATA_ADGUARD_VERSION] = version switches = [ - AdGuardHomeProtectionSwitch(adguard), - AdGuardHomeFilteringSwitch(adguard), - AdGuardHomeParentalSwitch(adguard), - AdGuardHomeSafeBrowsingSwitch(adguard), - AdGuardHomeSafeSearchSwitch(adguard), - AdGuardHomeQueryLogSwitch(adguard), + AdGuardHomeProtectionSwitch(adguard, entry), + AdGuardHomeFilteringSwitch(adguard, entry), + AdGuardHomeParentalSwitch(adguard, entry), + AdGuardHomeSafeBrowsingSwitch(adguard, entry), + AdGuardHomeSafeSearchSwitch(adguard, entry), + AdGuardHomeQueryLogSwitch(adguard, entry), ] async_add_entities(switches, True) @@ -54,6 +54,7 @@ class AdGuardHomeSwitch(AdGuardHomeDeviceEntity, SwitchEntity): def __init__( self, adguard: AdGuardHome, + entry: ConfigEntry, name: str, icon: str, key: str, @@ -62,7 +63,7 @@ def __init__( """Initialize AdGuard Home switch.""" self._state = False self._key = key - super().__init__(adguard, name, icon, enabled_default) + super().__init__(adguard, entry, name, icon, enabled_default) @property def unique_id(self) -> str: @@ -104,10 +105,10 @@ async def _adguard_turn_on(self) -> None: class AdGuardHomeProtectionSwitch(AdGuardHomeSwitch): """Defines a AdGuard Home protection switch.""" - def __init__(self, adguard: AdGuardHome) -> None: + def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: """Initialize AdGuard Home switch.""" super().__init__( - adguard, "AdGuard Protection", "mdi:shield-check", "protection" + adguard, entry, "AdGuard Protection", "mdi:shield-check", "protection" ) async def _adguard_turn_off(self) -> None: @@ -126,10 +127,10 @@ async def _adguard_update(self) -> None: class AdGuardHomeParentalSwitch(AdGuardHomeSwitch): """Defines a AdGuard Home parental control switch.""" - def __init__(self, adguard: AdGuardHome) -> None: + def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: """Initialize AdGuard Home switch.""" super().__init__( - adguard, "AdGuard Parental Control", "mdi:shield-check", "parental" + adguard, entry, "AdGuard Parental Control", "mdi:shield-check", "parental" ) async def _adguard_turn_off(self) -> None: @@ -148,10 +149,10 @@ async def _adguard_update(self) -> None: class AdGuardHomeSafeSearchSwitch(AdGuardHomeSwitch): """Defines a AdGuard Home safe search switch.""" - def __init__(self, adguard: AdGuardHome) -> None: + def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: """Initialize AdGuard Home switch.""" super().__init__( - adguard, "AdGuard Safe Search", "mdi:shield-check", "safesearch" + adguard, entry, "AdGuard Safe Search", "mdi:shield-check", "safesearch" ) async def _adguard_turn_off(self) -> None: @@ -170,10 +171,10 @@ async def _adguard_update(self) -> None: class AdGuardHomeSafeBrowsingSwitch(AdGuardHomeSwitch): """Defines a AdGuard Home safe search switch.""" - def __init__(self, adguard: AdGuardHome) -> None: + def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: """Initialize AdGuard Home switch.""" super().__init__( - adguard, "AdGuard Safe Browsing", "mdi:shield-check", "safebrowsing" + adguard, entry, "AdGuard Safe Browsing", "mdi:shield-check", "safebrowsing" ) async def _adguard_turn_off(self) -> None: @@ -192,9 +193,11 @@ async def _adguard_update(self) -> None: class AdGuardHomeFilteringSwitch(AdGuardHomeSwitch): """Defines a AdGuard Home filtering switch.""" - def __init__(self, adguard: AdGuardHome) -> None: + def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: """Initialize AdGuard Home switch.""" - super().__init__(adguard, "AdGuard Filtering", "mdi:shield-check", "filtering") + super().__init__( + adguard, entry, "AdGuard Filtering", "mdi:shield-check", "filtering" + ) async def _adguard_turn_off(self) -> None: """Turn off the switch.""" @@ -212,10 +215,11 @@ async def _adguard_update(self) -> None: class AdGuardHomeQueryLogSwitch(AdGuardHomeSwitch): """Defines a AdGuard Home query log switch.""" - def __init__(self, adguard: AdGuardHome) -> None: + def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: """Initialize AdGuard Home switch.""" super().__init__( adguard, + entry, "AdGuard Query Log", "mdi:shield-check", "querylog", diff --git a/tests/components/adguard/test_config_flow.py b/tests/components/adguard/test_config_flow.py index 94760cade9fa4..0923883274bf0 100644 --- a/tests/components/adguard/test_config_flow.py +++ b/tests/components/adguard/test_config_flow.py @@ -92,10 +92,14 @@ async def test_full_flow_implementation( async def test_integration_already_exists(hass: HomeAssistant) -> None: """Test we only allow a single config flow.""" - MockConfigEntry(domain=DOMAIN).add_to_hass(hass) + MockConfigEntry( + domain=DOMAIN, data={"host": "mock-adguard", "port": "3000"} + ).add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, + data={"host": "mock-adguard", "port": "3000"}, + context={"source": "user"}, ) assert result["type"] == "abort" assert result["reason"] == "single_instance_allowed" @@ -104,11 +108,11 @@ async def test_integration_already_exists(hass: HomeAssistant) -> None: async def test_hassio_single_instance(hass: HomeAssistant) -> None: """Test we only allow a single config flow.""" MockConfigEntry( - domain="adguard", data={"host": "mock-adguard", "port": "3000"} + domain=DOMAIN, data={"host": "mock-adguard", "port": "3000"} ).add_to_hass(hass) result = await hass.config_entries.flow.async_init( - "adguard", + DOMAIN, data={"addon": "AdGuard Home Addon", "host": "mock-adguard", "port": "3000"}, context={"source": "hassio"}, ) @@ -119,13 +123,13 @@ async def test_hassio_single_instance(hass: HomeAssistant) -> None: async def test_hassio_update_instance_not_running(hass: HomeAssistant) -> None: """Test we only allow a single config flow.""" entry = MockConfigEntry( - domain="adguard", data={"host": "mock-adguard", "port": "3000"} + domain=DOMAIN, data={"host": "mock-adguard", "port": "3000"} ) entry.add_to_hass(hass) assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED result = await hass.config_entries.flow.async_init( - "adguard", + DOMAIN, data={ "addon": "AdGuard Home Addon", "host": "mock-adguard-updated", @@ -153,7 +157,7 @@ async def test_hassio_update_instance_running( ) entry = MockConfigEntry( - domain="adguard", + domain=DOMAIN, data={ "host": "mock-adguard", "port": "3000", @@ -184,7 +188,7 @@ async def test_hassio_update_instance_running( return_value=True, ) as mock_load: result = await hass.config_entries.flow.async_init( - "adguard", + DOMAIN, data={ "addon": "AdGuard Home Addon", "host": "mock-adguard-updated", @@ -211,7 +215,7 @@ async def test_hassio_confirm( ) result = await hass.config_entries.flow.async_init( - "adguard", + DOMAIN, data={"addon": "AdGuard Home Addon", "host": "mock-adguard", "port": 3000}, context={"source": "hassio"}, ) @@ -239,7 +243,7 @@ async def test_hassio_connection_error( ) result = await hass.config_entries.flow.async_init( - "adguard", + DOMAIN, data={"addon": "AdGuard Home Addon", "host": "mock-adguard", "port": 3000}, context={"source": "hassio"}, ) From 5fb36ad9e110d0ef5bc53c48feee46bd2770675b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 15 Apr 2021 09:59:52 -1000 Subject: [PATCH 0294/1317] Add missing typing to data_entry_flow (#49271) --- homeassistant/data_entry_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 3a38cd0da7120..b75d956c5273f 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -164,7 +164,7 @@ async def _async_init( handler: str, context: dict, data: Any, - ) -> tuple[FlowHandler, Any]: + ) -> tuple[FlowHandler, FlowResultDict]: """Run the init in a task to allow it to be canceled at shutdown.""" flow = await self.async_create_flow(handler, context=context, data=data) if not flow: From 898a1a17be0e8519c5d3a4c34635c63daa5b96a4 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 15 Apr 2021 16:31:59 -0400 Subject: [PATCH 0295/1317] Add sensors for other ClimaCell data (#49259) * Add sensors for other ClimaCell data * add tests and add rounding * docstrings * fix pressure * Update homeassistant/components/climacell/sensor.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/climacell/sensor.py Co-authored-by: Martin Hjelmare * review comments * add another abstractmethod * use superscript * remove mypy ignore Co-authored-by: Martin Hjelmare --- .../components/climacell/__init__.py | 44 ++--- homeassistant/components/climacell/const.py | 186 +++++++++++++++++- homeassistant/components/climacell/sensor.py | 152 ++++++++++++++ homeassistant/components/climacell/weather.py | 165 ++++++++++------ tests/components/climacell/test_sensor.py | 148 ++++++++++++++ tests/components/climacell/test_weather.py | 71 +++---- tests/fixtures/climacell/v3_realtime.json | 53 +++++ tests/fixtures/climacell/v4.json | 17 +- 8 files changed, 717 insertions(+), 119 deletions(-) create mode 100644 homeassistant/components/climacell/sensor.py create mode 100644 tests/components/climacell/test_sensor.py diff --git a/homeassistant/components/climacell/__init__.py b/homeassistant/components/climacell/__init__.py index 39412520653d0..20a8dd4483e60 100644 --- a/homeassistant/components/climacell/__init__.py +++ b/homeassistant/components/climacell/__init__.py @@ -16,6 +16,7 @@ UnknownException, ) +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -34,6 +35,7 @@ ) from .const import ( + ATTR_FIELD, ATTRIBUTION, CC_ATTR_CLOUD_COVER, CC_ATTR_CONDITION, @@ -50,6 +52,7 @@ CC_ATTR_WIND_DIRECTION, CC_ATTR_WIND_GUST, CC_ATTR_WIND_SPEED, + CC_SENSOR_TYPES, CC_V3_ATTR_CLOUD_COVER, CC_V3_ATTR_CONDITION, CC_V3_ATTR_HUMIDITY, @@ -64,8 +67,8 @@ CC_V3_ATTR_WIND_DIRECTION, CC_V3_ATTR_WIND_GUST, CC_V3_ATTR_WIND_SPEED, + CC_V3_SENSOR_TYPES, CONF_TIMESTEP, - DEFAULT_FORECAST_TYPE, DEFAULT_TIMESTEP, DOMAIN, MAX_REQUESTS_PER_DAY, @@ -73,7 +76,7 @@ _LOGGER = logging.getLogger(__name__) -PLATFORMS = [WEATHER_DOMAIN] +PLATFORMS = [SENSOR_DOMAIN, WEATHER_DOMAIN] def _set_update_interval( @@ -232,6 +235,10 @@ async def _async_update_data(self) -> dict[str, Any]: CC_V3_ATTR_WIND_GUST, CC_V3_ATTR_CLOUD_COVER, CC_V3_ATTR_PRECIPITATION_TYPE, + *[ + sensor_type[ATTR_FIELD] + for sensor_type in CC_V3_SENSOR_TYPES + ], ] ) data[FORECASTS][HOURLY] = await self._api.forecast_hourly( @@ -288,6 +295,7 @@ async def _async_update_data(self) -> dict[str, Any]: CC_ATTR_WIND_GUST, CC_ATTR_CLOUD_COVER, CC_ATTR_PRECIPITATION_TYPE, + *[sensor_type[ATTR_FIELD] for sensor_type in CC_SENSOR_TYPES], ], [ CC_ATTR_TEMPERATURE_LOW, @@ -317,20 +325,22 @@ def __init__( self, config_entry: ConfigEntry, coordinator: ClimaCellDataUpdateCoordinator, - forecast_type: str, api_version: int, ) -> None: """Initialize ClimaCell Entity.""" super().__init__(coordinator) self.api_version = api_version - self.forecast_type = forecast_type self._config_entry = config_entry @staticmethod def _get_cc_value( weather_dict: dict[str, Any], key: str ) -> int | float | str | None: - """Return property from weather_dict.""" + """ + Return property from weather_dict. + + Used for V3 API. + """ items = weather_dict.get(key, {}) # Handle cases where value returned is a list. # Optimistically find the best value to return. @@ -347,23 +357,13 @@ def _get_cc_value( return items.get("value") - @property - def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" - if self.forecast_type == DEFAULT_FORECAST_TYPE: - return True - - return False + def _get_current_property(self, property_name: str) -> int | str | float | None: + """ + Get property from current conditions. - @property - def name(self) -> str: - """Return the name of the entity.""" - return f"{self._config_entry.data[CONF_NAME]} - {self.forecast_type.title()}" - - @property - def unique_id(self) -> str: - """Return the unique id of the entity.""" - return f"{self._config_entry.unique_id}_{self.forecast_type}" + Used for V4 API. + """ + return self.coordinator.data.get(CURRENT, {}).get(property_name) @property def attribution(self): @@ -377,6 +377,6 @@ def device_info(self) -> dict[str, Any]: "identifiers": {(DOMAIN, self._config_entry.data[CONF_API_KEY])}, "name": "ClimaCell", "manufacturer": "ClimaCell", - "sw_version": "v3", + "sw_version": f"v{self.api_version}", "entry_type": "service", } diff --git a/homeassistant/components/climacell/const.py b/homeassistant/components/climacell/const.py index 6d451fa6f066a..2c1646afc70d7 100644 --- a/homeassistant/components/climacell/const.py +++ b/homeassistant/components/climacell/const.py @@ -1,5 +1,13 @@ """Constants for the ClimaCell integration.""" -from pyclimacell.const import DAILY, HOURLY, NOWCAST, WeatherCode +from pyclimacell.const import ( + DAILY, + HOURLY, + NOWCAST, + HealthConcernType, + PollenIndex, + PrimaryPollutantType, + WeatherCode, +) from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, @@ -15,6 +23,15 @@ ATTR_CONDITION_SUNNY, ATTR_CONDITION_WINDY, ) +from homeassistant.const import ( + ATTR_NAME, + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_PARTS_PER_BILLION, + CONCENTRATION_PARTS_PER_MILLION, + CONF_UNIT_OF_MEASUREMENT, + CONF_UNIT_SYSTEM_IMPERIAL, + CONF_UNIT_SYSTEM_METRIC, +) CONF_TIMESTEP = "timestep" FORECAST_TYPES = [DAILY, HOURLY, NOWCAST] @@ -35,6 +52,12 @@ NOWCAST: 30, } +# Sensor type keys +ATTR_FIELD = "field" +ATTR_METRIC_CONVERSION = "metric_conversion" +ATTR_VALUE_MAP = "value_map" +ATTR_IS_METRIC_CHECK = "is_metric_check" + # Additional attributes ATTR_WIND_GUST = "wind_gust" ATTR_CLOUD_COVER = "cloud_cover" @@ -68,6 +91,7 @@ WeatherCode.PARTLY_CLOUDY: ATTR_CONDITION_PARTLYCLOUDY, } +# Weather constants CC_ATTR_TIMESTAMP = "startTime" CC_ATTR_TEMPERATURE = "temperature" CC_ATTR_TEMPERATURE_HIGH = "temperatureMax" @@ -85,6 +109,95 @@ CC_ATTR_CLOUD_COVER = "cloudCover" CC_ATTR_PRECIPITATION_TYPE = "precipitationType" +# Sensor attributes +CC_ATTR_PARTICULATE_MATTER_25 = "particulateMatter25" +CC_ATTR_PARTICULATE_MATTER_10 = "particulateMatter10" +CC_ATTR_NITROGEN_DIOXIDE = "pollutantNO2" +CC_ATTR_CARBON_MONOXIDE = "pollutantCO" +CC_ATTR_SULFUR_DIOXIDE = "pollutantSO2" +CC_ATTR_EPA_AQI = "epaIndex" +CC_ATTR_EPA_PRIMARY_POLLUTANT = "epaPrimaryPollutant" +CC_ATTR_EPA_HEALTH_CONCERN = "epaHealthConcern" +CC_ATTR_CHINA_AQI = "mepIndex" +CC_ATTR_CHINA_PRIMARY_POLLUTANT = "mepPrimaryPollutant" +CC_ATTR_CHINA_HEALTH_CONCERN = "mepHealthConcern" +CC_ATTR_POLLEN_TREE = "treeIndex" +CC_ATTR_POLLEN_WEED = "weedIndex" +CC_ATTR_POLLEN_GRASS = "grassIndex" +CC_ATTR_FIRE_INDEX = "fireIndex" + +CC_SENSOR_TYPES = [ + { + ATTR_FIELD: CC_ATTR_PARTICULATE_MATTER_25, + ATTR_NAME: "Particulate Matter < 2.5 μm", + CONF_UNIT_SYSTEM_IMPERIAL: "μg/ft³", + CONF_UNIT_SYSTEM_METRIC: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ATTR_METRIC_CONVERSION: 3.2808399 ** 3, + ATTR_IS_METRIC_CHECK: True, + }, + { + ATTR_FIELD: CC_ATTR_PARTICULATE_MATTER_10, + ATTR_NAME: "Particulate Matter < 10 μm", + CONF_UNIT_SYSTEM_IMPERIAL: "μg/ft³", + CONF_UNIT_SYSTEM_METRIC: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ATTR_METRIC_CONVERSION: 3.2808399 ** 3, + ATTR_IS_METRIC_CHECK: True, + }, + { + ATTR_FIELD: CC_ATTR_NITROGEN_DIOXIDE, + ATTR_NAME: "Nitrogen Dioxide", + CONF_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION, + }, + { + ATTR_FIELD: CC_ATTR_CARBON_MONOXIDE, + ATTR_NAME: "Carbon Monoxide", + CONF_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION, + }, + { + ATTR_FIELD: CC_ATTR_SULFUR_DIOXIDE, + ATTR_NAME: "Sulfur Dioxide", + CONF_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION, + }, + {ATTR_FIELD: CC_ATTR_EPA_AQI, ATTR_NAME: "US EPA Air Quality Index"}, + { + ATTR_FIELD: CC_ATTR_EPA_PRIMARY_POLLUTANT, + ATTR_NAME: "US EPA Primary Pollutant", + ATTR_VALUE_MAP: PrimaryPollutantType, + }, + { + ATTR_FIELD: CC_ATTR_EPA_HEALTH_CONCERN, + ATTR_NAME: "US EPA Health Concern", + ATTR_VALUE_MAP: HealthConcernType, + }, + {ATTR_FIELD: CC_ATTR_CHINA_AQI, ATTR_NAME: "China MEP Air Quality Index"}, + { + ATTR_FIELD: CC_ATTR_CHINA_PRIMARY_POLLUTANT, + ATTR_NAME: "China MEP Primary Pollutant", + ATTR_VALUE_MAP: PrimaryPollutantType, + }, + { + ATTR_FIELD: CC_ATTR_CHINA_HEALTH_CONCERN, + ATTR_NAME: "China MEP Health Concern", + ATTR_VALUE_MAP: HealthConcernType, + }, + { + ATTR_FIELD: CC_ATTR_POLLEN_TREE, + ATTR_NAME: "Tree Pollen Index", + ATTR_VALUE_MAP: PollenIndex, + }, + { + ATTR_FIELD: CC_ATTR_POLLEN_WEED, + ATTR_NAME: "Weed Pollen Index", + ATTR_VALUE_MAP: PollenIndex, + }, + { + ATTR_FIELD: CC_ATTR_POLLEN_GRASS, + ATTR_NAME: "Grass Pollen Index", + ATTR_VALUE_MAP: PollenIndex, + }, + {ATTR_FIELD: CC_ATTR_FIRE_INDEX, ATTR_NAME: "Fire Index"}, +] + # V3 constants CONDITIONS_V3 = { "breezy": ATTR_CONDITION_WINDY, @@ -111,6 +224,7 @@ "partly_cloudy": ATTR_CONDITION_PARTLYCLOUDY, } +# Weather attributes CC_V3_ATTR_TIMESTAMP = "observation_time" CC_V3_ATTR_TEMPERATURE = "temp" CC_V3_ATTR_TEMPERATURE_HIGH = "max" @@ -128,3 +242,73 @@ CC_V3_ATTR_WIND_GUST = "wind_gust" CC_V3_ATTR_CLOUD_COVER = "cloud_cover" CC_V3_ATTR_PRECIPITATION_TYPE = "precipitation_type" + +# Sensor attributes +CC_V3_ATTR_PARTICULATE_MATTER_25 = "pm25" +CC_V3_ATTR_PARTICULATE_MATTER_10 = "pm10" +CC_V3_ATTR_NITROGEN_DIOXIDE = "no2" +CC_V3_ATTR_CARBON_MONOXIDE = "co" +CC_V3_ATTR_SULFUR_DIOXIDE = "so2" +CC_V3_ATTR_EPA_AQI = "epa_aqi" +CC_V3_ATTR_EPA_PRIMARY_POLLUTANT = "epa_primary_pollutant" +CC_V3_ATTR_EPA_HEALTH_CONCERN = "epa_health_concern" +CC_V3_ATTR_CHINA_AQI = "china_aqi" +CC_V3_ATTR_CHINA_PRIMARY_POLLUTANT = "china_primary_pollutant" +CC_V3_ATTR_CHINA_HEALTH_CONCERN = "china_health_concern" +CC_V3_ATTR_POLLEN_TREE = "pollen_tree" +CC_V3_ATTR_POLLEN_WEED = "pollen_weed" +CC_V3_ATTR_POLLEN_GRASS = "pollen_grass" +CC_V3_ATTR_FIRE_INDEX = "fire_index" + +CC_V3_SENSOR_TYPES = [ + { + ATTR_FIELD: CC_V3_ATTR_PARTICULATE_MATTER_25, + ATTR_NAME: "Particulate Matter < 2.5 μm", + CONF_UNIT_SYSTEM_IMPERIAL: "μg/ft³", + CONF_UNIT_SYSTEM_METRIC: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ATTR_METRIC_CONVERSION: 1 / (3.2808399 ** 3), + ATTR_IS_METRIC_CHECK: False, + }, + { + ATTR_FIELD: CC_V3_ATTR_PARTICULATE_MATTER_10, + ATTR_NAME: "Particulate Matter < 10 μm", + CONF_UNIT_SYSTEM_IMPERIAL: "μg/ft³", + CONF_UNIT_SYSTEM_METRIC: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ATTR_METRIC_CONVERSION: 1 / (3.2808399 ** 3), + ATTR_IS_METRIC_CHECK: False, + }, + { + ATTR_FIELD: CC_V3_ATTR_NITROGEN_DIOXIDE, + ATTR_NAME: "Nitrogen Dioxide", + CONF_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION, + }, + { + ATTR_FIELD: CC_V3_ATTR_CARBON_MONOXIDE, + ATTR_NAME: "Carbon Monoxide", + CONF_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_MILLION, + }, + { + ATTR_FIELD: CC_V3_ATTR_SULFUR_DIOXIDE, + ATTR_NAME: "Sulfur Dioxide", + CONF_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION, + }, + {ATTR_FIELD: CC_V3_ATTR_EPA_AQI, ATTR_NAME: "US EPA Air Quality Index"}, + { + ATTR_FIELD: CC_V3_ATTR_EPA_PRIMARY_POLLUTANT, + ATTR_NAME: "US EPA Primary Pollutant", + }, + {ATTR_FIELD: CC_V3_ATTR_EPA_HEALTH_CONCERN, ATTR_NAME: "US EPA Health Concern"}, + {ATTR_FIELD: CC_V3_ATTR_CHINA_AQI, ATTR_NAME: "China MEP Air Quality Index"}, + { + ATTR_FIELD: CC_V3_ATTR_CHINA_PRIMARY_POLLUTANT, + ATTR_NAME: "China MEP Primary Pollutant", + }, + { + ATTR_FIELD: CC_V3_ATTR_CHINA_HEALTH_CONCERN, + ATTR_NAME: "China MEP Health Concern", + }, + {ATTR_FIELD: CC_V3_ATTR_POLLEN_TREE, ATTR_NAME: "Tree Pollen Index"}, + {ATTR_FIELD: CC_V3_ATTR_POLLEN_WEED, ATTR_NAME: "Weed Pollen Index"}, + {ATTR_FIELD: CC_V3_ATTR_POLLEN_GRASS, ATTR_NAME: "Grass Pollen Index"}, + {ATTR_FIELD: CC_V3_ATTR_FIRE_INDEX, ATTR_NAME: "Fire Index"}, +] diff --git a/homeassistant/components/climacell/sensor.py b/homeassistant/components/climacell/sensor.py new file mode 100644 index 0000000000000..8a6fb39a3810a --- /dev/null +++ b/homeassistant/components/climacell/sensor.py @@ -0,0 +1,152 @@ +"""Sensor component that handles additional ClimaCell data for your location.""" +from __future__ import annotations + +from abc import abstractmethod +import logging +from typing import Any, Callable, Mapping + +from pyclimacell.const import CURRENT + +from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_ATTRIBUTION, + ATTR_NAME, + CONF_API_VERSION, + CONF_NAME, + CONF_UNIT_OF_MEASUREMENT, + CONF_UNIT_SYSTEM_IMPERIAL, + CONF_UNIT_SYSTEM_METRIC, +) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.util import slugify + +from . import ClimaCellDataUpdateCoordinator, ClimaCellEntity +from .const import ( + ATTR_FIELD, + ATTR_IS_METRIC_CHECK, + ATTR_METRIC_CONVERSION, + ATTR_VALUE_MAP, + CC_SENSOR_TYPES, + CC_V3_SENSOR_TYPES, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistantType, + config_entry: ConfigEntry, + async_add_entities: Callable[[list[Entity], bool], None], +) -> None: + """Set up a config entry.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id] + api_version = config_entry.data[CONF_API_VERSION] + + if api_version == 3: + api_class = ClimaCellV3SensorEntity + sensor_types = CC_V3_SENSOR_TYPES + else: + api_class = ClimaCellSensorEntity + sensor_types = CC_SENSOR_TYPES + entities = [ + api_class(config_entry, coordinator, api_version, sensor_type) + for sensor_type in sensor_types + ] + async_add_entities(entities) + + +class BaseClimaCellSensorEntity(ClimaCellEntity, SensorEntity): + """Base ClimaCell sensor entity.""" + + def __init__( + self, + config_entry: ConfigEntry, + coordinator: ClimaCellDataUpdateCoordinator, + api_version: int, + sensor_type: dict[str, str | float], + ) -> None: + """Initialize ClimaCell Sensor Entity.""" + super().__init__(config_entry, coordinator, api_version) + self.sensor_type = sensor_type + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + return False + + @property + def name(self) -> str: + """Return the name of the entity.""" + return f"{self._config_entry.data[CONF_NAME]} - {self.sensor_type[ATTR_NAME]}" + + @property + def unique_id(self) -> str: + """Return the unique id of the entity.""" + return f"{self._config_entry.unique_id}_{slugify(self.sensor_type[ATTR_NAME])}" + + @property + def extra_state_attributes(self) -> Mapping[str, Any] | None: + """Return entity specific state attributes.""" + return {ATTR_ATTRIBUTION: self.attribution} + + @property + def unit_of_measurement(self) -> str | None: + """Return the unit of measurement.""" + if CONF_UNIT_OF_MEASUREMENT in self.sensor_type: + return self.sensor_type[CONF_UNIT_OF_MEASUREMENT] + + if ( + CONF_UNIT_SYSTEM_IMPERIAL in self.sensor_type + and CONF_UNIT_SYSTEM_METRIC in self.sensor_type + ): + if self.hass.config.units.is_metric: + return self.sensor_type[CONF_UNIT_SYSTEM_METRIC] + return self.sensor_type[CONF_UNIT_SYSTEM_IMPERIAL] + + return None + + @property + @abstractmethod + def _state(self) -> str | int | float | None: + """Return the raw state.""" + + @property + def state(self) -> str | int | float | None: + """Return the state.""" + if ( + self._state is not None + and CONF_UNIT_SYSTEM_IMPERIAL in self.sensor_type + and CONF_UNIT_SYSTEM_METRIC in self.sensor_type + and ATTR_METRIC_CONVERSION in self.sensor_type + and ATTR_IS_METRIC_CHECK in self.sensor_type + and self.hass.config.units.is_metric + == self.sensor_type[ATTR_IS_METRIC_CHECK] + ): + return round(self._state * self.sensor_type[ATTR_METRIC_CONVERSION], 4) + + if ATTR_VALUE_MAP in self.sensor_type: + return self.sensor_type[ATTR_VALUE_MAP](self._state).name.lower() + return self._state + + +class ClimaCellSensorEntity(BaseClimaCellSensorEntity): + """Sensor entity that talks to ClimaCell v4 API to retrieve non-weather data.""" + + @property + def _state(self) -> str | int | float | None: + """Return the raw state.""" + return self._get_current_property(self.sensor_type[ATTR_FIELD]) + + +class ClimaCellV3SensorEntity(BaseClimaCellSensorEntity): + """Sensor entity that talks to ClimaCell v3 API to retrieve non-weather data.""" + + @property + def _state(self) -> str | int | float | None: + """Return the raw state.""" + return self._get_cc_value( + self.coordinator.data[CURRENT], self.sensor_type[ATTR_FIELD] + ) diff --git a/homeassistant/components/climacell/weather.py b/homeassistant/components/climacell/weather.py index 0808a4bd7344d..2c31d4df4fad6 100644 --- a/homeassistant/components/climacell/weather.py +++ b/homeassistant/components/climacell/weather.py @@ -1,6 +1,7 @@ """Weather component that handles meteorological data for your location.""" from __future__ import annotations +from abc import abstractmethod from datetime import datetime import logging from typing import Any, Callable, Mapping @@ -29,6 +30,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_API_VERSION, + CONF_NAME, LENGTH_FEET, LENGTH_KILOMETERS, LENGTH_METERS, @@ -44,7 +46,7 @@ from homeassistant.util.distance import convert as distance_convert from homeassistant.util.pressure import convert as pressure_convert -from . import ClimaCellEntity +from . import ClimaCellDataUpdateCoordinator, ClimaCellEntity from .const import ( ATTR_CLOUD_COVER, ATTR_PRECIPITATION_TYPE, @@ -86,12 +88,11 @@ CONDITIONS, CONDITIONS_V3, CONF_TIMESTEP, + DEFAULT_FORECAST_TYPE, DOMAIN, MAX_FORECASTS, ) -# mypy: allow-untyped-defs, no-check-untyped-defs - _LOGGER = logging.getLogger(__name__) @@ -106,7 +107,7 @@ async def async_setup_entry( api_class = ClimaCellV3WeatherEntity if api_version == 3 else ClimaCellWeatherEntity entities = [ - api_class(config_entry, coordinator, forecast_type, api_version) + api_class(config_entry, coordinator, api_version, forecast_type) for forecast_type in [DAILY, HOURLY, NOWCAST] ] async_add_entities(entities) @@ -115,12 +116,41 @@ async def async_setup_entry( class BaseClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity): """Base ClimaCell weather entity.""" + def __init__( + self, + config_entry: ConfigEntry, + coordinator: ClimaCellDataUpdateCoordinator, + api_version: int, + forecast_type: str, + ) -> None: + """Initialize ClimaCell Weather Entity.""" + super().__init__(config_entry, coordinator, api_version) + self.forecast_type = forecast_type + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + if self.forecast_type == DEFAULT_FORECAST_TYPE: + return True + + return False + + @property + def name(self) -> str: + """Return the name of the entity.""" + return f"{self._config_entry.data[CONF_NAME]} - {self.forecast_type.title()}" + + @property + def unique_id(self) -> str: + """Return the unique id of the entity.""" + return f"{self._config_entry.unique_id}_{self.forecast_type}" + @staticmethod + @abstractmethod def _translate_condition( condition: int | None, sun_is_up: bool = True ) -> str | None: """Translate ClimaCell condition into an HA condition.""" - raise NotImplementedError() def _forecast_dict( self, @@ -144,13 +174,14 @@ def _forecast_dict( if self.hass.config.units.is_metric: if precipitation: - precipitation = ( + precipitation = round( distance_convert(precipitation / 12, LENGTH_FEET, LENGTH_METERS) - * 1000 + * 1000, + 4, ) if wind_speed: - wind_speed = distance_convert( - wind_speed, LENGTH_MILES, LENGTH_KILOMETERS + wind_speed = round( + distance_convert(wind_speed, LENGTH_MILES, LENGTH_KILOMETERS), 4 ) data = { @@ -171,8 +202,8 @@ def extra_state_attributes(self) -> Mapping[str, Any] | None: """Return additional state attributes.""" wind_gust = self.wind_gust if wind_gust and self.hass.config.units.is_metric: - wind_gust = distance_convert( - self.wind_gust, LENGTH_MILES, LENGTH_KILOMETERS + wind_gust = round( + distance_convert(self.wind_gust, LENGTH_MILES, LENGTH_KILOMETERS), 4 ) cloud_cover = self.cloud_cover if cloud_cover is not None: @@ -184,19 +215,61 @@ def extra_state_attributes(self) -> Mapping[str, Any] | None: } @property + @abstractmethod def cloud_cover(self): """Return cloud cover.""" - raise NotImplementedError @property + @abstractmethod def wind_gust(self): """Return wind gust speed.""" - raise NotImplementedError @property + @abstractmethod def precipitation_type(self): """Return precipitation type.""" - raise NotImplementedError + + @property + @abstractmethod + def _pressure(self): + """Return the raw pressure.""" + + @property + def pressure(self): + """Return the pressure.""" + if self.hass.config.units.is_metric and self._pressure: + return round( + pressure_convert(self._pressure, PRESSURE_INHG, PRESSURE_HPA), 4 + ) + return self._pressure + + @property + @abstractmethod + def _wind_speed(self): + """Return the raw wind speed.""" + + @property + def wind_speed(self): + """Return the wind speed.""" + if self.hass.config.units.is_metric and self._wind_speed: + return round( + distance_convert(self._wind_speed, LENGTH_MILES, LENGTH_KILOMETERS), 4 + ) + return self._wind_speed + + @property + @abstractmethod + def _visibility(self): + """Return the raw visibility.""" + + @property + def visibility(self): + """Return the visibility.""" + if self.hass.config.units.is_metric and self._visibility: + return round( + distance_convert(self._visibility, LENGTH_MILES, LENGTH_KILOMETERS), 4 + ) + return self._visibility class ClimaCellWeatherEntity(BaseClimaCellWeatherEntity): @@ -217,10 +290,6 @@ def _translate_condition( return CLEAR_CONDITIONS["night"] return CONDITIONS[condition] - def _get_current_property(self, property_name: str) -> int | str | float | None: - """Get property from current conditions.""" - return self.coordinator.data.get(CURRENT, {}).get(property_name) - @property def temperature(self): """Return the platform temperature.""" @@ -232,12 +301,9 @@ def temperature_unit(self): return TEMP_FAHRENHEIT @property - def pressure(self): - """Return the pressure.""" - pressure = self._get_current_property(CC_ATTR_PRESSURE) - if self.hass.config.units.is_metric and pressure: - return pressure_convert(pressure, PRESSURE_INHG, PRESSURE_HPA) - return pressure + def _pressure(self): + """Return the raw pressure.""" + return self._get_current_property(CC_ATTR_PRESSURE) @property def humidity(self): @@ -263,12 +329,9 @@ def precipitation_type(self): return PrecipitationType(precipitation_type).name.lower() @property - def wind_speed(self): - """Return the wind speed.""" - wind_speed = self._get_current_property(CC_ATTR_WIND_SPEED) - if self.hass.config.units.is_metric and wind_speed: - return distance_convert(wind_speed, LENGTH_MILES, LENGTH_KILOMETERS) - return wind_speed + def _wind_speed(self): + """Return the raw wind speed.""" + return self._get_current_property(CC_ATTR_WIND_SPEED) @property def wind_bearing(self): @@ -289,12 +352,9 @@ def condition(self): ) @property - def visibility(self): - """Return the visibility.""" - visibility = self._get_current_property(CC_ATTR_VISIBILITY) - if self.hass.config.units.is_metric and visibility: - return distance_convert(visibility, LENGTH_MILES, LENGTH_KILOMETERS) - return visibility + def _visibility(self): + """Return the raw visibility.""" + return self._get_current_property(CC_ATTR_VISIBILITY) @property def forecast(self): @@ -391,14 +451,9 @@ def temperature_unit(self): return TEMP_FAHRENHEIT @property - def pressure(self): - """Return the pressure.""" - pressure = self._get_cc_value( - self.coordinator.data[CURRENT], CC_V3_ATTR_PRESSURE - ) - if self.hass.config.units.is_metric and pressure: - return pressure_convert(pressure, PRESSURE_INHG, PRESSURE_HPA) - return pressure + def _pressure(self): + """Return the raw pressure.""" + return self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_PRESSURE) @property def humidity(self): @@ -425,14 +480,9 @@ def precipitation_type(self): ) @property - def wind_speed(self): - """Return the wind speed.""" - wind_speed = self._get_cc_value( - self.coordinator.data[CURRENT], CC_V3_ATTR_WIND_SPEED - ) - if self.hass.config.units.is_metric and wind_speed: - return distance_convert(wind_speed, LENGTH_MILES, LENGTH_KILOMETERS) - return wind_speed + def _wind_speed(self): + """Return the raw wind speed.""" + return self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_WIND_SPEED) @property def wind_bearing(self): @@ -455,14 +505,9 @@ def condition(self): ) @property - def visibility(self): - """Return the visibility.""" - visibility = self._get_cc_value( - self.coordinator.data[CURRENT], CC_V3_ATTR_VISIBILITY - ) - if self.hass.config.units.is_metric and visibility: - return distance_convert(visibility, LENGTH_MILES, LENGTH_KILOMETERS) - return visibility + def _visibility(self): + """Return the raw visibility.""" + return self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_VISIBILITY) @property def forecast(self): diff --git a/tests/components/climacell/test_sensor.py b/tests/components/climacell/test_sensor.py new file mode 100644 index 0000000000000..d82a70964cf1e --- /dev/null +++ b/tests/components/climacell/test_sensor.py @@ -0,0 +1,148 @@ +"""Tests for Climacell sensor entities.""" +from __future__ import annotations + +from datetime import datetime +import logging +from typing import Any +from unittest.mock import patch + +import pytest +import pytz + +from homeassistant.components.climacell.config_flow import ( + _get_config_schema, + _get_unique_id, +) +from homeassistant.components.climacell.const import ATTRIBUTION, DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.const import ATTR_ATTRIBUTION +from homeassistant.core import State, callback +from homeassistant.helpers.entity_registry import async_get +from homeassistant.helpers.typing import HomeAssistantType + +from .const import API_V3_ENTRY_DATA, API_V4_ENTRY_DATA + +from tests.common import MockConfigEntry + +_LOGGER = logging.getLogger(__name__) +CC_SENSOR_ENTITY_ID = "sensor.climacell_{}" + +CO = "carbon_monoxide" +NO2 = "nitrogen_dioxide" +SO2 = "sulfur_dioxide" +PM25 = "particulate_matter_2_5_mm" +PM10 = "particulate_matter_10_mm" +MEP_AQI = "china_mep_air_quality_index" +MEP_HEALTH_CONCERN = "china_mep_health_concern" +MEP_PRIMARY_POLLUTANT = "china_mep_primary_pollutant" +EPA_AQI = "us_epa_air_quality_index" +EPA_HEALTH_CONCERN = "us_epa_health_concern" +EPA_PRIMARY_POLLUTANT = "us_epa_primary_pollutant" +FIRE_INDEX = "fire_index" +GRASS_POLLEN = "grass_pollen_index" +WEED_POLLEN = "weed_pollen_index" +TREE_POLLEN = "tree_pollen_index" + + +@callback +def _enable_entity(hass: HomeAssistantType, entity_name: str) -> None: + """Enable disabled entity.""" + ent_reg = async_get(hass) + entry = ent_reg.async_get(entity_name) + updated_entry = ent_reg.async_update_entity( + entry.entity_id, **{"disabled_by": None} + ) + assert updated_entry != entry + assert updated_entry.disabled is False + + +async def _setup(hass: HomeAssistantType, config: dict[str, Any]) -> State: + """Set up entry and return entity state.""" + with patch( + "homeassistant.util.dt.utcnow", + return_value=datetime(2021, 3, 6, 23, 59, 59, tzinfo=pytz.UTC), + ): + data = _get_config_schema(hass)(config) + config_entry = MockConfigEntry( + domain=DOMAIN, + data=data, + unique_id=_get_unique_id(hass, data), + version=1, + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + for entity_name in ( + CO, + NO2, + SO2, + PM25, + PM10, + MEP_AQI, + MEP_HEALTH_CONCERN, + MEP_PRIMARY_POLLUTANT, + EPA_AQI, + EPA_HEALTH_CONCERN, + EPA_PRIMARY_POLLUTANT, + FIRE_INDEX, + GRASS_POLLEN, + WEED_POLLEN, + TREE_POLLEN, + ): + _enable_entity(hass, CC_SENSOR_ENTITY_ID.format(entity_name)) + await hass.async_block_till_done() + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 15 + + +def check_sensor_state(hass: HomeAssistantType, entity_name: str, value: str): + """Check the state of a ClimaCell sensor.""" + state = hass.states.get(CC_SENSOR_ENTITY_ID.format(entity_name)) + assert state + assert state.state == value + assert state.attributes[ATTR_ATTRIBUTION] == ATTRIBUTION + + +async def test_v3_sensor( + hass: HomeAssistantType, + climacell_config_entry_update: pytest.fixture, +) -> None: + """Test v3 sensor data.""" + await _setup(hass, API_V3_ENTRY_DATA) + check_sensor_state(hass, CO, "0.875") + check_sensor_state(hass, NO2, "14.1875") + check_sensor_state(hass, SO2, "2") + check_sensor_state(hass, PM25, "5.3125") + check_sensor_state(hass, PM10, "27") + check_sensor_state(hass, MEP_AQI, "27") + check_sensor_state(hass, MEP_HEALTH_CONCERN, "Good") + check_sensor_state(hass, MEP_PRIMARY_POLLUTANT, "pm10") + check_sensor_state(hass, EPA_AQI, "22.3125") + check_sensor_state(hass, EPA_HEALTH_CONCERN, "Good") + check_sensor_state(hass, EPA_PRIMARY_POLLUTANT, "pm25") + check_sensor_state(hass, FIRE_INDEX, "9") + check_sensor_state(hass, GRASS_POLLEN, "0") + check_sensor_state(hass, WEED_POLLEN, "0") + check_sensor_state(hass, TREE_POLLEN, "0") + + +async def test_v4_sensor( + hass: HomeAssistantType, + climacell_config_entry_update: pytest.fixture, +) -> None: + """Test v4 sensor data.""" + await _setup(hass, API_V4_ENTRY_DATA) + check_sensor_state(hass, CO, "0.63") + check_sensor_state(hass, NO2, "10.67") + check_sensor_state(hass, SO2, "1.65") + check_sensor_state(hass, PM25, "5.2972") + check_sensor_state(hass, PM10, "20.1294") + check_sensor_state(hass, MEP_AQI, "23") + check_sensor_state(hass, MEP_HEALTH_CONCERN, "good") + check_sensor_state(hass, MEP_PRIMARY_POLLUTANT, "pm10") + check_sensor_state(hass, EPA_AQI, "24") + check_sensor_state(hass, EPA_HEALTH_CONCERN, "good") + check_sensor_state(hass, EPA_PRIMARY_POLLUTANT, "pm25") + check_sensor_state(hass, FIRE_INDEX, "10") + check_sensor_state(hass, GRASS_POLLEN, "none") + check_sensor_state(hass, WEED_POLLEN, "none") + check_sensor_state(hass, TREE_POLLEN, "none") diff --git a/tests/components/climacell/test_weather.py b/tests/components/climacell/test_weather.py index 779b0afa2c0f2..43515d6aa6620 100644 --- a/tests/components/climacell/test_weather.py +++ b/tests/components/climacell/test_weather.py @@ -44,7 +44,7 @@ DOMAIN as WEATHER_DOMAIN, ) from homeassistant.const import ATTR_ATTRIBUTION, ATTR_FRIENDLY_NAME -from homeassistant.core import State +from homeassistant.core import State, callback from homeassistant.helpers.entity_registry import async_get from homeassistant.helpers.typing import HomeAssistantType @@ -55,7 +55,8 @@ _LOGGER = logging.getLogger(__name__) -async def _enable_entity(hass: HomeAssistantType, entity_name: str) -> None: +@callback +def _enable_entity(hass: HomeAssistantType, entity_name: str) -> None: """Enable disabled entity.""" ent_reg = async_get(hass) entry = ent_reg.async_get(entity_name) @@ -82,8 +83,8 @@ async def _setup(hass: HomeAssistantType, config: dict[str, Any]) -> State: config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - await _enable_entity(hass, "weather.climacell_hourly") - await _enable_entity(hass, "weather.climacell_nowcast") + for entity_name in ("hourly", "nowcast"): + _enable_entity(hass, f"weather.climacell_{entity_name}") await hass.async_block_till_done() assert len(hass.states.async_entity_ids(WEATHER_DOMAIN)) == 3 @@ -142,7 +143,7 @@ async def test_v3_weather( { ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, ATTR_FORECAST_TIME: "2021-03-12T00:00:00+00:00", - ATTR_FORECAST_PRECIPITATION: 0.04572, + ATTR_FORECAST_PRECIPITATION: 0.0457, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 25, ATTR_FORECAST_TEMP: 20, ATTR_FORECAST_TEMP_LOW: 12, @@ -158,7 +159,7 @@ async def test_v3_weather( { ATTR_FORECAST_CONDITION: ATTR_CONDITION_RAINY, ATTR_FORECAST_TIME: "2021-03-14T00:00:00+00:00", - ATTR_FORECAST_PRECIPITATION: 1.07442, + ATTR_FORECAST_PRECIPITATION: 1.0744, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 75, ATTR_FORECAST_TEMP: 6, ATTR_FORECAST_TEMP_LOW: 3, @@ -166,7 +167,7 @@ async def test_v3_weather( { ATTR_FORECAST_CONDITION: ATTR_CONDITION_SNOWY, ATTR_FORECAST_TIME: "2021-03-15T00:00:00+00:00", - ATTR_FORECAST_PRECIPITATION: 7.305040000000001, + ATTR_FORECAST_PRECIPITATION: 7.3050, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 95, ATTR_FORECAST_TEMP: 1, ATTR_FORECAST_TEMP_LOW: 0, @@ -174,7 +175,7 @@ async def test_v3_weather( { ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, ATTR_FORECAST_TIME: "2021-03-16T00:00:00+00:00", - ATTR_FORECAST_PRECIPITATION: 0.00508, + ATTR_FORECAST_PRECIPITATION: 0.0051, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 5, ATTR_FORECAST_TEMP: 6, ATTR_FORECAST_TEMP_LOW: -2, @@ -214,7 +215,7 @@ async def test_v3_weather( { ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, ATTR_FORECAST_TIME: "2021-03-21T00:00:00+00:00", - ATTR_FORECAST_PRECIPITATION: 0.043179999999999996, + ATTR_FORECAST_PRECIPITATION: 0.0432, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 20, ATTR_FORECAST_TEMP: 7, ATTR_FORECAST_TEMP_LOW: 1, @@ -223,13 +224,13 @@ async def test_v3_weather( assert weather_state.attributes[ATTR_FRIENDLY_NAME] == "ClimaCell - Daily" assert weather_state.attributes[ATTR_WEATHER_HUMIDITY] == 24 assert weather_state.attributes[ATTR_WEATHER_OZONE] == 52.625 - assert weather_state.attributes[ATTR_WEATHER_PRESSURE] == 1028.124632345 + assert weather_state.attributes[ATTR_WEATHER_PRESSURE] == 1028.1246 assert weather_state.attributes[ATTR_WEATHER_TEMPERATURE] == 7 - assert weather_state.attributes[ATTR_WEATHER_VISIBILITY] == 9.994026240000002 + assert weather_state.attributes[ATTR_WEATHER_VISIBILITY] == 9.9940 assert weather_state.attributes[ATTR_WEATHER_WIND_BEARING] == 320.31 - assert weather_state.attributes[ATTR_WEATHER_WIND_SPEED] == 14.62893696 + assert weather_state.attributes[ATTR_WEATHER_WIND_SPEED] == 14.6289 assert weather_state.attributes[ATTR_CLOUD_COVER] == 1 - assert weather_state.attributes[ATTR_WIND_GUST] == 24.075786240000003 + assert weather_state.attributes[ATTR_WIND_GUST] == 24.0758 assert weather_state.attributes[ATTR_PRECIPITATION_TYPE] == "rain" @@ -250,7 +251,7 @@ async def test_v4_weather( ATTR_FORECAST_TEMP: 8, ATTR_FORECAST_TEMP_LOW: -3, ATTR_FORECAST_WIND_BEARING: 239.6, - ATTR_FORECAST_WIND_SPEED: 15.272674560000002, + ATTR_FORECAST_WIND_SPEED: 15.2727, }, { ATTR_FORECAST_CONDITION: "cloudy", @@ -260,7 +261,7 @@ async def test_v4_weather( ATTR_FORECAST_TEMP: 10, ATTR_FORECAST_TEMP_LOW: -3, ATTR_FORECAST_WIND_BEARING: 262.82, - ATTR_FORECAST_WIND_SPEED: 11.65165056, + ATTR_FORECAST_WIND_SPEED: 11.6517, }, { ATTR_FORECAST_CONDITION: "cloudy", @@ -270,7 +271,7 @@ async def test_v4_weather( ATTR_FORECAST_TEMP: 19, ATTR_FORECAST_TEMP_LOW: 0, ATTR_FORECAST_WIND_BEARING: 229.3, - ATTR_FORECAST_WIND_SPEED: 11.3458752, + ATTR_FORECAST_WIND_SPEED: 11.3459, }, { ATTR_FORECAST_CONDITION: "cloudy", @@ -280,7 +281,7 @@ async def test_v4_weather( ATTR_FORECAST_TEMP: 18, ATTR_FORECAST_TEMP_LOW: 3, ATTR_FORECAST_WIND_BEARING: 149.91, - ATTR_FORECAST_WIND_SPEED: 17.123420160000002, + ATTR_FORECAST_WIND_SPEED: 17.1234, }, { ATTR_FORECAST_CONDITION: "cloudy", @@ -290,17 +291,17 @@ async def test_v4_weather( ATTR_FORECAST_TEMP: 19, ATTR_FORECAST_TEMP_LOW: 9, ATTR_FORECAST_WIND_BEARING: 210.45, - ATTR_FORECAST_WIND_SPEED: 25.250607360000004, + ATTR_FORECAST_WIND_SPEED: 25.2506, }, { ATTR_FORECAST_CONDITION: "rainy", ATTR_FORECAST_TIME: "2021-03-12T11:00:00+00:00", - ATTR_FORECAST_PRECIPITATION: 0.12192000000000001, + ATTR_FORECAST_PRECIPITATION: 0.1219, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 25, ATTR_FORECAST_TEMP: 20, ATTR_FORECAST_TEMP_LOW: 12, ATTR_FORECAST_WIND_BEARING: 217.98, - ATTR_FORECAST_WIND_SPEED: 19.794931200000004, + ATTR_FORECAST_WIND_SPEED: 19.7949, }, { ATTR_FORECAST_CONDITION: "cloudy", @@ -310,27 +311,27 @@ async def test_v4_weather( ATTR_FORECAST_TEMP: 12, ATTR_FORECAST_TEMP_LOW: 6, ATTR_FORECAST_WIND_BEARING: 58.79, - ATTR_FORECAST_WIND_SPEED: 15.642823680000001, + ATTR_FORECAST_WIND_SPEED: 15.6428, }, { ATTR_FORECAST_CONDITION: "snowy", ATTR_FORECAST_TIME: "2021-03-14T10:00:00+00:00", - ATTR_FORECAST_PRECIPITATION: 23.95728, + ATTR_FORECAST_PRECIPITATION: 23.9573, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 95, ATTR_FORECAST_TEMP: 6, ATTR_FORECAST_TEMP_LOW: 1, ATTR_FORECAST_WIND_BEARING: 70.25, - ATTR_FORECAST_WIND_SPEED: 26.15184, + ATTR_FORECAST_WIND_SPEED: 26.1518, }, { ATTR_FORECAST_CONDITION: "snowy", ATTR_FORECAST_TIME: "2021-03-15T10:00:00+00:00", - ATTR_FORECAST_PRECIPITATION: 1.46304, + ATTR_FORECAST_PRECIPITATION: 1.4630, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 55, ATTR_FORECAST_TEMP: 6, ATTR_FORECAST_TEMP_LOW: -1, ATTR_FORECAST_WIND_BEARING: 84.47, - ATTR_FORECAST_WIND_SPEED: 25.57247616, + ATTR_FORECAST_WIND_SPEED: 25.5725, }, { ATTR_FORECAST_CONDITION: "cloudy", @@ -340,7 +341,7 @@ async def test_v4_weather( ATTR_FORECAST_TEMP: 6, ATTR_FORECAST_TEMP_LOW: -2, ATTR_FORECAST_WIND_BEARING: 103.85, - ATTR_FORECAST_WIND_SPEED: 10.79869824, + ATTR_FORECAST_WIND_SPEED: 10.7987, }, { ATTR_FORECAST_CONDITION: "cloudy", @@ -350,7 +351,7 @@ async def test_v4_weather( ATTR_FORECAST_TEMP: 11, ATTR_FORECAST_TEMP_LOW: 1, ATTR_FORECAST_WIND_BEARING: 145.41, - ATTR_FORECAST_WIND_SPEED: 11.69993088, + ATTR_FORECAST_WIND_SPEED: 11.6999, }, { ATTR_FORECAST_CONDITION: "cloudy", @@ -360,17 +361,17 @@ async def test_v4_weather( ATTR_FORECAST_TEMP: 12, ATTR_FORECAST_TEMP_LOW: 5, ATTR_FORECAST_WIND_BEARING: 62.99, - ATTR_FORECAST_WIND_SPEED: 10.58948352, + ATTR_FORECAST_WIND_SPEED: 10.5895, }, { ATTR_FORECAST_CONDITION: "rainy", ATTR_FORECAST_TIME: "2021-03-19T10:00:00+00:00", - ATTR_FORECAST_PRECIPITATION: 2.92608, + ATTR_FORECAST_PRECIPITATION: 2.9261, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 55, ATTR_FORECAST_TEMP: 9, ATTR_FORECAST_TEMP_LOW: 4, ATTR_FORECAST_WIND_BEARING: 68.54, - ATTR_FORECAST_WIND_SPEED: 22.38597504, + ATTR_FORECAST_WIND_SPEED: 22.3860, }, { ATTR_FORECAST_CONDITION: "snowy", @@ -380,17 +381,17 @@ async def test_v4_weather( ATTR_FORECAST_TEMP: 5, ATTR_FORECAST_TEMP_LOW: 2, ATTR_FORECAST_WIND_BEARING: 56.98, - ATTR_FORECAST_WIND_SPEED: 27.922118400000002, + ATTR_FORECAST_WIND_SPEED: 27.9221, }, ] assert weather_state.attributes[ATTR_FRIENDLY_NAME] == "ClimaCell - Daily" assert weather_state.attributes[ATTR_WEATHER_HUMIDITY] == 23 assert weather_state.attributes[ATTR_WEATHER_OZONE] == 46.53 - assert weather_state.attributes[ATTR_WEATHER_PRESSURE] == 1027.7690615000001 + assert weather_state.attributes[ATTR_WEATHER_PRESSURE] == 1027.7691 assert weather_state.attributes[ATTR_WEATHER_TEMPERATURE] == 7 - assert weather_state.attributes[ATTR_WEATHER_VISIBILITY] == 13.116153600000002 + assert weather_state.attributes[ATTR_WEATHER_VISIBILITY] == 13.1162 assert weather_state.attributes[ATTR_WEATHER_WIND_BEARING] == 315.14 - assert weather_state.attributes[ATTR_WEATHER_WIND_SPEED] == 15.01517952 + assert weather_state.attributes[ATTR_WEATHER_WIND_SPEED] == 15.0152 assert weather_state.attributes[ATTR_CLOUD_COVER] == 1 - assert weather_state.attributes[ATTR_WIND_GUST] == 20.34210816 + assert weather_state.attributes[ATTR_WIND_GUST] == 20.3421 assert weather_state.attributes[ATTR_PRECIPITATION_TYPE] == "rain" diff --git a/tests/fixtures/climacell/v3_realtime.json b/tests/fixtures/climacell/v3_realtime.json index c4226ab5ad9b1..b7801d781607e 100644 --- a/tests/fixtures/climacell/v3_realtime.json +++ b/tests/fixtures/climacell/v3_realtime.json @@ -43,6 +43,59 @@ "value": 100, "units": "%" }, + "fire_index": { + "value": 9 + }, + "epa_aqi": { + "value": 22.3125 + }, + "epa_primary_pollutant": { + "value": "pm25" + }, + "china_aqi": { + "value": 27 + }, + "china_primary_pollutant": { + "value": "pm10" + }, + "pm25": { + "value": 5.3125, + "units": "\u00b5g/m3" + }, + "pm10": { + "value": 27, + "units": "\u00b5g/m3" + }, + "no2": { + "value": 14.1875, + "units": "ppb" + }, + "co": { + "value": 0.875, + "units": "ppm" + }, + "so2": { + "value": 2, + "units": "ppb" + }, + "epa_health_concern": { + "value": "Good" + }, + "china_health_concern": { + "value": "Good" + }, + "pollen_tree": { + "value": 0, + "units": "Climacell Pollen Index" + }, + "pollen_weed": { + "value": 0, + "units": "Climacell Pollen Index" + }, + "pollen_grass": { + "value": 0, + "units": "Climacell Pollen Index" + }, "observation_time": { "value": "2021-03-07T18:54:06.055Z" } diff --git a/tests/fixtures/climacell/v4.json b/tests/fixtures/climacell/v4.json index 7d778ba9f5107..f2f10b0360e45 100644 --- a/tests/fixtures/climacell/v4.json +++ b/tests/fixtures/climacell/v4.json @@ -10,7 +10,22 @@ "pollutantO3": 46.53, "windGust": 12.64, "cloudCover": 100, - "precipitationType": 1 + "precipitationType": 1, + "particulateMatter25": 0.15, + "particulateMatter10": 0.57, + "pollutantNO2": 10.67, + "pollutantCO": 0.63, + "pollutantSO2": 1.65, + "epaIndex": 24, + "epaPrimaryPollutant": 0, + "epaHealthConcern": 0, + "mepIndex": 23, + "mepPrimaryPollutant": 1, + "mepHealthConcern": 0, + "treeIndex": 0, + "weedIndex": 0, + "grassIndex": 0, + "fireIndex": 10 }, "forecasts": { "nowcast": [ From b213b55ca980d3849f301f7c68208e39a1e60ddb Mon Sep 17 00:00:00 2001 From: Lau1406 Date: Thu, 15 Apr 2021 22:48:39 +0200 Subject: [PATCH 0296/1317] Add missing target field to media_seek (#49031) --- homeassistant/components/media_player/services.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index e2a260dc80f55..bec89ed44fb57 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -89,6 +89,7 @@ media_seek: name: Seek description: Send the media player the command to seek in current playing media. + target: fields: seek_position: name: Position From a981b86b15aef742c897396f30c4c7d38b9670c6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 15 Apr 2021 22:49:13 +0200 Subject: [PATCH 0297/1317] Update issue form to use latest changes (#49272) --- .github/ISSUE_TEMPLATE/bug_report.yml | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index aa81d6e4df71c..50a3dd55e866d 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,7 +1,6 @@ name: Report an issue with Home Assistant Core description: Report an issue with Home Assistant Core. title: "" -issue_body: true body: - type: markdown attributes: @@ -85,13 +84,10 @@ body: label: Anything in the logs that might be useful for us? description: For example, error message, or stack traces. render: txt - - type: markdown - attributes: - value: | - ## Additional information - - type: markdown + - type: textarea attributes: - value: > + label: Additional information + description: > If you have any additional information for us, use the field below. Please note, you can attach screenshots or screen recordings here, by dragging and dropping files in the field below. From eb008e533e957c7627e9a8c650c10b71d5e057f3 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 15 Apr 2021 23:34:49 +0200 Subject: [PATCH 0298/1317] Process AdGuard review comments (#49274) --- homeassistant/components/adguard/__init__.py | 4 ++-- homeassistant/components/adguard/config_flow.py | 17 +++++++++-------- homeassistant/components/adguard/sensor.py | 16 ++++++++-------- homeassistant/components/adguard/strings.json | 2 +- .../components/adguard/translations/en.json | 4 ++-- tests/components/adguard/test_config_flow.py | 6 +++--- 6 files changed, 25 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/adguard/__init__.py b/homeassistant/components/adguard/__init__.py index be465b1e1a752..2cda6d92556df 100644 --- a/homeassistant/components/adguard/__init__.py +++ b/homeassistant/components/adguard/__init__.py @@ -68,9 +68,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except AdGuardHomeConnectionError as exception: raise ConfigEntryNotReady from exception - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) async def add_url(call) -> None: diff --git a/homeassistant/components/adguard/config_flow.py b/homeassistant/components/adguard/config_flow.py index d8e657dfc763b..c024d82b6ae07 100644 --- a/homeassistant/components/adguard/config_flow.py +++ b/homeassistant/components/adguard/config_flow.py @@ -16,6 +16,7 @@ CONF_USERNAME, CONF_VERIFY_SSL, ) +from homeassistant.data_entry_flow import FlowResultDict from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN @@ -31,7 +32,7 @@ class AdGuardHomeFlowHandler(ConfigFlow, domain=DOMAIN): async def _show_setup_form( self, errors: dict[str, str] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Show the setup form to the user.""" return self.async_show_form( step_id="user", @@ -50,7 +51,7 @@ async def _show_setup_form( async def _show_hassio_form( self, errors: dict[str, str] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Show the Hass.io confirmation form to the user.""" return self.async_show_form( step_id="hassio_confirm", @@ -61,7 +62,7 @@ async def _show_hassio_form( async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle a flow initiated by the user.""" if user_input is None: return await self._show_setup_form(user_input) @@ -72,7 +73,7 @@ async def async_step_user( entry.data[CONF_HOST] == user_input[CONF_HOST] and entry.data[CONF_PORT] == user_input[CONF_PORT] ): - return self.async_abort(reason="single_instance_allowed") + return self.async_abort(reason="already_configured") errors = {} @@ -106,7 +107,7 @@ async def async_step_user( }, ) - async def async_step_hassio(self, discovery_info: dict[str, Any]) -> dict[str, Any]: + async def async_step_hassio(self, discovery_info: dict[str, Any]) -> FlowResultDict: """Prepare configuration for a Hass.io AdGuard Home add-on. This flow is triggered by the discovery component. @@ -124,7 +125,7 @@ async def async_step_hassio(self, discovery_info: dict[str, Any]) -> dict[str, A cur_entry.data[CONF_HOST] == discovery_info[CONF_HOST] and cur_entry.data[CONF_PORT] == discovery_info[CONF_PORT] ): - return self.async_abort(reason="single_instance_allowed") + return self.async_abort(reason="already_configured") is_loaded = cur_entry.state == config_entries.ENTRY_STATE_LOADED @@ -147,8 +148,8 @@ async def async_step_hassio(self, discovery_info: dict[str, Any]) -> dict[str, A async def async_step_hassio_confirm( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: - """Confirm Hass.io discovery.""" + ) -> FlowResultDict: + """Confirm Supervisor discovery.""" if user_input is None: return await self._show_hassio_form() diff --git a/homeassistant/components/adguard/sensor.py b/homeassistant/components/adguard/sensor.py index 012df197684ff..4dd69d3370548 100644 --- a/homeassistant/components/adguard/sensor.py +++ b/homeassistant/components/adguard/sensor.py @@ -96,7 +96,7 @@ def unit_of_measurement(self) -> str | None: class AdGuardHomeDNSQueriesSensor(AdGuardHomeSensor): """Defines a AdGuard Home DNS Queries sensor.""" - def __init__(self, adguard: AdGuardHome, entry: ConfigEntry): + def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: """Initialize AdGuard Home sensor.""" super().__init__( adguard, @@ -115,7 +115,7 @@ async def _adguard_update(self) -> None: class AdGuardHomeBlockedFilteringSensor(AdGuardHomeSensor): """Defines a AdGuard Home blocked by filtering sensor.""" - def __init__(self, adguard: AdGuardHome, entry: ConfigEntry): + def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: """Initialize AdGuard Home sensor.""" super().__init__( adguard, @@ -135,7 +135,7 @@ async def _adguard_update(self) -> None: class AdGuardHomePercentageBlockedSensor(AdGuardHomeSensor): """Defines a AdGuard Home blocked percentage sensor.""" - def __init__(self, adguard: AdGuardHome, entry: ConfigEntry): + def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: """Initialize AdGuard Home sensor.""" super().__init__( adguard, @@ -155,7 +155,7 @@ async def _adguard_update(self) -> None: class AdGuardHomeReplacedParentalSensor(AdGuardHomeSensor): """Defines a AdGuard Home replaced by parental control sensor.""" - def __init__(self, adguard: AdGuardHome, entry: ConfigEntry): + def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: """Initialize AdGuard Home sensor.""" super().__init__( adguard, @@ -174,7 +174,7 @@ async def _adguard_update(self) -> None: class AdGuardHomeReplacedSafeBrowsingSensor(AdGuardHomeSensor): """Defines a AdGuard Home replaced by safe browsing sensor.""" - def __init__(self, adguard: AdGuardHome, entry: ConfigEntry): + def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: """Initialize AdGuard Home sensor.""" super().__init__( adguard, @@ -193,7 +193,7 @@ async def _adguard_update(self) -> None: class AdGuardHomeReplacedSafeSearchSensor(AdGuardHomeSensor): """Defines a AdGuard Home replaced by safe search sensor.""" - def __init__(self, adguard: AdGuardHome, entry: ConfigEntry): + def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: """Initialize AdGuard Home sensor.""" super().__init__( adguard, @@ -212,7 +212,7 @@ async def _adguard_update(self) -> None: class AdGuardHomeAverageProcessingTimeSensor(AdGuardHomeSensor): """Defines a AdGuard Home average processing time sensor.""" - def __init__(self, adguard: AdGuardHome, entry: ConfigEntry): + def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: """Initialize AdGuard Home sensor.""" super().__init__( adguard, @@ -232,7 +232,7 @@ async def _adguard_update(self) -> None: class AdGuardHomeRulesCountSensor(AdGuardHomeSensor): """Defines a AdGuard Home rules count sensor.""" - def __init__(self, adguard: AdGuardHome, entry: ConfigEntry): + def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: """Initialize AdGuard Home sensor.""" super().__init__( adguard, diff --git a/homeassistant/components/adguard/strings.json b/homeassistant/components/adguard/strings.json index 4e6a63cfd3a25..e593d4199a40d 100644 --- a/homeassistant/components/adguard/strings.json +++ b/homeassistant/components/adguard/strings.json @@ -22,7 +22,7 @@ }, "abort": { "existing_instance_updated": "Updated existing configuration.", - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" } } } diff --git a/homeassistant/components/adguard/translations/en.json b/homeassistant/components/adguard/translations/en.json index 5e09b42b9f2cc..f354aaab10a2a 100644 --- a/homeassistant/components/adguard/translations/en.json +++ b/homeassistant/components/adguard/translations/en.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "existing_instance_updated": "Updated existing configuration.", - "single_instance_allowed": "Already configured. Only a single configuration possible." + "already_configured": "Service is already configured", + "existing_instance_updated": "Updated existing configuration." }, "error": { "cannot_connect": "Failed to connect" diff --git a/tests/components/adguard/test_config_flow.py b/tests/components/adguard/test_config_flow.py index 0923883274bf0..7e46c8a4b4689 100644 --- a/tests/components/adguard/test_config_flow.py +++ b/tests/components/adguard/test_config_flow.py @@ -102,10 +102,10 @@ async def test_integration_already_exists(hass: HomeAssistant) -> None: context={"source": "user"}, ) assert result["type"] == "abort" - assert result["reason"] == "single_instance_allowed" + assert result["reason"] == "already_configured" -async def test_hassio_single_instance(hass: HomeAssistant) -> None: +async def test_hassio_already_configured(hass: HomeAssistant) -> None: """Test we only allow a single config flow.""" MockConfigEntry( domain=DOMAIN, data={"host": "mock-adguard", "port": "3000"} @@ -117,7 +117,7 @@ async def test_hassio_single_instance(hass: HomeAssistant) -> None: context={"source": "hassio"}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "single_instance_allowed" + assert result["reason"] == "already_configured" async def test_hassio_update_instance_not_running(hass: HomeAssistant) -> None: From 283342bafbd80482f90d4b0187d0d8eed1a12a03 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Fri, 16 Apr 2021 00:03:57 +0000 Subject: [PATCH 0299/1317] [ci skip] Translation update --- .../components/adguard/translations/en.json | 3 ++- .../enphase_envoy/translations/ca.json | 3 ++- .../enphase_envoy/translations/et.json | 3 ++- .../enphase_envoy/translations/ko.json | 3 ++- .../enphase_envoy/translations/ru.json | 3 ++- .../components/sma/translations/ko.json | 23 +++++++++++++++++++ 6 files changed, 33 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/sma/translations/ko.json diff --git a/homeassistant/components/adguard/translations/en.json b/homeassistant/components/adguard/translations/en.json index f354aaab10a2a..31eb1ff06a3a8 100644 --- a/homeassistant/components/adguard/translations/en.json +++ b/homeassistant/components/adguard/translations/en.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "Service is already configured", - "existing_instance_updated": "Updated existing configuration." + "existing_instance_updated": "Updated existing configuration.", + "single_instance_allowed": "Already configured. Only a single configuration possible." }, "error": { "cannot_connect": "Failed to connect" diff --git a/homeassistant/components/enphase_envoy/translations/ca.json b/homeassistant/components/enphase_envoy/translations/ca.json index f388abca5b808..fad9e8f4a18fb 100644 --- a/homeassistant/components/enphase_envoy/translations/ca.json +++ b/homeassistant/components/enphase_envoy/translations/ca.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "El dispositiu ja est\u00e0 configurat" + "already_configured": "El dispositiu ja est\u00e0 configurat", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", diff --git a/homeassistant/components/enphase_envoy/translations/et.json b/homeassistant/components/enphase_envoy/translations/et.json index 34f052809df9a..d4a0fb6dfb34e 100644 --- a/homeassistant/components/enphase_envoy/translations/et.json +++ b/homeassistant/components/enphase_envoy/translations/et.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { "cannot_connect": "\u00dchendamine nurjus", diff --git a/homeassistant/components/enphase_envoy/translations/ko.json b/homeassistant/components/enphase_envoy/translations/ko.json index 74ec68256be47..986bfe5d5a47f 100644 --- a/homeassistant/components/enphase_envoy/translations/ko.json +++ b/homeassistant/components/enphase_envoy/translations/ko.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4" }, "error": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", diff --git a/homeassistant/components/enphase_envoy/translations/ru.json b/homeassistant/components/enphase_envoy/translations/ru.json index f10538617398f..b04d0ac509311 100644 --- a/homeassistant/components/enphase_envoy/translations/ru.json +++ b/homeassistant/components/enphase_envoy/translations/ru.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", diff --git a/homeassistant/components/sma/translations/ko.json b/homeassistant/components/sma/translations/ko.json new file mode 100644 index 0000000000000..5e5aa899d9631 --- /dev/null +++ b/homeassistant/components/sma/translations/ko.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "password": "\ube44\ubc00\ubc88\ud638", + "ssl": "SSL \uc778\uc99d\uc11c \uc0ac\uc6a9", + "verify_ssl": "SSL \uc778\uc99d\uc11c \ud655\uc778" + } + } + } + } +} \ No newline at end of file From 6604614c399610cc40341e99da278627499b35a1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 15 Apr 2021 18:18:25 -0700 Subject: [PATCH 0300/1317] Move top-level av import behind type checking flag (#49281) * Move top-level av import behind type checking flag * Lint --- homeassistant/components/stream/core.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py index cac4aa1eccb33..0e513d3ae8187 100644 --- a/homeassistant/components/stream/core.py +++ b/homeassistant/components/stream/core.py @@ -4,12 +4,10 @@ import asyncio from collections import deque import io -from typing import Any, Callable +from typing import TYPE_CHECKING, Any, Callable from aiohttp import web import attr -import av.container -import av.video from homeassistant.components.http import HomeAssistantView from homeassistant.core import HomeAssistant, callback @@ -18,6 +16,10 @@ from .const import ATTR_STREAMS, DOMAIN +if TYPE_CHECKING: + import av.container + import av.video + PROVIDERS = Registry() From 564e7fa53c8c864f11b8ad181efdddc6d78fbfbb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 15 Apr 2021 20:16:17 -1000 Subject: [PATCH 0301/1317] Avoid sending empty integration list multiple times during subscribe_bootstrap_integrations (#49181) --- homeassistant/bootstrap.py | 9 ++++-- tests/test_bootstrap.py | 59 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 3 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index fc12ec065a9bd..45c0465146128 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -389,6 +389,7 @@ async def _async_watch_pending_setups(hass: core.HomeAssistant) -> None: """Periodic log of setups that are pending for longer than LOG_SLOW_STARTUP_INTERVAL.""" loop_count = 0 setup_started: dict[str, datetime] = hass.data[DATA_SETUP_STARTED] + previous_was_empty = True while True: now = dt_util.utcnow() remaining_with_setup_started = { @@ -396,9 +397,11 @@ async def _async_watch_pending_setups(hass: core.HomeAssistant) -> None: for domain in setup_started } _LOGGER.debug("Integration remaining: %s", remaining_with_setup_started) - async_dispatcher_send( - hass, SIGNAL_BOOTSTRAP_INTEGRATONS, remaining_with_setup_started - ) + if remaining_with_setup_started or not previous_was_empty: + async_dispatcher_send( + hass, SIGNAL_BOOTSTRAP_INTEGRATONS, remaining_with_setup_started + ) + previous_was_empty = not remaining_with_setup_started await asyncio.sleep(SLOW_STARTUP_CHECK_INTERVAL) loop_count += SLOW_STARTUP_CHECK_INTERVAL diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index 2464638627897..1fecf7be96bcd 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -7,8 +7,10 @@ import pytest from homeassistant import bootstrap, core, runner +from homeassistant.bootstrap import SIGNAL_BOOTSTRAP_INTEGRATONS import homeassistant.config as config_util from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.dt as dt_util from tests.common import ( @@ -610,3 +612,60 @@ async def test_setup_safe_mode_if_no_frontend( assert hass.config.skip_pip assert hass.config.internal_url == "http://192.168.1.100:8123" assert hass.config.external_url == "https://abcdef.ui.nabu.casa" + + +@pytest.mark.parametrize("load_registries", [False]) +async def test_empty_integrations_list_is_only_sent_at_the_end_of_bootstrap(hass): + """Test empty integrations list is only sent at the end of bootstrap.""" + order = [] + + def gen_domain_setup(domain): + async def async_setup(hass, config): + order.append(domain) + await asyncio.sleep(0.1) + + async def _background_task(): + await asyncio.sleep(0.2) + + await hass.async_create_task(_background_task()) + return True + + return async_setup + + mock_integration( + hass, + MockModule( + domain="normal_integration", + async_setup=gen_domain_setup("normal_integration"), + partial_manifest={"after_dependencies": ["an_after_dep"]}, + ), + ) + mock_integration( + hass, + MockModule( + domain="an_after_dep", + async_setup=gen_domain_setup("an_after_dep"), + ), + ) + + integrations = [] + + @core.callback + def _bootstrap_integrations(data): + integrations.append(data) + + async_dispatcher_connect( + hass, SIGNAL_BOOTSTRAP_INTEGRATONS, _bootstrap_integrations + ) + with patch.object(bootstrap, "SLOW_STARTUP_CHECK_INTERVAL", 0.05): + await bootstrap._async_set_up_integrations( + hass, {"normal_integration": {}, "an_after_dep": {}} + ) + + assert integrations[0] != {} + assert "an_after_dep" in integrations[0] + assert integrations[-3] != {} + assert integrations[-1] == {} + + assert "normal_integration" in hass.config.components + assert order == ["an_after_dep", "normal_integration"] From 2c8b7c56f5f8c46e9629c0433d29ca7a05a782ed Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 16 Apr 2021 09:03:34 +0200 Subject: [PATCH 0302/1317] Fix race when restarting script (#49247) --- homeassistant/helpers/script.py | 25 +++++++++++++------ tests/components/automation/test_blueprint.py | 12 ++++----- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 84e7b0639e5e5..6ecb25dfff133 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -1144,10 +1144,7 @@ async def async_run( self._log("Already running", level=LOGSEVERITY[self._max_exceeded]) script_execution_set("failed_single") return - if self.script_mode == SCRIPT_MODE_RESTART: - self._log("Restarting") - await self.async_stop(update_state=False) - elif len(self._runs) == self.max_runs: + if self.script_mode != SCRIPT_MODE_RESTART and self.runs == self.max_runs: if self._max_exceeded != "SILENT": self._log( "Maximum number of runs exceeded", @@ -1186,6 +1183,14 @@ async def async_run( self._hass, self, cast(dict, variables), context, self._log_exceptions ) self._runs.append(run) + if self.script_mode == SCRIPT_MODE_RESTART: + # When script mode is SCRIPT_MODE_RESTART, first add the new run and then + # stop any other runs. If we stop other runs first, self.is_running will + # return false after the other script runs were stopped until our task + # resumes running. + self._log("Restarting") + await self.async_stop(update_state=False, spare=run) + if started_action: self._hass.async_run_job(started_action) self.last_triggered = utcnow() @@ -1198,17 +1203,21 @@ async def async_run( self._changed() raise - async def _async_stop(self, update_state): - aws = [asyncio.create_task(run.async_stop()) for run in self._runs] + async def _async_stop(self, update_state, spare=None): + aws = [ + asyncio.create_task(run.async_stop()) for run in self._runs if run != spare + ] if not aws: return await asyncio.wait(aws) if update_state: self._changed() - async def async_stop(self, update_state: bool = True) -> None: + async def async_stop( + self, update_state: bool = True, spare: _ScriptRun | None = None + ) -> None: """Stop running script.""" - await asyncio.shield(self._async_stop(update_state)) + await asyncio.shield(self._async_stop(update_state, spare)) async def _async_get_condition(self, config): if isinstance(config, template.Template): diff --git a/tests/components/automation/test_blueprint.py b/tests/components/automation/test_blueprint.py index 747e162fe46d2..e035c238383f0 100644 --- a/tests/components/automation/test_blueprint.py +++ b/tests/components/automation/test_blueprint.py @@ -156,8 +156,8 @@ async def test_motion_light(hass): # Turn on motion hass.states.async_set("binary_sensor.kitchen", "on") # Can't block till done because delay is active - # So wait 5 event loop iterations to process script - for _ in range(5): + # So wait 10 event loop iterations to process script + for _ in range(10): await asyncio.sleep(0) assert len(turn_on_calls) == 1 @@ -165,7 +165,7 @@ async def test_motion_light(hass): # Test light doesn't turn off if motion stays async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200)) - for _ in range(5): + for _ in range(10): await asyncio.sleep(0) assert len(turn_off_calls) == 0 @@ -173,7 +173,7 @@ async def test_motion_light(hass): # Test light turns off off 120s after last motion hass.states.async_set("binary_sensor.kitchen", "off") - for _ in range(5): + for _ in range(10): await asyncio.sleep(0) async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=120)) @@ -184,7 +184,7 @@ async def test_motion_light(hass): # Test restarting the script hass.states.async_set("binary_sensor.kitchen", "on") - for _ in range(5): + for _ in range(10): await asyncio.sleep(0) assert len(turn_on_calls) == 2 @@ -192,7 +192,7 @@ async def test_motion_light(hass): hass.states.async_set("binary_sensor.kitchen", "off") - for _ in range(5): + for _ in range(10): await asyncio.sleep(0) hass.states.async_set("binary_sensor.kitchen", "on") From ec5c6e18eca013b8f92862be842292f312e29e10 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 16 Apr 2021 09:11:19 +0200 Subject: [PATCH 0303/1317] Fix ignorability of AdGuard hassio discovery step (#49276) --- .../components/adguard/config_flow.py | 35 +------- tests/components/adguard/test_config_flow.py | 85 ++----------------- 2 files changed, 11 insertions(+), 109 deletions(-) diff --git a/homeassistant/components/adguard/config_flow.py b/homeassistant/components/adguard/config_flow.py index c024d82b6ae07..f209e8c21b6b8 100644 --- a/homeassistant/components/adguard/config_flow.py +++ b/homeassistant/components/adguard/config_flow.py @@ -112,39 +112,10 @@ async def async_step_hassio(self, discovery_info: dict[str, Any]) -> FlowResultD This flow is triggered by the discovery component. """ - entries = self._async_current_entries() - - if not entries: - self._hassio_discovery = discovery_info - await self._async_handle_discovery_without_unique_id() - return await self.async_step_hassio_confirm() - - cur_entry = entries[0] - - if ( - cur_entry.data[CONF_HOST] == discovery_info[CONF_HOST] - and cur_entry.data[CONF_PORT] == discovery_info[CONF_PORT] - ): - return self.async_abort(reason="already_configured") - - is_loaded = cur_entry.state == config_entries.ENTRY_STATE_LOADED - - if is_loaded: - await self.hass.config_entries.async_unload(cur_entry.entry_id) - - self.hass.config_entries.async_update_entry( - cur_entry, - data={ - **cur_entry.data, - CONF_HOST: discovery_info[CONF_HOST], - CONF_PORT: discovery_info[CONF_PORT], - }, - ) - - if is_loaded: - await self.hass.config_entries.async_setup(cur_entry.entry_id) + await self._async_handle_discovery_without_unique_id() - return self.async_abort(reason="existing_instance_updated") + self._hassio_discovery = discovery_info + return await self.async_step_hassio_confirm() async def async_step_hassio_confirm( self, user_input: dict[str, Any] | None = None diff --git a/tests/components/adguard/test_config_flow.py b/tests/components/adguard/test_config_flow.py index 7e46c8a4b4689..17fcbda666da2 100644 --- a/tests/components/adguard/test_config_flow.py +++ b/tests/components/adguard/test_config_flow.py @@ -1,7 +1,4 @@ """Tests for the AdGuard Home config flow.""" - -from unittest.mock import patch - import aiohttp from homeassistant import config_entries, data_entry_flow @@ -120,88 +117,22 @@ async def test_hassio_already_configured(hass: HomeAssistant) -> None: assert result["reason"] == "already_configured" -async def test_hassio_update_instance_not_running(hass: HomeAssistant) -> None: - """Test we only allow a single config flow.""" - entry = MockConfigEntry( - domain=DOMAIN, data={"host": "mock-adguard", "port": "3000"} +async def test_hassio_ignored(hass: HomeAssistant) -> None: + """Test we supervisor discovered instance can be ignored.""" + MockConfigEntry(domain=DOMAIN, source=config_entries.SOURCE_IGNORE).add_to_hass( + hass ) - entry.add_to_hass(hass) - assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED result = await hass.config_entries.flow.async_init( DOMAIN, - data={ - "addon": "AdGuard Home Addon", - "host": "mock-adguard-updated", - "port": "3000", - }, + data={"addon": "AdGuard Home Addon", "host": "mock-adguard", "port": "3000"}, context={"source": "hassio"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "existing_instance_updated" - - -async def test_hassio_update_instance_running( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test we only allow a single config flow.""" - aioclient_mock.get( - "http://mock-adguard-updated:3000/control/status", - json={"version": "v0.99.0"}, - headers={"Content-Type": CONTENT_TYPE_JSON}, - ) - aioclient_mock.get( - "http://mock-adguard:3000/control/status", - json={"version": "v0.99.0"}, - headers={"Content-Type": CONTENT_TYPE_JSON}, - ) - - entry = MockConfigEntry( - domain=DOMAIN, - data={ - "host": "mock-adguard", - "port": "3000", - "verify_ssl": False, - "username": None, - "password": None, - "ssl": False, - }, - ) - entry.add_to_hass(hass) - - with patch.object( - hass.config_entries, - "async_forward_entry_setup", - return_value=True, - ) as mock_load: - assert await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == config_entries.ENTRY_STATE_LOADED - assert len(mock_load.mock_calls) == 2 - - with patch.object( - hass.config_entries, - "async_forward_entry_unload", - return_value=True, - ) as mock_unload, patch.object( - hass.config_entries, - "async_forward_entry_setup", - return_value=True, - ) as mock_load: - result = await hass.config_entries.flow.async_init( - DOMAIN, - data={ - "addon": "AdGuard Home Addon", - "host": "mock-adguard-updated", - "port": "3000", - }, - context={"source": "hassio"}, - ) - assert len(mock_unload.mock_calls) == 2 - assert len(mock_load.mock_calls) == 2 + assert "type" in result assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "existing_instance_updated" - assert entry.data["host"] == "mock-adguard-updated" + assert "reason" in result + assert result["reason"] == "already_configured" async def test_hassio_confirm( From ee37d8141a14a2d86f9e38dd8e19b12ce4166aeb Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 16 Apr 2021 09:35:19 +0200 Subject: [PATCH 0304/1317] Upgrade flake8 to 3.9.1 (#49284) --- .pre-commit-config.yaml | 2 +- requirements_test_pre_commit.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9ea2ea51348c6..1e257b537c2b2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,7 +23,7 @@ repos: exclude_types: [csv, json] exclude: ^tests/fixtures/ - repo: https://gitlab.com/pycqa/flake8 - rev: 3.9.0 + rev: 3.9.1 hooks: - id: flake8 additional_dependencies: diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 0bdcde4080816..01115cbede8e5 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -6,7 +6,7 @@ codespell==2.0.0 flake8-comprehensions==3.4.0 flake8-docstrings==1.6.0 flake8-noqa==1.1.0 -flake8==3.9.0 +flake8==3.9.1 isort==5.7.0 pycodestyle==2.7.0 pydocstyle==6.0.0 From 93dbc26db5f67d4af28490f9978d951f471be404 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 16 Apr 2021 10:53:19 +0200 Subject: [PATCH 0305/1317] Fix Coronavirus integration robustness (#49287) Co-authored-by: Martin Hjelmare --- .../components/coronavirus/__init__.py | 18 ++++++++----- .../components/coronavirus/config_flow.py | 13 ++++++++-- .../components/coronavirus/strings.json | 1 + .../coronavirus/translations/en.json | 3 ++- .../coronavirus/test_config_flow.py | 26 ++++++++++++++++++- tests/components/coronavirus/test_init.py | 25 +++++++++++++++++- 6 files changed, 74 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/coronavirus/__init__.py b/homeassistant/components/coronavirus/__init__.py index d05c4cef862ba..4bda4edcd3767 100644 --- a/homeassistant/components/coronavirus/__init__.py +++ b/homeassistant/components/coronavirus/__init__.py @@ -15,14 +15,14 @@ PLATFORMS = ["sensor"] -async def async_setup(hass: HomeAssistant, config: dict): +async def async_setup(hass: HomeAssistant, config: dict) -> bool: """Set up the Coronavirus component.""" # Make sure coordinator is initialized. await get_coordinator(hass) return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Coronavirus from a config entry.""" if isinstance(entry.data["country"], int): hass.config_entries.async_update_entry( @@ -44,6 +44,10 @@ def _async_migrator(entity_entry: entity_registry.RegistryEntry): if not entry.unique_id: hass.config_entries.async_update_entry(entry, unique_id=entry.data["country"]) + coordinator = await get_coordinator(hass) + if not coordinator.last_update_success: + await coordinator.async_config_entry_first_refresh() + for platform in PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, platform) @@ -52,9 +56,9 @@ def _async_migrator(entity_entry: entity_registry.RegistryEntry): return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = all( + return all( await asyncio.gather( *[ hass.config_entries.async_forward_entry_unload(entry, platform) @@ -63,10 +67,10 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): ) ) - return unload_ok - -async def get_coordinator(hass): +async def get_coordinator( + hass: HomeAssistant, +) -> update_coordinator.DataUpdateCoordinator: """Get the data update coordinator.""" if DOMAIN in hass.data: return hass.data[DOMAIN] diff --git a/homeassistant/components/coronavirus/config_flow.py b/homeassistant/components/coronavirus/config_flow.py index 6d2776c7ecc5e..4f6e865fa37fb 100644 --- a/homeassistant/components/coronavirus/config_flow.py +++ b/homeassistant/components/coronavirus/config_flow.py @@ -1,4 +1,8 @@ """Config flow for Coronavirus integration.""" +from __future__ import annotations + +from typing import Any + import voluptuous as vol from homeassistant import config_entries @@ -15,13 +19,18 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): _options = None - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> dict[str, Any]: """Handle the initial step.""" errors = {} if self._options is None: - self._options = {OPTION_WORLDWIDE: "Worldwide"} coordinator = await get_coordinator(self.hass) + if not coordinator.last_update_success: + return self.async_abort(reason="cannot_connect") + + self._options = {OPTION_WORLDWIDE: "Worldwide"} for case in sorted( coordinator.data.values(), key=lambda case: case.country ): diff --git a/homeassistant/components/coronavirus/strings.json b/homeassistant/components/coronavirus/strings.json index 6a5b262600385..e0b29d6c8db1e 100644 --- a/homeassistant/components/coronavirus/strings.json +++ b/homeassistant/components/coronavirus/strings.json @@ -7,6 +7,7 @@ } }, "abort": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" } } diff --git a/homeassistant/components/coronavirus/translations/en.json b/homeassistant/components/coronavirus/translations/en.json index cbd057bfce109..ea7ba1f6f9d81 100644 --- a/homeassistant/components/coronavirus/translations/en.json +++ b/homeassistant/components/coronavirus/translations/en.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Service is already configured" + "already_configured": "Service is already configured", + "cannot_connect": "Failed to connect" }, "step": { "user": { diff --git a/tests/components/coronavirus/test_config_flow.py b/tests/components/coronavirus/test_config_flow.py index 06d586ba2a5a3..bfc6920089367 100644 --- a/tests/components/coronavirus/test_config_flow.py +++ b/tests/components/coronavirus/test_config_flow.py @@ -1,9 +1,14 @@ """Test the Coronavirus config flow.""" +from unittest.mock import MagicMock, patch + +from aiohttp import ClientError + from homeassistant import config_entries, setup from homeassistant.components.coronavirus.const import DOMAIN, OPTION_WORLDWIDE +from homeassistant.core import HomeAssistant -async def test_form(hass): +async def test_form(hass: HomeAssistant) -> None: """Test we get the form.""" await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( @@ -24,3 +29,22 @@ async def test_form(hass): } await hass.async_block_till_done() assert len(hass.states.async_all()) == 4 + + +@patch( + "coronavirus.get_cases", + side_effect=ClientError, +) +async def test_abort_on_connection_error( + mock_get_cases: MagicMock, hass: HomeAssistant +) -> None: + """Test we abort on connection error.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert "type" in result + assert result["type"] == "abort" + assert "reason" in result + assert result["reason"] == "cannot_connect" diff --git a/tests/components/coronavirus/test_init.py b/tests/components/coronavirus/test_init.py index cc49bf7d4b6fe..c36255db9d148 100644 --- a/tests/components/coronavirus/test_init.py +++ b/tests/components/coronavirus/test_init.py @@ -1,12 +1,18 @@ """Test init of Coronavirus integration.""" +from unittest.mock import MagicMock, patch + +from aiohttp import ClientError + from homeassistant.components.coronavirus.const import DOMAIN, OPTION_WORLDWIDE +from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY +from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, mock_registry -async def test_migration(hass): +async def test_migration(hass: HomeAssistant) -> None: """Test that we can migrate coronavirus to stable unique ID.""" nl_entry = MockConfigEntry(domain=DOMAIN, title="Netherlands", data={"country": 34}) nl_entry.add_to_hass(hass) @@ -47,3 +53,20 @@ async def test_migration(hass): assert nl_entry.unique_id == "Netherlands" assert worldwide_entry.unique_id == OPTION_WORLDWIDE + + +@patch( + "coronavirus.get_cases", + side_effect=ClientError, +) +async def test_config_entry_not_ready( + mock_get_cases: MagicMock, hass: HomeAssistant +) -> None: + """Test the configuration entry not ready.""" + entry = MockConfigEntry(domain=DOMAIN, title="Netherlands", data={"country": 34}) + entry.add_to_hass(hass) + + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + assert entry.state == ENTRY_STATE_SETUP_RETRY From c98788edaedaaf3d9400b7a2c56b93af47bbe712 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Fri, 16 Apr 2021 15:00:21 +0200 Subject: [PATCH 0306/1317] Mark camera as a base platform (#49297) --- homeassistant/setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/setup.py b/homeassistant/setup.py index bead16c1d789e..c1d4173fff155 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -27,6 +27,7 @@ "air_quality", "alarm_control_panel", "binary_sensor", + "camera", "climate", "cover", "device_tracker", From 73a9cb6adbc22514559cf102a2fcc5f3adc30c59 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 16 Apr 2021 15:03:15 +0200 Subject: [PATCH 0307/1317] Deprecate GNTP (Growl) integration (#49273) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Joakim Sørensen --- homeassistant/components/gntp/notify.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/gntp/notify.py b/homeassistant/components/gntp/notify.py index c05ce84272c02..b3291e256174c 100644 --- a/homeassistant/components/gntp/notify.py +++ b/homeassistant/components/gntp/notify.py @@ -38,6 +38,11 @@ def get_service(hass, config, discovery_info=None): """Get the GNTP notification service.""" + _LOGGER.warning( + "The GNTP (Growl) integration has been deprecated and is going to be " + "removed in Home Assistant Core 2021.6. The Growl project has retired" + ) + logging.getLogger("gntp").setLevel(logging.ERROR) if config.get(CONF_APP_ICON) is None: From 969c147b77e8b44889d734d2922cc5fc63a3990d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 16 Apr 2021 17:46:49 +0200 Subject: [PATCH 0308/1317] Clean up superfluous integration setup - part 4 (#49295) * Clean up superfluous integration setup - part 4 * Adjust tests --- .../components/garmin_connect/__init__.py | 7 +---- homeassistant/components/goalzero/__init__.py | 9 +------ homeassistant/components/kmtronic/__init__.py | 8 +----- homeassistant/components/kodi/__init__.py | 7 +---- homeassistant/components/kulersky/__init__.py | 5 ---- homeassistant/components/met/__init__.py | 8 +----- homeassistant/components/onewire/__init__.py | 5 ---- .../components/ovo_energy/__init__.py | 5 ---- .../components/philips_js/__init__.py | 7 +---- homeassistant/components/plaato/__init__.py | 8 +----- homeassistant/components/plugwise/__init__.py | 5 ---- .../components/poolsense/__init__.py | 8 +----- .../components/powerwall/__init__.py | 8 +----- homeassistant/components/profiler/__init__.py | 6 ----- .../components/progettihwsw/__init__.py | 9 +------ homeassistant/components/risco/__init__.py | 7 +---- homeassistant/components/roon/__init__.py | 7 +---- homeassistant/components/sharkiq/__init__.py | 7 +---- .../components/srp_energy/__init__.py | 5 ---- homeassistant/components/subaru/__init__.py | 7 +---- .../garmin_connect/test_config_flow.py | 2 +- tests/components/kmtronic/test_config_flow.py | 3 --- tests/components/kodi/test_config_flow.py | 18 ------------- tests/components/kulersky/test_config_flow.py | 9 ------- tests/components/onewire/test_config_flow.py | 27 ------------------- .../components/ovo_energy/test_config_flow.py | 3 --- .../components/philips_js/test_config_flow.py | 24 ++++------------- tests/components/plaato/test_config_flow.py | 6 ----- tests/components/plugwise/test_config_flow.py | 15 ----------- .../components/poolsense/test_config_flow.py | 3 --- .../components/powerwall/test_config_flow.py | 9 ------- tests/components/profiler/test_config_flow.py | 3 --- .../progettihwsw/test_config_flow.py | 3 --- tests/components/risco/test_config_flow.py | 3 --- tests/components/roon/test_config_flow.py | 8 ------ tests/components/sharkiq/test_config_flow.py | 3 --- .../components/srp_energy/test_config_flow.py | 3 --- tests/components/subaru/test_config_flow.py | 7 ----- 38 files changed, 20 insertions(+), 267 deletions(-) diff --git a/homeassistant/components/garmin_connect/__init__.py b/homeassistant/components/garmin_connect/__init__.py index c009124b02449..f816196aa290b 100644 --- a/homeassistant/components/garmin_connect/__init__.py +++ b/homeassistant/components/garmin_connect/__init__.py @@ -24,12 +24,6 @@ MIN_SCAN_INTERVAL = timedelta(minutes=10) -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Garmin Connect component.""" - hass.data[DOMAIN] = {} - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Garmin Connect from a config entry.""" username = entry.data[CONF_USERNAME] @@ -55,6 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): return False garmin_data = GarminConnectData(hass, garmin_client) + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = garmin_data for platform in PLATFORMS: diff --git a/homeassistant/components/goalzero/__init__.py b/homeassistant/components/goalzero/__init__.py index e00b17ebae4b1..e2e8bd5981cc7 100644 --- a/homeassistant/components/goalzero/__init__.py +++ b/homeassistant/components/goalzero/__init__.py @@ -23,14 +23,6 @@ PLATFORMS = ["binary_sensor"] -async def async_setup(hass: HomeAssistant, config): - """Set up the Goal Zero Yeti component.""" - - hass.data[DOMAIN] = {} - - return True - - async def async_setup_entry(hass, entry): """Set up Goal Zero Yeti from a config entry.""" name = entry.data[CONF_NAME] @@ -58,6 +50,7 @@ async def async_update_data(): update_method=async_update_data, update_interval=MIN_TIME_BETWEEN_UPDATES, ) + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { DATA_KEY_API: api, DATA_KEY_COORDINATOR: coordinator, diff --git a/homeassistant/components/kmtronic/__init__.py b/homeassistant/components/kmtronic/__init__.py index d311940f4bca7..a028a62cbc54e 100644 --- a/homeassistant/components/kmtronic/__init__.py +++ b/homeassistant/components/kmtronic/__init__.py @@ -24,13 +24,6 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the kmtronic component.""" - hass.data[DOMAIN] = {} - - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up kmtronic from a config entry.""" session = aiohttp_client.async_get_clientsession(hass) @@ -60,6 +53,7 @@ async def async_update_data(): ) await coordinator.async_config_entry_first_refresh() + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { DATA_HUB: hub, DATA_COORDINATOR: coordinator, diff --git a/homeassistant/components/kodi/__init__.py b/homeassistant/components/kodi/__init__.py index ea867e8c407f1..d42b4aa2ec4b6 100644 --- a/homeassistant/components/kodi/__init__.py +++ b/homeassistant/components/kodi/__init__.py @@ -29,12 +29,6 @@ PLATFORMS = ["media_player"] -async def async_setup(hass, config): - """Set up the Kodi integration.""" - hass.data.setdefault(DOMAIN, {}) - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Kodi from a config entry.""" conn = get_kodi_connection( @@ -66,6 +60,7 @@ async def _close(event): remove_stop_listener = hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _close) + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { DATA_CONNECTION: conn, DATA_KODI: kodi, diff --git a/homeassistant/components/kulersky/__init__.py b/homeassistant/components/kulersky/__init__.py index 951e2a5353f3f..358d13dee564f 100644 --- a/homeassistant/components/kulersky/__init__.py +++ b/homeassistant/components/kulersky/__init__.py @@ -9,11 +9,6 @@ PLATFORMS = ["light"] -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Kuler Sky component.""" - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Kuler Sky from a config entry.""" if DOMAIN not in hass.data: diff --git a/homeassistant/components/met/__init__.py b/homeassistant/components/met/__init__.py index 47d946b92e7a6..1e1a203342ebe 100644 --- a/homeassistant/components/met/__init__.py +++ b/homeassistant/components/met/__init__.py @@ -13,7 +13,6 @@ LENGTH_FEET, LENGTH_METERS, ) -from homeassistant.core import Config, HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util.distance import convert as convert_distance @@ -32,12 +31,6 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: Config) -> bool: - """Set up configured Met.""" - hass.data.setdefault(DOMAIN, {}) - return True - - async def async_setup_entry(hass, config_entry): """Set up Met as config entry.""" # Don't setup if tracking home location and latitude or longitude isn't set. @@ -60,6 +53,7 @@ async def async_setup_entry(hass, config_entry): if config_entry.data.get(CONF_TRACK_HOME, False): coordinator.track_home() + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][config_entry.entry_id] = coordinator hass.async_create_task( diff --git a/homeassistant/components/onewire/__init__.py b/homeassistant/components/onewire/__init__.py index e5a214ce8a4a3..848cfc9086dc5 100644 --- a/homeassistant/components/onewire/__init__.py +++ b/homeassistant/components/onewire/__init__.py @@ -13,11 +13,6 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup(hass, config): - """Set up 1-Wire integrations.""" - return True - - async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry): """Set up a 1-Wire proxy for a config entry.""" hass.data.setdefault(DOMAIN, {}) diff --git a/homeassistant/components/ovo_energy/__init__.py b/homeassistant/components/ovo_energy/__init__.py index 77fafef05ca8f..84e1182b38150 100644 --- a/homeassistant/components/ovo_energy/__init__.py +++ b/homeassistant/components/ovo_energy/__init__.py @@ -25,11 +25,6 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: - """Set up the OVO Energy components.""" - return True - - async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: """Set up OVO Energy from a config entry.""" diff --git a/homeassistant/components/philips_js/__init__.py b/homeassistant/components/philips_js/__init__.py index b585451cdb076..836c5392f9fd5 100644 --- a/homeassistant/components/philips_js/__init__.py +++ b/homeassistant/components/philips_js/__init__.py @@ -28,12 +28,6 @@ LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Philips TV component.""" - hass.data[DOMAIN] = {} - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Philips TV from a config entry.""" @@ -47,6 +41,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): coordinator = PhilipsTVDataUpdateCoordinator(hass, tvapi) await coordinator.async_refresh() + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator for platform in PLATFORMS: diff --git a/homeassistant/components/plaato/__init__.py b/homeassistant/components/plaato/__init__.py index 2ec6028f9f94c..9ed8d85f2324a 100644 --- a/homeassistant/components/plaato/__init__.py +++ b/homeassistant/components/plaato/__init__.py @@ -84,15 +84,9 @@ ) -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Plaato component.""" - hass.data.setdefault(DOMAIN, {}) - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Configure based on config entry.""" - + hass.data.setdefault(DOMAIN, {}) use_webhook = entry.data[CONF_USE_WEBHOOK] if use_webhook: diff --git a/homeassistant/components/plugwise/__init__.py b/homeassistant/components/plugwise/__init__.py index 47a9a1e7d9c1d..d425cca246ebb 100644 --- a/homeassistant/components/plugwise/__init__.py +++ b/homeassistant/components/plugwise/__init__.py @@ -7,11 +7,6 @@ from .gateway import async_setup_entry_gw, async_unload_entry_gw -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Plugwise platform.""" - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Plugwise components from a config entry.""" if entry.data.get(CONF_HOST): diff --git a/homeassistant/components/poolsense/__init__.py b/homeassistant/components/poolsense/__init__.py index cfc2abb031642..4fee2d01a73d2 100644 --- a/homeassistant/components/poolsense/__init__.py +++ b/homeassistant/components/poolsense/__init__.py @@ -25,13 +25,6 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the PoolSense component.""" - # Make sure coordinator is initialized. - hass.data.setdefault(DOMAIN, {}) - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up PoolSense from a config entry.""" @@ -50,6 +43,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): await coordinator.async_config_entry_first_refresh() + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator for platform in PLATFORMS: diff --git a/homeassistant/components/powerwall/__init__.py b/homeassistant/components/powerwall/__init__.py index 6d61db659c8d1..e3c08e747701f 100644 --- a/homeassistant/components/powerwall/__init__.py +++ b/homeassistant/components/powerwall/__init__.py @@ -43,13 +43,6 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Tesla Powerwall component.""" - hass.data.setdefault(DOMAIN, {}) - - return True - - async def _migrate_old_unique_ids(hass, entry_id, powerwall_data): serial_numbers = powerwall_data[POWERWALL_API_SERIAL_NUMBERS] site_info = powerwall_data[POWERWALL_API_SITE_INFO] @@ -96,6 +89,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): entry_id = entry.entry_id + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN].setdefault(entry_id, {}) http_session = requests.Session() diff --git a/homeassistant/components/profiler/__init__.py b/homeassistant/components/profiler/__init__.py index c8f6c9fd1a22a..c3f4ab17686f5 100644 --- a/homeassistant/components/profiler/__init__.py +++ b/homeassistant/components/profiler/__init__.py @@ -15,7 +15,6 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.service import async_register_admin_service -from homeassistant.helpers.typing import ConfigType from .const import DOMAIN @@ -44,11 +43,6 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the profiler component.""" - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Profiler from a config entry.""" diff --git a/homeassistant/components/progettihwsw/__init__.py b/homeassistant/components/progettihwsw/__init__.py index 7597b2ff1a292..bb8757e096268 100644 --- a/homeassistant/components/progettihwsw/__init__.py +++ b/homeassistant/components/progettihwsw/__init__.py @@ -13,16 +13,9 @@ PLATFORMS = ["switch", "binary_sensor"] -async def async_setup(hass, config): - """Set up the ProgettiHWSW Automation component.""" - hass.data[DOMAIN] = {} - - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up ProgettiHWSW Automation from a config entry.""" - + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = ProgettiHWSWAPI( f'{entry.data["host"]}:{entry.data["port"]}' ) diff --git a/homeassistant/components/risco/__init__.py b/homeassistant/components/risco/__init__.py index eec30553870f9..3a39bbb00f3f9 100644 --- a/homeassistant/components/risco/__init__.py +++ b/homeassistant/components/risco/__init__.py @@ -27,12 +27,6 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Risco component.""" - hass.data.setdefault(DOMAIN, {}) - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Risco from a config entry.""" data = entry.data @@ -54,6 +48,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): undo_listener = entry.add_update_listener(_update_listener) + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { DATA_COORDINATOR: coordinator, UNDO_UPDATE_LISTENER: undo_listener, diff --git a/homeassistant/components/roon/__init__.py b/homeassistant/components/roon/__init__.py index 49527a44245a0..c9dbe86ee4b46 100644 --- a/homeassistant/components/roon/__init__.py +++ b/homeassistant/components/roon/__init__.py @@ -6,14 +6,9 @@ from .server import RoonServer -async def async_setup(hass, config): - """Set up the Roon platform.""" - hass.data[DOMAIN] = {} - return True - - async def async_setup_entry(hass, entry): """Set up a roonserver from a config entry.""" + hass.data.setdefault(DOMAIN, {}) host = entry.data[CONF_HOST] roonserver = RoonServer(hass, entry) diff --git a/homeassistant/components/sharkiq/__init__.py b/homeassistant/components/sharkiq/__init__.py index 94b6a8f2e3bfd..02e1bba85111d 100644 --- a/homeassistant/components/sharkiq/__init__.py +++ b/homeassistant/components/sharkiq/__init__.py @@ -23,12 +23,6 @@ class CannotConnect(exceptions.HomeAssistantError): """Error to indicate we cannot connect.""" -async def async_setup(hass, config): - """Set up the sharkiq environment.""" - hass.data.setdefault(DOMAIN, {}) - return True - - async def async_connect_or_timeout(ayla_api: AylaApi) -> bool: """Connect to vacuum.""" try: @@ -66,6 +60,7 @@ async def async_setup_entry(hass, config_entry): await coordinator.async_config_entry_first_refresh() + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][config_entry.entry_id] = coordinator for platform in PLATFORMS: diff --git a/homeassistant/components/srp_energy/__init__.py b/homeassistant/components/srp_energy/__init__.py index f7cc1ff8c16b5..b8a93ee44b07c 100644 --- a/homeassistant/components/srp_energy/__init__.py +++ b/homeassistant/components/srp_energy/__init__.py @@ -16,11 +16,6 @@ PLATFORMS = ["sensor"] -async def async_setup(hass, config): - """Old way of setting up the srp_energy component.""" - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up the SRP Energy component from a config entry.""" # Store an SrpEnergyClient object for your srp_energy to access diff --git a/homeassistant/components/subaru/__init__.py b/homeassistant/components/subaru/__init__.py index 04f6111167105..4807ca259101d 100644 --- a/homeassistant/components/subaru/__init__.py +++ b/homeassistant/components/subaru/__init__.py @@ -37,12 +37,6 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup(hass, base_config): - """Do nothing since this integration does not support configuration.yml setup.""" - hass.data.setdefault(DOMAIN, {}) - return True - - async def async_setup_entry(hass, entry): """Set up Subaru from a config entry.""" config = entry.data @@ -88,6 +82,7 @@ async def async_update_data(): await coordinator.async_refresh() + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { ENTRY_CONTROLLER: controller, ENTRY_COORDINATOR: coordinator, diff --git a/tests/components/garmin_connect/test_config_flow.py b/tests/components/garmin_connect/test_config_flow.py index 75146570d5563..eed9d8dceae7c 100644 --- a/tests/components/garmin_connect/test_config_flow.py +++ b/tests/components/garmin_connect/test_config_flow.py @@ -45,7 +45,7 @@ async def test_step_user(hass, mock_garmin_connect): with patch( "homeassistant.components.garmin_connect.async_setup_entry", return_value=True - ), patch("homeassistant.components.garmin_connect.async_setup", return_value=True): + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "user"}, data=MOCK_CONF ) diff --git a/tests/components/kmtronic/test_config_flow.py b/tests/components/kmtronic/test_config_flow.py index b5ebdc79c8ba4..71482d6f7b2eb 100644 --- a/tests/components/kmtronic/test_config_flow.py +++ b/tests/components/kmtronic/test_config_flow.py @@ -23,8 +23,6 @@ async def test_form(hass): "homeassistant.components.kmtronic.config_flow.KMTronicHubAPI.async_get_status", return_value=[Mock()], ), patch( - "homeassistant.components.kmtronic.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.kmtronic.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -45,7 +43,6 @@ async def test_form(hass): "password": "test-password", } await hass.async_block_till_done() - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/kodi/test_config_flow.py b/tests/components/kodi/test_config_flow.py index cba567e5bb52b..8b8bcf7e88acc 100644 --- a/tests/components/kodi/test_config_flow.py +++ b/tests/components/kodi/test_config_flow.py @@ -47,8 +47,6 @@ async def test_user_flow(hass, user_flow): "homeassistant.components.kodi.config_flow.get_kodi_connection", return_value=MockConnection(), ), patch( - "homeassistant.components.kodi.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.kodi.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -66,7 +64,6 @@ async def test_user_flow(hass, user_flow): "timeout": DEFAULT_TIMEOUT, } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -92,8 +89,6 @@ async def test_form_valid_auth(hass, user_flow): "homeassistant.components.kodi.config_flow.get_kodi_connection", return_value=MockConnection(), ), patch( - "homeassistant.components.kodi.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.kodi.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -112,7 +107,6 @@ async def test_form_valid_auth(hass, user_flow): "timeout": DEFAULT_TIMEOUT, } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -142,8 +136,6 @@ async def test_form_valid_ws_port(hass, user_flow): "homeassistant.components.kodi.config_flow.get_kodi_connection", return_value=MockConnection(), ), patch( - "homeassistant.components.kodi.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.kodi.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -163,7 +155,6 @@ async def test_form_valid_ws_port(hass, user_flow): "timeout": DEFAULT_TIMEOUT, } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -187,8 +178,6 @@ async def test_form_empty_ws_port(hass, user_flow): assert result["errors"] == {} with patch( - "homeassistant.components.kodi.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.kodi.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -208,7 +197,6 @@ async def test_form_empty_ws_port(hass, user_flow): "timeout": DEFAULT_TIMEOUT, } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -430,8 +418,6 @@ async def test_discovery(hass): assert result["step_id"] == "discovery_confirm" with patch( - "homeassistant.components.kodi.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.kodi.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -451,7 +437,6 @@ async def test_discovery(hass): "timeout": DEFAULT_TIMEOUT, } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -594,8 +579,6 @@ async def test_form_import(hass): "homeassistant.components.kodi.config_flow.get_kodi_connection", return_value=MockConnection(), ), patch( - "homeassistant.components.kodi.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.kodi.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -610,7 +593,6 @@ async def test_form_import(hass): assert result["title"] == TEST_IMPORT["name"] assert result["data"] == TEST_IMPORT - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/kulersky/test_config_flow.py b/tests/components/kulersky/test_config_flow.py index c6933a01d3a91..75b1326d3380f 100644 --- a/tests/components/kulersky/test_config_flow.py +++ b/tests/components/kulersky/test_config_flow.py @@ -23,8 +23,6 @@ async def test_flow_success(hass): "homeassistant.components.kulersky.config_flow.pykulersky.discover", return_value=[light], ), patch( - "homeassistant.components.kulersky.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.kulersky.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -38,7 +36,6 @@ async def test_flow_success(hass): assert result2["title"] == "Kuler Sky" assert result2["data"] == {} - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -55,8 +52,6 @@ async def test_flow_no_devices_found(hass): "homeassistant.components.kulersky.config_flow.pykulersky.discover", return_value=[], ), patch( - "homeassistant.components.kulersky.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.kulersky.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -68,7 +63,6 @@ async def test_flow_no_devices_found(hass): assert result2["type"] == "abort" assert result2["reason"] == "no_devices_found" await hass.async_block_till_done() - assert len(mock_setup.mock_calls) == 0 assert len(mock_setup_entry.mock_calls) == 0 @@ -85,8 +79,6 @@ async def test_flow_exceptions_caught(hass): "homeassistant.components.kulersky.config_flow.pykulersky.discover", side_effect=pykulersky.PykulerskyException("TEST"), ), patch( - "homeassistant.components.kulersky.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.kulersky.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -98,5 +90,4 @@ async def test_flow_exceptions_caught(hass): assert result2["type"] == "abort" assert result2["reason"] == "no_devices_found" await hass.async_block_till_done() - assert len(mock_setup.mock_calls) == 0 assert len(mock_setup_entry.mock_calls) == 0 diff --git a/tests/components/onewire/test_config_flow.py b/tests/components/onewire/test_config_flow.py index ea0b5e85dda6d..66025770f41f0 100644 --- a/tests/components/onewire/test_config_flow.py +++ b/tests/components/onewire/test_config_flow.py @@ -55,8 +55,6 @@ async def test_user_owserver(hass): # Valid server with patch("homeassistant.components.onewire.onewirehub.protocol.proxy",), patch( - "homeassistant.components.onewire.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.onewire.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -73,15 +71,12 @@ async def test_user_owserver(hass): CONF_PORT: 1234, } await hass.async_block_till_done() - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 async def test_user_owserver_duplicate(hass): """Test OWServer flow.""" with patch( - "homeassistant.components.onewire.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.onewire.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -111,7 +106,6 @@ async def test_user_owserver_duplicate(hass): assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "already_configured" await hass.async_block_till_done() - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -151,8 +145,6 @@ async def test_user_sysbus(hass): "homeassistant.components.onewire.onewirehub.os.path.isdir", return_value=True, ), patch( - "homeassistant.components.onewire.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.onewire.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -168,15 +160,12 @@ async def test_user_sysbus(hass): CONF_MOUNT_DIR: "/sys/bus/directory", } await hass.async_block_till_done() - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 async def test_user_sysbus_duplicate(hass): """Test SysBus duplicate flow.""" with patch( - "homeassistant.components.onewire.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.onewire.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -211,7 +200,6 @@ async def test_user_sysbus_duplicate(hass): assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "already_configured" await hass.async_block_till_done() - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -222,8 +210,6 @@ async def test_import_sysbus(hass): "homeassistant.components.onewire.onewirehub.os.path.isdir", return_value=True, ), patch( - "homeassistant.components.onewire.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.onewire.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -239,7 +225,6 @@ async def test_import_sysbus(hass): CONF_MOUNT_DIR: DEFAULT_SYSBUS_MOUNT_DIR, } await hass.async_block_till_done() - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -250,8 +235,6 @@ async def test_import_sysbus_with_mount_dir(hass): "homeassistant.components.onewire.onewirehub.os.path.isdir", return_value=True, ), patch( - "homeassistant.components.onewire.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.onewire.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -270,7 +253,6 @@ async def test_import_sysbus_with_mount_dir(hass): CONF_MOUNT_DIR: DEFAULT_SYSBUS_MOUNT_DIR, } await hass.async_block_till_done() - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -278,8 +260,6 @@ async def test_import_owserver(hass): """Test import step.""" with patch("homeassistant.components.onewire.onewirehub.protocol.proxy",), patch( - "homeassistant.components.onewire.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.onewire.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -299,7 +279,6 @@ async def test_import_owserver(hass): CONF_PORT: DEFAULT_OWSERVER_PORT, } await hass.async_block_till_done() - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -307,8 +286,6 @@ async def test_import_owserver_with_port(hass): """Test import step.""" with patch("homeassistant.components.onewire.onewirehub.protocol.proxy",), patch( - "homeassistant.components.onewire.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.onewire.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -329,7 +306,6 @@ async def test_import_owserver_with_port(hass): CONF_PORT: 1234, } await hass.async_block_till_done() - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -337,8 +313,6 @@ async def test_import_owserver_duplicate(hass): """Test OWServer flow.""" # Initialise with single entry with patch( - "homeassistant.components.onewire.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.onewire.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -358,5 +332,4 @@ async def test_import_owserver_duplicate(hass): assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "already_configured" await hass.async_block_till_done() - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/ovo_energy/test_config_flow.py b/tests/components/ovo_energy/test_config_flow.py index ccf485211aa70..d5ee8c6d3d973 100644 --- a/tests/components/ovo_energy/test_config_flow.py +++ b/tests/components/ovo_energy/test_config_flow.py @@ -84,9 +84,6 @@ async def test_full_flow_implementation(hass: HomeAssistant) -> None: with patch( "homeassistant.components.ovo_energy.config_flow.OVOEnergy.authenticate", return_value=True, - ), patch( - "homeassistant.components.ovo_energy.async_setup", - return_value=True, ), patch( "homeassistant.components.ovo_energy.async_setup_entry", return_value=True, diff --git a/tests/components/philips_js/test_config_flow.py b/tests/components/philips_js/test_config_flow.py index 45e896319f155..48230c72dc95d 100644 --- a/tests/components/philips_js/test_config_flow.py +++ b/tests/components/philips_js/test_config_flow.py @@ -18,15 +18,6 @@ ) -@fixture(autouse=True) -def mock_setup(): - """Disable component setup.""" - with patch( - "homeassistant.components.philips_js.async_setup", return_value=True - ) as mock_setup: - yield mock_setup - - @fixture(autouse=True) def mock_setup_entry(): """Disable component setup.""" @@ -50,7 +41,7 @@ async def mock_tv_pairable(mock_tv): return mock_tv -async def test_import(hass, mock_setup, mock_setup_entry): +async def test_import(hass, mock_setup_entry): """Test we get an item on import.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -61,7 +52,6 @@ async def test_import(hass, mock_setup, mock_setup_entry): assert result["type"] == "create_entry" assert result["title"] == "Philips TV (1234567890)" assert result["data"] == MOCK_CONFIG - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -77,7 +67,7 @@ async def test_import_exist(hass, mock_config_entry): assert result["reason"] == "already_configured" -async def test_form(hass, mock_setup, mock_setup_entry): +async def test_form(hass, mock_setup_entry): """Test we get the form.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -94,7 +84,6 @@ async def test_form(hass, mock_setup, mock_setup_entry): assert result2["type"] == "create_entry" assert result2["title"] == "Philips TV (1234567890)" assert result2["data"] == MOCK_CONFIG - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -128,7 +117,7 @@ async def test_form_unexpected_error(hass, mock_tv): assert result["errors"] == {"base": "unknown"} -async def test_pairing(hass, mock_tv_pairable, mock_setup, mock_setup_entry): +async def test_pairing(hass, mock_tv_pairable, mock_setup_entry): """Test we get the form.""" mock_tv = mock_tv_pairable @@ -166,13 +155,10 @@ async def test_pairing(hass, mock_tv_pairable, mock_setup, mock_setup_entry): } await hass.async_block_till_done() - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 -async def test_pair_request_failed( - hass, mock_tv_pairable, mock_setup, mock_setup_entry -): +async def test_pair_request_failed(hass, mock_tv_pairable, mock_setup_entry): """Test we get the form.""" mock_tv = mock_tv_pairable mock_tv.pairRequest.side_effect = PairingFailure({}) @@ -197,7 +183,7 @@ async def test_pair_request_failed( } -async def test_pair_grant_failed(hass, mock_tv_pairable, mock_setup, mock_setup_entry): +async def test_pair_grant_failed(hass, mock_tv_pairable, mock_setup_entry): """Test we get the form.""" mock_tv = mock_tv_pairable diff --git a/tests/components/plaato/test_config_flow.py b/tests/components/plaato/test_config_flow.py index 7966882a97794..9f0f6de5cd626 100644 --- a/tests/components/plaato/test_config_flow.py +++ b/tests/components/plaato/test_config_flow.py @@ -243,8 +243,6 @@ async def test_options(hass): config_entry.add_to_hass(hass) with patch( - "homeassistant.components.plaato.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.plaato.async_setup_entry", return_value=True ) as mock_setup_entry: @@ -266,7 +264,6 @@ async def test_options(hass): assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["data"][CONF_SCAN_INTERVAL] == 10 - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -281,8 +278,6 @@ async def test_options_webhook(hass, webhook_id): config_entry.add_to_hass(hass) with patch( - "homeassistant.components.plaato.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.plaato.async_setup_entry", return_value=True ) as mock_setup_entry: @@ -305,5 +300,4 @@ async def test_options_webhook(hass, webhook_id): assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["data"][CONF_WEBHOOK_ID] == CONF_WEBHOOK_ID - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/plugwise/test_config_flow.py b/tests/components/plugwise/test_config_flow.py index 382e7bc1a52a9..1697a43127e87 100644 --- a/tests/components/plugwise/test_config_flow.py +++ b/tests/components/plugwise/test_config_flow.py @@ -72,9 +72,6 @@ async def test_form(hass): "homeassistant.components.plugwise.config_flow.Smile.connect", return_value=True, ), patch( - "homeassistant.components.plugwise.async_setup", - return_value=True, - ) as mock_setup, patch( "homeassistant.components.plugwise.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -93,7 +90,6 @@ async def test_form(hass): CONF_USERNAME: TEST_USERNAME, } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -112,9 +108,6 @@ async def test_zeroconf_form(hass): "homeassistant.components.plugwise.config_flow.Smile.connect", return_value=True, ), patch( - "homeassistant.components.plugwise.async_setup", - return_value=True, - ) as mock_setup, patch( "homeassistant.components.plugwise.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -133,7 +126,6 @@ async def test_zeroconf_form(hass): CONF_USERNAME: TEST_USERNAME, } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -149,9 +141,6 @@ async def test_form_username(hass): "homeassistant.components.plugwise.config_flow.Smile.connect", return_value=True, ), patch( - "homeassistant.components.plugwise.async_setup", - return_value=True, - ) as mock_setup, patch( "homeassistant.components.plugwise.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -174,7 +163,6 @@ async def test_form_username(hass): CONF_USERNAME: TEST_USERNAME2, } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 result3 = await hass.config_entries.flow.async_init( @@ -189,9 +177,6 @@ async def test_form_username(hass): "homeassistant.components.plugwise.config_flow.Smile.connect", return_value=True, ), patch( - "homeassistant.components.plugwise.async_setup", - return_value=True, - ) as mock_setup, patch( "homeassistant.components.plugwise.async_setup_entry", return_value=True, ) as mock_setup_entry: diff --git a/tests/components/poolsense/test_config_flow.py b/tests/components/poolsense/test_config_flow.py index ca32a21758e9c..71fa76df7abef 100644 --- a/tests/components/poolsense/test_config_flow.py +++ b/tests/components/poolsense/test_config_flow.py @@ -38,8 +38,6 @@ async def test_valid_credentials(hass): with patch( "poolsense.PoolSense.test_poolsense_credentials", return_value=True ), patch( - "homeassistant.components.poolsense.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.poolsense.async_setup_entry", return_value=True ) as mock_setup_entry: result = await hass.config_entries.flow.async_init( @@ -52,5 +50,4 @@ async def test_valid_credentials(hass): assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == "test-email" - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/powerwall/test_config_flow.py b/tests/components/powerwall/test_config_flow.py index be071b4594779..407a63bac23fb 100644 --- a/tests/components/powerwall/test_config_flow.py +++ b/tests/components/powerwall/test_config_flow.py @@ -35,8 +35,6 @@ async def test_form_source_user(hass): "homeassistant.components.powerwall.config_flow.Powerwall", return_value=mock_powerwall, ), patch( - "homeassistant.components.powerwall.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.powerwall.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -49,7 +47,6 @@ async def test_form_source_user(hass): assert result2["type"] == "create_entry" assert result2["title"] == "My site" assert result2["data"] == VALID_CONFIG - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -197,8 +194,6 @@ async def test_dhcp_discovery(hass): "homeassistant.components.powerwall.config_flow.Powerwall", return_value=mock_powerwall, ), patch( - "homeassistant.components.powerwall.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.powerwall.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -211,7 +206,6 @@ async def test_dhcp_discovery(hass): assert result2["type"] == "create_entry" assert result2["title"] == "Some site" assert result2["data"] == VALID_CONFIG - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -237,8 +231,6 @@ async def test_form_reauth(hass): "homeassistant.components.powerwall.config_flow.Powerwall", return_value=mock_powerwall, ), patch( - "homeassistant.components.powerwall.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.powerwall.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -253,5 +245,4 @@ async def test_form_reauth(hass): assert result2["type"] == "abort" assert result2["reason"] == "reauth_successful" - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/profiler/test_config_flow.py b/tests/components/profiler/test_config_flow.py index d3b2b47301281..2b33472e93ad0 100644 --- a/tests/components/profiler/test_config_flow.py +++ b/tests/components/profiler/test_config_flow.py @@ -17,8 +17,6 @@ async def test_form_user(hass): assert result["errors"] is None with patch( - "homeassistant.components.profiler.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.profiler.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -31,7 +29,6 @@ async def test_form_user(hass): assert result2["type"] == "create_entry" assert result2["title"] == "Profiler" assert result2["data"] == {} - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/progettihwsw/test_config_flow.py b/tests/components/progettihwsw/test_config_flow.py index 883c1acd33bdd..5d256af35c87a 100644 --- a/tests/components/progettihwsw/test_config_flow.py +++ b/tests/components/progettihwsw/test_config_flow.py @@ -48,9 +48,6 @@ async def test_form(hass): assert result2["errors"] == {} with patch( - "homeassistant.components.progettihwsw.async_setup", - return_value=True, - ), patch( "homeassistant.components.progettihwsw.async_setup_entry", return_value=True, ): diff --git a/tests/components/risco/test_config_flow.py b/tests/components/risco/test_config_flow.py index cfb1a410960b6..dfd182d4a2484 100644 --- a/tests/components/risco/test_config_flow.py +++ b/tests/components/risco/test_config_flow.py @@ -59,8 +59,6 @@ async def test_form(hass): ), patch( "homeassistant.components.risco.config_flow.RiscoAPI.close" ) as mock_close, patch( - "homeassistant.components.risco.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.risco.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -72,7 +70,6 @@ async def test_form(hass): assert result2["type"] == "create_entry" assert result2["title"] == TEST_SITE_NAME assert result2["data"] == TEST_DATA - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 mock_close.assert_awaited_once() diff --git a/tests/components/roon/test_config_flow.py b/tests/components/roon/test_config_flow.py index 4b4daab088d79..a30441c24ff63 100644 --- a/tests/components/roon/test_config_flow.py +++ b/tests/components/roon/test_config_flow.py @@ -71,8 +71,6 @@ async def test_successful_discovery_and_auth(hass): ), patch( "homeassistant.components.roon.config_flow.RoonDiscovery", return_value=RoonDiscoveryMock(), - ), patch( - "homeassistant.components.roon.async_setup", return_value=True ), patch( "homeassistant.components.roon.async_setup_entry", return_value=True, @@ -111,8 +109,6 @@ async def test_unsuccessful_discovery_user_form_and_auth(hass): ), patch( "homeassistant.components.roon.config_flow.RoonDiscovery", return_value=RoonDiscoveryFailedMock(), - ), patch( - "homeassistant.components.roon.async_setup", return_value=True ), patch( "homeassistant.components.roon.async_setup_entry", return_value=True, @@ -160,8 +156,6 @@ async def test_successful_discovery_no_auth(hass): ), patch( "homeassistant.components.roon.config_flow.AUTHENTICATE_TIMEOUT", 0.01, - ), patch( - "homeassistant.components.roon.async_setup", return_value=True ), patch( "homeassistant.components.roon.async_setup_entry", return_value=True, @@ -195,8 +189,6 @@ async def test_unexpected_exception(hass): ), patch( "homeassistant.components.roon.config_flow.RoonDiscovery", return_value=RoonDiscoveryMock(), - ), patch( - "homeassistant.components.roon.async_setup", return_value=True ), patch( "homeassistant.components.roon.async_setup_entry", return_value=True, diff --git a/tests/components/sharkiq/test_config_flow.py b/tests/components/sharkiq/test_config_flow.py index 890efbf16797a..d291d9f1bd11d 100644 --- a/tests/components/sharkiq/test_config_flow.py +++ b/tests/components/sharkiq/test_config_flow.py @@ -24,8 +24,6 @@ async def test_form(hass): assert result["errors"] == {} with patch("sharkiqpy.AylaApi.async_sign_in", return_value=True), patch( - "homeassistant.components.sharkiq.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.sharkiq.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -41,7 +39,6 @@ async def test_form(hass): "password": TEST_PASSWORD, } await hass.async_block_till_done() - mock_setup.assert_called_once() mock_setup_entry.assert_called_once() diff --git a/tests/components/srp_energy/test_config_flow.py b/tests/components/srp_energy/test_config_flow.py index c63843723b1a6..5295d8cdb13b2 100644 --- a/tests/components/srp_energy/test_config_flow.py +++ b/tests/components/srp_energy/test_config_flow.py @@ -21,8 +21,6 @@ async def test_form(hass): with patch( "homeassistant.components.srp_energy.config_flow.SrpEnergyClient" ), patch( - "homeassistant.components.srp_energy.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.srp_energy.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -36,7 +34,6 @@ async def test_form(hass): assert result["title"] == "Test" assert result["data"][CONF_IS_TOU] is False - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/subaru/test_config_flow.py b/tests/components/subaru/test_config_flow.py index 0218c11003c5b..35e254fe3026e 100644 --- a/tests/components/subaru/test_config_flow.py +++ b/tests/components/subaru/test_config_flow.py @@ -27,7 +27,6 @@ from tests.common import MockConfigEntry -ASYNC_SETUP = "homeassistant.components.subaru.async_setup" ASYNC_SETUP_ENTRY = "homeassistant.components.subaru.async_setup_entry" @@ -96,8 +95,6 @@ async def test_user_form_pin_not_required(hass, user_form): MOCK_API_IS_PIN_REQUIRED, return_value=False, ) as mock_is_pin_required, patch( - ASYNC_SETUP, return_value=True - ) as mock_setup, patch( ASYNC_SETUP_ENTRY, return_value=True ) as mock_setup_entry: result = await hass.config_entries.flow.async_configure( @@ -106,7 +103,6 @@ async def test_user_form_pin_not_required(hass, user_form): ) assert len(mock_connect.mock_calls) == 1 assert len(mock_is_pin_required.mock_calls) == 1 - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 expected = { @@ -160,8 +156,6 @@ async def test_pin_form_success(hass, pin_form): MOCK_API_UPDATE_SAVED_PIN, return_value=True, ) as mock_update_saved_pin, patch( - ASYNC_SETUP, return_value=True - ) as mock_setup, patch( ASYNC_SETUP_ENTRY, return_value=True ) as mock_setup_entry: result = await hass.config_entries.flow.async_configure( @@ -170,7 +164,6 @@ async def test_pin_form_success(hass, pin_form): assert len(mock_test_pin.mock_calls) == 1 assert len(mock_update_saved_pin.mock_calls) == 1 - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 expected = { "title": TEST_USERNAME, From af80ca6795e590f4c5458b7e7edea5051bc48d38 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 16 Apr 2021 18:22:56 +0200 Subject: [PATCH 0309/1317] Clean up superfluous integration setup - part 5 (#49296) --- homeassistant/components/epson/__init__.py | 7 +----- .../components/faa_delays/__init__.py | 7 +----- homeassistant/components/flume/__init__.py | 7 +----- .../components/hvv_departures/__init__.py | 5 ---- .../components/keenetic_ndms2/__init__.py | 10 ++------ homeassistant/components/mill/__init__.py | 5 ---- homeassistant/components/mullvad/__init__.py | 5 ---- .../components/nightscout/__init__.py | 7 +----- homeassistant/components/nut/__init__.py | 8 +------ homeassistant/components/nws/__init__.py | 5 ---- .../components/omnilogic/__init__.py | 8 +------ .../components/ondilo_ico/__init__.py | 8 +------ homeassistant/components/ozw/__init__.py | 7 +----- homeassistant/components/roku/__init__.py | 7 +----- homeassistant/components/sentry/__init__.py | 5 ---- homeassistant/components/smhi/__init__.py | 8 +------ homeassistant/components/solarlog/__init__.py | 5 ---- homeassistant/components/syncthru/__init__.py | 9 ++----- .../components/totalconnect/__init__.py | 8 +------ homeassistant/components/wilight/__init__.py | 9 +------ tests/components/epson/test_config_flow.py | 5 +--- .../components/faa_delays/test_config_flow.py | 3 --- tests/components/flume/test_config_flow.py | 6 ----- .../hvv_departures/test_config_flow.py | 4 ---- .../keenetic_ndms2/test_config_flow.py | 9 ------- tests/components/mullvad/test_config_flow.py | 3 --- .../components/nightscout/test_config_flow.py | 7 +----- tests/components/nut/test_config_flow.py | 12 ---------- tests/components/nws/test_config_flow.py | 9 ------- .../components/omnilogic/test_config_flow.py | 3 --- tests/components/ozw/test_config_flow.py | 24 ------------------- tests/components/roku/test_config_flow.py | 9 ------- tests/components/sentry/test_config_flow.py | 3 --- tests/components/smhi/test_init.py | 10 -------- tests/components/solarlog/test_config_flow.py | 3 --- 35 files changed, 18 insertions(+), 232 deletions(-) diff --git a/homeassistant/components/epson/__init__.py b/homeassistant/components/epson/__init__.py index 51d464dacb5c7..94254f64f88a5 100644 --- a/homeassistant/components/epson/__init__.py +++ b/homeassistant/components/epson/__init__.py @@ -32,12 +32,6 @@ async def validate_projector(hass: HomeAssistant, host, port): return epson_proj -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the epson component.""" - hass.data.setdefault(DOMAIN, {}) - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up epson from a config entry.""" try: @@ -47,6 +41,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): except CannotConnect: _LOGGER.warning("Cannot connect to projector %s", entry.data[CONF_HOST]) return False + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = projector for platform in PLATFORMS: hass.async_create_task( diff --git a/homeassistant/components/faa_delays/__init__.py b/homeassistant/components/faa_delays/__init__.py index 2669105469e06..6db9b6675264e 100644 --- a/homeassistant/components/faa_delays/__init__.py +++ b/homeassistant/components/faa_delays/__init__.py @@ -20,12 +20,6 @@ PLATFORMS = ["binary_sensor"] -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the FAA Delays component.""" - hass.data[DOMAIN] = {} - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up FAA Delays from a config entry.""" code = entry.data[CONF_ID] @@ -33,6 +27,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): coordinator = FAADataUpdateCoordinator(hass, code) await coordinator.async_config_entry_first_refresh() + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator for platform in PLATFORMS: diff --git a/homeassistant/components/flume/__init__.py b/homeassistant/components/flume/__init__.py index fb87588ac8247..9acc575602365 100644 --- a/homeassistant/components/flume/__init__.py +++ b/homeassistant/components/flume/__init__.py @@ -29,12 +29,6 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the flume component.""" - hass.data.setdefault(DOMAIN, {}) - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up flume from a config entry.""" @@ -73,6 +67,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): _LOGGER.error("Invalid credentials for flume: %s", ex) return False + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { FLUME_DEVICES: flume_devices, FLUME_AUTH: flume_auth, diff --git a/homeassistant/components/hvv_departures/__init__.py b/homeassistant/components/hvv_departures/__init__.py index c90e5cb6d9c12..b3eb53bff7a3c 100644 --- a/homeassistant/components/hvv_departures/__init__.py +++ b/homeassistant/components/hvv_departures/__init__.py @@ -14,11 +14,6 @@ PLATFORMS = [DOMAIN_SENSOR, DOMAIN_BINARY_SENSOR] -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the HVV component.""" - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up HVV from a config entry.""" diff --git a/homeassistant/components/keenetic_ndms2/__init__.py b/homeassistant/components/keenetic_ndms2/__init__.py index d0217b2a4f556..6156fb00d0263 100644 --- a/homeassistant/components/keenetic_ndms2/__init__.py +++ b/homeassistant/components/keenetic_ndms2/__init__.py @@ -3,7 +3,7 @@ from homeassistant.components import binary_sensor, device_tracker from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_SCAN_INTERVAL -from homeassistant.core import Config, HomeAssistant +from homeassistant.core import HomeAssistant from .const import ( CONF_CONSIDER_HOME, @@ -23,15 +23,9 @@ PLATFORMS = [device_tracker.DOMAIN, binary_sensor.DOMAIN] -async def async_setup(hass: HomeAssistant, _config: Config) -> bool: - """Set up configured entries.""" - hass.data.setdefault(DOMAIN, {}) - return True - - async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up the component.""" - + hass.data.setdefault(DOMAIN, {}) async_add_defaults(hass, config_entry) router = KeeneticRouter(hass, config_entry) diff --git a/homeassistant/components/mill/__init__.py b/homeassistant/components/mill/__init__.py index 117f2bcb5aa2c..e58a7865e2857 100644 --- a/homeassistant/components/mill/__init__.py +++ b/homeassistant/components/mill/__init__.py @@ -1,11 +1,6 @@ """The mill component.""" -async def async_setup(hass, config): - """Set up the Mill platform.""" - return True - - async def async_setup_entry(hass, entry): """Set up the Mill heater.""" hass.async_create_task( diff --git a/homeassistant/components/mullvad/__init__.py b/homeassistant/components/mullvad/__init__.py index 541c6075cc3bf..325c0603f32bd 100644 --- a/homeassistant/components/mullvad/__init__.py +++ b/homeassistant/components/mullvad/__init__.py @@ -15,11 +15,6 @@ PLATFORMS = ["binary_sensor"] -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Mullvad VPN integration.""" - return True - - async def async_setup_entry(hass: HomeAssistant, entry: dict): """Set up Mullvad VPN integration.""" diff --git a/homeassistant/components/nightscout/__init__.py b/homeassistant/components/nightscout/__init__.py index dfaaf28048e80..dd9405317350a 100644 --- a/homeassistant/components/nightscout/__init__.py +++ b/homeassistant/components/nightscout/__init__.py @@ -19,12 +19,6 @@ _API_TIMEOUT = SLOW_UPDATE_WARNING - 1 -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Nightscout component.""" - hass.data.setdefault(DOMAIN, {}) - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Nightscout from a config entry.""" server_url = entry.data[CONF_URL] @@ -36,6 +30,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): except (ClientError, AsyncIOTimeoutError, OSError) as error: raise ConfigEntryNotReady from error + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = api device_registry = await dr.async_get_registry(hass) diff --git a/homeassistant/components/nut/__init__.py b/homeassistant/components/nut/__init__.py index be86ca5951c31..f526e49c6b832 100644 --- a/homeassistant/components/nut/__init__.py +++ b/homeassistant/components/nut/__init__.py @@ -37,13 +37,6 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Network UPS Tools (NUT) component.""" - hass.data.setdefault(DOMAIN, {}) - - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Network UPS Tools (NUT) from a config entry.""" @@ -90,6 +83,7 @@ async def async_update_data(): if unique_id is None: unique_id = entry.entry_id + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { COORDINATOR: coordinator, PYNUT_DATA: data, diff --git a/homeassistant/components/nws/__init__.py b/homeassistant/components/nws/__init__.py index 9cdf17fa264ac..5724175b4bb13 100644 --- a/homeassistant/components/nws/__init__.py +++ b/homeassistant/components/nws/__init__.py @@ -40,11 +40,6 @@ def base_unique_id(latitude, longitude): return f"{latitude}_{longitude}" -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the National Weather Service (NWS) component.""" - return True - - class NwsDataUpdateCoordinator(DataUpdateCoordinator): """ NWS data update coordinator. diff --git a/homeassistant/components/omnilogic/__init__.py b/homeassistant/components/omnilogic/__init__.py index e5a545e480688..8c5d460e549cc 100644 --- a/homeassistant/components/omnilogic/__init__.py +++ b/homeassistant/components/omnilogic/__init__.py @@ -18,13 +18,6 @@ PLATFORMS = ["sensor"] -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Omnilogic component.""" - hass.data.setdefault(DOMAIN, {}) - - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Omnilogic from a config entry.""" @@ -58,6 +51,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): ) await coordinator.async_config_entry_first_refresh() + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { COORDINATOR: coordinator, OMNI_API: api, diff --git a/homeassistant/components/ondilo_ico/__init__.py b/homeassistant/components/ondilo_ico/__init__.py index 4dac83815ba8e..0975802b9b251 100644 --- a/homeassistant/components/ondilo_ico/__init__.py +++ b/homeassistant/components/ondilo_ico/__init__.py @@ -12,13 +12,6 @@ PLATFORMS = ["sensor"] -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Ondilo ICO component.""" - hass.data.setdefault(DOMAIN, {}) - - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Ondilo ICO from a config entry.""" @@ -33,6 +26,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) ) + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = api.OndiloClient(hass, entry, implementation) for platform in PLATFORMS: diff --git a/homeassistant/components/ozw/__init__.py b/homeassistant/components/ozw/__init__.py index ace71e4af81c2..c484eb4e0c0af 100644 --- a/homeassistant/components/ozw/__init__.py +++ b/homeassistant/components/ozw/__init__.py @@ -56,14 +56,9 @@ DATA_STOP_MQTT_CLIENT = "ozw_stop_mqtt_client" -async def async_setup(hass: HomeAssistant, config: dict): - """Initialize basic config of ozw component.""" - hass.data[DOMAIN] = {} - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up ozw from a config entry.""" + hass.data.setdefault(DOMAIN, {}) ozw_data = hass.data[DOMAIN][entry.entry_id] = {} ozw_data[DATA_UNSUBSCRIBE] = [] diff --git a/homeassistant/components/roku/__init__.py b/homeassistant/components/roku/__init__.py index f8294c878dde2..3a12de51b06d2 100644 --- a/homeassistant/components/roku/__init__.py +++ b/homeassistant/components/roku/__init__.py @@ -39,14 +39,9 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistantType, config: dict) -> bool: - """Set up the Roku integration.""" - hass.data.setdefault(DOMAIN, {}) - return True - - async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: """Set up Roku from a config entry.""" + hass.data.setdefault(DOMAIN, {}) coordinator = hass.data[DOMAIN].get(entry.entry_id) if not coordinator: coordinator = RokuDataUpdateCoordinator(hass, host=entry.data[CONF_HOST]) diff --git a/homeassistant/components/sentry/__init__.py b/homeassistant/components/sentry/__init__.py index c58d7bcd1a8e2..8a87cba84d74e 100644 --- a/homeassistant/components/sentry/__init__.py +++ b/homeassistant/components/sentry/__init__.py @@ -40,11 +40,6 @@ LOGGER_INFO_REGEX = re.compile(r"^(\w+)\.?(\w+)?\.?(\w+)?\.?(\w+)?(?:\..*)?$") -async def async_setup(hass: HomeAssistant, config: dict) -> bool: - """Set up the Sentry component.""" - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Sentry from a config entry.""" diff --git a/homeassistant/components/smhi/__init__.py b/homeassistant/components/smhi/__init__.py index 6ede2e6b0ed96..84151bd35eea5 100644 --- a/homeassistant/components/smhi/__init__.py +++ b/homeassistant/components/smhi/__init__.py @@ -1,6 +1,6 @@ """Support for the Swedish weather institute weather service.""" from homeassistant.config_entries import ConfigEntry -from homeassistant.core import Config, HomeAssistant +from homeassistant.core import HomeAssistant # Have to import for config_flow to work even if they are not used here from .config_flow import smhi_locations # noqa: F401 @@ -9,12 +9,6 @@ DEFAULT_NAME = "smhi" -async def async_setup(hass: HomeAssistant, config: Config) -> bool: - """Set up configured SMHI.""" - # We allow setup only through config flow type of config - return True - - async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up SMHI forecast as config entry.""" hass.async_create_task( diff --git a/homeassistant/components/solarlog/__init__.py b/homeassistant/components/solarlog/__init__.py index c8035e1f7e62d..51aa21eb3151c 100644 --- a/homeassistant/components/solarlog/__init__.py +++ b/homeassistant/components/solarlog/__init__.py @@ -3,11 +3,6 @@ from homeassistant.helpers.typing import HomeAssistantType -async def async_setup(hass, config): - """Component setup, do nothing.""" - return True - - async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): """Set up a config entry for solarlog.""" hass.async_create_task( diff --git a/homeassistant/components/syncthru/__init__.py b/homeassistant/components/syncthru/__init__.py index 293680151ffc0..888dd22c0908e 100644 --- a/homeassistant/components/syncthru/__init__.py +++ b/homeassistant/components/syncthru/__init__.py @@ -10,23 +10,18 @@ from homeassistant.const import CONF_URL from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, device_registry as dr -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import HomeAssistantType from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: - """Set up.""" - hass.data.setdefault(DOMAIN, {}) - return True - - async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: """Set up config entry.""" session = aiohttp_client.async_get_clientsession(hass) + hass.data.setdefault(DOMAIN, {}) printer = hass.data[DOMAIN][entry.entry_id] = SyncThru( entry.data[CONF_URL], session ) diff --git a/homeassistant/components/totalconnect/__init__.py b/homeassistant/components/totalconnect/__init__.py index 4078655f075f3..db0fa1e5755b0 100644 --- a/homeassistant/components/totalconnect/__init__.py +++ b/homeassistant/components/totalconnect/__init__.py @@ -33,13 +33,6 @@ ) -async def async_setup(hass: HomeAssistant, config: dict): - """Set up by configuration file.""" - hass.data.setdefault(DOMAIN, {}) - - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up upon config entry in user interface.""" conf = entry.data @@ -62,6 +55,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): if not client.is_valid_credentials(): raise ConfigEntryAuthFailed("TotalConnect authentication failed") + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = client for platform in PLATFORMS: diff --git a/homeassistant/components/wilight/__init__.py b/homeassistant/components/wilight/__init__.py index 3e14ea20b0cdc..88589f1ed706f 100644 --- a/homeassistant/components/wilight/__init__.py +++ b/homeassistant/components/wilight/__init__.py @@ -14,14 +14,6 @@ PLATFORMS = ["cover", "fan", "light"] -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the WiLight with Config Flow component.""" - - hass.data[DOMAIN] = {} - - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up a wilight config entry.""" @@ -30,6 +22,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): if not await parent.async_setup(): raise ConfigEntryNotReady + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = parent # Set up all platforms for this device/entry. diff --git a/tests/components/epson/test_config_flow.py b/tests/components/epson/test_config_flow.py index 4a0b9f9675fb1..849a88ba112b0 100644 --- a/tests/components/epson/test_config_flow.py +++ b/tests/components/epson/test_config_flow.py @@ -20,8 +20,6 @@ async def test_form(hass): "homeassistant.components.epson.Projector.get_property", return_value="04", ), patch( - "homeassistant.components.epson.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.epson.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -33,7 +31,6 @@ async def test_form(hass): assert result2["title"] == "test-epson" assert result2["data"] == {CONF_HOST: "1.1.1.1", CONF_PORT: 80} await hass.async_block_till_done() - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -61,7 +58,7 @@ async def test_import(hass): with patch( "homeassistant.components.epson.Projector.get_property", return_value="04", - ), patch("homeassistant.components.epson.async_setup", return_value=True), patch( + ), patch( "homeassistant.components.epson.async_setup_entry", return_value=True, ): diff --git a/tests/components/faa_delays/test_config_flow.py b/tests/components/faa_delays/test_config_flow.py index c289f1544153f..df79c3953c33d 100644 --- a/tests/components/faa_delays/test_config_flow.py +++ b/tests/components/faa_delays/test_config_flow.py @@ -27,8 +27,6 @@ async def test_form(hass): assert result["errors"] == {} with patch.object(faadelays.Airport, "update", new=mock_valid_airport), patch( - "homeassistant.components.faa_delays.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.faa_delays.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -45,7 +43,6 @@ async def test_form(hass): "id": "test", } await hass.async_block_till_done() - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/flume/test_config_flow.py b/tests/components/flume/test_config_flow.py index 9ae0889d52ce3..3a9e3376f056e 100644 --- a/tests/components/flume/test_config_flow.py +++ b/tests/components/flume/test_config_flow.py @@ -37,8 +37,6 @@ async def test_form(hass): "homeassistant.components.flume.config_flow.FlumeDeviceList", return_value=mock_flume_device_list, ), patch( - "homeassistant.components.flume.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.flume.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -61,7 +59,6 @@ async def test_form(hass): CONF_CLIENT_ID: "client_id", CONF_CLIENT_SECRET: "client_secret", } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -77,8 +74,6 @@ async def test_form_import(hass): "homeassistant.components.flume.config_flow.FlumeDeviceList", return_value=mock_flume_device_list, ), patch( - "homeassistant.components.flume.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.flume.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -102,7 +97,6 @@ async def test_form_import(hass): CONF_CLIENT_ID: "client_id", CONF_CLIENT_SECRET: "client_secret", } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/hvv_departures/test_config_flow.py b/tests/components/hvv_departures/test_config_flow.py index a6a927afd2e5c..3773dbb5967b3 100644 --- a/tests/components/hvv_departures/test_config_flow.py +++ b/tests/components/hvv_departures/test_config_flow.py @@ -38,8 +38,6 @@ async def test_user_flow(hass): ), patch( "homeassistant.components.hvv_departures.hub.GTI.stationInformation", return_value=FIXTURE_STATION_INFORMATION, - ), patch( - "homeassistant.components.hvv_departures.async_setup", return_value=True ), patch( "homeassistant.components.hvv_departures.async_setup_entry", return_value=True, @@ -101,8 +99,6 @@ async def test_user_flow_no_results(hass): ), patch( "homeassistant.components.hvv_departures.hub.GTI.checkName", return_value={"returnCode": "OK", "results": []}, - ), patch( - "homeassistant.components.hvv_departures.async_setup", return_value=True ), patch( "homeassistant.components.hvv_departures.async_setup_entry", return_value=True, diff --git a/tests/components/keenetic_ndms2/test_config_flow.py b/tests/components/keenetic_ndms2/test_config_flow.py index aa5369fdc0a36..ae22ea31ffb37 100644 --- a/tests/components/keenetic_ndms2/test_config_flow.py +++ b/tests/components/keenetic_ndms2/test_config_flow.py @@ -53,8 +53,6 @@ async def test_flow_works(hass: HomeAssistantType, connect): assert result["step_id"] == "user" with patch( - "homeassistant.components.keenetic_ndms2.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.keenetic_ndms2.async_setup_entry", return_value=True ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( @@ -66,7 +64,6 @@ async def test_flow_works(hass: HomeAssistantType, connect): assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result2["title"] == MOCK_NAME assert result2["data"] == MOCK_DATA - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -74,8 +71,6 @@ async def test_import_works(hass: HomeAssistantType, connect): """Test config flow.""" with patch( - "homeassistant.components.keenetic_ndms2.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.keenetic_ndms2.async_setup_entry", return_value=True ) as mock_setup_entry: result = await hass.config_entries.flow.async_init( @@ -88,7 +83,6 @@ async def test_import_works(hass: HomeAssistantType, connect): assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == MOCK_NAME assert result["data"] == MOCK_DATA - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -97,14 +91,11 @@ async def test_options(hass): entry = MockConfigEntry(domain=keenetic.DOMAIN, data=MOCK_DATA) entry.add_to_hass(hass) with patch( - "homeassistant.components.keenetic_ndms2.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.keenetic_ndms2.async_setup_entry", return_value=True ) as mock_setup_entry: await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 # fake router diff --git a/tests/components/mullvad/test_config_flow.py b/tests/components/mullvad/test_config_flow.py index c101e5a724667..e34af4eb83b2f 100644 --- a/tests/components/mullvad/test_config_flow.py +++ b/tests/components/mullvad/test_config_flow.py @@ -20,8 +20,6 @@ async def test_form_user(hass): assert not result["errors"] with patch( - "homeassistant.components.mullvad.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.mullvad.async_setup_entry", return_value=True, ) as mock_setup_entry, patch( @@ -36,7 +34,6 @@ async def test_form_user(hass): assert result2["type"] == "create_entry" assert result2["title"] == "Mullvad VPN" assert result2["data"] == {} - assert len(mock_setup.mock_calls) == 0 assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_mullvad_api.mock_calls) == 1 diff --git a/tests/components/nightscout/test_config_flow.py b/tests/components/nightscout/test_config_flow.py index 9a86e14b4e577..9be3c95ef4236 100644 --- a/tests/components/nightscout/test_config_flow.py +++ b/tests/components/nightscout/test_config_flow.py @@ -27,7 +27,7 @@ async def test_form(hass): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {} - with _patch_glucose_readings(), _patch_server_status(), _patch_async_setup() as mock_setup, _patch_async_setup_entry() as mock_setup_entry: + with _patch_glucose_readings(), _patch_server_status(), _patch_async_setup_entry() as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], CONFIG, @@ -37,7 +37,6 @@ async def test_form(hass): assert result2["title"] == SERVER_STATUS.name # pylint: disable=maybe-no-member assert result2["data"] == CONFIG await hass.async_block_till_done() - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -116,10 +115,6 @@ async def test_user_form_duplicate(hass): assert result["reason"] == "already_configured" -def _patch_async_setup(): - return patch("homeassistant.components.nightscout.async_setup", return_value=True) - - def _patch_async_setup_entry(): return patch( "homeassistant.components.nightscout.async_setup_entry", diff --git a/tests/components/nut/test_config_flow.py b/tests/components/nut/test_config_flow.py index bbe975a67c01e..8543c68c2b848 100644 --- a/tests/components/nut/test_config_flow.py +++ b/tests/components/nut/test_config_flow.py @@ -49,8 +49,6 @@ async def test_form_zeroconf(hass): "homeassistant.components.nut.PyNUTClient", return_value=mock_pynut, ), patch( - "homeassistant.components.nut.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.nut.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -70,7 +68,6 @@ async def test_form_zeroconf(hass): "username": "test-username", } assert result3["result"].unique_id is None - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -108,8 +105,6 @@ async def test_form_user_one_ups(hass): "homeassistant.components.nut.PyNUTClient", return_value=mock_pynut, ), patch( - "homeassistant.components.nut.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.nut.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -128,7 +123,6 @@ async def test_form_user_one_ups(hass): "resources": ["battery.voltage", "ups.status", "ups.status.display"], "username": "test-username", } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -187,8 +181,6 @@ async def test_form_user_multiple_ups(hass): "homeassistant.components.nut.PyNUTClient", return_value=mock_pynut, ), patch( - "homeassistant.components.nut.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.nut.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -208,7 +200,6 @@ async def test_form_user_multiple_ups(hass): "resources": ["battery.voltage"], "username": "test-username", } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 2 @@ -251,8 +242,6 @@ async def test_form_user_one_ups_with_ignored_entry(hass): "homeassistant.components.nut.PyNUTClient", return_value=mock_pynut, ), patch( - "homeassistant.components.nut.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.nut.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -271,7 +260,6 @@ async def test_form_user_one_ups_with_ignored_entry(hass): "resources": ["battery.voltage", "ups.status", "ups.status.display"], "username": "test-username", } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/nws/test_config_flow.py b/tests/components/nws/test_config_flow.py index 81be7360e87c8..6945cb380d346 100644 --- a/tests/components/nws/test_config_flow.py +++ b/tests/components/nws/test_config_flow.py @@ -20,8 +20,6 @@ async def test_form(hass, mock_simple_nws_config): assert result["errors"] == {} with patch( - "homeassistant.components.nws.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.nws.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -38,7 +36,6 @@ async def test_form(hass, mock_simple_nws_config): "longitude": -90, "station": "ABC", } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -85,8 +82,6 @@ async def test_form_already_configured(hass, mock_simple_nws_config): ) with patch( - "homeassistant.components.nws.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.nws.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -97,7 +92,6 @@ async def test_form_already_configured(hass, mock_simple_nws_config): await hass.async_block_till_done() assert result2["type"] == "create_entry" - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 result = await hass.config_entries.flow.async_init( @@ -105,8 +99,6 @@ async def test_form_already_configured(hass, mock_simple_nws_config): ) with patch( - "homeassistant.components.nws.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.nws.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -117,5 +109,4 @@ async def test_form_already_configured(hass, mock_simple_nws_config): assert result2["type"] == "abort" assert result2["reason"] == "already_configured" await hass.async_block_till_done() - assert len(mock_setup.mock_calls) == 0 assert len(mock_setup_entry.mock_calls) == 0 diff --git a/tests/components/omnilogic/test_config_flow.py b/tests/components/omnilogic/test_config_flow.py index acf2df8861081..eda83b454556a 100644 --- a/tests/components/omnilogic/test_config_flow.py +++ b/tests/components/omnilogic/test_config_flow.py @@ -24,8 +24,6 @@ async def test_form(hass): "homeassistant.components.omnilogic.config_flow.OmniLogic.connect", return_value=True, ), patch( - "homeassistant.components.omnilogic.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.omnilogic.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -38,7 +36,6 @@ async def test_form(hass): assert result2["type"] == "create_entry" assert result2["title"] == "Omnilogic" assert result2["data"] == DATA - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/ozw/test_config_flow.py b/tests/components/ozw/test_config_flow.py index 0a746398cf9f1..6fdc86f710ecd 100644 --- a/tests/components/ozw/test_config_flow.py +++ b/tests/components/ozw/test_config_flow.py @@ -84,8 +84,6 @@ async def test_user_not_supervisor_create_entry(hass, mqtt): await setup.async_setup_component(hass, "persistent_notification", {}) with patch( - "homeassistant.components.ozw.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.ozw.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -102,7 +100,6 @@ async def test_user_not_supervisor_create_entry(hass, mqtt): "use_addon": False, "integration_created_addon": False, } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -136,8 +133,6 @@ async def test_not_addon(hass, supervisor, mqtt): ) with patch( - "homeassistant.components.ozw.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.ozw.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -154,7 +149,6 @@ async def test_not_addon(hass, supervisor, mqtt): "use_addon": False, "integration_created_addon": False, } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -169,8 +163,6 @@ async def test_addon_running(hass, supervisor, addon_running, addon_options): ) with patch( - "homeassistant.components.ozw.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.ozw.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -187,7 +179,6 @@ async def test_addon_running(hass, supervisor, addon_running, addon_options): "use_addon": True, "integration_created_addon": False, } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -222,8 +213,6 @@ async def test_addon_installed( ) with patch( - "homeassistant.components.ozw.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.ozw.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -240,7 +229,6 @@ async def test_addon_installed( "use_addon": True, "integration_created_addon": False, } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -319,8 +307,6 @@ async def test_addon_not_installed( assert result["step_id"] == "start_addon" with patch( - "homeassistant.components.ozw.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.ozw.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -337,7 +323,6 @@ async def test_addon_not_installed( "use_addon": True, "integration_created_addon": True, } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -379,8 +364,6 @@ async def test_supervisor_discovery(hass, supervisor, addon_running, addon_optio ) with patch( - "homeassistant.components.ozw.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.ozw.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -395,7 +378,6 @@ async def test_supervisor_discovery(hass, supervisor, addon_running, addon_optio "use_addon": True, "integration_created_addon": False, } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -421,8 +403,6 @@ async def test_clean_discovery_on_user_create( ) with patch( - "homeassistant.components.ozw.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.ozw.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -440,7 +420,6 @@ async def test_clean_discovery_on_user_create( "use_addon": False, "integration_created_addon": False, } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -562,8 +541,6 @@ async def test_import_addon_installed( default_input = result["data_schema"]({}) with patch( - "homeassistant.components.ozw.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.ozw.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -580,5 +557,4 @@ async def test_import_addon_installed( "use_addon": True, "integration_created_addon": False, } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/roku/test_config_flow.py b/tests/components/roku/test_config_flow.py index ed1b042e328a2..ab0072377cd61 100644 --- a/tests/components/roku/test_config_flow.py +++ b/tests/components/roku/test_config_flow.py @@ -72,8 +72,6 @@ async def test_form( user_input = {CONF_HOST: HOST} with patch( - "homeassistant.components.roku.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.roku.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -88,7 +86,6 @@ async def test_form( assert result["data"] assert result["data"][CONF_HOST] == HOST - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -188,8 +185,6 @@ async def test_homekit_discovery( assert result["description_placeholders"] == {CONF_NAME: NAME_ROKUTV} with patch( - "homeassistant.components.roku.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.roku.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -205,7 +200,6 @@ async def test_homekit_discovery( assert result["data"][CONF_HOST] == HOMEKIT_HOST assert result["data"][CONF_NAME] == NAME_ROKUTV - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 # test abort on existing host @@ -270,8 +264,6 @@ async def test_ssdp_discovery( assert result["description_placeholders"] == {CONF_NAME: UPNP_FRIENDLY_NAME} with patch( - "homeassistant.components.roku.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.roku.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -287,5 +279,4 @@ async def test_ssdp_discovery( assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_NAME] == UPNP_FRIENDLY_NAME - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/sentry/test_config_flow.py b/tests/components/sentry/test_config_flow.py index 82a1a70ec8b5f..259d8c65e1637 100644 --- a/tests/components/sentry/test_config_flow.py +++ b/tests/components/sentry/test_config_flow.py @@ -32,8 +32,6 @@ async def test_full_user_flow_implementation(hass): assert result["errors"] == {} with patch("homeassistant.components.sentry.config_flow.Dsn"), patch( - "homeassistant.components.sentry.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.sentry.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -49,7 +47,6 @@ async def test_full_user_flow_implementation(hass): } await hass.async_block_till_done() - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/smhi/test_init.py b/tests/components/smhi/test_init.py index 450ac7e6ef0d5..297a6f587d84d 100644 --- a/tests/components/smhi/test_init.py +++ b/tests/components/smhi/test_init.py @@ -14,16 +14,6 @@ } -async def test_setup_always_return_true() -> None: - """Test async_setup always returns True.""" - hass = Mock() - # Returns true with empty config - assert await smhi.async_setup(hass, {}) is True - - # Returns true with a config provided - assert await smhi.async_setup(hass, TEST_CONFIG) is True - - async def test_forward_async_setup_entry() -> None: """Test that it will forward setup entry.""" hass = Mock() diff --git a/tests/components/solarlog/test_config_flow.py b/tests/components/solarlog/test_config_flow.py index 3016a73f1b8c4..d3ba3c3c84a5e 100644 --- a/tests/components/solarlog/test_config_flow.py +++ b/tests/components/solarlog/test_config_flow.py @@ -27,8 +27,6 @@ async def test_form(hass): "homeassistant.components.solarlog.config_flow.SolarLogConfigFlow._test_connection", return_value={"title": "solarlog test 1 2 3"}, ), patch( - "homeassistant.components.solarlog.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.solarlog.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -40,7 +38,6 @@ async def test_form(hass): assert result2["type"] == "create_entry" assert result2["title"] == "solarlog_test_1_2_3" assert result2["data"] == {"host": "http://1.1.1.1"} - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 From 7264c952175709e6b0a8d281206fc344a3764828 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 16 Apr 2021 18:23:27 +0200 Subject: [PATCH 0310/1317] Clean up superfluous integration setup - part 6 (#49298) --- homeassistant/components/flick_electric/__init__.py | 7 +------ homeassistant/components/flo/__init__.py | 7 +------ homeassistant/components/harmony/__init__.py | 8 +------- .../components/hunterdouglas_powerview/__init__.py | 7 +------ homeassistant/components/ipp/__init__.py | 8 +------- homeassistant/components/litterrobot/__init__.py | 8 +------- homeassistant/components/mazda/__init__.py | 7 +------ homeassistant/components/metoffice/__init__.py | 5 ----- .../components/minecraft_server/__init__.py | 5 ----- homeassistant/components/monoprice/__init__.py | 5 ----- homeassistant/components/nexia/__init__.py | 9 +-------- homeassistant/components/nuheat/__init__.py | 7 +------ homeassistant/components/rpi_power/__init__.py | 5 ----- .../components/ruckus_unleashed/__init__.py | 7 +------ .../components/smart_meter_texas/__init__.py | 7 +------ homeassistant/components/smarttub/__init__.py | 9 +-------- homeassistant/components/sonarr/__init__.py | 7 +------ homeassistant/components/tado/__init__.py | 9 +-------- homeassistant/components/vilfo/__init__.py | 8 +------- homeassistant/components/xiaomi_miio/__init__.py | 5 ----- tests/components/flick_electric/test_config_flow.py | 3 --- tests/components/flo/test_config_flow.py | 3 --- tests/components/harmony/test_config_flow.py | 6 ------ .../hunterdouglas_powerview/test_config_flow.py | 8 -------- tests/components/ipp/test_config_flow.py | 12 +++--------- tests/components/litterrobot/test_config_flow.py | 3 --- tests/components/mazda/test_config_flow.py | 3 --- tests/components/metoffice/test_config_flow.py | 3 --- tests/components/monoprice/test_config_flow.py | 3 --- tests/components/nexia/test_config_flow.py | 3 --- tests/components/nuheat/test_config_flow.py | 3 --- .../components/ruckus_unleashed/test_config_flow.py | 3 --- .../components/smart_meter_texas/test_config_flow.py | 3 --- tests/components/smarttub/test_config_flow.py | 3 --- tests/components/sonarr/__init__.py | 7 ------- tests/components/sonarr/test_config_flow.py | 9 ++++----- tests/components/tado/test_config_flow.py | 3 --- tests/components/vilfo/test_config_flow.py | 3 --- 38 files changed, 22 insertions(+), 199 deletions(-) diff --git a/homeassistant/components/flick_electric/__init__.py b/homeassistant/components/flick_electric/__init__.py index 86af47a88bbf6..04d7b88f52b59 100644 --- a/homeassistant/components/flick_electric/__init__.py +++ b/homeassistant/components/flick_electric/__init__.py @@ -22,16 +22,11 @@ CONF_ID_TOKEN = "id_token" -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Flick Electric component.""" - hass.data[DOMAIN] = {} - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Flick Electric from a config entry.""" auth = HassFlickAuth(hass, entry) + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = FlickAPI(auth) hass.async_create_task( diff --git a/homeassistant/components/flo/__init__.py b/homeassistant/components/flo/__init__.py index 71f8a8bfe5c42..4ea6d1dec9342 100644 --- a/homeassistant/components/flo/__init__.py +++ b/homeassistant/components/flo/__init__.py @@ -19,15 +19,10 @@ PLATFORMS = ["binary_sensor", "sensor", "switch"] -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the flo component.""" - hass.data[DOMAIN] = {} - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up flo from a config entry.""" session = async_get_clientsession(hass) + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = {} try: hass.data[DOMAIN][entry.entry_id][CLIENT] = client = await async_get_api( diff --git a/homeassistant/components/harmony/__init__.py b/homeassistant/components/harmony/__init__.py index c4056044ca074..c273d08758092 100644 --- a/homeassistant/components/harmony/__init__.py +++ b/homeassistant/components/harmony/__init__.py @@ -16,13 +16,6 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Logitech Harmony Hub component.""" - hass.data.setdefault(DOMAIN, {}) - - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Logitech Harmony Hub from a config entry.""" # As there currently is no way to import options from yaml @@ -42,6 +35,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): if not connected_ok: raise ConfigEntryNotReady + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = data await _migrate_old_unique_ids(hass, entry.entry_id, data) diff --git a/homeassistant/components/hunterdouglas_powerview/__init__.py b/homeassistant/components/hunterdouglas_powerview/__init__.py index 2a5c5061cae61..2c606dda9f28c 100644 --- a/homeassistant/components/hunterdouglas_powerview/__init__.py +++ b/homeassistant/components/hunterdouglas_powerview/__init__.py @@ -63,12 +63,6 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, hass_config: dict): - """Set up the Hunter Douglas PowerView component.""" - hass.data.setdefault(DOMAIN, {}) - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Hunter Douglas PowerView from a config entry.""" @@ -122,6 +116,7 @@ async def async_update_data(): update_interval=timedelta(seconds=60), ) + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { PV_API: pv_request, PV_ROOM_DATA: room_data, diff --git a/homeassistant/components/ipp/__init__.py b/homeassistant/components/ipp/__init__.py index 86bde4bba6cd3..95a222ecfe4aa 100644 --- a/homeassistant/components/ipp/__init__.py +++ b/homeassistant/components/ipp/__init__.py @@ -40,15 +40,9 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: dict) -> bool: - """Set up the IPP component.""" - hass.data.setdefault(DOMAIN, {}) - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up IPP from a config entry.""" - + hass.data.setdefault(DOMAIN, {}) coordinator = hass.data[DOMAIN].get(entry.entry_id) if not coordinator: # Create IPP instance for this entry diff --git a/homeassistant/components/litterrobot/__init__.py b/homeassistant/components/litterrobot/__init__.py index 6fea013f54ca7..83bf9f785a26c 100644 --- a/homeassistant/components/litterrobot/__init__.py +++ b/homeassistant/components/litterrobot/__init__.py @@ -13,15 +13,9 @@ PLATFORMS = ["sensor", "switch", "vacuum"] -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Litter-Robot component.""" - hass.data.setdefault(DOMAIN, {}) - - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Litter-Robot from a config entry.""" + hass.data.setdefault(DOMAIN, {}) hub = hass.data[DOMAIN][entry.entry_id] = LitterRobotHub(hass, entry.data) try: await hub.login(load_robots=True) diff --git a/homeassistant/components/mazda/__init__.py b/homeassistant/components/mazda/__init__.py index 2f4e0e84f13bd..555cc9f3a001f 100644 --- a/homeassistant/components/mazda/__init__.py +++ b/homeassistant/components/mazda/__init__.py @@ -38,12 +38,6 @@ async def with_timeout(task, timeout_seconds=10): return await task -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Mazda Connected Services component.""" - hass.data[DOMAIN] = {} - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Mazda Connected Services from a config entry.""" email = entry.data[CONF_EMAIL] @@ -111,6 +105,7 @@ async def async_update_data(): update_interval=timedelta(seconds=60), ) + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { DATA_CLIENT: mazda_client, DATA_COORDINATOR: coordinator, diff --git a/homeassistant/components/metoffice/__init__.py b/homeassistant/components/metoffice/__init__.py index 87a5488fe0196..5dfeceb79f890 100644 --- a/homeassistant/components/metoffice/__init__.py +++ b/homeassistant/components/metoffice/__init__.py @@ -23,11 +23,6 @@ PLATFORMS = ["sensor", "weather"] -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Met Office weather component.""" - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up a Met Office entry.""" diff --git a/homeassistant/components/minecraft_server/__init__.py b/homeassistant/components/minecraft_server/__init__.py index f76e8e8467edb..f466988cda4af 100644 --- a/homeassistant/components/minecraft_server/__init__.py +++ b/homeassistant/components/minecraft_server/__init__.py @@ -27,11 +27,6 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: - """Set up the Minecraft Server component.""" - return True - - async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) -> bool: """Set up Minecraft Server from a config entry.""" domain_data = hass.data.setdefault(DOMAIN, {}) diff --git a/homeassistant/components/monoprice/__init__.py b/homeassistant/components/monoprice/__init__.py index adc0b05bab769..61aa8b408cf79 100644 --- a/homeassistant/components/monoprice/__init__.py +++ b/homeassistant/components/monoprice/__init__.py @@ -23,11 +23,6 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Monoprice 6-Zone Amplifier component.""" - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Monoprice 6-Zone Amplifier from a config entry.""" port = entry.data[CONF_PORT] diff --git a/homeassistant/components/nexia/__init__.py b/homeassistant/components/nexia/__init__.py index 4dde208440011..07f6230eb0dab 100644 --- a/homeassistant/components/nexia/__init__.py +++ b/homeassistant/components/nexia/__init__.py @@ -24,14 +24,6 @@ DEFAULT_UPDATE_RATE = 120 -async def async_setup(hass: HomeAssistant, config: dict) -> bool: - """Set up the nexia component from YAML.""" - - hass.data.setdefault(DOMAIN, {}) - - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Configure the base Nexia device for Home Assistant.""" @@ -75,6 +67,7 @@ async def _async_update_data(): update_interval=timedelta(seconds=DEFAULT_UPDATE_RATE), ) + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { NEXIA_DEVICE: nexia_home, UPDATE_COORDINATOR: coordinator, diff --git a/homeassistant/components/nuheat/__init__.py b/homeassistant/components/nuheat/__init__.py index 9fe4764e1afec..c04bfe647206d 100644 --- a/homeassistant/components/nuheat/__init__.py +++ b/homeassistant/components/nuheat/__init__.py @@ -25,12 +25,6 @@ CONFIG_SCHEMA = cv.deprecated(DOMAIN) -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the NuHeat component.""" - hass.data.setdefault(DOMAIN, {}) - return True - - def _get_thermostat(api, serial_number): """Authenticate and create the thermostat object.""" api.authenticate() @@ -78,6 +72,7 @@ async def _async_update_data(): update_interval=timedelta(minutes=5), ) + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = (thermostat, coordinator) for platform in PLATFORMS: diff --git a/homeassistant/components/rpi_power/__init__.py b/homeassistant/components/rpi_power/__init__.py index 993d0b313c0a2..3f9a9d6e74cc1 100644 --- a/homeassistant/components/rpi_power/__init__.py +++ b/homeassistant/components/rpi_power/__init__.py @@ -3,11 +3,6 @@ from homeassistant.core import HomeAssistant -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Raspberry Pi Power Supply Checker component.""" - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Raspberry Pi Power Supply Checker from a config entry.""" hass.async_create_task( diff --git a/homeassistant/components/ruckus_unleashed/__init__.py b/homeassistant/components/ruckus_unleashed/__init__.py index 2eb4f14313139..78d15f24a63f3 100644 --- a/homeassistant/components/ruckus_unleashed/__init__.py +++ b/homeassistant/components/ruckus_unleashed/__init__.py @@ -27,12 +27,6 @@ from .coordinator import RuckusUnleashedDataUpdateCoordinator -async def async_setup(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up the Ruckus Unleashed component.""" - hass.data[DOMAIN] = {} - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Ruckus Unleashed from a config entry.""" try: @@ -64,6 +58,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: sw_version=system_info[API_SYSTEM_OVERVIEW][API_VERSION], ) + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { COORDINATOR: coordinator, UNDO_UPDATE_LISTENERS: [], diff --git a/homeassistant/components/smart_meter_texas/__init__.py b/homeassistant/components/smart_meter_texas/__init__.py index 3180248fcd106..71504fb52aa13 100644 --- a/homeassistant/components/smart_meter_texas/__init__.py +++ b/homeassistant/components/smart_meter_texas/__init__.py @@ -32,12 +32,6 @@ PLATFORMS = ["sensor"] -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Smart Meter Texas component.""" - hass.data.setdefault(DOMAIN, {}) - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Smart Meter Texas from a config entry.""" @@ -76,6 +70,7 @@ async def async_update_data(): ), ) + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { DATA_COORDINATOR: coordinator, DATA_SMART_METER: smart_meter_texas_data, diff --git a/homeassistant/components/smarttub/__init__.py b/homeassistant/components/smarttub/__init__.py index 457af4b7bc0d9..c907bfdeae388 100644 --- a/homeassistant/components/smarttub/__init__.py +++ b/homeassistant/components/smarttub/__init__.py @@ -10,18 +10,11 @@ PLATFORMS = ["binary_sensor", "climate", "light", "sensor", "switch"] -async def async_setup(hass, config): - """Set up smarttub component.""" - - hass.data.setdefault(DOMAIN, {}) - - return True - - async def async_setup_entry(hass, entry): """Set up a smarttub config entry.""" controller = SmartTubController(hass) + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { SMARTTUB_CONTROLLER: controller, } diff --git a/homeassistant/components/sonarr/__init__.py b/homeassistant/components/sonarr/__init__.py index 810539220344a..ad5b0299f3eb5 100644 --- a/homeassistant/components/sonarr/__init__.py +++ b/homeassistant/components/sonarr/__init__.py @@ -41,12 +41,6 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistantType, config: dict) -> bool: - """Set up the Sonarr component.""" - hass.data.setdefault(DOMAIN, {}) - return True - - async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: """Set up Sonarr from a config entry.""" if not entry.options: @@ -81,6 +75,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool undo_listener = entry.add_update_listener(_async_update_listener) + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { DATA_SONARR: sonarr, DATA_UNDO_UPDATE_LISTENER: undo_listener, diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index 094465d38aaab..5a396bedcf2ab 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -39,14 +39,6 @@ CONFIG_SCHEMA = cv.deprecated(DOMAIN) -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Tado component.""" - - hass.data.setdefault(DOMAIN, {}) - - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Tado from a config entry.""" @@ -86,6 +78,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): update_listener = entry.add_update_listener(_async_update_listener) + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { DATA: tadoconnector, UPDATE_TRACK: update_track, diff --git a/homeassistant/components/vilfo/__init__.py b/homeassistant/components/vilfo/__init__.py index ffa628d6db244..16488269da68c 100644 --- a/homeassistant/components/vilfo/__init__.py +++ b/homeassistant/components/vilfo/__init__.py @@ -10,7 +10,6 @@ from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.util import Throttle from .const import ATTR_BOOT_TIME, ATTR_LOAD, DOMAIN, ROUTER_DEFAULT_HOST @@ -22,12 +21,6 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistantType, config: ConfigType): - """Set up the Vilfo Router component.""" - hass.data.setdefault(DOMAIN, {}) - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Vilfo Router from a config entry.""" host = entry.data[CONF_HOST] @@ -40,6 +33,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): if not vilfo_router.available: raise ConfigEntryNotReady + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = vilfo_router for platform in PLATFORMS: diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index f97d4623d6919..fa2dfcb9944a8 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -35,11 +35,6 @@ AIR_MONITOR_PLATFORMS = ["air_quality", "sensor"] -async def async_setup(hass: core.HomeAssistant, config: dict): - """Set up the Xiaomi Miio component.""" - return True - - async def async_setup_entry( hass: core.HomeAssistant, entry: config_entries.ConfigEntry ): diff --git a/tests/components/flick_electric/test_config_flow.py b/tests/components/flick_electric/test_config_flow.py index 1890ea9448a94..580db390afb55 100644 --- a/tests/components/flick_electric/test_config_flow.py +++ b/tests/components/flick_electric/test_config_flow.py @@ -34,8 +34,6 @@ async def test_form(hass): "homeassistant.components.flick_electric.config_flow.SimpleFlickAuth.async_get_access_token", return_value="123456789abcdef", ), patch( - "homeassistant.components.flick_electric.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.flick_electric.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -48,7 +46,6 @@ async def test_form(hass): assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result2["title"] == "Flick Electric: test-username" assert result2["data"] == CONF - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/flo/test_config_flow.py b/tests/components/flo/test_config_flow.py index d26051bbcb24e..3fd68979b050c 100644 --- a/tests/components/flo/test_config_flow.py +++ b/tests/components/flo/test_config_flow.py @@ -20,8 +20,6 @@ async def test_form(hass, aioclient_mock_fixture): assert result["errors"] == {} with patch( - "homeassistant.components.flo.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.flo.async_setup_entry", return_value=True ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( @@ -32,7 +30,6 @@ async def test_form(hass, aioclient_mock_fixture): assert result2["title"] == "Home" assert result2["data"] == {"username": TEST_USER_ID, "password": TEST_PASSWORD} await hass.async_block_till_done() - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/harmony/test_config_flow.py b/tests/components/harmony/test_config_flow.py index 7f65189086814..d81adabb91613 100644 --- a/tests/components/harmony/test_config_flow.py +++ b/tests/components/harmony/test_config_flow.py @@ -31,8 +31,6 @@ async def test_user_form(hass): "homeassistant.components.harmony.util.HarmonyAPI", return_value=harmonyapi, ), patch( - "homeassistant.components.harmony.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.harmony.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -45,7 +43,6 @@ async def test_user_form(hass): assert result2["type"] == "create_entry" assert result2["title"] == "friend" assert result2["data"] == {"host": "1.2.3.4", "name": "friend"} - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -83,8 +80,6 @@ async def test_form_ssdp(hass): "homeassistant.components.harmony.util.HarmonyAPI", return_value=harmonyapi, ), patch( - "homeassistant.components.harmony.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.harmony.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -97,7 +92,6 @@ async def test_form_ssdp(hass): assert result2["type"] == "create_entry" assert result2["title"] == "Harmony Hub" assert result2["data"] == {"host": "192.168.1.12", "name": "Harmony Hub"} - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/hunterdouglas_powerview/test_config_flow.py b/tests/components/hunterdouglas_powerview/test_config_flow.py index b5b9ee84f270b..442cd42f0bcd3 100644 --- a/tests/components/hunterdouglas_powerview/test_config_flow.py +++ b/tests/components/hunterdouglas_powerview/test_config_flow.py @@ -36,9 +36,6 @@ async def test_user_form(hass): "homeassistant.components.hunterdouglas_powerview.UserData", return_value=mock_powerview_userdata, ), patch( - "homeassistant.components.hunterdouglas_powerview.async_setup", - return_value=True, - ) as mock_setup, patch( "homeassistant.components.hunterdouglas_powerview.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -53,7 +50,6 @@ async def test_user_form(hass): assert result2["data"] == { "host": "1.2.3.4", } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 result3 = await hass.config_entries.flow.async_init( @@ -103,9 +99,6 @@ async def test_form_homekit(hass): "homeassistant.components.hunterdouglas_powerview.UserData", return_value=mock_powerview_userdata, ), patch( - "homeassistant.components.hunterdouglas_powerview.async_setup", - return_value=True, - ) as mock_setup, patch( "homeassistant.components.hunterdouglas_powerview.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -117,7 +110,6 @@ async def test_form_homekit(hass): assert result2["data"] == {"host": "1.2.3.4"} assert result2["result"].unique_id == "ABC123" - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 result3 = await hass.config_entries.flow.async_init( diff --git a/tests/components/ipp/test_config_flow.py b/tests/components/ipp/test_config_flow.py index 140570c3c5408..23670ff0d1a0f 100644 --- a/tests/components/ipp/test_config_flow.py +++ b/tests/components/ipp/test_config_flow.py @@ -344,9 +344,7 @@ async def test_full_user_flow_implementation( assert result["step_id"] == "user" assert result["type"] == RESULT_TYPE_FORM - with patch( - "homeassistant.components.ipp.async_setup_entry", return_value=True - ), patch("homeassistant.components.ipp.async_setup", return_value=True): + with patch("homeassistant.components.ipp.async_setup_entry", return_value=True): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: "192.168.1.31", CONF_BASE_PATH: "/ipp/print"}, @@ -379,9 +377,7 @@ async def test_full_zeroconf_flow_implementation( assert result["step_id"] == "zeroconf_confirm" assert result["type"] == RESULT_TYPE_FORM - with patch( - "homeassistant.components.ipp.async_setup_entry", return_value=True - ), patch("homeassistant.components.ipp.async_setup", return_value=True): + with patch("homeassistant.components.ipp.async_setup_entry", return_value=True): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) @@ -416,9 +412,7 @@ async def test_full_zeroconf_tls_flow_implementation( assert result["type"] == RESULT_TYPE_FORM assert result["description_placeholders"] == {CONF_NAME: "EPSON XP-6000 Series"} - with patch( - "homeassistant.components.ipp.async_setup_entry", return_value=True - ), patch("homeassistant.components.ipp.async_setup", return_value=True): + with patch("homeassistant.components.ipp.async_setup_entry", return_value=True): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) diff --git a/tests/components/litterrobot/test_config_flow.py b/tests/components/litterrobot/test_config_flow.py index 33b22b6a1bd44..8d39f7ac9e8ff 100644 --- a/tests/components/litterrobot/test_config_flow.py +++ b/tests/components/litterrobot/test_config_flow.py @@ -24,8 +24,6 @@ async def test_form(hass, mock_account): "homeassistant.components.litterrobot.hub.Account", return_value=mock_account, ), patch( - "homeassistant.components.litterrobot.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.litterrobot.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -37,7 +35,6 @@ async def test_form(hass, mock_account): assert result2["type"] == "create_entry" assert result2["title"] == CONFIG[DOMAIN][CONF_USERNAME] assert result2["data"] == CONFIG[DOMAIN] - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/mazda/test_config_flow.py b/tests/components/mazda/test_config_flow.py index fbdd74bfdfae2..f4bdfa930bdde 100644 --- a/tests/components/mazda/test_config_flow.py +++ b/tests/components/mazda/test_config_flow.py @@ -39,8 +39,6 @@ async def test_form(hass): "homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials", return_value=True, ), patch( - "homeassistant.components.mazda.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.mazda.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -53,7 +51,6 @@ async def test_form(hass): assert result2["type"] == "create_entry" assert result2["title"] == FIXTURE_USER_INPUT[CONF_EMAIL] assert result2["data"] == FIXTURE_USER_INPUT - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/metoffice/test_config_flow.py b/tests/components/metoffice/test_config_flow.py index f0023b0d8d597..8f01f4b964303 100644 --- a/tests/components/metoffice/test_config_flow.py +++ b/tests/components/metoffice/test_config_flow.py @@ -34,8 +34,6 @@ async def test_form(hass, requests_mock): assert result["errors"] == {} with patch( - "homeassistant.components.metoffice.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.metoffice.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -52,7 +50,6 @@ async def test_form(hass, requests_mock): "longitude": TEST_LONGITUDE_WAVERTREE, "name": TEST_SITE_NAME_WAVERTREE, } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/monoprice/test_config_flow.py b/tests/components/monoprice/test_config_flow.py index 0b954cb6e3429..3e133519d1b06 100644 --- a/tests/components/monoprice/test_config_flow.py +++ b/tests/components/monoprice/test_config_flow.py @@ -36,8 +36,6 @@ async def test_form(hass): "homeassistant.components.monoprice.config_flow.get_async_monoprice", return_value=True, ), patch( - "homeassistant.components.monoprice.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.monoprice.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -52,7 +50,6 @@ async def test_form(hass): CONF_PORT: CONFIG[CONF_PORT], CONF_SOURCES: {"1": CONFIG[CONF_SOURCE_1], "4": CONFIG[CONF_SOURCE_4]}, } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/nexia/test_config_flow.py b/tests/components/nexia/test_config_flow.py index 42917e77cfe03..b9726fdd9746d 100644 --- a/tests/components/nexia/test_config_flow.py +++ b/tests/components/nexia/test_config_flow.py @@ -24,8 +24,6 @@ async def test_form(hass): "homeassistant.components.nexia.config_flow.NexiaHome.login", side_effect=MagicMock(), ), patch( - "homeassistant.components.nexia.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.nexia.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -41,7 +39,6 @@ async def test_form(hass): CONF_USERNAME: "username", CONF_PASSWORD: "password", } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/nuheat/test_config_flow.py b/tests/components/nuheat/test_config_flow.py index a21e2e744de04..525ab18726a8e 100644 --- a/tests/components/nuheat/test_config_flow.py +++ b/tests/components/nuheat/test_config_flow.py @@ -28,8 +28,6 @@ async def test_form_user(hass): "homeassistant.components.nuheat.config_flow.nuheat.NuHeat.get_thermostat", return_value=mock_thermostat, ), patch( - "homeassistant.components.nuheat.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.nuheat.async_setup_entry", return_value=True ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( @@ -49,7 +47,6 @@ async def test_form_user(hass): CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password", } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/ruckus_unleashed/test_config_flow.py b/tests/components/ruckus_unleashed/test_config_flow.py index a11943bff0025..9a93dcf78a75a 100644 --- a/tests/components/ruckus_unleashed/test_config_flow.py +++ b/tests/components/ruckus_unleashed/test_config_flow.py @@ -30,8 +30,6 @@ async def test_form(hass): "homeassistant.components.ruckus_unleashed.Ruckus.system_info", return_value=DEFAULT_SYSTEM_INFO, ), patch( - "homeassistant.components.ruckus_unleashed.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.ruckus_unleashed.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -44,7 +42,6 @@ async def test_form(hass): assert result2["type"] == "create_entry" assert result2["title"] == DEFAULT_TITLE assert result2["data"] == CONFIG - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/smart_meter_texas/test_config_flow.py b/tests/components/smart_meter_texas/test_config_flow.py index d0108f2ee09ab..246ae4edc7d1f 100644 --- a/tests/components/smart_meter_texas/test_config_flow.py +++ b/tests/components/smart_meter_texas/test_config_flow.py @@ -28,8 +28,6 @@ async def test_form(hass): assert result["errors"] == {} with patch("smart_meter_texas.Client.authenticate", return_value=True), patch( - "homeassistant.components.smart_meter_texas.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.smart_meter_texas.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -41,7 +39,6 @@ async def test_form(hass): assert result2["type"] == "create_entry" assert result2["title"] == TEST_LOGIN[CONF_USERNAME] assert result2["data"] == TEST_LOGIN - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/smarttub/test_config_flow.py b/tests/components/smarttub/test_config_flow.py index 2608d867c0d6c..8e4d575119e7a 100644 --- a/tests/components/smarttub/test_config_flow.py +++ b/tests/components/smarttub/test_config_flow.py @@ -16,8 +16,6 @@ async def test_form(hass): assert result["errors"] == {} with patch( - "homeassistant.components.smarttub.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.smarttub.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -33,7 +31,6 @@ async def test_form(hass): "password": "test-password", } await hass.async_block_till_done() - mock_setup.assert_called_once() mock_setup_entry.assert_called_once() result = await hass.config_entries.flow.async_init( diff --git a/tests/components/sonarr/__init__.py b/tests/components/sonarr/__init__.py index 1313db4460dd5..c1d4fc30736f6 100644 --- a/tests/components/sonarr/__init__.py +++ b/tests/components/sonarr/__init__.py @@ -227,13 +227,6 @@ async def setup_integration( return entry -def _patch_async_setup(return_value=True): - """Patch the async setup of sonarr.""" - return patch( - "homeassistant.components.sonarr.async_setup", return_value=return_value - ) - - def _patch_async_setup_entry(return_value=True): """Patch the async entry setup of sonarr.""" return patch( diff --git a/tests/components/sonarr/test_config_flow.py b/tests/components/sonarr/test_config_flow.py index 5f32e72aee1f4..71ec142024407 100644 --- a/tests/components/sonarr/test_config_flow.py +++ b/tests/components/sonarr/test_config_flow.py @@ -21,7 +21,6 @@ HOST, MOCK_REAUTH_INPUT, MOCK_USER_INPUT, - _patch_async_setup, _patch_async_setup_entry, mock_connection, mock_connection_error, @@ -123,7 +122,7 @@ async def test_full_reauth_flow_implementation( assert result["step_id"] == "user" user_input = MOCK_REAUTH_INPUT.copy() - with _patch_async_setup(), _patch_async_setup_entry() as mock_setup_entry: + with _patch_async_setup_entry() as mock_setup_entry: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=user_input ) @@ -153,7 +152,7 @@ async def test_full_user_flow_implementation( user_input = MOCK_USER_INPUT.copy() - with _patch_async_setup(), _patch_async_setup_entry(): + with _patch_async_setup_entry(): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=user_input, @@ -184,7 +183,7 @@ async def test_full_user_flow_advanced_options( CONF_VERIFY_SSL: True, } - with _patch_async_setup(), _patch_async_setup_entry(): + with _patch_async_setup_entry(): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=user_input, @@ -211,7 +210,7 @@ async def test_options_flow(hass, aioclient_mock: AiohttpClientMocker): assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "init" - with _patch_async_setup(), _patch_async_setup_entry(): + with _patch_async_setup_entry(): result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_UPCOMING_DAYS: 2, CONF_WANTED_MAX_ITEMS: 100}, diff --git a/tests/components/tado/test_config_flow.py b/tests/components/tado/test_config_flow.py index c3e2bac68cacb..90b7e87504c83 100644 --- a/tests/components/tado/test_config_flow.py +++ b/tests/components/tado/test_config_flow.py @@ -34,8 +34,6 @@ async def test_form(hass): "homeassistant.components.tado.config_flow.Tado", return_value=mock_tado_api, ), patch( - "homeassistant.components.tado.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.tado.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -51,7 +49,6 @@ async def test_form(hass): "username": "test-username", "password": "test-password", } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/vilfo/test_config_flow.py b/tests/components/vilfo/test_config_flow.py index 6e98ef3fdd95c..d828963ba3928 100644 --- a/tests/components/vilfo/test_config_flow.py +++ b/tests/components/vilfo/test_config_flow.py @@ -21,8 +21,6 @@ async def test_form(hass): with patch("vilfo.Client.ping", return_value=None), patch( "vilfo.Client.get_board_information", return_value=None ), patch("vilfo.Client.resolve_mac_address", return_value=mock_mac), patch( - "homeassistant.components.vilfo.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.vilfo.async_setup_entry" ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( @@ -38,7 +36,6 @@ async def test_form(hass): "access_token": "test-token", } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 From e9cf8db302e5714b6e2a88222fecac07aeb069b1 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 16 Apr 2021 18:28:53 +0200 Subject: [PATCH 0311/1317] Add `device_info` property to OpenWeatherMap entities (#49293) --- .../openweathermap/abstract_owm_sensor.py | 21 ++++++++++++++++++- .../components/openweathermap/const.py | 1 + .../components/openweathermap/weather.py | 12 +++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/openweathermap/abstract_owm_sensor.py b/homeassistant/components/openweathermap/abstract_owm_sensor.py index 30a21a057f0fb..ea12123b707cc 100644 --- a/homeassistant/components/openweathermap/abstract_owm_sensor.py +++ b/homeassistant/components/openweathermap/abstract_owm_sensor.py @@ -3,7 +3,15 @@ from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import ATTRIBUTION, SENSOR_DEVICE_CLASS, SENSOR_NAME, SENSOR_UNIT +from .const import ( + ATTRIBUTION, + DEFAULT_NAME, + DOMAIN, + MANUFACTURER, + SENSOR_DEVICE_CLASS, + SENSOR_NAME, + SENSOR_UNIT, +) class AbstractOpenWeatherMapSensor(SensorEntity): @@ -36,6 +44,17 @@ def unique_id(self): """Return a unique_id for this entity.""" return self._unique_id + @property + def device_info(self): + """Return the device info.""" + split_unique_id = self._unique_id.split("-") + return { + "identifiers": {(DOMAIN, f"{split_unique_id[0]}-{split_unique_id[1]}")}, + "name": DEFAULT_NAME, + "manufacturer": MANUFACTURER, + "entry_type": "service", + } + @property def should_poll(self): """Return the polling requirement of the entity.""" diff --git a/homeassistant/components/openweathermap/const.py b/homeassistant/components/openweathermap/const.py index bde7e74159c86..36080a8e6f623 100644 --- a/homeassistant/components/openweathermap/const.py +++ b/homeassistant/components/openweathermap/const.py @@ -42,6 +42,7 @@ DEFAULT_NAME = "OpenWeatherMap" DEFAULT_LANGUAGE = "en" ATTRIBUTION = "Data provided by OpenWeatherMap" +MANUFACTURER = "OpenWeather" CONF_LANGUAGE = "language" CONFIG_FLOW_VERSION = 2 ENTRY_NAME = "name" diff --git a/homeassistant/components/openweathermap/weather.py b/homeassistant/components/openweathermap/weather.py index 63d63c3014716..ffd3e4b726999 100644 --- a/homeassistant/components/openweathermap/weather.py +++ b/homeassistant/components/openweathermap/weather.py @@ -12,9 +12,11 @@ ATTR_API_WIND_BEARING, ATTR_API_WIND_SPEED, ATTRIBUTION, + DEFAULT_NAME, DOMAIN, ENTRY_NAME, ENTRY_WEATHER_COORDINATOR, + MANUFACTURER, ) from .weather_update_coordinator import WeatherUpdateCoordinator @@ -55,6 +57,16 @@ def unique_id(self): """Return a unique_id for this entity.""" return self._unique_id + @property + def device_info(self): + """Return the device info.""" + return { + "identifiers": {(DOMAIN, self._unique_id)}, + "name": DEFAULT_NAME, + "manufacturer": MANUFACTURER, + "entry_type": "service", + } + @property def should_poll(self): """Return the polling requirement of the entity.""" From 65d092f1cc6557fe3d61e6086a8a5dce2a38110a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Fri, 16 Apr 2021 20:17:46 +0200 Subject: [PATCH 0312/1317] Upgrade pyMetno to 0.8.2 (#49308) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Hjelseth Høyer --- homeassistant/components/met/manifest.json | 2 +- homeassistant/components/norway_air/air_quality.py | 2 +- homeassistant/components/norway_air/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/met/manifest.json b/homeassistant/components/met/manifest.json index 950251958098d..d38c44c58809b 100644 --- a/homeassistant/components/met/manifest.json +++ b/homeassistant/components/met/manifest.json @@ -3,7 +3,7 @@ "name": "Meteorologisk institutt (Met.no)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/met", - "requirements": ["pyMetno==0.8.1"], + "requirements": ["pyMetno==0.8.2"], "codeowners": ["@danielhiversen", "@thimic"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/norway_air/air_quality.py b/homeassistant/components/norway_air/air_quality.py index 788f900ef70be..480121846e954 100644 --- a/homeassistant/components/norway_air/air_quality.py +++ b/homeassistant/components/norway_air/air_quality.py @@ -67,7 +67,7 @@ def _decorator(self): class AirSensor(AirQualityEntity): - """Representation of an Yr.no sensor.""" + """Representation of an air quality sensor.""" def __init__(self, name, coordinates, forecast, session): """Initialize the sensor.""" diff --git a/homeassistant/components/norway_air/manifest.json b/homeassistant/components/norway_air/manifest.json index db4415932a5bb..ae213e4f53941 100644 --- a/homeassistant/components/norway_air/manifest.json +++ b/homeassistant/components/norway_air/manifest.json @@ -2,7 +2,7 @@ "domain": "norway_air", "name": "Om Luftkvalitet i Norge (Norway Air)", "documentation": "https://www.home-assistant.io/integrations/norway_air", - "requirements": ["pyMetno==0.8.1"], + "requirements": ["pyMetno==0.8.2"], "codeowners": [], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index c06ed6a03cfc9..1b33a3b8ab559 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1235,7 +1235,7 @@ pyMetEireann==0.2 # homeassistant.components.met # homeassistant.components.norway_air -pyMetno==0.8.1 +pyMetno==0.8.2 # homeassistant.components.rfxtrx pyRFXtrx==0.26.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0419b9b3c5f97..ba5dfba394596 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -666,7 +666,7 @@ pyMetEireann==0.2 # homeassistant.components.met # homeassistant.components.norway_air -pyMetno==0.8.1 +pyMetno==0.8.2 # homeassistant.components.rfxtrx pyRFXtrx==0.26.1 From ea9641f9808e0ab789aa3067831d3a699efdfd49 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 16 Apr 2021 22:33:58 +0200 Subject: [PATCH 0313/1317] Apply Precision/Scale/Offset to struct in modbus sensor (#48544) The single values in struct are corrected with presicion, scale and offset, just as it is done with single values. --- homeassistant/components/modbus/sensor.py | 14 ++++++- tests/components/modbus/test_modbus_sensor.py | 41 +++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index 21069d8642773..dcc68b52db886 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -319,7 +319,19 @@ def _update(self): # If unpack() returns a tuple greater than 1, don't try to process the value. # Instead, return the values of unpack(...) separated by commas. if len(val) > 1: - self._value = ",".join(map(str, val)) + # Apply scale and precision to floats and ints + v_result = [] + for entry in val: + v_temp = self._scale * entry + self._offset + + # We could convert int to float, and the code would still work; however + # we lose some precision, and unit tests will fail. Therefore, we do + # the conversion only when it's absolutely necessary. + if isinstance(v_temp, int) and self._precision == 0: + v_result.append(str(v_temp)) + else: + v_result.append(f"{float(v_temp):.{self._precision}f}") + self._value = ",".join(map(str, v_result)) else: val = val[0] diff --git a/tests/components/modbus/test_modbus_sensor.py b/tests/components/modbus/test_modbus_sensor.py index ce9889d8aaa10..b81cc9c4c1ef1 100644 --- a/tests/components/modbus/test_modbus_sensor.py +++ b/tests/components/modbus/test_modbus_sensor.py @@ -12,6 +12,7 @@ CONF_REGISTERS, CONF_REVERSE_ORDER, CONF_SCALE, + DATA_TYPE_CUSTOM, DATA_TYPE_FLOAT, DATA_TYPE_INT, DATA_TYPE_STRING, @@ -26,6 +27,7 @@ CONF_OFFSET, CONF_SENSORS, CONF_SLAVE, + CONF_STRUCTURE, ) from .conftest import base_config_test, base_test @@ -338,6 +340,7 @@ async def test_config_sensor(hass, do_discovery, do_config): ) async def test_all_sensor(hass, cfg, regs, expected): """Run test for sensor.""" + sensor_name = "modbus_test_sensor" state = await base_test( hass, @@ -352,3 +355,41 @@ async def test_all_sensor(hass, cfg, regs, expected): scan_interval=5, ) assert state == expected + + +async def test_struct_sensor(hass): + """Run test for sensor struct.""" + + sensor_name = "modbus_test_sensor" + # floats: 7.931250095367432, 10.600000381469727, + # 1.000879611487865e-28, 10.566553115844727 + expected = "7.93,10.60,0.00,10.57" + state = await base_test( + hass, + { + CONF_NAME: sensor_name, + CONF_REGISTER: 1234, + CONF_COUNT: 8, + CONF_PRECISION: 2, + CONF_DATA_TYPE: DATA_TYPE_CUSTOM, + CONF_STRUCTURE: ">4f", + }, + sensor_name, + SENSOR_DOMAIN, + CONF_SENSORS, + CONF_REGISTERS, + [ + 0x40FD, + 0xCCCD, + 0x4129, + 0x999A, + 0x10FD, + 0xC0CD, + 0x4129, + 0x109A, + ], + expected, + method_discovery=False, + scan_interval=5, + ) + assert state == expected From 89f2996caa7addb6f184e5e976d28ae3d107574e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 16 Apr 2021 13:36:39 -0700 Subject: [PATCH 0314/1317] Bump frontend to 20210416.0 --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 2a90a867ce3b9..4e8cf0295d908 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20210407.3"], + "requirements": ["home-assistant-frontend==20210416.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 58649ee587f7a..7516c2e9981bb 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ defusedxml==0.6.0 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.43.0 -home-assistant-frontend==20210407.3 +home-assistant-frontend==20210416.0 httpx==0.17.1 jinja2>=2.11.3 netdisco==2.8.2 diff --git a/requirements_all.txt b/requirements_all.txt index 1b33a3b8ab559..28f01b66dd09d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -762,7 +762,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210407.3 +home-assistant-frontend==20210416.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ba5dfba394596..d34400cd1dd63 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -423,7 +423,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210407.3 +home-assistant-frontend==20210416.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From f026768725eeda8f812e0882f49a615590f83870 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 16 Apr 2021 11:04:18 -1000 Subject: [PATCH 0315/1317] Add dhcp discovery to tuya (#49312) Newer tuya devices use their own OUI instead of espressif --- homeassistant/components/tuya/manifest.json | 10 ++++++++- homeassistant/generated/dhcp.py | 24 +++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tuya/manifest.json b/homeassistant/components/tuya/manifest.json index 52b616a0e8319..5dae8e6a10196 100644 --- a/homeassistant/components/tuya/manifest.json +++ b/homeassistant/components/tuya/manifest.json @@ -5,5 +5,13 @@ "requirements": ["tuyaha==0.0.10"], "codeowners": ["@ollo69"], "config_flow": true, - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "dhcp": [ + {"macaddress": "508A06*"}, + {"macaddress": "7CF666*"}, + {"macaddress": "10D561*"}, + {"macaddress": "D4A651*"}, + {"macaddress": "68572D*"}, + {"macaddress": "1869D8*"} + ] } diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 4d4e3688c1ba4..fca0ce9fd692c 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -195,6 +195,30 @@ "hostname": "eneco-*", "macaddress": "74C63B*" }, + { + "domain": "tuya", + "macaddress": "508A06*" + }, + { + "domain": "tuya", + "macaddress": "7CF666*" + }, + { + "domain": "tuya", + "macaddress": "10D561*" + }, + { + "domain": "tuya", + "macaddress": "D4A651*" + }, + { + "domain": "tuya", + "macaddress": "68572D*" + }, + { + "domain": "tuya", + "macaddress": "1869D8*" + }, { "domain": "verisure", "macaddress": "0023C1*" From f4646637321be86eaccfd1d41613a1b5f5361a17 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 16 Apr 2021 14:53:41 -0700 Subject: [PATCH 0316/1317] Add DHCP to MyQ (#49319) --- homeassistant/components/myq/manifest.json | 3 ++- homeassistant/generated/dhcp.py | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/myq/manifest.json b/homeassistant/components/myq/manifest.json index 350ba24c7c0e8..407e5b7df19c9 100644 --- a/homeassistant/components/myq/manifest.json +++ b/homeassistant/components/myq/manifest.json @@ -8,5 +8,6 @@ "homekit": { "models": ["819LMB"] }, - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "dhcp": [{ "macaddress": "645299*" }] } diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index fca0ce9fd692c..4bead617dcb1e 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -87,6 +87,10 @@ "hostname": "lyric-*", "macaddress": "00D02D" }, + { + "domain": "myq", + "macaddress": "645299*" + }, { "domain": "nest", "macaddress": "18B430*" From 984962d985db3d2f491f87a26e87801fab8124aa Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 16 Apr 2021 16:32:12 -0700 Subject: [PATCH 0317/1317] Improve DHCP + Zeroconf manifest validation (#49321) --- homeassistant/components/lyric/manifest.json | 6 +++--- homeassistant/generated/dhcp.py | 6 +++--- script/hassfest/manifest.py | 15 +++++++++++++-- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/lyric/manifest.json b/homeassistant/components/lyric/manifest.json index 71976fa2ac13b..6317c6c3357ee 100644 --- a/homeassistant/components/lyric/manifest.json +++ b/homeassistant/components/lyric/manifest.json @@ -10,15 +10,15 @@ "dhcp": [ { "hostname": "lyric-*", - "macaddress": "48A2E6" + "macaddress": "48A2E6*" }, { "hostname": "lyric-*", - "macaddress": "B82CA0" + "macaddress": "B82CA0*" }, { "hostname": "lyric-*", - "macaddress": "00D02D" + "macaddress": "00D02D*" } ], "iot_class": "cloud_polling" diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 4bead617dcb1e..0fa1777d2a4c4 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -75,17 +75,17 @@ { "domain": "lyric", "hostname": "lyric-*", - "macaddress": "48A2E6" + "macaddress": "48A2E6*" }, { "domain": "lyric", "hostname": "lyric-*", - "macaddress": "B82CA0" + "macaddress": "B82CA0*" }, { "domain": "lyric", "hostname": "lyric-*", - "macaddress": "00D02D" + "macaddress": "00D02D*" }, { "domain": "myq", diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index ac9ab516dd1bd..8b3489facf636 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -148,6 +148,13 @@ def verify_version(value: str): return value +def verify_wildcard(value: str): + """Verify the matcher contains a wildcard.""" + if "*" not in value: + raise vol.Invalid(f"'{value}' needs to contain a wildcard matcher") + return value + + MANIFEST_SCHEMA = vol.Schema( { vol.Required("domain"): str, @@ -160,7 +167,9 @@ def verify_version(value: str): vol.Schema( { vol.Required("type"): str, - vol.Optional("macaddress"): vol.All(str, verify_uppercase), + vol.Optional("macaddress"): vol.All( + str, verify_uppercase, verify_wildcard + ), vol.Optional("manufacturer"): vol.All(str, verify_lowercase), vol.Optional("name"): vol.All(str, verify_lowercase), } @@ -174,7 +183,9 @@ def verify_version(value: str): vol.Optional("dhcp"): [ vol.Schema( { - vol.Optional("macaddress"): vol.All(str, verify_uppercase), + vol.Optional("macaddress"): vol.All( + str, verify_uppercase, verify_wildcard + ), vol.Optional("hostname"): vol.All(str, verify_lowercase), } ) From 343b8faf9b0038e3bf6a05ab6e30de1a87311721 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sat, 17 Apr 2021 00:03:46 +0000 Subject: [PATCH 0318/1317] [ci skip] Translation update --- .../components/adguard/translations/et.json | 1 + .../components/adguard/translations/it.json | 1 + .../components/adguard/translations/nl.json | 1 + .../components/adguard/translations/no.json | 1 + .../components/adguard/translations/ru.json | 1 + .../adguard/translations/zh-Hant.json | 1 + .../coronavirus/translations/et.json | 3 ++- .../coronavirus/translations/it.json | 3 ++- .../coronavirus/translations/nl.json | 3 ++- .../coronavirus/translations/ru.json | 3 ++- .../coronavirus/translations/zh-Hant.json | 3 ++- .../enphase_envoy/translations/it.json | 3 ++- .../enphase_envoy/translations/nl.json | 3 ++- .../enphase_envoy/translations/no.json | 3 ++- .../enphase_envoy/translations/zh-Hant.json | 3 ++- .../components/ialarm/translations/it.json | 20 ++++++++++++++ .../litterrobot/translations/it.json | 2 +- .../components/sma/translations/it.json | 27 +++++++++++++++++++ .../components/sma/translations/zh-Hant.json | 27 +++++++++++++++++++ 19 files changed, 99 insertions(+), 10 deletions(-) create mode 100644 homeassistant/components/ialarm/translations/it.json create mode 100644 homeassistant/components/sma/translations/it.json create mode 100644 homeassistant/components/sma/translations/zh-Hant.json diff --git a/homeassistant/components/adguard/translations/et.json b/homeassistant/components/adguard/translations/et.json index 18e67dedb3658..1e53492510bb1 100644 --- a/homeassistant/components/adguard/translations/et.json +++ b/homeassistant/components/adguard/translations/et.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Teenus on juba seadistatud", "existing_instance_updated": "Olemasolevad seaded v\u00e4rskendatud.", "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine." }, diff --git a/homeassistant/components/adguard/translations/it.json b/homeassistant/components/adguard/translations/it.json index cbafb68a83450..9383de7b853a4 100644 --- a/homeassistant/components/adguard/translations/it.json +++ b/homeassistant/components/adguard/translations/it.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato", "existing_instance_updated": "Configurazione esistente aggiornata.", "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." }, diff --git a/homeassistant/components/adguard/translations/nl.json b/homeassistant/components/adguard/translations/nl.json index a1bfaad6e05a1..3ad3fe741da56 100644 --- a/homeassistant/components/adguard/translations/nl.json +++ b/homeassistant/components/adguard/translations/nl.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Service is al geconfigureerd", "existing_instance_updated": "Bestaande configuratie bijgewerkt.", "single_instance_allowed": "Slechts \u00e9\u00e9n configuratie van AdGuard Home is toegestaan." }, diff --git a/homeassistant/components/adguard/translations/no.json b/homeassistant/components/adguard/translations/no.json index a35bfb181d624..442c5a9e6b4a0 100644 --- a/homeassistant/components/adguard/translations/no.json +++ b/homeassistant/components/adguard/translations/no.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Tjenesten er allerede konfigurert", "existing_instance_updated": "Oppdatert eksisterende konfigurasjon.", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." }, diff --git a/homeassistant/components/adguard/translations/ru.json b/homeassistant/components/adguard/translations/ru.json index 480204da0a185..b2eb34f061faa 100644 --- a/homeassistant/components/adguard/translations/ru.json +++ b/homeassistant/components/adguard/translations/ru.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", "existing_instance_updated": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0430.", "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." }, diff --git a/homeassistant/components/adguard/translations/zh-Hant.json b/homeassistant/components/adguard/translations/zh-Hant.json index 69d24d1fa7f35..eeec0d6b17cdc 100644 --- a/homeassistant/components/adguard/translations/zh-Hant.json +++ b/homeassistant/components/adguard/translations/zh-Hant.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "existing_instance_updated": "\u5df2\u66f4\u65b0\u73fe\u6709\u8a2d\u5b9a\u3002", "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, diff --git a/homeassistant/components/coronavirus/translations/et.json b/homeassistant/components/coronavirus/translations/et.json index 880ada2e7c2de..921b3466a232f 100644 --- a/homeassistant/components/coronavirus/translations/et.json +++ b/homeassistant/components/coronavirus/translations/et.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Teenus on juba seadistatud" + "already_configured": "Teenus on juba seadistatud", + "cannot_connect": "\u00dchendamine nurjus" }, "step": { "user": { diff --git a/homeassistant/components/coronavirus/translations/it.json b/homeassistant/components/coronavirus/translations/it.json index 8cc2065b94a64..fb6825893349e 100644 --- a/homeassistant/components/coronavirus/translations/it.json +++ b/homeassistant/components/coronavirus/translations/it.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Il servizio \u00e8 gi\u00e0 configurato" + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato", + "cannot_connect": "Impossibile connettersi" }, "step": { "user": { diff --git a/homeassistant/components/coronavirus/translations/nl.json b/homeassistant/components/coronavirus/translations/nl.json index fed3101b38e40..fec0b6462ebaf 100644 --- a/homeassistant/components/coronavirus/translations/nl.json +++ b/homeassistant/components/coronavirus/translations/nl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Service is al geconfigureerd" + "already_configured": "Service is al geconfigureerd", + "cannot_connect": "Kan geen verbinding maken" }, "step": { "user": { diff --git a/homeassistant/components/coronavirus/translations/ru.json b/homeassistant/components/coronavirus/translations/ru.json index e7e6798f6a4f6..02590c8100fc3 100644 --- a/homeassistant/components/coronavirus/translations/ru.json +++ b/homeassistant/components/coronavirus/translations/ru.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." + "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, "step": { "user": { diff --git a/homeassistant/components/coronavirus/translations/zh-Hant.json b/homeassistant/components/coronavirus/translations/zh-Hant.json index 9e2ed171453e2..4d54be5e3dedb 100644 --- a/homeassistant/components/coronavirus/translations/zh-Hant.json +++ b/homeassistant/components/coronavirus/translations/zh-Hant.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "cannot_connect": "\u9023\u7dda\u5931\u6557" }, "step": { "user": { diff --git a/homeassistant/components/enphase_envoy/translations/it.json b/homeassistant/components/enphase_envoy/translations/it.json index 18eab778b340f..2f0e1edc8450b 100644 --- a/homeassistant/components/enphase_envoy/translations/it.json +++ b/homeassistant/components/enphase_envoy/translations/it.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { "cannot_connect": "Impossibile connettersi", diff --git a/homeassistant/components/enphase_envoy/translations/nl.json b/homeassistant/components/enphase_envoy/translations/nl.json index 1679e5ce0f417..da43476cd813f 100644 --- a/homeassistant/components/enphase_envoy/translations/nl.json +++ b/homeassistant/components/enphase_envoy/translations/nl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Apparaat is al geconfigureerd" + "already_configured": "Apparaat is al geconfigureerd", + "reauth_successful": "Herauthenticatie was succesvol" }, "error": { "cannot_connect": "Kan geen verbinding maken", diff --git a/homeassistant/components/enphase_envoy/translations/no.json b/homeassistant/components/enphase_envoy/translations/no.json index b059bbf6be032..aee2b0f711a46 100644 --- a/homeassistant/components/enphase_envoy/translations/no.json +++ b/homeassistant/components/enphase_envoy/translations/no.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Enheten er allerede konfigurert" + "already_configured": "Enheten er allerede konfigurert", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", diff --git a/homeassistant/components/enphase_envoy/translations/zh-Hant.json b/homeassistant/components/enphase_envoy/translations/zh-Hant.json index c6ae58a74c0ad..6fd6d4d038aca 100644 --- a/homeassistant/components/enphase_envoy/translations/zh-Hant.json +++ b/homeassistant/components/enphase_envoy/translations/zh-Hant.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/ialarm/translations/it.json b/homeassistant/components/ialarm/translations/it.json new file mode 100644 index 0000000000000..89cb26f8e450c --- /dev/null +++ b/homeassistant/components/ialarm/translations/it.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "host": "Host", + "pin": "Codice PIN", + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/it.json b/homeassistant/components/litterrobot/translations/it.json index 843262aa31858..aee18749ab033 100644 --- a/homeassistant/components/litterrobot/translations/it.json +++ b/homeassistant/components/litterrobot/translations/it.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + "already_configured": "L'account \u00e8 gi\u00e0 configurato" }, "error": { "cannot_connect": "Impossibile connettersi", diff --git a/homeassistant/components/sma/translations/it.json b/homeassistant/components/sma/translations/it.json new file mode 100644 index 0000000000000..0c61a0065fafe --- /dev/null +++ b/homeassistant/components/sma/translations/it.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "cannot_retrieve_device_info": "Connessione riuscita, ma impossibile recuperare le informazioni sul dispositivo", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "group": "Gruppo", + "host": "Host", + "password": "Password", + "ssl": "Utilizza un certificato SSL", + "verify_ssl": "Verificare il certificato SSL" + }, + "description": "Inserisci le informazioni sul tuo dispositivo SMA.", + "title": "Configurare SMA Solar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sma/translations/zh-Hant.json b/homeassistant/components/sma/translations/zh-Hant.json new file mode 100644 index 0000000000000..0d655b5ed04ce --- /dev/null +++ b/homeassistant/components/sma/translations/zh-Hant.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "cannot_retrieve_device_info": "\u6210\u529f\u9023\u7dda\u3001\u4f46\u7121\u6cd5\u53d6\u5f97\u88dd\u7f6e\u8cc7\u8a0a", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "group": "\u7fa4\u7d44", + "host": "\u4e3b\u6a5f\u7aef", + "password": "\u5bc6\u78bc", + "ssl": "\u4f7f\u7528 SSL \u8a8d\u8b49", + "verify_ssl": "\u78ba\u8a8d SSL \u8a8d\u8b49" + }, + "description": "\u8f38\u5165 SMA \u88dd\u7f6e\u8cc7\u8a0a\u3002", + "title": "\u8a2d\u5b9a SMA Solar" + } + } + } +} \ No newline at end of file From 673f542cdebfedc0c10bb239cc775c5d30a81a3f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 16 Apr 2021 17:57:28 -1000 Subject: [PATCH 0319/1317] Do not wait for websocket response to be delivered before shutdown (#49323) - Waiting was unreliable since the websocket response could take a few seconds to get delivered - Alternate frontend fix is https://github.com/home-assistant/frontend/pull/8932 --- .../components/homeassistant/__init__.py | 26 ++----------------- tests/components/homeassistant/test_init.py | 5 ---- 2 files changed, 2 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/homeassistant/__init__.py b/homeassistant/components/homeassistant/__init__.py index 86be5862e7c33..f80d3a0efb4ce 100644 --- a/homeassistant/components/homeassistant/__init__.py +++ b/homeassistant/components/homeassistant/__init__.py @@ -21,7 +21,6 @@ import homeassistant.core as ha from homeassistant.exceptions import HomeAssistantError, Unauthorized, UnknownUser from homeassistant.helpers import config_validation as cv, recorder -from homeassistant.helpers.event import async_call_later from homeassistant.helpers.service import ( async_extract_config_entry_ids, async_extract_referenced_entity_ids, @@ -49,7 +48,6 @@ SHUTDOWN_SERVICES = (SERVICE_HOMEASSISTANT_STOP, SERVICE_HOMEASSISTANT_RESTART) -WEBSOCKET_RECEIVE_DELAY = 1 async def async_setup(hass: ha.HomeAssistant, config: dict) -> bool: @@ -143,15 +141,7 @@ async def async_handle_core_service(call): ) if call.service == SERVICE_HOMEASSISTANT_STOP: - # We delay the stop by WEBSOCKET_RECEIVE_DELAY to ensure the frontend - # can receive the response before the webserver shuts down - @ha.callback - def _async_stop(_): - # This must not be a tracked task otherwise - # the task itself will block stop - asyncio.create_task(hass.async_stop()) - - async_call_later(hass, WEBSOCKET_RECEIVE_DELAY, _async_stop) + asyncio.create_task(hass.async_stop()) return errors = await conf_util.async_check_ha_config_file(hass) @@ -172,19 +162,7 @@ def _async_stop(_): ) if call.service == SERVICE_HOMEASSISTANT_RESTART: - # We delay the restart by WEBSOCKET_RECEIVE_DELAY to ensure the frontend - # can receive the response before the webserver shuts down - @ha.callback - def _async_stop_with_code(_): - # This must not be a tracked task otherwise - # the task itself will block restart - asyncio.create_task(hass.async_stop(RESTART_EXIT_CODE)) - - async_call_later( - hass, - WEBSOCKET_RECEIVE_DELAY, - _async_stop_with_code, - ) + asyncio.create_task(hass.async_stop(RESTART_EXIT_CODE)) async def async_handle_update_service(call): """Service handler for updating an entity.""" diff --git a/tests/components/homeassistant/test_init.py b/tests/components/homeassistant/test_init.py index 451c226eb87b5..d12cc8d9a7b1e 100644 --- a/tests/components/homeassistant/test_init.py +++ b/tests/components/homeassistant/test_init.py @@ -1,7 +1,6 @@ """The tests for Core components.""" # pylint: disable=protected-access import asyncio -from datetime import timedelta import unittest from unittest.mock import Mock, patch @@ -34,12 +33,10 @@ from homeassistant.exceptions import HomeAssistantError, Unauthorized from homeassistant.helpers import entity from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util from tests.common import ( MockConfigEntry, async_capture_events, - async_fire_time_changed, async_mock_service, get_test_home_assistant, mock_registry, @@ -526,7 +523,6 @@ async def test_restart_homeassistant(hass): blocking=True, ) assert mock_check.called - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=2)) await hass.async_block_till_done() assert mock_restart.called @@ -545,6 +541,5 @@ async def test_stop_homeassistant(hass): blocking=True, ) assert not mock_check.called - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=2)) await hass.async_block_till_done() assert mock_restart.called From 94c803d83b59918d457062f69b26003e8dd90659 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 16 Apr 2021 18:01:24 -1000 Subject: [PATCH 0320/1317] Cancel tuya updates on the stop event (#49324) --- homeassistant/components/tuya/__init__.py | 46 +++++++++++++++-------- 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index 1f16d131e39ee..6dacc2e27497a 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -14,7 +14,12 @@ import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_PASSWORD, CONF_PLATFORM, CONF_USERNAME +from homeassistant.const import ( + CONF_PASSWORD, + CONF_PLATFORM, + CONF_USERNAME, + EVENT_HOMEASSISTANT_STOP, +) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv @@ -61,6 +66,7 @@ } TUYA_TRACKER = "tuya_tracker" +STOP_CANCEL = "stop_event_cancel" CONFIG_SCHEMA = vol.Schema( vol.All( @@ -139,8 +145,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): raise ConfigEntryNotReady() from exc except TuyaAPIRateLimitException as exc: - _LOGGER.error("Tuya login rate limited") - raise ConfigEntryNotReady() from exc + raise ConfigEntryNotReady("Tuya login rate limited") from exc except TuyaAPIException as exc: _LOGGER.error( @@ -149,7 +154,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): ) return False - hass.data[DOMAIN] = { + domain_data = hass.data[DOMAIN] = { TUYA_DATA: tuya, TUYA_DEVICES_CONF: entry.options.copy(), TUYA_TRACKER: None, @@ -174,22 +179,22 @@ async def async_load_devices(device_list): dev_type = device.device_type() if ( dev_type in TUYA_TYPE_TO_HA - and device.object_id() not in hass.data[DOMAIN]["entities"] + and device.object_id() not in domain_data["entities"] ): ha_type = TUYA_TYPE_TO_HA[dev_type] if ha_type not in device_type_list: device_type_list[ha_type] = [] device_type_list[ha_type].append(device.object_id()) - hass.data[DOMAIN]["entities"][device.object_id()] = None + domain_data["entities"][device.object_id()] = None for ha_type, dev_ids in device_type_list.items(): config_entries_key = f"{ha_type}.tuya" - if config_entries_key not in hass.data[DOMAIN][ENTRY_IS_SETUP]: - hass.data[DOMAIN]["pending"][ha_type] = dev_ids + if config_entries_key not in domain_data[ENTRY_IS_SETUP]: + domain_data["pending"][ha_type] = dev_ids hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, ha_type) ) - hass.data[DOMAIN][ENTRY_IS_SETUP].add(config_entries_key) + domain_data[ENTRY_IS_SETUP].add(config_entries_key) else: async_dispatcher_send(hass, TUYA_DISCOVERY_NEW.format(ha_type), dev_ids) @@ -212,15 +217,23 @@ async def async_poll_devices_update(event_time): newlist_ids = [] for device in device_list: newlist_ids.append(device.object_id()) - for dev_id in list(hass.data[DOMAIN]["entities"]): + for dev_id in list(domain_data["entities"]): if dev_id not in newlist_ids: async_dispatcher_send(hass, SIGNAL_DELETE_ENTITY, dev_id) - hass.data[DOMAIN]["entities"].pop(dev_id) + domain_data["entities"].pop(dev_id) - hass.data[DOMAIN][TUYA_TRACKER] = async_track_time_interval( + domain_data[TUYA_TRACKER] = async_track_time_interval( hass, async_poll_devices_update, timedelta(minutes=2) ) + @callback + def _async_cancel_tuya_tracker(event): + domain_data[TUYA_TRACKER]() + + domain_data[STOP_CANCEL] = hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, _async_cancel_tuya_tracker + ) + hass.services.async_register( DOMAIN, SERVICE_PULL_DEVICES, async_poll_devices_update ) @@ -236,19 +249,22 @@ async def async_force_update(call): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unloading the Tuya platforms.""" + domain_data = hass.data[DOMAIN] + unload_ok = all( await asyncio.gather( *[ hass.config_entries.async_forward_entry_unload( entry, platform.split(".", 1)[0] ) - for platform in hass.data[DOMAIN][ENTRY_IS_SETUP] + for platform in domain_data[ENTRY_IS_SETUP] ] ) ) if unload_ok: - hass.data[DOMAIN]["listener"]() - hass.data[DOMAIN][TUYA_TRACKER]() + domain_data["listener"]() + domain_data[STOP_CANCEL]() + domain_data[TUYA_TRACKER]() hass.services.async_remove(DOMAIN, SERVICE_FORCE_UPDATE) hass.services.async_remove(DOMAIN, SERVICE_PULL_DEVICES) hass.data.pop(DOMAIN) From f7b7a805f505c58e0ab3d0ca1f0e3323f6a76ec3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 16 Apr 2021 20:19:50 -1000 Subject: [PATCH 0321/1317] Bump pysonos to 0.0.43 (#49330) - Downgrade asyncio log severity --- homeassistant/components/sonos/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index 5875baf0fb96c..a3aa499c128fb 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -3,7 +3,7 @@ "name": "Sonos", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sonos", - "requirements": ["pysonos==0.0.42"], + "requirements": ["pysonos==0.0.43"], "after_dependencies": ["plex"], "ssdp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 28f01b66dd09d..de18a479d4417 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1732,7 +1732,7 @@ pysnmp==4.4.12 pysoma==0.0.10 # homeassistant.components.sonos -pysonos==0.0.42 +pysonos==0.0.43 # homeassistant.components.spc pyspcwebgw==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d34400cd1dd63..c861ef69fb0fd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -950,7 +950,7 @@ pysmartthings==0.7.6 pysoma==0.0.10 # homeassistant.components.sonos -pysonos==0.0.42 +pysonos==0.0.43 # homeassistant.components.spc pyspcwebgw==0.4.0 From 970cbcbe15926de9b3c8cb975732963d02fed1ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 17 Apr 2021 09:35:21 +0300 Subject: [PATCH 0322/1317] Type hint improvements (#49320) --- homeassistant/core.py | 4 ++- homeassistant/helpers/config_validation.py | 2 +- homeassistant/helpers/event.py | 10 +++--- homeassistant/helpers/location.py | 4 +-- homeassistant/helpers/script.py | 41 ++++++++++++++-------- homeassistant/helpers/storage.py | 12 +++---- homeassistant/helpers/template.py | 29 +++++++-------- 7 files changed, 58 insertions(+), 44 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index d172b3445e81d..3b7fad883da7b 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -784,7 +784,9 @@ def remove_listener() -> None: return remove_listener - def listen_once(self, event_type: str, listener: Callable) -> CALLBACK_TYPE: + def listen_once( + self, event_type: str, listener: Callable[[Event], None] + ) -> CALLBACK_TYPE: """Listen once for event of a specific type. To listen to all events specify the constant ``MATCH_ALL`` diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 21d04f1155179..bbac18ab8390d 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -1200,7 +1200,7 @@ def STATE_CONDITION_SCHEMA(value: Any) -> dict: # pylint: disable=invalid-name SCRIPT_ACTION_VARIABLES = "variables" -def determine_script_action(action: dict) -> str: +def determine_script_action(action: dict[str, Any]) -> str: """Determine action type.""" if CONF_DELAY in action: return SCRIPT_ACTION_DELAY diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index d52ebdb551f46..abba6f12a256e 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -8,7 +8,7 @@ import functools as ft import logging import time -from typing import Any, Awaitable, Callable, Iterable, List +from typing import Any, Awaitable, Callable, Iterable, List, cast import attr @@ -1453,10 +1453,10 @@ def process_state_match(parameter: None | str | Iterable[str]) -> Callable[[str] @callback def _entities_domains_from_render_infos( render_infos: Iterable[RenderInfo], -) -> tuple[set, set]: +) -> tuple[set[str], set[str]]: """Combine from multiple RenderInfo.""" - entities = set() - domains = set() + entities: set[str] = set() + domains: set[str] = set() for render_info in render_infos: if render_info.entities: @@ -1497,7 +1497,7 @@ def _render_infos_to_track_states(render_infos: Iterable[RenderInfo]) -> TrackSt @callback def _event_triggers_rerender(event: Event, info: RenderInfo) -> bool: """Determine if a template should be re-rendered from an event.""" - entity_id = event.data.get(ATTR_ENTITY_ID) + entity_id = cast(str, event.data.get(ATTR_ENTITY_ID)) if info.filter(entity_id): return True diff --git a/homeassistant/helpers/location.py b/homeassistant/helpers/location.py index ff27c580d2327..597787ac173e2 100644 --- a/homeassistant/helpers/location.py +++ b/homeassistant/helpers/location.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import Sequence +from typing import Iterable import voluptuous as vol @@ -25,7 +25,7 @@ def has_location(state: State) -> bool: ) -def closest(latitude: float, longitude: float, states: Sequence[State]) -> State | None: +def closest(latitude: float, longitude: float, states: Iterable[State]) -> State | None: """Return closest state to point. Async friendly. diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 6ecb25dfff133..7103fe17ac971 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -8,7 +8,7 @@ import itertools import logging from types import MappingProxyType -from typing import Any, Callable, Dict, Sequence, Union, cast +from typing import Any, Callable, Dict, Sequence, TypedDict, Union, cast import async_timeout import voluptuous as vol @@ -56,7 +56,10 @@ callback, ) from homeassistant.helpers import condition, config_validation as cv, service, template -from homeassistant.helpers.condition import trace_condition_function +from homeassistant.helpers.condition import ( + ConditionCheckerType, + trace_condition_function, +) from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -492,7 +495,7 @@ def async_script_wait(entity_id, from_s, to_s): task.cancel() unsub() - async def _async_run_long_action(self, long_task): + async def _async_run_long_action(self, long_task: asyncio.tasks.Task) -> None: """Run a long task while monitoring for stop request.""" async def async_cancel_long_task() -> None: @@ -741,7 +744,7 @@ async def _async_choose_step(self) -> None: except exceptions.ConditionError as ex: _LOGGER.warning("Error in 'choose' evaluation:\n%s", ex) - if choose_data["default"]: + if choose_data["default"] is not None: trace_set_result(choice="default") with trace_path(["default"]): await self._async_run_script(choose_data["default"]) @@ -808,7 +811,7 @@ async def _async_variables_step(self): self._hass, self._variables, render_as_defaults=False ) - async def _async_run_script(self, script): + async def _async_run_script(self, script: Script) -> None: """Execute a script.""" await self._async_run_long_action( self._hass.async_create_task( @@ -912,6 +915,11 @@ def _referenced_extract_ids(data: dict[str, Any], key: str, found: set[str]) -> found.add(item_id) +class _ChooseData(TypedDict): + choices: list[tuple[list[ConditionCheckerType], Script]] + default: Script | None + + class Script: """Representation of a script.""" @@ -973,7 +981,7 @@ def __init__( self._queue_lck = asyncio.Lock() self._config_cache: dict[set[tuple], Callable[..., bool]] = {} self._repeat_script: dict[int, Script] = {} - self._choose_data: dict[int, dict[str, Any]] = {} + self._choose_data: dict[int, _ChooseData] = {} self._referenced_entities: set[str] | None = None self._referenced_devices: set[str] | None = None self._referenced_areas: set[str] | None = None @@ -1011,14 +1019,14 @@ def update_logger(self, logger: logging.Logger | None = None) -> None: for choose_data in self._choose_data.values(): for _, script in choose_data["choices"]: script.update_logger(self._logger) - if choose_data["default"]: + if choose_data["default"] is not None: choose_data["default"].update_logger(self._logger) def _changed(self) -> None: if self._change_listener_job: self._hass.async_run_hass_job(self._change_listener_job) - def _chain_change_listener(self, sub_script): + def _chain_change_listener(self, sub_script: Script) -> None: if sub_script.is_running: self.last_action = sub_script.last_action self._changed() @@ -1203,7 +1211,9 @@ async def async_run( self._changed() raise - async def _async_stop(self, update_state, spare=None): + async def _async_stop( + self, update_state: bool, spare: _ScriptRun | None = None + ) -> None: aws = [ asyncio.create_task(run.async_stop()) for run in self._runs if run != spare ] @@ -1230,7 +1240,7 @@ async def _async_get_condition(self, config): self._config_cache[config_cache_key] = cond return cond - def _prep_repeat_script(self, step): + def _prep_repeat_script(self, step: int) -> Script: action = self.sequence[step] step_name = action.get(CONF_ALIAS, f"Repeat at step {step+1}") sub_script = Script( @@ -1247,14 +1257,14 @@ def _prep_repeat_script(self, step): sub_script.change_listener = partial(self._chain_change_listener, sub_script) return sub_script - def _get_repeat_script(self, step): + def _get_repeat_script(self, step: int) -> Script: sub_script = self._repeat_script.get(step) if not sub_script: sub_script = self._prep_repeat_script(step) self._repeat_script[step] = sub_script return sub_script - async def _async_prep_choose_data(self, step): + async def _async_prep_choose_data(self, step: int) -> _ChooseData: action = self.sequence[step] step_name = action.get(CONF_ALIAS, f"Choose at step {step+1}") choices = [] @@ -1280,6 +1290,7 @@ async def _async_prep_choose_data(self, step): ) choices.append((conditions, sub_script)) + default_script: Script | None if CONF_DEFAULT in action: default_script = Script( self._hass, @@ -1300,7 +1311,7 @@ async def _async_prep_choose_data(self, step): return {"choices": choices, "default": default_script} - async def _async_get_choose_data(self, step): + async def _async_get_choose_data(self, step: int) -> _ChooseData: choose_data = self._choose_data.get(step) if not choose_data: choose_data = await self._async_prep_choose_data(step) @@ -1330,7 +1341,7 @@ def breakpoint_clear(hass, key, run_id, node): @callback -def breakpoint_clear_all(hass): +def breakpoint_clear_all(hass: HomeAssistant) -> None: """Clear all breakpoints.""" hass.data[DATA_SCRIPT_BREAKPOINTS] = {} @@ -1348,7 +1359,7 @@ def breakpoint_set(hass, key, run_id, node): @callback -def breakpoint_list(hass): +def breakpoint_list(hass: HomeAssistant) -> list[dict[str, Any]]: """List breakpoints.""" breakpoints = hass.data[DATA_SCRIPT_BREAKPOINTS] diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index 5a08a97a21004..456e9b04709ef 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -9,7 +9,7 @@ from typing import Any, Callable from homeassistant.const import EVENT_HOMEASSISTANT_FINAL_WRITE -from homeassistant.core import CALLBACK_TYPE, CoreState, HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, CoreState, Event, HomeAssistant, callback from homeassistant.helpers.event import async_call_later from homeassistant.loader import bind_hass from homeassistant.util import json as json_util @@ -169,7 +169,7 @@ def async_delay_save(self, data_func: Callable[[], dict], delay: float = 0) -> N ) @callback - def _async_ensure_final_write_listener(self): + def _async_ensure_final_write_listener(self) -> None: """Ensure that we write if we quit before delay has passed.""" if self._unsub_final_write_listener is None: self._unsub_final_write_listener = self.hass.bus.async_listen_once( @@ -177,14 +177,14 @@ def _async_ensure_final_write_listener(self): ) @callback - def _async_cleanup_final_write_listener(self): + def _async_cleanup_final_write_listener(self) -> None: """Clean up a stop listener.""" if self._unsub_final_write_listener is not None: self._unsub_final_write_listener() self._unsub_final_write_listener = None @callback - def _async_cleanup_delay_listener(self): + def _async_cleanup_delay_listener(self) -> None: """Clean up a delay listener.""" if self._unsub_delay_listener is not None: self._unsub_delay_listener() @@ -198,7 +198,7 @@ async def _async_callback_delayed_write(self, _now): return await self._async_handle_write_data() - async def _async_callback_final_write(self, _event): + async def _async_callback_final_write(self, _event: Event) -> None: """Handle a write because Home Assistant is in final write state.""" self._unsub_final_write_listener = None await self._async_handle_write_data() @@ -239,7 +239,7 @@ async def _async_migrate_func(self, old_version, old_data): """Migrate to the new version.""" raise NotImplementedError - async def async_remove(self): + async def async_remove(self) -> None: """Remove all data.""" self._async_cleanup_delay_listener() self._async_cleanup_final_write_listener() diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index b024c8f265612..06fe5d288f570 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -16,7 +16,7 @@ import random import re import sys -from typing import Any, Generator, Iterable, cast +from typing import Any, Callable, Generator, Iterable, cast from urllib.parse import urlencode as urllib_urlencode import weakref @@ -193,31 +193,32 @@ def __str__(self) -> str: RESULT_WRAPPERS[tuple] = TupleWrapper -def _true(arg: Any) -> bool: +def _true(arg: str) -> bool: return True -def _false(arg: Any) -> bool: +def _false(arg: str) -> bool: return False class RenderInfo: """Holds information about a template render.""" - def __init__(self, template): + def __init__(self, template: Template) -> None: """Initialise.""" self.template = template # Will be set sensibly once frozen. - self.filter_lifecycle = _true - self.filter = _true + self.filter_lifecycle: Callable[[str], bool] = _true + self.filter: Callable[[str], bool] = _true self._result: str | None = None self.is_static = False self.exception: TemplateError | None = None self.all_states = False self.all_states_lifecycle = False - self.domains = set() - self.domains_lifecycle = set() - self.entities = set() + # pylint: disable=unsubscriptable-object # for abc.Set, https://github.com/PyCQA/pylint/pull/4275 + self.domains: collections.abc.Set[str] = set() + self.domains_lifecycle: collections.abc.Set[str] = set() + self.entities: collections.abc.Set[str] = set() self.rate_limit: timedelta | None = None self.has_time = False @@ -491,7 +492,7 @@ def async_render_to_info( """Render the template and collect an entity filter.""" assert self.hass and _RENDER_INFO not in self.hass.data - render_info = RenderInfo(self) # type: ignore[no-untyped-call] + render_info = RenderInfo(self) # pylint: disable=protected-access if self.is_static: @@ -1039,13 +1040,13 @@ def is_state(hass: HomeAssistant, entity_id: str, state: State) -> bool: return state_obj is not None and state_obj.state == state -def is_state_attr(hass, entity_id, name, value): +def is_state_attr(hass: HomeAssistant, entity_id: str, name: str, value: Any) -> bool: """Test if a state's attribute is a specific value.""" attr = state_attr(hass, entity_id, name) return attr is not None and attr == value -def state_attr(hass, entity_id, name): +def state_attr(hass: HomeAssistant, entity_id: str, name: str) -> Any: """Get a specific attribute from a state.""" state_obj = _get_state(hass, entity_id) if state_obj is not None: @@ -1053,7 +1054,7 @@ def state_attr(hass, entity_id, name): return None -def now(hass): +def now(hass: HomeAssistant) -> datetime: """Record fetching now.""" render_info = hass.data.get(_RENDER_INFO) if render_info is not None: @@ -1062,7 +1063,7 @@ def now(hass): return dt_util.now() -def utcnow(hass): +def utcnow(hass: HomeAssistant) -> datetime: """Record fetching utcnow.""" render_info = hass.data.get(_RENDER_INFO) if render_info is not None: From 41ed1f818ccc83d364f89728c79f20a1e4a4e4a0 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sat, 17 Apr 2021 08:57:21 +0200 Subject: [PATCH 0323/1317] Exclude epson init module from coverage (#49316) --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index 40daa9ce2307c..576a0e9603668 100644 --- a/.coveragerc +++ b/.coveragerc @@ -255,6 +255,7 @@ omit = homeassistant/components/envirophat/sensor.py homeassistant/components/envisalink/* homeassistant/components/ephember/climate.py + homeassistant/components/epson/__init__.py homeassistant/components/epson/const.py homeassistant/components/epson/media_player.py homeassistant/components/epsonworkforce/sensor.py From f96a6e878fbf813a1a9212d53fb32ced19837d15 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 16 Apr 2021 21:03:18 -1000 Subject: [PATCH 0324/1317] Ensure restore state is not written after the stop event (#49329) If everything lined up, the states could be written while Home Assistant is shutting down after the stop event because the interval tracker was not canceled on the stop event. --- homeassistant/helpers/restore_state.py | 12 +++++- tests/helpers/test_restore_state.py | 52 +++++++++++++++++++++++++- 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/homeassistant/helpers/restore_state.py b/homeassistant/helpers/restore_state.py index 3350ed7a073cc..67b2d329af1a4 100644 --- a/homeassistant/helpers/restore_state.py +++ b/homeassistant/helpers/restore_state.py @@ -177,10 +177,18 @@ async def _async_dump_states(*_: Any) -> None: self.hass.async_create_task(_async_dump_states()) # Dump states periodically - async_track_time_interval(self.hass, _async_dump_states, STATE_DUMP_INTERVAL) + cancel_interval = async_track_time_interval( + self.hass, _async_dump_states, STATE_DUMP_INTERVAL + ) + + async def _async_dump_states_at_stop(*_: Any) -> None: + cancel_interval() + await self.async_dump_states() # Dump states when stopping hass - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_dump_states) + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, _async_dump_states_at_stop + ) @callback def async_restore_entity_added(self, entity_id: str) -> None: diff --git a/tests/helpers/test_restore_state.py b/tests/helpers/test_restore_state.py index 1a2fb2f57b569..1d3be2ca98d11 100644 --- a/tests/helpers/test_restore_state.py +++ b/tests/helpers/test_restore_state.py @@ -1,8 +1,8 @@ """The tests for the Restore component.""" -from datetime import datetime +from datetime import datetime, timedelta from unittest.mock import patch -from homeassistant.const import EVENT_HOMEASSISTANT_START +from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP from homeassistant.core import CoreState, State from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import Entity @@ -15,6 +15,8 @@ ) from homeassistant.util import dt as dt_util +from tests.common import async_fire_time_changed + async def test_caching_data(hass): """Test that we cache data.""" @@ -50,6 +52,52 @@ async def test_caching_data(hass): assert mock_write_data.called +async def test_periodic_write(hass): + """Test that we write periodiclly but not after stop.""" + data = await RestoreStateData.async_get_instance(hass) + await hass.async_block_till_done() + await data.store.async_save([]) + + # Emulate a fresh load + hass.data[DATA_RESTORE_STATE_TASK] = None + + entity = RestoreEntity() + entity.hass = hass + entity.entity_id = "input_boolean.b1" + + with patch( + "homeassistant.helpers.restore_state.Store.async_save" + ) as mock_write_data: + await entity.async_get_last_state() + await hass.async_block_till_done() + + assert mock_write_data.called + + with patch( + "homeassistant.helpers.restore_state.Store.async_save" + ) as mock_write_data: + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=15)) + await hass.async_block_till_done() + + assert mock_write_data.called + + with patch( + "homeassistant.helpers.restore_state.Store.async_save" + ) as mock_write_data: + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + + assert mock_write_data.called + + with patch( + "homeassistant.helpers.restore_state.Store.async_save" + ) as mock_write_data: + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=30)) + await hass.async_block_till_done() + + assert not mock_write_data.called + + async def test_hass_starting(hass): """Test that we cache data.""" hass.state = CoreState.starting From 7f29d028a35b6dd543b6a367aaf03df6fc80aebe Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 17 Apr 2021 09:19:02 +0200 Subject: [PATCH 0325/1317] Upgrade pre-commit to 2.12.1 (#49331) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 1d4ada0afcb00..d3c858f6f32c6 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -9,7 +9,7 @@ coverage==5.5 jsonpickle==1.4.1 mock-open==1.4.0 mypy==0.812 -pre-commit==2.12.0 +pre-commit==2.12.1 pylint==2.7.4 astroid==2.5.2 pipdeptree==1.0.0 From 3a0b0380c7acbbe06f793c8b11bdc962609f0002 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 17 Apr 2021 10:25:20 +0300 Subject: [PATCH 0326/1317] Remove some unneeded pylint disables, update ref to util.process one (#49314) --- homeassistant/components/ezviz/config_flow.py | 2 +- homeassistant/components/ialarm/config_flow.py | 1 - homeassistant/components/sma/config_flow.py | 3 +-- homeassistant/components/zha/config_flow.py | 2 -- homeassistant/util/process.py | 2 +- 5 files changed, 3 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/ezviz/config_flow.py b/homeassistant/components/ezviz/config_flow.py index ba514879703f0..82203e170e590 100644 --- a/homeassistant/components/ezviz/config_flow.py +++ b/homeassistant/components/ezviz/config_flow.py @@ -17,7 +17,7 @@ ) from homeassistant.core import callback -from .const import ( # pylint: disable=unused-import +from .const import ( ATTR_SERIAL, ATTR_TYPE_CAMERA, ATTR_TYPE_CLOUD, diff --git a/homeassistant/components/ialarm/config_flow.py b/homeassistant/components/ialarm/config_flow.py index 64eab90719b51..8608a3f1d7881 100644 --- a/homeassistant/components/ialarm/config_flow.py +++ b/homeassistant/components/ialarm/config_flow.py @@ -7,7 +7,6 @@ from homeassistant import config_entries, core from homeassistant.const import CONF_HOST, CONF_PORT -# pylint: disable=unused-import from .const import DEFAULT_PORT, DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sma/config_flow.py b/homeassistant/components/sma/config_flow.py index 08c1aed2e7bbd..e4186ec987e4f 100644 --- a/homeassistant/components/sma/config_flow.py +++ b/homeassistant/components/sma/config_flow.py @@ -19,8 +19,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from .const import CONF_CUSTOM, CONF_GROUP, DEVICE_INFO, GROUPS -from .const import DOMAIN # pylint: disable=unused-import +from .const import CONF_CUSTOM, CONF_GROUP, DEVICE_INFO, DOMAIN, GROUPS _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index 9c440c29cd310..a1e161a81323f 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -112,7 +112,6 @@ async def async_step_zeroconf(self, discovery_info: DiscoveryInfoType): if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context["title_placeholders"] = { CONF_NAME: node_name, } @@ -151,7 +150,6 @@ async def async_step_port_config(self, user_input=None): if isinstance(radio_schema, vol.Schema): radio_schema = radio_schema.schema - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 source = self.context.get("source") for param, value in radio_schema.items(): if param in SUPPORTED_PORT_SETTINGS: diff --git a/homeassistant/util/process.py b/homeassistant/util/process.py index 6f8bafda7a70a..f89b2eb96eea3 100644 --- a/homeassistant/util/process.py +++ b/homeassistant/util/process.py @@ -9,7 +9,7 @@ def kill_subprocess( - # pylint: disable=unsubscriptable-object # https://github.com/PyCQA/pylint/issues/4034 + # pylint: disable=unsubscriptable-object # https://github.com/PyCQA/pylint/issues/4369 process: subprocess.Popen[Any], ) -> None: """Force kill a subprocess and wait for it to exit.""" From 189511724ac68f8a47a3c389f466897f5b4be18f Mon Sep 17 00:00:00 2001 From: Brandon Rothweiler Date: Sat, 17 Apr 2021 05:26:07 -0400 Subject: [PATCH 0327/1317] Add device tracker platform to Mazda integration (#47974) * Add device tracker platform for Mazda integration * Split device tests into a separate file * Address review comments --- homeassistant/components/mazda/__init__.py | 2 +- .../components/mazda/device_tracker.py | 58 +++++++++++++++++++ tests/components/mazda/test_device_tracker.py | 30 ++++++++++ tests/components/mazda/test_init.py | 29 ++++++++++ tests/components/mazda/test_sensor.py | 31 +--------- 5 files changed, 119 insertions(+), 31 deletions(-) create mode 100644 homeassistant/components/mazda/device_tracker.py create mode 100644 tests/components/mazda/test_device_tracker.py diff --git a/homeassistant/components/mazda/__init__.py b/homeassistant/components/mazda/__init__.py index 555cc9f3a001f..a264ec24389ef 100644 --- a/homeassistant/components/mazda/__init__.py +++ b/homeassistant/components/mazda/__init__.py @@ -29,7 +29,7 @@ _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["sensor"] +PLATFORMS = ["device_tracker", "sensor"] async def with_timeout(task, timeout_seconds=10): diff --git a/homeassistant/components/mazda/device_tracker.py b/homeassistant/components/mazda/device_tracker.py new file mode 100644 index 0000000000000..ea05d2c8c8b2c --- /dev/null +++ b/homeassistant/components/mazda/device_tracker.py @@ -0,0 +1,58 @@ +"""Platform for Mazda device tracker integration.""" +from homeassistant.components.device_tracker import SOURCE_TYPE_GPS +from homeassistant.components.device_tracker.config_entry import TrackerEntity + +from . import MazdaEntity +from .const import DATA_COORDINATOR, DOMAIN + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the device tracker platform.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR] + + entities = [] + + for index, _ in enumerate(coordinator.data): + entities.append(MazdaDeviceTracker(coordinator, index)) + + async_add_entities(entities) + + +class MazdaDeviceTracker(MazdaEntity, TrackerEntity): + """Class for the device tracker.""" + + @property + def name(self): + """Return the name of the entity.""" + vehicle_name = self.get_vehicle_name() + return f"{vehicle_name} Device Tracker" + + @property + def unique_id(self): + """Return a unique identifier for this entity.""" + return self.vin + + @property + def icon(self): + """Return the icon to use in the frontend.""" + return "mdi:car" + + @property + def source_type(self): + """Return the source type, eg gps or router, of the device.""" + return SOURCE_TYPE_GPS + + @property + def force_update(self): + """All updates do not need to be written to the state machine.""" + return False + + @property + def latitude(self): + """Return latitude value of the device.""" + return self.coordinator.data[self.index]["status"]["latitude"] + + @property + def longitude(self): + """Return longitude value of the device.""" + return self.coordinator.data[self.index]["status"]["longitude"] diff --git a/tests/components/mazda/test_device_tracker.py b/tests/components/mazda/test_device_tracker.py new file mode 100644 index 0000000000000..5e09c23ecd843 --- /dev/null +++ b/tests/components/mazda/test_device_tracker.py @@ -0,0 +1,30 @@ +"""The device tracker tests for the Mazda Connected Services integration.""" +from homeassistant.components.device_tracker import SOURCE_TYPE_GPS +from homeassistant.components.device_tracker.const import ATTR_SOURCE_TYPE +from homeassistant.const import ( + ATTR_FRIENDLY_NAME, + ATTR_ICON, + ATTR_LATITUDE, + ATTR_LONGITUDE, +) +from homeassistant.helpers import entity_registry as er + +from tests.components.mazda import init_integration + + +async def test_device_tracker(hass): + """Test creation of the device tracker.""" + await init_integration(hass) + + entity_registry = er.async_get(hass) + + state = hass.states.get("device_tracker.my_mazda3_device_tracker") + assert state + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Device Tracker" + assert state.attributes.get(ATTR_ICON) == "mdi:car" + assert state.attributes.get(ATTR_LATITUDE) == 1.234567 + assert state.attributes.get(ATTR_LONGITUDE) == -2.345678 + assert state.attributes.get(ATTR_SOURCE_TYPE) == SOURCE_TYPE_GPS + entry = entity_registry.async_get("device_tracker.my_mazda3_device_tracker") + assert entry + assert entry.unique_id == "JM000000000000000" diff --git a/tests/components/mazda/test_init.py b/tests/components/mazda/test_init.py index ebd118260bc99..fe5b96096f172 100644 --- a/tests/components/mazda/test_init.py +++ b/tests/components/mazda/test_init.py @@ -14,6 +14,7 @@ ) from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_REGION from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr from homeassistant.util import dt as dt_util from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture @@ -109,3 +110,31 @@ async def test_unload_config_entry(hass: HomeAssistant) -> None: await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() assert entry.state == ENTRY_STATE_NOT_LOADED + + +async def test_device_nickname(hass): + """Test creation of the device when vehicle has a nickname.""" + await init_integration(hass, use_nickname=True) + + device_registry = dr.async_get(hass) + reg_device = device_registry.async_get_device( + identifiers={(DOMAIN, "JM000000000000000")}, + ) + + assert reg_device.model == "2021 MAZDA3 2.5 S SE AWD" + assert reg_device.manufacturer == "Mazda" + assert reg_device.name == "My Mazda3" + + +async def test_device_no_nickname(hass): + """Test creation of the device when vehicle has no nickname.""" + await init_integration(hass, use_nickname=False) + + device_registry = dr.async_get(hass) + reg_device = device_registry.async_get_device( + identifiers={(DOMAIN, "JM000000000000000")}, + ) + + assert reg_device.model == "2021 MAZDA3 2.5 S SE AWD" + assert reg_device.manufacturer == "Mazda" + assert reg_device.name == "2021 MAZDA3 2.5 S SE AWD" diff --git a/tests/components/mazda/test_sensor.py b/tests/components/mazda/test_sensor.py index d5f25bce2f31b..179ad96d533c6 100644 --- a/tests/components/mazda/test_sensor.py +++ b/tests/components/mazda/test_sensor.py @@ -1,6 +1,5 @@ """The sensor tests for the Mazda Connected Services integration.""" -from homeassistant.components.mazda.const import DOMAIN from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_ICON, @@ -10,40 +9,12 @@ PERCENTAGE, PRESSURE_PSI, ) -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import entity_registry as er from homeassistant.util.unit_system import IMPERIAL_SYSTEM from tests.components.mazda import init_integration -async def test_device_nickname(hass): - """Test creation of the device when vehicle has a nickname.""" - await init_integration(hass, use_nickname=True) - - device_registry = dr.async_get(hass) - reg_device = device_registry.async_get_device( - identifiers={(DOMAIN, "JM000000000000000")}, - ) - - assert reg_device.model == "2021 MAZDA3 2.5 S SE AWD" - assert reg_device.manufacturer == "Mazda" - assert reg_device.name == "My Mazda3" - - -async def test_device_no_nickname(hass): - """Test creation of the device when vehicle has no nickname.""" - await init_integration(hass, use_nickname=False) - - device_registry = dr.async_get(hass) - reg_device = device_registry.async_get_device( - identifiers={(DOMAIN, "JM000000000000000")}, - ) - - assert reg_device.model == "2021 MAZDA3 2.5 S SE AWD" - assert reg_device.manufacturer == "Mazda" - assert reg_device.name == "2021 MAZDA3 2.5 S SE AWD" - - async def test_sensors(hass): """Test creation of the sensors.""" await init_integration(hass) From 7a9385d85714c222342b4c16719e7a6c3453dc6b Mon Sep 17 00:00:00 2001 From: Ruslan Sayfutdinov Date: Sat, 17 Apr 2021 11:42:31 +0100 Subject: [PATCH 0328/1317] Explicitly define all methods in ConfigFlow (#49341) --- homeassistant/components/bond/config_flow.py | 2 +- .../components/broadlink/config_flow.py | 6 +-- .../components/emonitor/config_flow.py | 8 ++-- .../components/huawei_lte/config_flow.py | 2 +- .../hunterdouglas_powerview/config_flow.py | 12 +++--- .../components/hyperion/config_flow.py | 2 +- homeassistant/components/myq/config_flow.py | 4 +- .../components/powerwall/config_flow.py | 6 +-- .../components/rachio/config_flow.py | 4 +- .../components/roomba/config_flow.py | 10 ++--- .../components/screenlogic/config_flow.py | 10 ++--- .../components/shelly/config_flow.py | 10 ++--- .../components/somfy_mylink/config_flow.py | 12 +++--- homeassistant/components/tado/config_flow.py | 4 +- homeassistant/components/wled/config_flow.py | 12 +++--- .../components/zwave_js/config_flow.py | 2 +- homeassistant/config_entries.py | 41 ++++++++++++++++--- 17 files changed, 88 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/bond/config_flow.py b/homeassistant/components/bond/config_flow.py index d4bf0275ad9e3..6829cfd4cc6bf 100644 --- a/homeassistant/components/bond/config_flow.py +++ b/homeassistant/components/bond/config_flow.py @@ -92,7 +92,7 @@ async def _async_try_automatic_configure(self) -> None: _, hub_name = await _validate_input(self.hass, self._discovered) self._discovered[CONF_NAME] = hub_name - async def async_step_zeroconf( # type: ignore[override] + async def async_step_zeroconf( self, discovery_info: DiscoveryInfoType ) -> FlowResultDict: """Handle a flow initialized by zeroconf discovery.""" diff --git a/homeassistant/components/broadlink/config_flow.py b/homeassistant/components/broadlink/config_flow.py index 158f3a2711310..766c2c6094037 100644 --- a/homeassistant/components/broadlink/config_flow.py +++ b/homeassistant/components/broadlink/config_flow.py @@ -56,10 +56,10 @@ async def async_set_device(self, device, raise_on_progress=True): "host": device.host[0], } - async def async_step_dhcp(self, dhcp_discovery): + async def async_step_dhcp(self, discovery_info): """Handle dhcp discovery.""" - host = dhcp_discovery[IP_ADDRESS] - unique_id = dhcp_discovery[MAC_ADDRESS].lower().replace(":", "") + host = discovery_info[IP_ADDRESS] + unique_id = discovery_info[MAC_ADDRESS].lower().replace(":", "") await self.async_set_unique_id(unique_id) self._abort_if_unique_id_configured(updates={CONF_HOST: host}) try: diff --git a/homeassistant/components/emonitor/config_flow.py b/homeassistant/components/emonitor/config_flow.py index bd5650d28cd19..70fa46e4ee7e4 100644 --- a/homeassistant/components/emonitor/config_flow.py +++ b/homeassistant/components/emonitor/config_flow.py @@ -63,12 +63,12 @@ async def async_step_user(self, user_input=None): errors=errors, ) - async def async_step_dhcp(self, dhcp_discovery): + async def async_step_dhcp(self, discovery_info): """Handle dhcp discovery.""" - self.discovered_ip = dhcp_discovery[IP_ADDRESS] - await self.async_set_unique_id(format_mac(dhcp_discovery[MAC_ADDRESS])) + self.discovered_ip = discovery_info[IP_ADDRESS] + await self.async_set_unique_id(format_mac(discovery_info[MAC_ADDRESS])) self._abort_if_unique_id_configured(updates={CONF_HOST: self.discovered_ip}) - name = name_short_mac(short_mac(dhcp_discovery[MAC_ADDRESS])) + name = name_short_mac(short_mac(discovery_info[MAC_ADDRESS])) self.context["title_placeholders"] = {"name": name} try: self.discovered_info = await fetch_mac_and_title( diff --git a/homeassistant/components/huawei_lte/config_flow.py b/homeassistant/components/huawei_lte/config_flow.py index 5f1cdf9325281..cfd197e151507 100644 --- a/homeassistant/components/huawei_lte/config_flow.py +++ b/homeassistant/components/huawei_lte/config_flow.py @@ -213,7 +213,7 @@ def get_router_title(conn: Connection) -> str: return self.async_create_entry(title=title, data=user_input) - async def async_step_ssdp( # type: ignore[override] + async def async_step_ssdp( self, discovery_info: DiscoveryInfoType ) -> FlowResultDict: """Handle SSDP initiated config flow.""" diff --git a/homeassistant/components/hunterdouglas_powerview/config_flow.py b/homeassistant/components/hunterdouglas_powerview/config_flow.py index 928c4b4819f46..8332e1e856f86 100644 --- a/homeassistant/components/hunterdouglas_powerview/config_flow.py +++ b/homeassistant/components/hunterdouglas_powerview/config_flow.py @@ -78,30 +78,30 @@ async def async_step_user(self, user_input=None): step_id="user", data_schema=DATA_SCHEMA, errors=errors ) - async def async_step_homekit(self, homekit_info): + async def async_step_homekit(self, discovery_info): """Handle HomeKit discovery.""" # If we already have the host configured do # not open connections to it if we can avoid it. - if self._host_already_configured(homekit_info[CONF_HOST]): + if self._host_already_configured(discovery_info[CONF_HOST]): return self.async_abort(reason="already_configured") try: - info = await validate_input(self.hass, homekit_info) + info = await validate_input(self.hass, discovery_info) except CannotConnect: return self.async_abort(reason="cannot_connect") except Exception: # pylint: disable=broad-except return self.async_abort(reason="unknown") await self.async_set_unique_id(info["unique_id"], raise_on_progress=False) - self._abort_if_unique_id_configured({CONF_HOST: homekit_info["host"]}) + self._abort_if_unique_id_configured({CONF_HOST: discovery_info["host"]}) - name = homekit_info["name"] + name = discovery_info["name"] if name.endswith(HAP_SUFFIX): name = name[: -len(HAP_SUFFIX)] self.powerview_config = { - CONF_HOST: homekit_info["host"], + CONF_HOST: discovery_info["host"], CONF_NAME: name, } return await self.async_step_link() diff --git a/homeassistant/components/hyperion/config_flow.py b/homeassistant/components/hyperion/config_flow.py index 1a087460151fd..229859111ac66 100644 --- a/homeassistant/components/hyperion/config_flow.py +++ b/homeassistant/components/hyperion/config_flow.py @@ -154,7 +154,7 @@ async def async_step_reauth( return self.async_abort(reason="cannot_connect") return await self._advance_to_auth_step_if_necessary(hyperion_client) - async def async_step_ssdp(self, discovery_info: dict[str, Any]) -> FlowResultDict: # type: ignore[override] + async def async_step_ssdp(self, discovery_info: dict[str, Any]) -> FlowResultDict: """Handle a flow initiated by SSDP.""" # Sample data provided by SSDP: { # 'ssdp_location': 'http://192.168.0.1:8090/description.xml', diff --git a/homeassistant/components/myq/config_flow.py b/homeassistant/components/myq/config_flow.py index 17c98195a4ed8..b472184616f72 100644 --- a/homeassistant/components/myq/config_flow.py +++ b/homeassistant/components/myq/config_flow.py @@ -65,7 +65,7 @@ async def async_step_user(self, user_input=None): step_id="user", data_schema=DATA_SCHEMA, errors=errors ) - async def async_step_homekit(self, homekit_info): + async def async_step_homekit(self, discovery_info): """Handle HomeKit discovery.""" if self._async_current_entries(): # We can see myq on the network to tell them to configure @@ -76,7 +76,7 @@ async def async_step_homekit(self, homekit_info): # add a new one via "+" return self.async_abort(reason="already_configured") properties = { - key.lower(): value for (key, value) in homekit_info["properties"].items() + key.lower(): value for (key, value) in discovery_info["properties"].items() } await self.async_set_unique_id(properties["id"]) return await self.async_step_user() diff --git a/homeassistant/components/powerwall/config_flow.py b/homeassistant/components/powerwall/config_flow.py index 579c916a15ada..640993af74d9d 100644 --- a/homeassistant/components/powerwall/config_flow.py +++ b/homeassistant/components/powerwall/config_flow.py @@ -59,12 +59,12 @@ def __init__(self): """Initialize the powerwall flow.""" self.ip_address = None - async def async_step_dhcp(self, dhcp_discovery): + async def async_step_dhcp(self, discovery_info): """Handle dhcp discovery.""" - if self._async_ip_address_already_configured(dhcp_discovery[IP_ADDRESS]): + if self._async_ip_address_already_configured(discovery_info[IP_ADDRESS]): return self.async_abort(reason="already_configured") - self.ip_address = dhcp_discovery[IP_ADDRESS] + self.ip_address = discovery_info[IP_ADDRESS] self.context["title_placeholders"] = {CONF_IP_ADDRESS: self.ip_address} return await self.async_step_user() diff --git a/homeassistant/components/rachio/config_flow.py b/homeassistant/components/rachio/config_flow.py index 5719dd810660f..306b05d09a67d 100644 --- a/homeassistant/components/rachio/config_flow.py +++ b/homeassistant/components/rachio/config_flow.py @@ -78,7 +78,7 @@ async def async_step_user(self, user_input=None): step_id="user", data_schema=DATA_SCHEMA, errors=errors ) - async def async_step_homekit(self, homekit_info): + async def async_step_homekit(self, discovery_info): """Handle HomeKit discovery.""" if self._async_current_entries(): # We can see rachio on the network to tell them to configure @@ -89,7 +89,7 @@ async def async_step_homekit(self, homekit_info): # add a new one via "+" return self.async_abort(reason="already_configured") properties = { - key.lower(): value for (key, value) in homekit_info["properties"].items() + key.lower(): value for (key, value) in discovery_info["properties"].items() } await self.async_set_unique_id(properties["id"]) return await self.async_step_user() diff --git a/homeassistant/components/roomba/config_flow.py b/homeassistant/components/roomba/config_flow.py index 45c2d8b9a1bdc..5603d9d9d7e0b 100644 --- a/homeassistant/components/roomba/config_flow.py +++ b/homeassistant/components/roomba/config_flow.py @@ -78,16 +78,16 @@ def async_get_options_flow(config_entry): """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) - async def async_step_dhcp(self, dhcp_discovery): + async def async_step_dhcp(self, discovery_info): """Handle dhcp discovery.""" - if self._async_host_already_configured(dhcp_discovery[IP_ADDRESS]): + if self._async_host_already_configured(discovery_info[IP_ADDRESS]): return self.async_abort(reason="already_configured") - if not dhcp_discovery[HOSTNAME].startswith(("irobot-", "roomba-")): + if not discovery_info[HOSTNAME].startswith(("irobot-", "roomba-")): return self.async_abort(reason="not_irobot_device") - self.host = dhcp_discovery[IP_ADDRESS] - self.blid = _async_blid_from_hostname(dhcp_discovery[HOSTNAME]) + self.host = discovery_info[IP_ADDRESS] + self.blid = _async_blid_from_hostname(discovery_info[HOSTNAME]) await self.async_set_unique_id(self.blid) self._abort_if_unique_id_configured(updates={CONF_HOST: self.host}) diff --git a/homeassistant/components/screenlogic/config_flow.py b/homeassistant/components/screenlogic/config_flow.py index 4f38872211712..fb33bd7e2276a 100644 --- a/homeassistant/components/screenlogic/config_flow.py +++ b/homeassistant/components/screenlogic/config_flow.py @@ -89,15 +89,15 @@ async def async_step_user(self, user_input=None): self.discovered_gateways = await async_discover_gateways_by_unique_id(self.hass) return await self.async_step_gateway_select() - async def async_step_dhcp(self, dhcp_discovery): + async def async_step_dhcp(self, discovery_info): """Handle dhcp discovery.""" - mac = _extract_mac_from_name(dhcp_discovery[HOSTNAME]) + mac = _extract_mac_from_name(discovery_info[HOSTNAME]) await self.async_set_unique_id(mac) self._abort_if_unique_id_configured( - updates={CONF_IP_ADDRESS: dhcp_discovery[IP_ADDRESS]} + updates={CONF_IP_ADDRESS: discovery_info[IP_ADDRESS]} ) - self.discovered_ip = dhcp_discovery[IP_ADDRESS] - self.context["title_placeholders"] = {"name": dhcp_discovery[HOSTNAME]} + self.discovered_ip = discovery_info[IP_ADDRESS] + self.context["title_placeholders"] = {"name": discovery_info[HOSTNAME]} return await self.async_step_gateway_entry() async def async_step_gateway_select(self, user_input=None): diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index 73c231086eff5..a2eaa21bf1d0d 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -146,21 +146,21 @@ async def async_step_credentials(self, user_input=None): step_id="credentials", data_schema=schema, errors=errors ) - async def async_step_zeroconf(self, zeroconf_info): + async def async_step_zeroconf(self, discovery_info): """Handle zeroconf discovery.""" try: - self.info = info = await self._async_get_info(zeroconf_info["host"]) + self.info = info = await self._async_get_info(discovery_info["host"]) except HTTP_CONNECT_ERRORS: return self.async_abort(reason="cannot_connect") except aioshelly.FirmwareUnsupported: return self.async_abort(reason="unsupported_firmware") await self.async_set_unique_id(info["mac"]) - self._abort_if_unique_id_configured({CONF_HOST: zeroconf_info["host"]}) - self.host = zeroconf_info["host"] + self._abort_if_unique_id_configured({CONF_HOST: discovery_info["host"]}) + self.host = discovery_info["host"] self.context["title_placeholders"] = { - "name": zeroconf_info.get("name", "").split(".")[0] + "name": discovery_info.get("name", "").split(".")[0] } if info["auth"]: diff --git a/homeassistant/components/somfy_mylink/config_flow.py b/homeassistant/components/somfy_mylink/config_flow.py index d1a1e19609ad3..739251e041fe5 100644 --- a/homeassistant/components/somfy_mylink/config_flow.py +++ b/homeassistant/components/somfy_mylink/config_flow.py @@ -59,19 +59,19 @@ def __init__(self): self.mac = None self.ip_address = None - async def async_step_dhcp(self, dhcp_discovery): + async def async_step_dhcp(self, discovery_info): """Handle dhcp discovery.""" - if self._host_already_configured(dhcp_discovery[IP_ADDRESS]): + if self._host_already_configured(discovery_info[IP_ADDRESS]): return self.async_abort(reason="already_configured") - formatted_mac = format_mac(dhcp_discovery[MAC_ADDRESS]) + formatted_mac = format_mac(discovery_info[MAC_ADDRESS]) await self.async_set_unique_id(format_mac(formatted_mac)) self._abort_if_unique_id_configured( - updates={CONF_HOST: dhcp_discovery[IP_ADDRESS]} + updates={CONF_HOST: discovery_info[IP_ADDRESS]} ) - self.host = dhcp_discovery[HOSTNAME] + self.host = discovery_info[HOSTNAME] self.mac = formatted_mac - self.ip_address = dhcp_discovery[IP_ADDRESS] + self.ip_address = discovery_info[IP_ADDRESS] self.context["title_placeholders"] = {"ip": self.ip_address, "mac": self.mac} return await self.async_step_user() diff --git a/homeassistant/components/tado/config_flow.py b/homeassistant/components/tado/config_flow.py index 5f97212abf34f..77824affecaa3 100644 --- a/homeassistant/components/tado/config_flow.py +++ b/homeassistant/components/tado/config_flow.py @@ -81,7 +81,7 @@ async def async_step_user(self, user_input=None): step_id="user", data_schema=DATA_SCHEMA, errors=errors ) - async def async_step_homekit(self, homekit_info): + async def async_step_homekit(self, discovery_info): """Handle HomeKit discovery.""" if self._async_current_entries(): # We can see tado on the network to tell them to configure @@ -92,7 +92,7 @@ async def async_step_homekit(self, homekit_info): # add a new one via "+" return self.async_abort(reason="already_configured") properties = { - key.lower(): value for (key, value) in homekit_info["properties"].items() + key.lower(): value for (key, value) in discovery_info["properties"].items() } await self.async_set_unique_id(properties["id"]) return await self.async_step_user() diff --git a/homeassistant/components/wled/config_flow.py b/homeassistant/components/wled/config_flow.py index a85a74fa94bc0..9b57109d18fa9 100644 --- a/homeassistant/components/wled/config_flow.py +++ b/homeassistant/components/wled/config_flow.py @@ -31,27 +31,27 @@ async def async_step_user( return await self._handle_config_flow(user_input) async def async_step_zeroconf( - self, user_input: ConfigType | None = None + self, discovery_info: ConfigType | None = None ) -> dict[str, Any]: """Handle zeroconf discovery.""" - if user_input is None: + if discovery_info is None: return self.async_abort(reason="cannot_connect") # Hostname is format: wled-livingroom.local. - host = user_input["hostname"].rstrip(".") + host = discovery_info["hostname"].rstrip(".") name, _ = host.rsplit(".") self.context.update( { - CONF_HOST: user_input["host"], + CONF_HOST: discovery_info["host"], CONF_NAME: name, - CONF_MAC: user_input["properties"].get(CONF_MAC), + CONF_MAC: discovery_info["properties"].get(CONF_MAC), "title_placeholders": {"name": name}, } ) # Prepare configuration flow - return await self._handle_config_flow(user_input, True) + return await self._handle_config_flow(discovery_info, True) async def async_step_zeroconf_confirm( self, user_input: ConfigType = None diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index a2429a25c1b12..b2bc5c0e0e035 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -134,7 +134,7 @@ async def async_step_manual( step_id="manual", data_schema=STEP_USER_DATA_SCHEMA, errors=errors ) - async def async_step_hassio(self, discovery_info: dict[str, Any]) -> FlowResultDict: # type: ignore[override] + async def async_step_hassio(self, discovery_info: dict[str, Any]) -> FlowResultDict: """Receive configuration from add-on discovery info. This flow is triggered by the Z-Wave JS add-on. diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index c69cd0c9d5b48..bf9a45d06f0cb 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -1229,12 +1229,41 @@ def async_abort( reason=reason, description_placeholders=description_placeholders ) - async_step_hassio = async_step_discovery - async_step_homekit = async_step_discovery - async_step_mqtt = async_step_discovery - async_step_ssdp = async_step_discovery - async_step_zeroconf = async_step_discovery - async_step_dhcp = async_step_discovery + async def async_step_hassio( + self, discovery_info: DiscoveryInfoType + ) -> data_entry_flow.FlowResultDict: + """Handle a flow initialized by HASS IO discovery.""" + return await self.async_step_discovery(discovery_info) + + async def async_step_homekit( + self, discovery_info: DiscoveryInfoType + ) -> data_entry_flow.FlowResultDict: + """Handle a flow initialized by Homekit discovery.""" + return await self.async_step_discovery(discovery_info) + + async def async_step_mqtt( + self, discovery_info: DiscoveryInfoType + ) -> data_entry_flow.FlowResultDict: + """Handle a flow initialized by MQTT discovery.""" + return await self.async_step_discovery(discovery_info) + + async def async_step_ssdp( + self, discovery_info: DiscoveryInfoType + ) -> data_entry_flow.FlowResultDict: + """Handle a flow initialized by SSDP discovery.""" + return await self.async_step_discovery(discovery_info) + + async def async_step_zeroconf( + self, discovery_info: DiscoveryInfoType + ) -> data_entry_flow.FlowResultDict: + """Handle a flow initialized by Zeroconf discovery.""" + return await self.async_step_discovery(discovery_info) + + async def async_step_dhcp( + self, discovery_info: DiscoveryInfoType + ) -> data_entry_flow.FlowResultDict: + """Handle a flow initialized by DHCP discovery.""" + return await self.async_step_discovery(discovery_info) class OptionsFlowManager(data_entry_flow.FlowManager): From 006bcde435372b0b061181ddb2cebea9e690544c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 17 Apr 2021 12:48:03 +0200 Subject: [PATCH 0329/1317] Remove HomeAssistantType alias - Part 3 (#49339) --- homeassistant/components/alarmdecoder/__init__.py | 8 ++++---- .../components/alarmdecoder/alarm_control_panel.py | 4 ++-- .../components/alarmdecoder/binary_sensor.py | 4 ++-- homeassistant/components/alarmdecoder/sensor.py | 4 ++-- homeassistant/components/arcam_fmj/__init__.py | 7 ++++--- homeassistant/components/arcam_fmj/media_player.py | 5 ++--- homeassistant/components/asuswrt/__init__.py | 8 ++++---- homeassistant/components/asuswrt/device_tracker.py | 5 ++--- homeassistant/components/asuswrt/router.py | 5 ++--- homeassistant/components/asuswrt/sensor.py | 4 ++-- homeassistant/components/awair/sensor.py | 5 +++-- homeassistant/components/azure_devops/__init__.py | 7 ++++--- homeassistant/components/azure_devops/sensor.py | 4 ++-- .../components/bluetooth_tracker/device_tracker.py | 8 ++++---- homeassistant/components/bsblan/climate.py | 4 ++-- homeassistant/components/canary/__init__.py | 10 +++++----- .../components/canary/alarm_control_panel.py | 4 ++-- homeassistant/components/canary/camera.py | 4 ++-- homeassistant/components/canary/config_flow.py | 6 +++--- homeassistant/components/canary/coordinator.py | 4 ++-- homeassistant/components/canary/sensor.py | 4 ++-- homeassistant/components/cast/media_player.py | 5 ++--- homeassistant/components/cert_expiry/__init__.py | 4 ++-- homeassistant/components/climacell/__init__.py | 14 +++++--------- homeassistant/components/climacell/config_flow.py | 5 ++--- homeassistant/components/climacell/sensor.py | 4 ++-- homeassistant/components/climacell/weather.py | 4 ++-- homeassistant/components/daikin/__init__.py | 4 ++-- .../components/devolo_home_control/__init__.py | 6 +++--- .../devolo_home_control/binary_sensor.py | 4 ++-- .../components/devolo_home_control/climate.py | 4 ++-- .../components/devolo_home_control/cover.py | 4 ++-- .../components/devolo_home_control/light.py | 4 ++-- .../components/devolo_home_control/sensor.py | 4 ++-- .../components/devolo_home_control/switch.py | 4 ++-- homeassistant/components/directv/config_flow.py | 9 +++------ homeassistant/components/directv/media_player.py | 4 ++-- homeassistant/components/directv/remote.py | 4 ++-- homeassistant/components/dlna_dmr/media_player.py | 6 +++--- homeassistant/components/dsmr/sensor.py | 5 ++--- 40 files changed, 101 insertions(+), 111 deletions(-) diff --git a/homeassistant/components/alarmdecoder/__init__.py b/homeassistant/components/alarmdecoder/__init__.py index 849aae9b3cc11..09afa84f7f56f 100644 --- a/homeassistant/components/alarmdecoder/__init__.py +++ b/homeassistant/components/alarmdecoder/__init__.py @@ -14,7 +14,7 @@ CONF_PROTOCOL, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util from .const import ( @@ -39,7 +39,7 @@ PLATFORMS = ["alarm_control_panel", "sensor", "binary_sensor"] -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up AlarmDecoder config flow.""" undo_listener = entry.add_update_listener(_update_listener) @@ -132,7 +132,7 @@ def handle_rel_message(sender, message): return True -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a AlarmDecoder entry.""" hass.data[DOMAIN][entry.entry_id][DATA_RESTART] = False @@ -160,7 +160,7 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): return True -async def _update_listener(hass: HomeAssistantType, entry: ConfigEntry): +async def _update_listener(hass: HomeAssistant, entry: ConfigEntry): """Handle options update.""" _LOGGER.debug("AlarmDecoder options updated: %s", entry.as_dict()["options"]) await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/alarmdecoder/alarm_control_panel.py b/homeassistant/components/alarmdecoder/alarm_control_panel.py index 9cab2afa43cbc..d081c9e56a318 100644 --- a/homeassistant/components/alarmdecoder/alarm_control_panel.py +++ b/homeassistant/components/alarmdecoder/alarm_control_panel.py @@ -19,9 +19,9 @@ STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import HomeAssistantType from .const import ( CONF_ALT_NIGHT_MODE, @@ -41,7 +41,7 @@ async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ): """Set up for AlarmDecoder alarm panels.""" options = entry.options diff --git a/homeassistant/components/alarmdecoder/binary_sensor.py b/homeassistant/components/alarmdecoder/binary_sensor.py index 4cc3bb6b5cf8b..71bcc399e08c8 100644 --- a/homeassistant/components/alarmdecoder/binary_sensor.py +++ b/homeassistant/components/alarmdecoder/binary_sensor.py @@ -3,7 +3,7 @@ from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .const import ( CONF_RELAY_ADDR, @@ -34,7 +34,7 @@ async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ): """Set up for AlarmDecoder sensor.""" diff --git a/homeassistant/components/alarmdecoder/sensor.py b/homeassistant/components/alarmdecoder/sensor.py index 80b9c1261a3b6..e3c85cb589338 100644 --- a/homeassistant/components/alarmdecoder/sensor.py +++ b/homeassistant/components/alarmdecoder/sensor.py @@ -1,13 +1,13 @@ """Support for AlarmDecoder sensors (Shows Panel Display).""" from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .const import SIGNAL_PANEL_MESSAGE async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ): """Set up for AlarmDecoder sensor.""" diff --git a/homeassistant/components/arcam_fmj/__init__.py b/homeassistant/components/arcam_fmj/__init__.py index fe62c41c061c1..8d22cb7723f0b 100644 --- a/homeassistant/components/arcam_fmj/__init__.py +++ b/homeassistant/components/arcam_fmj/__init__.py @@ -9,8 +9,9 @@ from homeassistant import config_entries from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from .const import ( DEFAULT_SCAN_INTERVAL, @@ -33,7 +34,7 @@ async def _await_cancel(task): await task -async def async_setup(hass: HomeAssistantType, config: ConfigType): +async def async_setup(hass: HomeAssistant, config: ConfigType): """Set up the component.""" hass.data[DOMAIN_DATA_ENTRIES] = {} hass.data[DOMAIN_DATA_TASKS] = {} @@ -48,7 +49,7 @@ async def _stop(_): return True -async def async_setup_entry(hass: HomeAssistantType, entry: config_entries.ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: config_entries.ConfigEntry): """Set up config entry.""" entries = hass.data[DOMAIN_DATA_ENTRIES] tasks = hass.data[DOMAIN_DATA_TASKS] diff --git a/homeassistant/components/arcam_fmj/media_player.py b/homeassistant/components/arcam_fmj/media_player.py index 1f0f564c59b0a..8a119d020fe47 100644 --- a/homeassistant/components/arcam_fmj/media_player.py +++ b/homeassistant/components/arcam_fmj/media_player.py @@ -22,8 +22,7 @@ ) from homeassistant.components.media_player.errors import BrowseError from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON -from homeassistant.core import callback -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant, callback from .config_flow import get_entry_client from .const import ( @@ -38,7 +37,7 @@ async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, config_entry: config_entries.ConfigEntry, async_add_entities, ): diff --git a/homeassistant/components/asuswrt/__init__.py b/homeassistant/components/asuswrt/__init__.py index 25a78f6a523ee..a736a0996d23d 100644 --- a/homeassistant/components/asuswrt/__init__.py +++ b/homeassistant/components/asuswrt/__init__.py @@ -14,8 +14,8 @@ CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.typing import HomeAssistantType from .const import ( CONF_DNSMASQ, @@ -112,7 +112,7 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up AsusWrt platform.""" # import options from yaml if empty @@ -146,7 +146,7 @@ async def async_close_connection(event): return True -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" unload_ok = all( await asyncio.gather( @@ -166,7 +166,7 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): return unload_ok -async def update_listener(hass: HomeAssistantType, entry: ConfigEntry): +async def update_listener(hass: HomeAssistant, entry: ConfigEntry): """Update when config_entry options update.""" router = hass.data[DOMAIN][entry.entry_id][DATA_ASUSWRT] diff --git a/homeassistant/components/asuswrt/device_tracker.py b/homeassistant/components/asuswrt/device_tracker.py index 0db5dba0b17a5..bf5d120c4761a 100644 --- a/homeassistant/components/asuswrt/device_tracker.py +++ b/homeassistant/components/asuswrt/device_tracker.py @@ -4,10 +4,9 @@ from homeassistant.components.device_tracker import SOURCE_TYPE_ROUTER from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.typing import HomeAssistantType from .const import DATA_ASUSWRT, DOMAIN from .router import AsusWrtRouter @@ -16,7 +15,7 @@ async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up device tracker for AsusWrt component.""" router = hass.data[DOMAIN][entry.entry_id][DATA_ASUSWRT] diff --git a/homeassistant/components/asuswrt/router.py b/homeassistant/components/asuswrt/router.py index c5880ea11bb63..9fc7ce41d0580 100644 --- a/homeassistant/components/asuswrt/router.py +++ b/homeassistant/components/asuswrt/router.py @@ -21,11 +21,10 @@ CONF_PROTOCOL, CONF_USERNAME, ) -from homeassistant.core import CALLBACK_TYPE, callback +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import dt as dt_util @@ -187,7 +186,7 @@ def last_activity(self): class AsusWrtRouter: """Representation of a AsusWrt router.""" - def __init__(self, hass: HomeAssistantType, entry: ConfigEntry) -> None: + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: """Initialize a AsusWrt router.""" self.hass = hass self._entry = entry diff --git a/homeassistant/components/asuswrt/sensor.py b/homeassistant/components/asuswrt/sensor.py index 7e38243e3d6eb..a1a9b2ff3e8ea 100644 --- a/homeassistant/components/asuswrt/sensor.py +++ b/homeassistant/components/asuswrt/sensor.py @@ -7,7 +7,7 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import DATA_GIGABYTES, DATA_RATE_MEGABITS_PER_SECOND -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -78,7 +78,7 @@ async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up the sensors.""" router: AsusWrtRouter = hass.data[DOMAIN][entry.entry_id][DATA_ASUSWRT] diff --git a/homeassistant/components/awair/sensor.py b/homeassistant/components/awair/sensor.py index 502fa3dc62605..ee7453c01017a 100644 --- a/homeassistant/components/awair/sensor.py +++ b/homeassistant/components/awair/sensor.py @@ -10,10 +10,11 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ATTR_ATTRIBUTION, ATTR_DEVICE_CLASS, CONF_ACCESS_TOKEN +from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( @@ -54,7 +55,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, config_entry: ConfigType, async_add_entities: Callable[[list[Entity], bool], None], ): diff --git a/homeassistant/components/azure_devops/__init__.py b/homeassistant/components/azure_devops/__init__.py index a971c06826ca3..5b0a42bb2a194 100644 --- a/homeassistant/components/azure_devops/__init__.py +++ b/homeassistant/components/azure_devops/__init__.py @@ -15,14 +15,15 @@ DOMAIN, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Azure DevOps from a config entry.""" client = DevOpsClient() @@ -49,7 +50,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool return True -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigType) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigType) -> bool: """Unload Azure DevOps config entry.""" del hass.data[f"{DOMAIN}_{entry.data[CONF_ORG]}_{entry.data[CONF_PROJECT]}"] diff --git a/homeassistant/components/azure_devops/sensor.py b/homeassistant/components/azure_devops/sensor.py index 01018d34c78eb..ef6697dea5fa5 100644 --- a/homeassistant/components/azure_devops/sensor.py +++ b/homeassistant/components/azure_devops/sensor.py @@ -19,8 +19,8 @@ ) from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.typing import HomeAssistantType _LOGGER = logging.getLogger(__name__) @@ -31,7 +31,7 @@ async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up Azure DevOps sensor based on a config entry.""" instance_key = f"{DOMAIN}_{entry.data[CONF_ORG]}_{entry.data[CONF_PROJECT]}" diff --git a/homeassistant/components/bluetooth_tracker/device_tracker.py b/homeassistant/components/bluetooth_tracker/device_tracker.py index f00bd672892c0..11037e2bc24d8 100644 --- a/homeassistant/components/bluetooth_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_tracker/device_tracker.py @@ -21,9 +21,9 @@ async_load_config, ) from homeassistant.const import CONF_DEVICE_ID +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.typing import HomeAssistantType from .const import DOMAIN, SERVICE_UPDATE @@ -65,7 +65,7 @@ def discover_devices(device_id: int) -> list[tuple[str, str]]: async def see_device( - hass: HomeAssistantType, async_see, mac: str, device_name: str, rssi=None + hass: HomeAssistant, async_see, mac: str, device_name: str, rssi=None ) -> None: """Mark a device as seen.""" attributes = {} @@ -80,7 +80,7 @@ async def see_device( ) -async def get_tracking_devices(hass: HomeAssistantType) -> tuple[set[str], set[str]]: +async def get_tracking_devices(hass: HomeAssistant) -> tuple[set[str], set[str]]: """ Load all known devices. @@ -108,7 +108,7 @@ def lookup_name(mac: str) -> str | None: async def async_setup_scanner( - hass: HomeAssistantType, config: dict, async_see, discovery_info=None + hass: HomeAssistant, config: dict, async_see, discovery_info=None ): """Set up the Bluetooth Scanner.""" device_id: int = config[CONF_DEVICE_ID] diff --git a/homeassistant/components/bsblan/climate.py b/homeassistant/components/bsblan/climate.py index 4d83fb04dbe87..f55472e105bb9 100644 --- a/homeassistant/components/bsblan/climate.py +++ b/homeassistant/components/bsblan/climate.py @@ -26,8 +26,8 @@ TEMP_CELSIUS, TEMP_FAHRENHEIT, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import HomeAssistantType from .const import ( ATTR_IDENTIFIERS, @@ -74,7 +74,7 @@ async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable[[list[Entity], bool], None], ) -> None: diff --git a/homeassistant/components/canary/__init__.py b/homeassistant/components/canary/__init__.py index ca6c4118753cf..04290711cb90b 100644 --- a/homeassistant/components/canary/__init__.py +++ b/homeassistant/components/canary/__init__.py @@ -10,9 +10,9 @@ from homeassistant.components.camera.const import DOMAIN as CAMERA_DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import HomeAssistantType from .const import ( CONF_FFMPEG_ARGUMENTS, @@ -44,7 +44,7 @@ PLATFORMS = ["alarm_control_panel", "camera", "sensor"] -async def async_setup(hass: HomeAssistantType, config: dict) -> bool: +async def async_setup(hass: HomeAssistant, config: dict) -> bool: """Set up the Canary integration.""" hass.data.setdefault(DOMAIN, {}) @@ -77,7 +77,7 @@ async def async_setup(hass: HomeAssistantType, config: dict) -> bool: return True -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Canary from a config entry.""" if not entry.options: options = { @@ -112,7 +112,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool return True -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = all( await asyncio.gather( @@ -130,7 +130,7 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> boo return unload_ok -async def _async_update_listener(hass: HomeAssistantType, entry: ConfigEntry) -> None: +async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: """Handle options update.""" await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/canary/alarm_control_panel.py b/homeassistant/components/canary/alarm_control_panel.py index 933e6708e22f7..3e964c186fb13 100644 --- a/homeassistant/components/canary/alarm_control_panel.py +++ b/homeassistant/components/canary/alarm_control_panel.py @@ -18,8 +18,8 @@ STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DATA_COORDINATOR, DOMAIN @@ -27,7 +27,7 @@ async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable[[list[Entity], bool], None], ) -> None: diff --git a/homeassistant/components/canary/camera.py b/homeassistant/components/canary/camera.py index 703ae2edc8a2c..1ead5dcd44e37 100644 --- a/homeassistant/components/canary/camera.py +++ b/homeassistant/components/canary/camera.py @@ -12,10 +12,10 @@ from homeassistant.components.camera import PLATFORM_SCHEMA, Camera from homeassistant.components.ffmpeg import DATA_FFMPEG from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import Throttle @@ -44,7 +44,7 @@ async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable[[list[Entity], bool], None], ) -> None: diff --git a/homeassistant/components/canary/config_flow.py b/homeassistant/components/canary/config_flow.py index 2d324e09cc8d6..d02be83a7eef2 100644 --- a/homeassistant/components/canary/config_flow.py +++ b/homeassistant/components/canary/config_flow.py @@ -10,8 +10,8 @@ from homeassistant.config_entries import CONN_CLASS_CLOUD_POLL, ConfigFlow, OptionsFlow from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME -from homeassistant.core import callback -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.typing import ConfigType from .const import ( CONF_FFMPEG_ARGUMENTS, @@ -23,7 +23,7 @@ _LOGGER = logging.getLogger(__name__) -def validate_input(hass: HomeAssistantType, data: dict) -> dict[str, Any]: +def validate_input(hass: HomeAssistant, data: dict) -> dict[str, Any]: """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. diff --git a/homeassistant/components/canary/coordinator.py b/homeassistant/components/canary/coordinator.py index 650bc3d70eab0..a7f8ea7c8de4c 100644 --- a/homeassistant/components/canary/coordinator.py +++ b/homeassistant/components/canary/coordinator.py @@ -6,7 +6,7 @@ from canary.api import Api from requests import ConnectTimeout, HTTPError -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN @@ -17,7 +17,7 @@ class CanaryDataUpdateCoordinator(DataUpdateCoordinator): """Class to manage fetching Canary data.""" - def __init__(self, hass: HomeAssistantType, *, api: Api): + def __init__(self, hass: HomeAssistant, *, api: Api): """Initialize global Canary data updater.""" self.canary = api update_interval = timedelta(seconds=30) diff --git a/homeassistant/components/canary/sensor.py b/homeassistant/components/canary/sensor.py index d7a6648857a50..9da8ad4298636 100644 --- a/homeassistant/components/canary/sensor.py +++ b/homeassistant/components/canary/sensor.py @@ -16,8 +16,8 @@ SIGNAL_STRENGTH_DECIBELS_MILLIWATT, TEMP_CELSIUS, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DATA_COORDINATOR, DOMAIN, MANUFACTURER @@ -55,7 +55,7 @@ async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable[[list[Entity], bool], None], ) -> None: diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index afd6065cb9878..c5914e93cc7c6 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -52,11 +52,10 @@ STATE_PAUSED, STATE_PLAYING, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.network import NoURLAvailableError, get_url -from homeassistant.helpers.typing import HomeAssistantType import homeassistant.util.dt as dt_util from homeassistant.util.logging import async_create_catching_coro @@ -98,7 +97,7 @@ @callback -def _async_create_cast_device(hass: HomeAssistantType, info: ChromecastInfo): +def _async_create_cast_device(hass: HomeAssistant, info: ChromecastInfo): """Create a CastDevice Entity from the chromecast object. Returns None if the cast device has already been added. diff --git a/homeassistant/components/cert_expiry/__init__.py b/homeassistant/components/cert_expiry/__init__.py index 22b3ce561298a..aab996873ca89 100644 --- a/homeassistant/components/cert_expiry/__init__.py +++ b/homeassistant/components/cert_expiry/__init__.py @@ -6,7 +6,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DEFAULT_PORT, DOMAIN @@ -18,7 +18,7 @@ SCAN_INTERVAL = timedelta(hours=12) -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Load the saved entities.""" host = entry.data[CONF_HOST] port = entry.data[CONF_PORT] diff --git a/homeassistant/components/climacell/__init__.py b/homeassistant/components/climacell/__init__.py index 20a8dd4483e60..74555e86af844 100644 --- a/homeassistant/components/climacell/__init__.py +++ b/homeassistant/components/climacell/__init__.py @@ -26,8 +26,8 @@ CONF_LONGITUDE, CONF_NAME, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -79,9 +79,7 @@ PLATFORMS = [SENSOR_DOMAIN, WEATHER_DOMAIN] -def _set_update_interval( - hass: HomeAssistantType, current_entry: ConfigEntry -) -> timedelta: +def _set_update_interval(hass: HomeAssistant, current_entry: ConfigEntry) -> timedelta: """Recalculate update_interval based on existing ClimaCell instances and update them.""" api_calls = 4 if current_entry.data[CONF_API_VERSION] == 3 else 2 # We check how many ClimaCell configured instances are using the same API key and @@ -111,7 +109,7 @@ def _set_update_interval( return interval -async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up ClimaCell API from a config entry.""" hass.data.setdefault(DOMAIN, {}) @@ -172,9 +170,7 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) return True -async def async_unload_entry( - hass: HomeAssistantType, config_entry: ConfigEntry -) -> bool: +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = all( await asyncio.gather( @@ -197,7 +193,7 @@ class ClimaCellDataUpdateCoordinator(DataUpdateCoordinator): def __init__( self, - hass: HomeAssistantType, + hass: HomeAssistant, config_entry: ConfigEntry, api: ClimaCellV3 | ClimaCellV4, update_interval: timedelta, diff --git a/homeassistant/components/climacell/config_flow.py b/homeassistant/components/climacell/config_flow.py index 1457479e62aca..69cf0c052a104 100644 --- a/homeassistant/components/climacell/config_flow.py +++ b/homeassistant/components/climacell/config_flow.py @@ -21,10 +21,9 @@ CONF_LONGITUDE, CONF_NAME, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import HomeAssistantType from .const import ( CC_ATTR_TEMPERATURE, @@ -72,7 +71,7 @@ def _get_config_schema( ) -def _get_unique_id(hass: HomeAssistantType, input_dict: dict[str, Any]): +def _get_unique_id(hass: HomeAssistant, input_dict: dict[str, Any]): """Return unique ID from config data.""" return ( f"{input_dict[CONF_API_KEY]}" diff --git a/homeassistant/components/climacell/sensor.py b/homeassistant/components/climacell/sensor.py index 8a6fb39a3810a..3d3006638f91d 100644 --- a/homeassistant/components/climacell/sensor.py +++ b/homeassistant/components/climacell/sensor.py @@ -18,8 +18,8 @@ CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import slugify from . import ClimaCellDataUpdateCoordinator, ClimaCellEntity @@ -37,7 +37,7 @@ async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable[[list[Entity], bool], None], ) -> None: diff --git a/homeassistant/components/climacell/weather.py b/homeassistant/components/climacell/weather.py index 2c31d4df4fad6..7183a3ebcf6d9 100644 --- a/homeassistant/components/climacell/weather.py +++ b/homeassistant/components/climacell/weather.py @@ -39,9 +39,9 @@ PRESSURE_INHG, TEMP_FAHRENHEIT, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity from homeassistant.helpers.sun import is_up -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import dt as dt_util from homeassistant.util.distance import convert as distance_convert from homeassistant.util.pressure import convert as pressure_convert @@ -97,7 +97,7 @@ async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable[[list[Entity], bool], None], ) -> None: diff --git a/homeassistant/components/daikin/__init__.py b/homeassistant/components/daikin/__init__.py index 092bbf8866d40..eb013e2ba30a1 100644 --- a/homeassistant/components/daikin/__init__.py +++ b/homeassistant/components/daikin/__init__.py @@ -10,10 +10,10 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_HOSTS, CONF_PASSWORD +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import Throttle from .const import CONF_UUID, DOMAIN, KEY_MAC, TIMEOUT @@ -63,7 +63,7 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Establish connection with Daikin.""" conf = entry.data # For backwards compat, set unique ID diff --git a/homeassistant/components/devolo_home_control/__init__.py b/homeassistant/components/devolo_home_control/__init__.py index 2fb31c6291c18..e9620f1955107 100644 --- a/homeassistant/components/devolo_home_control/__init__.py +++ b/homeassistant/components/devolo_home_control/__init__.py @@ -9,13 +9,13 @@ from homeassistant.components import zeroconf from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers.typing import HomeAssistantType from .const import CONF_MYDEVOLO, DOMAIN, GATEWAY_SERIAL_PATTERN, PLATFORMS -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up the devolo account from a config entry.""" hass.data.setdefault(DOMAIN, {}) @@ -71,7 +71,7 @@ def shutdown(event): return True -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload = all( await asyncio.gather( diff --git a/homeassistant/components/devolo_home_control/binary_sensor.py b/homeassistant/components/devolo_home_control/binary_sensor.py index 200b24ac7ff91..c8007792857f7 100644 --- a/homeassistant/components/devolo_home_control/binary_sensor.py +++ b/homeassistant/components/devolo_home_control/binary_sensor.py @@ -8,7 +8,7 @@ BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .const import DOMAIN from .devolo_device import DevoloDeviceEntity @@ -23,7 +23,7 @@ async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Get all binary sensor and multi level sensor devices and setup them via config entry.""" entities = [] diff --git a/homeassistant/components/devolo_home_control/climate.py b/homeassistant/components/devolo_home_control/climate.py index 7ad375bf44de4..018c9cf36eccd 100644 --- a/homeassistant/components/devolo_home_control/climate.py +++ b/homeassistant/components/devolo_home_control/climate.py @@ -10,14 +10,14 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import PRECISION_HALVES, PRECISION_TENTHS -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .const import DOMAIN from .devolo_multi_level_switch import DevoloMultiLevelSwitchDeviceEntity async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Get all cover devices and setup them via config entry.""" entities = [] diff --git a/homeassistant/components/devolo_home_control/cover.py b/homeassistant/components/devolo_home_control/cover.py index 7514a9b7c9f90..d552c53bbfc59 100644 --- a/homeassistant/components/devolo_home_control/cover.py +++ b/homeassistant/components/devolo_home_control/cover.py @@ -7,14 +7,14 @@ CoverEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .const import DOMAIN from .devolo_multi_level_switch import DevoloMultiLevelSwitchDeviceEntity async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Get all cover devices and setup them via config entry.""" entities = [] diff --git a/homeassistant/components/devolo_home_control/light.py b/homeassistant/components/devolo_home_control/light.py index 2a9be33223fdb..7fd59bd7d11ec 100644 --- a/homeassistant/components/devolo_home_control/light.py +++ b/homeassistant/components/devolo_home_control/light.py @@ -5,14 +5,14 @@ LightEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .const import DOMAIN from .devolo_multi_level_switch import DevoloMultiLevelSwitchDeviceEntity async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Get all light devices and setup them via config entry.""" entities = [] diff --git a/homeassistant/components/devolo_home_control/sensor.py b/homeassistant/components/devolo_home_control/sensor.py index e3c16670dfd54..041eb7cae38e0 100644 --- a/homeassistant/components/devolo_home_control/sensor.py +++ b/homeassistant/components/devolo_home_control/sensor.py @@ -10,7 +10,7 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .const import DOMAIN from .devolo_device import DevoloDeviceEntity @@ -27,7 +27,7 @@ async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Get all sensor devices and setup them via config entry.""" entities = [] diff --git a/homeassistant/components/devolo_home_control/switch.py b/homeassistant/components/devolo_home_control/switch.py index b4e070c50c8a6..2a96198826b05 100644 --- a/homeassistant/components/devolo_home_control/switch.py +++ b/homeassistant/components/devolo_home_control/switch.py @@ -1,14 +1,14 @@ """Platform for switch integration.""" from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .const import DOMAIN from .devolo_device import DevoloDeviceEntity async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Get all devices and setup the switch devices via config entry.""" entities = [] diff --git a/homeassistant/components/directv/config_flow.py b/homeassistant/components/directv/config_flow.py index 71a8e052c4746..3b8b591371618 100644 --- a/homeassistant/components/directv/config_flow.py +++ b/homeassistant/components/directv/config_flow.py @@ -11,12 +11,9 @@ from homeassistant.components.ssdp import ATTR_SSDP_LOCATION, ATTR_UPNP_SERIAL from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL, ConfigFlow from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.typing import ( - ConfigType, - DiscoveryInfoType, - HomeAssistantType, -) +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import CONF_RECEIVER_ID, DOMAIN @@ -26,7 +23,7 @@ ERROR_UNKNOWN = "unknown" -async def validate_input(hass: HomeAssistantType, data: dict) -> dict[str, Any]: +async def validate_input(hass: HomeAssistant, data: dict) -> dict[str, Any]: """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. diff --git a/homeassistant/components/directv/media_player.py b/homeassistant/components/directv/media_player.py index 4004592e5dcc0..65a120ba2ceea 100644 --- a/homeassistant/components/directv/media_player.py +++ b/homeassistant/components/directv/media_player.py @@ -26,7 +26,7 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util from . import DIRECTVEntity @@ -64,7 +64,7 @@ async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable[[list, bool], None], ) -> bool: diff --git a/homeassistant/components/directv/remote.py b/homeassistant/components/directv/remote.py index b35580928acb5..d1a4d236ebb38 100644 --- a/homeassistant/components/directv/remote.py +++ b/homeassistant/components/directv/remote.py @@ -9,7 +9,7 @@ from homeassistant.components.remote import ATTR_NUM_REPEATS, RemoteEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from . import DIRECTVEntity from .const import DOMAIN @@ -20,7 +20,7 @@ async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable[[list, bool], None], ) -> bool: diff --git a/homeassistant/components/dlna_dmr/media_player.py b/homeassistant/components/dlna_dmr/media_player.py index c208e1eb2ffae..260c7c4d98fa0 100644 --- a/homeassistant/components/dlna_dmr/media_player.py +++ b/homeassistant/components/dlna_dmr/media_player.py @@ -34,10 +34,10 @@ STATE_PAUSED, STATE_PLAYING, ) +from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import get_local_ip import homeassistant.util.dt as dt_util @@ -83,7 +83,7 @@ async def wrapper(self, *args, **kwargs): async def async_start_event_handler( - hass: HomeAssistantType, + hass: HomeAssistant, server_host: str, server_port: int, requester, @@ -118,7 +118,7 @@ async def async_stop_server(event): async def async_setup_platform( - hass: HomeAssistantType, config, async_add_entities, discovery_info=None + hass: HomeAssistant, config, async_add_entities, discovery_info=None ): """Set up DLNA DMR platform.""" if config.get(CONF_URL) is not None: diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index d17c3b780e42d..656c066b980bd 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -21,9 +21,8 @@ EVENT_HOMEASSISTANT_STOP, TIME_HOURS, ) -from homeassistant.core import CoreState, callback +from homeassistant.core import CoreState, HomeAssistant, callback from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import Throttle from .const import ( @@ -73,7 +72,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up the DSMR sensor.""" config = entry.data From ad967cfebb875cbedf1930490a49041d5a6a6cf7 Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Sat, 17 Apr 2021 15:41:45 +0200 Subject: [PATCH 0330/1317] Rituals Perfume Genie improvements (#49277) * Rituals Perfume Genie integration improvements * Add return type FlowResultDict to async_step_user * Rollback async_update_data * Add return type to DiffuserEntity init * check super().available too * Merge iterations * Use RitualsPerufmeGenieDataUpdateCoordinator --- .../rituals_perfume_genie/__init__.py | 55 ++++++++++--------- .../rituals_perfume_genie/binary_sensor.py | 21 ++++--- .../rituals_perfume_genie/config_flow.py | 3 +- .../rituals_perfume_genie/entity.py | 33 ++++++++--- .../rituals_perfume_genie/sensor.py | 54 ++++++++---------- .../rituals_perfume_genie/switch.py | 39 +++++++------ 6 files changed, 116 insertions(+), 89 deletions(-) diff --git a/homeassistant/components/rituals_perfume_genie/__init__.py b/homeassistant/components/rituals_perfume_genie/__init__.py index 93e5619f446df..3cc5c29d36909 100644 --- a/homeassistant/components/rituals_perfume_genie/__init__.py +++ b/homeassistant/components/rituals_perfume_genie/__init__.py @@ -3,8 +3,8 @@ from datetime import timedelta import logging -from aiohttp.client_exceptions import ClientConnectorError -from pyrituals import Account +import aiohttp +from pyrituals import Account, Diffuser from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -19,6 +19,7 @@ EMPTY_CREDENTIALS = "" _LOGGER = logging.getLogger(__name__) + UPDATE_INTERVAL = timedelta(seconds=30) @@ -30,38 +31,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): try: account_devices = await account.get_devices() - except ClientConnectorError as ex: - raise ConfigEntryNotReady from ex - - hublots = [] - devices = {} - for device in account_devices: - hublot = device.data[HUB][HUBLOT] - hublots.append(hublot) - devices[hublot] = device + except aiohttp.ClientError as err: + raise ConfigEntryNotReady from err hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { COORDINATORS: {}, - DEVICES: devices, + DEVICES: {}, } - for hublot in hublots: - device = hass.data[DOMAIN][entry.entry_id][DEVICES][hublot] - - async def async_update_data(): - await device.update_data() - return device.data - - coordinator = DataUpdateCoordinator( - hass, - _LOGGER, - name=f"{DOMAIN}-{hublot}", - update_method=async_update_data, - update_interval=UPDATE_INTERVAL, - ) + for device in account_devices: + hublot = device.data[HUB][HUBLOT] + coordinator = RitualsPerufmeGenieDataUpdateCoordinator(hass, device) await coordinator.async_refresh() + hass.data[DOMAIN][entry.entry_id][DEVICES][hublot] = device hass.data[DOMAIN][entry.entry_id][COORDINATORS][hublot] = coordinator for platform in PLATFORMS: @@ -86,3 +70,22 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data[DOMAIN].pop(entry.entry_id) return unload_ok + + +class RitualsPerufmeGenieDataUpdateCoordinator(DataUpdateCoordinator): + """Class to manage fetching Rituals Perufme Genie device data from single endpoint.""" + + def __init__(self, hass: HomeAssistant, device: Diffuser): + """Initialize global Rituals Perufme Genie data updater.""" + self._device = device + super().__init__( + hass, + _LOGGER, + name=f"{DOMAIN}-{device.data[HUB][HUBLOT]}", + update_interval=UPDATE_INTERVAL, + ) + + async def _async_update_data(self) -> dict: + """Fetch data from Rituals.""" + await self._device.update_data() + return self._device.data diff --git a/homeassistant/components/rituals_perfume_genie/binary_sensor.py b/homeassistant/components/rituals_perfume_genie/binary_sensor.py index 39c8cb8415a28..a7c6732cb132b 100644 --- a/homeassistant/components/rituals_perfume_genie/binary_sensor.py +++ b/homeassistant/components/rituals_perfume_genie/binary_sensor.py @@ -1,8 +1,15 @@ """Support for Rituals Perfume Genie binary sensors.""" +from typing import Callable + +from pyrituals import Diffuser + from homeassistant.components.binary_sensor import ( DEVICE_CLASS_BATTERY_CHARGING, BinarySensorEntity, ) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import BATTERY, COORDINATORS, DEVICES, DOMAIN, HUB, ID from .entity import SENSORS, DiffuserEntity @@ -11,7 +18,9 @@ BATTERY_CHARGING_ID = 21 -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable +) -> None: """Set up the diffuser binary sensors.""" diffusers = hass.data[DOMAIN][config_entry.entry_id][DEVICES] coordinators = hass.data[DOMAIN][config_entry.entry_id][COORDINATORS] @@ -27,18 +36,16 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class DiffuserBatteryChargingBinarySensor(DiffuserEntity, BinarySensorEntity): """Representation of a diffuser battery charging binary sensor.""" - def __init__(self, diffuser, coordinator): + def __init__(self, diffuser: Diffuser, coordinator: CoordinatorEntity) -> None: """Initialize the battery charging binary sensor.""" super().__init__(diffuser, coordinator, CHARGING_SUFFIX) @property - def is_on(self): + def is_on(self) -> bool: """Return the state of the battery charging binary sensor.""" - return bool( - self.coordinator.data[HUB][SENSORS][BATTERY][ID] == BATTERY_CHARGING_ID - ) + return self.coordinator.data[HUB][SENSORS][BATTERY][ID] == BATTERY_CHARGING_ID @property - def device_class(self): + def device_class(self) -> str: """Return the device class of the battery charging binary sensor.""" return DEVICE_CLASS_BATTERY_CHARGING diff --git a/homeassistant/components/rituals_perfume_genie/config_flow.py b/homeassistant/components/rituals_perfume_genie/config_flow.py index 7bd75cdbbc089..4c46cf09d55d5 100644 --- a/homeassistant/components/rituals_perfume_genie/config_flow.py +++ b/homeassistant/components/rituals_perfume_genie/config_flow.py @@ -7,6 +7,7 @@ from homeassistant import config_entries from homeassistant.const import CONF_EMAIL, CONF_PASSWORD +from homeassistant.data_entry_flow import FlowResultDict from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ACCOUNT_HASH, DOMAIN @@ -27,7 +28,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL - async def async_step_user(self, user_input=None): + async def async_step_user(self, user_input=None) -> FlowResultDict: """Handle the initial step.""" if user_input is None: return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA) diff --git a/homeassistant/components/rituals_perfume_genie/entity.py b/homeassistant/components/rituals_perfume_genie/entity.py index 4f89856ad0888..a3b4f568bc5dc 100644 --- a/homeassistant/components/rituals_perfume_genie/entity.py +++ b/homeassistant/components/rituals_perfume_genie/entity.py @@ -1,19 +1,31 @@ """Base class for Rituals Perfume Genie diffuser entity.""" +from __future__ import annotations + +from typing import Any + +from pyrituals import Diffuser + from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import ATTRIBUTES, DOMAIN, HUB, HUBLOT, SENSORS +from .const import ATTRIBUTES, BATTERY, DOMAIN, HUB, HUBLOT, SENSORS MANUFACTURER = "Rituals Cosmetics" -MODEL = "Diffuser" +MODEL = "The Perfume Genie" +MODEL2 = "The Perfume Genie 2.0" ROOMNAME = "roomnamec" +STATUS = "status" VERSION = "versionc" +AVAILABLE_STATE = 1 + class DiffuserEntity(CoordinatorEntity): """Representation of a diffuser entity.""" - def __init__(self, diffuser, coordinator, entity_suffix): + def __init__( + self, diffuser: Diffuser, coordinator: CoordinatorEntity, entity_suffix: str + ) -> None: """Init from config, hookup diffuser and coordinator.""" super().__init__(coordinator) self._diffuser = diffuser @@ -22,22 +34,29 @@ def __init__(self, diffuser, coordinator, entity_suffix): self._hubname = self.coordinator.data[HUB][ATTRIBUTES][ROOMNAME] @property - def unique_id(self): + def unique_id(self) -> str: """Return the unique ID of the entity.""" return f"{self._hublot}{self._entity_suffix}" @property - def name(self): + def name(self) -> str: """Return the name of the entity.""" return f"{self._hubname}{self._entity_suffix}" @property - def device_info(self): + def available(self) -> bool: + """Return if the entity is available.""" + return ( + super().available and self.coordinator.data[HUB][STATUS] == AVAILABLE_STATE + ) + + @property + def device_info(self) -> dict[str, Any]: """Return information about the device.""" return { "name": self._hubname, "identifiers": {(DOMAIN, self._hublot)}, "manufacturer": MANUFACTURER, - "model": MODEL, + "model": MODEL if BATTERY in self._diffuser.data[HUB][SENSORS] else MODEL2, "sw_version": self.coordinator.data[HUB][SENSORS][VERSION], } diff --git a/homeassistant/components/rituals_perfume_genie/sensor.py b/homeassistant/components/rituals_perfume_genie/sensor.py index 87c2da21bc769..acdb2331e71e5 100644 --- a/homeassistant/components/rituals_perfume_genie/sensor.py +++ b/homeassistant/components/rituals_perfume_genie/sensor.py @@ -1,10 +1,16 @@ """Support for Rituals Perfume Genie sensors.""" +from typing import Callable + +from pyrituals import Diffuser + +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - ATTR_BATTERY_LEVEL, DEVICE_CLASS_BATTERY, DEVICE_CLASS_SIGNAL_STRENGTH, PERCENTAGE, ) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import BATTERY, COORDINATORS, DEVICES, DOMAIN, HUB, ID, SENSORS from .entity import DiffuserEntity @@ -26,7 +32,9 @@ ATTR_SIGNAL_STRENGTH = "signal_strength" -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable +) -> None: """Set up the diffuser sensors.""" diffusers = hass.data[DOMAIN][config_entry.entry_id][DEVICES] coordinators = hass.data[DOMAIN][config_entry.entry_id][COORDINATORS] @@ -45,19 +53,19 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class DiffuserPerfumeSensor(DiffuserEntity): """Representation of a diffuser perfume sensor.""" - def __init__(self, diffuser, coordinator): + def __init__(self, diffuser: Diffuser, coordinator: CoordinatorEntity) -> None: """Initialize the perfume sensor.""" super().__init__(diffuser, coordinator, PERFUME_SUFFIX) @property - def icon(self): + def icon(self) -> str: """Return the perfume sensor icon.""" if self.coordinator.data[HUB][SENSORS][PERFUME][ID] == PERFUME_NO_CARTRIDGE_ID: return "mdi:tag-remove" return "mdi:tag-text" @property - def state(self): + def state(self) -> str: """Return the state of the perfume sensor.""" return self.coordinator.data[HUB][SENSORS][PERFUME][TITLE] @@ -65,19 +73,19 @@ def state(self): class DiffuserFillSensor(DiffuserEntity): """Representation of a diffuser fill sensor.""" - def __init__(self, diffuser, coordinator): + def __init__(self, diffuser: Diffuser, coordinator: CoordinatorEntity) -> None: """Initialize the fill sensor.""" super().__init__(diffuser, coordinator, FILL_SUFFIX) @property - def icon(self): + def icon(self) -> str: """Return the fill sensor icon.""" if self.coordinator.data[HUB][SENSORS][FILL][ID] == FILL_NO_CARTRIDGE_ID: return "mdi:beaker-question" return "mdi:beaker" @property - def state(self): + def state(self) -> str: """Return the state of the fill sensor.""" return self.coordinator.data[HUB][SENSORS][FILL][TITLE] @@ -85,12 +93,12 @@ def state(self): class DiffuserBatterySensor(DiffuserEntity): """Representation of a diffuser battery sensor.""" - def __init__(self, diffuser, coordinator): + def __init__(self, diffuser: Diffuser, coordinator: CoordinatorEntity) -> None: """Initialize the battery sensor.""" super().__init__(diffuser, coordinator, BATTERY_SUFFIX) @property - def state(self): + def state(self) -> int: """Return the state of the battery sensor.""" # Use ICON because TITLE may change in the future. # ICON filename does not match the image. @@ -103,19 +111,12 @@ def state(self): }[self.coordinator.data[HUB][SENSORS][BATTERY][ICON]] @property - def device_class(self): + def device_class(self) -> str: """Return the class of the battery sensor.""" return DEVICE_CLASS_BATTERY @property - def extra_state_attributes(self): - """Return the battery state attributes.""" - return { - ATTR_BATTERY_LEVEL: self.coordinator.data[HUB][SENSORS][BATTERY][TITLE], - } - - @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str: """Return the battery unit of measurement.""" return PERCENTAGE @@ -123,12 +124,12 @@ def unit_of_measurement(self): class DiffuserWifiSensor(DiffuserEntity): """Representation of a diffuser wifi sensor.""" - def __init__(self, diffuser, coordinator): + def __init__(self, diffuser: Diffuser, coordinator: CoordinatorEntity) -> None: """Initialize the wifi sensor.""" super().__init__(diffuser, coordinator, WIFI_SUFFIX) @property - def state(self): + def state(self) -> int: """Return the state of the wifi sensor.""" # Use ICON because TITLE may change in the future. return { @@ -139,18 +140,11 @@ def state(self): }[self.coordinator.data[HUB][SENSORS][WIFI][ICON]] @property - def device_class(self): + def device_class(self) -> str: """Return the class of the wifi sensor.""" return DEVICE_CLASS_SIGNAL_STRENGTH @property - def extra_state_attributes(self): - """Return the wifi state attributes.""" - return { - ATTR_SIGNAL_STRENGTH: self.coordinator.data[HUB][SENSORS][WIFI][TITLE], - } - - @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str: """Return the wifi unit of measurement.""" return PERCENTAGE diff --git a/homeassistant/components/rituals_perfume_genie/switch.py b/homeassistant/components/rituals_perfume_genie/switch.py index d1fff166f6e0c..1328a18d766bc 100644 --- a/homeassistant/components/rituals_perfume_genie/switch.py +++ b/homeassistant/components/rituals_perfume_genie/switch.py @@ -1,20 +1,28 @@ """Support for Rituals Perfume Genie switches.""" +from __future__ import annotations + +from typing import Any, Callable + +from pyrituals import Diffuser + from homeassistant.components.switch import SwitchEntity -from homeassistant.core import callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ATTRIBUTES, COORDINATORS, DEVICES, DOMAIN, HUB from .entity import DiffuserEntity -STATUS = "status" FAN = "fanc" SPEED = "speedc" ROOM = "roomc" ON_STATE = "1" -AVAILABLE_STATE = 1 -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable +) -> None: """Set up the diffuser switch.""" diffusers = hass.data[DOMAIN][config_entry.entry_id][DEVICES] coordinators = hass.data[DOMAIN][config_entry.entry_id][COORDINATORS] @@ -29,23 +37,18 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class DiffuserSwitch(SwitchEntity, DiffuserEntity): """Representation of a diffuser switch.""" - def __init__(self, diffuser, coordinator): + def __init__(self, diffuser: Diffuser, coordinator: CoordinatorEntity) -> None: """Initialize the diffuser switch.""" super().__init__(diffuser, coordinator, "") self._is_on = self.coordinator.data[HUB][ATTRIBUTES][FAN] == ON_STATE @property - def available(self): - """Return if the device is available.""" - return self.coordinator.data[HUB][STATUS] == AVAILABLE_STATE - - @property - def icon(self): + def icon(self) -> str: """Return the icon of the device.""" return "mdi:fan" @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the device state attributes.""" attributes = { "fan_speed": self.coordinator.data[HUB][ATTRIBUTES][SPEED], @@ -54,24 +57,24 @@ def extra_state_attributes(self): return attributes @property - def is_on(self): + def is_on(self) -> bool: """If the device is currently on or off.""" return self._is_on - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" await self._diffuser.turn_on() self._is_on = True - self.schedule_update_ha_state() + self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" await self._diffuser.turn_off() self._is_on = False - self.schedule_update_ha_state() + self.async_write_ha_state() @callback - def _handle_coordinator_update(self): + def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" self._is_on = self.coordinator.data[HUB][ATTRIBUTES][FAN] == ON_STATE self.async_write_ha_state() From 912d5c347cd90c8e65f73b0bb42c3f334e549d2a Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sat, 17 Apr 2021 18:20:16 +0100 Subject: [PATCH 0331/1317] Add reauth flow for lyric (#47863) --- homeassistant/components/lyric/__init__.py | 9 ++- homeassistant/components/lyric/config_flow.py | 24 +++++++ homeassistant/components/lyric/strings.json | 7 +- .../components/lyric/translations/en.json | 7 +- tests/components/lyric/test_config_flow.py | 68 +++++++++++++++++++ 5 files changed, 111 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/lyric/__init__.py b/homeassistant/components/lyric/__init__.py index c3ef18e7c7fa1..7a6e00da7d2d8 100644 --- a/homeassistant/components/lyric/__init__.py +++ b/homeassistant/components/lyric/__init__.py @@ -6,7 +6,9 @@ import logging from typing import Any +from aiohttp.client_exceptions import ClientResponseError from aiolyric import Lyric +from aiolyric.exceptions import LyricAuthenticationException, LyricException from aiolyric.objects.device import LyricDevice from aiolyric.objects.location import LyricLocation import async_timeout @@ -15,6 +17,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers import ( aiohttp_client, config_entry_oauth2_flow, @@ -29,7 +32,7 @@ from .api import ConfigEntryLyricClient, LyricLocalOAuth2Implementation from .config_flow import OAuth2FlowHandler -from .const import DOMAIN, LYRIC_EXCEPTIONS, OAUTH2_AUTHORIZE, OAUTH2_TOKEN +from .const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN CONFIG_SCHEMA = vol.Schema( { @@ -94,7 +97,9 @@ async def async_update_data() -> Lyric: async with async_timeout.timeout(60): await lyric.get_locations() return lyric - except LYRIC_EXCEPTIONS as exception: + except LyricAuthenticationException as exception: + raise ConfigEntryAuthFailed from exception + except (LyricException, ClientResponseError) as exception: raise UpdateFailed(exception) from exception coordinator = DataUpdateCoordinator( diff --git a/homeassistant/components/lyric/config_flow.py b/homeassistant/components/lyric/config_flow.py index 1370d5e67ea9c..dedd84c4757d5 100644 --- a/homeassistant/components/lyric/config_flow.py +++ b/homeassistant/components/lyric/config_flow.py @@ -1,6 +1,8 @@ """Config flow for Honeywell Lyric.""" import logging +import voluptuous as vol + from homeassistant import config_entries from homeassistant.helpers import config_entry_oauth2_flow @@ -21,3 +23,25 @@ class OAuth2FlowHandler( def logger(self) -> logging.Logger: """Return logger.""" return logging.getLogger(__name__) + + async def async_step_reauth(self, user_input=None): + """Perform reauth upon an API authentication error.""" + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm(self, user_input=None): + """Dialog that informs the user that reauth is required.""" + if user_input is None: + return self.async_show_form( + step_id="reauth_confirm", + data_schema=vol.Schema({}), + ) + return await self.async_step_user() + + async def async_oauth_create_entry(self, data: dict) -> dict: + """Create an oauth config entry or update existing entry for reauth.""" + existing_entry = await self.async_set_unique_id(DOMAIN) + if existing_entry: + self.hass.config_entries.async_update_entry(existing_entry, data=data) + await self.hass.config_entries.async_reload(existing_entry.entry_id) + return self.async_abort(reason="reauth_successful") + return self.async_create_entry(title="Lyric", data=data) diff --git a/homeassistant/components/lyric/strings.json b/homeassistant/components/lyric/strings.json index 4e5f2330840ee..3c9cd6043dfa2 100644 --- a/homeassistant/components/lyric/strings.json +++ b/homeassistant/components/lyric/strings.json @@ -3,11 +3,16 @@ "step": { "pick_implementation": { "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" + }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "The Lyric integration needs to re-authenticate your account." } }, "abort": { + "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", - "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]" + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" diff --git a/homeassistant/components/lyric/translations/en.json b/homeassistant/components/lyric/translations/en.json index e3849fc17a3aa..17586f16109ed 100644 --- a/homeassistant/components/lyric/translations/en.json +++ b/homeassistant/components/lyric/translations/en.json @@ -2,7 +2,8 @@ "config": { "abort": { "authorize_url_timeout": "Timeout generating authorize URL.", - "missing_configuration": "The component is not configured. Please follow the documentation." + "missing_configuration": "The component is not configured. Please follow the documentation.", + "reauth_successful": "Re-authentication was successful" }, "create_entry": { "default": "Successfully authenticated" @@ -10,6 +11,10 @@ "step": { "pick_implementation": { "title": "Pick Authentication Method" + }, + "reauth_confirm": { + "description": "The Lyric integration needs to re-authenticate your account.", + "title": "Reauthenticate Integration" } } } diff --git a/tests/components/lyric/test_config_flow.py b/tests/components/lyric/test_config_flow.py index bfdd45f0f8e83..71fb473127d42 100644 --- a/tests/components/lyric/test_config_flow.py +++ b/tests/components/lyric/test_config_flow.py @@ -11,6 +11,8 @@ from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.helpers import config_entry_oauth2_flow +from tests.common import MockConfigEntry + CLIENT_ID = "1234" CLIENT_SECRET = "5678" @@ -131,3 +133,69 @@ async def test_abort_if_authorization_timeout( assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "authorize_url_timeout" + + +async def test_reauthentication_flow( + hass, aiohttp_client, aioclient_mock, current_request_with_host +): + """Test reauthentication flow.""" + await setup.async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + CONF_CLIENT_ID: CLIENT_ID, + CONF_CLIENT_SECRET: CLIENT_SECRET, + }, + DOMAIN_HTTP: {CONF_BASE_URL: "https://example.com"}, + }, + ) + + old_entry = MockConfigEntry( + domain=DOMAIN, + unique_id=DOMAIN, + version=1, + data={"id": "timmo", "auth_implementation": DOMAIN}, + ) + old_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "reauth"}, data=old_entry.data + ) + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + result = await hass.config_entries.flow.async_configure(flows[0]["flow_id"], {}) + + # pylint: disable=protected-access + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + client = await aiohttp_client(hass.http.app) + await client.get(f"/auth/external/callback?code=abcd&state={state}") + + aioclient_mock.post( + OAUTH2_TOKEN, + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + + with patch("homeassistant.components.lyric.api.ConfigEntryLyricClient"): + with patch( + "homeassistant.components.lyric.async_setup_entry", return_value=True + ) as mock_setup: + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "reauth_successful" + + assert len(mock_setup.mock_calls) == 1 From 18cbf3cdb2838b63043ceb6e7b2d406fa4bcf4f2 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sat, 17 Apr 2021 18:20:35 +0100 Subject: [PATCH 0332/1317] Fix lyric heat cool setting (#47875) --- homeassistant/components/lyric/climate.py | 24 +++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/lyric/climate.py b/homeassistant/components/lyric/climate.py index e57bfd0c514bd..649706f9d8ef2 100644 --- a/homeassistant/components/lyric/climate.py +++ b/homeassistant/components/lyric/climate.py @@ -248,19 +248,27 @@ async def async_set_temperature(self, **kwargs) -> None: device = self.device if device.hasDualSetpointStatus: - if target_temp_low is not None and target_temp_high is not None: - temp = (target_temp_low, target_temp_high) - else: + if target_temp_low is None or target_temp_high is None: raise HomeAssistantError( "Could not find target_temp_low and/or target_temp_high in arguments" ) + _LOGGER.debug("Set temperature: %s - %s", target_temp_low, target_temp_high) + try: + await self._update_thermostat( + self.location, + device, + coolSetpoint=target_temp_low, + heatSetpoint=target_temp_high, + ) + except LYRIC_EXCEPTIONS as exception: + _LOGGER.error(exception) else: temp = kwargs.get(ATTR_TEMPERATURE) - _LOGGER.debug("Set temperature: %s", temp) - try: - await self._update_thermostat(self.location, device, heatSetpoint=temp) - except LYRIC_EXCEPTIONS as exception: - _LOGGER.error(exception) + _LOGGER.debug("Set temperature: %s", temp) + try: + await self._update_thermostat(self.location, device, heatSetpoint=temp) + except LYRIC_EXCEPTIONS as exception: + _LOGGER.error(exception) await self.coordinator.async_refresh() async def async_set_hvac_mode(self, hvac_mode: str) -> None: From 46c28f349a098f779837fd3b20ce4e3c972f3730 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 17 Apr 2021 09:25:13 -1000 Subject: [PATCH 0333/1317] Update mazda to use ConfigEntryAuthFailed (#49333) --- homeassistant/components/mazda/__init__.py | 24 +-- homeassistant/components/mazda/config_flow.py | 85 ++++----- homeassistant/components/mazda/strings.json | 9 - .../components/mazda/translations/en.json | 9 - tests/components/mazda/test_config_flow.py | 176 ++++++++++++++---- tests/components/mazda/test_init.py | 4 +- 6 files changed, 183 insertions(+), 124 deletions(-) diff --git a/homeassistant/components/mazda/__init__.py b/homeassistant/components/mazda/__init__.py index a264ec24389ef..c640dd2528ff0 100644 --- a/homeassistant/components/mazda/__init__.py +++ b/homeassistant/components/mazda/__init__.py @@ -13,10 +13,10 @@ MazdaTokenExpiredException, ) -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_REGION from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import aiohttp_client from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -49,15 +49,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): try: await mazda_client.validate_credentials() - except MazdaAuthenticationException: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_REAUTH}, - data=entry.data, - ) - ) - return False + except MazdaAuthenticationException as ex: + raise ConfigEntryAuthFailed from ex except ( MazdaException, MazdaAccountLockedException, @@ -83,14 +76,7 @@ async def async_update_data(): return vehicles except MazdaAuthenticationException as ex: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_REAUTH}, - data=entry.data, - ) - ) - raise UpdateFailed("Not authenticated with Mazda API") from ex + raise ConfigEntryAuthFailed("Not authenticated with Mazda API") from ex except Exception as ex: _LOGGER.exception( "Unknown error occurred during Mazda update request: %s", ex diff --git a/homeassistant/components/mazda/config_flow.py b/homeassistant/components/mazda/config_flow.py index 3c1137b8e8053..dc4300d2e4da8 100644 --- a/homeassistant/components/mazda/config_flow.py +++ b/homeassistant/components/mazda/config_flow.py @@ -32,12 +32,23 @@ class MazdaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + def __init__(self): + """Start the mazda config flow.""" + self._reauth_entry = None + self._email = None + self._region = None + async def async_step_user(self, user_input=None): """Handle the initial step.""" errors = {} if user_input is not None: - await self.async_set_unique_id(user_input[CONF_EMAIL].lower()) + self._email = user_input[CONF_EMAIL] + self._region = user_input[CONF_REGION] + unique_id = user_input[CONF_EMAIL].lower() + await self.async_set_unique_id(unique_id) + if not self._reauth_entry: + self._abort_if_unique_id_configured() websession = aiohttp_client.async_get_clientsession(self.hass) mazda_client = MazdaAPI( user_input[CONF_EMAIL], @@ -60,56 +71,38 @@ async def async_step_user(self, user_input=None): "Unknown error occurred during Mazda login request: %s", ex ) else: - return self.async_create_entry( - title=user_input[CONF_EMAIL], data=user_input + if not self._reauth_entry: + return self.async_create_entry( + title=user_input[CONF_EMAIL], data=user_input + ) + self.hass.config_entries.async_update_entry( + self._reauth_entry, data=user_input, unique_id=unique_id + ) + # Reload the config entry otherwise devices will remain unavailable + self.hass.async_create_task( + self.hass.config_entries.async_reload(self._reauth_entry.entry_id) ) + return self.async_abort(reason="reauth_successful") return self.async_show_form( - step_id="user", data_schema=DATA_SCHEMA, errors=errors + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_EMAIL, default=self._email): str, + vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_REGION, default=self._region): vol.In( + MAZDA_REGIONS + ), + } + ), + errors=errors, ) async def async_step_reauth(self, user_input=None): """Perform reauth if the user credentials have changed.""" - errors = {} - - if user_input is not None: - try: - websession = aiohttp_client.async_get_clientsession(self.hass) - mazda_client = MazdaAPI( - user_input[CONF_EMAIL], - user_input[CONF_PASSWORD], - user_input[CONF_REGION], - websession, - ) - await mazda_client.validate_credentials() - except MazdaAuthenticationException: - errors["base"] = "invalid_auth" - except MazdaAccountLockedException: - errors["base"] = "account_locked" - except aiohttp.ClientError: - errors["base"] = "cannot_connect" - except Exception as ex: # pylint: disable=broad-except - errors["base"] = "unknown" - _LOGGER.exception( - "Unknown error occurred during Mazda login request: %s", ex - ) - else: - await self.async_set_unique_id(user_input[CONF_EMAIL].lower()) - - for entry in self._async_current_entries(): - if entry.unique_id == self.unique_id: - self.hass.config_entries.async_update_entry( - entry, data=user_input - ) - - # Reload the config entry otherwise devices will remain unavailable - self.hass.async_create_task( - self.hass.config_entries.async_reload(entry.entry_id) - ) - - return self.async_abort(reason="reauth_successful") - errors["base"] = "unknown" - - return self.async_show_form( - step_id="reauth", data_schema=DATA_SCHEMA, errors=errors + self._reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] ) + self._email = user_input[CONF_EMAIL] + self._region = user_input[CONF_REGION] + return await self.async_step_user() diff --git a/homeassistant/components/mazda/strings.json b/homeassistant/components/mazda/strings.json index 1950260bfcbea..a7bed8725af7a 100644 --- a/homeassistant/components/mazda/strings.json +++ b/homeassistant/components/mazda/strings.json @@ -11,15 +11,6 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "step": { - "reauth": { - "data": { - "email": "[%key:common::config_flow::data::email%]", - "password": "[%key:common::config_flow::data::password%]", - "region": "Region" - }, - "description": "Authentication failed for Mazda Connected Services. Please enter your current credentials.", - "title": "Mazda Connected Services - Authentication Failed" - }, "user": { "data": { "email": "[%key:common::config_flow::data::email%]", diff --git a/homeassistant/components/mazda/translations/en.json b/homeassistant/components/mazda/translations/en.json index b9e02fb3a412f..b483947aaa0f2 100644 --- a/homeassistant/components/mazda/translations/en.json +++ b/homeassistant/components/mazda/translations/en.json @@ -11,15 +11,6 @@ "unknown": "Unexpected error" }, "step": { - "reauth": { - "data": { - "email": "Email", - "password": "Password", - "region": "Region" - }, - "description": "Authentication failed for Mazda Connected Services. Please enter your current credentials.", - "title": "Mazda Connected Services - Authentication Failed" - }, "user": { "data": { "email": "Email", diff --git a/tests/components/mazda/test_config_flow.py b/tests/components/mazda/test_config_flow.py index f4bdfa930bdde..06cb0e15d09b8 100644 --- a/tests/components/mazda/test_config_flow.py +++ b/tests/components/mazda/test_config_flow.py @@ -24,6 +24,11 @@ CONF_PASSWORD: "password_fixed", CONF_REGION: "MNAO", } +FIXTURE_USER_INPUT_REAUTH_CHANGED_EMAIL = { + CONF_EMAIL: "example2@example.com", + CONF_PASSWORD: "password_fixed", + CONF_REGION: "MNAO", +} async def test_form(hass): @@ -54,6 +59,36 @@ async def test_form(hass): assert len(mock_setup_entry.mock_calls) == 1 +async def test_account_already_exists(hass): + """Test account already exists.""" + mock_config = MockConfigEntry( + domain=DOMAIN, + unique_id=FIXTURE_USER_INPUT[CONF_EMAIL], + data=FIXTURE_USER_INPUT, + ) + mock_config.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + with patch( + "homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials", + return_value=True, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + FIXTURE_USER_INPUT, + ) + await hass.async_block_till_done() + + assert result2["type"] == "abort" + assert result2["reason"] == "already_configured" + + async def test_form_invalid_auth(hass: HomeAssistant) -> None: """Test we handle invalid auth.""" result = await hass.config_entries.flow.async_init( @@ -145,37 +180,40 @@ async def test_form_unknown_error(hass): async def test_reauth_flow(hass: HomeAssistant) -> None: """Test reauth works.""" await setup.async_setup_component(hass, "persistent_notification", {}) + mock_config = MockConfigEntry( + domain=DOMAIN, + unique_id=FIXTURE_USER_INPUT[CONF_EMAIL], + data=FIXTURE_USER_INPUT, + ) + mock_config.add_to_hass(hass) with patch( "homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials", side_effect=MazdaAuthenticationException("Failed to authenticate"), + ), patch( + "homeassistant.components.mazda.async_setup_entry", + return_value=True, ): - mock_config = MockConfigEntry( - domain=DOMAIN, - unique_id=FIXTURE_USER_INPUT[CONF_EMAIL], - data=FIXTURE_USER_INPUT, - ) - mock_config.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config.entry_id) await hass.async_block_till_done() result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT + DOMAIN, + context={"source": "reauth", "entry_id": mock_config.entry_id}, + data=FIXTURE_USER_INPUT, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "reauth" - assert result["errors"] == {"base": "invalid_auth"} + assert result["step_id"] == "user" + assert result["errors"] == {} with patch( "homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials", return_value=True, - ): - result2 = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": "reauth", "unique_id": FIXTURE_USER_INPUT[CONF_EMAIL]}, - data=FIXTURE_USER_INPUT_REAUTH, + ), patch("homeassistant.components.mazda.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + FIXTURE_USER_INPUT_REAUTH, ) await hass.async_block_till_done() @@ -185,16 +223,28 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: async def test_reauth_authorization_error(hass: HomeAssistant) -> None: """Test we show user form on authorization error.""" + mock_config = MockConfigEntry( + domain=DOMAIN, + unique_id=FIXTURE_USER_INPUT[CONF_EMAIL], + data=FIXTURE_USER_INPUT, + ) + mock_config.add_to_hass(hass) + with patch( "homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials", side_effect=MazdaAuthenticationException("Failed to authenticate"), + ), patch( + "homeassistant.components.mazda.async_setup_entry", + return_value=True, ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT + DOMAIN, + context={"source": "reauth", "entry_id": mock_config.entry_id}, + data=FIXTURE_USER_INPUT, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "reauth" + assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -203,22 +253,34 @@ async def test_reauth_authorization_error(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result2["step_id"] == "reauth" + assert result2["step_id"] == "user" assert result2["errors"] == {"base": "invalid_auth"} async def test_reauth_account_locked(hass: HomeAssistant) -> None: """Test we show user form on account_locked error.""" + mock_config = MockConfigEntry( + domain=DOMAIN, + unique_id=FIXTURE_USER_INPUT[CONF_EMAIL], + data=FIXTURE_USER_INPUT, + ) + mock_config.add_to_hass(hass) + with patch( "homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials", side_effect=MazdaAccountLockedException("Account locked"), + ), patch( + "homeassistant.components.mazda.async_setup_entry", + return_value=True, ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT + DOMAIN, + context={"source": "reauth", "entry_id": mock_config.entry_id}, + data=FIXTURE_USER_INPUT, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "reauth" + assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -227,22 +289,34 @@ async def test_reauth_account_locked(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result2["step_id"] == "reauth" + assert result2["step_id"] == "user" assert result2["errors"] == {"base": "account_locked"} async def test_reauth_connection_error(hass: HomeAssistant) -> None: """Test we show user form on connection error.""" + mock_config = MockConfigEntry( + domain=DOMAIN, + unique_id=FIXTURE_USER_INPUT[CONF_EMAIL], + data=FIXTURE_USER_INPUT, + ) + mock_config.add_to_hass(hass) + with patch( "homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials", side_effect=aiohttp.ClientError, + ), patch( + "homeassistant.components.mazda.async_setup_entry", + return_value=True, ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT + DOMAIN, + context={"source": "reauth", "entry_id": mock_config.entry_id}, + data=FIXTURE_USER_INPUT, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "reauth" + assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -251,22 +325,34 @@ async def test_reauth_connection_error(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result2["step_id"] == "reauth" + assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} async def test_reauth_unknown_error(hass: HomeAssistant) -> None: """Test we show user form on unknown error.""" + mock_config = MockConfigEntry( + domain=DOMAIN, + unique_id=FIXTURE_USER_INPUT[CONF_EMAIL], + data=FIXTURE_USER_INPUT, + ) + mock_config.add_to_hass(hass) + with patch( "homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials", side_effect=Exception, + ), patch( + "homeassistant.components.mazda.async_setup_entry", + return_value=True, ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT + DOMAIN, + context={"source": "reauth", "entry_id": mock_config.entry_id}, + data=FIXTURE_USER_INPUT, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "reauth" + assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -275,33 +361,45 @@ async def test_reauth_unknown_error(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result2["step_id"] == "reauth" + assert result2["step_id"] == "user" assert result2["errors"] == {"base": "unknown"} -async def test_reauth_unique_id_not_found(hass: HomeAssistant) -> None: - """Test we show user form when unique id not found during reauth.""" +async def test_reauth_user_has_new_email_address(hass: HomeAssistant) -> None: + """Test reauth with a new email address but same account.""" + mock_config = MockConfigEntry( + domain=DOMAIN, + unique_id=FIXTURE_USER_INPUT[CONF_EMAIL], + data=FIXTURE_USER_INPUT, + ) + mock_config.add_to_hass(hass) + with patch( "homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials", return_value=True, + ), patch( + "homeassistant.components.mazda.async_setup_entry", + return_value=True, ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT + DOMAIN, + context={"source": "reauth", "entry_id": mock_config.entry_id}, + data=FIXTURE_USER_INPUT, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "reauth" - - # Change the unique_id of the flow in order to cause a mismatch - flows = hass.config_entries.flow.async_progress() - flows[0]["context"]["unique_id"] = "example2@example.com" + assert result["step_id"] == "user" + # Change the email and ensure the entry and its unique id gets + # updated in the event the user has changed their email with mazda result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - FIXTURE_USER_INPUT_REAUTH, + FIXTURE_USER_INPUT_REAUTH_CHANGED_EMAIL, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result2["step_id"] == "reauth" - assert result2["errors"] == {"base": "unknown"} + assert ( + mock_config.unique_id == FIXTURE_USER_INPUT_REAUTH_CHANGED_EMAIL[CONF_EMAIL] + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["reason"] == "reauth_successful" diff --git a/tests/components/mazda/test_init.py b/tests/components/mazda/test_init.py index fe5b96096f172..1b062dd84f1aa 100644 --- a/tests/components/mazda/test_init.py +++ b/tests/components/mazda/test_init.py @@ -60,7 +60,7 @@ async def test_init_auth_failure(hass: HomeAssistant): flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 - assert flows[0]["step_id"] == "reauth" + assert flows[0]["step_id"] == "user" async def test_update_auth_failure(hass: HomeAssistant): @@ -99,7 +99,7 @@ async def test_update_auth_failure(hass: HomeAssistant): flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 - assert flows[0]["step_id"] == "reauth" + assert flows[0]["step_id"] == "user" async def test_unload_config_entry(hass: HomeAssistant) -> None: From f8a02c2762b79271798c4cc33ff2788a3d648429 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sun, 18 Apr 2021 00:04:57 +0000 Subject: [PATCH 0334/1317] [ci skip] Translation update --- homeassistant/components/adguard/translations/ca.json | 1 + .../components/coronavirus/translations/ca.json | 3 ++- homeassistant/components/lyric/translations/ca.json | 7 ++++++- homeassistant/components/lyric/translations/et.json | 7 ++++++- homeassistant/components/lyric/translations/ru.json | 7 ++++++- homeassistant/components/mazda/translations/en.json | 9 +++++++++ 6 files changed, 30 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/adguard/translations/ca.json b/homeassistant/components/adguard/translations/ca.json index 0c7057a67eef4..82897df6b2aa2 100644 --- a/homeassistant/components/adguard/translations/ca.json +++ b/homeassistant/components/adguard/translations/ca.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "El servei ja est\u00e0 configurat", "existing_instance_updated": "S'ha actualitzat la configuraci\u00f3 existent.", "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." }, diff --git a/homeassistant/components/coronavirus/translations/ca.json b/homeassistant/components/coronavirus/translations/ca.json index 82e46a209d02a..51fe2d3e2f851 100644 --- a/homeassistant/components/coronavirus/translations/ca.json +++ b/homeassistant/components/coronavirus/translations/ca.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "El servei ja est\u00e0 configurat" + "already_configured": "El servei ja est\u00e0 configurat", + "cannot_connect": "Ha fallat la connexi\u00f3" }, "step": { "user": { diff --git a/homeassistant/components/lyric/translations/ca.json b/homeassistant/components/lyric/translations/ca.json index 195d3d59262a9..3e301a4bf4b65 100644 --- a/homeassistant/components/lyric/translations/ca.json +++ b/homeassistant/components/lyric/translations/ca.json @@ -2,7 +2,8 @@ "config": { "abort": { "authorize_url_timeout": "Temps d'espera esgotat durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.", - "missing_configuration": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3." + "missing_configuration": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3.", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "create_entry": { "default": "Autenticaci\u00f3 exitosa" @@ -10,6 +11,10 @@ "step": { "pick_implementation": { "title": "Selecciona el m\u00e8tode d'autenticaci\u00f3" + }, + "reauth_confirm": { + "description": "La integraci\u00f3 Lyric ha de tornar a autenticar-se amb el teu compte.", + "title": "Reautenticaci\u00f3 de la integraci\u00f3" } } } diff --git a/homeassistant/components/lyric/translations/et.json b/homeassistant/components/lyric/translations/et.json index c7d46e7e9426d..b3e19a93b26a5 100644 --- a/homeassistant/components/lyric/translations/et.json +++ b/homeassistant/components/lyric/translations/et.json @@ -2,7 +2,8 @@ "config": { "abort": { "authorize_url_timeout": "Kinnitus-URLi loomise ajal\u00f5pp", - "missing_configuration": "Komponent pole seadistatud. Palun loe dokumentatsiooni." + "missing_configuration": "Komponent pole seadistatud. Palun loe dokumentatsiooni.", + "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "create_entry": { "default": "Tuvastamine \u00f5nnestus" @@ -10,6 +11,10 @@ "step": { "pick_implementation": { "title": "Vali tuvastusmeetod" + }, + "reauth_confirm": { + "description": "Lyricu sidumine peab konto uuesti tuvastama.", + "title": "Taastuvastamine" } } } diff --git a/homeassistant/components/lyric/translations/ru.json b/homeassistant/components/lyric/translations/ru.json index 8d41a95fd2993..3092d64b03fbb 100644 --- a/homeassistant/components/lyric/translations/ru.json +++ b/homeassistant/components/lyric/translations/ru.json @@ -2,7 +2,8 @@ "config": { "abort": { "authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", - "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438." + "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, "create_entry": { "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." @@ -10,6 +11,10 @@ "step": { "pick_implementation": { "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" + }, + "reauth_confirm": { + "description": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Lyric.", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" } } } diff --git a/homeassistant/components/mazda/translations/en.json b/homeassistant/components/mazda/translations/en.json index b483947aaa0f2..b9e02fb3a412f 100644 --- a/homeassistant/components/mazda/translations/en.json +++ b/homeassistant/components/mazda/translations/en.json @@ -11,6 +11,15 @@ "unknown": "Unexpected error" }, "step": { + "reauth": { + "data": { + "email": "Email", + "password": "Password", + "region": "Region" + }, + "description": "Authentication failed for Mazda Connected Services. Please enter your current credentials.", + "title": "Mazda Connected Services - Authentication Failed" + }, "user": { "data": { "email": "Email", From e06bb3b5e7a63db1449683e64c46e7a8c27c3a41 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 17 Apr 2021 21:44:29 -1000 Subject: [PATCH 0335/1317] Shutdown harmony connection on stop (#49335) --- homeassistant/components/harmony/__init__.py | 34 ++++++++++++++----- .../components/harmony/config_flow.py | 4 +-- homeassistant/components/harmony/const.py | 5 +++ homeassistant/components/harmony/remote.py | 3 +- homeassistant/components/harmony/switch.py | 4 +-- tests/components/harmony/test_remote.py | 9 ++--- 6 files changed, 42 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/harmony/__init__.py b/homeassistant/components/harmony/__init__.py index c273d08758092..cd69bd8017cb7 100644 --- a/homeassistant/components/harmony/__init__.py +++ b/homeassistant/components/harmony/__init__.py @@ -4,13 +4,20 @@ from homeassistant.components.remote import ATTR_ACTIVITY, ATTR_DELAY_SECS from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.const import CONF_HOST, CONF_NAME, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import entity_registry from homeassistant.helpers.dispatcher import async_dispatcher_send -from .const import DOMAIN, HARMONY_OPTIONS_UPDATE, PLATFORMS +from .const import ( + CANCEL_LISTENER, + CANCEL_STOP, + DOMAIN, + HARMONY_DATA, + HARMONY_OPTIONS_UPDATE, + PLATFORMS, +) from .data import HarmonyData _LOGGER = logging.getLogger(__name__) @@ -35,12 +42,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): if not connected_ok: raise ConfigEntryNotReady - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = data - await _migrate_old_unique_ids(hass, entry.entry_id, data) - entry.add_update_listener(_update_listener) + cancel_listener = entry.add_update_listener(_update_listener) + + async def _async_on_stop(event): + await data.shutdown() + + cancel_stop = hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, _async_on_stop) + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = { + HARMONY_DATA: data, + CANCEL_LISTENER: cancel_listener, + CANCEL_STOP: cancel_stop, + } for platform in PLATFORMS: hass.async_create_task( @@ -109,8 +125,10 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): ) # Shutdown a harmony remote for removal - data = hass.data[DOMAIN][entry.entry_id] - await data.shutdown() + entry_data = hass.data[DOMAIN][entry.entry_id] + entry_data[CANCEL_LISTENER]() + entry_data[CANCEL_STOP]() + await entry_data[HARMONY_DATA].shutdown() if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/harmony/config_flow.py b/homeassistant/components/harmony/config_flow.py index a91c1f3b5ca23..2f2f7dc7ce435 100644 --- a/homeassistant/components/harmony/config_flow.py +++ b/homeassistant/components/harmony/config_flow.py @@ -14,7 +14,7 @@ from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import callback -from .const import DOMAIN, PREVIOUS_ACTIVE_ACTIVITY, UNIQUE_ID +from .const import DOMAIN, HARMONY_DATA, PREVIOUS_ACTIVE_ACTIVITY, UNIQUE_ID from .util import ( find_best_name_for_remote, find_unique_id_for_remote, @@ -180,7 +180,7 @@ async def async_step_init(self, user_input=None): if user_input is not None: return self.async_create_entry(title="", data=user_input) - remote = self.hass.data[DOMAIN][self.config_entry.entry_id] + remote = self.hass.data[DOMAIN][self.config_entry.entry_id][HARMONY_DATA] data_schema = vol.Schema( { diff --git a/homeassistant/components/harmony/const.py b/homeassistant/components/harmony/const.py index d7b4d8248ed13..0d8d893a98e27 100644 --- a/homeassistant/components/harmony/const.py +++ b/homeassistant/components/harmony/const.py @@ -10,3 +10,8 @@ ATTR_LAST_ACTIVITY = "last_activity" ATTR_ACTIVITY_STARTING = "activity_starting" PREVIOUS_ACTIVE_ACTIVITY = "Previous Active Activity" + + +HARMONY_DATA = "harmony_data" +CANCEL_LISTENER = "cancel_listener" +CANCEL_STOP = "cancel_stop" diff --git a/homeassistant/components/harmony/remote.py b/homeassistant/components/harmony/remote.py index a09f32ee95e4d..518ff92368c4d 100644 --- a/homeassistant/components/harmony/remote.py +++ b/homeassistant/components/harmony/remote.py @@ -29,6 +29,7 @@ ATTR_DEVICES_LIST, ATTR_LAST_ACTIVITY, DOMAIN, + HARMONY_DATA, HARMONY_OPTIONS_UPDATE, PREVIOUS_ACTIVE_ACTIVITY, SERVICE_CHANGE_CHANNEL, @@ -58,7 +59,7 @@ async def async_setup_entry( ): """Set up the Harmony config entry.""" - data = hass.data[DOMAIN][entry.entry_id] + data = hass.data[DOMAIN][entry.entry_id][HARMONY_DATA] _LOGGER.debug("HarmonyData : %s", data) diff --git a/homeassistant/components/harmony/switch.py b/homeassistant/components/harmony/switch.py index 5aac145e74993..1da128b3d7b78 100644 --- a/homeassistant/components/harmony/switch.py +++ b/homeassistant/components/harmony/switch.py @@ -5,7 +5,7 @@ from homeassistant.const import CONF_NAME from .connection_state import ConnectionStateMixin -from .const import DOMAIN +from .const import DOMAIN, HARMONY_DATA from .data import HarmonyData from .subscriber import HarmonyCallback @@ -14,7 +14,7 @@ async def async_setup_entry(hass, entry, async_add_entities): """Set up harmony activity switches.""" - data = hass.data[DOMAIN][entry.entry_id] + data = hass.data[DOMAIN][entry.entry_id][HARMONY_DATA] activities = data.activities switches = [] diff --git a/tests/components/harmony/test_remote.py b/tests/components/harmony/test_remote.py index 8c4d67e1117da..4222244f00d95 100644 --- a/tests/components/harmony/test_remote.py +++ b/tests/components/harmony/test_remote.py @@ -6,6 +6,7 @@ from homeassistant.components.harmony.const import ( DOMAIN, + HARMONY_DATA, SERVICE_CHANGE_CHANNEL, SERVICE_SYNC, ) @@ -159,7 +160,7 @@ async def test_async_send_command(mock_hc, hass, mock_write_config): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - data = hass.data[DOMAIN][entry.entry_id] + data = hass.data[DOMAIN][entry.entry_id][HARMONY_DATA] send_commands_mock = data._client.send_commands # No device provided @@ -297,7 +298,7 @@ async def test_async_send_command_custom_delay(mock_hc, hass, mock_write_config) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - data = hass.data[DOMAIN][entry.entry_id] + data = hass.data[DOMAIN][entry.entry_id][HARMONY_DATA] send_commands_mock = data._client.send_commands # Tell the TV to play by id @@ -333,7 +334,7 @@ async def test_change_channel(mock_hc, hass, mock_write_config): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - data = hass.data[DOMAIN][entry.entry_id] + data = hass.data[DOMAIN][entry.entry_id][HARMONY_DATA] change_channel_mock = data._client.change_channel # Tell the remote to change channels @@ -358,7 +359,7 @@ async def test_sync(mock_hc, mock_write_config, hass): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - data = hass.data[DOMAIN][entry.entry_id] + data = hass.data[DOMAIN][entry.entry_id][HARMONY_DATA] sync_mock = data._client.sync # Tell the remote to change channels From e10c105058206b89d9a8e18689c64f9542b27df7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 17 Apr 2021 21:46:39 -1000 Subject: [PATCH 0336/1317] Bump aiodiscover to 1.4.0 for dhcp (#49359) - Switches to using dnspython to generate the queries/parse them from the wire --- homeassistant/components/dhcp/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/dhcp/manifest.json b/homeassistant/components/dhcp/manifest.json index e6f181401c32c..47cdc464fadb6 100644 --- a/homeassistant/components/dhcp/manifest.json +++ b/homeassistant/components/dhcp/manifest.json @@ -2,7 +2,7 @@ "domain": "dhcp", "name": "DHCP Discovery", "documentation": "https://www.home-assistant.io/integrations/dhcp", - "requirements": ["scapy==2.4.4", "aiodiscover==1.3.4"], + "requirements": ["scapy==2.4.4", "aiodiscover==1.4.0"], "codeowners": ["@bdraco"], "quality_scale": "internal", "iot_class": "local_push" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7516c2e9981bb..2f6700ff2efc0 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,6 +1,6 @@ PyJWT==1.7.1 PyNaCl==1.3.0 -aiodiscover==1.3.4 +aiodiscover==1.4.0 aiohttp==3.7.4.post0 aiohttp_cors==0.7.0 astral==2.2 diff --git a/requirements_all.txt b/requirements_all.txt index de18a479d4417..9384eca20f23d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -147,7 +147,7 @@ aioazuredevops==1.3.5 aiobotocore==1.2.2 # homeassistant.components.dhcp -aiodiscover==1.3.4 +aiodiscover==1.4.0 # homeassistant.components.dnsip # homeassistant.components.minecraft_server diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c861ef69fb0fd..5746359c6ac94 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -87,7 +87,7 @@ aioazuredevops==1.3.5 aiobotocore==1.2.2 # homeassistant.components.dhcp -aiodiscover==1.3.4 +aiodiscover==1.4.0 # homeassistant.components.dnsip # homeassistant.components.minecraft_server From 252bcabbea2f8e1d0f09c16de06241f203d7129a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 17 Apr 2021 21:48:02 -1000 Subject: [PATCH 0337/1317] Fix exception in roomba discovery when the device does not respond on the first try (#49360) --- .../components/roomba/config_flow.py | 5 +- tests/components/roomba/test_config_flow.py | 64 ++++++++++++++++++- 2 files changed, 65 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/roomba/config_flow.py b/homeassistant/components/roomba/config_flow.py index 5603d9d9d7e0b..11bd7fe275826 100644 --- a/homeassistant/components/roomba/config_flow.py +++ b/homeassistant/components/roomba/config_flow.py @@ -328,9 +328,8 @@ async def _async_discover_roombas(hass, host): discovery = _async_get_roomba_discovery() try: if host: - discovered = [ - await hass.async_add_executor_job(discovery.get, host) - ] + device = await hass.async_add_executor_job(discovery.get, host) + discovered = [device] if device else [] else: discovered = await hass.async_add_executor_job(discovery.get_all) except OSError: diff --git a/tests/components/roomba/test_config_flow.py b/tests/components/roomba/test_config_flow.py index e125e9bd5ba17..ffea0c3140c07 100644 --- a/tests/components/roomba/test_config_flow.py +++ b/tests/components/roomba/test_config_flow.py @@ -687,7 +687,7 @@ async def test_dhcp_discovery_and_roomba_discovery_finds(hass, discovery_data): @pytest.mark.parametrize("discovery_data", DHCP_DISCOVERY_DEVICES_WITHOUT_MATCHING_IP) async def test_dhcp_discovery_falls_back_to_manual(hass, discovery_data): - """Test we can process the discovery from dhcp but roomba discovery cannot find the device.""" + """Test we can process the discovery from dhcp but roomba discovery cannot find the specific device.""" await setup.async_setup_component(hass, "persistent_notification", {}) mocked_roomba = _create_mocked_roomba( @@ -755,6 +755,68 @@ async def test_dhcp_discovery_falls_back_to_manual(hass, discovery_data): assert len(mock_setup_entry.mock_calls) == 1 +@pytest.mark.parametrize("discovery_data", DHCP_DISCOVERY_DEVICES_WITHOUT_MATCHING_IP) +async def test_dhcp_discovery_no_devices_falls_back_to_manual(hass, discovery_data): + """Test we can process the discovery from dhcp but roomba discovery cannot find any devices.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + mocked_roomba = _create_mocked_roomba( + roomba_connected=True, + master_state={"state": {"reported": {"name": "myroomba"}}}, + ) + + with patch( + "homeassistant.components.roomba.config_flow.RoombaDiscovery", + _mocked_no_devices_found_discovery, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=discovery_data, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] is None + assert result["step_id"] == "manual" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: MOCK_IP, CONF_BLID: "blid"}, + ) + await hass.async_block_till_done() + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] is None + + with patch( + "homeassistant.components.roomba.config_flow.Roomba", + return_value=mocked_roomba, + ), patch( + "homeassistant.components.roomba.config_flow.RoombaPassword", + _mocked_getpassword, + ), patch( + "homeassistant.components.roomba.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["title"] == "myroomba" + assert result3["result"].unique_id == "BLID" + assert result3["data"] == { + CONF_BLID: "BLID", + CONF_CONTINUOUS: True, + CONF_DELAY: 1, + CONF_HOST: MOCK_IP, + CONF_PASSWORD: "password", + } + assert len(mock_setup_entry.mock_calls) == 1 + + async def test_dhcp_discovery_with_ignored(hass): """Test ignored entries do not break checking for existing entries.""" await setup.async_setup_component(hass, "persistent_notification", {}) From b2c33c13732b00b56b4efe89e66c9f05130ade12 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 17 Apr 2021 22:04:45 -1000 Subject: [PATCH 0338/1317] Only fetch the local ip once per run (#49336) Wrap get_local_ip in lru_cache --- homeassistant/util/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index 79ceeada0c28a..c684d14d27664 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -4,7 +4,7 @@ import asyncio from datetime import datetime, timedelta import enum -from functools import wraps +from functools import lru_cache, wraps import random import re import socket @@ -129,6 +129,7 @@ def ensure_unique_string( # Taken from: http://stackoverflow.com/a/11735897 +@lru_cache(maxsize=None) def get_local_ip() -> str: """Try to determine the local IP address of the machine.""" try: From afd79a675cfbccdc7983ee6afd0518556c1ae848 Mon Sep 17 00:00:00 2001 From: Brett Date: Sun, 18 Apr 2021 18:36:34 +1000 Subject: [PATCH 0339/1317] Add set_myzone service to Advantage Air (#46934) * Add set_myzone service requested on forums * Add MyZone binary sensor for climate zones * Fixed Black on binary_sensor.py * Add the new entity * Fix spelling * Test myZone value * MyZone Binary Sensor test * Fixed new binary sensor tests * Fix removed dependancy * Correct fixture * Update homeassistant/components/advantage_air/binary_sensor.py Co-authored-by: Philip Allgaier * Updated services.yaml to use target Co-authored-by: Philip Allgaier --- .../components/advantage_air/binary_sensor.py | 27 +++++++++++ .../components/advantage_air/climate.py | 15 ++++++ .../components/advantage_air/services.yaml | 15 ++++-- .../advantage_air/test_binary_sensor.py | 48 +++++++++++++++++++ .../components/advantage_air/test_climate.py | 17 +++++++ .../fixtures/advantage_air/getSystemData.json | 12 ++--- 6 files changed, 125 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/advantage_air/binary_sensor.py b/homeassistant/components/advantage_air/binary_sensor.py index 50a7ef83895d1..f7b295c963471 100644 --- a/homeassistant/components/advantage_air/binary_sensor.py +++ b/homeassistant/components/advantage_air/binary_sensor.py @@ -24,6 +24,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): # Only add motion sensor when motion is enabled if zone["motionConfig"] >= 2: entities.append(AdvantageAirZoneMotion(instance, ac_key, zone_key)) + # Only add MyZone if it is available + if zone["type"] != 0: + entities.append(AdvantageAirZoneMyZone(instance, ac_key, zone_key)) async_add_entities(entities) @@ -73,3 +76,27 @@ def device_class(self): def is_on(self): """Return if motion is detect.""" return self._zone["motion"] + + +class AdvantageAirZoneMyZone(AdvantageAirEntity, BinarySensorEntity): + """Advantage Air Zone MyZone.""" + + @property + def name(self): + """Return the name.""" + return f'{self._zone["name"]} MyZone' + + @property + def unique_id(self): + """Return a unique id.""" + return f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}-{self.zone_key}-myzone' + + @property + def is_on(self): + """Return if this zone is the myZone.""" + return self._zone["number"] == self._ac["myZone"] + + @property + def entity_registry_enabled_default(self): + """Return false to disable this entity by default.""" + return False diff --git a/homeassistant/components/advantage_air/climate.py b/homeassistant/components/advantage_air/climate.py index d3c4e89781933..ca25edbda4ffa 100644 --- a/homeassistant/components/advantage_air/climate.py +++ b/homeassistant/components/advantage_air/climate.py @@ -15,6 +15,7 @@ SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS +from homeassistant.helpers import entity_platform from .const import ( ADVANTAGE_AIR_STATE_CLOSE, @@ -49,6 +50,7 @@ HVAC_MODE_FAN_ONLY, HVAC_MODE_DRY, ] +ADVANTAGE_AIR_SERVICE_SET_MYZONE = "set_myzone" ZONE_HVAC_MODES = [HVAC_MODE_OFF, HVAC_MODE_FAN_ONLY] PARALLEL_UPDATES = 0 @@ -68,6 +70,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entities.append(AdvantageAirZone(instance, ac_key, zone_key)) async_add_entities(entities) + platform = entity_platform.current_platform.get() + platform.async_register_entity_service( + ADVANTAGE_AIR_SERVICE_SET_MYZONE, + {}, + "set_myzone", + ) + class AdvantageAirClimateEntity(AdvantageAirEntity, ClimateEntity): """AdvantageAir Climate class.""" @@ -233,3 +242,9 @@ async def async_set_temperature(self, **kwargs): await self.async_change( {self.ac_key: {"zones": {self.zone_key: {"setTemp": temp}}}} ) + + async def set_myzone(self, **kwargs): + """Set this zone as the 'MyZone'.""" + await self.async_change( + {self.ac_key: {"info": {"myZone": self._zone["number"]}}} + ) diff --git a/homeassistant/components/advantage_air/services.yaml b/homeassistant/components/advantage_air/services.yaml index aa222577b2f44..e70208c4ac1f4 100644 --- a/homeassistant/components/advantage_air/services.yaml +++ b/homeassistant/components/advantage_air/services.yaml @@ -1,9 +1,18 @@ set_time_to: + name: Set Time To description: Control timers to turn the system on or off after a set number of minutes + target: + entity: + integration: advantage_air + domain: sensor fields: - entity_id: - description: Time To sensor entity - example: "sensor.ac_time_to_on" minutes: description: Minutes until action example: "60" +set_myzone: + name: Set MyZone + description: Change which zone is set as the reference for temperature control + target: + entity: + integration: advantage_air + domain: climate diff --git a/tests/components/advantage_air/test_binary_sensor.py b/tests/components/advantage_air/test_binary_sensor.py index dee4b9fd99a31..275b5fc4e5295 100644 --- a/tests/components/advantage_air/test_binary_sensor.py +++ b/tests/components/advantage_air/test_binary_sensor.py @@ -1,8 +1,12 @@ """Test the Advantage Air Binary Sensor Platform.""" +from datetime import timedelta +from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers import entity_registry as er +from homeassistant.util import dt +from tests.common import async_fire_time_changed from tests.components.advantage_air import ( TEST_SET_RESPONSE, TEST_SET_URL, @@ -68,3 +72,47 @@ async def test_binary_sensor_async_setup_entry(hass, aioclient_mock): entry = registry.async_get(entity_id) assert entry assert entry.unique_id == "uniqueid-ac1-z02-motion" + + # Test First MyZone Sensor (disabled by default) + entity_id = "binary_sensor.zone_open_with_sensor_myzone" + + assert not hass.states.get(entity_id) + + registry.async_update_entity(entity_id=entity_id, disabled_by=None) + await hass.async_block_till_done() + + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), + ) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_ON + + entry = registry.async_get(entity_id) + assert entry + assert entry.unique_id == "uniqueid-ac1-z01-myzone" + + # Test Second Motion Sensor (disabled by default) + entity_id = "binary_sensor.zone_closed_with_sensor_myzone" + + assert not hass.states.get(entity_id) + + registry.async_update_entity(entity_id=entity_id, disabled_by=None) + await hass.async_block_till_done() + + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), + ) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_OFF + + entry = registry.async_get(entity_id) + assert entry + assert entry.unique_id == "uniqueid-ac1-z02-myzone" diff --git a/tests/components/advantage_air/test_climate.py b/tests/components/advantage_air/test_climate.py index ea0cf02546211..4374057bbb212 100644 --- a/tests/components/advantage_air/test_climate.py +++ b/tests/components/advantage_air/test_climate.py @@ -3,12 +3,14 @@ from json import loads from homeassistant.components.advantage_air.climate import ( + ADVANTAGE_AIR_SERVICE_SET_MYZONE, HASS_FAN_MODES, HASS_HVAC_MODES, ) from homeassistant.components.advantage_air.const import ( ADVANTAGE_AIR_STATE_OFF, ADVANTAGE_AIR_STATE_ON, + DOMAIN as ADVANTAGE_AIR_DOMAIN, ) from homeassistant.components.climate.const import ( ATTR_FAN_MODE, @@ -170,6 +172,21 @@ async def test_climate_async_setup_entry(hass, aioclient_mock): assert aioclient_mock.mock_calls[-1][0] == "GET" assert aioclient_mock.mock_calls[-1][1].path == "/getSystemData" + # Test set_myair service + await hass.services.async_call( + ADVANTAGE_AIR_DOMAIN, + ADVANTAGE_AIR_SERVICE_SET_MYZONE, + {ATTR_ENTITY_ID: [entity_id]}, + blocking=True, + ) + assert len(aioclient_mock.mock_calls) == 17 + assert aioclient_mock.mock_calls[-2][0] == "GET" + assert aioclient_mock.mock_calls[-2][1].path == "/setAircon" + data = loads(aioclient_mock.mock_calls[-2][1].query["json"]) + assert data["ac1"]["info"]["myZone"] == 1 + assert aioclient_mock.mock_calls[-1][0] == "GET" + assert aioclient_mock.mock_calls[-1][1].path == "/getSystemData" + async def test_climate_async_failed_update(hass, aioclient_mock): """Test climate change failure.""" diff --git a/tests/fixtures/advantage_air/getSystemData.json b/tests/fixtures/advantage_air/getSystemData.json index 65dbf8d672b59..19dda28fec1fb 100644 --- a/tests/fixtures/advantage_air/getSystemData.json +++ b/tests/fixtures/advantage_air/getSystemData.json @@ -9,7 +9,7 @@ "filterCleanStatus": 0, "freshAirStatus": "off", "mode": "vent", - "myZone": 0, + "myZone": 1, "name": "AC One", "setTemp": 24, "state": "on" @@ -38,7 +38,7 @@ "motion": 0, "motionConfig": 2, "name": "Zone closed with Sensor", - "number": 1, + "number": 2, "rssi": 10, "setTemp": 24, "state": "close", @@ -53,7 +53,7 @@ "motion": 1, "motionConfig": 1, "name": "Zone 3", - "number": 1, + "number": 3, "rssi": 25, "setTemp": 24, "state": "close", @@ -68,7 +68,7 @@ "motion": 1, "motionConfig": 1, "name": "Zone 4", - "number": 1, + "number": 4, "rssi": 75, "setTemp": 24, "state": "close", @@ -80,7 +80,7 @@ "maxDamper": 100, "measuredTemp": 25, "minDamper": 0, - "motion": 1, + "motion": 5, "motionConfig": 1, "name": "Zone 5", "number": 1, @@ -130,7 +130,7 @@ "motion": 0, "motionConfig": 0, "name": "Zone closed without sensor", - "number": 1, + "number": 2, "rssi": 0, "setTemp": 24, "state": "close", From 04a0ca14e0f15e6fde8f6b6e346755c343517ef2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 18 Apr 2021 08:55:51 -1000 Subject: [PATCH 0340/1317] Ensure shutdown does not deadlock (#49282) --- homeassistant/core.py | 2 +- homeassistant/runner.py | 11 +++- homeassistant/util/executor.py | 108 +++++++++++++++++++++++++++++++++ homeassistant/util/thread.py | 35 ++++++++++- tests/test_runner.py | 39 ++++++++++++ tests/util/test_executor.py | 91 +++++++++++++++++++++++++++ tests/util/test_thread.py | 56 +++++++++++++++++ 7 files changed, 335 insertions(+), 7 deletions(-) create mode 100644 homeassistant/util/executor.py create mode 100644 tests/test_runner.py create mode 100644 tests/util/test_executor.py diff --git a/homeassistant/core.py b/homeassistant/core.py index 3b7fad883da7b..1356de0b57281 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -87,7 +87,7 @@ from homeassistant.config_entries import ConfigEntries -STAGE_1_SHUTDOWN_TIMEOUT = 120 +STAGE_1_SHUTDOWN_TIMEOUT = 100 STAGE_2_SHUTDOWN_TIMEOUT = 60 STAGE_3_SHUTDOWN_TIMEOUT = 30 diff --git a/homeassistant/runner.py b/homeassistant/runner.py index 5adddb5f6efdd..86bebecb7b15f 100644 --- a/homeassistant/runner.py +++ b/homeassistant/runner.py @@ -2,14 +2,16 @@ from __future__ import annotations import asyncio -from concurrent.futures import ThreadPoolExecutor import dataclasses import logging +import threading from typing import Any from homeassistant import bootstrap from homeassistant.core import callback from homeassistant.helpers.frame import warn_use +from homeassistant.util.executor import InterruptibleThreadPoolExecutor +from homeassistant.util.thread import deadlock_safe_shutdown # mypy: disallow-any-generics @@ -64,7 +66,7 @@ def new_event_loop(self) -> asyncio.AbstractEventLoop: if self.debug: loop.set_debug(True) - executor = ThreadPoolExecutor( + executor = InterruptibleThreadPoolExecutor( thread_name_prefix="SyncWorker", max_workers=MAX_EXECUTOR_WORKERS ) loop.set_default_executor(executor) @@ -76,7 +78,7 @@ def new_event_loop(self) -> asyncio.AbstractEventLoop: orig_close = loop.close def close() -> None: - executor.shutdown(wait=True) + executor.logged_shutdown() orig_close() loop.close = close # type: ignore @@ -104,6 +106,9 @@ async def setup_and_run_hass(runtime_config: RuntimeConfig) -> int: if hass is None: return 1 + # threading._shutdown can deadlock forever + threading._shutdown = deadlock_safe_shutdown # type: ignore[attr-defined] # pylint: disable=protected-access + return await hass.async_run() diff --git a/homeassistant/util/executor.py b/homeassistant/util/executor.py new file mode 100644 index 0000000000000..6765fc5d8ae4a --- /dev/null +++ b/homeassistant/util/executor.py @@ -0,0 +1,108 @@ +"""Executor util helpers.""" +from __future__ import annotations + +from concurrent.futures import ThreadPoolExecutor +import logging +import queue +import sys +from threading import Thread +import time +import traceback + +from homeassistant.util.thread import async_raise + +_LOGGER = logging.getLogger(__name__) + +MAX_LOG_ATTEMPTS = 2 + +_JOIN_ATTEMPTS = 10 + +EXECUTOR_SHUTDOWN_TIMEOUT = 10 + + +def _log_thread_running_at_shutdown(name: str, ident: int) -> None: + """Log the stack of a thread that was still running at shutdown.""" + frames = sys._current_frames() # pylint: disable=protected-access + stack = frames.get(ident) + formatted_stack = traceback.format_stack(stack) + _LOGGER.warning( + "Thread[%s] is still running at shutdown: %s", + name, + "".join(formatted_stack).strip(), + ) + + +def join_or_interrupt_threads( + threads: set[Thread], timeout: float, log: bool +) -> set[Thread]: + """Attempt to join or interrupt a set of threads.""" + joined = set() + timeout_per_thread = timeout / len(threads) + + for thread in threads: + thread.join(timeout=timeout_per_thread) + + if not thread.is_alive() or thread.ident is None: + joined.add(thread) + continue + + if log: + _log_thread_running_at_shutdown(thread.name, thread.ident) + + async_raise(thread.ident, SystemExit) + + return joined + + +class InterruptibleThreadPoolExecutor(ThreadPoolExecutor): + """A ThreadPoolExecutor instance that will not deadlock on shutdown.""" + + def logged_shutdown(self) -> None: + """Shutdown backport from cpython 3.9 with interrupt support added.""" + with self._shutdown_lock: # type: ignore[attr-defined] + self._shutdown = True + # Drain all work items from the queue, and then cancel their + # associated futures. + while True: + try: + work_item = self._work_queue.get_nowait() + except queue.Empty: + break + if work_item is not None: + work_item.future.cancel() + # Send a wake-up to prevent threads calling + # _work_queue.get(block=True) from permanently blocking. + self._work_queue.put(None) + + # The above code is backported from python 3.9 + # + # For maintainability join_threads_or_timeout is + # a separate function since it is not a backport from + # cpython itself + # + self.join_threads_or_timeout() + + def join_threads_or_timeout(self) -> None: + """Join threads or timeout.""" + remaining_threads = set(self._threads) # type: ignore[attr-defined] + start_time = time.monotonic() + timeout_remaining: float = EXECUTOR_SHUTDOWN_TIMEOUT + attempt = 0 + + while True: + if not remaining_threads: + return + + attempt += 1 + + remaining_threads -= join_or_interrupt_threads( + remaining_threads, + timeout_remaining / _JOIN_ATTEMPTS, + attempt <= MAX_LOG_ATTEMPTS, + ) + + timeout_remaining = EXECUTOR_SHUTDOWN_TIMEOUT - ( + time.monotonic() - start_time + ) + if timeout_remaining <= 0: + return diff --git a/homeassistant/util/thread.py b/homeassistant/util/thread.py index 7743e1d159c2c..0d600486f2fde 100644 --- a/homeassistant/util/thread.py +++ b/homeassistant/util/thread.py @@ -1,16 +1,45 @@ """Threading util helpers.""" import ctypes import inspect +import logging import threading from typing import Any +THREADING_SHUTDOWN_TIMEOUT = 10 -def _async_raise(tid: int, exctype: Any) -> None: +_LOGGER = logging.getLogger(__name__) + + +def deadlock_safe_shutdown() -> None: + """Shutdown that will not deadlock.""" + # threading._shutdown can deadlock forever + # see https://github.com/justengel/continuous_threading#shutdown-update + # for additional detail + remaining_threads = [ + thread + for thread in threading.enumerate() + if thread is not threading.main_thread() + and not thread.daemon + and thread.is_alive() + ] + + if not remaining_threads: + return + + timeout_per_thread = THREADING_SHUTDOWN_TIMEOUT / len(remaining_threads) + for thread in remaining_threads: + try: + thread.join(timeout_per_thread) + except Exception as err: # pylint: disable=broad-except + _LOGGER.warning("Failed to join thread: %s", err) + + +def async_raise(tid: int, exctype: Any) -> None: """Raise an exception in the threads with id tid.""" if not inspect.isclass(exctype): raise TypeError("Only types can be raised (not instances)") - c_tid = ctypes.c_long(tid) + c_tid = ctypes.c_ulong(tid) # changed in python 3.7+ res = ctypes.pythonapi.PyThreadState_SetAsyncExc(c_tid, ctypes.py_object(exctype)) if res == 1: @@ -33,4 +62,4 @@ class ThreadWithException(threading.Thread): def raise_exc(self, exctype: Any) -> None: """Raise the given exception type in the context of this thread.""" assert self.ident - _async_raise(self.ident, exctype) + async_raise(self.ident, exctype) diff --git a/tests/test_runner.py b/tests/test_runner.py new file mode 100644 index 0000000000000..7bbe96dd077a6 --- /dev/null +++ b/tests/test_runner.py @@ -0,0 +1,39 @@ +"""Test the runner.""" + +import threading +from unittest.mock import patch + +from homeassistant import core, runner +from homeassistant.util import executor, thread + +# https://github.com/home-assistant/supervisor/blob/main/supervisor/docker/homeassistant.py +SUPERVISOR_HARD_TIMEOUT = 220 + +TIMEOUT_SAFETY_MARGIN = 10 + + +async def test_cumulative_shutdown_timeout_less_than_supervisor(): + """Verify the cumulative shutdown timeout is at least 10s less than the supervisor.""" + assert ( + core.STAGE_1_SHUTDOWN_TIMEOUT + + core.STAGE_2_SHUTDOWN_TIMEOUT + + core.STAGE_3_SHUTDOWN_TIMEOUT + + executor.EXECUTOR_SHUTDOWN_TIMEOUT + + thread.THREADING_SHUTDOWN_TIMEOUT + + TIMEOUT_SAFETY_MARGIN + <= SUPERVISOR_HARD_TIMEOUT + ) + + +async def test_setup_and_run_hass(hass, tmpdir): + """Test we can setup and run.""" + test_dir = tmpdir.mkdir("config") + default_config = runner.RuntimeConfig(test_dir) + + with patch("homeassistant.bootstrap.async_setup_hass", return_value=hass), patch( + "threading._shutdown" + ), patch("homeassistant.core.HomeAssistant.async_run") as mock_run: + await runner.setup_and_run_hass(default_config) + assert threading._shutdown == thread.deadlock_safe_shutdown + + assert mock_run.called diff --git a/tests/util/test_executor.py b/tests/util/test_executor.py new file mode 100644 index 0000000000000..911145ecc4eb8 --- /dev/null +++ b/tests/util/test_executor.py @@ -0,0 +1,91 @@ +"""Test Home Assistant executor util.""" + +import concurrent.futures +import time +from unittest.mock import patch + +import pytest + +from homeassistant.util import executor +from homeassistant.util.executor import InterruptibleThreadPoolExecutor + + +async def test_executor_shutdown_can_interrupt_threads(caplog): + """Test that the executor shutdown can interrupt threads.""" + + iexecutor = InterruptibleThreadPoolExecutor() + + def _loop_sleep_in_executor(): + while True: + time.sleep(0.1) + + sleep_futures = [] + + for _ in range(100): + sleep_futures.append(iexecutor.submit(_loop_sleep_in_executor)) + + iexecutor.logged_shutdown() + + for future in sleep_futures: + with pytest.raises((concurrent.futures.CancelledError, SystemExit)): + future.result() + + assert "is still running at shutdown" in caplog.text + assert "time.sleep(0.1)" in caplog.text + + +async def test_executor_shutdown_only_logs_max_attempts(caplog): + """Test that the executor shutdown will only log max attempts.""" + + iexecutor = InterruptibleThreadPoolExecutor() + + def _loop_sleep_in_executor(): + time.sleep(0.2) + + iexecutor.submit(_loop_sleep_in_executor) + + with patch.object(executor, "EXECUTOR_SHUTDOWN_TIMEOUT", 0.3): + iexecutor.logged_shutdown() + + assert "time.sleep(0.2)" in caplog.text + assert ( + caplog.text.count("is still running at shutdown") == executor.MAX_LOG_ATTEMPTS + ) + iexecutor.logged_shutdown() + + +async def test_executor_shutdown_does_not_log_shutdown_on_first_attempt(caplog): + """Test that the executor shutdown does not log on first attempt.""" + + iexecutor = InterruptibleThreadPoolExecutor() + + def _do_nothing(): + return + + for _ in range(5): + iexecutor.submit(_do_nothing) + + iexecutor.logged_shutdown() + + assert "is still running at shutdown" not in caplog.text + + +async def test_overall_timeout_reached(caplog): + """Test that shutdown moves on when the overall timeout is reached.""" + + iexecutor = InterruptibleThreadPoolExecutor() + + def _loop_sleep_in_executor(): + time.sleep(1) + + for _ in range(6): + iexecutor.submit(_loop_sleep_in_executor) + + start = time.monotonic() + with patch.object(executor, "EXECUTOR_SHUTDOWN_TIMEOUT", 0.5): + iexecutor.logged_shutdown() + finish = time.monotonic() + + assert finish - start < 1 + + iexecutor.logged_shutdown() diff --git a/tests/util/test_thread.py b/tests/util/test_thread.py index d5f05f5c93ee1..e33cde0c51b0b 100644 --- a/tests/util/test_thread.py +++ b/tests/util/test_thread.py @@ -1,9 +1,11 @@ """Test Home Assistant thread utils.""" import asyncio +from unittest.mock import Mock, patch import pytest +from homeassistant.util import thread from homeassistant.util.async_ import run_callback_threadsafe from homeassistant.util.thread import ThreadWithException @@ -53,3 +55,57 @@ def _do_nothing(*_): class _EmptyClass: """An empty class.""" + + +async def test_deadlock_safe_shutdown_no_threads(): + """Test we can shutdown without deadlock without any threads to join.""" + + dead_thread_mock = Mock( + join=Mock(), daemon=False, is_alive=Mock(return_value=False) + ) + daemon_thread_mock = Mock( + join=Mock(), daemon=True, is_alive=Mock(return_value=True) + ) + mock_threads = [ + dead_thread_mock, + daemon_thread_mock, + ] + + with patch("homeassistant.util.threading.enumerate", return_value=mock_threads): + thread.deadlock_safe_shutdown() + + assert not dead_thread_mock.join.called + assert not daemon_thread_mock.join.called + + +async def test_deadlock_safe_shutdown(): + """Test we can shutdown without deadlock.""" + + normal_thread_mock = Mock( + join=Mock(), daemon=False, is_alive=Mock(return_value=True) + ) + dead_thread_mock = Mock( + join=Mock(), daemon=False, is_alive=Mock(return_value=False) + ) + daemon_thread_mock = Mock( + join=Mock(), daemon=True, is_alive=Mock(return_value=True) + ) + exception_thread_mock = Mock( + join=Mock(side_effect=Exception), daemon=False, is_alive=Mock(return_value=True) + ) + mock_threads = [ + normal_thread_mock, + dead_thread_mock, + daemon_thread_mock, + exception_thread_mock, + ] + + with patch("homeassistant.util.threading.enumerate", return_value=mock_threads): + thread.deadlock_safe_shutdown() + + expected_timeout = thread.THREADING_SHUTDOWN_TIMEOUT / 2 + + assert normal_thread_mock.join.call_args[0] == (expected_timeout,) + assert not dead_thread_mock.join.called + assert not daemon_thread_mock.join.called + assert exception_thread_mock.join.call_args[0] == (expected_timeout,) From 080c89c76188b4666cd32babe02645d08daff603 Mon Sep 17 00:00:00 2001 From: Brent Petit Date: Sun, 18 Apr 2021 15:35:03 -0500 Subject: [PATCH 0341/1317] Only set fan state in ecobee set_fan_mode service (#48086) --- homeassistant/components/ecobee/climate.py | 5 +---- homeassistant/components/ecobee/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/ecobee/test_climate.py | 4 ++-- 5 files changed, 6 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index 47c2ff969ecf7..dd29918ec186b 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -644,14 +644,11 @@ def set_fan_mode(self, fan_mode): _LOGGER.error(error) return - cool_temp = self.thermostat["runtime"]["desiredCool"] / 10.0 - heat_temp = self.thermostat["runtime"]["desiredHeat"] / 10.0 self.data.ecobee.set_fan_mode( self.thermostat_index, fan_mode, - cool_temp, - heat_temp, self.hold_preference(), + holdHours=self.hold_hours(), ) _LOGGER.info("Setting fan mode to: %s", fan_mode) diff --git a/homeassistant/components/ecobee/manifest.json b/homeassistant/components/ecobee/manifest.json index f27cb8e425eea..c1d11a8ee7b04 100644 --- a/homeassistant/components/ecobee/manifest.json +++ b/homeassistant/components/ecobee/manifest.json @@ -3,7 +3,7 @@ "name": "ecobee", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ecobee", - "requirements": ["python-ecobee-api==0.2.10"], + "requirements": ["python-ecobee-api==0.2.11"], "codeowners": ["@marthoc"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 9384eca20f23d..18816b89c9393 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1771,7 +1771,7 @@ python-clementine-remote==1.0.1 python-digitalocean==1.13.2 # homeassistant.components.ecobee -python-ecobee-api==0.2.10 +python-ecobee-api==0.2.11 # homeassistant.components.eq3btsmart # python-eq3bt==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5746359c6ac94..650655c28d707 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -962,7 +962,7 @@ pysqueezebox==0.5.5 pysyncthru==0.7.0 # homeassistant.components.ecobee -python-ecobee-api==0.2.10 +python-ecobee-api==0.2.11 # homeassistant.components.darksky python-forecastio==1.4.0 diff --git a/tests/components/ecobee/test_climate.py b/tests/components/ecobee/test_climate.py index 270c6cfec1527..86f9926b75696 100644 --- a/tests/components/ecobee/test_climate.py +++ b/tests/components/ecobee/test_climate.py @@ -320,7 +320,7 @@ async def test_set_fan_mode_on(thermostat, data): data.reset_mock() thermostat.set_fan_mode("on") data.ecobee.set_fan_mode.assert_has_calls( - [mock.call(1, "on", 20, 40, "nextTransition")] + [mock.call(1, "on", "nextTransition", holdHours=None)] ) @@ -329,5 +329,5 @@ async def test_set_fan_mode_auto(thermostat, data): data.reset_mock() thermostat.set_fan_mode("auto") data.ecobee.set_fan_mode.assert_has_calls( - [mock.call(1, "auto", 20, 40, "nextTransition")] + [mock.call(1, "auto", "nextTransition", holdHours=None)] ) From 6e911ba19f9bb9b34cfd2981786540a6d485d4f9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 18 Apr 2021 10:46:46 -1000 Subject: [PATCH 0342/1317] Shutdown bond bpup and skip polling after the stop event (#49326) --- homeassistant/components/bond/__init__.py | 14 ++++++++++-- homeassistant/components/bond/entity.py | 7 +++++- tests/components/bond/test_entity.py | 26 ++++++++++++++++++++++- 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bond/__init__.py b/homeassistant/components/bond/__init__.py index 800e130251742..c14c50d7c5294 100644 --- a/homeassistant/components/bond/__init__.py +++ b/homeassistant/components/bond/__init__.py @@ -6,8 +6,8 @@ from bond_api import Bond, BPUPSubscriptions, start_bpup from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST -from homeassistant.core import HomeAssistant, callback +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, EVENT_HOMEASSISTANT_STOP +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -18,6 +18,7 @@ PLATFORMS = ["cover", "fan", "light", "switch"] _API_TIMEOUT = SLOW_UPDATE_WARNING - 1 +_STOP_CANCEL = "stop_cancel" async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -41,11 +42,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: bpup_subs = BPUPSubscriptions() stop_bpup = await start_bpup(host, bpup_subs) + @callback + def _async_stop_event(event: Event) -> None: + stop_bpup() + + stop_event_cancel = hass.bus.async_listen( + EVENT_HOMEASSISTANT_STOP, _async_stop_event + ) hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { HUB: hub, BPUP_SUBS: bpup_subs, BPUP_STOP: stop_bpup, + _STOP_CANCEL: stop_event_cancel, } if not entry.unique_id: @@ -86,6 +95,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) data = hass.data[DOMAIN][entry.entry_id] + data[_STOP_CANCEL]() if BPUP_STOP in data: data[BPUP_STOP]() diff --git a/homeassistant/components/bond/entity.py b/homeassistant/components/bond/entity.py index a676d99e9ad9d..65bb79e42f3f8 100644 --- a/homeassistant/components/bond/entity.py +++ b/homeassistant/components/bond/entity.py @@ -104,7 +104,12 @@ async def async_update(self) -> None: async def _async_update_if_bpup_not_alive(self, *_: Any) -> None: """Fetch via the API if BPUP is not alive.""" - if self._bpup_subs.alive and self._initialized and self._available: + if ( + self.hass.is_stopping + or self._bpup_subs.alive + and self._initialized + and self._available + ): return assert self._update_lock is not None diff --git a/tests/components/bond/test_entity.py b/tests/components/bond/test_entity.py index e0a3f156ff51a..122e9c2f04ee4 100644 --- a/tests/components/bond/test_entity.py +++ b/tests/components/bond/test_entity.py @@ -8,7 +8,8 @@ from homeassistant import core from homeassistant.components import fan from homeassistant.components.fan import DOMAIN as FAN_DOMAIN -from homeassistant.const import STATE_ON, STATE_UNAVAILABLE +from homeassistant.const import EVENT_HOMEASSISTANT_STOP, STATE_ON, STATE_UNAVAILABLE +from homeassistant.core import CoreState from homeassistant.util import utcnow from .common import patch_bond_device_state, setup_platform @@ -167,3 +168,26 @@ async def test_polling_fails_and_recovers(hass: core.HomeAssistant): state = hass.states.get("fan.name_1") assert state.state == STATE_ON assert state.attributes[fan.ATTR_PERCENTAGE] == 33 + + +async def test_polling_stops_at_the_stop_event(hass: core.HomeAssistant): + """Test that polling stops at the stop event.""" + await setup_platform( + hass, FAN_DOMAIN, ceiling_fan("name-1"), bond_device_id="test-device-id" + ) + + with patch_bond_device_state(side_effect=asyncio.TimeoutError): + async_fire_time_changed(hass, utcnow() + timedelta(seconds=230)) + await hass.async_block_till_done() + + assert hass.states.get("fan.name_1").state == STATE_UNAVAILABLE + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + hass.state = CoreState.stopping + await hass.async_block_till_done() + + with patch_bond_device_state(return_value={"power": 1, "speed": 1}): + async_fire_time_changed(hass, utcnow() + timedelta(seconds=230)) + await hass.async_block_till_done() + + assert hass.states.get("fan.name_1").state == STATE_UNAVAILABLE From a050c8827b49f00bceb6715980f268a87a052c61 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 19 Apr 2021 00:30:58 +0200 Subject: [PATCH 0343/1317] Add battery sensor to fritzbox smart home devices (#49374) --- homeassistant/components/fritzbox/sensor.py | 56 ++++++++++++++++++++- tests/components/fritzbox/__init__.py | 1 + tests/components/fritzbox/test_sensor.py | 8 +++ 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/fritzbox/sensor.py b/homeassistant/components/fritzbox/sensor.py index 4d9c1693c1f60..52d2617b223e3 100644 --- a/homeassistant/components/fritzbox/sensor.py +++ b/homeassistant/components/fritzbox/sensor.py @@ -2,7 +2,12 @@ import requests from homeassistant.components.sensor import SensorEntity -from homeassistant.const import CONF_DEVICES, TEMP_CELSIUS +from homeassistant.const import ( + CONF_DEVICES, + DEVICE_CLASS_BATTERY, + PERCENTAGE, + TEMP_CELSIUS, +) from .const import ( ATTR_STATE_DEVICE_LOCKED, @@ -29,9 +34,58 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entities.append(FritzBoxTempSensor(device, fritz)) devices.add(device.ain) + if device.battery_level is not None: + entities.append(FritzBoxBatterySensor(device, fritz)) + devices.add(f"{device.ain}_battery") + async_add_entities(entities) +class FritzBoxBatterySensor(SensorEntity): + """The entity class for Fritzbox battery sensors.""" + + def __init__(self, device, fritz): + """Initialize the sensor.""" + self._device = device + self._fritz = fritz + + @property + def device_info(self): + """Return device specific attributes.""" + return { + "name": self.name, + "identifiers": {(FRITZBOX_DOMAIN, self._device.ain)}, + "manufacturer": self._device.manufacturer, + "model": self._device.productname, + "sw_version": self._device.fw_version, + } + + @property + def unique_id(self): + """Return the unique ID of the device.""" + return f"{self._device.ain}_battery" + + @property + def name(self): + """Return the name of the device.""" + return f"{self._device.name} Battery" + + @property + def state(self): + """Return the state of the sensor.""" + return self._device.battery_level + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return PERCENTAGE + + @property + def device_class(self): + """Return the device class.""" + return DEVICE_CLASS_BATTERY + + class FritzBoxTempSensor(SensorEntity): """The entity class for Fritzbox temperature sensors.""" diff --git a/tests/components/fritzbox/__init__.py b/tests/components/fritzbox/__init__.py index f19e05b84dfee..8e0932b9000b3 100644 --- a/tests/components/fritzbox/__init__.py +++ b/tests/components/fritzbox/__init__.py @@ -64,6 +64,7 @@ class FritzDeviceSensorMock(Mock): """Mock of a AVM Fritz!Box sensor device.""" ain = "fake_ain" + battery_level = 23 device_lock = "fake_locked_device" fw_version = "1.2.3" has_alarm = False diff --git a/tests/components/fritzbox/test_sensor.py b/tests/components/fritzbox/test_sensor.py index 6dde22f074e45..00c9923bbeaae 100644 --- a/tests/components/fritzbox/test_sensor.py +++ b/tests/components/fritzbox/test_sensor.py @@ -13,6 +13,7 @@ from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, + PERCENTAGE, TEMP_CELSIUS, ) from homeassistant.helpers.typing import HomeAssistantType @@ -47,6 +48,13 @@ async def test_setup(hass: HomeAssistantType, fritz: Mock): assert state.attributes[ATTR_STATE_LOCKED] == "fake_locked" assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + state = hass.states.get(f"{ENTITY_ID}_battery") + + assert state + assert state.state == "23" + assert state.attributes[ATTR_FRIENDLY_NAME] == "fake_name Battery" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE + async def test_update(hass: HomeAssistantType, fritz: Mock): """Test update with error.""" From a67a45624d8b729080b17ceee349392692d6f33a Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Mon, 19 Apr 2021 00:04:29 +0000 Subject: [PATCH 0344/1317] [ci skip] Translation update --- .../components/adguard/translations/cs.json | 1 + .../components/bond/translations/cs.json | 2 +- .../components/climacell/translations/cs.json | 1 + .../coronavirus/translations/cs.json | 3 +- .../components/emonitor/translations/cs.json | 15 ++++++++ .../enphase_envoy/translations/cs.json | 7 ++++ .../components/ezviz/translations/cs.json | 36 +++++++++++++++++++ .../components/hive/translations/cs.json | 7 ++++ .../components/ialarm/translations/cs.json | 20 +++++++++++ .../kostal_plenticore/translations/cs.json | 20 +++++++++++ .../components/litejet/translations/cs.json | 14 ++++++++ .../litterrobot/translations/cs.json | 20 +++++++++++ .../components/lyric/translations/cs.json | 6 +++- .../components/lyric/translations/nl.json | 7 +++- .../lyric/translations/zh-Hant.json | 7 +++- .../met_eireann/translations/cs.json | 18 ++++++++++ .../components/nuki/translations/cs.json | 9 +++++ .../components/nut/translations/cs.json | 4 +++ .../translations/cs.json | 20 +++++++++++ .../components/sma/translations/cs.json | 24 +++++++++++++ .../totalconnect/translations/cs.json | 3 ++ .../waze_travel_time/translations/cs.json | 10 ++++++ 22 files changed, 249 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/emonitor/translations/cs.json create mode 100644 homeassistant/components/enphase_envoy/translations/cs.json create mode 100644 homeassistant/components/ezviz/translations/cs.json create mode 100644 homeassistant/components/ialarm/translations/cs.json create mode 100644 homeassistant/components/kostal_plenticore/translations/cs.json create mode 100644 homeassistant/components/litejet/translations/cs.json create mode 100644 homeassistant/components/litterrobot/translations/cs.json create mode 100644 homeassistant/components/met_eireann/translations/cs.json create mode 100644 homeassistant/components/rituals_perfume_genie/translations/cs.json create mode 100644 homeassistant/components/sma/translations/cs.json create mode 100644 homeassistant/components/waze_travel_time/translations/cs.json diff --git a/homeassistant/components/adguard/translations/cs.json b/homeassistant/components/adguard/translations/cs.json index 00531088a08f1..b56ed228b4ddf 100644 --- a/homeassistant/components/adguard/translations/cs.json +++ b/homeassistant/components/adguard/translations/cs.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Slu\u017eba je ji\u017e nastavena", "existing_instance_updated": "St\u00e1vaj\u00edc\u00ed nastaven\u00ed aktualizov\u00e1no.", "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." }, diff --git a/homeassistant/components/bond/translations/cs.json b/homeassistant/components/bond/translations/cs.json index 13135dbf53e59..6ee951350caf1 100644 --- a/homeassistant/components/bond/translations/cs.json +++ b/homeassistant/components/bond/translations/cs.json @@ -9,7 +9,7 @@ "old_firmware": "Nepodporovan\u00fd star\u00fd firmware na za\u0159\u00edzen\u00ed Bond - p\u0159ed pokra\u010dov\u00e1n\u00edm prove\u010fte aktualizaci", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, - "flow_title": "Bond: {bond_id} ({host})", + "flow_title": "Bond: {name} ({host})", "step": { "confirm": { "data": { diff --git a/homeassistant/components/climacell/translations/cs.json b/homeassistant/components/climacell/translations/cs.json index 1ae29deb08c1f..e9a608680d58c 100644 --- a/homeassistant/components/climacell/translations/cs.json +++ b/homeassistant/components/climacell/translations/cs.json @@ -9,6 +9,7 @@ "user": { "data": { "api_key": "Kl\u00ed\u010d API", + "api_version": "Verze API", "latitude": "Zem\u011bpisn\u00e1 \u0161\u00ed\u0159ka", "longitude": "Zem\u011bpisn\u00e1 d\u00e9lka", "name": "Jm\u00e9no" diff --git a/homeassistant/components/coronavirus/translations/cs.json b/homeassistant/components/coronavirus/translations/cs.json index 744f0e158ac5b..fb1a3937a9e6e 100644 --- a/homeassistant/components/coronavirus/translations/cs.json +++ b/homeassistant/components/coronavirus/translations/cs.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Slu\u017eba je ji\u017e nastavena" + "already_configured": "Slu\u017eba je ji\u017e nastavena", + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" }, "step": { "user": { diff --git a/homeassistant/components/emonitor/translations/cs.json b/homeassistant/components/emonitor/translations/cs.json new file mode 100644 index 0000000000000..347c9ee3ae077 --- /dev/null +++ b/homeassistant/components/emonitor/translations/cs.json @@ -0,0 +1,15 @@ +{ + "config": { + "flow_title": "SiteSage {name}", + "step": { + "confirm": { + "description": "Chcete nastavit {name} ({host})?" + }, + "user": { + "data": { + "host": "Hostitel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/cs.json b/homeassistant/components/enphase_envoy/translations/cs.json new file mode 100644 index 0000000000000..08830492748eb --- /dev/null +++ b/homeassistant/components/enphase_envoy/translations/cs.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ezviz/translations/cs.json b/homeassistant/components/ezviz/translations/cs.json new file mode 100644 index 0000000000000..294c32539de66 --- /dev/null +++ b/homeassistant/components/ezviz/translations/cs.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured_account": "\u00da\u010det je ji\u017e nastaven", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "invalid_host": "Neplatn\u00fd hostitel nebo IP adresa" + }, + "flow_title": "{serial}", + "step": { + "confirm": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + }, + "user": { + "data": { + "password": "Heslo", + "url": "URL", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + }, + "user_custom_url": { + "data": { + "password": "Heslo", + "url": "URL", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hive/translations/cs.json b/homeassistant/components/hive/translations/cs.json index 8544a3de7b893..81e2a4b288e48 100644 --- a/homeassistant/components/hive/translations/cs.json +++ b/homeassistant/components/hive/translations/cs.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "\u00da\u010det je ji\u017e nastaven", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, + "error": { + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, "step": { "reauth": { "data": { diff --git a/homeassistant/components/ialarm/translations/cs.json b/homeassistant/components/ialarm/translations/cs.json new file mode 100644 index 0000000000000..f6e1a56ca4a56 --- /dev/null +++ b/homeassistant/components/ialarm/translations/cs.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "host": "Hostitel", + "pin": "PIN k\u00f3d", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kostal_plenticore/translations/cs.json b/homeassistant/components/kostal_plenticore/translations/cs.json new file mode 100644 index 0000000000000..d4f77a8563155 --- /dev/null +++ b/homeassistant/components/kostal_plenticore/translations/cs.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "host": "Hostitel", + "password": "Heslo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litejet/translations/cs.json b/homeassistant/components/litejet/translations/cs.json new file mode 100644 index 0000000000000..04489d219078c --- /dev/null +++ b/homeassistant/components/litejet/translations/cs.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." + }, + "step": { + "user": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/cs.json b/homeassistant/components/litterrobot/translations/cs.json new file mode 100644 index 0000000000000..b6c00c053898b --- /dev/null +++ b/homeassistant/components/litterrobot/translations/cs.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u00da\u010det je ji\u017e nastaven" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/cs.json b/homeassistant/components/lyric/translations/cs.json index 2a54a82f41b42..f78f809cc4143 100644 --- a/homeassistant/components/lyric/translations/cs.json +++ b/homeassistant/components/lyric/translations/cs.json @@ -2,7 +2,8 @@ "config": { "abort": { "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el", - "missing_configuration": "Komponenta nen\u00ed nastavena. Postupujte podle dokumentace." + "missing_configuration": "Komponenta nen\u00ed nastavena. Postupujte podle dokumentace.", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "create_entry": { "default": "\u00dasp\u011b\u0161n\u011b ov\u011b\u0159eno" @@ -10,6 +11,9 @@ "step": { "pick_implementation": { "title": "Vyberte metodu ov\u011b\u0159en\u00ed" + }, + "reauth_confirm": { + "title": "Znovu ov\u011b\u0159it integraci" } } } diff --git a/homeassistant/components/lyric/translations/nl.json b/homeassistant/components/lyric/translations/nl.json index d490acb1b599f..0d766d1823ffd 100644 --- a/homeassistant/components/lyric/translations/nl.json +++ b/homeassistant/components/lyric/translations/nl.json @@ -2,7 +2,8 @@ "config": { "abort": { "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", - "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen." + "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen.", + "reauth_successful": "Herauthenticatie was succesvol" }, "create_entry": { "default": "Succesvol geauthenticeerd" @@ -10,6 +11,10 @@ "step": { "pick_implementation": { "title": "Kies een authenticatie methode" + }, + "reauth_confirm": { + "description": "De Lyric-integratie moet uw account opnieuw verifi\u00ebren.", + "title": "Verifieer de integratie opnieuw" } } } diff --git a/homeassistant/components/lyric/translations/zh-Hant.json b/homeassistant/components/lyric/translations/zh-Hant.json index b740fd3e063c9..850507ec0b3e8 100644 --- a/homeassistant/components/lyric/translations/zh-Hant.json +++ b/homeassistant/components/lyric/translations/zh-Hant.json @@ -2,7 +2,8 @@ "config": { "abort": { "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", - "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002" + "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "create_entry": { "default": "\u5df2\u6210\u529f\u8a8d\u8b49" @@ -10,6 +11,10 @@ "step": { "pick_implementation": { "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" + }, + "reauth_confirm": { + "description": "Lyric \u6574\u5408\u9700\u8981\u91cd\u65b0\u8a8d\u8b49\u60a8\u7684\u5e33\u865f", + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" } } } diff --git a/homeassistant/components/met_eireann/translations/cs.json b/homeassistant/components/met_eireann/translations/cs.json new file mode 100644 index 0000000000000..1088f8028bd4d --- /dev/null +++ b/homeassistant/components/met_eireann/translations/cs.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "already_configured": "Slu\u017eba je ji\u017e nastavena" + }, + "step": { + "user": { + "data": { + "elevation": "Nadmo\u0159sk\u00e1 v\u00fd\u0161ka", + "latitude": "Zem\u011bpisn\u00e1 \u0161\u00ed\u0159ka", + "longitude": "Zem\u011bpisn\u00e1 d\u00e9lka", + "name": "Jm\u00e9no" + }, + "title": "Um\u00edst\u011bn\u00ed" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuki/translations/cs.json b/homeassistant/components/nuki/translations/cs.json index 349c92805cf8f..52c1e3e9a8e17 100644 --- a/homeassistant/components/nuki/translations/cs.json +++ b/homeassistant/components/nuki/translations/cs.json @@ -1,11 +1,20 @@ { "config": { + "abort": { + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { + "reauth_confirm": { + "data": { + "token": "P\u0159\u00edstupov\u00fd token" + }, + "title": "Znovu ov\u011b\u0159it integraci" + }, "user": { "data": { "host": "Hostitel", diff --git a/homeassistant/components/nut/translations/cs.json b/homeassistant/components/nut/translations/cs.json index d5cf361ba03d1..37d73391ecf50 100644 --- a/homeassistant/components/nut/translations/cs.json +++ b/homeassistant/components/nut/translations/cs.json @@ -33,6 +33,10 @@ } }, "options": { + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, "step": { "init": { "data": { diff --git a/homeassistant/components/rituals_perfume_genie/translations/cs.json b/homeassistant/components/rituals_perfume_genie/translations/cs.json new file mode 100644 index 0000000000000..29c2ebc17138c --- /dev/null +++ b/homeassistant/components/rituals_perfume_genie/translations/cs.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "email": "E-mail", + "password": "Heslo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sma/translations/cs.json b/homeassistant/components/sma/translations/cs.json new file mode 100644 index 0000000000000..cac352ceb39c3 --- /dev/null +++ b/homeassistant/components/sma/translations/cs.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "group": "Skupina", + "host": "Hostitel", + "password": "Heslo", + "ssl": "Pou\u017e\u00edv\u00e1 SSL certifik\u00e1t", + "verify_ssl": "Ov\u011b\u0159it certifik\u00e1t SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/totalconnect/translations/cs.json b/homeassistant/components/totalconnect/translations/cs.json index 74dece0c54ef6..ad3b8dd6618c2 100644 --- a/homeassistant/components/totalconnect/translations/cs.json +++ b/homeassistant/components/totalconnect/translations/cs.json @@ -13,6 +13,9 @@ "location": "Um\u00edst\u011bn\u00ed" } }, + "reauth_confirm": { + "title": "Znovu ov\u011b\u0159it integraci" + }, "user": { "data": { "password": "Heslo", diff --git a/homeassistant/components/waze_travel_time/translations/cs.json b/homeassistant/components/waze_travel_time/translations/cs.json new file mode 100644 index 0000000000000..3f6b731b9bfe2 --- /dev/null +++ b/homeassistant/components/waze_travel_time/translations/cs.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "Um\u00edst\u011bn\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + } + } +} \ No newline at end of file From 6a3832484c8bf889896915535bcd632d8db1d4fa Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Mon, 19 Apr 2021 01:12:27 -0300 Subject: [PATCH 0345/1317] Do not log error messages when discovering Broadlink devices (#49394) --- .../components/broadlink/config_flow.py | 10 +++++--- .../components/broadlink/test_config_flow.py | 24 ++++++++++--------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/broadlink/config_flow.py b/homeassistant/components/broadlink/config_flow.py index 766c2c6094037..689ff28523f17 100644 --- a/homeassistant/components/broadlink/config_flow.py +++ b/homeassistant/components/broadlink/config_flow.py @@ -62,19 +62,23 @@ async def async_step_dhcp(self, discovery_info): unique_id = discovery_info[MAC_ADDRESS].lower().replace(":", "") await self.async_set_unique_id(unique_id) self._abort_if_unique_id_configured(updates={CONF_HOST: host}) + try: hello = partial(blk.discover, discover_ip_address=host) device = (await self.hass.async_add_executor_job(hello))[0] + except IndexError: return self.async_abort(reason="cannot_connect") + except OSError as err: if err.errno == errno.ENETUNREACH: return self.async_abort(reason="cannot_connect") - return self.async_abort(reason="invalid_host") - except Exception as ex: # pylint: disable=broad-except - _LOGGER.error("Failed to connect to the device at %s", host, exc_info=ex) return self.async_abort(reason="unknown") + supported_types = set.union(*DOMAINS_AND_TYPES.values()) + if device.type not in supported_types: + return self.async_abort(reason="not_supported") + await self.async_set_device(device) return await self.async_step_auth() diff --git a/tests/components/broadlink/test_config_flow.py b/tests/components/broadlink/test_config_flow.py index 503db632cf591..f8f8c81d52047 100644 --- a/tests/components/broadlink/test_config_flow.py +++ b/tests/components/broadlink/test_config_flow.py @@ -900,10 +900,10 @@ async def test_dhcp_unreachable(hass): assert result["reason"] == "cannot_connect" -async def test_dhcp_connect_einval(hass): - """Test DHCP discovery flow that fails to connect with EINVAL.""" +async def test_dhcp_connect_unknown_error(hass): + """Test DHCP discovery flow that fails to connect with an OSError.""" await setup.async_setup_component(hass, "persistent_notification", {}) - with patch(DEVICE_DISCOVERY, side_effect=OSError(errno.EINVAL, None)): + with patch(DEVICE_DISCOVERY, side_effect=OSError()): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "dhcp"}, @@ -916,26 +916,28 @@ async def test_dhcp_connect_einval(hass): await hass.async_block_till_done() assert result["type"] == "abort" - assert result["reason"] == "invalid_host" + assert result["reason"] == "unknown" -async def test_dhcp_connect_unknown_error(hass): - """Test DHCP discovery flow that fails to connect with an unknown error.""" +async def test_dhcp_device_not_supported(hass): + """Test DHCP discovery flow that fails because the device is not supported.""" await setup.async_setup_component(hass, "persistent_notification", {}) - with patch(DEVICE_DISCOVERY, side_effect=ValueError("Unknown failure")): + device = get_device("Kitchen") + mock_api = device.get_mock_api() + + with patch(DEVICE_DISCOVERY, return_value=[mock_api]): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "dhcp"}, data={ HOSTNAME: "broadlink", - IP_ADDRESS: "1.2.3.4", - MAC_ADDRESS: "34:ea:34:b4:3b:5a", + IP_ADDRESS: device.host, + MAC_ADDRESS: device_registry.format_mac(device.mac), }, ) - await hass.async_block_till_done() assert result["type"] == "abort" - assert result["reason"] == "unknown" + assert result["reason"] == "not_supported" async def test_dhcp_already_exists(hass): From 344717d07d163ef7f61c4d799144163f6c7d4f02 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 18 Apr 2021 18:17:30 -1000 Subject: [PATCH 0346/1317] Reduce time to first byte for frontend index (#49396) Cache template and manifest.json generation --- homeassistant/components/frontend/__init__.py | 171 ++++++++++++------ tests/components/frontend/test_init.py | 42 +++-- 2 files changed, 141 insertions(+), 72 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 0529fd6dbb2f0..ed339b9dc8b31 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -1,6 +1,7 @@ """Handle the frontend for Home Assistant.""" from __future__ import annotations +from functools import lru_cache import json import logging import mimetypes @@ -45,37 +46,6 @@ DEFAULT_THEME_COLOR = "#03A9F4" -MANIFEST_JSON = { - "background_color": "#FFFFFF", - "description": "Home automation platform that puts local control and privacy first.", - "dir": "ltr", - "display": "standalone", - "icons": [ - { - "src": f"/static/icons/favicon-{size}x{size}.png", - "sizes": f"{size}x{size}", - "type": "image/png", - "purpose": "maskable any", - } - for size in (192, 384, 512, 1024) - ], - "screenshots": [ - { - "src": "/static/images/screenshots/screenshot-1.png", - "sizes": "413x792", - "type": "image/png", - } - ], - "lang": "en-US", - "name": "Home Assistant", - "short_name": "Assistant", - "start_url": "/?homescreen=1", - "theme_color": DEFAULT_THEME_COLOR, - "prefer_related_applications": True, - "related_applications": [ - {"platform": "play", "id": "io.homeassistant.companion.android"} - ], -} DATA_PANELS = "frontend_panels" DATA_JS_VERSION = "frontend_js_version" @@ -124,6 +94,88 @@ SERVICE_RELOAD_THEMES = "reload_themes" +class Manifest: + """Manage the manifest.json contents.""" + + def __init__(self, data: dict) -> None: + """Init the manifest manager.""" + self.manifest = data + self._serialize() + + def __getitem__(self, key: str) -> Any: + """Return an item in the manifest.""" + return self.manifest[key] + + @property + def json(self) -> str: + """Return the serialized manifest.""" + return self._serialized + + def _serialize(self) -> None: + self._serialized = json.dumps(self.manifest, sort_keys=True) + + def update_key(self, key: str, val: str) -> None: + """Add a keyval to the manifest.json.""" + self.manifest[key] = val + self._serialize() + + +MANIFEST_JSON = Manifest( + { + "background_color": "#FFFFFF", + "description": "Home automation platform that puts local control and privacy first.", + "dir": "ltr", + "display": "standalone", + "icons": [ + { + "src": f"/static/icons/favicon-{size}x{size}.png", + "sizes": f"{size}x{size}", + "type": "image/png", + "purpose": "maskable any", + } + for size in (192, 384, 512, 1024) + ], + "screenshots": [ + { + "src": "/static/images/screenshots/screenshot-1.png", + "sizes": "413x792", + "type": "image/png", + } + ], + "lang": "en-US", + "name": "Home Assistant", + "short_name": "Assistant", + "start_url": "/?homescreen=1", + "theme_color": DEFAULT_THEME_COLOR, + "prefer_related_applications": True, + "related_applications": [ + {"platform": "play", "id": "io.homeassistant.companion.android"} + ], + } +) + + +class UrlManager: + """Manage urls to be used on the frontend. + + This is abstracted into a class because + some integrations add a remove these directly + on hass.data + """ + + def __init__(self, urls): + """Init the url manager.""" + self.urls = frozenset(urls) + + def add(self, url): + """Add a url to the set.""" + self.urls = frozenset([*self.urls, url]) + + def remove(self, url): + """Remove a url from the set.""" + self.urls = self.urls - {url} + + class Panel: """Abstract class for panels.""" @@ -223,15 +275,12 @@ def async_remove_panel(hass, frontend_url_path): def add_extra_js_url(hass, url, es5=False): """Register extra js or module url to load.""" key = DATA_EXTRA_JS_URL_ES5 if es5 else DATA_EXTRA_MODULE_URL - url_set = hass.data.get(key) - if url_set is None: - url_set = hass.data[key] = set() - url_set.add(url) + hass.data[key].add(url) def add_manifest_json_key(key, val): """Add a keyval to the manifest.json.""" - MANIFEST_JSON[key] = val + MANIFEST_JSON.update_key(key, val) def _frontend_root(dev_repo_path): @@ -311,17 +360,8 @@ async def async_setup(hass, config): sidebar_icon="hass:hammer", ) - if DATA_EXTRA_MODULE_URL not in hass.data: - hass.data[DATA_EXTRA_MODULE_URL] = set() - - for url in conf.get(CONF_EXTRA_MODULE_URL, []): - add_extra_js_url(hass, url) - - if DATA_EXTRA_JS_URL_ES5 not in hass.data: - hass.data[DATA_EXTRA_JS_URL_ES5] = set() - - for url in conf.get(CONF_EXTRA_JS_URL_ES5, []): - add_extra_js_url(hass, url, True) + hass.data[DATA_EXTRA_MODULE_URL] = UrlManager(conf.get(CONF_EXTRA_MODULE_URL, [])) + hass.data[DATA_EXTRA_JS_URL_ES5] = UrlManager(conf.get(CONF_EXTRA_JS_URL_ES5, [])) await _async_setup_themes(hass, conf.get(CONF_THEMES)) @@ -353,12 +393,16 @@ def update_theme_and_fire_event(): """Update theme_color in manifest.""" name = hass.data[DATA_DEFAULT_THEME] themes = hass.data[DATA_THEMES] - MANIFEST_JSON["theme_color"] = DEFAULT_THEME_COLOR if name != DEFAULT_THEME: - MANIFEST_JSON["theme_color"] = themes[name].get( - "app-header-background-color", - themes[name].get(PRIMARY_COLOR, DEFAULT_THEME_COLOR), + MANIFEST_JSON.update_key( + "theme_color", + themes[name].get( + "app-header-background-color", + themes[name].get(PRIMARY_COLOR, DEFAULT_THEME_COLOR), + ), ) + else: + MANIFEST_JSON.update_key("theme_color", DEFAULT_THEME_COLOR) hass.bus.async_fire(EVENT_THEMES_UPDATED) @callback @@ -426,6 +470,12 @@ async def reload_themes(_): ) +@callback +@lru_cache(maxsize=1) +def _async_render_index_cached(template, **kwargs): + return template.render(**kwargs) + + class IndexView(web_urldispatcher.AbstractResource): """Serve the frontend.""" @@ -504,16 +554,16 @@ async def get(self, request: web.Request) -> web.Response: if not hass.components.onboarding.async_is_onboarded(): return web.Response(status=302, headers={"location": "/onboarding.html"}) - template = self._template_cache - - if template is None: - template = await hass.async_add_executor_job(self.get_template) + template = self._template_cache or await hass.async_add_executor_job( + self.get_template + ) return web.Response( - text=template.render( + text=_async_render_index_cached( + template, theme_color=MANIFEST_JSON["theme_color"], - extra_modules=hass.data[DATA_EXTRA_MODULE_URL], - extra_js_es5=hass.data[DATA_EXTRA_JS_URL_ES5], + extra_modules=hass.data[DATA_EXTRA_MODULE_URL].urls, + extra_js_es5=hass.data[DATA_EXTRA_JS_URL_ES5].urls, ), content_type="text/html", ) @@ -537,8 +587,9 @@ class ManifestJSONView(HomeAssistantView): @callback def get(self, request): # pylint: disable=no-self-use """Return the manifest.json.""" - msg = json.dumps(MANIFEST_JSON, sort_keys=True) - return web.Response(text=msg, content_type="application/manifest+json") + return web.Response( + text=MANIFEST_JSON.json, content_type="application/manifest+json" + ) @callback diff --git a/tests/components/frontend/test_init.py b/tests/components/frontend/test_init.py index 0e8e31bb20de9..fe62445247519 100644 --- a/tests/components/frontend/test_init.py +++ b/tests/components/frontend/test_init.py @@ -10,27 +10,26 @@ CONF_EXTRA_HTML_URL_ES5, CONF_JS_VERSION, CONF_THEMES, + DEFAULT_THEME_COLOR, DOMAIN, EVENT_PANELS_UPDATED, THEMES_STORAGE_KEY, ) from homeassistant.components.websocket_api.const import TYPE_RESULT -from homeassistant.const import HTTP_NOT_FOUND +from homeassistant.const import HTTP_NOT_FOUND, HTTP_OK from homeassistant.loader import async_get_integration from homeassistant.setup import async_setup_component from homeassistant.util import dt from tests.common import async_capture_events, async_fire_time_changed -CONFIG_THEMES = { - DOMAIN: { - CONF_THEMES: { - "happy": {"primary-color": "red"}, - "dark": {"primary-color": "black"}, - } - } +MOCK_THEMES = { + "happy": {"primary-color": "red", "app-header-background-color": "blue"}, + "dark": {"primary-color": "black"}, } +CONFIG_THEMES = {DOMAIN: {CONF_THEMES: MOCK_THEMES}} + @pytest.fixture async def ignore_frontend_deps(hass): @@ -148,10 +147,7 @@ async def test_themes_api(hass, themes_ws_client): assert msg["result"]["default_theme"] == "default" assert msg["result"]["default_dark_theme"] is None - assert msg["result"]["themes"] == { - "happy": {"primary-color": "red"}, - "dark": {"primary-color": "black"}, - } + assert msg["result"]["themes"] == MOCK_THEMES # safe mode hass.config.safe_mode = True @@ -474,3 +470,25 @@ async def test_static_paths(hass, mock_http_client): ) assert resp.status == 302 assert resp.headers["location"] == "/profile" + + +async def test_manifest_json(hass, frontend_themes, mock_http_client): + """Test for fetching manifest.json.""" + resp = await mock_http_client.get("/manifest.json") + assert resp.status == HTTP_OK + assert "cache-control" not in resp.headers + + json = await resp.json() + assert json["theme_color"] == DEFAULT_THEME_COLOR + + await hass.services.async_call( + DOMAIN, "set_theme", {"name": "happy"}, blocking=True + ) + await hass.async_block_till_done() + + resp = await mock_http_client.get("/manifest.json") + assert resp.status == HTTP_OK + assert "cache-control" not in resp.headers + + json = await resp.json() + assert json["theme_color"] != DEFAULT_THEME_COLOR From cf51e079531654f30fef388117ab3cdf90378080 Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Mon, 19 Apr 2021 03:31:43 -0300 Subject: [PATCH 0347/1317] Fix esphome registering invalid service name (#49398) --- homeassistant/components/esphome/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 0caf00af8ef0d..4cd9744a2f802 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -473,7 +473,7 @@ async def _async_setup_device_registry( async def _register_service( hass: HomeAssistantType, entry_data: RuntimeEntryData, service: UserService ): - service_name = f"{entry_data.device_info.name}_{service.name}" + service_name = f"{entry_data.device_info.name.replace('-', '_')}_{service.name}" schema = {} fields = {} From 0f90678e0ed79aef806504e7602b236f93a2f5f6 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 19 Apr 2021 10:13:32 +0200 Subject: [PATCH 0348/1317] Change HomeAssistantType -> HomeAssistant in modbus (#49400) --- homeassistant/components/modbus/binary_sensor.py | 9 +++------ homeassistant/components/modbus/climate.py | 9 +++------ homeassistant/components/modbus/cover.py | 9 +++------ homeassistant/components/modbus/sensor.py | 9 +++------ homeassistant/components/modbus/switch.py | 5 +++-- 5 files changed, 15 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py index e422eb7528ea8..32f3527f8018c 100644 --- a/homeassistant/components/modbus/binary_sensor.py +++ b/homeassistant/components/modbus/binary_sensor.py @@ -21,13 +21,10 @@ CONF_SCAN_INTERVAL, CONF_SLAVE, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.typing import ( - ConfigType, - DiscoveryInfoType, - HomeAssistantType, -) +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import ( CALL_TYPE_COIL, @@ -72,7 +69,7 @@ async def async_setup_platform( - hass: HomeAssistantType, + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info: DiscoveryInfoType | None = None, diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index 6140ac038f747..25893ce008095 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -24,12 +24,9 @@ TEMP_CELSIUS, TEMP_FAHRENHEIT, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.typing import ( - ConfigType, - DiscoveryInfoType, - HomeAssistantType, -) +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import ( ATTR_TEMPERATURE, @@ -56,7 +53,7 @@ async def async_setup_platform( - hass: HomeAssistantType, + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info: DiscoveryInfoType | None = None, diff --git a/homeassistant/components/modbus/cover.py b/homeassistant/components/modbus/cover.py index bc7c946402b5c..3a1c1c565368b 100644 --- a/homeassistant/components/modbus/cover.py +++ b/homeassistant/components/modbus/cover.py @@ -15,13 +15,10 @@ CONF_SCAN_INTERVAL, CONF_SLAVE, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.typing import ( - ConfigType, - DiscoveryInfoType, - HomeAssistantType, -) +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import ( CALL_TYPE_COIL, @@ -40,7 +37,7 @@ async def async_setup_platform( - hass: HomeAssistantType, + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info: DiscoveryInfoType | None = None, diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index dcc68b52db886..b926ef6c5bd6d 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -27,14 +27,11 @@ CONF_STRUCTURE, CONF_UNIT_OF_MEASUREMENT, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.typing import ( - ConfigType, - DiscoveryInfoType, - HomeAssistantType, -) +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import ( CALL_TYPE_REGISTER_HOLDING, @@ -117,7 +114,7 @@ def number(value: Any) -> int | float: async def async_setup_platform( - hass: HomeAssistantType, + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info: DiscoveryInfoType | None = None, diff --git a/homeassistant/components/modbus/switch.py b/homeassistant/components/modbus/switch.py index 2985d8b2c0591..b5aef6d42c069 100644 --- a/homeassistant/components/modbus/switch.py +++ b/homeassistant/components/modbus/switch.py @@ -21,10 +21,11 @@ CONF_SWITCHES, STATE_ON, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from .const import ( CALL_TYPE_COIL, @@ -88,7 +89,7 @@ async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None ): """Read configuration and create Modbus switches.""" switches = [] From e98f27ead656ff3feaabd84e027822b423cc83bf Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Mon, 19 Apr 2021 05:16:03 -0300 Subject: [PATCH 0349/1317] Use broadlink.hello() for direct discovery (#49405) --- .../components/broadlink/config_flow.py | 11 +- .../components/broadlink/test_config_flow.py | 100 +++++++++--------- 2 files changed, 55 insertions(+), 56 deletions(-) diff --git a/homeassistant/components/broadlink/config_flow.py b/homeassistant/components/broadlink/config_flow.py index 689ff28523f17..b13838699abc5 100644 --- a/homeassistant/components/broadlink/config_flow.py +++ b/homeassistant/components/broadlink/config_flow.py @@ -64,10 +64,9 @@ async def async_step_dhcp(self, discovery_info): self._abort_if_unique_id_configured(updates={CONF_HOST: host}) try: - hello = partial(blk.discover, discover_ip_address=host) - device = (await self.hass.async_add_executor_job(hello))[0] + device = await self.hass.async_add_executor_job(blk.hello, host) - except IndexError: + except NetworkTimeoutError: return self.async_abort(reason="cannot_connect") except OSError as err: @@ -91,10 +90,10 @@ async def async_step_user(self, user_input=None): timeout = user_input.get(CONF_TIMEOUT, DEFAULT_TIMEOUT) try: - hello = partial(blk.discover, discover_ip_address=host, timeout=timeout) - device = (await self.hass.async_add_executor_job(hello))[0] + hello = partial(blk.hello, host, timeout=timeout) + device = await self.hass.async_add_executor_job(hello) - except IndexError: + except NetworkTimeoutError: errors["base"] = "cannot_connect" err_msg = "Device not found" diff --git a/tests/components/broadlink/test_config_flow.py b/tests/components/broadlink/test_config_flow.py index f8f8c81d52047..135362d62d9df 100644 --- a/tests/components/broadlink/test_config_flow.py +++ b/tests/components/broadlink/test_config_flow.py @@ -13,7 +13,7 @@ from . import get_device -DEVICE_DISCOVERY = "homeassistant.components.broadlink.config_flow.blk.discover" +DEVICE_HELLO = "homeassistant.components.broadlink.config_flow.blk.hello" DEVICE_FACTORY = "homeassistant.components.broadlink.config_flow.blk.gendevice" @@ -42,7 +42,7 @@ async def test_flow_user_works(hass): assert result["step_id"] == "user" assert result["errors"] == {} - with patch(DEVICE_DISCOVERY, return_value=[mock_api]) as mock_discover: + with patch(DEVICE_HELLO, return_value=mock_api) as mock_hello: result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host, "timeout": device.timeout}, @@ -61,7 +61,7 @@ async def test_flow_user_works(hass): assert result["title"] == device.name assert result["data"] == device.get_entry_data() - assert mock_discover.call_count == 1 + assert mock_hello.call_count == 1 assert mock_api.auth.call_count == 1 @@ -73,7 +73,7 @@ async def test_flow_user_already_in_progress(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch(DEVICE_DISCOVERY, return_value=[device.get_mock_api()]): + with patch(DEVICE_HELLO, return_value=device.get_mock_api()): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host, "timeout": device.timeout}, @@ -83,7 +83,7 @@ async def test_flow_user_already_in_progress(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch(DEVICE_DISCOVERY, return_value=[device.get_mock_api()]): + with patch(DEVICE_HELLO, return_value=device.get_mock_api()): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host, "timeout": device.timeout}, @@ -110,7 +110,7 @@ async def test_flow_user_mac_already_configured(hass): device.timeout = 20 mock_api = device.get_mock_api() - with patch(DEVICE_DISCOVERY, return_value=[mock_api]): + with patch(DEVICE_HELLO, return_value=mock_api): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host, "timeout": device.timeout}, @@ -129,7 +129,7 @@ async def test_flow_user_invalid_ip_address(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch(DEVICE_DISCOVERY, side_effect=OSError(errno.EINVAL, None)): + with patch(DEVICE_HELLO, side_effect=OSError(errno.EINVAL, None)): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "0.0.0.1"}, @@ -146,7 +146,7 @@ async def test_flow_user_invalid_hostname(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch(DEVICE_DISCOVERY, side_effect=OSError(socket.EAI_NONAME, None)): + with patch(DEVICE_HELLO, side_effect=OSError(socket.EAI_NONAME, None)): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "pancakemaster.local"}, @@ -165,7 +165,7 @@ async def test_flow_user_device_not_found(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch(DEVICE_DISCOVERY, return_value=[]): + with patch(DEVICE_HELLO, side_effect=blke.NetworkTimeoutError()): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host}, @@ -185,7 +185,7 @@ async def test_flow_user_device_not_supported(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch(DEVICE_DISCOVERY, return_value=[mock_api]): + with patch(DEVICE_HELLO, return_value=mock_api): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host}, @@ -201,7 +201,7 @@ async def test_flow_user_network_unreachable(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch(DEVICE_DISCOVERY, side_effect=OSError(errno.ENETUNREACH, None)): + with patch(DEVICE_HELLO, side_effect=OSError(errno.ENETUNREACH, None)): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "192.168.1.32"}, @@ -218,7 +218,7 @@ async def test_flow_user_os_error(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch(DEVICE_DISCOVERY, side_effect=OSError()): + with patch(DEVICE_HELLO, side_effect=OSError()): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "192.168.1.32"}, @@ -239,7 +239,7 @@ async def test_flow_auth_authentication_error(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch(DEVICE_DISCOVERY, return_value=[mock_api]): + with patch(DEVICE_HELLO, return_value=mock_api): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host, "timeout": device.timeout}, @@ -260,7 +260,7 @@ async def test_flow_auth_network_timeout(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch(DEVICE_DISCOVERY, return_value=[mock_api]): + with patch(DEVICE_HELLO, return_value=mock_api): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host}, @@ -281,7 +281,7 @@ async def test_flow_auth_firmware_error(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch(DEVICE_DISCOVERY, return_value=[mock_api]): + with patch(DEVICE_HELLO, return_value=mock_api): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host}, @@ -302,7 +302,7 @@ async def test_flow_auth_network_unreachable(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch(DEVICE_DISCOVERY, return_value=[mock_api]): + with patch(DEVICE_HELLO, return_value=mock_api): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host}, @@ -323,7 +323,7 @@ async def test_flow_auth_os_error(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch(DEVICE_DISCOVERY, return_value=[mock_api]): + with patch(DEVICE_HELLO, return_value=mock_api): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host}, @@ -344,13 +344,13 @@ async def test_flow_reset_works(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch(DEVICE_DISCOVERY, return_value=[mock_api]): + with patch(DEVICE_HELLO, return_value=mock_api): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host, "timeout": device.timeout}, ) - with patch(DEVICE_DISCOVERY, return_value=[device.get_mock_api()]): + with patch(DEVICE_HELLO, return_value=device.get_mock_api()): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host, "timeout": device.timeout}, @@ -376,7 +376,7 @@ async def test_flow_unlock_works(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch(DEVICE_DISCOVERY, return_value=[mock_api]): + with patch(DEVICE_HELLO, return_value=mock_api): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host, "timeout": device.timeout}, @@ -415,7 +415,7 @@ async def test_flow_unlock_network_timeout(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch(DEVICE_DISCOVERY, return_value=[mock_api]): + with patch(DEVICE_HELLO, return_value=mock_api): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host, "timeout": device.timeout}, @@ -442,7 +442,7 @@ async def test_flow_unlock_firmware_error(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch(DEVICE_DISCOVERY, return_value=[mock_api]): + with patch(DEVICE_HELLO, return_value=mock_api): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host, "timeout": device.timeout}, @@ -469,7 +469,7 @@ async def test_flow_unlock_network_unreachable(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch(DEVICE_DISCOVERY, return_value=[mock_api]): + with patch(DEVICE_HELLO, return_value=mock_api): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host, "timeout": device.timeout}, @@ -496,7 +496,7 @@ async def test_flow_unlock_os_error(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch(DEVICE_DISCOVERY, return_value=[mock_api]): + with patch(DEVICE_HELLO, return_value=mock_api): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host, "timeout": device.timeout}, @@ -522,7 +522,7 @@ async def test_flow_do_not_unlock(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch(DEVICE_DISCOVERY, return_value=[mock_api]): + with patch(DEVICE_HELLO, return_value=mock_api): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host, "timeout": device.timeout}, @@ -550,7 +550,7 @@ async def test_flow_import_works(hass): device = get_device("Living Room") mock_api = device.get_mock_api() - with patch(DEVICE_DISCOVERY, return_value=[mock_api]) as mock_discover: + with patch(DEVICE_HELLO, return_value=mock_api) as mock_hello: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, @@ -573,7 +573,7 @@ async def test_flow_import_works(hass): assert result["data"]["type"] == device.devtype assert mock_api.auth.call_count == 1 - assert mock_discover.call_count == 1 + assert mock_hello.call_count == 1 async def test_flow_import_already_in_progress(hass): @@ -581,12 +581,12 @@ async def test_flow_import_already_in_progress(hass): device = get_device("Living Room") data = {"host": device.host} - with patch(DEVICE_DISCOVERY, return_value=[device.get_mock_api()]): + with patch(DEVICE_HELLO, return_value=device.get_mock_api()): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=data ) - with patch(DEVICE_DISCOVERY, return_value=[device.get_mock_api()]): + with patch(DEVICE_HELLO, return_value=device.get_mock_api()): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=data ) @@ -602,7 +602,7 @@ async def test_flow_import_host_already_configured(hass): mock_entry.add_to_hass(hass) mock_api = device.get_mock_api() - with patch(DEVICE_DISCOVERY, return_value=[mock_api]): + with patch(DEVICE_HELLO, return_value=mock_api): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, @@ -625,7 +625,7 @@ async def test_flow_import_mac_already_configured(hass): device.host = "192.168.1.16" mock_api = device.get_mock_api() - with patch(DEVICE_DISCOVERY, return_value=[mock_api]): + with patch(DEVICE_HELLO, return_value=mock_api): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, @@ -643,7 +643,7 @@ async def test_flow_import_mac_already_configured(hass): async def test_flow_import_device_not_found(hass): """Test we handle a device not found in the import step.""" - with patch(DEVICE_DISCOVERY, return_value=[]): + with patch(DEVICE_HELLO, side_effect=blke.NetworkTimeoutError()): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, @@ -659,7 +659,7 @@ async def test_flow_import_device_not_supported(hass): device = get_device("Kitchen") mock_api = device.get_mock_api() - with patch(DEVICE_DISCOVERY, return_value=[mock_api]): + with patch(DEVICE_HELLO, return_value=mock_api): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, @@ -672,7 +672,7 @@ async def test_flow_import_device_not_supported(hass): async def test_flow_import_invalid_ip_address(hass): """Test we handle an invalid IP address in the import step.""" - with patch(DEVICE_DISCOVERY, side_effect=OSError(errno.EINVAL, None)): + with patch(DEVICE_HELLO, side_effect=OSError(errno.EINVAL, None)): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, @@ -685,7 +685,7 @@ async def test_flow_import_invalid_ip_address(hass): async def test_flow_import_invalid_hostname(hass): """Test we handle an invalid hostname in the import step.""" - with patch(DEVICE_DISCOVERY, side_effect=OSError(socket.EAI_NONAME, None)): + with patch(DEVICE_HELLO, side_effect=OSError(socket.EAI_NONAME, None)): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, @@ -698,7 +698,7 @@ async def test_flow_import_invalid_hostname(hass): async def test_flow_import_network_unreachable(hass): """Test we handle a network unreachable in the import step.""" - with patch(DEVICE_DISCOVERY, side_effect=OSError(errno.ENETUNREACH, None)): + with patch(DEVICE_HELLO, side_effect=OSError(errno.ENETUNREACH, None)): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, @@ -711,7 +711,7 @@ async def test_flow_import_network_unreachable(hass): async def test_flow_import_os_error(hass): """Test we handle an OS error in the import step.""" - with patch(DEVICE_DISCOVERY, side_effect=OSError()): + with patch(DEVICE_HELLO, side_effect=OSError()): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, @@ -741,7 +741,7 @@ async def test_flow_reauth_works(hass): mock_api = device.get_mock_api() - with patch(DEVICE_DISCOVERY, return_value=[mock_api]) as mock_discover: + with patch(DEVICE_HELLO, return_value=mock_api) as mock_hello: result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host, "timeout": device.timeout}, @@ -752,7 +752,7 @@ async def test_flow_reauth_works(hass): assert dict(mock_entry.data) == device.get_entry_data() assert mock_api.auth.call_count == 1 - assert mock_discover.call_count == 1 + assert mock_hello.call_count == 1 async def test_flow_reauth_invalid_host(hass): @@ -775,7 +775,7 @@ async def test_flow_reauth_invalid_host(hass): device.mac = get_device("Office").mac mock_api = device.get_mock_api() - with patch(DEVICE_DISCOVERY, return_value=[mock_api]) as mock_discover: + with patch(DEVICE_HELLO, return_value=mock_api) as mock_hello: result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host, "timeout": device.timeout}, @@ -785,7 +785,7 @@ async def test_flow_reauth_invalid_host(hass): assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_host"} - assert mock_discover.call_count == 1 + assert mock_hello.call_count == 1 assert mock_api.auth.call_count == 0 @@ -809,7 +809,7 @@ async def test_flow_reauth_valid_host(hass): device.host = "192.168.1.128" mock_api = device.get_mock_api() - with patch(DEVICE_DISCOVERY, return_value=[mock_api]) as mock_discover: + with patch(DEVICE_HELLO, return_value=mock_api) as mock_hello: result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host, "timeout": device.timeout}, @@ -819,7 +819,7 @@ async def test_flow_reauth_valid_host(hass): assert result["reason"] == "already_configured" assert mock_entry.data["host"] == device.host - assert mock_discover.call_count == 1 + assert mock_hello.call_count == 1 assert mock_api.auth.call_count == 1 @@ -831,7 +831,7 @@ async def test_dhcp_can_finish(hass): device.host = "1.2.3.4" mock_api = device.get_mock_api() - with patch(DEVICE_DISCOVERY, return_value=[mock_api]): + with patch(DEVICE_HELLO, return_value=mock_api): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "dhcp"}, @@ -865,7 +865,7 @@ async def test_dhcp_can_finish(hass): async def test_dhcp_fails_to_connect(hass): """Test DHCP discovery flow that fails to connect.""" await setup.async_setup_component(hass, "persistent_notification", {}) - with patch(DEVICE_DISCOVERY, side_effect=IndexError()): + with patch(DEVICE_HELLO, side_effect=blke.NetworkTimeoutError()): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "dhcp"}, @@ -884,7 +884,7 @@ async def test_dhcp_fails_to_connect(hass): async def test_dhcp_unreachable(hass): """Test DHCP discovery flow that fails to connect.""" await setup.async_setup_component(hass, "persistent_notification", {}) - with patch(DEVICE_DISCOVERY, side_effect=OSError(errno.ENETUNREACH, None)): + with patch(DEVICE_HELLO, side_effect=OSError(errno.ENETUNREACH, None)): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "dhcp"}, @@ -903,7 +903,7 @@ async def test_dhcp_unreachable(hass): async def test_dhcp_connect_unknown_error(hass): """Test DHCP discovery flow that fails to connect with an OSError.""" await setup.async_setup_component(hass, "persistent_notification", {}) - with patch(DEVICE_DISCOVERY, side_effect=OSError()): + with patch(DEVICE_HELLO, side_effect=OSError()): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "dhcp"}, @@ -925,7 +925,7 @@ async def test_dhcp_device_not_supported(hass): device = get_device("Kitchen") mock_api = device.get_mock_api() - with patch(DEVICE_DISCOVERY, return_value=[mock_api]): + with patch(DEVICE_HELLO, return_value=mock_api): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "dhcp"}, @@ -949,7 +949,7 @@ async def test_dhcp_already_exists(hass): device.host = "1.2.3.4" mock_api = device.get_mock_api() - with patch(DEVICE_DISCOVERY, return_value=[mock_api]): + with patch(DEVICE_HELLO, return_value=mock_api): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "dhcp"}, @@ -975,7 +975,7 @@ async def test_dhcp_updates_host(hass): mock_entry.add_to_hass(hass) mock_api = device.get_mock_api() - with patch(DEVICE_DISCOVERY, return_value=[mock_api]): + with patch(DEVICE_HELLO, return_value=mock_api): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "dhcp"}, From 0b26294fb0d8a6d5a98ab45d171108591038899b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 18 Apr 2021 22:39:45 -1000 Subject: [PATCH 0350/1317] Small cleanups to rachio (#49404) - Remove unused async_step - Reduce async callbacks from executor --- homeassistant/components/rachio/__init__.py | 11 +-- homeassistant/components/rachio/device.py | 94 +++++++++++---------- tests/components/rachio/test_config_flow.py | 3 - 3 files changed, 53 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/rachio/__init__.py b/homeassistant/components/rachio/__init__.py index 30015dcf8c1de..0335bd9928c05 100644 --- a/homeassistant/components/rachio/__init__.py +++ b/homeassistant/components/rachio/__init__.py @@ -26,14 +26,6 @@ CONFIG_SCHEMA = cv.deprecated(DOMAIN) -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the rachio component from YAML.""" - - hass.data.setdefault(DOMAIN, {}) - - return True - - async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" unload_ok = all( @@ -84,7 +76,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): # Get the API user try: - await hass.async_add_executor_job(person.setup, hass) + await person.async_setup(hass) except ConnectTimeout as error: _LOGGER.error("Could not reach the Rachio API: %s", error) raise ConfigEntryNotReady from error @@ -100,6 +92,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): ) # Enable platform + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = person async_register_webhook(hass, webhook_id, entry.entry_id) diff --git a/homeassistant/components/rachio/device.py b/homeassistant/components/rachio/device.py index a6ed596db0421..ac2fea20bcfda 100644 --- a/homeassistant/components/rachio/device.py +++ b/homeassistant/components/rachio/device.py @@ -57,23 +57,65 @@ def __init__(self, rachio, config_entry): self._id = None self._controllers = [] - def setup(self, hass): - """Rachio device setup.""" - all_devices = [] + async def async_setup(self, hass): + """Create rachio devices and services.""" + await hass.async_add_executor_job(self._setup, hass) can_pause = False - response = self.rachio.person.info() + for rachio_iro in self._controllers: + # Generation 1 controllers don't support pause or resume + if rachio_iro.model.split("_")[0] != MODEL_GENERATION_1: + can_pause = True + break + + if not can_pause: + return + + all_devices = [rachio_iro.name for rachio_iro in self._controllers] + + def pause_water(service): + """Service to pause watering on all or specific controllers.""" + duration = service.data[ATTR_DURATION] + devices = service.data.get(ATTR_DEVICES, all_devices) + for iro in self._controllers: + if iro.name in devices: + iro.pause_watering(duration) + + def resume_water(service): + """Service to resume watering on all or specific controllers.""" + devices = service.data.get(ATTR_DEVICES, all_devices) + for iro in self._controllers: + if iro.name in devices: + iro.resume_watering() + + hass.services.async_register( + DOMAIN, + SERVICE_PAUSE_WATERING, + pause_water, + schema=PAUSE_SERVICE_SCHEMA, + ) + + hass.services.async_register( + DOMAIN, + SERVICE_RESUME_WATERING, + resume_water, + schema=RESUME_SERVICE_SCHEMA, + ) + + def _setup(self, hass): + """Rachio device setup.""" + rachio = self.rachio + + response = rachio.person.info() assert int(response[0][KEY_STATUS]) == HTTP_OK, "API key error" self._id = response[1][KEY_ID] # Use user ID to get user data - data = self.rachio.person.get(self._id) + data = rachio.person.get(self._id) assert int(data[0][KEY_STATUS]) == HTTP_OK, "User ID error" self.username = data[1][KEY_USERNAME] devices = data[1][KEY_DEVICES] for controller in devices: - webhooks = self.rachio.notification.get_device_webhook(controller[KEY_ID])[ - 1 - ] + webhooks = rachio.notification.get_device_webhook(controller[KEY_ID])[1] # The API does not provide a way to tell if a controller is shared # or if they are the owner. To work around this problem we fetch the webooks # before we setup the device so we can skip it instead of failing. @@ -94,46 +136,12 @@ def setup(self, hass): ) continue - rachio_iro = RachioIro(hass, self.rachio, controller, webhooks) + rachio_iro = RachioIro(hass, rachio, controller, webhooks) rachio_iro.setup() self._controllers.append(rachio_iro) - all_devices.append(rachio_iro.name) - # Generation 1 controllers don't support pause or resume - if rachio_iro.model.split("_")[0] != MODEL_GENERATION_1: - can_pause = True _LOGGER.info('Using Rachio API as user "%s"', self.username) - def pause_water(service): - """Service to pause watering on all or specific controllers.""" - duration = service.data[ATTR_DURATION] - devices = service.data.get(ATTR_DEVICES, all_devices) - for iro in self._controllers: - if iro.name in devices: - iro.pause_watering(duration) - - def resume_water(service): - """Service to resume watering on all or specific controllers.""" - devices = service.data.get(ATTR_DEVICES, all_devices) - for iro in self._controllers: - if iro.name in devices: - iro.resume_watering() - - if can_pause: - hass.services.register( - DOMAIN, - SERVICE_PAUSE_WATERING, - pause_water, - schema=PAUSE_SERVICE_SCHEMA, - ) - - hass.services.register( - DOMAIN, - SERVICE_RESUME_WATERING, - resume_water, - schema=RESUME_SERVICE_SCHEMA, - ) - @property def user_id(self) -> str: """Get the user ID as defined by the Rachio API.""" diff --git a/tests/components/rachio/test_config_flow.py b/tests/components/rachio/test_config_flow.py index 6b0fc2e69cbe4..ddf403343cffe 100644 --- a/tests/components/rachio/test_config_flow.py +++ b/tests/components/rachio/test_config_flow.py @@ -38,8 +38,6 @@ async def test_form(hass): with patch( "homeassistant.components.rachio.config_flow.Rachio", return_value=rachio_mock ), patch( - "homeassistant.components.rachio.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.rachio.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -60,7 +58,6 @@ async def test_form(hass): CONF_CUSTOM_URL: "http://custom.url", CONF_MANUAL_RUN_MINS: 5, } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 From 6048e88c8bcf63e7cee6330339f96ba5258b2ed6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 18 Apr 2021 23:02:17 -1000 Subject: [PATCH 0351/1317] Improve debuggability by providing job as an arg to loop.call_later (#49328) Before `.run_action() at /usr/src/homeassistant/homeassistant/helpers/event.py:1177>` After `.run_action(>>) at /usr/src/homeassistant/homeassistant/helpers/event.py:1175>` --- homeassistant/helpers/event.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index abba6f12a256e..1a7e11ff5c992 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -1170,12 +1170,10 @@ def async_track_point_in_utc_time( # Since this is called once, we accept a HassJob so we can avoid # having to figure out how to call the action every time its called. - job = action if isinstance(action, HassJob) else HassJob(action) - cancel_callback: asyncio.TimerHandle | None = None @callback - def run_action() -> None: + def run_action(job: HassJob) -> None: """Call the action.""" nonlocal cancel_callback @@ -1190,13 +1188,14 @@ def run_action() -> None: if delta > 0: _LOGGER.debug("Called %f seconds too early, rearming", delta) - cancel_callback = hass.loop.call_later(delta, run_action) + cancel_callback = hass.loop.call_later(delta, run_action, job) return hass.async_run_hass_job(job, utc_point_in_time) + job = action if isinstance(action, HassJob) else HassJob(action) delta = utc_point_in_time.timestamp() - time.time() - cancel_callback = hass.loop.call_later(delta, run_action) + cancel_callback = hass.loop.call_later(delta, run_action, job) @callback def unsub_point_in_time_listener() -> None: From e24f5831a2b4f3bf84b2894b9c5bb8aca3893067 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 18 Apr 2021 23:24:17 -1000 Subject: [PATCH 0352/1317] Force recorder shutdown at final write event (#49145) * Force recorder shutdown at EVENT_HOMEASSISTANT_FINAL_WRITE * remove unreachable * remove unreachable * simplify * cancel in async --- homeassistant/components/recorder/__init__.py | 32 ++++++++++++++++--- tests/components/recorder/test_init.py | 31 ++++++++++++++++++ 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 98199bab430bd..733d8f248a896 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -21,6 +21,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, CONF_EXCLUDE, + EVENT_HOMEASSISTANT_FINAL_WRITE, EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED, @@ -338,10 +339,11 @@ def _async_check_queue(self, *_): "The recorder queue reached the maximum size of %s; Events are no longer being recorded", MAX_QUEUE_BACKLOG, ) - self._stop_queue_watcher_and_event_listener() + self._async_stop_queue_watcher_and_event_listener() - def _stop_queue_watcher_and_event_listener(self): - """Stop watching the queue.""" + @callback + def _async_stop_queue_watcher_and_event_listener(self): + """Stop watching the queue and listening for events.""" if self._queue_watcher: self._queue_watcher() self._queue_watcher = None @@ -370,11 +372,31 @@ def do_adhoc_purge(self, **kwargs): def async_register(self, shutdown_task, hass_started): """Post connection initialize.""" + def _empty_queue(event): + """Empty the queue if its still present at final write.""" + + # If the queue is full of events to be processed because + # the database is so broken that every event results in a retry + # we will never be able to get though the events to shutdown in time. + # + # We drain all the events in the queue and then insert + # an empty one to ensure the next thing the recorder sees + # is a request to shutdown. + while True: + try: + self.queue.get_nowait() + except queue.Empty: + break + self.queue.put(None) + + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_FINAL_WRITE, _empty_queue) + def shutdown(event): """Shut down the Recorder.""" if not hass_started.done(): hass_started.set_result(shutdown_task) self.queue.put(None) + self.hass.add_job(self._async_stop_queue_watcher_and_event_listener) self.join() self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown) @@ -399,7 +421,7 @@ def async_connection_failed(self): "The recorder could not start, check [the logs](/config/logs)", "Recorder", ) - self._stop_queue_watcher_and_event_listener() + self._async_stop_queue_watcher_and_event_listener() @callback def async_connection_success(self): @@ -836,6 +858,6 @@ def _end_session(self): def _shutdown(self): """Save end time for current run.""" - self._stop_queue_watcher_and_event_listener() + self.hass.add_job(self._async_stop_queue_watcher_and_event_listener) self._end_session() self._close_connection() diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 67032e9f077d2..d346408839410 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -24,6 +24,7 @@ from homeassistant.components.recorder.models import Events, RecorderRuns, States from homeassistant.components.recorder.util import session_scope from homeassistant.const import ( + EVENT_HOMEASSISTANT_FINAL_WRITE, EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, MATCH_ALL, @@ -265,6 +266,36 @@ def _throw_if_state_in_session(*args, **kwargs): assert "SQLAlchemyError error processing event" not in caplog.text +async def test_force_shutdown_with_queue_of_writes_that_generate_exceptions( + hass, async_setup_recorder_instance, caplog +): + """Test forcing shutdown.""" + instance = await async_setup_recorder_instance(hass) + + entity_id = "test.recorder" + attributes = {"test_attr": 5, "test_attr_10": "nice"} + + await async_wait_recording_done(hass, instance) + + with patch.object(instance, "db_retry_wait", 0.2), patch.object( + instance.event_session, + "flush", + side_effect=OperationalError( + "insert the state", "fake params", "forced to fail" + ), + ): + for _ in range(100): + hass.states.async_set(entity_id, "on", attributes) + hass.states.async_set(entity_id, "off", attributes) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + hass.bus.async_fire(EVENT_HOMEASSISTANT_FINAL_WRITE) + await hass.async_block_till_done() + + assert "Error executing query" in caplog.text + assert "Error saving events" not in caplog.text + + def test_saving_event(hass, hass_recorder): """Test saving and restoring an event.""" hass = hass_recorder() From 7f6572893d392ece0b4eff0e820eeec0c513af93 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 18 Apr 2021 23:39:34 -1000 Subject: [PATCH 0353/1317] Add services to the profiler to log threads and event loop schedule (#49327) * Add services to the profiler to log threads and event loop schedule * improve readability * increase log debug * bigger * tweaks * Update homeassistant/components/profiler/__init__.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/profiler/__init__.py Co-authored-by: Martin Hjelmare * remove schema= and cleanup existing Co-authored-by: Martin Hjelmare --- homeassistant/components/profiler/__init__.py | 52 ++++++++++++++++++- .../components/profiler/services.yaml | 4 ++ tests/components/profiler/test_init.py | 46 ++++++++++++++++ 3 files changed, 101 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/profiler/__init__.py b/homeassistant/components/profiler/__init__.py index c3f4ab17686f5..e6aa2ce557da2 100644 --- a/homeassistant/components/profiler/__init__.py +++ b/homeassistant/components/profiler/__init__.py @@ -3,7 +3,11 @@ import cProfile from datetime import timedelta import logging +import reprlib +import sys +import threading import time +import traceback from guppy import hpy import objgraph @@ -23,6 +27,9 @@ SERVICE_START_LOG_OBJECTS = "start_log_objects" SERVICE_STOP_LOG_OBJECTS = "stop_log_objects" SERVICE_DUMP_LOG_OBJECTS = "dump_log_objects" +SERVICE_LOG_THREAD_FRAMES = "log_thread_frames" +SERVICE_LOG_EVENT_LOOP_SCHEDULED = "log_event_loop_scheduled" + SERVICES = ( SERVICE_START, @@ -30,6 +37,8 @@ SERVICE_START_LOG_OBJECTS, SERVICE_STOP_LOG_OBJECTS, SERVICE_DUMP_LOG_OBJECTS, + SERVICE_LOG_THREAD_FRAMES, + SERVICE_LOG_EVENT_LOOP_SCHEDULED, ) DEFAULT_SCAN_INTERVAL = timedelta(seconds=30) @@ -93,6 +102,34 @@ def _dump_log_objects(call: ServiceCall): notification_id="profile_object_dump", ) + async def _async_dump_thread_frames(call: ServiceCall) -> None: + """Log all thread frames.""" + frames = sys._current_frames() # pylint: disable=protected-access + main_thread = threading.main_thread() + for thread in threading.enumerate(): + if thread == main_thread: + continue + _LOGGER.critical( + "Thread [%s]: %s", + thread.name, + "".join(traceback.format_stack(frames.get(thread.ident))).strip(), + ) + + async def _async_dump_scheduled(call: ServiceCall) -> None: + """Log all scheduled in the event loop.""" + arepr = reprlib.aRepr + original_maxstring = arepr.maxstring + original_maxother = arepr.maxother + arepr.maxstring = 300 + arepr.maxother = 300 + try: + for handle in hass.loop._scheduled: # pylint: disable=protected-access + if not handle.cancelled(): + _LOGGER.critical("Scheduled: %s", handle) + finally: + arepr.max_string = original_maxstring + arepr.max_other = original_maxother + async_register_admin_service( hass, DOMAIN, @@ -132,7 +169,6 @@ def _dump_log_objects(call: ServiceCall): DOMAIN, SERVICE_STOP_LOG_OBJECTS, _async_stop_log_objects, - schema=vol.Schema({}), ) async_register_admin_service( @@ -143,6 +179,20 @@ def _dump_log_objects(call: ServiceCall): schema=vol.Schema({vol.Required(CONF_TYPE): str}), ) + async_register_admin_service( + hass, + DOMAIN, + SERVICE_LOG_THREAD_FRAMES, + _async_dump_thread_frames, + ) + + async_register_admin_service( + hass, + DOMAIN, + SERVICE_LOG_EVENT_LOOP_SCHEDULED, + _async_dump_scheduled, + ) + return True diff --git a/homeassistant/components/profiler/services.yaml b/homeassistant/components/profiler/services.yaml index f0b04e9e00239..2b59c7a405418 100644 --- a/homeassistant/components/profiler/services.yaml +++ b/homeassistant/components/profiler/services.yaml @@ -24,3 +24,7 @@ dump_log_objects: type: description: The type of objects to dump to the log example: State +log_thread_frames: + description: Log the current frames for all threads +log_event_loop_scheduled: + description: Log what is scheduled in the event loop diff --git a/tests/components/profiler/test_init.py b/tests/components/profiler/test_init.py index efed6ef612630..be376ea8aed98 100644 --- a/tests/components/profiler/test_init.py +++ b/tests/components/profiler/test_init.py @@ -9,6 +9,8 @@ CONF_SECONDS, CONF_TYPE, SERVICE_DUMP_LOG_OBJECTS, + SERVICE_LOG_EVENT_LOOP_SCHEDULED, + SERVICE_LOG_THREAD_FRAMES, SERVICE_MEMORY, SERVICE_START, SERVICE_START_LOG_OBJECTS, @@ -147,3 +149,47 @@ async def test_dump_log_object(hass, caplog): assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() + + +async def test_log_thread_frames(hass, caplog): + """Test we can log thread frames.""" + + await setup.async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry(domain=DOMAIN) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert hass.services.has_service(DOMAIN, SERVICE_LOG_THREAD_FRAMES) + + await hass.services.async_call(DOMAIN, SERVICE_LOG_THREAD_FRAMES, {}) + await hass.async_block_till_done() + + assert "SyncWorker_0" in caplog.text + caplog.clear() + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_log_scheduled(hass, caplog): + """Test we can log scheduled items in the event loop.""" + + await setup.async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry(domain=DOMAIN) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert hass.services.has_service(DOMAIN, SERVICE_LOG_EVENT_LOOP_SCHEDULED) + + await hass.services.async_call(DOMAIN, SERVICE_LOG_EVENT_LOOP_SCHEDULED, {}) + await hass.async_block_till_done() + + assert "Scheduled" in caplog.text + caplog.clear() + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() From 591d09c1778bc84fd236489d9f18889b5c7f4993 Mon Sep 17 00:00:00 2001 From: Chris Browet Date: Mon, 19 Apr 2021 11:41:30 +0200 Subject: [PATCH 0354/1317] Use google assistant TemperatureControl trait to report sensor (#46491) * CHG: use TemperatureControl trait to report sensor * fixup: blacked * fixup: flaked * fixup: flaked * Adjust tests * fixup test and rebase * test coverage --- .../components/google_assistant/trait.py | 180 ++++++++++-------- .../components/google_assistant/test_trait.py | 45 ++++- 2 files changed, 135 insertions(+), 90 deletions(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 25013dad1713a..7a1e1f9d94143 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -88,6 +88,7 @@ TRAIT_COLOR_SETTING = f"{PREFIX_TRAITS}ColorSetting" TRAIT_SCENE = f"{PREFIX_TRAITS}Scene" TRAIT_TEMPERATURE_SETTING = f"{PREFIX_TRAITS}TemperatureSetting" +TRAIT_TEMPERATURE_CONTROL = f"{PREFIX_TRAITS}TemperatureControl" TRAIT_LOCKUNLOCK = f"{PREFIX_TRAITS}LockUnlock" TRAIT_FANSPEED = f"{PREFIX_TRAITS}FanSpeed" TRAIT_MODES = f"{PREFIX_TRAITS}Modes" @@ -683,6 +684,52 @@ async def _execute_cover(self, command, data, params, challenge): ) +@register_trait +class TemperatureControlTrait(_Trait): + """Trait for devices (other than thermostats) that support controlling temperature. Workaround for Temperature sensors. + + https://developers.google.com/assistant/smarthome/traits/temperaturecontrol + """ + + name = TRAIT_TEMPERATURE_CONTROL + + @staticmethod + def supported(domain, features, device_class, _): + """Test if state is supported.""" + return ( + domain == sensor.DOMAIN and device_class == sensor.DEVICE_CLASS_TEMPERATURE + ) + + def sync_attributes(self): + """Return temperature attributes for a sync request.""" + return { + "temperatureUnitForUX": _google_temp_unit( + self.hass.config.units.temperature_unit + ), + "queryOnlyTemperatureSetting": True, + "temperatureRange": { + "minThresholdCelsius": -100, + "maxThresholdCelsius": 100, + }, + } + + def query_attributes(self): + """Return temperature states.""" + response = {} + unit = self.hass.config.units.temperature_unit + current_temp = self.state.state + if current_temp not in (STATE_UNKNOWN, STATE_UNAVAILABLE): + temp = round(temp_util.convert(float(current_temp), unit, TEMP_CELSIUS), 1) + response["temperatureSetpointCelsius"] = temp + response["temperatureAmbientCelsius"] = temp + + return response + + async def execute(self, command, data, params, challenge): + """Unsupported.""" + raise SmartHomeError(ERR_NOT_SUPPORTED, "Execute is not supported by sensor") + + @register_trait class TemperatureSettingTrait(_Trait): """Trait to offer handling both temperature point and modes functionality. @@ -715,12 +762,7 @@ class TemperatureSettingTrait(_Trait): @staticmethod def supported(domain, features, device_class, _): """Test if state is supported.""" - if domain == climate.DOMAIN: - return True - - return ( - domain == sensor.DOMAIN and device_class == sensor.DEVICE_CLASS_TEMPERATURE - ) + return domain == climate.DOMAIN @property def climate_google_modes(self): @@ -743,32 +785,24 @@ def climate_google_modes(self): def sync_attributes(self): """Return temperature point and modes attributes for a sync request.""" response = {} - attrs = self.state.attributes - domain = self.state.domain response["thermostatTemperatureUnit"] = _google_temp_unit( self.hass.config.units.temperature_unit ) - if domain == sensor.DOMAIN: - device_class = attrs.get(ATTR_DEVICE_CLASS) - if device_class == sensor.DEVICE_CLASS_TEMPERATURE: - response["queryOnlyTemperatureSetting"] = True - - elif domain == climate.DOMAIN: - modes = self.climate_google_modes + modes = self.climate_google_modes - # Some integrations don't support modes (e.g. opentherm), but Google doesn't - # support changing the temperature if we don't have any modes. If there's - # only one Google doesn't support changing it, so the default mode here is - # only cosmetic. - if len(modes) == 0: - modes.append("heat") + # Some integrations don't support modes (e.g. opentherm), but Google doesn't + # support changing the temperature if we don't have any modes. If there's + # only one Google doesn't support changing it, so the default mode here is + # only cosmetic. + if len(modes) == 0: + modes.append("heat") - if "off" in modes and any( - mode in modes for mode in ("heatcool", "heat", "cool") - ): - modes.append("on") - response["availableThermostatModes"] = modes + if "off" in modes and any( + mode in modes for mode in ("heatcool", "heat", "cool") + ): + modes.append("on") + response["availableThermostatModes"] = modes return response @@ -776,76 +810,60 @@ def query_attributes(self): """Return temperature point and modes query attributes.""" response = {} attrs = self.state.attributes - domain = self.state.domain unit = self.hass.config.units.temperature_unit - if domain == sensor.DOMAIN: - device_class = attrs.get(ATTR_DEVICE_CLASS) - if device_class == sensor.DEVICE_CLASS_TEMPERATURE: - current_temp = self.state.state - if current_temp not in (STATE_UNKNOWN, STATE_UNAVAILABLE): - response["thermostatTemperatureAmbient"] = round( - temp_util.convert(float(current_temp), unit, TEMP_CELSIUS), 1 - ) - elif domain == climate.DOMAIN: - operation = self.state.state - preset = attrs.get(climate.ATTR_PRESET_MODE) - supported = attrs.get(ATTR_SUPPORTED_FEATURES, 0) + operation = self.state.state + preset = attrs.get(climate.ATTR_PRESET_MODE) + supported = attrs.get(ATTR_SUPPORTED_FEATURES, 0) - if preset in self.preset_to_google: - response["thermostatMode"] = self.preset_to_google[preset] - else: - response["thermostatMode"] = self.hvac_to_google.get(operation) + if preset in self.preset_to_google: + response["thermostatMode"] = self.preset_to_google[preset] + else: + response["thermostatMode"] = self.hvac_to_google.get(operation) - current_temp = attrs.get(climate.ATTR_CURRENT_TEMPERATURE) - if current_temp is not None: - response["thermostatTemperatureAmbient"] = round( - temp_util.convert(current_temp, unit, TEMP_CELSIUS), 1 - ) + current_temp = attrs.get(climate.ATTR_CURRENT_TEMPERATURE) + if current_temp is not None: + response["thermostatTemperatureAmbient"] = round( + temp_util.convert(current_temp, unit, TEMP_CELSIUS), 1 + ) - current_humidity = attrs.get(climate.ATTR_CURRENT_HUMIDITY) - if current_humidity is not None: - response["thermostatHumidityAmbient"] = current_humidity - - if operation in (climate.HVAC_MODE_AUTO, climate.HVAC_MODE_HEAT_COOL): - if supported & climate.SUPPORT_TARGET_TEMPERATURE_RANGE: - response["thermostatTemperatureSetpointHigh"] = round( - temp_util.convert( - attrs[climate.ATTR_TARGET_TEMP_HIGH], unit, TEMP_CELSIUS - ), - 1, - ) - response["thermostatTemperatureSetpointLow"] = round( - temp_util.convert( - attrs[climate.ATTR_TARGET_TEMP_LOW], unit, TEMP_CELSIUS - ), - 1, - ) - else: - target_temp = attrs.get(ATTR_TEMPERATURE) - if target_temp is not None: - target_temp = round( - temp_util.convert(target_temp, unit, TEMP_CELSIUS), 1 - ) - response["thermostatTemperatureSetpointHigh"] = target_temp - response["thermostatTemperatureSetpointLow"] = target_temp + current_humidity = attrs.get(climate.ATTR_CURRENT_HUMIDITY) + if current_humidity is not None: + response["thermostatHumidityAmbient"] = current_humidity + + if operation in (climate.HVAC_MODE_AUTO, climate.HVAC_MODE_HEAT_COOL): + if supported & climate.SUPPORT_TARGET_TEMPERATURE_RANGE: + response["thermostatTemperatureSetpointHigh"] = round( + temp_util.convert( + attrs[climate.ATTR_TARGET_TEMP_HIGH], unit, TEMP_CELSIUS + ), + 1, + ) + response["thermostatTemperatureSetpointLow"] = round( + temp_util.convert( + attrs[climate.ATTR_TARGET_TEMP_LOW], unit, TEMP_CELSIUS + ), + 1, + ) else: target_temp = attrs.get(ATTR_TEMPERATURE) if target_temp is not None: - response["thermostatTemperatureSetpoint"] = round( + target_temp = round( temp_util.convert(target_temp, unit, TEMP_CELSIUS), 1 ) + response["thermostatTemperatureSetpointHigh"] = target_temp + response["thermostatTemperatureSetpointLow"] = target_temp + else: + target_temp = attrs.get(ATTR_TEMPERATURE) + if target_temp is not None: + response["thermostatTemperatureSetpoint"] = round( + temp_util.convert(target_temp, unit, TEMP_CELSIUS), 1 + ) return response async def execute(self, command, data, params, challenge): """Execute a temperature point or mode command.""" - domain = self.state.domain - if domain == sensor.DOMAIN: - raise SmartHomeError( - ERR_NOT_SUPPORTED, "Execute is not supported by sensor" - ) - # All sent in temperatures are always in Celsius unit = self.hass.config.units.temperature_unit min_temp = self.state.attributes[climate.ATTR_MIN_TEMP] diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 1d70027024a46..3d506be644d75 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -954,6 +954,29 @@ async def test_temperature_setting_climate_setpoint_auto(hass): assert calls[0].data == {ATTR_ENTITY_ID: "climate.bla", ATTR_TEMPERATURE: 19} +async def test_temperature_control(hass): + """Test TemperatureControl trait support for sensor domain.""" + hass.config.units.temperature_unit = TEMP_CELSIUS + + trt = trait.TemperatureControlTrait( + hass, + State("sensor.temp", 18), + BASIC_CONFIG, + ) + assert trt.sync_attributes() == { + "queryOnlyTemperatureSetting": True, + "temperatureUnitForUX": "C", + "temperatureRange": {"maxThresholdCelsius": 100, "minThresholdCelsius": -100}, + } + assert trt.query_attributes() == { + "temperatureSetpointCelsius": 18, + "temperatureAmbientCelsius": 18, + } + with pytest.raises(helpers.SmartHomeError) as err: + await trt.execute(trait.COMMAND_ONOFF, BASIC_DATA, {"on": False}, {}) + assert err.value.code == const.ERR_NOT_SUPPORTED + + async def test_humidity_setting_humidifier_setpoint(hass): """Test HumiditySetting trait support for humidifier domain - setpoint.""" assert helpers.get_google_type(humidifier.DOMAIN, None) is not None @@ -2380,16 +2403,16 @@ async def test_media_player_mute(hass): } -async def test_temperature_setting_sensor(hass): - """Test TemperatureSetting trait support for temperature sensor.""" +async def test_temperature_control_sensor(hass): + """Test TemperatureControl trait support for temperature sensor.""" assert ( helpers.get_google_type(sensor.DOMAIN, sensor.DEVICE_CLASS_TEMPERATURE) is not None ) - assert not trait.TemperatureSettingTrait.supported( + assert not trait.TemperatureControlTrait.supported( sensor.DOMAIN, 0, sensor.DEVICE_CLASS_HUMIDITY, None ) - assert trait.TemperatureSettingTrait.supported( + assert trait.TemperatureControlTrait.supported( sensor.DOMAIN, 0, sensor.DEVICE_CLASS_TEMPERATURE, None ) @@ -2403,11 +2426,11 @@ async def test_temperature_setting_sensor(hass): (TEMP_FAHRENHEIT, "F", "unknown", None), ], ) -async def test_temperature_setting_sensor_data(hass, unit_in, unit_out, state, ambient): - """Test TemperatureSetting trait support for temperature sensor.""" +async def test_temperature_control_sensor_data(hass, unit_in, unit_out, state, ambient): + """Test TemperatureControl trait support for temperature sensor.""" hass.config.units.temperature_unit = unit_in - trt = trait.TemperatureSettingTrait( + trt = trait.TemperatureControlTrait( hass, State( "sensor.test", state, {ATTR_DEVICE_CLASS: sensor.DEVICE_CLASS_TEMPERATURE} @@ -2417,11 +2440,15 @@ async def test_temperature_setting_sensor_data(hass, unit_in, unit_out, state, a assert trt.sync_attributes() == { "queryOnlyTemperatureSetting": True, - "thermostatTemperatureUnit": unit_out, + "temperatureUnitForUX": unit_out, + "temperatureRange": {"maxThresholdCelsius": 100, "minThresholdCelsius": -100}, } if ambient: - assert trt.query_attributes() == {"thermostatTemperatureAmbient": ambient} + assert trt.query_attributes() == { + "temperatureAmbientCelsius": ambient, + "temperatureSetpointCelsius": ambient, + } else: assert trt.query_attributes() == {} hass.config.units.temperature_unit = TEMP_CELSIUS From 26cb511d02ea1b4846dcc5907c99731d04c7e07c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Apr 2021 12:18:06 +0200 Subject: [PATCH 0355/1317] Bump codecov/codecov-action from v1.3.2 to v1.4.0 (#49412) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from v1.3.2 to v1.4.0. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/master/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v1.3.2...0e28ff86a50029a44d10df6ed4c308711925a6a8) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 71b69ebfe8619..16665acc9cb08 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -739,4 +739,4 @@ jobs: coverage report --fail-under=94 coverage xml - name: Upload coverage to Codecov - uses: codecov/codecov-action@v1.3.2 + uses: codecov/codecov-action@v1.4.0 From 4361be613d10cbd1461b066a6880d6b3fcfd996a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 19 Apr 2021 01:25:30 -1000 Subject: [PATCH 0356/1317] Expose the hostname of the device in asuswrt (#49393) --- homeassistant/components/asuswrt/device_tracker.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/asuswrt/device_tracker.py b/homeassistant/components/asuswrt/device_tracker.py index bf5d120c4761a..dabbc25ba107e 100644 --- a/homeassistant/components/asuswrt/device_tracker.py +++ b/homeassistant/components/asuswrt/device_tracker.py @@ -87,6 +87,11 @@ def extra_state_attributes(self) -> dict[str, any]: ) return attrs + @property + def hostname(self) -> str: + """Return the hostname of device.""" + return self._device.name + @property def ip_address(self) -> str: """Return the primary ip address of the device.""" From 2de257f85f2a6a9ff408f92701410828b40e122f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 19 Apr 2021 13:48:31 +0200 Subject: [PATCH 0357/1317] Upgrade dsmr_parser to 0.29 (#49417) --- homeassistant/components/dsmr/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/dsmr/manifest.json b/homeassistant/components/dsmr/manifest.json index de81d14f2480c..a5c9b8e62bced 100644 --- a/homeassistant/components/dsmr/manifest.json +++ b/homeassistant/components/dsmr/manifest.json @@ -2,7 +2,7 @@ "domain": "dsmr", "name": "DSMR Slimme Meter", "documentation": "https://www.home-assistant.io/integrations/dsmr", - "requirements": ["dsmr_parser==0.28"], + "requirements": ["dsmr_parser==0.29"], "codeowners": ["@Robbie1221"], "config_flow": false, "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 18816b89c9393..a6b707f8173cf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -506,7 +506,7 @@ doorbirdpy==2.1.0 dovado==0.4.1 # homeassistant.components.dsmr -dsmr_parser==0.28 +dsmr_parser==0.29 # homeassistant.components.dwd_weather_warnings dwdwfsapi==1.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 650655c28d707..e39c69ce0e762 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -279,7 +279,7 @@ distro==1.5.0 doorbirdpy==2.1.0 # homeassistant.components.dsmr -dsmr_parser==0.28 +dsmr_parser==0.29 # homeassistant.components.dynalite dynalite_devices==0.1.46 From 69932d44354746673367c36fe41c19152ab4672f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 19 Apr 2021 02:03:31 -1000 Subject: [PATCH 0358/1317] Add additional myq homekit models (#49381) --- homeassistant/components/myq/manifest.json | 2 +- homeassistant/generated/zeroconf.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/myq/manifest.json b/homeassistant/components/myq/manifest.json index 407e5b7df19c9..a93501c941f38 100644 --- a/homeassistant/components/myq/manifest.json +++ b/homeassistant/components/myq/manifest.json @@ -6,7 +6,7 @@ "codeowners": ["@bdraco"], "config_flow": true, "homekit": { - "models": ["819LMB"] + "models": ["819LMB", "MYQ"] }, "iot_class": "cloud_polling", "dhcp": [{ "macaddress": "645299*" }] diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index b3fa7064aee34..03f06fbc4c127 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -194,6 +194,7 @@ "Healty Home Coach": "netatmo", "Iota": "abode", "LIFX": "lifx", + "MYQ": "myq", "Netatmo Relay": "netatmo", "PowerView": "hunterdouglas_powerview", "Presence": "netatmo", From 83ecabe0a221cb228055957eb312ba04f2456369 Mon Sep 17 00:00:00 2001 From: Daniel Rheinbay Date: Mon, 19 Apr 2021 14:25:46 +0200 Subject: [PATCH 0359/1317] Bump fritzconnection to 1.4.2 (#49356) --- homeassistant/components/fritz/manifest.json | 2 +- homeassistant/components/fritzbox_callmonitor/manifest.json | 2 +- homeassistant/components/fritzbox_netmonitor/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/fritz/manifest.json b/homeassistant/components/fritz/manifest.json index 0b9a2a8302d1c..522c7574b0625 100644 --- a/homeassistant/components/fritz/manifest.json +++ b/homeassistant/components/fritz/manifest.json @@ -2,7 +2,7 @@ "domain": "fritz", "name": "AVM FRITZ!Box", "documentation": "https://www.home-assistant.io/integrations/fritz", - "requirements": ["fritzconnection==1.4.0"], + "requirements": ["fritzconnection==1.4.2"], "codeowners": [], "iot_class": "local_polling" } diff --git a/homeassistant/components/fritzbox_callmonitor/manifest.json b/homeassistant/components/fritzbox_callmonitor/manifest.json index 6c92cfab458aa..531fa13e23292 100644 --- a/homeassistant/components/fritzbox_callmonitor/manifest.json +++ b/homeassistant/components/fritzbox_callmonitor/manifest.json @@ -3,7 +3,7 @@ "name": "AVM FRITZ!Box Call Monitor", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/fritzbox_callmonitor", - "requirements": ["fritzconnection==1.4.0"], + "requirements": ["fritzconnection==1.4.2"], "codeowners": [], "iot_class": "local_polling" } diff --git a/homeassistant/components/fritzbox_netmonitor/manifest.json b/homeassistant/components/fritzbox_netmonitor/manifest.json index d0406c99dfab5..b52872fc04489 100644 --- a/homeassistant/components/fritzbox_netmonitor/manifest.json +++ b/homeassistant/components/fritzbox_netmonitor/manifest.json @@ -2,7 +2,7 @@ "domain": "fritzbox_netmonitor", "name": "AVM FRITZ!Box Net Monitor", "documentation": "https://www.home-assistant.io/integrations/fritzbox_netmonitor", - "requirements": ["fritzconnection==1.4.0"], + "requirements": ["fritzconnection==1.4.2"], "codeowners": [], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index a6b707f8173cf..7f298e428e1bc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -623,7 +623,7 @@ freesms==0.1.2 # homeassistant.components.fritz # homeassistant.components.fritzbox_callmonitor # homeassistant.components.fritzbox_netmonitor -fritzconnection==1.4.0 +fritzconnection==1.4.2 # homeassistant.components.google_translate gTTS==2.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e39c69ce0e762..2cab2ae8964b2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -329,7 +329,7 @@ freebox-api==0.0.10 # homeassistant.components.fritz # homeassistant.components.fritzbox_callmonitor # homeassistant.components.fritzbox_netmonitor -fritzconnection==1.4.0 +fritzconnection==1.4.2 # homeassistant.components.google_translate gTTS==2.2.2 From a968dea1525f1c4f303a6a1055ef8838e96f630e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 19 Apr 2021 14:45:01 +0200 Subject: [PATCH 0360/1317] Fix deadlock when restarting scripts (#49410) --- homeassistant/helpers/script.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 7103fe17ac971..12f75960c4129 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -1212,13 +1212,8 @@ async def async_run( raise async def _async_stop( - self, update_state: bool, spare: _ScriptRun | None = None + self, aws: list[asyncio.Task], update_state: bool, spare: _ScriptRun | None ) -> None: - aws = [ - asyncio.create_task(run.async_stop()) for run in self._runs if run != spare - ] - if not aws: - return await asyncio.wait(aws) if update_state: self._changed() @@ -1227,7 +1222,15 @@ async def async_stop( self, update_state: bool = True, spare: _ScriptRun | None = None ) -> None: """Stop running script.""" - await asyncio.shield(self._async_stop(update_state, spare)) + # Collect a a list of script runs to stop. This must be done before calling + # asyncio.shield as asyncio.shield yields to the event loop, which would cause + # us to wait for script runs added after the call to async_stop. + aws = [ + asyncio.create_task(run.async_stop()) for run in self._runs if run != spare + ] + if not aws: + return + await asyncio.shield(self._async_stop(aws, update_state, spare)) async def _async_get_condition(self, config): if isinstance(config, template.Template): From 05755c27f2ef89b8fc7e3dbd167604a1f504185a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Z=C3=A1hradn=C3=ADk?= Date: Mon, 19 Apr 2021 16:52:08 +0200 Subject: [PATCH 0361/1317] Log an error if modbus Cover is not initialized correctly (#48829) --- homeassistant/components/modbus/cover.py | 8 +++++ tests/components/modbus/conftest.py | 8 ++++- tests/components/modbus/test_modbus_cover.py | 33 +++++++++++++++++++- 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/modbus/cover.py b/homeassistant/components/modbus/cover.py index 3a1c1c565368b..4b0fa1aee8761 100644 --- a/homeassistant/components/modbus/cover.py +++ b/homeassistant/components/modbus/cover.py @@ -2,6 +2,7 @@ from __future__ import annotations from datetime import timedelta +import logging from typing import Any from pymodbus.exceptions import ConnectionException, ModbusException @@ -35,6 +36,8 @@ ) from .modbus import ModbusHub +_LOGGER = logging.getLogger(__name__) + async def async_setup_platform( hass: HomeAssistant, @@ -44,6 +47,11 @@ async def async_setup_platform( ): """Read configuration and create Modbus cover.""" if discovery_info is None: + _LOGGER.warning( + "You're trying to init Modbus Cover in an unsupported way." + " Check https://www.home-assistant.io/integrations/modbus/#configuring-platform-cover" + " and fix your configuration" + ) return covers = [] diff --git a/tests/components/modbus/conftest.py b/tests/components/modbus/conftest.py index 2c83f40546f3d..761f2c7e1413e 100644 --- a/tests/components/modbus/conftest.py +++ b/tests/components/modbus/conftest.py @@ -44,6 +44,7 @@ async def base_test( check_config_only=False, config_modbus=None, scan_interval=None, + expect_init_to_fail=False, ): """Run test on device for given config.""" @@ -107,7 +108,10 @@ async def base_test( if config_device is not None: entity_id = f"{entity_domain}.{device_name}" device = hass.states.get(entity_id) - if device is None: + + if expect_init_to_fail: + assert device is None + elif device is None: pytest.fail("CONFIG failed, see output") if check_config_only: return @@ -132,6 +136,7 @@ async def base_config_test( array_name_old_config, method_discovery=False, config_modbus=None, + expect_init_to_fail=False, ): """Check config of device for given config.""" @@ -147,4 +152,5 @@ async def base_config_test( method_discovery=method_discovery, check_config_only=True, config_modbus=config_modbus, + expect_init_to_fail=expect_init_to_fail, ) diff --git a/tests/components/modbus/test_modbus_cover.py b/tests/components/modbus/test_modbus_cover.py index b101c6784d551..eddaa6099d7e2 100644 --- a/tests/components/modbus/test_modbus_cover.py +++ b/tests/components/modbus/test_modbus_cover.py @@ -1,4 +1,6 @@ """The tests for the Modbus cover component.""" +import logging + import pytest from homeassistant.components.cover import DOMAIN as COVER_DOMAIN @@ -117,7 +119,7 @@ async def test_coil_cover(hass, regs, expected): ), ], ) -async def test_register_COVER(hass, regs, expected): +async def test_register_cover(hass, regs, expected): """Run test for given config.""" cover_name = "modbus_test_cover" state = await base_test( @@ -137,3 +139,32 @@ async def test_register_COVER(hass, regs, expected): scan_interval=5, ) assert state == expected + + +@pytest.mark.parametrize("read_type", [CALL_TYPE_COIL, CONF_REGISTER]) +async def test_unsupported_config_cover(hass, read_type, caplog): + """ + Run test for cover. + + Initialize the Cover in the legacy manner via platform. + This test expects that the Cover won't be initialized, and that we get a config warning. + """ + device_name = "test_cover" + device_config = {CONF_NAME: device_name, read_type: 1234} + + caplog.set_level(logging.WARNING) + caplog.clear() + + await base_config_test( + hass, + device_config, + device_name, + COVER_DOMAIN, + CONF_COVERS, + None, + method_discovery=False, + expect_init_to_fail=True, + ) + + assert len(caplog.records) == 1 + assert caplog.records[0].levelname == "WARNING" From fe2f4e27905a2003a6a9b884672b6d474345e1b4 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 16 Apr 2021 22:33:58 +0200 Subject: [PATCH 0362/1317] Apply Precision/Scale/Offset to struct in modbus sensor (#48544) The single values in struct are corrected with presicion, scale and offset, just as it is done with single values. --- homeassistant/components/modbus/sensor.py | 14 ++++++- tests/components/modbus/test_modbus_sensor.py | 41 +++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index 7aa08070d6765..b2b8e27b8c8b3 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -319,7 +319,19 @@ def _update(self): # If unpack() returns a tuple greater than 1, don't try to process the value. # Instead, return the values of unpack(...) separated by commas. if len(val) > 1: - self._value = ",".join(map(str, val)) + # Apply scale and precision to floats and ints + v_result = [] + for entry in val: + v_temp = self._scale * entry + self._offset + + # We could convert int to float, and the code would still work; however + # we lose some precision, and unit tests will fail. Therefore, we do + # the conversion only when it's absolutely necessary. + if isinstance(v_temp, int) and self._precision == 0: + v_result.append(str(v_temp)) + else: + v_result.append(f"{float(v_temp):.{self._precision}f}") + self._value = ",".join(map(str, v_result)) else: val = val[0] diff --git a/tests/components/modbus/test_modbus_sensor.py b/tests/components/modbus/test_modbus_sensor.py index dd485e59835a2..516979b22d27c 100644 --- a/tests/components/modbus/test_modbus_sensor.py +++ b/tests/components/modbus/test_modbus_sensor.py @@ -14,6 +14,7 @@ CONF_REVERSE_ORDER, CONF_SCALE, CONF_SENSORS, + DATA_TYPE_CUSTOM, DATA_TYPE_FLOAT, DATA_TYPE_INT, DATA_TYPE_STRING, @@ -26,6 +27,7 @@ CONF_NAME, CONF_OFFSET, CONF_SLAVE, + CONF_STRUCTURE, ) from .conftest import base_config_test, base_test @@ -338,6 +340,7 @@ async def test_config_sensor(hass, do_discovery, do_config): ) async def test_all_sensor(hass, cfg, regs, expected): """Run test for sensor.""" + sensor_name = "modbus_test_sensor" state = await base_test( hass, @@ -352,3 +355,41 @@ async def test_all_sensor(hass, cfg, regs, expected): scan_interval=5, ) assert state == expected + + +async def test_struct_sensor(hass): + """Run test for sensor struct.""" + + sensor_name = "modbus_test_sensor" + # floats: 7.931250095367432, 10.600000381469727, + # 1.000879611487865e-28, 10.566553115844727 + expected = "7.93,10.60,0.00,10.57" + state = await base_test( + hass, + { + CONF_NAME: sensor_name, + CONF_REGISTER: 1234, + CONF_COUNT: 8, + CONF_PRECISION: 2, + CONF_DATA_TYPE: DATA_TYPE_CUSTOM, + CONF_STRUCTURE: ">4f", + }, + sensor_name, + SENSOR_DOMAIN, + CONF_SENSORS, + CONF_REGISTERS, + [ + 0x40FD, + 0xCCCD, + 0x4129, + 0x999A, + 0x10FD, + 0xC0CD, + 0x4129, + 0x109A, + ], + expected, + method_discovery=False, + scan_interval=5, + ) + assert state == expected From fc4c49ab8319b9e36dded5b47738bcf816c24c08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Fri, 16 Apr 2021 20:17:46 +0200 Subject: [PATCH 0363/1317] Upgrade pyMetno to 0.8.2 (#49308) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Hjelseth Høyer --- homeassistant/components/met/manifest.json | 2 +- homeassistant/components/norway_air/air_quality.py | 2 +- homeassistant/components/norway_air/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/met/manifest.json b/homeassistant/components/met/manifest.json index 38b77a0afd2df..2724818ad494c 100644 --- a/homeassistant/components/met/manifest.json +++ b/homeassistant/components/met/manifest.json @@ -3,6 +3,6 @@ "name": "Meteorologisk institutt (Met.no)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/met", - "requirements": ["pyMetno==0.8.1"], + "requirements": ["pyMetno==0.8.2"], "codeowners": ["@danielhiversen", "@thimic"] } diff --git a/homeassistant/components/norway_air/air_quality.py b/homeassistant/components/norway_air/air_quality.py index 788f900ef70be..480121846e954 100644 --- a/homeassistant/components/norway_air/air_quality.py +++ b/homeassistant/components/norway_air/air_quality.py @@ -67,7 +67,7 @@ def _decorator(self): class AirSensor(AirQualityEntity): - """Representation of an Yr.no sensor.""" + """Representation of an air quality sensor.""" def __init__(self, name, coordinates, forecast, session): """Initialize the sensor.""" diff --git a/homeassistant/components/norway_air/manifest.json b/homeassistant/components/norway_air/manifest.json index 193d96e2a189b..5306fa8e3e640 100644 --- a/homeassistant/components/norway_air/manifest.json +++ b/homeassistant/components/norway_air/manifest.json @@ -2,6 +2,6 @@ "domain": "norway_air", "name": "Om Luftkvalitet i Norge (Norway Air)", "documentation": "https://www.home-assistant.io/integrations/norway_air", - "requirements": ["pyMetno==0.8.1"], + "requirements": ["pyMetno==0.8.2"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index df61a7be783c4..704ac3fdf01b6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1229,7 +1229,7 @@ pyHS100==0.3.5.2 # homeassistant.components.met # homeassistant.components.norway_air -pyMetno==0.8.1 +pyMetno==0.8.2 # homeassistant.components.rfxtrx pyRFXtrx==0.26.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5c2fdb06cb040..4808f3c7c2ee9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -648,7 +648,7 @@ pyHS100==0.3.5.2 # homeassistant.components.met # homeassistant.components.norway_air -pyMetno==0.8.1 +pyMetno==0.8.2 # homeassistant.components.rfxtrx pyRFXtrx==0.26.1 From 7ed8f00075a9fcd6664f311e1a5ea890e2b16ffb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 17 Apr 2021 21:48:02 -1000 Subject: [PATCH 0364/1317] Fix exception in roomba discovery when the device does not respond on the first try (#49360) --- .../components/roomba/config_flow.py | 5 +- tests/components/roomba/test_config_flow.py | 64 ++++++++++++++++++- 2 files changed, 65 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/roomba/config_flow.py b/homeassistant/components/roomba/config_flow.py index 45c2d8b9a1bdc..92d9ff05dc00b 100644 --- a/homeassistant/components/roomba/config_flow.py +++ b/homeassistant/components/roomba/config_flow.py @@ -328,9 +328,8 @@ async def _async_discover_roombas(hass, host): discovery = _async_get_roomba_discovery() try: if host: - discovered = [ - await hass.async_add_executor_job(discovery.get, host) - ] + device = await hass.async_add_executor_job(discovery.get, host) + discovered = [device] if device else [] else: discovered = await hass.async_add_executor_job(discovery.get_all) except OSError: diff --git a/tests/components/roomba/test_config_flow.py b/tests/components/roomba/test_config_flow.py index ee3b7d4b49752..a15ad7e43a676 100644 --- a/tests/components/roomba/test_config_flow.py +++ b/tests/components/roomba/test_config_flow.py @@ -711,7 +711,7 @@ async def test_dhcp_discovery_and_roomba_discovery_finds(hass, discovery_data): @pytest.mark.parametrize("discovery_data", DHCP_DISCOVERY_DEVICES_WITHOUT_MATCHING_IP) async def test_dhcp_discovery_falls_back_to_manual(hass, discovery_data): - """Test we can process the discovery from dhcp but roomba discovery cannot find the device.""" + """Test we can process the discovery from dhcp but roomba discovery cannot find the specific device.""" await setup.async_setup_component(hass, "persistent_notification", {}) mocked_roomba = _create_mocked_roomba( @@ -782,6 +782,68 @@ async def test_dhcp_discovery_falls_back_to_manual(hass, discovery_data): assert len(mock_setup_entry.mock_calls) == 1 +@pytest.mark.parametrize("discovery_data", DHCP_DISCOVERY_DEVICES_WITHOUT_MATCHING_IP) +async def test_dhcp_discovery_no_devices_falls_back_to_manual(hass, discovery_data): + """Test we can process the discovery from dhcp but roomba discovery cannot find any devices.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + mocked_roomba = _create_mocked_roomba( + roomba_connected=True, + master_state={"state": {"reported": {"name": "myroomba"}}}, + ) + + with patch( + "homeassistant.components.roomba.config_flow.RoombaDiscovery", + _mocked_no_devices_found_discovery, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=discovery_data, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] is None + assert result["step_id"] == "manual" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: MOCK_IP, CONF_BLID: "blid"}, + ) + await hass.async_block_till_done() + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] is None + + with patch( + "homeassistant.components.roomba.config_flow.Roomba", + return_value=mocked_roomba, + ), patch( + "homeassistant.components.roomba.config_flow.RoombaPassword", + _mocked_getpassword, + ), patch( + "homeassistant.components.roomba.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["title"] == "myroomba" + assert result3["result"].unique_id == "BLID" + assert result3["data"] == { + CONF_BLID: "BLID", + CONF_CONTINUOUS: True, + CONF_DELAY: 1, + CONF_HOST: MOCK_IP, + CONF_PASSWORD: "password", + } + assert len(mock_setup_entry.mock_calls) == 1 + + async def test_dhcp_discovery_with_ignored(hass): """Test ignored entries do not break checking for existing entries.""" await setup.async_setup_component(hass, "persistent_notification", {}) From eb9ba527d0d6c4df25272cc63aa11c9ccdb573aa Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 19 Apr 2021 17:18:15 +0200 Subject: [PATCH 0365/1317] Add pymodbus exception handling and isolate pymodbus to class modbusHub (#49052) --- .../components/modbus/binary_sensor.py | 17 +- homeassistant/components/modbus/climate.py | 40 ++-- homeassistant/components/modbus/cover.py | 57 ++---- homeassistant/components/modbus/modbus.py | 187 ++++++++++++------ homeassistant/components/modbus/sensor.py | 25 +-- homeassistant/components/modbus/switch.py | 51 ++--- 6 files changed, 186 insertions(+), 191 deletions(-) diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py index 32f3527f8018c..0a76baf1fda5c 100644 --- a/homeassistant/components/modbus/binary_sensor.py +++ b/homeassistant/components/modbus/binary_sensor.py @@ -4,8 +4,6 @@ from datetime import timedelta import logging -from pymodbus.exceptions import ConnectionException, ModbusException -from pymodbus.pdu import ExceptionResponse import voluptuous as vol from homeassistant.components.binary_sensor import ( @@ -165,16 +163,11 @@ def available(self) -> bool: def _update(self): """Update the state of the sensor.""" - try: - if self._input_type == CALL_TYPE_COIL: - result = self._hub.read_coils(self._slave, self._address, 1) - else: - result = self._hub.read_discrete_inputs(self._slave, self._address, 1) - except ConnectionException: - self._available = False - return - - if isinstance(result, (ModbusException, ExceptionResponse)): + if self._input_type == CALL_TYPE_COIL: + result = self._hub.read_coils(self._slave, self._address, 1) + else: + result = self._hub.read_discrete_inputs(self._slave, self._address, 1) + if result is None: self._available = False return diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index 25893ce008095..7d326407c3b57 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -6,9 +6,6 @@ import struct from typing import Any -from pymodbus.exceptions import ConnectionException, ModbusException -from pymodbus.pdu import ExceptionResponse - from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( HVAC_MODE_AUTO, @@ -212,7 +209,11 @@ def set_temperature(self, **kwargs): ) byte_string = struct.pack(self._structure, target_temperature) register_value = struct.unpack(">h", byte_string[0:2])[0] - self._write_register(self._target_temperature_register, register_value) + self._available = self._hub.write_registers( + self._slave, + self._target_temperature_register, + register_value, + ) self._update() @property @@ -233,20 +234,13 @@ def _update(self): def _read_register(self, register_type, register) -> float | None: """Read register using the Modbus hub slave.""" - try: - if register_type == CALL_TYPE_REGISTER_INPUT: - result = self._hub.read_input_registers( - self._slave, register, self._count - ) - else: - result = self._hub.read_holding_registers( - self._slave, register, self._count - ) - except ConnectionException: - self._available = False - return - - if isinstance(result, (ModbusException, ExceptionResponse)): + if register_type == CALL_TYPE_REGISTER_INPUT: + result = self._hub.read_input_registers(self._slave, register, self._count) + else: + result = self._hub.read_holding_registers( + self._slave, register, self._count + ) + if result is None: self._available = False return @@ -269,13 +263,3 @@ def _read_register(self, register_type, register) -> float | None: self._available = True return register_value - - def _write_register(self, register, value): - """Write holding register using the Modbus hub slave.""" - try: - self._hub.write_registers(self._slave, register, value) - except ConnectionException: - self._available = False - return - - self._available = True diff --git a/homeassistant/components/modbus/cover.py b/homeassistant/components/modbus/cover.py index 4b0fa1aee8761..dc3da1faa78be 100644 --- a/homeassistant/components/modbus/cover.py +++ b/homeassistant/components/modbus/cover.py @@ -5,9 +5,6 @@ import logging from typing import Any -from pymodbus.exceptions import ConnectionException, ModbusException -from pymodbus.pdu import ExceptionResponse - from homeassistant.components.cover import SUPPORT_CLOSE, SUPPORT_OPEN, CoverEntity from homeassistant.const import ( CONF_COVERS, @@ -187,22 +184,17 @@ def _update(self): def _read_status_register(self) -> int | None: """Read status register using the Modbus hub slave.""" - try: - if self._status_register_type == CALL_TYPE_REGISTER_INPUT: - result = self._hub.read_input_registers( - self._slave, self._status_register, 1 - ) - else: - result = self._hub.read_holding_registers( - self._slave, self._status_register, 1 - ) - except ConnectionException: - self._available = False - return - - if isinstance(result, (ModbusException, ExceptionResponse)): + if self._status_register_type == CALL_TYPE_REGISTER_INPUT: + result = self._hub.read_input_registers( + self._slave, self._status_register, 1 + ) + else: + result = self._hub.read_holding_registers( + self._slave, self._status_register, 1 + ) + if result is None: self._available = False - return + return None value = int(result.registers[0]) self._available = True @@ -211,37 +203,18 @@ def _read_status_register(self) -> int | None: def _write_register(self, value): """Write holding register using the Modbus hub slave.""" - try: - self._hub.write_register(self._slave, self._register, value) - except ConnectionException: - self._available = False - return - - self._available = True + self._available = self._hub.write_register(self._slave, self._register, value) def _read_coil(self) -> bool | None: """Read coil using the Modbus hub slave.""" - try: - result = self._hub.read_coils(self._slave, self._coil, 1) - except ConnectionException: + result = self._hub.read_coils(self._slave, self._coil, 1) + if result is None: self._available = False - return - - if isinstance(result, (ModbusException, ExceptionResponse)): - self._available = False - return + return None value = bool(result.bits[0] & 1) - self._available = True - return value def _write_coil(self, value): """Write coil using the Modbus hub slave.""" - try: - self._hub.write_coil(self._slave, self._coil, value) - except ConnectionException: - self._available = False - return - - self._available = True + self._available = self._hub.write_coil(self._slave, self._coil, value) diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index 0a5422ff6be0e..6784357f1e8f8 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -3,6 +3,7 @@ import threading from pymodbus.client.sync import ModbusSerialClient, ModbusTcpClient, ModbusUdpClient +from pymodbus.exceptions import ModbusException from pymodbus.transaction import ModbusRtuFramer from homeassistant.const import ( @@ -37,6 +38,7 @@ CONF_SENSOR, CONF_STOPBITS, CONF_SWITCH, + DEFAULT_HUB, MODBUS_DOMAIN as DOMAIN, SERVICE_WRITE_COIL, SERVICE_WRITE_REGISTER, @@ -49,8 +51,8 @@ def modbus_setup( hass, config, service_write_register_schema, service_write_coil_schema ): """Set up Modbus component.""" - hass.data[DOMAIN] = hub_collect = {} + hass.data[DOMAIN] = hub_collect = {} for conf_hub in config[DOMAIN]: hub_collect[conf_hub[CONF_NAME]] = ModbusHub(conf_hub) @@ -71,15 +73,19 @@ def modbus_setup( def stop_modbus(event): """Stop Modbus service.""" + for client in hub_collect.values(): client.close() + del client def write_register(service): """Write Modbus registers.""" unit = int(float(service.data[ATTR_UNIT])) address = int(float(service.data[ATTR_ADDRESS])) value = service.data[ATTR_VALUE] - client_name = service.data[ATTR_HUB] + client_name = ( + service.data[ATTR_HUB] if ATTR_HUB in service.data else DEFAULT_HUB + ) if isinstance(value, list): hub_collect[client_name].write_registers( unit, address, [int(float(i)) for i in value] @@ -92,7 +98,9 @@ def write_coil(service): unit = service.data[ATTR_UNIT] address = service.data[ATTR_ADDRESS] state = service.data[ATTR_STATE] - client_name = service.data[ATTR_HUB] + client_name = ( + service.data[ATTR_HUB] if ATTR_HUB in service.data else DEFAULT_HUB + ) if isinstance(state, list): hub_collect[client_name].write_coils(unit, address, state) else: @@ -122,6 +130,7 @@ def __init__(self, client_config): # generic configuration self._client = None + self._in_error = False self._lock = threading.Lock() self._config_name = client_config[CONF_NAME] self._config_type = client_config[CONF_TYPE] @@ -140,48 +149,58 @@ def __init__(self, client_config): # network configuration self._config_host = client_config[CONF_HOST] self._config_delay = client_config[CONF_DELAY] - if self._config_delay > 0: - _LOGGER.warning( - "Parameter delay is accepted but not used in this version" - ) + + if self._config_delay > 0: + _LOGGER.warning("Parameter delay is accepted but not used in this version") @property def name(self): """Return the name of this hub.""" return self._config_name + def _log_error(self, exception_error: ModbusException, error_state=True): + if self._in_error: + _LOGGER.debug(str(exception_error)) + else: + _LOGGER.error(str(exception_error)) + self._in_error = error_state + def setup(self): """Set up pymodbus client.""" - if self._config_type == "serial": - self._client = ModbusSerialClient( - method=self._config_method, - port=self._config_port, - baudrate=self._config_baudrate, - stopbits=self._config_stopbits, - bytesize=self._config_bytesize, - parity=self._config_parity, - timeout=self._config_timeout, - retry_on_empty=True, - ) - elif self._config_type == "rtuovertcp": - self._client = ModbusTcpClient( - host=self._config_host, - port=self._config_port, - framer=ModbusRtuFramer, - timeout=self._config_timeout, - ) - elif self._config_type == "tcp": - self._client = ModbusTcpClient( - host=self._config_host, - port=self._config_port, - timeout=self._config_timeout, - ) - elif self._config_type == "udp": - self._client = ModbusUdpClient( - host=self._config_host, - port=self._config_port, - timeout=self._config_timeout, - ) + try: + if self._config_type == "serial": + self._client = ModbusSerialClient( + method=self._config_method, + port=self._config_port, + baudrate=self._config_baudrate, + stopbits=self._config_stopbits, + bytesize=self._config_bytesize, + parity=self._config_parity, + timeout=self._config_timeout, + retry_on_empty=True, + ) + elif self._config_type == "rtuovertcp": + self._client = ModbusTcpClient( + host=self._config_host, + port=self._config_port, + framer=ModbusRtuFramer, + timeout=self._config_timeout, + ) + elif self._config_type == "tcp": + self._client = ModbusTcpClient( + host=self._config_host, + port=self._config_port, + timeout=self._config_timeout, + ) + elif self._config_type == "udp": + self._client = ModbusUdpClient( + host=self._config_host, + port=self._config_port, + timeout=self._config_timeout, + ) + except ModbusException as exception_error: + self._log_error(exception_error, error_state=False) + return # Connect device self.connect() @@ -189,57 +208,115 @@ def setup(self): def close(self): """Disconnect client.""" with self._lock: - self._client.close() + try: + self._client.close() + del self._client + self._client = None + except ModbusException as exception_error: + self._log_error(exception_error, error_state=False) + return def connect(self): """Connect client.""" with self._lock: - self._client.connect() + try: + self._client.connect() + except ModbusException as exception_error: + self._log_error(exception_error, error_state=False) + return def read_coils(self, unit, address, count): """Read coils.""" with self._lock: kwargs = {"unit": unit} if unit else {} - return self._client.read_coils(address, count, **kwargs) + try: + result = self._client.read_coils(address, count, **kwargs) + except ModbusException as exception_error: + self._log_error(exception_error) + return None + self._in_error = False + return result def read_discrete_inputs(self, unit, address, count): """Read discrete inputs.""" with self._lock: kwargs = {"unit": unit} if unit else {} - return self._client.read_discrete_inputs(address, count, **kwargs) + try: + result = self._client.read_discrete_inputs(address, count, **kwargs) + except ModbusException as exception_error: + self._log_error(exception_error) + return None + self._in_error = False + return result def read_input_registers(self, unit, address, count): """Read input registers.""" with self._lock: kwargs = {"unit": unit} if unit else {} - return self._client.read_input_registers(address, count, **kwargs) + try: + result = self._client.read_input_registers(address, count, **kwargs) + except ModbusException as exception_error: + self._log_error(exception_error) + return None + self._in_error = False + return result def read_holding_registers(self, unit, address, count): """Read holding registers.""" with self._lock: kwargs = {"unit": unit} if unit else {} - return self._client.read_holding_registers(address, count, **kwargs) - - def write_coil(self, unit, address, value): + try: + result = self._client.read_holding_registers(address, count, **kwargs) + except ModbusException as exception_error: + self._log_error(exception_error) + return None + self._in_error = False + return result + + def write_coil(self, unit, address, value) -> bool: """Write coil.""" with self._lock: kwargs = {"unit": unit} if unit else {} - self._client.write_coil(address, value, **kwargs) - - def write_coils(self, unit, address, value): + try: + self._client.write_coil(address, value, **kwargs) + except ModbusException as exception_error: + self._log_error(exception_error) + return False + self._in_error = False + return True + + def write_coils(self, unit, address, values) -> bool: """Write coil.""" with self._lock: kwargs = {"unit": unit} if unit else {} - self._client.write_coils(address, value, **kwargs) - - def write_register(self, unit, address, value): + try: + self._client.write_coils(address, values, **kwargs) + except ModbusException as exception_error: + self._log_error(exception_error) + return False + self._in_error = False + return True + + def write_register(self, unit, address, value) -> bool: """Write register.""" with self._lock: kwargs = {"unit": unit} if unit else {} - self._client.write_register(address, value, **kwargs) - - def write_registers(self, unit, address, values): + try: + self._client.write_register(address, value, **kwargs) + except ModbusException as exception_error: + self._log_error(exception_error) + return False + self._in_error = False + return True + + def write_registers(self, unit, address, values) -> bool: """Write registers.""" with self._lock: kwargs = {"unit": unit} if unit else {} - self._client.write_registers(address, values, **kwargs) + try: + self._client.write_registers(address, values, **kwargs) + except ModbusException as exception_error: + self._log_error(exception_error) + return False + self._in_error = False + return True diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index b926ef6c5bd6d..b8cca30be6000 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -6,8 +6,6 @@ import struct from typing import Any -from pymodbus.exceptions import ConnectionException, ModbusException -from pymodbus.pdu import ExceptionResponse import voluptuous as vol from homeassistant.components.sensor import ( @@ -285,20 +283,15 @@ def available(self) -> bool: def _update(self): """Update the state of the sensor.""" - try: - if self._register_type == CALL_TYPE_REGISTER_INPUT: - result = self._hub.read_input_registers( - self._slave, self._register, self._count - ) - else: - result = self._hub.read_holding_registers( - self._slave, self._register, self._count - ) - except ConnectionException: - self._available = False - return - - if isinstance(result, (ModbusException, ExceptionResponse)): + if self._register_type == CALL_TYPE_REGISTER_INPUT: + result = self._hub.read_input_registers( + self._slave, self._register, self._count + ) + else: + result = self._hub.read_holding_registers( + self._slave, self._register, self._count + ) + if result is None: self._available = False return diff --git a/homeassistant/components/modbus/switch.py b/homeassistant/components/modbus/switch.py index b5aef6d42c069..1c0b64462cb6e 100644 --- a/homeassistant/components/modbus/switch.py +++ b/homeassistant/components/modbus/switch.py @@ -6,8 +6,6 @@ import logging from typing import Any -from pymodbus.exceptions import ConnectionException, ModbusException -from pymodbus.pdu import ExceptionResponse import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity @@ -213,13 +211,8 @@ def _update(self): def _read_coil(self, coil) -> bool: """Read coil using the Modbus hub slave.""" - try: - result = self._hub.read_coils(self._slave, coil, 1) - except ConnectionException: - self._available = False - return False - - if isinstance(result, (ModbusException, ExceptionResponse)): + result = self._hub.read_coils(self._slave, coil, 1) + if result is None: self._available = False return False @@ -231,13 +224,7 @@ def _read_coil(self, coil) -> bool: def _write_coil(self, coil, value): """Write coil using the Modbus hub slave.""" - try: - self._hub.write_coil(self._slave, coil, value) - except ConnectionException: - self._available = False - return - - self._available = True + self._available = self._hub.write_coil(self._slave, coil, value) class ModbusRegisterSwitch(ModbusBaseSwitch, SwitchEntity): @@ -301,33 +288,21 @@ def _update(self): self.schedule_update_ha_state() def _read_register(self) -> int | None: - try: - if self._register_type == CALL_TYPE_REGISTER_INPUT: - result = self._hub.read_input_registers( - self._slave, self._verify_register, 1 - ) - else: - result = self._hub.read_holding_registers( - self._slave, self._verify_register, 1 - ) - except ConnectionException: - self._available = False - return - - if isinstance(result, (ModbusException, ExceptionResponse)): + if self._register_type == CALL_TYPE_REGISTER_INPUT: + result = self._hub.read_input_registers( + self._slave, self._verify_register, 1 + ) + else: + result = self._hub.read_holding_registers( + self._slave, self._verify_register, 1 + ) + if result is None: self._available = False return - self._available = True return int(result.registers[0]) def _write_register(self, value): """Write holding register using the Modbus hub slave.""" - try: - self._hub.write_register(self._slave, self._register, value) - except ConnectionException: - self._available = False - return - - self._available = True + self._available = self._hub.write_register(self._slave, self._register, value) From b69b55987d07f599cb16a9ba154464f1312d196a Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 19 Apr 2021 17:20:00 +0200 Subject: [PATCH 0366/1317] Google report state: thermostatMode should be a string, not null (#49342) --- homeassistant/components/google_assistant/trait.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 7a1e1f9d94143..64f803dab25b2 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -819,7 +819,7 @@ def query_attributes(self): if preset in self.preset_to_google: response["thermostatMode"] = self.preset_to_google[preset] else: - response["thermostatMode"] = self.hvac_to_google.get(operation) + response["thermostatMode"] = self.hvac_to_google.get(operation, "none") current_temp = attrs.get(climate.ATTR_CURRENT_TEMPERATURE) if current_temp is not None: From 6d137d23160f6e30ca3dcd63905637be61578ead Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 19 Apr 2021 05:22:38 -1000 Subject: [PATCH 0367/1317] Increase recorder test coverage (#49362) Co-authored-by: Martin Hjelmare --- homeassistant/components/recorder/__init__.py | 47 ++++--------- homeassistant/components/recorder/purge.py | 17 ++--- homeassistant/components/recorder/util.py | 40 +++++++++++ tests/components/recorder/test_purge.py | 65 ++++++++++++++++- tests/components/recorder/test_util.py | 69 ++++++++++++++++++- 5 files changed, 192 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 733d8f248a896..db20c72c81edc 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -45,8 +45,10 @@ from .models import Base, Events, RecorderRuns, States from .util import ( dburl_to_path, + end_incomplete_runs, move_away_broken_database, session_scope, + setup_connection_for_dialect, validate_or_move_away_sqlite_database, ) @@ -93,6 +95,9 @@ CONF_EVENT_TYPES = "event_types" CONF_COMMIT_INTERVAL = "commit_interval" +INVALIDATED_ERR = "Database connection invalidated" +CONNECTIVITY_ERR = "Error in database connectivity during commit" + EXCLUDE_SCHEMA = INCLUDE_EXCLUDE_FILTER_SCHEMA_INNER.extend( {vol.Optional(CONF_EVENT_TYPES): vol.All(cv.ensure_list, [cv.string])} ) @@ -667,13 +672,9 @@ def _commit_event_session_or_retry(self): self._commit_event_session() return except (exc.InternalError, exc.OperationalError) as err: - if err.connection_invalidated: - message = "Database connection invalidated" - else: - message = "Error in database connectivity during commit" _LOGGER.error( "%s: Error executing query: %s. (retrying in %s seconds)", - message, + INVALIDATED_ERR if err.connection_invalidated else CONNECTIVITY_ERR, err, self.db_retry_wait, ) @@ -771,25 +772,9 @@ def setup_recorder_connection(dbapi_connection, connection_record): """Dbapi specific connection settings.""" if self._completed_database_setup: return - - # We do not import sqlite3 here so mysql/other - # users do not have to pay for it to be loaded in - # memory - if self.db_url.startswith(SQLITE_URL_PREFIX): - old_isolation = dbapi_connection.isolation_level - dbapi_connection.isolation_level = None - cursor = dbapi_connection.cursor() - cursor.execute("PRAGMA journal_mode=WAL") - cursor.close() - dbapi_connection.isolation_level = old_isolation - # WAL mode only needs to be setup once - # instead of every time we open the sqlite connection - # as its persistent and isn't free to call every time. - self._completed_database_setup = True - elif self.db_url.startswith("mysql"): - cursor = dbapi_connection.cursor() - cursor.execute("SET session wait_timeout=28800") - cursor.close() + self._completed_database_setup = setup_connection_for_dialect( + self.engine.dialect.name, dbapi_connection + ) if self.db_url == SQLITE_URL_PREFIX or ":memory:" in self.db_url: kwargs["connect_args"] = {"check_same_thread": False} @@ -825,17 +810,9 @@ def _close_connection(self): def _setup_run(self): """Log the start of the current run.""" with session_scope(session=self.get_session()) as session: - for run in session.query(RecorderRuns).filter_by(end=None): - run.closed_incorrect = True - run.end = self.recording_start - _LOGGER.warning( - "Ended unfinished session (id=%s from %s)", run.run_id, run.start - ) - session.add(run) - - self.run_info = RecorderRuns( - start=self.recording_start, created=dt_util.utcnow() - ) + start = self.recording_start + end_incomplete_runs(session, start) + self.run_info = RecorderRuns(start=start, created=dt_util.utcnow()) session.add(self.run_info) session.flush() session.expunge(self.run_info) diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index 424070156b05c..22202ad1bbfff 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -22,6 +22,12 @@ _LOGGER = logging.getLogger(__name__) +# Retry when one of the following MySQL errors occurred: +RETRYABLE_MYSQL_ERRORS = (1205, 1206, 1213) +# 1205: Lock wait timeout exceeded; try restarting transaction +# 1206: The total number of locks exceeds the lock table size +# 1213: Deadlock found when trying to get lock; try restarting transaction + def purge_old_data( instance: Recorder, purge_days: int, repack: bool, apply_filter: bool = False @@ -55,14 +61,9 @@ def purge_old_data( if repack: repack_database(instance) except OperationalError as err: - # Retry when one of the following MySQL errors occurred: - # 1205: Lock wait timeout exceeded; try restarting transaction - # 1206: The total number of locks exceeds the lock table size - # 1213: Deadlock found when trying to get lock; try restarting transaction - if instance.engine.driver in ("mysqldb", "pymysql") and err.orig.args[0] in ( - 1205, - 1206, - 1213, + if ( + instance.engine.dialect.name == "mysql" + and err.orig.args[0] in RETRYABLE_MYSQL_ERRORS ): _LOGGER.info("%s; purge not completed, retrying", err.orig.args[1]) time.sleep(instance.db_retry_wait) diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index 89f74c44f4e67..c18ff0a9830aa 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -19,6 +19,7 @@ ALL_TABLES, TABLE_RECORDER_RUNS, TABLE_SCHEMA_CHANGES, + RecorderRuns, process_timestamp, ) @@ -230,3 +231,42 @@ def move_away_broken_database(dbfile: str) -> None: if not os.path.exists(path): continue os.rename(path, f"{path}{corrupt_postfix}") + + +def execute_on_connection(dbapi_connection, statement): + """Execute a single statement with a dbapi connection.""" + cursor = dbapi_connection.cursor() + cursor.execute(statement) + cursor.close() + + +def setup_connection_for_dialect(dialect_name, dbapi_connection): + """Execute statements needed for dialect connection.""" + # Returns False if the the connection needs to be setup + # on the next connection, returns True if the connection + # never needs to be setup again. + if dialect_name == "sqlite": + old_isolation = dbapi_connection.isolation_level + dbapi_connection.isolation_level = None + execute_on_connection(dbapi_connection, "PRAGMA journal_mode=WAL") + dbapi_connection.isolation_level = old_isolation + # WAL mode only needs to be setup once + # instead of every time we open the sqlite connection + # as its persistent and isn't free to call every time. + return True + + if dialect_name == "mysql": + execute_on_connection(dbapi_connection, "SET session wait_timeout=28800") + + return False + + +def end_incomplete_runs(session, start_time): + """End any incomplete recorder runs.""" + for run in session.query(RecorderRuns).filter_by(end=None): + run.closed_incorrect = True + run.end = start_time + _LOGGER.warning( + "Ended unfinished session (id=%s from %s)", run.run_id, run.start + ) + session.add(run) diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index b97873df62e2a..d1825663ccc5d 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -2,9 +2,9 @@ from datetime import datetime, timedelta import json import sqlite3 -from unittest.mock import patch +from unittest.mock import MagicMock, patch -from sqlalchemy.exc import DatabaseError +from sqlalchemy.exc import DatabaseError, OperationalError from sqlalchemy.orm.session import Session from homeassistant.components import recorder @@ -88,6 +88,67 @@ async def test_purge_old_states_encouters_database_corruption( assert states_after_purge.count() == 0 +async def test_purge_old_states_encounters_temporary_mysql_error( + hass: HomeAssistantType, + async_setup_recorder_instance: SetupRecorderInstanceT, + caplog, +): + """Test retry on specific mysql operational errors.""" + instance = await async_setup_recorder_instance(hass) + + await _add_test_states(hass, instance) + await async_wait_recording_done_without_instance(hass) + + mysql_exception = OperationalError("statement", {}, []) + mysql_exception.orig = MagicMock(args=(1205, "retryable")) + + with patch( + "homeassistant.components.recorder.purge.time.sleep" + ) as sleep_mock, patch( + "homeassistant.components.recorder.purge._purge_old_recorder_runs", + side_effect=[mysql_exception, None], + ), patch.object( + instance.engine.dialect, "name", "mysql" + ): + await hass.services.async_call( + recorder.DOMAIN, recorder.SERVICE_PURGE, {"keep_days": 0} + ) + await hass.async_block_till_done() + await async_wait_recording_done_without_instance(hass) + await async_wait_recording_done_without_instance(hass) + + assert "retrying" in caplog.text + assert sleep_mock.called + + +async def test_purge_old_states_encounters_operational_error( + hass: HomeAssistantType, + async_setup_recorder_instance: SetupRecorderInstanceT, + caplog, +): + """Test error on operational errors that are not mysql does not retry.""" + instance = await async_setup_recorder_instance(hass) + + await _add_test_states(hass, instance) + await async_wait_recording_done_without_instance(hass) + + exception = OperationalError("statement", {}, []) + + with patch( + "homeassistant.components.recorder.purge._purge_old_recorder_runs", + side_effect=exception, + ): + await hass.services.async_call( + recorder.DOMAIN, recorder.SERVICE_PURGE, {"keep_days": 0} + ) + await hass.async_block_till_done() + await async_wait_recording_done_without_instance(hass) + await async_wait_recording_done_without_instance(hass) + + assert "retrying" not in caplog.text + assert "Error purging history" in caplog.text + + async def test_purge_old_events( hass: HomeAssistantType, async_setup_recorder_instance: SetupRecorderInstanceT ): diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index 4da635209b334..e4d942246c5d0 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -6,8 +6,10 @@ import pytest -from homeassistant.components.recorder import util +from homeassistant.components.recorder import run_information_with_session, util from homeassistant.components.recorder.const import DATA_INSTANCE, SQLITE_URL_PREFIX +from homeassistant.components.recorder.models import RecorderRuns +from homeassistant.components.recorder.util import end_incomplete_runs, session_scope from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.util import dt as dt_util @@ -37,6 +39,16 @@ def setup_recorder(config=None): hass.stop() +def test_session_scope_not_setup(hass_recorder): + """Try to create a session scope when not setup.""" + hass = hass_recorder() + with patch.object( + hass.data[DATA_INSTANCE], "get_session", return_value=None + ), pytest.raises(RuntimeError): + with util.session_scope(hass=hass): + pass + + def test_recorder_bad_commit(hass_recorder): """Bad _commit should retry 3 times.""" hass = hass_recorder() @@ -130,6 +142,36 @@ async def test_last_run_was_recently_clean(hass): ) +def test_setup_connection_for_dialect_mysql(): + """Test setting up the connection for a mysql dialect.""" + execute_mock = MagicMock() + close_mock = MagicMock() + + def _make_cursor_mock(*_): + return MagicMock(execute=execute_mock, close=close_mock) + + dbapi_connection = MagicMock(cursor=_make_cursor_mock) + + assert util.setup_connection_for_dialect("mysql", dbapi_connection) is False + + assert execute_mock.call_args[0][0] == "SET session wait_timeout=28800" + + +def test_setup_connection_for_dialect_sqlite(): + """Test setting up the connection for a sqlite dialect.""" + execute_mock = MagicMock() + close_mock = MagicMock() + + def _make_cursor_mock(*_): + return MagicMock(execute=execute_mock, close=close_mock) + + dbapi_connection = MagicMock(cursor=_make_cursor_mock) + + assert util.setup_connection_for_dialect("sqlite", dbapi_connection) is True + + assert execute_mock.call_args[0][0] == "PRAGMA journal_mode=WAL" + + def test_basic_sanity_check(hass_recorder): """Test the basic sanity checks with a missing table.""" hass = hass_recorder() @@ -194,3 +236,28 @@ def test_combined_checks(hass_recorder, caplog): caplog.clear() with pytest.raises(sqlite3.DatabaseError): util.run_checks_on_open_db("fake_db_path", cursor) + + +def test_end_incomplete_runs(hass_recorder, caplog): + """Ensure we can end incomplete runs.""" + hass = hass_recorder() + + with session_scope(hass=hass) as session: + run_info = run_information_with_session(session) + assert isinstance(run_info, RecorderRuns) + assert run_info.closed_incorrect is False + + now = dt_util.utcnow() + now_without_tz = now.replace(tzinfo=None) + end_incomplete_runs(session, now) + run_info = run_information_with_session(session) + assert run_info.closed_incorrect is True + assert run_info.end == now_without_tz + session.flush() + + later = dt_util.utcnow() + end_incomplete_runs(session, later) + run_info = run_information_with_session(session) + assert run_info.end == now_without_tz + + assert "Ended unfinished session" in caplog.text From b8001b951b6c1a816e3887fda5d4941a0510e669 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 19 Apr 2021 05:23:10 -1000 Subject: [PATCH 0368/1317] Avoid executor jumps in history stats when no update is needed (#49407) --- .../components/history_stats/sensor.py | 20 +- tests/components/history_stats/test_sensor.py | 278 +++++++++--------- 2 files changed, 153 insertions(+), 145 deletions(-) diff --git a/homeassistant/components/history_stats/sensor.py b/homeassistant/components/history_stats/sensor.py index b8d3dc39187ab..d6587f435d745 100644 --- a/homeassistant/components/history_stats/sensor.py +++ b/homeassistant/components/history_stats/sensor.py @@ -20,7 +20,7 @@ from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_state_change_event -from homeassistant.helpers.reload import setup_reload_service +from homeassistant.helpers.reload import async_setup_reload_service import homeassistant.util.dt as dt_util from . import DOMAIN, PLATFORMS @@ -74,9 +74,9 @@ def exactly_two_period_keys(conf): # noinspection PyUnusedLocal -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the History Stats sensor.""" - setup_reload_service(hass, DOMAIN, PLATFORMS) + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) entity_id = config.get(CONF_ENTITY_ID) entity_states = config.get(CONF_STATE) @@ -90,7 +90,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if template is not None: template.hass = hass - add_entities( + async_add_entities( [ HistoryStatsSensor( hass, entity_id, entity_states, start, end, duration, sensor_type, name @@ -108,6 +108,7 @@ def __init__( self, hass, entity_id, entity_states, start, end, duration, sensor_type, name ): """Initialize the HistoryStats sensor.""" + self.hass = hass self._entity_id = entity_id self._entity_states = entity_states self._duration = duration @@ -186,7 +187,7 @@ def icon(self): """Return the icon to use in the frontend, if any.""" return ICON - def update(self): + async def async_update(self): """Get the latest data and updates the states.""" # Get previous values of start and end p_start, p_end = self._period @@ -218,6 +219,11 @@ def update(self): # Don't compute anything as the value cannot have changed return + await self.hass.async_add_executor_job( + self._update, start, end, now_timestamp, start_timestamp, end_timestamp + ) + + def _update(self, start, end, now_timestamp, start_timestamp, end_timestamp): # Get history between start and end history_list = history.state_changes_during_period( self.hass, start, end, str(self._entity_id) @@ -265,7 +271,7 @@ def update_period(self): # Parse start if self._start is not None: try: - start_rendered = self._start.render() + start_rendered = self._start.async_render() except (TemplateError, TypeError) as ex: HistoryStatsHelper.handle_template_exception(ex, "start") return @@ -285,7 +291,7 @@ def update_period(self): # Parse end if self._end is not None: try: - end_rendered = self._end.render() + end_rendered = self._end.async_render() except (TemplateError, TypeError) as ex: HistoryStatsHelper.handle_template_exception(ex, "end") return diff --git a/tests/components/history_stats/test_sensor.py b/tests/components/history_stats/test_sensor.py index f074003ab86df..37dc27e9e91b8 100644 --- a/tests/components/history_stats/test_sensor.py +++ b/tests/components/history_stats/test_sensor.py @@ -118,144 +118,6 @@ def test_period_parsing(self, mock): assert sensor2_end.minute == 0 assert sensor2_end.second == 0 - def test_measure(self): - """Test the history statistics sensor measure.""" - t0 = dt_util.utcnow() - timedelta(minutes=40) - t1 = t0 + timedelta(minutes=20) - t2 = dt_util.utcnow() - timedelta(minutes=10) - - # Start t0 t1 t2 End - # |--20min--|--20min--|--10min--|--10min--| - # |---off---|---on----|---off---|---on----| - - fake_states = { - "binary_sensor.test_id": [ - ha.State("binary_sensor.test_id", "on", last_changed=t0), - ha.State("binary_sensor.test_id", "off", last_changed=t1), - ha.State("binary_sensor.test_id", "on", last_changed=t2), - ] - } - - start = Template("{{ as_timestamp(now()) - 3600 }}", self.hass) - end = Template("{{ now() }}", self.hass) - - sensor1 = HistoryStatsSensor( - self.hass, "binary_sensor.test_id", "on", start, end, None, "time", "Test" - ) - - sensor2 = HistoryStatsSensor( - self.hass, "unknown.id", "on", start, end, None, "time", "Test" - ) - - sensor3 = HistoryStatsSensor( - self.hass, "binary_sensor.test_id", "on", start, end, None, "count", "test" - ) - - sensor4 = HistoryStatsSensor( - self.hass, "binary_sensor.test_id", "on", start, end, None, "ratio", "test" - ) - - assert sensor1._type == "time" - assert sensor3._type == "count" - assert sensor4._type == "ratio" - - with patch( - "homeassistant.components.history.state_changes_during_period", - return_value=fake_states, - ), patch("homeassistant.components.history.get_state", return_value=None): - sensor1.update() - sensor2.update() - sensor3.update() - sensor4.update() - - assert sensor1.state == 0.5 - assert sensor2.state is None - assert sensor3.state == 2 - assert sensor4.state == 50 - - def test_measure_multiple(self): - """Test the history statistics sensor measure for multiple states.""" - t0 = dt_util.utcnow() - timedelta(minutes=40) - t1 = t0 + timedelta(minutes=20) - t2 = dt_util.utcnow() - timedelta(minutes=10) - - # Start t0 t1 t2 End - # |--20min--|--20min--|--10min--|--10min--| - # |---------|--orange-|-default-|---blue--| - - fake_states = { - "input_select.test_id": [ - ha.State("input_select.test_id", "orange", last_changed=t0), - ha.State("input_select.test_id", "default", last_changed=t1), - ha.State("input_select.test_id", "blue", last_changed=t2), - ] - } - - start = Template("{{ as_timestamp(now()) - 3600 }}", self.hass) - end = Template("{{ now() }}", self.hass) - - sensor1 = HistoryStatsSensor( - self.hass, - "input_select.test_id", - ["orange", "blue"], - start, - end, - None, - "time", - "Test", - ) - - sensor2 = HistoryStatsSensor( - self.hass, - "unknown.id", - ["orange", "blue"], - start, - end, - None, - "time", - "Test", - ) - - sensor3 = HistoryStatsSensor( - self.hass, - "input_select.test_id", - ["orange", "blue"], - start, - end, - None, - "count", - "test", - ) - - sensor4 = HistoryStatsSensor( - self.hass, - "input_select.test_id", - ["orange", "blue"], - start, - end, - None, - "ratio", - "test", - ) - - assert sensor1._type == "time" - assert sensor3._type == "count" - assert sensor4._type == "ratio" - - with patch( - "homeassistant.components.history.state_changes_during_period", - return_value=fake_states, - ), patch("homeassistant.components.history.get_state", return_value=None): - sensor1.update() - sensor2.update() - sensor3.update() - sensor4.update() - - assert sensor1.state == 0.5 - assert sensor2.state is None - assert sensor3.state == 2 - assert sensor4.state == 50 - def test_wrong_date(self): """Test when start or end value is not a timestamp or a date.""" good = Template("{{ now() }}", self.hass) @@ -415,5 +277,145 @@ async def test_reload(hass): assert hass.states.get("sensor.second_test") +async def test_measure_multiple(hass): + """Test the history statistics sensor measure for multiple states.""" + t0 = dt_util.utcnow() - timedelta(minutes=40) + t1 = t0 + timedelta(minutes=20) + t2 = dt_util.utcnow() - timedelta(minutes=10) + + # Start t0 t1 t2 End + # |--20min--|--20min--|--10min--|--10min--| + # |---------|--orange-|-default-|---blue--| + + fake_states = { + "input_select.test_id": [ + ha.State("input_select.test_id", "orange", last_changed=t0), + ha.State("input_select.test_id", "default", last_changed=t1), + ha.State("input_select.test_id", "blue", last_changed=t2), + ] + } + + start = Template("{{ as_timestamp(now()) - 3600 }}", hass) + end = Template("{{ now() }}", hass) + + sensor1 = HistoryStatsSensor( + hass, + "input_select.test_id", + ["orange", "blue"], + start, + end, + None, + "time", + "Test", + ) + + sensor2 = HistoryStatsSensor( + hass, + "unknown.id", + ["orange", "blue"], + start, + end, + None, + "time", + "Test", + ) + + sensor3 = HistoryStatsSensor( + hass, + "input_select.test_id", + ["orange", "blue"], + start, + end, + None, + "count", + "test", + ) + + sensor4 = HistoryStatsSensor( + hass, + "input_select.test_id", + ["orange", "blue"], + start, + end, + None, + "ratio", + "test", + ) + + assert sensor1._type == "time" + assert sensor3._type == "count" + assert sensor4._type == "ratio" + + with patch( + "homeassistant.components.history.state_changes_during_period", + return_value=fake_states, + ), patch("homeassistant.components.history.get_state", return_value=None): + await sensor1.async_update() + await sensor2.async_update() + await sensor3.async_update() + await sensor4.async_update() + + assert sensor1.state == 0.5 + assert sensor2.state is None + assert sensor3.state == 2 + assert sensor4.state == 50 + + +async def async_test_measure(hass): + """Test the history statistics sensor measure.""" + t0 = dt_util.utcnow() - timedelta(minutes=40) + t1 = t0 + timedelta(minutes=20) + t2 = dt_util.utcnow() - timedelta(minutes=10) + + # Start t0 t1 t2 End + # |--20min--|--20min--|--10min--|--10min--| + # |---off---|---on----|---off---|---on----| + + fake_states = { + "binary_sensor.test_id": [ + ha.State("binary_sensor.test_id", "on", last_changed=t0), + ha.State("binary_sensor.test_id", "off", last_changed=t1), + ha.State("binary_sensor.test_id", "on", last_changed=t2), + ] + } + + start = Template("{{ as_timestamp(now()) - 3600 }}", hass) + end = Template("{{ now() }}", hass) + + sensor1 = HistoryStatsSensor( + hass, "binary_sensor.test_id", "on", start, end, None, "time", "Test" + ) + + sensor2 = HistoryStatsSensor( + hass, "unknown.id", "on", start, end, None, "time", "Test" + ) + + sensor3 = HistoryStatsSensor( + hass, "binary_sensor.test_id", "on", start, end, None, "count", "test" + ) + + sensor4 = HistoryStatsSensor( + hass, "binary_sensor.test_id", "on", start, end, None, "ratio", "test" + ) + + assert sensor1._type == "time" + assert sensor3._type == "count" + assert sensor4._type == "ratio" + + with patch( + "homeassistant.components.history.state_changes_during_period", + return_value=fake_states, + ), patch("homeassistant.components.history.get_state", return_value=None): + await sensor1.async_update() + await sensor2.async_update() + await sensor3.async_update() + await sensor4.async_update() + + assert sensor1.state == 0.5 + assert sensor2.state is None + assert sensor3.state == 2 + assert sensor4.state == 50 + + def _get_fixtures_base_path(): return path.dirname(path.dirname(path.dirname(__file__))) From a5806b59f27d407990c0c0feb159e74c373e00bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 19 Apr 2021 17:23:43 +0200 Subject: [PATCH 0369/1317] Raise HassioAPIError when error is returned (#49418) Co-authored-by: Martin Hjelmare --- homeassistant/components/hassio/const.py | 1 + .../components/hassio/websocket_api.py | 5 +++- tests/components/hassio/test_websocket_api.py | 24 +++++++++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/hassio/const.py b/homeassistant/components/hassio/const.py index 417a62a1a8c08..435d42349fd04 100644 --- a/homeassistant/components/hassio/const.py +++ b/homeassistant/components/hassio/const.py @@ -21,6 +21,7 @@ ATTR_WS_EVENT = "event" ATTR_ENDPOINT = "endpoint" ATTR_METHOD = "method" +ATTR_RESULT = "result" ATTR_TIMEOUT = "timeout" diff --git a/homeassistant/components/hassio/websocket_api.py b/homeassistant/components/hassio/websocket_api.py index 387aa9264891a..dfc2b7dc01dc5 100644 --- a/homeassistant/components/hassio/websocket_api.py +++ b/homeassistant/components/hassio/websocket_api.py @@ -16,6 +16,7 @@ ATTR_DATA, ATTR_ENDPOINT, ATTR_METHOD, + ATTR_RESULT, ATTR_TIMEOUT, ATTR_WS_EVENT, DOMAIN, @@ -94,7 +95,6 @@ async def websocket_supervisor_api( ): """Websocket handler to call Supervisor API.""" supervisor: HassIO = hass.data[DOMAIN] - result = False try: result = await supervisor.send_command( msg[ATTR_ENDPOINT], @@ -102,6 +102,9 @@ async def websocket_supervisor_api( timeout=msg.get(ATTR_TIMEOUT, 10), payload=msg.get(ATTR_DATA, {}), ) + + if result.get(ATTR_RESULT) == "error": + raise hass.components.hassio.HassioAPIError(result.get("message")) except hass.components.hassio.HassioAPIError as err: _LOGGER.error("Failed to to call %s - %s", msg[ATTR_ENDPOINT], err) connection.send_error( diff --git a/tests/components/hassio/test_websocket_api.py b/tests/components/hassio/test_websocket_api.py index dcf6b64d9e271..5278d2cbb9170 100644 --- a/tests/components/hassio/test_websocket_api.py +++ b/tests/components/hassio/test_websocket_api.py @@ -88,3 +88,27 @@ async def test_websocket_supervisor_api( msg = await websocket_client.receive_json() assert msg["result"]["version_latest"] == "1.0.0" + + +async def test_websocket_supervisor_api_error( + hassio_env, hass: HomeAssistant, hass_ws_client, aioclient_mock +): + """Test Supervisor websocket api error.""" + assert await async_setup_component(hass, "hassio", {}) + websocket_client = await hass_ws_client(hass) + aioclient_mock.get( + "http://127.0.0.1/ping", + json={"result": "error", "message": "example error"}, + ) + + await websocket_client.send_json( + { + WS_ID: 1, + WS_TYPE: WS_TYPE_API, + ATTR_ENDPOINT: "/ping", + ATTR_METHOD: "get", + } + ) + + msg = await websocket_client.receive_json() + assert msg["error"]["message"] == "example error" From a5a62154d4f53f42e116cc9bc722302798b1b326 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 19 Apr 2021 14:45:01 +0200 Subject: [PATCH 0370/1317] Fix deadlock when restarting scripts (#49410) --- homeassistant/helpers/script.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 52be38666398b..f2afe15256917 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -1203,12 +1203,9 @@ async def async_run( self._changed() raise - async def _async_stop(self, update_state, spare=None): - aws = [ - asyncio.create_task(run.async_stop()) for run in self._runs if run != spare - ] - if not aws: - return + async def _async_stop( + self, aws: list[asyncio.Task], update_state: bool, spare: _ScriptRun | None + ) -> None: await asyncio.wait(aws) if update_state: self._changed() @@ -1217,7 +1214,15 @@ async def async_stop( self, update_state: bool = True, spare: _ScriptRun | None = None ) -> None: """Stop running script.""" - await asyncio.shield(self._async_stop(update_state, spare)) + # Collect a a list of script runs to stop. This must be done before calling + # asyncio.shield as asyncio.shield yields to the event loop, which would cause + # us to wait for script runs added after the call to async_stop. + aws = [ + asyncio.create_task(run.async_stop()) for run in self._runs if run != spare + ] + if not aws: + return + await asyncio.shield(self._async_stop(aws, update_state, spare)) async def _async_get_condition(self, config): if isinstance(config, template.Template): From d61281b6fb7d1c76aa0563d7987c189116f38f19 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 19 Apr 2021 17:20:00 +0200 Subject: [PATCH 0371/1317] Google report state: thermostatMode should be a string, not null (#49342) --- homeassistant/components/google_assistant/trait.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 384c5bfd0ae29..63f76e1d6ea3d 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -791,7 +791,7 @@ def query_attributes(self): if preset in self.preset_to_google: response["thermostatMode"] = self.preset_to_google[preset] else: - response["thermostatMode"] = self.hvac_to_google.get(operation) + response["thermostatMode"] = self.hvac_to_google.get(operation, "none") current_temp = attrs.get(climate.ATTR_CURRENT_TEMPERATURE) if current_temp is not None: From 44744dc0bc7940c70efbe99e07a5e06f4305efa4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 19 Apr 2021 17:32:43 +0200 Subject: [PATCH 0372/1317] Bumped version to 2021.4.6 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 9ebf7516628ad..472a5401c6ae3 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 2021 MINOR_VERSION = 4 -PATCH_VERSION = "5" +PATCH_VERSION = "6" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 8, 0) From 8acc3f0b03a260b7957ce058053a78963705115d Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 19 Apr 2021 19:35:32 +0200 Subject: [PATCH 0373/1317] Fix modbus switch "old style" config problem (#49352) Fix that using CONF_HUB in switch, changed the hub for all subsequent switches. --- homeassistant/components/modbus/binary_sensor.py | 5 +++-- homeassistant/components/modbus/sensor.py | 5 +++-- homeassistant/components/modbus/switch.py | 5 +++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py index 0a76baf1fda5c..c9f551e3d348a 100644 --- a/homeassistant/components/modbus/binary_sensor.py +++ b/homeassistant/components/modbus/binary_sensor.py @@ -89,10 +89,11 @@ async def async_setup_platform( for entry in discovery_info[CONF_BINARY_SENSORS]: if CONF_HUB in entry: # from old config! - discovery_info[CONF_NAME] = entry[CONF_HUB] + hub: ModbusHub = hass.data[MODBUS_DOMAIN][entry[CONF_HUB]] + else: + hub: ModbusHub = hass.data[MODBUS_DOMAIN][discovery_info[CONF_NAME]] if CONF_SCAN_INTERVAL not in entry: entry[CONF_SCAN_INTERVAL] = DEFAULT_SCAN_INTERVAL - hub: ModbusHub = hass.data[MODBUS_DOMAIN][discovery_info[CONF_NAME]] sensors.append( ModbusBinarySensor( hub, diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index b8cca30be6000..cb76bedd18ff1 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -167,10 +167,11 @@ async def async_setup_platform( if CONF_HUB in entry: # from old config! - discovery_info[CONF_NAME] = entry[CONF_HUB] + hub: ModbusHub = hass.data[MODBUS_DOMAIN][entry[CONF_HUB]] + else: + hub: ModbusHub = hass.data[MODBUS_DOMAIN][discovery_info[CONF_NAME]] if CONF_SCAN_INTERVAL not in entry: entry[CONF_SCAN_INTERVAL] = DEFAULT_SCAN_INTERVAL - hub: ModbusHub = hass.data[MODBUS_DOMAIN][discovery_info[CONF_NAME]] sensors.append( ModbusRegisterSensor( hub, diff --git a/homeassistant/components/modbus/switch.py b/homeassistant/components/modbus/switch.py index 1c0b64462cb6e..c3fe567d9b5eb 100644 --- a/homeassistant/components/modbus/switch.py +++ b/homeassistant/components/modbus/switch.py @@ -123,8 +123,9 @@ async def async_setup_platform( for entry in discovery_info[CONF_SWITCHES]: if CONF_HUB in entry: # from old config! - discovery_info[CONF_NAME] = entry[CONF_HUB] - hub: ModbusHub = hass.data[MODBUS_DOMAIN][discovery_info[CONF_NAME]] + hub: ModbusHub = hass.data[MODBUS_DOMAIN][entry[CONF_HUB]] + else: + hub: ModbusHub = hass.data[MODBUS_DOMAIN][discovery_info[CONF_NAME]] if entry[CONF_INPUT_TYPE] == CALL_TYPE_COIL: switches.append(ModbusCoilSwitch(hub, entry)) else: From 1560c00db1c748ba4a07cb2a13008b19b35ccb77 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Mon, 19 Apr 2021 14:46:18 -0700 Subject: [PATCH 0374/1317] Use Hyperion human-readable effect names instead of API identifiers (#45763) --- homeassistant/components/hyperion/const.py | 24 -- homeassistant/components/hyperion/light.py | 41 +++- .../components/hyperion/manifest.json | 2 +- homeassistant/components/hyperion/switch.py | 6 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/hyperion/test_light.py | 218 +++++++++++++++--- tests/components/hyperion/test_switch.py | 13 +- 8 files changed, 236 insertions(+), 72 deletions(-) diff --git a/homeassistant/components/hyperion/const.py b/homeassistant/components/hyperion/const.py index 87600f7c27bcf..9deeba9d019de 100644 --- a/homeassistant/components/hyperion/const.py +++ b/homeassistant/components/hyperion/const.py @@ -1,29 +1,5 @@ """Constants for Hyperion integration.""" -from hyperion.const import ( - KEY_COMPONENTID_ALL, - KEY_COMPONENTID_BLACKBORDER, - KEY_COMPONENTID_BOBLIGHTSERVER, - KEY_COMPONENTID_FORWARDER, - KEY_COMPONENTID_GRABBER, - KEY_COMPONENTID_LEDDEVICE, - KEY_COMPONENTID_SMOOTHING, - KEY_COMPONENTID_V4L, -) - -# Maps between Hyperion API component names to Hyperion UI names. This allows Home -# Assistant to use names that match what Hyperion users may expect from the Hyperion UI. -COMPONENT_TO_NAME = { - KEY_COMPONENTID_ALL: "All", - KEY_COMPONENTID_SMOOTHING: "Smoothing", - KEY_COMPONENTID_BLACKBORDER: "Blackbar Detection", - KEY_COMPONENTID_FORWARDER: "Forwarder", - KEY_COMPONENTID_BOBLIGHTSERVER: "Boblight Server", - KEY_COMPONENTID_GRABBER: "Platform Capture", - KEY_COMPONENTID_LEDDEVICE: "LED Device", - KEY_COMPONENTID_V4L: "USB Capture", -} - CONF_AUTH_ID = "auth_id" CONF_CREATE_TOKEN = "create_token" CONF_INSTANCE = "instance" diff --git a/homeassistant/components/hyperion/light.py b/homeassistant/components/hyperion/light.py index 5ab74f1141be2..ac2160120ccf7 100644 --- a/homeassistant/components/hyperion/light.py +++ b/homeassistant/components/hyperion/light.py @@ -147,7 +147,10 @@ def __init__( self._static_effect_list: list[str] = [KEY_EFFECT_SOLID] if self._support_external_effects: - self._static_effect_list += list(const.KEY_COMPONENTID_EXTERNAL_SOURCES) + self._static_effect_list += [ + const.KEY_COMPONENTID_TO_NAME[component] + for component in const.KEY_COMPONENTID_EXTERNAL_SOURCES + ] self._effect_list: list[str] = self._static_effect_list[:] self._client_callbacks: Mapping[str, Callable[[dict[str, Any]], None]] = { @@ -195,7 +198,11 @@ def hs_color(self) -> tuple[float, float]: def icon(self) -> str: """Return state specific icon.""" if self.is_on: - if self.effect in const.KEY_COMPONENTID_EXTERNAL_SOURCES: + if ( + self.effect in const.KEY_COMPONENTID_FROM_NAME + and const.KEY_COMPONENTID_FROM_NAME[self.effect] + in const.KEY_COMPONENTID_EXTERNAL_SOURCES + ): return ICON_EXTERNAL_SOURCE if self.effect != KEY_EFFECT_SOLID: return ICON_EFFECT @@ -280,8 +287,21 @@ async def async_turn_on(self, **kwargs: Any) -> None: if ( effect and self._support_external_effects - and effect in const.KEY_COMPONENTID_EXTERNAL_SOURCES + and ( + effect in const.KEY_COMPONENTID_EXTERNAL_SOURCES + or effect in const.KEY_COMPONENTID_FROM_NAME + ) ): + if effect in const.KEY_COMPONENTID_FROM_NAME: + component = const.KEY_COMPONENTID_FROM_NAME[effect] + else: + _LOGGER.warning( + "Use of Hyperion effect '%s' is deprecated and will be removed " + "in a future release. Please use '%s' instead", + effect, + const.KEY_COMPONENTID_TO_NAME[effect], + ) + component = effect # Clear any color/effect. if not await self._client.async_send_clear( @@ -295,7 +315,7 @@ async def async_turn_on(self, **kwargs: Any) -> None: **{ const.KEY_COMPONENTSTATE: { const.KEY_COMPONENT: key, - const.KEY_STATE: effect == key, + const.KEY_STATE: component == key, } } ): @@ -371,8 +391,12 @@ def _update_priorities(self, _: dict[str, Any] | None = None) -> None: if ( self._support_external_effects and componentid in const.KEY_COMPONENTID_EXTERNAL_SOURCES + and componentid in const.KEY_COMPONENTID_TO_NAME ): - self._set_internal_state(rgb_color=DEFAULT_COLOR, effect=componentid) + self._set_internal_state( + rgb_color=DEFAULT_COLOR, + effect=const.KEY_COMPONENTID_TO_NAME[componentid], + ) elif componentid == const.KEY_COMPONENTID_EFFECT: # Owner is the effect name. # See: https://docs.hyperion-project.org/en/json/ServerInfo.html#priorities @@ -594,9 +618,10 @@ def _get_priority_entry_that_dictates_state(self) -> dict[str, Any] | None: @classmethod def _is_priority_entry_black(cls, priority: dict[str, Any] | None) -> bool: """Determine if a given priority entry is the color black.""" - if not priority: - return False - if priority.get(const.KEY_COMPONENTID) == const.KEY_COMPONENTID_COLOR: + if ( + priority + and priority.get(const.KEY_COMPONENTID) == const.KEY_COMPONENTID_COLOR + ): rgb_color = priority.get(const.KEY_VALUE, {}).get(const.KEY_RGB) if rgb_color is not None and tuple(rgb_color) == COLOR_BLACK: return True diff --git a/homeassistant/components/hyperion/manifest.json b/homeassistant/components/hyperion/manifest.json index 0c5e46b83e25e..08b852f5302eb 100644 --- a/homeassistant/components/hyperion/manifest.json +++ b/homeassistant/components/hyperion/manifest.json @@ -5,7 +5,7 @@ "domain": "hyperion", "name": "Hyperion", "quality_scale": "platinum", - "requirements": ["hyperion-py==0.7.0"], + "requirements": ["hyperion-py==0.7.2"], "ssdp": [ { "manufacturer": "Hyperion Open Source Ambient Lighting", diff --git a/homeassistant/components/hyperion/switch.py b/homeassistant/components/hyperion/switch.py index dce92df6f3573..5a7dd0c2cf552 100644 --- a/homeassistant/components/hyperion/switch.py +++ b/homeassistant/components/hyperion/switch.py @@ -14,6 +14,7 @@ KEY_COMPONENTID_GRABBER, KEY_COMPONENTID_LEDDEVICE, KEY_COMPONENTID_SMOOTHING, + KEY_COMPONENTID_TO_NAME, KEY_COMPONENTID_V4L, KEY_COMPONENTS, KEY_COMPONENTSTATE, @@ -39,7 +40,6 @@ listen_for_instance_updates, ) from .const import ( - COMPONENT_TO_NAME, CONF_INSTANCE_CLIENTS, DOMAIN, HYPERION_MANUFACTURER_NAME, @@ -67,7 +67,7 @@ def _component_to_unique_id(server_id: str, component: str, instance_num: int) - server_id, instance_num, slugify( - f"{TYPE_HYPERION_COMPONENT_SWITCH_BASE} {COMPONENT_TO_NAME[component]}" + f"{TYPE_HYPERION_COMPONENT_SWITCH_BASE} {KEY_COMPONENTID_TO_NAME[component]}" ), ) @@ -77,7 +77,7 @@ def _component_to_switch_name(component: str, instance_name: str) -> str: return ( f"{instance_name} " f"{NAME_SUFFIX_HYPERION_COMPONENT_SWITCH} " - f"{COMPONENT_TO_NAME.get(component, component.capitalize())}" + f"{KEY_COMPONENTID_TO_NAME.get(component, component.capitalize())}" ) diff --git a/requirements_all.txt b/requirements_all.txt index 7f298e428e1bc..301e2665fa744 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -793,7 +793,7 @@ huisbaasje-client==0.1.0 hydrawiser==0.2 # homeassistant.components.hyperion -hyperion-py==0.7.0 +hyperion-py==0.7.2 # homeassistant.components.bh1750 # homeassistant.components.bme280 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2cab2ae8964b2..e5f0f72081ff3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -448,7 +448,7 @@ huawei-lte-api==1.4.17 huisbaasje-client==0.1.0 # homeassistant.components.hyperion -hyperion-py==0.7.0 +hyperion-py==0.7.2 # homeassistant.components.iaqualink iaqualink==0.3.4 diff --git a/tests/components/hyperion/test_light.py b/tests/components/hyperion/test_light.py index a774a5ba86812..bb20e6445650e 100644 --- a/tests/components/hyperion/test_light.py +++ b/tests/components/hyperion/test_light.py @@ -382,8 +382,9 @@ async def test_light_async_turn_on(hass: HomeAssistantType) -> None: assert entity_state assert entity_state.attributes["brightness"] == brightness - # On (=), 100% (=), V4L (!), [0,255,255] (=) - effect = const.KEY_COMPONENTID_EXTERNAL_SOURCES[2] # V4L + # On (=), 100% (=), "USB Capture (!), [0,255,255] (=) + component = "V4L" + effect = const.KEY_COMPONENTID_TO_NAME[component] client.async_send_clear = AsyncMock(return_value=True) client.async_send_set_component = AsyncMock(return_value=True) await hass.services.async_call( @@ -422,7 +423,7 @@ async def test_light_async_turn_on(hass: HomeAssistantType) -> None: } ), ] - client.visible_priority = {const.KEY_COMPONENTID: effect} + client.visible_priority = {const.KEY_COMPONENTID: component} call_registered_callback(client, "priorities-update") entity_state = hass.states.get(TEST_ENTITY_ID_1) assert entity_state @@ -505,30 +506,126 @@ async def test_light_async_turn_on(hass: HomeAssistantType) -> None: assert not client.async_send_set_effect.called -async def test_light_async_turn_on_error_conditions(hass: HomeAssistantType) -> None: - """Test error conditions when turning the light on.""" +async def test_light_async_turn_on_fail_async_send_set_component( + hass: HomeAssistantType, +) -> None: + """Test set_component failure when turning the light on.""" client = create_mock_client() client.async_send_set_component = AsyncMock(return_value=False) client.is_on = Mock(return_value=False) await setup_test_config_entry(hass, hyperion_client=client) - - # On (=), 100% (=), solid (=), [255,255,255] (=) await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: TEST_ENTITY_ID_1}, blocking=True ) + assert client.method_calls[-1] == call.async_send_set_component( + componentstate={"component": "ALL", "state": True} + ) - assert client.async_send_set_component.call_args == call( - **{ - const.KEY_COMPONENTSTATE: { - const.KEY_COMPONENT: const.KEY_COMPONENTID_ALL, - const.KEY_STATE: True, - } - } + +async def test_light_async_turn_on_fail_async_send_set_component_source( + hass: HomeAssistantType, +) -> None: + """Test async_send_set_component failure when selecting the source.""" + client = create_mock_client() + client.async_send_clear = AsyncMock(return_value=True) + client.async_send_set_component = AsyncMock(return_value=False) + client.is_on = Mock(return_value=True) + await setup_test_config_entry(hass, hyperion_client=client) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: TEST_ENTITY_ID_1, + ATTR_EFFECT: const.KEY_COMPONENTID_TO_NAME["V4L"], + }, + blocking=True, + ) + assert client.method_calls[-1] == call.async_send_set_component( + componentstate={"component": "BOBLIGHTSERVER", "state": False} ) -async def test_light_async_turn_off_error_conditions(hass: HomeAssistantType) -> None: - """Test error conditions when turning the light off.""" +async def test_light_async_turn_on_fail_async_send_clear_source( + hass: HomeAssistantType, +) -> None: + """Test async_send_clear failure when turning the light on.""" + client = create_mock_client() + client.is_on = Mock(return_value=True) + client.async_send_clear = AsyncMock(return_value=False) + await setup_test_config_entry(hass, hyperion_client=client) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: TEST_ENTITY_ID_1, + ATTR_EFFECT: const.KEY_COMPONENTID_TO_NAME["V4L"], + }, + blocking=True, + ) + assert client.method_calls[-1] == call.async_send_clear(priority=180) + + +async def test_light_async_turn_on_fail_async_send_clear_effect( + hass: HomeAssistantType, +) -> None: + """Test async_send_clear failure when turning on an effect.""" + client = create_mock_client() + client.is_on = Mock(return_value=True) + client.async_send_clear = AsyncMock(return_value=False) + await setup_test_config_entry(hass, hyperion_client=client) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: TEST_ENTITY_ID_1, ATTR_EFFECT: "Warm Mood Blobs"}, + blocking=True, + ) + assert client.method_calls[-1] == call.async_send_clear(priority=180) + + +async def test_light_async_turn_on_fail_async_send_set_effect( + hass: HomeAssistantType, +) -> None: + """Test async_send_set_effect failure when turning on the light.""" + client = create_mock_client() + client.is_on = Mock(return_value=True) + client.async_send_clear = AsyncMock(return_value=True) + client.async_send_set_effect = AsyncMock(return_value=False) + await setup_test_config_entry(hass, hyperion_client=client) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: TEST_ENTITY_ID_1, ATTR_EFFECT: "Warm Mood Blobs"}, + blocking=True, + ) + assert client.method_calls[-1] == call.async_send_set_effect( + priority=180, effect={"name": "Warm Mood Blobs"}, origin="Home Assistant" + ) + + +async def test_light_async_turn_on_fail_async_send_set_color( + hass: HomeAssistantType, +) -> None: + """Test async_send_set_color failure when turning on the light.""" + client = create_mock_client() + client.is_on = Mock(return_value=True) + client.async_send_clear = AsyncMock(return_value=True) + client.async_send_set_color = AsyncMock(return_value=False) + await setup_test_config_entry(hass, hyperion_client=client) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: TEST_ENTITY_ID_1, ATTR_HS_COLOR: (240.0, 100.0)}, + blocking=True, + ) + assert client.method_calls[-1] == call.async_send_set_color( + priority=180, color=(0, 0, 255), origin="Home Assistant" + ) + + +async def test_light_async_turn_off_fail_async_send_set_component( + hass: HomeAssistantType, +) -> None: + """Test async_send_set_component failure when turning off the light.""" client = create_mock_client() client.async_send_set_component = AsyncMock(return_value=False) await setup_test_config_entry(hass, hyperion_client=client) @@ -539,15 +636,30 @@ async def test_light_async_turn_off_error_conditions(hass: HomeAssistantType) -> {ATTR_ENTITY_ID: TEST_ENTITY_ID_1}, blocking=True, ) + assert client.method_calls[-1] == call.async_send_set_component( + componentstate={"component": "LEDDEVICE", "state": False} + ) - assert client.async_send_set_component.call_args == call( - **{ - const.KEY_COMPONENTSTATE: { - const.KEY_COMPONENT: const.KEY_COMPONENTID_LEDDEVICE, - const.KEY_STATE: False, - } - } + +async def test_priority_light_async_turn_off_fail_async_send_clear( + hass: HomeAssistantType, +) -> None: + """Test async_send_clear failure when turning off a priority light.""" + client = create_mock_client() + client.async_send_clear = AsyncMock(return_value=False) + with patch( + "homeassistant.components.hyperion.light.HyperionPriorityLight.entity_registry_enabled_default" + ) as enabled_by_default_mock: + enabled_by_default_mock.return_value = True + await setup_test_config_entry(hass, hyperion_client=client) + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: TEST_PRIORITY_LIGHT_ENTITY_ID_1}, + blocking=True, ) + assert client.method_calls[-1] == call.async_send_clear(priority=180) async def test_light_async_turn_off(hass: HomeAssistantType) -> None: @@ -636,7 +748,10 @@ async def test_light_async_updates_from_hyperion_client( assert entity_state assert entity_state.attributes["icon"] == hyperion_light.ICON_EXTERNAL_SOURCE assert entity_state.attributes["hs_color"] == (0.0, 0.0) - assert entity_state.attributes["effect"] == const.KEY_COMPONENTID_V4L + assert ( + entity_state.attributes["effect"] + == const.KEY_COMPONENTID_TO_NAME[const.KEY_COMPONENTID_V4L] + ) # Update priorities (Effect) effect = "foo" @@ -682,7 +797,10 @@ async def test_light_async_updates_from_hyperion_client( assert entity_state assert entity_state.attributes["effect_list"] == [ hyperion_light.KEY_EFFECT_SOLID - ] + const.KEY_COMPONENTID_EXTERNAL_SOURCES + [ + ] + [ + const.KEY_COMPONENTID_TO_NAME[component] + for component in const.KEY_COMPONENTID_EXTERNAL_SOURCES + ] + [ effect[const.KEY_NAME] for effect in effects ] @@ -1171,15 +1289,17 @@ async def test_light_option_effect_hide_list(hass: HomeAssistantType) -> None: client.effects = [{const.KEY_NAME: "One"}, {const.KEY_NAME: "Two"}] await setup_test_config_entry( - hass, hyperion_client=client, options={CONF_EFFECT_HIDE_LIST: ["Two", "V4L"]} + hass, + hyperion_client=client, + options={CONF_EFFECT_HIDE_LIST: ["Two", "USB Capture"]}, ) entity_state = hass.states.get(TEST_ENTITY_ID_1) assert entity_state assert entity_state.attributes["effect_list"] == [ "Solid", - "BOBLIGHTSERVER", - "GRABBER", + "Boblight Server", + "Platform Capture", "One", ] @@ -1247,3 +1367,45 @@ async def test_lights_can_be_enabled(hass: HomeAssistantType) -> None: entity_state = hass.states.get(TEST_PRIORITY_LIGHT_ENTITY_ID_1) assert entity_state + + +async def test_deprecated_effect_names(caplog, hass: HomeAssistantType) -> None: # type: ignore[no-untyped-def] + """Test deprecated effects function and issue a warning.""" + client = create_mock_client() + client.async_send_clear = AsyncMock(return_value=True) + client.async_send_set_component = AsyncMock(return_value=True) + + await setup_test_config_entry(hass, hyperion_client=client) + + for component in const.KEY_COMPONENTID_EXTERNAL_SOURCES: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: TEST_ENTITY_ID_1, ATTR_EFFECT: component}, + blocking=True, + ) + assert "Use of Hyperion effect '%s' is deprecated" % component in caplog.text + + # Simulate a state callback from Hyperion. + client.visible_priority = { + const.KEY_COMPONENTID: component, + } + call_registered_callback(client, "priorities-update") + + entity_state = hass.states.get(TEST_ENTITY_ID_1) + assert entity_state + assert ( + entity_state.attributes["effect"] + == const.KEY_COMPONENTID_TO_NAME[component] + ) + + +async def test_deprecated_effect_names_not_in_effect_list( + hass: HomeAssistantType, +) -> None: + """Test deprecated effects are not in shown effect list.""" + await setup_test_config_entry(hass) + entity_state = hass.states.get(TEST_ENTITY_ID_1) + assert entity_state + for component in const.KEY_COMPONENTID_EXTERNAL_SOURCES: + assert component not in entity_state.attributes["effect_list"] diff --git a/tests/components/hyperion/test_switch.py b/tests/components/hyperion/test_switch.py index af1336bf0f86d..5105d80f40d5d 100644 --- a/tests/components/hyperion/test_switch.py +++ b/tests/components/hyperion/test_switch.py @@ -5,13 +5,13 @@ from hyperion.const import ( KEY_COMPONENT, KEY_COMPONENTID_ALL, + KEY_COMPONENTID_TO_NAME, KEY_COMPONENTSTATE, KEY_STATE, ) from homeassistant.components.hyperion import get_hyperion_device_id from homeassistant.components.hyperion.const import ( - COMPONENT_TO_NAME, DOMAIN, HYPERION_MANUFACTURER_NAME, HYPERION_MODEL_NAME, @@ -128,7 +128,7 @@ async def test_switch_has_correct_entities(hass: HomeAssistantType) -> None: # Setup component switch. for component in TEST_COMPONENTS: - name = slugify(COMPONENT_TO_NAME[str(component["name"])]) + name = slugify(KEY_COMPONENTID_TO_NAME[str(component["name"])]) register_test_entity( hass, SWITCH_DOMAIN, @@ -138,7 +138,7 @@ async def test_switch_has_correct_entities(hass: HomeAssistantType) -> None: await setup_test_config_entry(hass, hyperion_client=client) for component in TEST_COMPONENTS: - name = slugify(COMPONENT_TO_NAME[str(component["name"])]) + name = slugify(KEY_COMPONENTID_TO_NAME[str(component["name"])]) entity_id = TEST_SWITCH_COMPONENT_BASE_ENTITY_ID + "_" + name entity_state = hass.states.get(entity_id) assert entity_state, f"Couldn't find entity: {entity_id}" @@ -150,13 +150,14 @@ async def test_device_info(hass: HomeAssistantType) -> None: client.components = TEST_COMPONENTS for component in TEST_COMPONENTS: - name = slugify(COMPONENT_TO_NAME[str(component["name"])]) + name = slugify(KEY_COMPONENTID_TO_NAME[str(component["name"])]) register_test_entity( hass, SWITCH_DOMAIN, f"{TYPE_HYPERION_COMPONENT_SWITCH_BASE}_{name}", f"{TEST_SWITCH_COMPONENT_BASE_ENTITY_ID}_{name}", ) + await setup_test_config_entry(hass, hyperion_client=client) assert hass.states.get(TEST_SWITCH_COMPONENT_ALL_ENTITY_ID) is not None @@ -178,7 +179,7 @@ async def test_device_info(hass: HomeAssistantType) -> None: ] for component in TEST_COMPONENTS: - name = slugify(COMPONENT_TO_NAME[str(component["name"])]) + name = slugify(KEY_COMPONENTID_TO_NAME[str(component["name"])]) entity_id = TEST_SWITCH_COMPONENT_BASE_ENTITY_ID + "_" + name assert entity_id in entities_from_device @@ -192,7 +193,7 @@ async def test_switches_can_be_enabled(hass: HomeAssistantType) -> None: entity_registry = er.async_get(hass) for component in TEST_COMPONENTS: - name = slugify(COMPONENT_TO_NAME[str(component["name"])]) + name = slugify(KEY_COMPONENTID_TO_NAME[str(component["name"])]) entity_id = TEST_SWITCH_COMPONENT_BASE_ENTITY_ID + "_" + name entry = entity_registry.async_get(entity_id) From 8305fbc0ebbbd50906d265423a1249fdfe48bbcb Mon Sep 17 00:00:00 2001 From: Nathan Tilley Date: Mon, 19 Apr 2021 18:39:24 -0400 Subject: [PATCH 0375/1317] Bump faadelays to 0.0.7 (#49443) --- homeassistant/components/faa_delays/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/faa_delays/manifest.json b/homeassistant/components/faa_delays/manifest.json index c829ac5b1719e..caa6c3bb33a9b 100644 --- a/homeassistant/components/faa_delays/manifest.json +++ b/homeassistant/components/faa_delays/manifest.json @@ -3,7 +3,7 @@ "name": "FAA Delays", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/faa_delays", - "requirements": ["faadelays==0.0.6"], + "requirements": ["faadelays==0.0.7"], "codeowners": ["@ntilley905"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 301e2665fa744..5077b0f653871 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -578,7 +578,7 @@ eternalegypt==0.0.12 evohome-async==0.3.8 # homeassistant.components.faa_delays -faadelays==0.0.6 +faadelays==0.0.7 # homeassistant.components.dlib_face_detect # homeassistant.components.dlib_face_identify diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e5f0f72081ff3..4ecedfe9cd976 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -312,7 +312,7 @@ ephem==3.7.7.0 epson-projector==0.2.3 # homeassistant.components.faa_delays -faadelays==0.0.6 +faadelays==0.0.7 # homeassistant.components.feedreader feedparser==6.0.2 From f6a24e8d68e5a2abf9039595c197fc7bcbd4c01f Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Tue, 20 Apr 2021 00:04:05 +0000 Subject: [PATCH 0376/1317] [ci skip] Translation update --- .../components/abode/translations/ro.json | 7 +++++ .../coronavirus/translations/no.json | 3 +- .../components/ezviz/translations/ro.json | 29 +++++++++++++++++++ .../components/hue/translations/ro.json | 3 +- .../kostal_plenticore/translations/ro.json | 7 +++++ .../components/lyric/translations/it.json | 7 ++++- .../components/lyric/translations/no.json | 7 ++++- .../components/mysensors/translations/ro.json | 29 +++++++++++++++++++ .../components/nest/translations/ro.json | 6 ++++ .../components/nuki/translations/ro.json | 7 +++++ .../philips_js/translations/ro.json | 11 +++++++ .../components/verisure/translations/ro.json | 15 ++++++++++ .../components/zone/translations/ro.json | 19 ++++++++++++ 13 files changed, 146 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/abode/translations/ro.json create mode 100644 homeassistant/components/ezviz/translations/ro.json create mode 100644 homeassistant/components/kostal_plenticore/translations/ro.json create mode 100644 homeassistant/components/mysensors/translations/ro.json create mode 100644 homeassistant/components/nuki/translations/ro.json create mode 100644 homeassistant/components/philips_js/translations/ro.json create mode 100644 homeassistant/components/verisure/translations/ro.json create mode 100644 homeassistant/components/zone/translations/ro.json diff --git a/homeassistant/components/abode/translations/ro.json b/homeassistant/components/abode/translations/ro.json new file mode 100644 index 0000000000000..0b5f3c35ea709 --- /dev/null +++ b/homeassistant/components/abode/translations/ro.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "reauth_successful": "Re-autentificare efectuata cu succes" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coronavirus/translations/no.json b/homeassistant/components/coronavirus/translations/no.json index bf111868e4bf1..59cb02ac22d28 100644 --- a/homeassistant/components/coronavirus/translations/no.json +++ b/homeassistant/components/coronavirus/translations/no.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Tjenesten er allerede konfigurert" + "already_configured": "Tjenesten er allerede konfigurert", + "cannot_connect": "Tilkobling mislyktes" }, "step": { "user": { diff --git a/homeassistant/components/ezviz/translations/ro.json b/homeassistant/components/ezviz/translations/ro.json new file mode 100644 index 0000000000000..86ea033d66ec7 --- /dev/null +++ b/homeassistant/components/ezviz/translations/ro.json @@ -0,0 +1,29 @@ +{ + "config": { + "error": { + "invalid_auth": "Autentificare nereu\u0219it\u0103" + }, + "step": { + "confirm": { + "data": { + "password": "Parola", + "username": "Utilizator" + }, + "title": "Camera Ezviz a fost descoperit\u0103" + }, + "user": { + "data": { + "password": "Parola", + "url": "URL", + "username": "Utilizator" + } + }, + "user_custom_url": { + "data": { + "password": "Parola", + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/translations/ro.json b/homeassistant/components/hue/translations/ro.json index 055cd02dffbdb..54308c7670890 100644 --- a/homeassistant/components/hue/translations/ro.json +++ b/homeassistant/components/hue/translations/ro.json @@ -4,7 +4,8 @@ "all_configured": "Toate pun\u021bile Philips Hue sunt deja configurate", "already_configured": "Gateway-ul este deja configurat", "cannot_connect": "Nu se poate conecta la gateway.", - "discover_timeout": "Imposibil de descoperit podurile Hue" + "discover_timeout": "Imposibil de descoperit podurile Hue", + "unknown": "Eroare nea\u0219teptat\u0103" }, "error": { "linking": "A ap\u0103rut o eroare de leg\u0103tur\u0103 necunoscut\u0103.", diff --git a/homeassistant/components/kostal_plenticore/translations/ro.json b/homeassistant/components/kostal_plenticore/translations/ro.json new file mode 100644 index 0000000000000..65465dc1bb3e2 --- /dev/null +++ b/homeassistant/components/kostal_plenticore/translations/ro.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "unknown": "Eroare nea\u0219teptat\u0103" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/it.json b/homeassistant/components/lyric/translations/it.json index 42536508716c7..809e6608b80c7 100644 --- a/homeassistant/components/lyric/translations/it.json +++ b/homeassistant/components/lyric/translations/it.json @@ -2,7 +2,8 @@ "config": { "abort": { "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.", - "missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione." + "missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione.", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "create_entry": { "default": "Autenticazione riuscita" @@ -10,6 +11,10 @@ "step": { "pick_implementation": { "title": "Scegli il metodo di autenticazione" + }, + "reauth_confirm": { + "description": "L'integrazione di Lyric deve autenticare nuovamente il tuo account.", + "title": "Autenticare nuovamente l'integrazione" } } } diff --git a/homeassistant/components/lyric/translations/no.json b/homeassistant/components/lyric/translations/no.json index a8f6ce4f9a3f3..537cc7fcced40 100644 --- a/homeassistant/components/lyric/translations/no.json +++ b/homeassistant/components/lyric/translations/no.json @@ -2,7 +2,8 @@ "config": { "abort": { "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse", - "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen" + "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "create_entry": { "default": "Vellykket godkjenning" @@ -10,6 +11,10 @@ "step": { "pick_implementation": { "title": "Velg godkjenningsmetode" + }, + "reauth_confirm": { + "description": "Lyric-integrasjonen m\u00e5 godkjenne kontoen din p\u00e5 nytt.", + "title": "Godkjenne integrering p\u00e5 nytt" } } } diff --git a/homeassistant/components/mysensors/translations/ro.json b/homeassistant/components/mysensors/translations/ro.json new file mode 100644 index 0000000000000..5a8cb19a928e9 --- /dev/null +++ b/homeassistant/components/mysensors/translations/ro.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "unknown": "Eroare nea\u0219teptat\u0103" + }, + "error": { + "already_configured": "Dispozitivul este deja configurat", + "invalid_auth": "Autentificare nereu\u0219it\u0103" + }, + "step": { + "gw_serial": { + "description": "Configurare gateway serial" + }, + "gw_tcp": { + "data": { + "device": "Adresa IP a gateway-ului", + "tcp_port": "port", + "version": "Versiunea SenzorulMeu" + }, + "description": "Configurare gateway Ethernet" + }, + "user": { + "data": { + "gateway_type": "Tip gateway" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/translations/ro.json b/homeassistant/components/nest/translations/ro.json index afad668a98bee..be88440071785 100644 --- a/homeassistant/components/nest/translations/ro.json +++ b/homeassistant/components/nest/translations/ro.json @@ -1,6 +1,12 @@ { "config": { + "error": { + "unknown": "Eroare nea\u0219teptat\u0103" + }, "step": { + "init": { + "description": "Alege metoda de autentificare" + }, "link": { "data": { "code": "Cod PIN" diff --git a/homeassistant/components/nuki/translations/ro.json b/homeassistant/components/nuki/translations/ro.json new file mode 100644 index 0000000000000..0b5f3c35ea709 --- /dev/null +++ b/homeassistant/components/nuki/translations/ro.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "reauth_successful": "Re-autentificare efectuata cu succes" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/philips_js/translations/ro.json b/homeassistant/components/philips_js/translations/ro.json new file mode 100644 index 0000000000000..aea8efa9d0d83 --- /dev/null +++ b/homeassistant/components/philips_js/translations/ro.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "pair": { + "data": { + "pin": "Cod PIN" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/verisure/translations/ro.json b/homeassistant/components/verisure/translations/ro.json new file mode 100644 index 0000000000000..9fbfe6002ae23 --- /dev/null +++ b/homeassistant/components/verisure/translations/ro.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "reauth_successful": "Re-autentificare efectuata cu succes" + }, + "step": { + "reauth_confirm": { + "data": { + "email": "E-mail", + "password": "Parola" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/translations/ro.json b/homeassistant/components/zone/translations/ro.json new file mode 100644 index 0000000000000..f6e4a39bcc339 --- /dev/null +++ b/homeassistant/components/zone/translations/ro.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "Numele exista deja" + }, + "step": { + "init": { + "data": { + "latitude": "Latitudine", + "longitude": "Longitudine", + "name": "Nume", + "radius": "Raza" + }, + "title": "Definire parametrii zona" + } + }, + "title": "Zona" + } +} \ No newline at end of file From 523a71ac208b1d5e5ca923b2b4b66b400fae97c2 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 19 Apr 2021 19:41:30 -0500 Subject: [PATCH 0377/1317] Set temperature precision for Ecobee climate entities to tenths (#48697) --- homeassistant/components/ecobee/climate.py | 14 ++++++++++---- tests/components/ecobee/test_climate.py | 6 +++--- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index dd29918ec186b..6de23f09c603c 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -32,6 +32,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_TEMPERATURE, + PRECISION_TENTHS, STATE_ON, TEMP_FAHRENHEIT, ) @@ -379,6 +380,11 @@ def temperature_unit(self): """Return the unit of measurement.""" return TEMP_FAHRENHEIT + @property + def precision(self) -> float: + """Return the precision of the system.""" + return PRECISION_TENTHS + @property def current_temperature(self): """Return the current temperature.""" @@ -388,14 +394,14 @@ def current_temperature(self): def target_temperature_low(self): """Return the lower bound temperature we try to reach.""" if self.hvac_mode == HVAC_MODE_HEAT_COOL: - return self.thermostat["runtime"]["desiredHeat"] / 10.0 + return round(self.thermostat["runtime"]["desiredHeat"] / 10.0) return None @property def target_temperature_high(self): """Return the upper bound temperature we try to reach.""" if self.hvac_mode == HVAC_MODE_HEAT_COOL: - return self.thermostat["runtime"]["desiredCool"] / 10.0 + return round(self.thermostat["runtime"]["desiredCool"] / 10.0) return None @property @@ -429,9 +435,9 @@ def target_temperature(self): if self.hvac_mode == HVAC_MODE_HEAT_COOL: return None if self.hvac_mode == HVAC_MODE_HEAT: - return self.thermostat["runtime"]["desiredHeat"] / 10.0 + return round(self.thermostat["runtime"]["desiredHeat"] / 10.0) if self.hvac_mode == HVAC_MODE_COOL: - return self.thermostat["runtime"]["desiredCool"] / 10.0 + return round(self.thermostat["runtime"]["desiredCool"] / 10.0) return None @property diff --git a/tests/components/ecobee/test_climate.py b/tests/components/ecobee/test_climate.py index 86f9926b75696..da6017a71a145 100644 --- a/tests/components/ecobee/test_climate.py +++ b/tests/components/ecobee/test_climate.py @@ -83,14 +83,14 @@ async def test_target_temperature_low(ecobee_fixture, thermostat): """Test target low temperature.""" assert thermostat.target_temperature_low == 40 ecobee_fixture["runtime"]["desiredHeat"] = 502 - assert thermostat.target_temperature_low == 50.2 + assert thermostat.target_temperature_low == 50 async def test_target_temperature_high(ecobee_fixture, thermostat): """Test target high temperature.""" assert thermostat.target_temperature_high == 20 - ecobee_fixture["runtime"]["desiredCool"] = 103 - assert thermostat.target_temperature_high == 10.3 + ecobee_fixture["runtime"]["desiredCool"] = 679 + assert thermostat.target_temperature_high == 68 async def test_target_temperature(ecobee_fixture, thermostat): From a278ebd37b276869f65b5a4042f030326e8d28d4 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Tue, 20 Apr 2021 10:43:14 +0200 Subject: [PATCH 0378/1317] Bump pymodbus version to 2.5.1 (#49401) --- homeassistant/components/modbus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/modbus/manifest.json b/homeassistant/components/modbus/manifest.json index 8d033968e2ff2..0833292a7e318 100644 --- a/homeassistant/components/modbus/manifest.json +++ b/homeassistant/components/modbus/manifest.json @@ -2,7 +2,7 @@ "domain": "modbus", "name": "Modbus", "documentation": "https://www.home-assistant.io/integrations/modbus", - "requirements": ["pymodbus==2.3.0"], + "requirements": ["pymodbus==2.5.1"], "codeowners": ["@adamchengtkc", "@janiversen", "@vzahradnik"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 5077b0f653871..677e2ff37f2e8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1551,7 +1551,7 @@ pymitv==1.4.3 pymochad==0.2.0 # homeassistant.components.modbus -pymodbus==2.3.0 +pymodbus==2.5.1 # homeassistant.components.monoprice pymonoprice==0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4ecedfe9cd976..e2841afab7659 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -850,7 +850,7 @@ pymfy==0.9.3 pymochad==0.2.0 # homeassistant.components.modbus -pymodbus==2.3.0 +pymodbus==2.5.1 # homeassistant.components.monoprice pymonoprice==0.3 From 12853438c53b4da5887935b6dba1ab082d841c6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Klomp?= Date: Tue, 20 Apr 2021 10:59:02 +0200 Subject: [PATCH 0379/1317] SMA code quality improvement and bugfix (#49346) * Minor code quality improvements Thanks to @MartinHjelmare * Convert legacy dict config to list * Improved test * Typo * Test improvements * Create fixtures in conftest.py --- homeassistant/components/sma/__init__.py | 49 +++++++----- tests/components/sma/__init__.py | 59 +++++---------- tests/components/sma/conftest.py | 33 ++++++++ tests/components/sma/test_config_flow.py | 95 ++++++++++++++---------- tests/components/sma/test_sensor.py | 6 +- 5 files changed, 142 insertions(+), 100 deletions(-) create mode 100644 tests/components/sma/conftest.py diff --git a/homeassistant/components/sma/__init__.py b/homeassistant/components/sma/__init__.py index 5a4123ec10b6b..e17437db065b6 100644 --- a/homeassistant/components/sma/__init__.py +++ b/homeassistant/components/sma/__init__.py @@ -2,6 +2,7 @@ import asyncio from datetime import timedelta import logging +from typing import List import pysma @@ -39,7 +40,7 @@ _LOGGER = logging.getLogger(__name__) -async def _parse_legacy_options(entry: ConfigEntry, sensor_def: pysma.Sensors) -> None: +def _parse_legacy_options(entry: ConfigEntry, sensor_def: pysma.Sensors) -> List[str]: """Parse legacy configuration options. This will parse the legacy CONF_SENSORS and CONF_CUSTOM configuration options @@ -57,7 +58,18 @@ async def _parse_legacy_options(entry: ConfigEntry, sensor_def: pysma.Sensors) - # Parsing of sensors configuration config_sensors = entry.data.get(CONF_SENSORS) if not config_sensors: - return + return [] + + # Support import of legacy config that should have been removed from 0.99, but was still functional + # See also #25880 and #26306. Functional support was dropped in #48003 + if isinstance(config_sensors, dict): + config_sensors_list = [] + + for name, attr in config_sensors.items(): + config_sensors_list.append(name) + config_sensors_list.extend(attr) + + config_sensors = config_sensors_list # Find and replace sensors removed from pysma # This only alters the config, the actual sensor migration takes place in _migrate_old_unique_ids @@ -70,20 +82,21 @@ async def _parse_legacy_options(entry: ConfigEntry, sensor_def: pysma.Sensors) - for sensor in sensor_def: sensor.enabled = sensor.name in config_sensors + return config_sensors -async def _migrate_old_unique_ids( - hass: HomeAssistant, entry: ConfigEntry, sensor_def: pysma.Sensors + +def _migrate_old_unique_ids( + hass: HomeAssistant, + entry: ConfigEntry, + sensor_def: pysma.Sensors, + config_sensors: List[str], ) -> None: """Migrate legacy sensor entity_id format to new format.""" entity_registry = er.async_get(hass) # Create list of all possible sensor names - possible_sensors = list( - set( - entry.data.get(CONF_SENSORS) - + [s.name for s in sensor_def] - + list(pysma.LEGACY_MAP) - ) + possible_sensors = set( + config_sensors + [s.name for s in sensor_def] + list(pysma.LEGACY_MAP) ) for sensor in possible_sensors: @@ -107,7 +120,7 @@ async def _migrate_old_unique_ids( if not entity_id: continue - # Change entity_id to new format using the device serial in entry.unique_id + # Change unique_id to new format using the device serial in entry.unique_id new_unique_id = f"{entry.unique_id}-{pysma_sensor.key}_{pysma_sensor.key_idx}" entity_registry.async_update_entity(entity_id, new_unique_id=new_unique_id) @@ -118,15 +131,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: sensor_def = pysma.Sensors() if entry.source == SOURCE_IMPORT: - await _parse_legacy_options(entry, sensor_def) - await _migrate_old_unique_ids(hass, entry, sensor_def) + config_sensors = _parse_legacy_options(entry, sensor_def) + _migrate_old_unique_ids(hass, entry, sensor_def, config_sensors) # Init the SMA interface - protocol = "https" if entry.data.get(CONF_SSL) else "http" - url = f"{protocol}://{entry.data.get(CONF_HOST)}" - verify_ssl = entry.data.get(CONF_VERIFY_SSL) - group = entry.data.get(CONF_GROUP) - password = entry.data.get(CONF_PASSWORD) + protocol = "https" if entry.data[CONF_SSL] else "http" + url = f"{protocol}://{entry.data[CONF_HOST]}" + verify_ssl = entry.data[CONF_VERIFY_SSL] + group = entry.data[CONF_GROUP] + password = entry.data[CONF_PASSWORD] session = async_get_clientsession(hass, verify_ssl=verify_ssl) sma = pysma.SMA(session, url, password, group) diff --git a/tests/components/sma/__init__.py b/tests/components/sma/__init__.py index 05e9dc9f4cf41..0797558958e70 100644 --- a/tests/components/sma/__init__.py +++ b/tests/components/sma/__init__.py @@ -1,11 +1,6 @@ """Tests for the sma integration.""" from unittest.mock import patch -from homeassistant.components.sma.const import DOMAIN -from homeassistant.helpers import entity_registry as er - -from tests.common import MockConfigEntry - MOCK_DEVICE = { "manufacturer": "SMA", "name": "SMA Device Name", @@ -38,6 +33,25 @@ }, } +MOCK_IMPORT_DICT = { + "platform": "sma", + "host": "1.1.1.1", + "ssl": True, + "verify_ssl": False, + "group": "user", + "password": "password", + "sensors": { + "pv_power": [], + "pv_gen_meter": [], + "solar_daily": ["daily_yield", "total_yield"], + "status": ["grid_power", "frequency", "voltage_l1", "operating_time"], + }, + "custom": { + "operating_time": {"key": "6400_00462E00", "unit": "uur", "factor": 3600}, + "solar_daily": {"key": "6400_00262200", "unit": "kWh", "factor": 1000}, + }, +} + MOCK_CUSTOM_SENSOR = { "name": "yesterday_consumption", "key": "6400_00543A01", @@ -83,41 +97,6 @@ **MOCK_USER_INPUT, ) -MOCK_LEGACY_ENTRY = er.RegistryEntry( - entity_id="sensor.pv_power", - unique_id="sma-6100_0046C200-pv_power", - platform="sma", - unit_of_measurement="W", - original_name="pv_power", -) - - -async def init_integration(hass): - """Create a fake SMA Config Entry.""" - entry = MockConfigEntry( - domain=DOMAIN, - title=MOCK_DEVICE["name"], - unique_id=MOCK_DEVICE["serial"], - data=MOCK_CUSTOM_SETUP_DATA, - source="import", - ) - entry.add_to_hass(hass) - - with patch( - "homeassistant.helpers.update_coordinator.DataUpdateCoordinator.async_config_entry_first_refresh" - ): - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - return entry - - -def _patch_validate_input(return_value=MOCK_DEVICE, side_effect=None): - return patch( - "homeassistant.components.sma.config_flow.validate_input", - return_value=return_value, - side_effect=side_effect, - ) - def _patch_async_setup_entry(return_value=True): return patch( diff --git a/tests/components/sma/conftest.py b/tests/components/sma/conftest.py new file mode 100644 index 0000000000000..7522aeedf1b0a --- /dev/null +++ b/tests/components/sma/conftest.py @@ -0,0 +1,33 @@ +"""Fixtures for sma tests.""" +from unittest.mock import patch + +import pytest + +from homeassistant.components.sma.const import DOMAIN + +from . import MOCK_CUSTOM_SETUP_DATA, MOCK_DEVICE + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_config_entry(): + """Return the default mocked config entry.""" + return MockConfigEntry( + domain=DOMAIN, + title=MOCK_DEVICE["name"], + unique_id=MOCK_DEVICE["serial"], + data=MOCK_CUSTOM_SETUP_DATA, + source="import", + ) + + +@pytest.fixture +async def init_integration(hass, mock_config_entry): + """Create a fake SMA Config Entry.""" + mock_config_entry.add_to_hass(hass) + + with patch("pysma.SMA.read"): + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + return mock_config_entry diff --git a/tests/components/sma/test_config_flow.py b/tests/components/sma/test_config_flow.py index d248b2206dae7..dbcecbeb43c33 100644 --- a/tests/components/sma/test_config_flow.py +++ b/tests/components/sma/test_config_flow.py @@ -11,20 +11,18 @@ RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM, ) -from homeassistant.helpers import entity_registry as er from . import ( MOCK_DEVICE, MOCK_IMPORT, - MOCK_LEGACY_ENTRY, + MOCK_IMPORT_DICT, MOCK_SETUP_DATA, MOCK_USER_INPUT, _patch_async_setup_entry, - _patch_validate_input, ) -async def test_form(hass, aioclient_mock): +async def test_form(hass): """Test we get the form.""" await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( @@ -49,30 +47,34 @@ async def test_form(hass, aioclient_mock): assert len(mock_setup_entry.mock_calls) == 1 -async def test_form_cannot_connect(hass, aioclient_mock): +async def test_form_cannot_connect(hass): """Test we handle cannot connect error.""" - aioclient_mock.get("https://1.1.1.1/data/l10n/en-US.json", exc=aiohttp.ClientError) - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - MOCK_USER_INPUT, - ) + with patch( + "pysma.SMA.new_session", side_effect=aiohttp.ClientError + ), _patch_async_setup_entry() as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_USER_INPUT, + ) assert result["type"] == RESULT_TYPE_FORM assert result["errors"] == {"base": "cannot_connect"} + assert len(mock_setup_entry.mock_calls) == 0 -async def test_form_invalid_auth(hass, aioclient_mock): +async def test_form_invalid_auth(hass): """Test we handle invalid auth error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - with patch("pysma.SMA.new_session", return_value=False): + with patch( + "pysma.SMA.new_session", return_value=False + ), _patch_async_setup_entry() as mock_setup_entry: result = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_USER_INPUT, @@ -80,9 +82,10 @@ async def test_form_invalid_auth(hass, aioclient_mock): assert result["type"] == RESULT_TYPE_FORM assert result["errors"] == {"base": "invalid_auth"} + assert len(mock_setup_entry.mock_calls) == 0 -async def test_form_cannot_retrieve_device_info(hass, aioclient_mock): +async def test_form_cannot_retrieve_device_info(hass): """Test we handle cannot retrieve device info error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -90,7 +93,7 @@ async def test_form_cannot_retrieve_device_info(hass, aioclient_mock): with patch("pysma.SMA.new_session", return_value=True), patch( "pysma.SMA.read", return_value=False - ): + ), _patch_async_setup_entry() as mock_setup_entry: result = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_USER_INPUT, @@ -98,6 +101,7 @@ async def test_form_cannot_retrieve_device_info(hass, aioclient_mock): assert result["type"] == RESULT_TYPE_FORM assert result["errors"] == {"base": "cannot_retrieve_device_info"} + assert len(mock_setup_entry.mock_calls) == 0 async def test_form_unexpected_exception(hass): @@ -106,7 +110,9 @@ async def test_form_unexpected_exception(hass): DOMAIN, context={"source": SOURCE_USER} ) - with _patch_validate_input(side_effect=Exception): + with patch( + "pysma.SMA.new_session", side_effect=Exception + ), _patch_async_setup_entry() as mock_setup_entry: result = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_USER_INPUT, @@ -114,28 +120,23 @@ async def test_form_unexpected_exception(hass): assert result["type"] == RESULT_TYPE_FORM assert result["errors"] == {"base": "unknown"} + assert len(mock_setup_entry.mock_calls) == 0 -async def test_form_already_configured(hass): +async def test_form_already_configured(hass, mock_config_entry): """Test starting a flow by user when already configured.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) - - with _patch_validate_input(): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - MOCK_USER_INPUT, - ) - - assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["result"].unique_id == MOCK_DEVICE["serial"] + mock_config_entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - with _patch_validate_input(): + with patch("pysma.SMA.new_session", return_value=True), patch( + "pysma.SMA.device_info", return_value=MOCK_DEVICE + ), patch( + "pysma.SMA.close_session", return_value=True + ), _patch_async_setup_entry() as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_USER_INPUT, @@ -143,16 +144,18 @@ async def test_form_already_configured(hass): assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "already_configured" + assert len(mock_setup_entry.mock_calls) == 0 async def test_import(hass): """Test we can import.""" - entity_registry = er.async_get(hass) - entity_registry._register_entry(MOCK_LEGACY_ENTRY) - await setup.async_setup_component(hass, "persistent_notification", {}) - with _patch_validate_input(): + with patch("pysma.SMA.new_session", return_value=True), patch( + "pysma.SMA.device_info", return_value=MOCK_DEVICE + ), patch( + "pysma.SMA.close_session", return_value=True + ), _patch_async_setup_entry() as mock_setup_entry: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, @@ -162,9 +165,25 @@ async def test_import(hass): assert result["type"] == RESULT_TYPE_CREATE_ENTRY assert result["title"] == MOCK_USER_INPUT["host"] assert result["data"] == MOCK_IMPORT + assert len(mock_setup_entry.mock_calls) == 1 - assert MOCK_LEGACY_ENTRY.original_name not in result["data"]["sensors"] - assert "pv_power_a" in result["data"]["sensors"] - entity = entity_registry.async_get(MOCK_LEGACY_ENTRY.entity_id) - assert entity.unique_id == f"{MOCK_DEVICE['serial']}-6380_40251E00_0" +async def test_import_sensor_dict(hass): + """Test we can import.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + with patch("pysma.SMA.new_session", return_value=True), patch( + "pysma.SMA.device_info", return_value=MOCK_DEVICE + ), patch( + "pysma.SMA.close_session", return_value=True + ), _patch_async_setup_entry() as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=MOCK_IMPORT_DICT, + ) + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == MOCK_USER_INPUT["host"] + assert result["data"] == MOCK_IMPORT_DICT + assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/sma/test_sensor.py b/tests/components/sma/test_sensor.py index 7d5be09222c78..b86533a11df77 100644 --- a/tests/components/sma/test_sensor.py +++ b/tests/components/sma/test_sensor.py @@ -5,13 +5,11 @@ POWER_WATT, ) -from . import MOCK_CUSTOM_SENSOR, init_integration +from . import MOCK_CUSTOM_SENSOR -async def test_sensors(hass): +async def test_sensors(hass, init_integration): """Test states of the sensors.""" - await init_integration(hass) - state = hass.states.get("sensor.current_consumption") assert state assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT From b557d20fbb7c25bf9d4ea2b7e55f1568c02cb3ae Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 19 Apr 2021 23:03:07 -1000 Subject: [PATCH 0380/1317] Fix memory leak in netatmo (#49464) --- homeassistant/components/netatmo/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index b9b04a08febbe..131542acb0e34 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -188,7 +188,9 @@ async def handle_event(event): except pyatmo.ApiError as err: _LOGGER.error("Error during webhook registration - %s", err) - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, unregister_webhook) + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, unregister_webhook) + ) if hass.state == CoreState.running: await register_webhook(None) From 82152616bb9f8910c7100ee541d7143a1c937fba Mon Sep 17 00:00:00 2001 From: chpego <38792705+chpego@users.noreply.github.com> Date: Tue, 20 Apr 2021 09:49:54 +0000 Subject: [PATCH 0381/1317] Bump youtube-dl to 2021.04.17 (#49474) --- homeassistant/components/media_extractor/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index 5872e0bd84191..1d59c02d9aced 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -2,7 +2,7 @@ "domain": "media_extractor", "name": "Media Extractor", "documentation": "https://www.home-assistant.io/integrations/media_extractor", - "requirements": ["youtube_dl==2021.03.14"], + "requirements": ["youtube_dl==2021.04.17"], "dependencies": ["media_player"], "codeowners": [], "quality_scale": "internal", diff --git a/requirements_all.txt b/requirements_all.txt index 677e2ff37f2e8..cae71d7529518 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2369,7 +2369,7 @@ yeelight==0.6.0 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2021.03.14 +youtube_dl==2021.04.17 # homeassistant.components.onvif zeep[async]==4.0.0 From bc5add82e01d6802c0ce23582355b3f5e67aed8e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 20 Apr 2021 12:01:19 +0200 Subject: [PATCH 0382/1317] Fix/Workaround GitHub issue forms (#49475) --- .github/ISSUE_TEMPLATE/bug_report.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 50a3dd55e866d..116afec36eecb 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,6 +1,5 @@ name: Report an issue with Home Assistant Core description: Report an issue with Home Assistant Core. -title: "" body: - type: markdown attributes: From c14e525ac33fe05e19dfbbf18f482a3a3e8b5624 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Tue, 20 Apr 2021 14:54:20 +0200 Subject: [PATCH 0383/1317] Update modbus state when sensor fails (#49481) --- .coveragerc | 1 + homeassistant/components/modbus/binary_sensor.py | 1 + homeassistant/components/modbus/sensor.py | 1 + 3 files changed, 3 insertions(+) diff --git a/.coveragerc b/.coveragerc index 576a0e9603668..0078882e167f5 100644 --- a/.coveragerc +++ b/.coveragerc @@ -619,6 +619,7 @@ omit = homeassistant/components/modbus/modbus.py homeassistant/components/modbus/switch.py homeassistant/components/modbus/sensor.py + homeassistant/components/modbus/binary_sensor.py homeassistant/components/modem_callerid/sensor.py homeassistant/components/motion_blinds/__init__.py homeassistant/components/motion_blinds/const.py diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py index c9f551e3d348a..cd336ba4f7361 100644 --- a/homeassistant/components/modbus/binary_sensor.py +++ b/homeassistant/components/modbus/binary_sensor.py @@ -170,6 +170,7 @@ def _update(self): result = self._hub.read_discrete_inputs(self._slave, self._address, 1) if result is None: self._available = False + self.schedule_update_ha_state() return self._value = result.bits[0] & 1 diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index cb76bedd18ff1..89c68947d3fc8 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -294,6 +294,7 @@ def _update(self): ) if result is None: self._available = False + self.schedule_update_ha_state() return registers = result.registers From 05982ffc6052e9fc1aad80c52b9510a2b966b156 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 20 Apr 2021 03:09:46 -1000 Subject: [PATCH 0384/1317] Ensure harmony callbacks run in the event loop (#49450) --- .../components/harmony/connection_state.py | 4 ++-- homeassistant/components/harmony/remote.py | 24 ++++++++++--------- homeassistant/components/harmony/switch.py | 12 ++++++---- 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/harmony/connection_state.py b/homeassistant/components/harmony/connection_state.py index 9706ba28776d0..84ad353480c08 100644 --- a/homeassistant/components/harmony/connection_state.py +++ b/homeassistant/components/harmony/connection_state.py @@ -16,14 +16,14 @@ def __init__(self): super().__init__() self._unsub_mark_disconnected = None - async def got_connected(self, _=None): + async def async_got_connected(self, _=None): """Notification that we're connected to the HUB.""" _LOGGER.debug("%s: connected to the HUB", self._name) self.async_write_ha_state() self._clear_disconnection_delay() - async def got_disconnected(self, _=None): + async def async_got_disconnected(self, _=None): """Notification that we're disconnected from the HUB.""" _LOGGER.debug("%s: disconnected from the HUB", self._name) # We're going to wait for 10 seconds before announcing we're diff --git a/homeassistant/components/harmony/remote.py b/homeassistant/components/harmony/remote.py index 518ff92368c4d..54d6b0fa7d1b3 100644 --- a/homeassistant/components/harmony/remote.py +++ b/homeassistant/components/harmony/remote.py @@ -16,7 +16,7 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ENTITY_ID -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -115,16 +115,17 @@ async def _async_update_options(self, data): def _setup_callbacks(self): callbacks = { - "connected": self.got_connected, - "disconnected": self.got_disconnected, - "config_updated": self.new_config, - "activity_starting": self.new_activity, - "activity_started": self._new_activity_finished, + "connected": self.async_got_connected, + "disconnected": self.async_got_disconnected, + "config_updated": self.async_new_config, + "activity_starting": self.async_new_activity, + "activity_started": self.async_new_activity_finished, } self.async_on_remove(self._data.async_subscribe(HarmonyCallback(**callbacks))) - def _new_activity_finished(self, activity_info: tuple) -> None: + @callback + def async_new_activity_finished(self, activity_info: tuple) -> None: """Call for finished updated current activity.""" self._activity_starting = None self.async_write_ha_state() @@ -148,7 +149,7 @@ async def async_added_to_hass(self): # Store Harmony HUB config, this will also update our current # activity - await self.new_config() + await self.async_new_config() # Restore the last activity so we know # how what to turn on if nothing @@ -212,7 +213,8 @@ def available(self): """Return True if connected to Hub, otherwise False.""" return self._data.available - def new_activity(self, activity_info: tuple) -> None: + @callback + def async_new_activity(self, activity_info: tuple) -> None: """Call for updating the current activity.""" activity_id, activity_name = activity_info _LOGGER.debug("%s: activity reported as: %s", self._name, activity_name) @@ -229,10 +231,10 @@ def new_activity(self, activity_info: tuple) -> None: self._state = bool(activity_id != -1) self.async_write_ha_state() - async def new_config(self, _=None): + async def async_new_config(self, _=None): """Call for updating the current activity.""" _LOGGER.debug("%s: configuration has been updated", self._name) - self.new_activity(self._data.current_activity) + self.async_new_activity(self._data.current_activity) await self.hass.async_add_executor_job(self.write_config_file) async def async_turn_on(self, **kwargs): diff --git a/homeassistant/components/harmony/switch.py b/homeassistant/components/harmony/switch.py index 1da128b3d7b78..16b83c80478de 100644 --- a/homeassistant/components/harmony/switch.py +++ b/homeassistant/components/harmony/switch.py @@ -3,6 +3,7 @@ from homeassistant.components.switch import SwitchEntity from homeassistant.const import CONF_NAME +from homeassistant.core import callback from .connection_state import ConnectionStateMixin from .const import DOMAIN, HARMONY_DATA @@ -80,14 +81,15 @@ async def async_added_to_hass(self): """Call when entity is added to hass.""" callbacks = { - "connected": self.got_connected, - "disconnected": self.got_disconnected, - "activity_starting": self._activity_update, - "activity_started": self._activity_update, + "connected": self.async_got_connected, + "disconnected": self.async_got_disconnected, + "activity_starting": self._async_activity_update, + "activity_started": self._async_activity_update, "config_updated": None, } self.async_on_remove(self._data.async_subscribe(HarmonyCallback(**callbacks))) - def _activity_update(self, activity_info: tuple): + @callback + def _async_activity_update(self, activity_info: tuple): self.async_write_ha_state() From ff367cfcb65e79ce42d33455535f5573a49743b8 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 20 Apr 2021 15:47:40 +0200 Subject: [PATCH 0385/1317] Mqtt cover avoid warnings on empty payload (#49253) * No warnings on extra json values with templates * ignore empty received payload --- homeassistant/components/mqtt/cover.py | 12 +++ tests/components/mqtt/test_cover.py | 127 ++++++++++++++++++++++++- 2 files changed, 138 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 010f751dad47c..6de5050833fd1 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -267,6 +267,10 @@ def tilt_message_received(msg): payload ) + if not payload: + _LOGGER.debug("Ignoring empty tilt message from '%s'", msg.topic) + return + if not payload.isnumeric(): _LOGGER.warning("Payload '%s' is not numeric", payload) elif ( @@ -297,6 +301,10 @@ def state_message_received(msg): if value_template is not None: payload = value_template.async_render_with_possible_json_value(payload) + if not payload: + _LOGGER.debug("Ignoring empty state message from '%s'", msg.topic) + return + if payload == self._config[CONF_STATE_STOPPED]: if self._config.get(CONF_GET_POSITION_TOPIC) is not None: self._state = ( @@ -341,6 +349,10 @@ def position_message_received(msg): if template is not None: payload = template.async_render_with_possible_json_value(payload) + if not payload: + _LOGGER.debug("Ignoring empty position message from '%s'", msg.topic) + return + if payload.isnumeric(): percentage_payload = self.find_percentage_in_range( float(payload), COVER_PAYLOAD diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index e28cd697457a2..8d729ca9ddef4 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -260,6 +260,45 @@ async def test_state_via_template(hass, mqtt_mock): assert state.state == STATE_CLOSED +async def test_state_via_template_with_json_value(hass, mqtt_mock, caplog): + """Test the controlling state via topic with JSON value.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "qos": 0, + "value_template": "{{ value_json.Var1 }}", + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("cover.test") + assert state.state == STATE_UNKNOWN + + async_fire_mqtt_message(hass, "state-topic", '{ "Var1": "open", "Var2": "other" }') + + state = hass.states.get("cover.test") + assert state.state == STATE_OPEN + + async_fire_mqtt_message( + hass, "state-topic", '{ "Var1": "closed", "Var2": "other" }' + ) + + state = hass.states.get("cover.test") + assert state.state == STATE_CLOSED + + async_fire_mqtt_message(hass, "state-topic", '{ "Var2": "other" }') + assert ( + "Template variable warning: 'dict object' has no attribute 'Var1' when rendering" + ) in caplog.text + + async def test_position_via_template(hass, mqtt_mock): """Test the controlling state via topic.""" assert await async_setup_component( @@ -1269,6 +1308,52 @@ async def test_tilt_via_topic_template(hass, mqtt_mock): assert current_cover_tilt_position == 50 +async def test_tilt_via_topic_template_json_value(hass, mqtt_mock, caplog): + """Test tilt by updating status via MQTT and template with JSON value.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "qos": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + "tilt_command_topic": "tilt-command-topic", + "tilt_status_topic": "tilt-status-topic", + "tilt_status_template": "{{ value_json.Var1 }}", + "tilt_opened_value": 400, + "tilt_closed_value": 125, + } + }, + ) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, "tilt-status-topic", '{"Var1": 9, "Var2": 30}') + + current_cover_tilt_position = hass.states.get("cover.test").attributes[ + ATTR_CURRENT_TILT_POSITION + ] + assert current_cover_tilt_position == 9 + + async_fire_mqtt_message(hass, "tilt-status-topic", '{"Var1": 50, "Var2": 10}') + + current_cover_tilt_position = hass.states.get("cover.test").attributes[ + ATTR_CURRENT_TILT_POSITION + ] + assert current_cover_tilt_position == 50 + + async_fire_mqtt_message(hass, "tilt-status-topic", '{"Var2": 10}') + + assert ( + "Template variable warning: 'dict object' has no attribute 'Var1' when rendering" + ) in caplog.text + + async def test_tilt_via_topic_altered_range(hass, mqtt_mock): """Test tilt status via MQTT with altered tilt range.""" assert await async_setup_component( @@ -2018,7 +2103,7 @@ async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): ) -async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_json(hass, mqtt_mock, caplog): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( hass, mqtt_mock, caplog, cover.DOMAIN, DEFAULT_CONFIG @@ -2383,6 +2468,46 @@ async def test_position_via_position_topic_template(hass, mqtt_mock): assert current_cover_position_position == 50 +async def test_position_via_position_topic_template_json_value(hass, mqtt_mock, caplog): + """Test position by updating status via position template with a JSON value.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "set_position_topic": "set-position-topic", + "position_topic": "get-position-topic", + "position_template": "{{ value_json.Var1 }}", + } + }, + ) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, "get-position-topic", '{"Var1": 9, "Var2": 60}') + + current_cover_position_position = hass.states.get("cover.test").attributes[ + ATTR_CURRENT_POSITION + ] + assert current_cover_position_position == 9 + + async_fire_mqtt_message(hass, "get-position-topic", '{"Var1": 50, "Var2": 10}') + + current_cover_position_position = hass.states.get("cover.test").attributes[ + ATTR_CURRENT_POSITION + ] + assert current_cover_position_position == 50 + + async_fire_mqtt_message(hass, "get-position-topic", '{"Var2": 60}') + + assert ( + "Template variable warning: 'dict object' has no attribute 'Var1' when rendering" + ) in caplog.text + + async def test_set_state_via_stopped_state_no_position_topic(hass, mqtt_mock): """Test the controlling state via stopped state when no position topic.""" assert await async_setup_component( From fa05e5a8a0d909da378abe254626d354df8664e6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 20 Apr 2021 05:13:27 -1000 Subject: [PATCH 0386/1317] Fix memory leak in wemo on reload (#49457) --- homeassistant/components/wemo/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/wemo/__init__.py b/homeassistant/components/wemo/__init__.py index a013d1fdd3461..6ae016954f235 100644 --- a/homeassistant/components/wemo/__init__.py +++ b/homeassistant/components/wemo/__init__.py @@ -109,7 +109,9 @@ async def async_stop_wemo(event): await hass.async_add_executor_job(registry.stop) wemo_discovery.async_stop_discovery() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_wemo) + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_wemo) + ) static_conf = config.get(CONF_STATIC, []) if static_conf: From 34245c3add50d59b0fe3c74c201a7b997e89efdc Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 20 Apr 2021 17:34:11 +0200 Subject: [PATCH 0387/1317] Add alarm control panel support to deCONZ integration (#48736) * Infrastructure in place * Base implementation * Add alarm event * Add custom services to alarm control panel * Add service descriptions * Increase test coverage * Simplified to one entity service with an options selector * Remove everything but the essentials * Add library with proper support * Fix stale comments --- .../components/deconz/alarm_control_panel.py | 133 +++++++++++ homeassistant/components/deconz/const.py | 4 + .../components/deconz/deconz_event.py | 75 +++++- homeassistant/components/deconz/manifest.json | 2 +- homeassistant/components/deconz/sensor.py | 4 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../deconz/test_alarm_control_panel.py | 216 ++++++++++++++++++ tests/components/deconz/test_deconz_event.py | 122 +++++++++- tests/components/deconz/test_gateway.py | 23 +- 10 files changed, 561 insertions(+), 22 deletions(-) create mode 100644 homeassistant/components/deconz/alarm_control_panel.py create mode 100644 tests/components/deconz/test_alarm_control_panel.py diff --git a/homeassistant/components/deconz/alarm_control_panel.py b/homeassistant/components/deconz/alarm_control_panel.py new file mode 100644 index 0000000000000..4592a8014fc6b --- /dev/null +++ b/homeassistant/components/deconz/alarm_control_panel.py @@ -0,0 +1,133 @@ +"""Support for deCONZ alarm control panel devices.""" +from __future__ import annotations + +from pydeconz.sensor import ( + ANCILLARY_CONTROL_ARMED_AWAY, + ANCILLARY_CONTROL_ARMED_NIGHT, + ANCILLARY_CONTROL_ARMED_STAY, + ANCILLARY_CONTROL_DISARMED, + AncillaryControl, +) + +from homeassistant.components.alarm_control_panel import ( + DOMAIN, + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, + AlarmControlPanelEntity, +) +from homeassistant.const import ( + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, + STATE_UNKNOWN, +) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import NEW_SENSOR +from .deconz_device import DeconzDevice +from .gateway import get_gateway_from_config_entry + +DECONZ_TO_ALARM_STATE = { + ANCILLARY_CONTROL_ARMED_AWAY: STATE_ALARM_ARMED_AWAY, + ANCILLARY_CONTROL_ARMED_NIGHT: STATE_ALARM_ARMED_NIGHT, + ANCILLARY_CONTROL_ARMED_STAY: STATE_ALARM_ARMED_HOME, + ANCILLARY_CONTROL_DISARMED: STATE_ALARM_DISARMED, +} + + +async def async_setup_entry(hass, config_entry, async_add_entities) -> None: + """Set up the deCONZ alarm control panel devices. + + Alarm control panels are based on the same device class as sensors in deCONZ. + """ + gateway = get_gateway_from_config_entry(hass, config_entry) + gateway.entities[DOMAIN] = set() + + @callback + def async_add_alarm_control_panel(sensors=gateway.api.sensors.values()) -> None: + """Add alarm control panel devices from deCONZ.""" + entities = [] + + for sensor in sensors: + + if ( + sensor.type in AncillaryControl.ZHATYPE + and sensor.uniqueid not in gateway.entities[DOMAIN] + ): + entities.append(DeconzAlarmControlPanel(sensor, gateway)) + + if entities: + async_add_entities(entities) + + gateway.listeners.append( + async_dispatcher_connect( + hass, + gateway.async_signal_new_device(NEW_SENSOR), + async_add_alarm_control_panel, + ) + ) + + async_add_alarm_control_panel() + + +class DeconzAlarmControlPanel(DeconzDevice, AlarmControlPanelEntity): + """Representation of a deCONZ alarm control panel.""" + + TYPE = DOMAIN + + def __init__(self, device, gateway) -> None: + """Set up alarm control panel device.""" + super().__init__(device, gateway) + + self._features = SUPPORT_ALARM_ARM_AWAY + self._features |= SUPPORT_ALARM_ARM_HOME + self._features |= SUPPORT_ALARM_ARM_NIGHT + + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return self._features + + @property + def code_arm_required(self) -> bool: + """Code is not required for arm actions.""" + return False + + @property + def code_format(self) -> None: + """Code is not supported.""" + return None + + @callback + def async_update_callback(self, force_update: bool = False) -> None: + """Update the control panels state.""" + keys = {"armed", "reachable"} + if force_update or ( + self._device.changed_keys.intersection(keys) + and self._device.state in DECONZ_TO_ALARM_STATE + ): + super().async_update_callback(force_update=force_update) + + @property + def state(self) -> str: + """Return the state of the control panel.""" + return DECONZ_TO_ALARM_STATE.get(self._device.state, STATE_UNKNOWN) + + async def async_alarm_arm_away(self, code: None = None) -> None: + """Send arm away command.""" + await self._device.arm_away() + + async def async_alarm_arm_home(self, code: None = None) -> None: + """Send arm home command.""" + await self._device.arm_stay() + + async def async_alarm_arm_night(self, code: None = None) -> None: + """Send arm night command.""" + await self._device.arm_night() + + async def async_alarm_disarm(self, code: None = None) -> None: + """Send disarm command.""" + await self._device.disarm() diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index 5ed1def66c284..fb4e497587d61 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -1,6 +1,9 @@ """Constants for the deCONZ component.""" import logging +from homeassistant.components.alarm_control_panel import ( + DOMAIN as ALARM_CONTROL_PANEL_DOMAIN, +) from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN from homeassistant.components.cover import DOMAIN as COVER_DOMAIN @@ -29,6 +32,7 @@ CONF_MASTER_GATEWAY = "master" PLATFORMS = [ + ALARM_CONTROL_PANEL_DOMAIN, BINARY_SENSOR_DOMAIN, CLIMATE_DOMAIN, COVER_DOMAIN, diff --git a/homeassistant/components/deconz/deconz_event.py b/homeassistant/components/deconz/deconz_event.py index 706850477d8a1..da80e2e6bf2e2 100644 --- a/homeassistant/components/deconz/deconz_event.py +++ b/homeassistant/components/deconz/deconz_event.py @@ -1,7 +1,26 @@ -"""Representation of a deCONZ remote.""" -from pydeconz.sensor import Switch - -from homeassistant.const import CONF_DEVICE_ID, CONF_EVENT, CONF_ID, CONF_UNIQUE_ID +"""Representation of a deCONZ remote or keypad.""" + +from pydeconz.sensor import ( + ANCILLARY_CONTROL_ARMED_AWAY, + ANCILLARY_CONTROL_ARMED_NIGHT, + ANCILLARY_CONTROL_ARMED_STAY, + ANCILLARY_CONTROL_DISARMED, + AncillaryControl, + Switch, +) + +from homeassistant.const import ( + CONF_CODE, + CONF_DEVICE_ID, + CONF_EVENT, + CONF_ID, + CONF_UNIQUE_ID, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, + STATE_UNKNOWN, +) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util import slugify @@ -10,6 +29,14 @@ from .deconz_device import DeconzBase CONF_DECONZ_EVENT = "deconz_event" +CONF_DECONZ_ALARM_EVENT = "deconz_alarm_event" + +DECONZ_TO_ALARM_STATE = { + ANCILLARY_CONTROL_ARMED_AWAY: STATE_ALARM_ARMED_AWAY, + ANCILLARY_CONTROL_ARMED_NIGHT: STATE_ALARM_ARMED_NIGHT, + ANCILLARY_CONTROL_ARMED_STAY: STATE_ALARM_ARMED_HOME, + ANCILLARY_CONTROL_DISARMED: STATE_ALARM_DISARMED, +} async def async_setup_events(gateway) -> None: @@ -23,12 +50,18 @@ def async_add_sensor(sensors=gateway.api.sensors.values()): if not gateway.option_allow_clip_sensor and sensor.type.startswith("CLIP"): continue - if sensor.type not in Switch.ZHATYPE or sensor.uniqueid in { - event.unique_id for event in gateway.events - }: + if ( + sensor.type not in Switch.ZHATYPE + AncillaryControl.ZHATYPE + or sensor.uniqueid in {event.unique_id for event in gateway.events} + ): continue - new_event = DeconzEvent(sensor, gateway) + if sensor.type in Switch.ZHATYPE: + new_event = DeconzEvent(sensor, gateway) + + elif sensor.type in AncillaryControl.ZHATYPE: + new_event = DeconzAlarmEvent(sensor, gateway) + gateway.hass.async_create_task(new_event.async_update_device_registry()) gateway.events.append(new_event) @@ -119,3 +152,29 @@ async def async_update_device_registry(self) -> None: config_entry_id=self.gateway.config_entry.entry_id, **self.device_info ) self.device_id = entry.id + + +class DeconzAlarmEvent(DeconzEvent): + """Alarm control panel companion event when user inputs a code.""" + + @callback + def async_update_callback(self, force_update=False): + """Fire the event if reason is that state is updated.""" + if ( + self.gateway.ignore_state_updates + or "action" not in self._device.changed_keys + or self._device.action == "" + ): + return + + state, code, _area = self._device.action.split(",") + + data = { + CONF_ID: self.event_id, + CONF_UNIQUE_ID: self.serial, + CONF_DEVICE_ID: self.device_id, + CONF_EVENT: DECONZ_TO_ALARM_STATE.get(state, STATE_UNKNOWN), + CONF_CODE: code, + } + + self.gateway.hass.bus.async_fire(CONF_DECONZ_ALARM_EVENT, data) diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index 97dbc9a4854d1..c4dfd0d4dfce4 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -3,7 +3,7 @@ "name": "deCONZ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", - "requirements": ["pydeconz==78"], + "requirements": ["pydeconz==79"], "ssdp": [ { "manufacturer": "Royal Philips Electronics" diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index a38b7cb20aa3a..311dd9be82cfa 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -1,5 +1,6 @@ """Support for deCONZ sensors.""" from pydeconz.sensor import ( + AncillaryControl, Battery, Consumption, Daylight, @@ -104,7 +105,8 @@ def async_add_sensor(sensors=gateway.api.sensors.values()): if ( not sensor.BINARY and sensor.type - not in Battery.ZHATYPE + not in AncillaryControl.ZHATYPE + + Battery.ZHATYPE + DoorLock.ZHATYPE + Switch.ZHATYPE + Thermostat.ZHATYPE diff --git a/requirements_all.txt b/requirements_all.txt index cae71d7529518..7ceb300cc23a5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1343,7 +1343,7 @@ pydaikin==2.4.1 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==78 +pydeconz==79 # homeassistant.components.delijn pydelijn==0.6.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e2841afab7659..64da38880f8d2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -726,7 +726,7 @@ pycoolmasternet-async==0.1.2 pydaikin==2.4.1 # homeassistant.components.deconz -pydeconz==78 +pydeconz==79 # homeassistant.components.dexcom pydexcom==0.2.0 diff --git a/tests/components/deconz/test_alarm_control_panel.py b/tests/components/deconz/test_alarm_control_panel.py new file mode 100644 index 0000000000000..b0425d5701aa3 --- /dev/null +++ b/tests/components/deconz/test_alarm_control_panel.py @@ -0,0 +1,216 @@ +"""deCONZ alarm control panel platform tests.""" + +from unittest.mock import patch + +from pydeconz.sensor import ( + ANCILLARY_CONTROL_ARMED_AWAY, + ANCILLARY_CONTROL_ARMED_NIGHT, + ANCILLARY_CONTROL_ARMED_STAY, + ANCILLARY_CONTROL_DISARMED, +) + +from homeassistant.components.alarm_control_panel import ( + DOMAIN as ALARM_CONTROL_PANEL_DOMAIN, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_ALARM_ARM_AWAY, + SERVICE_ALARM_ARM_HOME, + SERVICE_ALARM_ARM_NIGHT, + SERVICE_ALARM_DISARM, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, + STATE_UNAVAILABLE, +) + +from .test_gateway import ( + DECONZ_WEB_REQUEST, + mock_deconz_put_request, + setup_deconz_integration, +) + + +async def test_no_sensors(hass, aioclient_mock): + """Test that no sensors in deconz results in no climate entities.""" + await setup_deconz_integration(hass, aioclient_mock) + assert len(hass.states.async_all()) == 0 + + +async def test_alarm_control_panel(hass, aioclient_mock, mock_deconz_websocket): + """Test successful creation of alarm control panel entities.""" + data = { + "sensors": { + "0": { + "config": { + "armed": "disarmed", + "enrolled": 0, + "on": True, + "panel": "disarmed", + "pending": [], + "reachable": True, + }, + "ep": 1, + "etag": "3c4008d74035dfaa1f0bb30d24468b12", + "lastseen": "2021-04-02T13:07Z", + "manufacturername": "Universal Electronics Inc", + "modelid": "URC4450BC0-X-R", + "name": "Keypad", + "state": { + "action": "armed_away,1111,55", + "lastupdated": "2021-04-02T13:08:18.937", + "lowbattery": False, + "tampered": True, + }, + "type": "ZHAAncillaryControl", + "uniqueid": "00:0d:6f:00:13:4f:61:39-01-0501", + } + } + } + with patch.dict(DECONZ_WEB_REQUEST, data): + config_entry = await setup_deconz_integration(hass, aioclient_mock) + + assert len(hass.states.async_all()) == 1 + assert hass.states.get("alarm_control_panel.keypad").state == STATE_ALARM_DISARMED + + # Event signals alarm control panel armed away + + event_changed_sensor = { + "t": "event", + "e": "changed", + "r": "sensors", + "id": "0", + "config": {"armed": ANCILLARY_CONTROL_ARMED_AWAY}, + } + await mock_deconz_websocket(data=event_changed_sensor) + await hass.async_block_till_done() + + assert hass.states.get("alarm_control_panel.keypad").state == STATE_ALARM_ARMED_AWAY + + # Event signals alarm control panel armed night + + event_changed_sensor = { + "t": "event", + "e": "changed", + "r": "sensors", + "id": "0", + "config": {"armed": ANCILLARY_CONTROL_ARMED_NIGHT}, + } + await mock_deconz_websocket(data=event_changed_sensor) + await hass.async_block_till_done() + + assert ( + hass.states.get("alarm_control_panel.keypad").state == STATE_ALARM_ARMED_NIGHT + ) + + # Event signals alarm control panel armed home + + event_changed_sensor = { + "t": "event", + "e": "changed", + "r": "sensors", + "id": "0", + "config": {"armed": ANCILLARY_CONTROL_ARMED_STAY}, + } + await mock_deconz_websocket(data=event_changed_sensor) + await hass.async_block_till_done() + + assert hass.states.get("alarm_control_panel.keypad").state == STATE_ALARM_ARMED_HOME + + # Event signals alarm control panel armed night + + event_changed_sensor = { + "t": "event", + "e": "changed", + "r": "sensors", + "id": "0", + "config": {"armed": ANCILLARY_CONTROL_ARMED_NIGHT}, + } + await mock_deconz_websocket(data=event_changed_sensor) + await hass.async_block_till_done() + + assert ( + hass.states.get("alarm_control_panel.keypad").state == STATE_ALARM_ARMED_NIGHT + ) + + # Event signals alarm control panel disarmed + + event_changed_sensor = { + "t": "event", + "e": "changed", + "r": "sensors", + "id": "0", + "config": {"armed": ANCILLARY_CONTROL_DISARMED}, + } + await mock_deconz_websocket(data=event_changed_sensor) + await hass.async_block_till_done() + + assert hass.states.get("alarm_control_panel.keypad").state == STATE_ALARM_DISARMED + + # Verify service calls + + mock_deconz_put_request(aioclient_mock, config_entry.data, "/sensors/0/config") + + # Service set alarm to away mode + + await hass.services.async_call( + ALARM_CONTROL_PANEL_DOMAIN, + SERVICE_ALARM_ARM_AWAY, + {ATTR_ENTITY_ID: "alarm_control_panel.keypad"}, + blocking=True, + ) + assert aioclient_mock.mock_calls[1][2] == { + "armed": ANCILLARY_CONTROL_ARMED_AWAY, + "panel": ANCILLARY_CONTROL_ARMED_AWAY, + } + + # Service set alarm to home mode + + await hass.services.async_call( + ALARM_CONTROL_PANEL_DOMAIN, + SERVICE_ALARM_ARM_HOME, + {ATTR_ENTITY_ID: "alarm_control_panel.keypad"}, + blocking=True, + ) + assert aioclient_mock.mock_calls[2][2] == { + "armed": ANCILLARY_CONTROL_ARMED_STAY, + "panel": ANCILLARY_CONTROL_ARMED_STAY, + } + + # Service set alarm to night mode + + await hass.services.async_call( + ALARM_CONTROL_PANEL_DOMAIN, + SERVICE_ALARM_ARM_NIGHT, + {ATTR_ENTITY_ID: "alarm_control_panel.keypad"}, + blocking=True, + ) + assert aioclient_mock.mock_calls[3][2] == { + "armed": ANCILLARY_CONTROL_ARMED_NIGHT, + "panel": ANCILLARY_CONTROL_ARMED_NIGHT, + } + + # Service set alarm to disarmed + + await hass.services.async_call( + ALARM_CONTROL_PANEL_DOMAIN, + SERVICE_ALARM_DISARM, + {ATTR_ENTITY_ID: "alarm_control_panel.keypad"}, + blocking=True, + ) + assert aioclient_mock.mock_calls[4][2] == { + "armed": ANCILLARY_CONTROL_DISARMED, + "panel": ANCILLARY_CONTROL_DISARMED, + } + + await hass.config_entries.async_unload(config_entry.entry_id) + + states = hass.states.async_all() + assert len(states) == 1 + for state in states: + assert state.state == STATE_UNAVAILABLE + + await hass.config_entries.async_remove(config_entry.entry_id) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 0 diff --git a/tests/components/deconz/test_deconz_event.py b/tests/components/deconz/test_deconz_event.py index fc7544f39185e..ed488e0545940 100644 --- a/tests/components/deconz/test_deconz_event.py +++ b/tests/components/deconz/test_deconz_event.py @@ -3,8 +3,18 @@ from unittest.mock import patch from homeassistant.components.deconz.const import DOMAIN as DECONZ_DOMAIN -from homeassistant.components.deconz.deconz_event import CONF_DECONZ_EVENT -from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.components.deconz.deconz_event import ( + CONF_DECONZ_ALARM_EVENT, + CONF_DECONZ_EVENT, +) +from homeassistant.const import ( + CONF_CODE, + CONF_DEVICE_ID, + CONF_EVENT, + CONF_ID, + CONF_UNIQUE_ID, + STATE_UNAVAILABLE, +) from homeassistant.helpers.device_registry import async_entries_for_config_entry from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration @@ -161,6 +171,20 @@ async def test_deconz_events(hass, aioclient_mock, mock_deconz_websocket): "device_id": device.id, } + # Unsupported event + + event_changed_sensor = { + "t": "event", + "e": "changed", + "r": "sensors", + "id": "1", + "name": "other name", + } + await mock_deconz_websocket(data=event_changed_sensor) + await hass.async_block_till_done() + + assert len(captured_events) == 4 + await hass.config_entries.async_unload(config_entry.entry_id) states = hass.states.async_all() @@ -173,6 +197,100 @@ async def test_deconz_events(hass, aioclient_mock, mock_deconz_websocket): assert len(hass.states.async_all()) == 0 +async def test_deconz_alarm_events(hass, aioclient_mock, mock_deconz_websocket): + """Test successful creation of deconz alarm events.""" + data = { + "sensors": { + "1": { + "config": { + "armed": "disarmed", + "enrolled": 0, + "on": True, + "panel": "disarmed", + "pending": [], + "reachable": True, + }, + "ep": 1, + "etag": "3c4008d74035dfaa1f0bb30d24468b12", + "lastseen": "2021-04-02T13:07Z", + "manufacturername": "Universal Electronics Inc", + "modelid": "URC4450BC0-X-R", + "name": "Keypad", + "state": { + "action": "armed_away,1111,55", + "lastupdated": "2021-04-02T13:08:18.937", + "lowbattery": False, + "tampered": True, + }, + "type": "ZHAAncillaryControl", + "uniqueid": "00:00:00:00:00:00:00:01-01-0501", + } + } + } + with patch.dict(DECONZ_WEB_REQUEST, data): + config_entry = await setup_deconz_integration(hass, aioclient_mock) + + device_registry = await hass.helpers.device_registry.async_get_registry() + + assert len(hass.states.async_all()) == 1 + # 1 alarm control device + 2 additional devices for deconz service and host + assert ( + len(async_entries_for_config_entry(device_registry, config_entry.entry_id)) == 3 + ) + + captured_events = async_capture_events(hass, CONF_DECONZ_ALARM_EVENT) + + # Armed away event + + event_changed_sensor = { + "t": "event", + "e": "changed", + "r": "sensors", + "id": "1", + "state": {"action": "armed_away,1234,1"}, + } + await mock_deconz_websocket(data=event_changed_sensor) + await hass.async_block_till_done() + + device = device_registry.async_get_device( + identifiers={(DECONZ_DOMAIN, "00:00:00:00:00:00:00:01")} + ) + + assert len(captured_events) == 1 + assert captured_events[0].data == { + CONF_ID: "keypad", + CONF_UNIQUE_ID: "00:00:00:00:00:00:00:01", + CONF_DEVICE_ID: device.id, + CONF_EVENT: "armed_away", + CONF_CODE: "1234", + } + + # Unsupported event + + event_changed_sensor = { + "t": "event", + "e": "changed", + "r": "sensors", + "id": "1", + "config": {"panel": "armed_away"}, + } + await mock_deconz_websocket(data=event_changed_sensor) + await hass.async_block_till_done() + + assert len(captured_events) == 1 + + await hass.config_entries.async_unload(config_entry.entry_id) + + states = hass.states.async_all() + assert len(hass.states.async_all()) == 1 + for state in states: + assert state.state == STATE_UNAVAILABLE + + await hass.config_entries.async_remove(config_entry.entry_id) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 0 + + async def test_deconz_events_bad_unique_id(hass, aioclient_mock, mock_deconz_websocket): """Verify no devices are created if unique id is bad or missing.""" data = { diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_gateway.py index 603644f47e355..afd4e55499db5 100644 --- a/tests/components/deconz/test_gateway.py +++ b/tests/components/deconz/test_gateway.py @@ -7,6 +7,9 @@ from pydeconz.websocket import STATE_RETRYING, STATE_RUNNING import pytest +from homeassistant.components.alarm_control_panel import ( + DOMAIN as ALARM_CONTROL_PANEL_DOMAIN, +) from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN from homeassistant.components.cover import DOMAIN as COVER_DOMAIN @@ -147,17 +150,21 @@ async def test_gateway_setup(hass, aioclient_mock): assert len(hass.states.async_all()) == 0 assert forward_entry_setup.mock_calls[0][1] == ( + config_entry, + ALARM_CONTROL_PANEL_DOMAIN, + ) + assert forward_entry_setup.mock_calls[1][1] == ( config_entry, BINARY_SENSOR_DOMAIN, ) - assert forward_entry_setup.mock_calls[1][1] == (config_entry, CLIMATE_DOMAIN) - assert forward_entry_setup.mock_calls[2][1] == (config_entry, COVER_DOMAIN) - assert forward_entry_setup.mock_calls[3][1] == (config_entry, FAN_DOMAIN) - assert forward_entry_setup.mock_calls[4][1] == (config_entry, LIGHT_DOMAIN) - assert forward_entry_setup.mock_calls[5][1] == (config_entry, LOCK_DOMAIN) - assert forward_entry_setup.mock_calls[6][1] == (config_entry, SCENE_DOMAIN) - assert forward_entry_setup.mock_calls[7][1] == (config_entry, SENSOR_DOMAIN) - assert forward_entry_setup.mock_calls[8][1] == (config_entry, SWITCH_DOMAIN) + assert forward_entry_setup.mock_calls[2][1] == (config_entry, CLIMATE_DOMAIN) + assert forward_entry_setup.mock_calls[3][1] == (config_entry, COVER_DOMAIN) + assert forward_entry_setup.mock_calls[4][1] == (config_entry, FAN_DOMAIN) + assert forward_entry_setup.mock_calls[5][1] == (config_entry, LIGHT_DOMAIN) + assert forward_entry_setup.mock_calls[6][1] == (config_entry, LOCK_DOMAIN) + assert forward_entry_setup.mock_calls[7][1] == (config_entry, SCENE_DOMAIN) + assert forward_entry_setup.mock_calls[8][1] == (config_entry, SENSOR_DOMAIN) + assert forward_entry_setup.mock_calls[9][1] == (config_entry, SWITCH_DOMAIN) async def test_gateway_retry(hass): From c07646db5dcfe290d8b1e11c696629cbd14c1f8c Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 20 Apr 2021 17:40:41 +0200 Subject: [PATCH 0388/1317] Update typing syntax (#49480) * Update typing syntax * Replace typing imports with ones from collections where possible * Changes after review --- .../alarm_control_panel/reproduce_state.py | 3 ++- homeassistant/components/alert/reproduce_state.py | 3 ++- .../components/automation/reproduce_state.py | 3 ++- homeassistant/components/blueprint/errors.py | 5 ++++- homeassistant/components/climacell/sensor.py | 3 ++- homeassistant/components/climacell/weather.py | 3 ++- .../components/climate/reproduce_state.py | 3 ++- .../components/counter/reproduce_state.py | 3 ++- homeassistant/components/cover/reproduce_state.py | 3 ++- homeassistant/components/denonavr/media_player.py | 3 ++- .../components/device_automation/__init__.py | 3 ++- homeassistant/components/device_tracker/legacy.py | 3 ++- homeassistant/components/directv/remote.py | 3 ++- homeassistant/components/fan/reproduce_state.py | 3 ++- homeassistant/components/gogogate2/common.py | 3 ++- homeassistant/components/group/__init__.py | 3 ++- homeassistant/components/group/light.py | 3 ++- homeassistant/components/group/reproduce_state.py | 3 ++- homeassistant/components/guardian/util.py | 5 ++++- homeassistant/components/harmony/data.py | 3 ++- homeassistant/components/heos/media_player.py | 4 +++- homeassistant/components/history/__init__.py | 3 ++- homeassistant/components/http/data_validator.py | 5 ++++- homeassistant/components/huawei_lte/sensor.py | 6 +++--- .../components/humidifier/reproduce_state.py | 3 ++- homeassistant/components/hyperion/light.py | 3 ++- .../components/input_boolean/reproduce_state.py | 3 ++- .../components/input_datetime/reproduce_state.py | 3 ++- .../components/input_number/reproduce_state.py | 3 ++- .../components/input_select/reproduce_state.py | 3 ++- .../components/input_text/reproduce_state.py | 3 ++- homeassistant/components/knx/binary_sensor.py | 3 ++- homeassistant/components/knx/climate.py | 3 ++- homeassistant/components/knx/cover.py | 3 ++- homeassistant/components/knx/fan.py | 3 ++- homeassistant/components/knx/light.py | 3 ++- homeassistant/components/knx/scene.py | 3 ++- homeassistant/components/knx/sensor.py | 3 ++- homeassistant/components/knx/switch.py | 3 ++- homeassistant/components/knx/weather.py | 3 ++- homeassistant/components/light/reproduce_state.py | 3 ++- homeassistant/components/lock/reproduce_state.py | 3 ++- .../components/media_player/reproduce_state.py | 3 ++- homeassistant/components/minio/minio_helper.py | 3 +-- homeassistant/components/mysensors/gateway.py | 3 ++- homeassistant/components/mysensors/helpers.py | 6 +++--- homeassistant/components/netatmo/data_handler.py | 3 +-- .../components/number/reproduce_state.py | 3 ++- homeassistant/components/nws/__init__.py | 3 ++- .../persistent_notification/__init__.py | 3 ++- homeassistant/components/rainmachine/switch.py | 5 ++++- homeassistant/components/remote/__init__.py | 3 ++- .../components/remote/reproduce_state.py | 3 ++- homeassistant/components/sharkiq/vacuum.py | 2 +- homeassistant/components/sma/__init__.py | 7 ++++--- homeassistant/components/sma/sensor.py | 3 ++- homeassistant/components/smartthings/__init__.py | 4 +++- .../components/smartthings/binary_sensor.py | 2 +- homeassistant/components/smartthings/climate.py | 3 +-- homeassistant/components/smartthings/cover.py | 2 +- homeassistant/components/smartthings/fan.py | 2 +- homeassistant/components/smartthings/light.py | 2 +- homeassistant/components/smartthings/lock.py | 2 +- homeassistant/components/smartthings/sensor.py | 2 +- homeassistant/components/smartthings/switch.py | 2 +- homeassistant/components/solaredge/sensor.py | 3 ++- homeassistant/components/sonos/media_player.py | 3 ++- homeassistant/components/ssdp/__init__.py | 5 ++++- homeassistant/components/stream/recorder.py | 4 ++-- homeassistant/components/switch/light.py | 3 ++- .../components/switch/reproduce_state.py | 3 ++- .../components/system_health/__init__.py | 3 ++- homeassistant/components/timer/reproduce_state.py | 3 ++- homeassistant/components/trace/__init__.py | 7 ++++--- homeassistant/components/upnp/config_flow.py | 3 ++- homeassistant/components/upnp/device.py | 2 +- .../components/vacuum/reproduce_state.py | 3 ++- homeassistant/components/vera/common.py | 5 +++-- .../components/verisure/alarm_control_panel.py | 3 ++- .../components/verisure/binary_sensor.py | 3 ++- homeassistant/components/verisure/camera.py | 3 ++- homeassistant/components/verisure/lock.py | 3 ++- homeassistant/components/verisure/sensor.py | 3 ++- homeassistant/components/verisure/switch.py | 3 ++- .../components/water_heater/reproduce_state.py | 3 ++- .../components/websocket_api/connection.py | 3 ++- .../components/websocket_api/decorators.py | 5 ++++- homeassistant/components/wemo/entity.py | 3 ++- homeassistant/components/xbox/remote.py | 5 ++++- .../components/zha/core/channels/general.py | 3 ++- .../zha/core/channels/homeautomation.py | 2 +- .../components/zha/core/channels/lighting.py | 2 +- .../components/zha/core/channels/security.py | 4 +++- .../components/zha/core/channels/smartenergy.py | 2 +- homeassistant/components/zha/core/helpers.py | 3 ++- homeassistant/components/zha/core/store.py | 3 ++- homeassistant/components/zha/entity.py | 3 ++- homeassistant/components/zwave_js/discovery.py | 3 ++- homeassistant/config.py | 3 ++- homeassistant/core.py | 15 ++------------- homeassistant/exceptions.py | 3 ++- homeassistant/helpers/__init__.py | 3 ++- homeassistant/helpers/aiohttp_client.py | 3 ++- homeassistant/helpers/area_registry.py | 3 ++- homeassistant/helpers/collection.py | 3 ++- homeassistant/helpers/condition.py | 3 ++- homeassistant/helpers/config_entry_oauth2_flow.py | 3 ++- homeassistant/helpers/config_validation.py | 5 +++-- homeassistant/helpers/debounce.py | 3 ++- homeassistant/helpers/entity.py | 4 ++-- homeassistant/helpers/entity_component.py | 3 ++- homeassistant/helpers/entity_platform.py | 3 ++- homeassistant/helpers/entity_registry.py | 3 ++- homeassistant/helpers/entity_values.py | 4 ++-- homeassistant/helpers/entityfilter.py | 6 +++--- homeassistant/helpers/event.py | 3 ++- homeassistant/helpers/integration_platform.py | 5 ++++- homeassistant/helpers/intent.py | 3 ++- homeassistant/helpers/location.py | 2 +- homeassistant/helpers/logging.py | 3 ++- homeassistant/helpers/ratelimit.py | 3 ++- homeassistant/helpers/reload.py | 2 +- homeassistant/helpers/script.py | 3 ++- homeassistant/helpers/script_variables.py | 3 ++- homeassistant/helpers/service.py | 3 ++- homeassistant/helpers/state.py | 3 ++- homeassistant/helpers/template.py | 3 ++- homeassistant/helpers/trace.py | 7 ++++--- homeassistant/helpers/update_coordinator.py | 3 ++- homeassistant/requirements.py | 3 ++- homeassistant/scripts/__init__.py | 2 +- homeassistant/setup.py | 3 ++- homeassistant/util/__init__.py | 3 ++- homeassistant/util/async_.py | 3 ++- homeassistant/util/logging.py | 3 ++- homeassistant/util/yaml/loader.py | 3 ++- 136 files changed, 284 insertions(+), 168 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/reproduce_state.py b/homeassistant/components/alarm_control_panel/reproduce_state.py index 90979d97dd04a..e7e4c07b8ade2 100644 --- a/homeassistant/components/alarm_control_panel/reproduce_state.py +++ b/homeassistant/components/alarm_control_panel/reproduce_state.py @@ -2,8 +2,9 @@ from __future__ import annotations import asyncio +from collections.abc import Iterable import logging -from typing import Any, Iterable +from typing import Any from homeassistant.const import ( ATTR_ENTITY_ID, diff --git a/homeassistant/components/alert/reproduce_state.py b/homeassistant/components/alert/reproduce_state.py index dfe51df753181..9c8cbd19810f8 100644 --- a/homeassistant/components/alert/reproduce_state.py +++ b/homeassistant/components/alert/reproduce_state.py @@ -2,8 +2,9 @@ from __future__ import annotations import asyncio +from collections.abc import Iterable import logging -from typing import Any, Iterable +from typing import Any from homeassistant.const import ( ATTR_ENTITY_ID, diff --git a/homeassistant/components/automation/reproduce_state.py b/homeassistant/components/automation/reproduce_state.py index ff716f3a83bf9..dd2ba824f8aaf 100644 --- a/homeassistant/components/automation/reproduce_state.py +++ b/homeassistant/components/automation/reproduce_state.py @@ -2,8 +2,9 @@ from __future__ import annotations import asyncio +from collections.abc import Iterable import logging -from typing import Any, Iterable +from typing import Any from homeassistant.const import ( ATTR_ENTITY_ID, diff --git a/homeassistant/components/blueprint/errors.py b/homeassistant/components/blueprint/errors.py index b422b2dcbe3a8..b5032af932697 100644 --- a/homeassistant/components/blueprint/errors.py +++ b/homeassistant/components/blueprint/errors.py @@ -1,5 +1,8 @@ """Blueprint errors.""" -from typing import Any, Iterable +from __future__ import annotations + +from collections.abc import Iterable +from typing import Any import voluptuous as vol from voluptuous.humanize import humanize_error diff --git a/homeassistant/components/climacell/sensor.py b/homeassistant/components/climacell/sensor.py index 3d3006638f91d..50e051813c44b 100644 --- a/homeassistant/components/climacell/sensor.py +++ b/homeassistant/components/climacell/sensor.py @@ -2,8 +2,9 @@ from __future__ import annotations from abc import abstractmethod +from collections.abc import Mapping import logging -from typing import Any, Callable, Mapping +from typing import Any, Callable from pyclimacell.const import CURRENT diff --git a/homeassistant/components/climacell/weather.py b/homeassistant/components/climacell/weather.py index 7183a3ebcf6d9..9c80a547f0670 100644 --- a/homeassistant/components/climacell/weather.py +++ b/homeassistant/components/climacell/weather.py @@ -2,9 +2,10 @@ from __future__ import annotations from abc import abstractmethod +from collections.abc import Mapping from datetime import datetime import logging -from typing import Any, Callable, Mapping +from typing import Any, Callable from pyclimacell.const import ( CURRENT, diff --git a/homeassistant/components/climate/reproduce_state.py b/homeassistant/components/climate/reproduce_state.py index be52138e3e56a..767a38b2e5744 100644 --- a/homeassistant/components/climate/reproduce_state.py +++ b/homeassistant/components/climate/reproduce_state.py @@ -2,7 +2,8 @@ from __future__ import annotations import asyncio -from typing import Any, Iterable +from collections.abc import Iterable +from typing import Any from homeassistant.const import ATTR_TEMPERATURE from homeassistant.core import Context, HomeAssistant, State diff --git a/homeassistant/components/counter/reproduce_state.py b/homeassistant/components/counter/reproduce_state.py index 8fb15bd84e854..0ced9bad06d6a 100644 --- a/homeassistant/components/counter/reproduce_state.py +++ b/homeassistant/components/counter/reproduce_state.py @@ -2,8 +2,9 @@ from __future__ import annotations import asyncio +from collections.abc import Iterable import logging -from typing import Any, Iterable +from typing import Any from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import Context, HomeAssistant, State diff --git a/homeassistant/components/cover/reproduce_state.py b/homeassistant/components/cover/reproduce_state.py index 3b82596a21c25..c96b9ec5acc67 100644 --- a/homeassistant/components/cover/reproduce_state.py +++ b/homeassistant/components/cover/reproduce_state.py @@ -2,8 +2,9 @@ from __future__ import annotations import asyncio +from collections.abc import Iterable import logging -from typing import Any, Iterable +from typing import Any from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index 799f07ed71b6a..d7e0f8510dd51 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -1,9 +1,10 @@ """Support for Denon AVR receivers using their HTTP interface.""" +from __future__ import annotations +from collections.abc import Coroutine from datetime import timedelta from functools import wraps import logging -from typing import Coroutine from denonavr import DenonAVR from denonavr.const import POWER_ON diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index 4741dbdb7f557..12083a8d139ce 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -2,9 +2,10 @@ from __future__ import annotations import asyncio +from collections.abc import MutableMapping from functools import wraps from types import ModuleType -from typing import Any, MutableMapping +from typing import Any import voluptuous as vol import voluptuous_serialize diff --git a/homeassistant/components/device_tracker/legacy.py b/homeassistant/components/device_tracker/legacy.py index 2614bd4228a0f..e1eb897f1ba58 100644 --- a/homeassistant/components/device_tracker/legacy.py +++ b/homeassistant/components/device_tracker/legacy.py @@ -2,10 +2,11 @@ from __future__ import annotations import asyncio +from collections.abc import Sequence from datetime import timedelta import hashlib from types import ModuleType -from typing import Any, Callable, Sequence, final +from typing import Any, Callable, final import attr import voluptuous as vol diff --git a/homeassistant/components/directv/remote.py b/homeassistant/components/directv/remote.py index d1a4d236ebb38..dc28e287f54b9 100644 --- a/homeassistant/components/directv/remote.py +++ b/homeassistant/components/directv/remote.py @@ -1,9 +1,10 @@ """Support for the DIRECTV remote.""" from __future__ import annotations +from collections.abc import Iterable from datetime import timedelta import logging -from typing import Any, Callable, Iterable +from typing import Any, Callable from directv import DIRECTV, DIRECTVError diff --git a/homeassistant/components/fan/reproduce_state.py b/homeassistant/components/fan/reproduce_state.py index c9da43ebe3aac..2d4244ec2dce4 100644 --- a/homeassistant/components/fan/reproduce_state.py +++ b/homeassistant/components/fan/reproduce_state.py @@ -2,9 +2,10 @@ from __future__ import annotations import asyncio +from collections.abc import Iterable import logging from types import MappingProxyType -from typing import Any, Iterable +from typing import Any from homeassistant.const import ( ATTR_ENTITY_ID, diff --git a/homeassistant/components/gogogate2/common.py b/homeassistant/components/gogogate2/common.py index e8b17184bbe55..8a51b210c5b34 100644 --- a/homeassistant/components/gogogate2/common.py +++ b/homeassistant/components/gogogate2/common.py @@ -1,9 +1,10 @@ """Common code for GogoGate2 component.""" from __future__ import annotations +from collections.abc import Awaitable from datetime import timedelta import logging -from typing import Awaitable, Callable, NamedTuple +from typing import Callable, NamedTuple from gogogate2_api import AbstractGateApi, GogoGate2Api, ISmartGateApi from gogogate2_api.common import AbstractDoor, get_door_by_id diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 5af53768bc0eb..096108b460e6b 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -3,9 +3,10 @@ from abc import abstractmethod import asyncio +from collections.abc import Iterable from contextvars import ContextVar import logging -from typing import Any, Iterable, List, cast +from typing import Any, List, cast import voluptuous as vol diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index b45dd1ec5e3f4..26cc8e1c11c82 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -3,8 +3,9 @@ import asyncio from collections import Counter +from collections.abc import Iterator import itertools -from typing import Any, Callable, Iterator, cast +from typing import Any, Callable, cast import voluptuous as vol diff --git a/homeassistant/components/group/reproduce_state.py b/homeassistant/components/group/reproduce_state.py index ea21c147b9b7f..c99f098b22259 100644 --- a/homeassistant/components/group/reproduce_state.py +++ b/homeassistant/components/group/reproduce_state.py @@ -1,7 +1,8 @@ """Module that groups code required to handle state restore for component.""" from __future__ import annotations -from typing import Any, Iterable +from collections.abc import Iterable +from typing import Any from homeassistant.core import Context, HomeAssistant, State from homeassistant.helpers.state import async_reproduce_state diff --git a/homeassistant/components/guardian/util.py b/homeassistant/components/guardian/util.py index ad2a074564c25..884bbcde7c192 100644 --- a/homeassistant/components/guardian/util.py +++ b/homeassistant/components/guardian/util.py @@ -1,7 +1,10 @@ """Define Guardian-specific utilities.""" +from __future__ import annotations + import asyncio +from collections.abc import Awaitable from datetime import timedelta -from typing import Awaitable, Callable +from typing import Callable from aioguardian import Client from aioguardian.errors import GuardianError diff --git a/homeassistant/components/harmony/data.py b/homeassistant/components/harmony/data.py index 340596ff1efb4..6fdf18df61248 100644 --- a/homeassistant/components/harmony/data.py +++ b/homeassistant/components/harmony/data.py @@ -1,7 +1,8 @@ """Harmony data object which contains the Harmony Client.""" +from __future__ import annotations +from collections.abc import Iterable import logging -from typing import Iterable from aioharmony.const import ClientCallbackType, SendCommandDevice import aioharmony.exceptions as aioexc diff --git a/homeassistant/components/heos/media_player.py b/homeassistant/components/heos/media_player.py index 6e271bf60cd99..b919db5834514 100644 --- a/homeassistant/components/heos/media_player.py +++ b/homeassistant/components/heos/media_player.py @@ -1,8 +1,10 @@ """Denon HEOS Media Player.""" +from __future__ import annotations + +from collections.abc import Sequence from functools import reduce, wraps import logging from operator import ior -from typing import Sequence from pyheos import HeosError, const as heos_const diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 09f459b32d6b2..35be51a99d9ba 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -2,12 +2,13 @@ from __future__ import annotations from collections import defaultdict +from collections.abc import Iterable from datetime import datetime as dt, timedelta from itertools import groupby import json import logging import time -from typing import Iterable, cast +from typing import cast from aiohttp import web from sqlalchemy import and_, bindparam, func, not_, or_ diff --git a/homeassistant/components/http/data_validator.py b/homeassistant/components/http/data_validator.py index d63912360a2a6..2768350c183a5 100644 --- a/homeassistant/components/http/data_validator.py +++ b/homeassistant/components/http/data_validator.py @@ -1,7 +1,10 @@ """Decorator for view methods to help with data validation.""" +from __future__ import annotations + +from collections.abc import Awaitable from functools import wraps import logging -from typing import Any, Awaitable, Callable +from typing import Any, Callable from aiohttp import web import voluptuous as vol diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index da21894745795..0384c872d4c24 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -4,7 +4,7 @@ from bisect import bisect import logging import re -from typing import Callable, NamedTuple, Pattern +from typing import Callable, NamedTuple import attr @@ -52,8 +52,8 @@ class SensorMeta(NamedTuple): icon: str | Callable[[StateType], str] | None = None unit: str | None = None enabled_default: bool = False - include: Pattern[str] | None = None - exclude: Pattern[str] | None = None + include: re.Pattern[str] | None = None + exclude: re.Pattern[str] | None = None formatter: Callable[[str], tuple[StateType, str | None]] | None = None diff --git a/homeassistant/components/humidifier/reproduce_state.py b/homeassistant/components/humidifier/reproduce_state.py index 3f73ebf4e0abc..1303dee451865 100644 --- a/homeassistant/components/humidifier/reproduce_state.py +++ b/homeassistant/components/humidifier/reproduce_state.py @@ -2,8 +2,9 @@ from __future__ import annotations import asyncio +from collections.abc import Iterable import logging -from typing import Any, Iterable +from typing import Any from homeassistant.const import ( ATTR_MODE, diff --git a/homeassistant/components/hyperion/light.py b/homeassistant/components/hyperion/light.py index ac2160120ccf7..f8d760c0a9fdf 100644 --- a/homeassistant/components/hyperion/light.py +++ b/homeassistant/components/hyperion/light.py @@ -1,10 +1,11 @@ """Support for Hyperion-NG remotes.""" from __future__ import annotations +from collections.abc import Mapping, Sequence import functools import logging from types import MappingProxyType -from typing import Any, Callable, Mapping, Sequence +from typing import Any, Callable from hyperion import client, const diff --git a/homeassistant/components/input_boolean/reproduce_state.py b/homeassistant/components/input_boolean/reproduce_state.py index 5fe7e779a988a..961345b742994 100644 --- a/homeassistant/components/input_boolean/reproduce_state.py +++ b/homeassistant/components/input_boolean/reproduce_state.py @@ -2,8 +2,9 @@ from __future__ import annotations import asyncio +from collections.abc import Iterable import logging -from typing import Any, Iterable +from typing import Any from homeassistant.const import ( ATTR_ENTITY_ID, diff --git a/homeassistant/components/input_datetime/reproduce_state.py b/homeassistant/components/input_datetime/reproduce_state.py index f996721eabd57..230a0ed235c9b 100644 --- a/homeassistant/components/input_datetime/reproduce_state.py +++ b/homeassistant/components/input_datetime/reproduce_state.py @@ -2,8 +2,9 @@ from __future__ import annotations import asyncio +from collections.abc import Iterable import logging -from typing import Any, Iterable +from typing import Any from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import Context, HomeAssistant, State diff --git a/homeassistant/components/input_number/reproduce_state.py b/homeassistant/components/input_number/reproduce_state.py index a897aec2ba8d3..c198236789ce2 100644 --- a/homeassistant/components/input_number/reproduce_state.py +++ b/homeassistant/components/input_number/reproduce_state.py @@ -2,8 +2,9 @@ from __future__ import annotations import asyncio +from collections.abc import Iterable import logging -from typing import Any, Iterable +from typing import Any import voluptuous as vol diff --git a/homeassistant/components/input_select/reproduce_state.py b/homeassistant/components/input_select/reproduce_state.py index 5ea7072e932d8..a2cb2cadd0b23 100644 --- a/homeassistant/components/input_select/reproduce_state.py +++ b/homeassistant/components/input_select/reproduce_state.py @@ -2,9 +2,10 @@ from __future__ import annotations import asyncio +from collections.abc import Iterable import logging from types import MappingProxyType -from typing import Any, Iterable +from typing import Any from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import Context, HomeAssistant, State diff --git a/homeassistant/components/input_text/reproduce_state.py b/homeassistant/components/input_text/reproduce_state.py index ce1b7c12c4611..56a03b0d133f2 100644 --- a/homeassistant/components/input_text/reproduce_state.py +++ b/homeassistant/components/input_text/reproduce_state.py @@ -2,8 +2,9 @@ from __future__ import annotations import asyncio +from collections.abc import Iterable import logging -from typing import Any, Iterable +from typing import Any from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import Context, HomeAssistant, State diff --git a/homeassistant/components/knx/binary_sensor.py b/homeassistant/components/knx/binary_sensor.py index 47462f272d470..455fd877c0e4c 100644 --- a/homeassistant/components/knx/binary_sensor.py +++ b/homeassistant/components/knx/binary_sensor.py @@ -1,7 +1,8 @@ """Support for KNX/IP binary sensors.""" from __future__ import annotations -from typing import Any, Callable, Iterable +from collections.abc import Iterable +from typing import Any, Callable from xknx.devices import BinarySensor as XknxBinarySensor diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index ca3f7b0f22aa7..63a8bd634d5b0 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -1,7 +1,8 @@ """Support for KNX/IP climate devices.""" from __future__ import annotations -from typing import Any, Callable, Iterable +from collections.abc import Iterable +from typing import Any, Callable from xknx.devices import Climate as XknxClimate from xknx.dpt.dpt_hvac_mode import HVACControllerMode, HVACOperationMode diff --git a/homeassistant/components/knx/cover.py b/homeassistant/components/knx/cover.py index c45d057c3afde..10f1be57be44e 100644 --- a/homeassistant/components/knx/cover.py +++ b/homeassistant/components/knx/cover.py @@ -1,8 +1,9 @@ """Support for KNX/IP covers.""" from __future__ import annotations +from collections.abc import Iterable from datetime import datetime -from typing import Any, Callable, Iterable +from typing import Any, Callable from xknx.devices import Cover as XknxCover, Device as XknxDevice diff --git a/homeassistant/components/knx/fan.py b/homeassistant/components/knx/fan.py index 38680e15bf85e..ca8ce74a52a84 100644 --- a/homeassistant/components/knx/fan.py +++ b/homeassistant/components/knx/fan.py @@ -1,8 +1,9 @@ """Support for KNX/IP fans.""" from __future__ import annotations +from collections.abc import Iterable import math -from typing import Any, Callable, Iterable +from typing import Any, Callable from xknx.devices import Fan as XknxFan diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index 0eb6243373432..26bd27baa75f1 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -1,7 +1,8 @@ """Support for KNX/IP lights.""" from __future__ import annotations -from typing import Any, Callable, Iterable +from collections.abc import Iterable +from typing import Any, Callable from xknx.devices import Light as XknxLight diff --git a/homeassistant/components/knx/scene.py b/homeassistant/components/knx/scene.py index ff08cdf411c7b..d845cb9467694 100644 --- a/homeassistant/components/knx/scene.py +++ b/homeassistant/components/knx/scene.py @@ -1,7 +1,8 @@ """Support for KNX scenes.""" from __future__ import annotations -from typing import Any, Callable, Iterable +from collections.abc import Iterable +from typing import Any, Callable from xknx.devices import Scene as XknxScene diff --git a/homeassistant/components/knx/sensor.py b/homeassistant/components/knx/sensor.py index f75f483b9fbb5..51951304f9349 100644 --- a/homeassistant/components/knx/sensor.py +++ b/homeassistant/components/knx/sensor.py @@ -1,7 +1,8 @@ """Support for KNX/IP sensors.""" from __future__ import annotations -from typing import Any, Callable, Iterable +from collections.abc import Iterable +from typing import Any, Callable from xknx.devices import Sensor as XknxSensor diff --git a/homeassistant/components/knx/switch.py b/homeassistant/components/knx/switch.py index c52beaea2ef64..fa8a33cc5bb28 100644 --- a/homeassistant/components/knx/switch.py +++ b/homeassistant/components/knx/switch.py @@ -1,7 +1,8 @@ """Support for KNX/IP switches.""" from __future__ import annotations -from typing import Any, Callable, Iterable +from collections.abc import Iterable +from typing import Any, Callable from xknx import XKNX from xknx.devices import Switch as XknxSwitch diff --git a/homeassistant/components/knx/weather.py b/homeassistant/components/knx/weather.py index cc2f3c0a09c60..1a29e7d8e1266 100644 --- a/homeassistant/components/knx/weather.py +++ b/homeassistant/components/knx/weather.py @@ -1,7 +1,8 @@ """Support for KNX/IP weather station.""" from __future__ import annotations -from typing import Callable, Iterable +from collections.abc import Iterable +from typing import Callable from xknx.devices import Weather as XknxWeather diff --git a/homeassistant/components/light/reproduce_state.py b/homeassistant/components/light/reproduce_state.py index 68fac8aa5c96c..fa70670eee76b 100644 --- a/homeassistant/components/light/reproduce_state.py +++ b/homeassistant/components/light/reproduce_state.py @@ -2,9 +2,10 @@ from __future__ import annotations import asyncio +from collections.abc import Iterable import logging from types import MappingProxyType -from typing import Any, Iterable +from typing import Any from homeassistant.const import ( ATTR_ENTITY_ID, diff --git a/homeassistant/components/lock/reproduce_state.py b/homeassistant/components/lock/reproduce_state.py index 0d575964b2bd3..e7e79f49be98f 100644 --- a/homeassistant/components/lock/reproduce_state.py +++ b/homeassistant/components/lock/reproduce_state.py @@ -2,8 +2,9 @@ from __future__ import annotations import asyncio +from collections.abc import Iterable import logging -from typing import Any, Iterable +from typing import Any from homeassistant.const import ( ATTR_ENTITY_ID, diff --git a/homeassistant/components/media_player/reproduce_state.py b/homeassistant/components/media_player/reproduce_state.py index 1707109197f83..5d491a83ce187 100644 --- a/homeassistant/components/media_player/reproduce_state.py +++ b/homeassistant/components/media_player/reproduce_state.py @@ -2,7 +2,8 @@ from __future__ import annotations import asyncio -from typing import Any, Iterable +from collections.abc import Iterable +from typing import Any from homeassistant.const import ( SERVICE_MEDIA_PAUSE, diff --git a/homeassistant/components/minio/minio_helper.py b/homeassistant/components/minio/minio_helper.py index f2d860675525c..c77e41727a4e5 100644 --- a/homeassistant/components/minio/minio_helper.py +++ b/homeassistant/components/minio/minio_helper.py @@ -1,14 +1,13 @@ """Minio helper methods.""" from __future__ import annotations -from collections.abc import Iterable +from collections.abc import Iterable, Iterator import json import logging from queue import Queue import re import threading import time -from typing import Iterator from urllib.parse import unquote from minio import Minio diff --git a/homeassistant/components/mysensors/gateway.py b/homeassistant/components/mysensors/gateway.py index 6cf8e7d738317..dc6caa939492c 100644 --- a/homeassistant/components/mysensors/gateway.py +++ b/homeassistant/components/mysensors/gateway.py @@ -3,10 +3,11 @@ import asyncio from collections import defaultdict +from collections.abc import Coroutine import logging import socket import sys -from typing import Any, Callable, Coroutine +from typing import Any, Callable import async_timeout from mysensors import BaseAsyncGateway, Message, Sensor, mysensors diff --git a/homeassistant/components/mysensors/helpers.py b/homeassistant/components/mysensors/helpers.py index 0d18b243520d5..71ea97bc3719d 100644 --- a/homeassistant/components/mysensors/helpers.py +++ b/homeassistant/components/mysensors/helpers.py @@ -4,7 +4,7 @@ from collections import defaultdict from enum import IntEnum import logging -from typing import Callable, DefaultDict +from typing import Callable from mysensors import BaseAsyncGateway, Message from mysensors.sensor import ChildSensor @@ -174,9 +174,9 @@ def validate_child( node_id: int, child: ChildSensor, value_type: int | None = None, -) -> DefaultDict[str, list[DevId]]: +) -> defaultdict[str, list[DevId]]: """Validate a child. Returns a dict mapping hass platform names to list of DevId.""" - validated: DefaultDict[str, list[DevId]] = defaultdict(list) + validated: defaultdict[str, list[DevId]] = defaultdict(list) pres: IntEnum = gateway.const.Presentation set_req: IntEnum = gateway.const.SetReq child_type_name: SensorType | None = next( diff --git a/homeassistant/components/netatmo/data_handler.py b/homeassistant/components/netatmo/data_handler.py index 6982a651a4543..41e7d158c0ca8 100644 --- a/homeassistant/components/netatmo/data_handler.py +++ b/homeassistant/components/netatmo/data_handler.py @@ -7,7 +7,6 @@ from itertools import islice import logging from time import time -from typing import Deque import pyatmo @@ -60,7 +59,7 @@ def __init__(self, hass: HomeAssistant, entry: ConfigEntry): self.listeners: list[CALLBACK_TYPE] = [] self._data_classes: dict = {} self.data = {} - self._queue: Deque = deque() + self._queue = deque() self._webhook: bool = False async def async_setup(self): diff --git a/homeassistant/components/number/reproduce_state.py b/homeassistant/components/number/reproduce_state.py index 4364dffe1e853..d628db825ca1d 100644 --- a/homeassistant/components/number/reproduce_state.py +++ b/homeassistant/components/number/reproduce_state.py @@ -2,8 +2,9 @@ from __future__ import annotations import asyncio +from collections.abc import Iterable import logging -from typing import Any, Iterable +from typing import Any from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import Context, State diff --git a/homeassistant/components/nws/__init__.py b/homeassistant/components/nws/__init__.py index 5724175b4bb13..021b996c9453d 100644 --- a/homeassistant/components/nws/__init__.py +++ b/homeassistant/components/nws/__init__.py @@ -2,9 +2,10 @@ from __future__ import annotations import asyncio +from collections.abc import Awaitable import datetime import logging -from typing import Awaitable, Callable +from typing import Callable from pynws import SimpleNWS diff --git a/homeassistant/components/persistent_notification/__init__.py b/homeassistant/components/persistent_notification/__init__.py index 05d52cf7830e3..071261e7b2387 100644 --- a/homeassistant/components/persistent_notification/__init__.py +++ b/homeassistant/components/persistent_notification/__init__.py @@ -2,8 +2,9 @@ from __future__ import annotations from collections import OrderedDict +from collections.abc import Mapping, MutableMapping import logging -from typing import Any, Mapping, MutableMapping +from typing import Any import voluptuous as vol diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index 6741abbfc9f58..f901600c98b0d 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -1,6 +1,9 @@ """This component provides support for RainMachine programs and zones.""" +from __future__ import annotations + +from collections.abc import Coroutine from datetime import datetime -from typing import Callable, Coroutine +from typing import Callable from regenmaschine.controller import Controller from regenmaschine.errors import RequestError diff --git a/homeassistant/components/remote/__init__.py b/homeassistant/components/remote/__init__.py index ecde6f67b67c5..94c54dd323d93 100644 --- a/homeassistant/components/remote/__init__.py +++ b/homeassistant/components/remote/__init__.py @@ -1,10 +1,11 @@ """Support to interface with universal remote control devices.""" from __future__ import annotations +from collections.abc import Iterable from datetime import timedelta import functools as ft import logging -from typing import Any, Iterable, cast, final +from typing import Any, cast, final import voluptuous as vol diff --git a/homeassistant/components/remote/reproduce_state.py b/homeassistant/components/remote/reproduce_state.py index b42a0bdc611a7..24f748d4a0279 100644 --- a/homeassistant/components/remote/reproduce_state.py +++ b/homeassistant/components/remote/reproduce_state.py @@ -2,8 +2,9 @@ from __future__ import annotations import asyncio +from collections.abc import Iterable import logging -from typing import Any, Iterable +from typing import Any from homeassistant.const import ( ATTR_ENTITY_ID, diff --git a/homeassistant/components/sharkiq/vacuum.py b/homeassistant/components/sharkiq/vacuum.py index eed41fb14387c..dd6e67667060a 100644 --- a/homeassistant/components/sharkiq/vacuum.py +++ b/homeassistant/components/sharkiq/vacuum.py @@ -1,8 +1,8 @@ """Shark IQ Wrapper.""" from __future__ import annotations +from collections.abc import Iterable import logging -from typing import Iterable from sharkiqpy import OperatingModes, PowerModes, Properties, SharkIqVacuum diff --git a/homeassistant/components/sma/__init__.py b/homeassistant/components/sma/__init__.py index e17437db065b6..6ca5fe712b78e 100644 --- a/homeassistant/components/sma/__init__.py +++ b/homeassistant/components/sma/__init__.py @@ -1,8 +1,9 @@ """The sma integration.""" +from __future__ import annotations + import asyncio from datetime import timedelta import logging -from typing import List import pysma @@ -40,7 +41,7 @@ _LOGGER = logging.getLogger(__name__) -def _parse_legacy_options(entry: ConfigEntry, sensor_def: pysma.Sensors) -> List[str]: +def _parse_legacy_options(entry: ConfigEntry, sensor_def: pysma.Sensors) -> list[str]: """Parse legacy configuration options. This will parse the legacy CONF_SENSORS and CONF_CUSTOM configuration options @@ -89,7 +90,7 @@ def _migrate_old_unique_ids( hass: HomeAssistant, entry: ConfigEntry, sensor_def: pysma.Sensors, - config_sensors: List[str], + config_sensors: list[str], ) -> None: """Migrate legacy sensor entity_id format to new format.""" entity_registry = er.async_get(hass) diff --git a/homeassistant/components/sma/sensor.py b/homeassistant/components/sma/sensor.py index ea5b5666408c5..4e950651ab092 100644 --- a/homeassistant/components/sma/sensor.py +++ b/homeassistant/components/sma/sensor.py @@ -1,8 +1,9 @@ """SMA Solar Webconnect interface.""" from __future__ import annotations +from collections.abc import Coroutine import logging -from typing import Any, Callable, Coroutine +from typing import Any, Callable import pysma import voluptuous as vol diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index 77ef913c629cf..d36739c955107 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -1,8 +1,10 @@ """Support for SmartThings Cloud.""" +from __future__ import annotations + import asyncio +from collections.abc import Iterable import importlib import logging -from typing import Iterable from aiohttp.client_exceptions import ClientConnectionError, ClientResponseError from pysmartapp.event import EVENT_TYPE_DEVICE diff --git a/homeassistant/components/smartthings/binary_sensor.py b/homeassistant/components/smartthings/binary_sensor.py index dd4c1e2928c5d..74eb253ebbbaa 100644 --- a/homeassistant/components/smartthings/binary_sensor.py +++ b/homeassistant/components/smartthings/binary_sensor.py @@ -1,7 +1,7 @@ """Support for binary sensors through the SmartThings cloud API.""" from __future__ import annotations -from typing import Sequence +from collections.abc import Sequence from pysmartthings import Attribute, Capability diff --git a/homeassistant/components/smartthings/climate.py b/homeassistant/components/smartthings/climate.py index 76c168fbc381e..da9e0fd090ab2 100644 --- a/homeassistant/components/smartthings/climate.py +++ b/homeassistant/components/smartthings/climate.py @@ -2,9 +2,8 @@ from __future__ import annotations import asyncio -from collections.abc import Iterable +from collections.abc import Iterable, Sequence import logging -from typing import Sequence from pysmartthings import Attribute, Capability diff --git a/homeassistant/components/smartthings/cover.py b/homeassistant/components/smartthings/cover.py index 8fff4ebbdfa51..66715edfe601d 100644 --- a/homeassistant/components/smartthings/cover.py +++ b/homeassistant/components/smartthings/cover.py @@ -1,7 +1,7 @@ """Support for covers through the SmartThings cloud API.""" from __future__ import annotations -from typing import Sequence +from collections.abc import Sequence from pysmartthings import Attribute, Capability diff --git a/homeassistant/components/smartthings/fan.py b/homeassistant/components/smartthings/fan.py index 167f3a38edf7b..62b84b19099c4 100644 --- a/homeassistant/components/smartthings/fan.py +++ b/homeassistant/components/smartthings/fan.py @@ -1,8 +1,8 @@ """Support for fans through the SmartThings cloud API.""" from __future__ import annotations +from collections.abc import Sequence import math -from typing import Sequence from pysmartthings import Capability diff --git a/homeassistant/components/smartthings/light.py b/homeassistant/components/smartthings/light.py index de678f255fa6e..cba4439368bc9 100644 --- a/homeassistant/components/smartthings/light.py +++ b/homeassistant/components/smartthings/light.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from typing import Sequence +from collections.abc import Sequence from pysmartthings import Capability diff --git a/homeassistant/components/smartthings/lock.py b/homeassistant/components/smartthings/lock.py index 2cd0b283cca86..601e207a6f5f6 100644 --- a/homeassistant/components/smartthings/lock.py +++ b/homeassistant/components/smartthings/lock.py @@ -1,7 +1,7 @@ """Support for locks through the SmartThings cloud API.""" from __future__ import annotations -from typing import Sequence +from collections.abc import Sequence from pysmartthings import Attribute, Capability diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py index 533d8f6476e4d..a7e2926036c22 100644 --- a/homeassistant/components/smartthings/sensor.py +++ b/homeassistant/components/smartthings/sensor.py @@ -2,7 +2,7 @@ from __future__ import annotations from collections import namedtuple -from typing import Sequence +from collections.abc import Sequence from pysmartthings import Attribute, Capability diff --git a/homeassistant/components/smartthings/switch.py b/homeassistant/components/smartthings/switch.py index d8bcd4554154b..7b8364d9ba38a 100644 --- a/homeassistant/components/smartthings/switch.py +++ b/homeassistant/components/smartthings/switch.py @@ -1,7 +1,7 @@ """Support for switches through the SmartThings cloud API.""" from __future__ import annotations -from typing import Sequence +from collections.abc import Sequence from pysmartthings import Attribute, Capability diff --git a/homeassistant/components/solaredge/sensor.py b/homeassistant/components/solaredge/sensor.py index d827990ac55b5..2195c10cc1d29 100644 --- a/homeassistant/components/solaredge/sensor.py +++ b/homeassistant/components/solaredge/sensor.py @@ -2,9 +2,10 @@ from __future__ import annotations from abc import abstractmethod +from collections.abc import Iterable from datetime import date, datetime, timedelta import logging -from typing import Any, Callable, Iterable +from typing import Any, Callable from requests.exceptions import ConnectTimeout, HTTPError from solaredge import Solaredge diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 3ee458ec9db42..8c7b61e96ecda 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -2,12 +2,13 @@ from __future__ import annotations import asyncio +from collections.abc import Coroutine from contextlib import suppress import datetime import functools as ft import logging import socket -from typing import Any, Callable, Coroutine +from typing import Any, Callable import urllib.parse import async_timeout diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index b6e2897ade2b5..d9f74e5e77696 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -1,8 +1,11 @@ """The SSDP integration.""" +from __future__ import annotations + import asyncio +from collections.abc import Mapping from datetime import timedelta import logging -from typing import Any, Mapping +from typing import Any import aiohttp from async_upnp_client.search import async_search diff --git a/homeassistant/components/stream/recorder.py b/homeassistant/components/stream/recorder.py index 01a8ca9ea6b49..f5393078ab9e3 100644 --- a/homeassistant/components/stream/recorder.py +++ b/homeassistant/components/stream/recorder.py @@ -1,10 +1,10 @@ """Provide functionality to record stream.""" from __future__ import annotations +from collections import deque import logging import os import threading -from typing import Deque import av @@ -21,7 +21,7 @@ def async_setup_recorder(hass): """Only here so Provider Registry works.""" -def recorder_save_worker(file_out: str, segments: Deque[Segment]): +def recorder_save_worker(file_out: str, segments: deque[Segment]): """Handle saving stream.""" if not segments: diff --git a/homeassistant/components/switch/light.py b/homeassistant/components/switch/light.py index e21548105225b..039090d3124bb 100644 --- a/homeassistant/components/switch/light.py +++ b/homeassistant/components/switch/light.py @@ -1,7 +1,8 @@ """Light support for switch entities.""" from __future__ import annotations -from typing import Any, Callable, Sequence, cast +from collections.abc import Sequence +from typing import Any, Callable, cast import voluptuous as vol diff --git a/homeassistant/components/switch/reproduce_state.py b/homeassistant/components/switch/reproduce_state.py index 94fd836631bb1..4cc1ec1f693fd 100644 --- a/homeassistant/components/switch/reproduce_state.py +++ b/homeassistant/components/switch/reproduce_state.py @@ -2,8 +2,9 @@ from __future__ import annotations import asyncio +from collections.abc import Iterable import logging -from typing import Any, Iterable +from typing import Any from homeassistant.const import ( ATTR_ENTITY_ID, diff --git a/homeassistant/components/system_health/__init__.py b/homeassistant/components/system_health/__init__.py index 2ad4863dbec3d..a7a92d3baf708 100644 --- a/homeassistant/components/system_health/__init__.py +++ b/homeassistant/components/system_health/__init__.py @@ -2,10 +2,11 @@ from __future__ import annotations import asyncio +from collections.abc import Awaitable import dataclasses from datetime import datetime import logging -from typing import Awaitable, Callable +from typing import Callable import aiohttp import async_timeout diff --git a/homeassistant/components/timer/reproduce_state.py b/homeassistant/components/timer/reproduce_state.py index 377f8a1dda22c..3ab7d4815cf30 100644 --- a/homeassistant/components/timer/reproduce_state.py +++ b/homeassistant/components/timer/reproduce_state.py @@ -2,8 +2,9 @@ from __future__ import annotations import asyncio +from collections.abc import Iterable import logging -from typing import Any, Iterable +from typing import Any from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import Context, State diff --git a/homeassistant/components/trace/__init__.py b/homeassistant/components/trace/__init__.py index eca22a56da84a..e845f92806849 100644 --- a/homeassistant/components/trace/__init__.py +++ b/homeassistant/components/trace/__init__.py @@ -1,9 +1,10 @@ """Support for script and automation tracing and debugging.""" from __future__ import annotations +from collections import deque import datetime as dt from itertools import count -from typing import Any, Deque +from typing import Any from homeassistant.core import Context from homeassistant.helpers.trace import ( @@ -52,7 +53,7 @@ def __init__( context: Context, ): """Container for script trace.""" - self._trace: dict[str, Deque[TraceElement]] | None = None + self._trace: dict[str, deque[TraceElement]] | None = None self._config: dict[str, Any] = config self._blueprint_inputs: dict[str, Any] = blueprint_inputs self.context: Context = context @@ -67,7 +68,7 @@ def __init__( trace_set_child_id(self.key, self.run_id) trace_id_set((key, self.run_id)) - def set_trace(self, trace: dict[str, Deque[TraceElement]]) -> None: + def set_trace(self, trace: dict[str, deque[TraceElement]]) -> None: """Set trace.""" self._trace = trace diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py index 7a1a3d4a06c28..51e340179396b 100644 --- a/homeassistant/components/upnp/config_flow.py +++ b/homeassistant/components/upnp/config_flow.py @@ -1,8 +1,9 @@ """Config flow for UPNP.""" from __future__ import annotations +from collections.abc import Mapping from datetime import timedelta -from typing import Any, Mapping +from typing import Any import voluptuous as vol diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index aafd9f5151679..c116e64ca7f35 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -2,8 +2,8 @@ from __future__ import annotations import asyncio +from collections.abc import Mapping from ipaddress import IPv4Address -from typing import Mapping from urllib.parse import urlparse from async_upnp_client import UpnpFactory diff --git a/homeassistant/components/vacuum/reproduce_state.py b/homeassistant/components/vacuum/reproduce_state.py index 38958bd47908a..4d5a9baf46e55 100644 --- a/homeassistant/components/vacuum/reproduce_state.py +++ b/homeassistant/components/vacuum/reproduce_state.py @@ -2,8 +2,9 @@ from __future__ import annotations import asyncio +from collections.abc import Iterable import logging -from typing import Any, Iterable +from typing import Any from homeassistant.const import ( ATTR_ENTITY_ID, diff --git a/homeassistant/components/vera/common.py b/homeassistant/components/vera/common.py index fcc501c20945c..243ee4d7594d9 100644 --- a/homeassistant/components/vera/common.py +++ b/homeassistant/components/vera/common.py @@ -1,7 +1,8 @@ """Common vera code.""" from __future__ import annotations -from typing import DefaultDict, NamedTuple +from collections import defaultdict +from typing import NamedTuple import pyvera as pv @@ -17,7 +18,7 @@ class ControllerData(NamedTuple): """Controller data.""" controller: pv.VeraController - devices: DefaultDict[str, list[pv.VeraDevice]] + devices: defaultdict[str, list[pv.VeraDevice]] scenes: list[pv.VeraScene] config_entry: ConfigEntry diff --git a/homeassistant/components/verisure/alarm_control_panel.py b/homeassistant/components/verisure/alarm_control_panel.py index 1cefd6af27293..b84affe9e8d3f 100644 --- a/homeassistant/components/verisure/alarm_control_panel.py +++ b/homeassistant/components/verisure/alarm_control_panel.py @@ -2,7 +2,8 @@ from __future__ import annotations import asyncio -from typing import Any, Callable, Iterable +from collections.abc import Iterable +from typing import Any, Callable from homeassistant.components.alarm_control_panel import ( FORMAT_NUMBER, diff --git a/homeassistant/components/verisure/binary_sensor.py b/homeassistant/components/verisure/binary_sensor.py index 3363178efe28d..758636bee9826 100644 --- a/homeassistant/components/verisure/binary_sensor.py +++ b/homeassistant/components/verisure/binary_sensor.py @@ -1,7 +1,8 @@ """Support for Verisure binary sensors.""" from __future__ import annotations -from typing import Any, Callable, Iterable +from collections.abc import Iterable +from typing import Any, Callable from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, diff --git a/homeassistant/components/verisure/camera.py b/homeassistant/components/verisure/camera.py index e667829bb10e0..a4442d2ae4bde 100644 --- a/homeassistant/components/verisure/camera.py +++ b/homeassistant/components/verisure/camera.py @@ -1,9 +1,10 @@ """Support for Verisure cameras.""" from __future__ import annotations +from collections.abc import Iterable import errno import os -from typing import Any, Callable, Iterable +from typing import Any, Callable from verisure import Error as VerisureError diff --git a/homeassistant/components/verisure/lock.py b/homeassistant/components/verisure/lock.py index eeec7e53a0a73..bcd5ac214ee90 100644 --- a/homeassistant/components/verisure/lock.py +++ b/homeassistant/components/verisure/lock.py @@ -2,7 +2,8 @@ from __future__ import annotations import asyncio -from typing import Any, Callable, Iterable +from collections.abc import Iterable +from typing import Any, Callable from verisure import Error as VerisureError diff --git a/homeassistant/components/verisure/sensor.py b/homeassistant/components/verisure/sensor.py index 93e1793da8d03..72b061bd62856 100644 --- a/homeassistant/components/verisure/sensor.py +++ b/homeassistant/components/verisure/sensor.py @@ -1,7 +1,8 @@ """Support for Verisure sensors.""" from __future__ import annotations -from typing import Any, Callable, Iterable +from collections.abc import Iterable +from typing import Any, Callable from homeassistant.components.sensor import ( DEVICE_CLASS_HUMIDITY, diff --git a/homeassistant/components/verisure/switch.py b/homeassistant/components/verisure/switch.py index f55db8ce4282f..a97758d17f9da 100644 --- a/homeassistant/components/verisure/switch.py +++ b/homeassistant/components/verisure/switch.py @@ -1,8 +1,9 @@ """Support for Verisure Smartplugs.""" from __future__ import annotations +from collections.abc import Iterable from time import monotonic -from typing import Any, Callable, Iterable +from typing import Any, Callable from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/water_heater/reproduce_state.py b/homeassistant/components/water_heater/reproduce_state.py index 4675cdb8621de..235eac5cd57db 100644 --- a/homeassistant/components/water_heater/reproduce_state.py +++ b/homeassistant/components/water_heater/reproduce_state.py @@ -2,8 +2,9 @@ from __future__ import annotations import asyncio +from collections.abc import Iterable import logging -from typing import Any, Iterable +from typing import Any from homeassistant.const import ( ATTR_ENTITY_ID, diff --git a/homeassistant/components/websocket_api/connection.py b/homeassistant/components/websocket_api/connection.py index dd1bb33369385..4e0ba257d59e5 100644 --- a/homeassistant/components/websocket_api/connection.py +++ b/homeassistant/components/websocket_api/connection.py @@ -2,7 +2,8 @@ from __future__ import annotations import asyncio -from typing import Any, Callable, Hashable +from collections.abc import Hashable +from typing import Any, Callable import voluptuous as vol diff --git a/homeassistant/components/websocket_api/decorators.py b/homeassistant/components/websocket_api/decorators.py index 283734f75784e..cbb0e8563c5b4 100644 --- a/homeassistant/components/websocket_api/decorators.py +++ b/homeassistant/components/websocket_api/decorators.py @@ -1,7 +1,10 @@ """Decorators for the Websocket API.""" +from __future__ import annotations + import asyncio +from collections.abc import Awaitable from functools import wraps -from typing import Awaitable, Callable +from typing import Callable from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import Unauthorized diff --git a/homeassistant/components/wemo/entity.py b/homeassistant/components/wemo/entity.py index d10707f459047..904d62639ebfb 100644 --- a/homeassistant/components/wemo/entity.py +++ b/homeassistant/components/wemo/entity.py @@ -2,9 +2,10 @@ from __future__ import annotations import asyncio +from collections.abc import Generator import contextlib import logging -from typing import Any, Generator +from typing import Any import async_timeout from pywemo import WeMoDevice diff --git a/homeassistant/components/xbox/remote.py b/homeassistant/components/xbox/remote.py index 12d9a6336c803..31e6220172a72 100644 --- a/homeassistant/components/xbox/remote.py +++ b/homeassistant/components/xbox/remote.py @@ -1,7 +1,10 @@ """Xbox Remote support.""" +from __future__ import annotations + import asyncio +from collections.abc import Iterable import re -from typing import Any, Iterable +from typing import Any from xbox.webapi.api.client import XboxLiveClient from xbox.webapi.api.provider.smartglass.models import ( diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py index 6ef0bd9e66515..3bd08e6f93e5d 100644 --- a/homeassistant/components/zha/core/channels/general.py +++ b/homeassistant/components/zha/core/channels/general.py @@ -2,7 +2,8 @@ from __future__ import annotations import asyncio -from typing import Any, Coroutine +from collections.abc import Coroutine +from typing import Any import zigpy.exceptions import zigpy.zcl.clusters.general as general diff --git a/homeassistant/components/zha/core/channels/homeautomation.py b/homeassistant/components/zha/core/channels/homeautomation.py index 989cc17f97de0..6e9d41386217b 100644 --- a/homeassistant/components/zha/core/channels/homeautomation.py +++ b/homeassistant/components/zha/core/channels/homeautomation.py @@ -1,7 +1,7 @@ """Home automation channels module for Zigbee Home Automation.""" from __future__ import annotations -from typing import Coroutine +from collections.abc import Coroutine import zigpy.zcl.clusters.homeautomation as homeautomation diff --git a/homeassistant/components/zha/core/channels/lighting.py b/homeassistant/components/zha/core/channels/lighting.py index eef4c56e3794a..8c2b2bddd6730 100644 --- a/homeassistant/components/zha/core/channels/lighting.py +++ b/homeassistant/components/zha/core/channels/lighting.py @@ -1,8 +1,8 @@ """Lighting channels module for Zigbee Home Automation.""" from __future__ import annotations +from collections.abc import Coroutine from contextlib import suppress -from typing import Coroutine import zigpy.zcl.clusters.lighting as lighting diff --git a/homeassistant/components/zha/core/channels/security.py b/homeassistant/components/zha/core/channels/security.py index 7c600d984014a..313d016935e9a 100644 --- a/homeassistant/components/zha/core/channels/security.py +++ b/homeassistant/components/zha/core/channels/security.py @@ -4,8 +4,10 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/integrations/zha/ """ +from __future__ import annotations + import asyncio -from typing import Coroutine +from collections.abc import Coroutine from zigpy.exceptions import ZigbeeException import zigpy.zcl.clusters.security as security diff --git a/homeassistant/components/zha/core/channels/smartenergy.py b/homeassistant/components/zha/core/channels/smartenergy.py index a815c75c8b36e..cfe395773adda 100644 --- a/homeassistant/components/zha/core/channels/smartenergy.py +++ b/homeassistant/components/zha/core/channels/smartenergy.py @@ -1,7 +1,7 @@ """Smart energy channels module for Zigbee Home Automation.""" from __future__ import annotations -from typing import Coroutine +from collections.abc import Coroutine import zigpy.zcl.clusters.smartenergy as smartenergy diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index f8fb12e159627..f38e4c2c69577 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -8,13 +8,14 @@ import asyncio import binascii +from collections.abc import Iterator from dataclasses import dataclass import functools import itertools import logging from random import uniform import re -from typing import Any, Callable, Iterator +from typing import Any, Callable import voluptuous as vol import zigpy.exceptions diff --git a/homeassistant/components/zha/core/store.py b/homeassistant/components/zha/core/store.py index 9381c52918720..c4bbca2567a6f 100644 --- a/homeassistant/components/zha/core/store.py +++ b/homeassistant/components/zha/core/store.py @@ -2,9 +2,10 @@ from __future__ import annotations from collections import OrderedDict +from collections.abc import MutableMapping import datetime import time -from typing import MutableMapping, cast +from typing import cast import attr diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index 445151899ee0b..2e5fe9354354e 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -2,9 +2,10 @@ from __future__ import annotations import asyncio +from collections.abc import Awaitable import functools import logging -from typing import Any, Awaitable +from typing import Any from homeassistant.const import ATTR_NAME from homeassistant.core import CALLBACK_TYPE, Event, callback diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 17ae01aa9b210..a7df9998f6a78 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -1,8 +1,9 @@ """Map Z-Wave nodes and values to Home Assistant entities.""" from __future__ import annotations +from collections.abc import Generator from dataclasses import dataclass -from typing import Any, Generator +from typing import Any from zwave_js_server.const import CommandClass from zwave_js_server.model.device_class import DeviceClassItem diff --git a/homeassistant/config.py b/homeassistant/config.py index 362c93d04fa86..958dcea555f4a 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -2,13 +2,14 @@ from __future__ import annotations from collections import OrderedDict +from collections.abc import Sequence import logging import os from pathlib import Path import re import shutil from types import ModuleType -from typing import Any, Callable, Sequence +from typing import Any, Callable from awesomeversion import AwesomeVersion import voluptuous as vol diff --git a/homeassistant/core.py b/homeassistant/core.py index 1356de0b57281..3313da887c267 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -7,6 +7,7 @@ from __future__ import annotations import asyncio +from collections.abc import Awaitable, Collection, Iterable, Mapping import datetime import enum import functools @@ -17,19 +18,7 @@ import threading from time import monotonic from types import MappingProxyType -from typing import ( - TYPE_CHECKING, - Any, - Awaitable, - Callable, - Collection, - Coroutine, - Iterable, - Mapping, - Optional, - TypeVar, - cast, -) +from typing import TYPE_CHECKING, Any, Callable, Coroutine, Optional, TypeVar, cast import attr import voluptuous as vol diff --git a/homeassistant/exceptions.py b/homeassistant/exceptions.py index a081cfe3cc2c5..844fd369cacee 100644 --- a/homeassistant/exceptions.py +++ b/homeassistant/exceptions.py @@ -1,7 +1,8 @@ """The exceptions used by Home Assistant.""" from __future__ import annotations -from typing import TYPE_CHECKING, Generator, Sequence +from collections.abc import Generator, Sequence +from typing import TYPE_CHECKING import attr diff --git a/homeassistant/helpers/__init__.py b/homeassistant/helpers/__init__.py index a1964c432fc0d..a0642e8ead292 100644 --- a/homeassistant/helpers/__init__.py +++ b/homeassistant/helpers/__init__.py @@ -1,8 +1,9 @@ """Helper methods for components within Home Assistant.""" from __future__ import annotations +from collections.abc import Iterable, Sequence import re -from typing import TYPE_CHECKING, Any, Iterable, Sequence +from typing import TYPE_CHECKING, Any from homeassistant.const import CONF_PLATFORM diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index 53b906efd353b..0bb9a815c8467 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -2,10 +2,11 @@ from __future__ import annotations import asyncio +from collections.abc import Awaitable from contextlib import suppress from ssl import SSLContext import sys -from typing import Any, Awaitable, Callable, cast +from typing import Any, Callable, cast import aiohttp from aiohttp import web diff --git a/homeassistant/helpers/area_registry.py b/homeassistant/helpers/area_registry.py index af568b404188f..67d713e508788 100644 --- a/homeassistant/helpers/area_registry.py +++ b/homeassistant/helpers/area_registry.py @@ -2,7 +2,8 @@ from __future__ import annotations from collections import OrderedDict -from typing import Container, Iterable, MutableMapping, cast +from collections.abc import Container, Iterable, MutableMapping +from typing import cast import attr diff --git a/homeassistant/helpers/collection.py b/homeassistant/helpers/collection.py index 248059f7f93f6..bfffb8523dd92 100644 --- a/homeassistant/helpers/collection.py +++ b/homeassistant/helpers/collection.py @@ -3,10 +3,11 @@ from abc import ABC, abstractmethod import asyncio +from collections.abc import Coroutine from dataclasses import dataclass from itertools import groupby import logging -from typing import Any, Awaitable, Callable, Coroutine, Iterable, Optional, cast +from typing import Any, Awaitable, Callable, Iterable, Optional, cast import voluptuous as vol from voluptuous.humanize import humanize_error diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index 18ef4c2082e67..138fa81947c5f 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -3,13 +3,14 @@ import asyncio from collections import deque +from collections.abc import Container, Generator from contextlib import contextmanager from datetime import datetime, timedelta import functools as ft import logging import re import sys -from typing import Any, Callable, Container, Generator, cast +from typing import Any, Callable, cast from homeassistant.components import zone as zone_cmp from homeassistant.components.device_automation import ( diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index 891d6c7d28c4c..e19a065eb0262 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -9,10 +9,11 @@ from abc import ABC, ABCMeta, abstractmethod import asyncio +from collections.abc import Awaitable import logging import secrets import time -from typing import Any, Awaitable, Callable, Dict, cast +from typing import Any, Callable, Dict, cast from aiohttp import client, web import async_timeout diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index bbac18ab8390d..e0afbc49af294 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -1,6 +1,7 @@ """Helpers for config validation using voluptuous.""" from __future__ import annotations +from collections.abc import Hashable from datetime import ( date as date_sys, datetime as datetime_sys, @@ -14,7 +15,7 @@ import os import re from socket import _GLOBAL_DEFAULT_TIMEOUT # type: ignore # private, not in typeshed -from typing import Any, Callable, Dict, Hashable, Pattern, TypeVar, cast +from typing import Any, Callable, Dict, TypeVar, cast from urllib.parse import urlparse from uuid import UUID @@ -204,7 +205,7 @@ def validator(value: Any) -> str: return validator -def is_regex(value: Any) -> Pattern[Any]: +def is_regex(value: Any) -> re.Pattern[Any]: """Validate that a string is a valid regular expression.""" try: r = re.compile(value) diff --git a/homeassistant/helpers/debounce.py b/homeassistant/helpers/debounce.py index 705f48bbd708a..8e7e57fa14239 100644 --- a/homeassistant/helpers/debounce.py +++ b/homeassistant/helpers/debounce.py @@ -2,8 +2,9 @@ from __future__ import annotations import asyncio +from collections.abc import Awaitable from logging import Logger -from typing import Any, Awaitable, Callable +from typing import Any, Callable from homeassistant.core import HassJob, HomeAssistant, callback diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index f30832479c2c8..e6af7751c88f8 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -3,12 +3,12 @@ from abc import ABC import asyncio -from collections.abc import Mapping +from collections.abc import Awaitable, Iterable, Mapping from datetime import datetime, timedelta import functools as ft import logging from timeit import default_timer as timer -from typing import Any, Awaitable, Iterable +from typing import Any from homeassistant.config import DATA_CUSTOMIZE from homeassistant.const import ( diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 46279fcb14010..7ac221ea06e34 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -2,11 +2,12 @@ from __future__ import annotations import asyncio +from collections.abc import Iterable from datetime import timedelta from itertools import chain import logging from types import ModuleType -from typing import Any, Callable, Iterable +from typing import Any, Callable import voluptuous as vol diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index ef45b8dcd97a5..abeeb40ca76cd 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -2,12 +2,13 @@ from __future__ import annotations import asyncio +from collections.abc import Coroutine, Iterable from contextvars import ContextVar from datetime import datetime, timedelta import logging from logging import Logger from types import ModuleType -from typing import TYPE_CHECKING, Callable, Coroutine, Iterable +from typing import TYPE_CHECKING, Callable from homeassistant import config_entries from homeassistant.const import ( diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index db16b3cc0b184..936464dc423d9 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -10,8 +10,9 @@ from __future__ import annotations from collections import OrderedDict +from collections.abc import Iterable import logging -from typing import TYPE_CHECKING, Any, Callable, Iterable, cast +from typing import TYPE_CHECKING, Any, Callable, cast import attr diff --git a/homeassistant/helpers/entity_values.py b/homeassistant/helpers/entity_values.py index 57dbb34c560a0..8f11fbf31161b 100644 --- a/homeassistant/helpers/entity_values.py +++ b/homeassistant/helpers/entity_values.py @@ -4,7 +4,7 @@ from collections import OrderedDict import fnmatch import re -from typing import Any, Pattern +from typing import Any from homeassistant.core import split_entity_id @@ -26,7 +26,7 @@ def __init__( self._domain = domain if glob is None: - compiled: dict[Pattern[str], Any] | None = None + compiled: dict[re.Pattern[str], Any] | None = None else: compiled = OrderedDict() for key, value in glob.items(): diff --git a/homeassistant/helpers/entityfilter.py b/homeassistant/helpers/entityfilter.py index ebde309de144e..e026955f286d7 100644 --- a/homeassistant/helpers/entityfilter.py +++ b/homeassistant/helpers/entityfilter.py @@ -3,7 +3,7 @@ import fnmatch import re -from typing import Callable, Pattern +from typing import Callable import voluptuous as vol @@ -104,12 +104,12 @@ def convert_include_exclude_filter( ) -def _glob_to_re(glob: str) -> Pattern[str]: +def _glob_to_re(glob: str) -> re.Pattern[str]: """Translate and compile glob string into pattern.""" return re.compile(fnmatch.translate(glob)) -def _test_against_patterns(patterns: list[Pattern[str]], entity_id: str) -> bool: +def _test_against_patterns(patterns: list[re.Pattern[str]], entity_id: str) -> bool: """Test entity against list of patterns, true if any match.""" for pattern in patterns: if pattern.match(entity_id): diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 1a7e11ff5c992..b8a8db8f03df2 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -2,13 +2,14 @@ from __future__ import annotations import asyncio +from collections.abc import Awaitable, Iterable import copy from dataclasses import dataclass from datetime import datetime, timedelta import functools as ft import logging import time -from typing import Any, Awaitable, Callable, Iterable, List, cast +from typing import Any, Callable, List, cast import attr diff --git a/homeassistant/helpers/integration_platform.py b/homeassistant/helpers/integration_platform.py index 93f3f3f742755..5becda0545b9c 100644 --- a/homeassistant/helpers/integration_platform.py +++ b/homeassistant/helpers/integration_platform.py @@ -1,7 +1,10 @@ """Helpers to help with integration platforms.""" +from __future__ import annotations + import asyncio +from collections.abc import Awaitable import logging -from typing import Any, Awaitable, Callable +from typing import Any, Callable from homeassistant.core import Event, HomeAssistant from homeassistant.loader import async_get_integration, bind_hass diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index 6ed8a6b596813..96cfbf0e1b567 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -1,9 +1,10 @@ """Module to coordinate user intentions.""" from __future__ import annotations +from collections.abc import Iterable import logging import re -from typing import Any, Callable, Dict, Iterable +from typing import Any, Callable, Dict import voluptuous as vol diff --git a/homeassistant/helpers/location.py b/homeassistant/helpers/location.py index 597787ac173e2..da81040185c0b 100644 --- a/homeassistant/helpers/location.py +++ b/homeassistant/helpers/location.py @@ -1,8 +1,8 @@ """Location helpers for Home Assistant.""" from __future__ import annotations +from collections.abc import Iterable import logging -from typing import Iterable import voluptuous as vol diff --git a/homeassistant/helpers/logging.py b/homeassistant/helpers/logging.py index a32d13ce51335..e996e7cca103b 100644 --- a/homeassistant/helpers/logging.py +++ b/homeassistant/helpers/logging.py @@ -1,9 +1,10 @@ """Helpers for logging allowing more advanced logging styles to be used.""" from __future__ import annotations +from collections.abc import Mapping, MutableMapping import inspect import logging -from typing import Any, Mapping, MutableMapping +from typing import Any class KeywordMessage: diff --git a/homeassistant/helpers/ratelimit.py b/homeassistant/helpers/ratelimit.py index fa671c6627f91..c34cfb72b3678 100644 --- a/homeassistant/helpers/ratelimit.py +++ b/homeassistant/helpers/ratelimit.py @@ -2,9 +2,10 @@ from __future__ import annotations import asyncio +from collections.abc import Hashable from datetime import datetime, timedelta import logging -from typing import Any, Callable, Hashable +from typing import Any, Callable from homeassistant.core import HomeAssistant, callback import homeassistant.util.dt as dt_util diff --git a/homeassistant/helpers/reload.py b/homeassistant/helpers/reload.py index ef1d033cfa7ad..01350b579c44e 100644 --- a/homeassistant/helpers/reload.py +++ b/homeassistant/helpers/reload.py @@ -2,8 +2,8 @@ from __future__ import annotations import asyncio +from collections.abc import Iterable import logging -from typing import Iterable from homeassistant import config as conf_util from homeassistant.const import SERVICE_RELOAD diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 12f75960c4129..5a7fdcd1767fd 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -2,13 +2,14 @@ from __future__ import annotations import asyncio +from collections.abc import Sequence from contextlib import asynccontextmanager, suppress from datetime import datetime, timedelta from functools import partial import itertools import logging from types import MappingProxyType -from typing import Any, Callable, Dict, Sequence, TypedDict, Union, cast +from typing import Any, Callable, Dict, TypedDict, Union, cast import async_timeout import voluptuous as vol diff --git a/homeassistant/helpers/script_variables.py b/homeassistant/helpers/script_variables.py index 86a700bc62bf1..a72d0b5543fb0 100644 --- a/homeassistant/helpers/script_variables.py +++ b/homeassistant/helpers/script_variables.py @@ -1,7 +1,8 @@ """Script variables.""" from __future__ import annotations -from typing import Any, Mapping +from collections.abc import Mapping +from typing import Any from homeassistant.core import HomeAssistant, callback diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 9dec919d4b5f1..ed23926b0a3e6 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -2,10 +2,11 @@ from __future__ import annotations import asyncio +from collections.abc import Awaitable, Iterable import dataclasses from functools import partial, wraps import logging -from typing import TYPE_CHECKING, Any, Awaitable, Callable, Iterable, TypedDict +from typing import TYPE_CHECKING, Any, Callable, TypedDict import voluptuous as vol diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index c9f267a89f554..38647792b7a0c 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -3,10 +3,11 @@ import asyncio from collections import defaultdict +from collections.abc import Iterable import datetime as dt import logging from types import ModuleType, TracebackType -from typing import Any, Iterable +from typing import Any from homeassistant.components.sun import STATE_ABOVE_HORIZON, STATE_BELOW_HORIZON from homeassistant.const import ( diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 06fe5d288f570..053ab2947dd1b 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -5,6 +5,7 @@ import asyncio import base64 import collections.abc +from collections.abc import Generator, Iterable from contextlib import suppress from contextvars import ContextVar from datetime import datetime, timedelta @@ -16,7 +17,7 @@ import random import re import sys -from typing import Any, Callable, Generator, Iterable, cast +from typing import Any, Callable, cast from urllib.parse import urlencode as urllib_urlencode import weakref diff --git a/homeassistant/helpers/trace.py b/homeassistant/helpers/trace.py index 32e387d972fa1..e2d5144f3742b 100644 --- a/homeassistant/helpers/trace.py +++ b/homeassistant/helpers/trace.py @@ -2,10 +2,11 @@ from __future__ import annotations from collections import deque +from collections.abc import Generator from contextlib import contextmanager from contextvars import ContextVar from functools import wraps -from typing import Any, Callable, Deque, Generator, cast +from typing import Any, Callable, cast from homeassistant.helpers.typing import TemplateVarsType import homeassistant.util.dt as dt_util @@ -76,7 +77,7 @@ def as_dict(self) -> dict[str, Any]: # Context variables for tracing # Current trace -trace_cv: ContextVar[dict[str, Deque[TraceElement]] | None] = ContextVar( +trace_cv: ContextVar[dict[str, deque[TraceElement]] | None] = ContextVar( "trace_cv", default=None ) # Stack of TraceElements @@ -168,7 +169,7 @@ def trace_append_element( trace[path].append(trace_element) -def trace_get(clear: bool = True) -> dict[str, Deque[TraceElement]] | None: +def trace_get(clear: bool = True) -> dict[str, deque[TraceElement]] | None: """Return the current trace.""" if clear: trace_clear() diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index d2d7612972d54..863fa71d43cdb 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -2,10 +2,11 @@ from __future__ import annotations import asyncio +from collections.abc import Awaitable from datetime import datetime, timedelta import logging from time import monotonic -from typing import Any, Awaitable, Callable, Generic, TypeVar +from typing import Any, Callable, Generic, TypeVar import urllib.error import aiohttp diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py index aaad5c1f25143..cc4ce32d8082c 100644 --- a/homeassistant/requirements.py +++ b/homeassistant/requirements.py @@ -2,8 +2,9 @@ from __future__ import annotations import asyncio +from collections.abc import Iterable import os -from typing import Any, Iterable, cast +from typing import Any, cast from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError diff --git a/homeassistant/scripts/__init__.py b/homeassistant/scripts/__init__.py index 9aa07b94dc895..b31fc7181731f 100644 --- a/homeassistant/scripts/__init__.py +++ b/homeassistant/scripts/__init__.py @@ -3,11 +3,11 @@ import argparse import asyncio +from collections.abc import Sequence import importlib import logging import os import sys -from typing import Sequence from homeassistant import runner from homeassistant.bootstrap import async_mount_local_lib_path diff --git a/homeassistant/setup.py b/homeassistant/setup.py index c1d4173fff155..f5a6f9b97214c 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -2,11 +2,12 @@ from __future__ import annotations import asyncio +from collections.abc import Awaitable, Generator, Iterable import contextlib import logging.handlers from timeit import default_timer as timer from types import ModuleType -from typing import Awaitable, Callable, Generator, Iterable +from typing import Callable from homeassistant import config as conf_util, core, loader, requirements from homeassistant.config import async_notify_setup_error diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index c684d14d27664..f7f0743455590 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from collections.abc import Coroutine, Iterable, KeysView from datetime import datetime, timedelta import enum from functools import lru_cache, wraps @@ -11,7 +12,7 @@ import string import threading from types import MappingProxyType -from typing import Any, Callable, Coroutine, Iterable, KeysView, TypeVar +from typing import Any, Callable, TypeVar import slugify as unicode_slug diff --git a/homeassistant/util/async_.py b/homeassistant/util/async_.py index 15353d1f7eb8d..a467d544174da 100644 --- a/homeassistant/util/async_.py +++ b/homeassistant/util/async_.py @@ -3,12 +3,13 @@ from asyncio import Semaphore, coroutines, ensure_future, gather, get_running_loop from asyncio.events import AbstractEventLoop +from collections.abc import Awaitable, Coroutine import concurrent.futures import functools import logging import threading from traceback import extract_stack -from typing import Any, Awaitable, Callable, Coroutine, TypeVar +from typing import Any, Callable, TypeVar _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/util/logging.py b/homeassistant/util/logging.py index 1c0ff3de5d7bd..dd3cb119c6bbe 100644 --- a/homeassistant/util/logging.py +++ b/homeassistant/util/logging.py @@ -2,13 +2,14 @@ from __future__ import annotations import asyncio +from collections.abc import Coroutine from functools import partial, wraps import inspect import logging import logging.handlers import queue import traceback -from typing import Any, Awaitable, Callable, Coroutine, cast, overload +from typing import Any, Awaitable, Callable, cast, overload from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE from homeassistant.core import HomeAssistant, callback, is_callback diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index b03e93f17dfaa..d63ddd6afa3e5 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -2,11 +2,12 @@ from __future__ import annotations from collections import OrderedDict +from collections.abc import Iterator import fnmatch import logging import os from pathlib import Path -from typing import Any, Dict, Iterator, List, TextIO, TypeVar, Union, overload +from typing import Any, Dict, List, TextIO, TypeVar, Union, overload import yaml From fd21c460a017531e8d9b21a2e96e6ac25d256364 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 20 Apr 2021 06:04:34 -1000 Subject: [PATCH 0389/1317] Fix memory leak in verisure (#49460) --- homeassistant/components/verisure/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/verisure/__init__.py b/homeassistant/components/verisure/__init__.py index 55e3d020b1381..622f2aecc1441 100644 --- a/homeassistant/components/verisure/__init__.py +++ b/homeassistant/components/verisure/__init__.py @@ -127,7 +127,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not await coordinator.async_login(): raise ConfigEntryAuthFailed - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, coordinator.async_logout) + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, coordinator.async_logout) + ) await coordinator.async_config_entry_first_refresh() From baa8de2f8998377074d42d164769cfc334d33543 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 20 Apr 2021 06:11:58 -1000 Subject: [PATCH 0390/1317] Fix homekit memory leak on entry reload (#49452) --- homeassistant/components/homekit/__init__.py | 14 +++++--------- homeassistant/components/homekit/const.py | 1 - 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 0e4bcc28aabfd..04545d8a247ad 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -95,7 +95,6 @@ SERVICE_HOMEKIT_RESET_ACCESSORY, SERVICE_HOMEKIT_START, SHUTDOWN_TIMEOUT, - UNDO_UPDATE_LISTENER, ) from .util import ( accessory_friendly_name, @@ -276,12 +275,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): entry.title, ) - hass.data[DOMAIN][entry.entry_id] = { - HOMEKIT: homekit, - UNDO_UPDATE_LISTENER: entry.add_update_listener(_async_update_listener), - } + entry.async_on_unload(entry.add_update_listener(_async_update_listener)) + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, homekit.async_stop) + ) - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, homekit.async_stop) + hass.data[DOMAIN][entry.entry_id] = {HOMEKIT: homekit} if hass.state == CoreState.running: await homekit.async_start() @@ -301,9 +300,6 @@ async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" dismiss_setup_message(hass, entry.entry_id) - - hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() - homekit = hass.data[DOMAIN][entry.entry_id][HOMEKIT] if homekit.status == STATUS_RUNNING: diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index abfc6a2aa381f..073650aba4001 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -8,7 +8,6 @@ HOMEKIT_PAIRING_QR = "homekit-pairing-qr" HOMEKIT_PAIRING_QR_SECRET = "homekit-pairing-qr-secret" HOMEKIT = "homekit" -UNDO_UPDATE_LISTENER = "undo_update_listener" SHUTDOWN_TIMEOUT = 30 CONF_ENTRY_INDEX = "index" From 3cbfa36397753f7f1832d0d24756f1bbd7c686d2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 20 Apr 2021 06:12:11 -1000 Subject: [PATCH 0391/1317] Fix memory leak on apple_tv reload (#49454) --- homeassistant/components/apple_tv/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/apple_tv/__init__.py b/homeassistant/components/apple_tv/__init__.py index b4e0e1be66634..d7b5054683279 100644 --- a/homeassistant/components/apple_tv/__init__.py +++ b/homeassistant/components/apple_tv/__init__.py @@ -50,7 +50,9 @@ async def on_hass_stop(event): """Stop push updates when hass stops.""" await manager.disconnect() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop) + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop) + ) async def setup_platforms(): """Set up platforms and initiate connection.""" From 11281e1cdb0c01a533f0cfbca438d9c9c52febf0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 20 Apr 2021 06:12:21 -1000 Subject: [PATCH 0392/1317] Fix memory leak in logi_circle (#49458) --- homeassistant/components/logi_circle/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/logi_circle/__init__.py b/homeassistant/components/logi_circle/__init__.py index 2b6553f9d3203..1311e50f29347 100644 --- a/homeassistant/components/logi_circle/__init__.py +++ b/homeassistant/components/logi_circle/__init__.py @@ -220,7 +220,9 @@ async def shut_down(event=None): """Close Logi Circle aiohttp session.""" await logi_circle.auth_provider.close() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shut_down) + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shut_down) + ) return True From 2279b5593df3f1c257be12e4d01e35288b517b1e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 20 Apr 2021 06:12:32 -1000 Subject: [PATCH 0393/1317] Fix memory leak in vera (#49459) --- homeassistant/components/vera/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/vera/__init__.py b/homeassistant/components/vera/__init__.py index 3654db5072d88..9feba3cd08d9d 100644 --- a/homeassistant/components/vera/__init__.py +++ b/homeassistant/components/vera/__init__.py @@ -155,7 +155,9 @@ def stop_subscription(event): controller.stop() await hass.async_add_executor_job(controller.start) - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_subscription) + config_entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_subscription) + ) return True From c9fbdfbbbe7ea7c17eb4a04f204b058d46876f0d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 20 Apr 2021 06:12:42 -1000 Subject: [PATCH 0394/1317] Fix memory leak in heos (#49461) --- homeassistant/components/heos/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/heos/__init__.py b/homeassistant/components/heos/__init__.py index a71d0d2de50a0..a4db978a39de7 100644 --- a/homeassistant/components/heos/__init__.py +++ b/homeassistant/components/heos/__init__.py @@ -82,7 +82,9 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): async def disconnect_controller(event): await controller.disconnect() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, disconnect_controller) + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, disconnect_controller) + ) # Get players and sources try: From 052e935c2be7a2edf75a1f73d18c3934631ea83c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 20 Apr 2021 06:12:54 -1000 Subject: [PATCH 0395/1317] Fix memory leak in fritzbox (#49462) --- homeassistant/components/fritzbox/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index ff417b25daf19..2d9812b9ff9c5 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -96,7 +96,9 @@ def logout_fritzbox(event): """Close connections to this fritzbox.""" fritz.logout() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, logout_fritzbox) + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, logout_fritzbox) + ) return True From 7db5d50ce4996c2184b57b9402268333be5820bb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 20 Apr 2021 06:13:07 -1000 Subject: [PATCH 0396/1317] Fix memory leak in unifi on reload (#49456) --- homeassistant/components/unifi/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py index 8d24a9b642ffd..0877cda7475b3 100644 --- a/homeassistant/components/unifi/__init__.py +++ b/homeassistant/components/unifi/__init__.py @@ -44,7 +44,9 @@ async def async_setup_entry(hass, config_entry): hass.data[UNIFI_DOMAIN][config_entry.entry_id] = controller - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, controller.shutdown) + config_entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, controller.shutdown) + ) LOGGER.debug("UniFi config options %s", config_entry.options) From 786f3163ac4fe6687be2c3f7c9844c8d89b577f5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 20 Apr 2021 06:13:28 -1000 Subject: [PATCH 0397/1317] Fix memory leak in freebox (#49463) --- homeassistant/components/freebox/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/freebox/__init__.py b/homeassistant/components/freebox/__init__.py index c6c98e6c2dfb1..a54f34b4d1292 100644 --- a/homeassistant/components/freebox/__init__.py +++ b/homeassistant/components/freebox/__init__.py @@ -61,7 +61,9 @@ async def async_close_connection(event): """Close Freebox connection on HA Stop.""" await router.close() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_close_connection) + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_close_connection) + ) return True From 1193c5360da01a767713f3e8ff855e249632f45a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 20 Apr 2021 06:13:41 -1000 Subject: [PATCH 0398/1317] Fix memory leak in tibber (#49465) --- homeassistant/components/tibber/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tibber/__init__.py b/homeassistant/components/tibber/__init__.py index fd7fc389c75c8..ed5b0c4ce6068 100644 --- a/homeassistant/components/tibber/__init__.py +++ b/homeassistant/components/tibber/__init__.py @@ -60,7 +60,7 @@ async def async_setup_entry(hass, entry): async def _close(event): await tibber_connection.rt_disconnect() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _close) + entry.async_on_unload(hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _close)) try: await tibber_connection.update_info() From 30c99ce954b7b888e552deea4a1b91e056360fd1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 20 Apr 2021 06:13:50 -1000 Subject: [PATCH 0399/1317] Fix memory leak in insteon (#49466) --- homeassistant/components/insteon/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/insteon/__init__.py b/homeassistant/components/insteon/__init__.py index 509878f961325..2e2d801e1f2b3 100644 --- a/homeassistant/components/insteon/__init__.py +++ b/homeassistant/components/insteon/__init__.py @@ -96,7 +96,9 @@ async def async_setup_entry(hass, entry): _LOGGER.error("Could not connect to Insteon modem") raise ConfigEntryNotReady from exception - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, close_insteon_connection) + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, close_insteon_connection) + ) await devices.async_load( workdir=hass.config.config_dir, id_devices=0, load_modem_aldb=0 From b2db9d3ca29cfc74db1e3260fa24060220e8b509 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 20 Apr 2021 06:14:00 -1000 Subject: [PATCH 0400/1317] Fix memory leak in firmata (#49467) --- homeassistant/components/firmata/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/firmata/__init__.py b/homeassistant/components/firmata/__init__.py index c0394a95a49ac..24b6420e8a524 100644 --- a/homeassistant/components/firmata/__init__.py +++ b/homeassistant/components/firmata/__init__.py @@ -184,7 +184,9 @@ async def handle_shutdown(event) -> None: if config_entry.entry_id in hass.data[DOMAIN]: await board.async_reset() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, handle_shutdown) + config_entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, handle_shutdown) + ) device_registry = await dr.async_get_registry(hass) device_registry.async_get_or_create( From d858d2ff254c0ea66d7eb771d823548a34b532eb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 20 Apr 2021 06:14:11 -1000 Subject: [PATCH 0401/1317] Fix memory leak in deconz (#49468) --- homeassistant/components/deconz/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 8b609fe312696..eb659b870c163 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -45,7 +45,9 @@ async def async_setup_entry(hass, config_entry): await async_setup_services(hass) - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, gateway.shutdown) + config_entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, gateway.shutdown) + ) return True From 77916706073790f690ee2051e08edee39e36ff8e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 20 Apr 2021 06:14:23 -1000 Subject: [PATCH 0402/1317] Fix memory leak in legacy nest (#49469) --- homeassistant/components/nest/legacy/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/nest/legacy/__init__.py b/homeassistant/components/nest/legacy/__init__.py index 60faa90e8b462..b0083dcf99016 100644 --- a/homeassistant/components/nest/legacy/__init__.py +++ b/homeassistant/components/nest/legacy/__init__.py @@ -249,7 +249,9 @@ def shut_down(event): """Stop Nest update event listener.""" nest.update_event.set() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shut_down) + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shut_down) + ) _LOGGER.debug("async_setup_nest is done") From 853707691765a8e267f5264f525237311f737369 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 20 Apr 2021 06:14:34 -1000 Subject: [PATCH 0403/1317] Fix memory leak in huawei_lte (#49470) --- homeassistant/components/huawei_lte/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index 25df0f620fa50..8a729b3c38ce7 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -445,7 +445,9 @@ def _update_router(*_: Any) -> None: ) # Clean up at end - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, router.cleanup) + config_entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, router.cleanup) + ) return True From 76b59a3983e320ca6c73f7005b9938a59754ecb0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 20 Apr 2021 06:14:48 -1000 Subject: [PATCH 0404/1317] Fix memory leak in hangouts (#49471) --- homeassistant/components/hangouts/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/hangouts/__init__.py b/homeassistant/components/hangouts/__init__.py index d4892c6689098..04814a9c3e9f2 100644 --- a/homeassistant/components/hangouts/__init__.py +++ b/homeassistant/components/hangouts/__init__.py @@ -125,7 +125,9 @@ async def async_setup_entry(hass, config): bot.async_update_conversation_commands, ) - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, bot.async_handle_hass_stop) + config.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, bot.async_handle_hass_stop) + ) await bot.async_connect() From e288afa7a34a57f3e59d76092d61a2850e7108e0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 20 Apr 2021 06:15:04 -1000 Subject: [PATCH 0405/1317] Fix memory leak in plum_lightpad (#49472) --- homeassistant/components/plum_lightpad/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/plum_lightpad/__init__.py b/homeassistant/components/plum_lightpad/__init__.py index aeabe8634f859..ecc1dacfb2f8f 100644 --- a/homeassistant/components/plum_lightpad/__init__.py +++ b/homeassistant/components/plum_lightpad/__init__.py @@ -76,5 +76,5 @@ def cleanup(event): """Clean up resources.""" plum.cleanup() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, cleanup) + entry.async_on_unload(hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, cleanup)) return True From f600649016e5bffa96906f7f976a1fee49975bec Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 20 Apr 2021 06:15:17 -1000 Subject: [PATCH 0406/1317] Fix memory leak in onvif (#49473) --- homeassistant/components/onvif/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/onvif/__init__.py b/homeassistant/components/onvif/__init__.py index 0eb39064db7cd..46303781673b5 100644 --- a/homeassistant/components/onvif/__init__.py +++ b/homeassistant/components/onvif/__init__.py @@ -93,7 +93,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.config_entries.async_forward_entry_setup(entry, platform) ) - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, device.async_stop) + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, device.async_stop) + ) return True From 3164eef05916604a32e1d818de6cee8bd984eea7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 20 Apr 2021 06:16:17 -1000 Subject: [PATCH 0407/1317] Limit executor jobs during custom_components load to match non-custom behavior (#49451) --- homeassistant/loader.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 492233d8bcaf6..51bd0c2da1f50 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -23,6 +23,7 @@ from homeassistant.generated.mqtt import MQTT from homeassistant.generated.ssdp import SSDP from homeassistant.generated.zeroconf import HOMEKIT, ZEROCONF +from homeassistant.util.async_ import gather_with_concurrency # Typing imports that create a circular dependency if TYPE_CHECKING: @@ -128,13 +129,14 @@ def get_sub_directories(paths: list[str]) -> list[pathlib.Path]: get_sub_directories, custom_components.__path__ ) - integrations = await asyncio.gather( + integrations = await gather_with_concurrency( + MAX_LOAD_CONCURRENTLY, *( hass.async_add_executor_job( Integration.resolve_from_root, hass, custom_components, comp.name ) for comp in dirs - ) + ), ) return { From 20ead7902a343397a1b8347e6f78a8a2a5b2df7b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 20 Apr 2021 06:17:08 -1000 Subject: [PATCH 0408/1317] Fix memory leak in ambient_station on reload (#49455) --- homeassistant/components/ambient_station/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index 9d3359ca98183..4879f68f07998 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -355,7 +355,11 @@ async def async_setup_entry(hass, config_entry): async def _async_disconnect_websocket(*_): await ambient.client.websocket.disconnect() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_disconnect_websocket) + config_entry.async_on_unload( + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, _async_disconnect_websocket + ) + ) return True From 410f0e36049b20fbae5fff8a5e4f52ab64d58b21 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 20 Apr 2021 18:21:38 +0200 Subject: [PATCH 0409/1317] Fix mysensors mqtt integration setup guard (#49423) --- .../components/mysensors/config_flow.py | 15 ++++++++-- homeassistant/components/mysensors/const.py | 1 - homeassistant/components/mysensors/gateway.py | 8 ++++-- .../components/mysensors/strings.json | 5 ++-- .../components/mysensors/translations/en.json | 1 + tests/components/mysensors/conftest.py | 10 +++++++ .../components/mysensors/test_config_flow.py | 28 ++++++++++++++++--- tests/components/mysensors/test_init.py | 4 ++- 8 files changed, 58 insertions(+), 14 deletions(-) create mode 100644 tests/components/mysensors/conftest.py diff --git a/homeassistant/components/mysensors/config_flow.py b/homeassistant/components/mysensors/config_flow.py index bdf1b9392a82e..4fd52f29bf368 100644 --- a/homeassistant/components/mysensors/config_flow.py +++ b/homeassistant/components/mysensors/config_flow.py @@ -14,7 +14,11 @@ import voluptuous as vol from homeassistant import config_entries -from homeassistant.components.mqtt import valid_publish_topic, valid_subscribe_topic +from homeassistant.components.mqtt import ( + DOMAIN as MQTT_DOMAIN, + valid_publish_topic, + valid_subscribe_topic, +) from homeassistant.components.mysensors import ( CONF_DEVICE, DEFAULT_BAUD_RATE, @@ -135,18 +139,23 @@ async def async_step_user(self, user_input: dict[str, str] | None = None): """Create a config entry from frontend user input.""" schema = {vol.Required(CONF_GATEWAY_TYPE): vol.In(CONF_GATEWAY_TYPE_ALL)} schema = vol.Schema(schema) + errors = {} if user_input is not None: gw_type = self._gw_type = user_input[CONF_GATEWAY_TYPE] input_pass = user_input if CONF_DEVICE in user_input else None if gw_type == CONF_GATEWAY_TYPE_MQTT: - return await self.async_step_gw_mqtt(input_pass) + # Naive check that doesn't consider config entry state. + if MQTT_DOMAIN in self.hass.config.components: + return await self.async_step_gw_mqtt(input_pass) + + errors["base"] = "mqtt_required" if gw_type == CONF_GATEWAY_TYPE_TCP: return await self.async_step_gw_tcp(input_pass) if gw_type == CONF_GATEWAY_TYPE_SERIAL: return await self.async_step_gw_serial(input_pass) - return self.async_show_form(step_id="user", data_schema=schema) + return self.async_show_form(step_id="user", data_schema=schema, errors=errors) async def async_step_gw_serial(self, user_input: dict[str, str] | None = None): """Create config entry for a serial gateway.""" diff --git a/homeassistant/components/mysensors/const.py b/homeassistant/components/mysensors/const.py index 7a9027d9b724e..1bd071be9a9ce 100644 --- a/homeassistant/components/mysensors/const.py +++ b/homeassistant/components/mysensors/const.py @@ -29,7 +29,6 @@ CONF_GATEWAY_TYPE_TCP, ] - DOMAIN: str = "mysensors" MYSENSORS_GATEWAY_START_TASK: str = "mysensors_gateway_start_task_{}" MYSENSORS_GATEWAYS: str = "mysensors_gateways" diff --git a/homeassistant/components/mysensors/gateway.py b/homeassistant/components/mysensors/gateway.py index dc6caa939492c..0d800e0215e85 100644 --- a/homeassistant/components/mysensors/gateway.py +++ b/homeassistant/components/mysensors/gateway.py @@ -13,6 +13,7 @@ from mysensors import BaseAsyncGateway, Message, Sensor, mysensors import voluptuous as vol +from homeassistant.components.mqtt import DOMAIN as MQTT_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, callback @@ -163,9 +164,10 @@ async def _get_gateway( persistence_file = hass.config.path(persistence_file) if device == MQTT_COMPONENT: - # what is the purpose of this? - # if not await async_setup_component(hass, MQTT_COMPONENT, entry): - # return None + # Make sure the mqtt integration is set up. + # Naive check that doesn't consider config entry state. + if MQTT_DOMAIN not in hass.config.components: + return None mqtt = hass.components.mqtt def pub_callback(topic, payload, qos, retain): diff --git a/homeassistant/components/mysensors/strings.json b/homeassistant/components/mysensors/strings.json index 43a68f61e247a..54821877b4fd8 100644 --- a/homeassistant/components/mysensors/strings.json +++ b/homeassistant/components/mysensors/strings.json @@ -41,7 +41,7 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "invalid_subscribe_topic": "Invalid subscribe topic", + "invalid_subscribe_topic": "Invalid subscribe topic", "invalid_publish_topic": "Invalid publish topic", "duplicate_topic": "Topic already in use", "same_topic": "Subscribe and publish topics are the same", @@ -52,6 +52,7 @@ "invalid_serial": "Invalid serial port", "invalid_device": "Invalid device", "invalid_version": "Invalid MySensors version", + "mqtt_required": "The MQTT integration is not set up", "not_a_number": "Please enter a number", "port_out_of_range": "Port number must be at least 1 and at most 65535", "unknown": "[%key:common::config_flow::error::unknown%]" @@ -60,7 +61,7 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "invalid_subscribe_topic": "Invalid subscribe topic", + "invalid_subscribe_topic": "Invalid subscribe topic", "invalid_publish_topic": "Invalid publish topic", "duplicate_topic": "Topic already in use", "same_topic": "Subscribe and publish topics are the same", diff --git a/homeassistant/components/mysensors/translations/en.json b/homeassistant/components/mysensors/translations/en.json index 63af85488f0a0..7ca3516e50d1a 100644 --- a/homeassistant/components/mysensors/translations/en.json +++ b/homeassistant/components/mysensors/translations/en.json @@ -33,6 +33,7 @@ "invalid_serial": "Invalid serial port", "invalid_subscribe_topic": "Invalid subscribe topic", "invalid_version": "Invalid MySensors version", + "mqtt_required": "The MQTT integration is not set up", "not_a_number": "Please enter a number", "port_out_of_range": "Port number must be at least 1 and at most 65535", "same_topic": "Subscribe and publish topics are the same", diff --git a/tests/components/mysensors/conftest.py b/tests/components/mysensors/conftest.py new file mode 100644 index 0000000000000..7a4733e8ce28c --- /dev/null +++ b/tests/components/mysensors/conftest.py @@ -0,0 +1,10 @@ +"""Provide common mysensors fixtures.""" +import pytest + +from homeassistant.components.mqtt import DOMAIN as MQTT_DOMAIN + + +@pytest.fixture(name="mqtt") +async def mock_mqtt_fixture(hass): + """Mock the MQTT integration.""" + hass.config.components.add(MQTT_DOMAIN) diff --git a/tests/components/mysensors/test_config_flow.py b/tests/components/mysensors/test_config_flow.py index e4c4016d11ac1..a91159e439562 100644 --- a/tests/components/mysensors/test_config_flow.py +++ b/tests/components/mysensors/test_config_flow.py @@ -50,7 +50,7 @@ async def get_form( return result -async def test_config_mqtt(hass: HomeAssistantType): +async def test_config_mqtt(hass: HomeAssistantType, mqtt: None) -> None: """Test configuring a mqtt gateway.""" step = await get_form(hass, CONF_GATEWAY_TYPE_MQTT, "gw_mqtt") flow_id = step["flow_id"] @@ -88,6 +88,24 @@ async def test_config_mqtt(hass: HomeAssistantType): assert len(mock_setup_entry.mock_calls) == 1 +async def test_missing_mqtt(hass: HomeAssistantType) -> None: + """Test configuring a mqtt gateway without mqtt integration setup.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert not result["errors"] + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_MQTT}, + ) + assert result["step_id"] == "user" + assert result["type"] == "form" + assert result["errors"] == {"base": "mqtt_required"} + + async def test_config_serial(hass: HomeAssistantType): """Test configuring a gateway via serial.""" step = await get_form(hass, CONF_GATEWAY_TYPE_SERIAL, "gw_serial") @@ -348,12 +366,13 @@ async def test_fail_to_connect(hass: HomeAssistantType): ) async def test_config_invalid( hass: HomeAssistantType, + mqtt: config_entries.ConfigEntry, gateway_type: ConfGatewayType, expected_step_id: str, user_input: dict[str, any], err_field, err_string, -): +) -> None: """Perform a test that is expected to generate an error.""" step = await get_form(hass, gateway_type, expected_step_id) flow_id = step["flow_id"] @@ -421,7 +440,7 @@ async def test_config_invalid( }, ], ) -async def test_import(hass: HomeAssistantType, user_input: dict): +async def test_import(hass: HomeAssistantType, mqtt: None, user_input: dict) -> None: """Test importing a gateway.""" await setup.async_setup_component(hass, "persistent_notification", {}) @@ -713,10 +732,11 @@ async def test_import(hass: HomeAssistantType, user_input: dict): ) async def test_duplicate( hass: HomeAssistantType, + mqtt: None, first_input: dict, second_input: dict, expected_result: tuple[str, str] | None, -): +) -> None: """Test duplicate detection.""" await setup.async_setup_component(hass, "persistent_notification", {}) diff --git a/tests/components/mysensors/test_init.py b/tests/components/mysensors/test_init.py index c85c627df9ff0..780621112abab 100644 --- a/tests/components/mysensors/test_init.py +++ b/tests/components/mysensors/test_init.py @@ -227,12 +227,14 @@ ) async def test_import( hass: HomeAssistantType, + mqtt: None, config: ConfigType, expected_calls: int, expected_to_succeed: bool, expected_config_flow_user_input: dict[str, any], -): +) -> None: """Test importing a gateway.""" + await async_setup_component(hass, "persistent_notification", {}) with patch("sys.platform", "win32"), patch( "homeassistant.components.mysensors.config_flow.try_connect", return_value=True ), patch( From 7e7267f8229e8429e09b8177784b5e52b0abef93 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 20 Apr 2021 09:21:52 -0700 Subject: [PATCH 0410/1317] Send only a single event per incoming Google command (#49449) --- .../components/google_assistant/logbook.py | 21 +-- .../components/google_assistant/smart_home.py | 46 +++---- .../google_assistant/test_logbook.py | 30 +++-- .../google_assistant/test_smart_home.py | 121 +++++------------- 4 files changed, 85 insertions(+), 133 deletions(-) diff --git a/homeassistant/components/google_assistant/logbook.py b/homeassistant/components/google_assistant/logbook.py index ef2bccd2c6510..86caa8a9e6c52 100644 --- a/homeassistant/components/google_assistant/logbook.py +++ b/homeassistant/components/google_assistant/logbook.py @@ -1,8 +1,7 @@ """Describe logbook events.""" -from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import callback -from .const import DOMAIN, EVENT_COMMAND_RECEIVED +from .const import DOMAIN, EVENT_COMMAND_RECEIVED, SOURCE_CLOUD COMMON_COMMAND_PREFIX = "action.devices.commands." @@ -14,16 +13,18 @@ def async_describe_events(hass, async_describe_event): @callback def async_describe_logbook_event(event): """Describe a logbook event.""" - entity_id = event.data[ATTR_ENTITY_ID] - state = hass.states.get(entity_id) - name = state.name if state else entity_id + commands = [] - command = event.data["execution"]["command"] - if command.startswith(COMMON_COMMAND_PREFIX): - command = command[len(COMMON_COMMAND_PREFIX) :] + for command_payload in event.data["execution"]: + command = command_payload["command"] + if command.startswith(COMMON_COMMAND_PREFIX): + command = command[len(COMMON_COMMAND_PREFIX) :] + commands.append(command) - message = f"sent command {command} for {name} (via {event.data['source']})" + message = f"sent command {', '.join(commands)}" + if event.data["source"] != SOURCE_CLOUD: + message += f" (via {event.data['source']})" - return {"name": "Google Assistant", "message": message, "entity_id": entity_id} + return {"name": "Google Assistant", "message": message} async_describe_event(DOMAIN, EVENT_COMMAND_RECEIVED, async_describe_logbook_event) diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index a9a97f047e92a..747dc234efe2c 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -116,21 +116,23 @@ async def async_devices_query(hass, data, payload): https://developers.google.com/assistant/smarthome/develop/process-intents#QUERY """ + payload_devices = payload.get("devices", []) + + hass.bus.async_fire( + EVENT_QUERY_RECEIVED, + { + "request_id": data.request_id, + ATTR_ENTITY_ID: [device["id"] for device in payload_devices], + "source": data.source, + }, + context=data.context, + ) + devices = {} - for device in payload.get("devices", []): + for device in payload_devices: devid = device["id"] state = hass.states.get(devid) - hass.bus.async_fire( - EVENT_QUERY_RECEIVED, - { - "request_id": data.request_id, - ATTR_ENTITY_ID: devid, - "source": data.source, - }, - context=data.context, - ) - if not state: # If we can't find a state, the device is offline devices[devid] = {"online": False} @@ -175,20 +177,20 @@ async def handle_devices_execute(hass, data, payload): results = {} for command in payload["commands"]: + hass.bus.async_fire( + EVENT_COMMAND_RECEIVED, + { + "request_id": data.request_id, + ATTR_ENTITY_ID: [device["id"] for device in command["devices"]], + "execution": command["execution"], + "source": data.source, + }, + context=data.context, + ) + for device, execution in product(command["devices"], command["execution"]): entity_id = device["id"] - hass.bus.async_fire( - EVENT_COMMAND_RECEIVED, - { - "request_id": data.request_id, - ATTR_ENTITY_ID: entity_id, - "execution": execution, - "source": data.source, - }, - context=data.context, - ) - # Happens if error occurred. Skip entity for further processing if entity_id in results: continue diff --git a/tests/components/google_assistant/test_logbook.py b/tests/components/google_assistant/test_logbook.py index 4f996ba038f9e..09d0b12e4173e 100644 --- a/tests/components/google_assistant/test_logbook.py +++ b/tests/components/google_assistant/test_logbook.py @@ -32,11 +32,13 @@ async def test_humanify_command_received(hass): EVENT_COMMAND_RECEIVED, { "request_id": "abcd", - ATTR_ENTITY_ID: "light.kitchen", - "execution": { - "command": "action.devices.commands.OnOff", - "params": {"on": True}, - }, + ATTR_ENTITY_ID: ["light.kitchen"], + "execution": [ + { + "command": "action.devices.commands.OnOff", + "params": {"on": True}, + } + ], "source": SOURCE_LOCAL, }, ), @@ -44,11 +46,13 @@ async def test_humanify_command_received(hass): EVENT_COMMAND_RECEIVED, { "request_id": "abcd", - ATTR_ENTITY_ID: "light.non_existing", - "execution": { - "command": "action.devices.commands.OnOff", - "params": {"on": False}, - }, + ATTR_ENTITY_ID: ["light.non_existing"], + "execution": [ + { + "command": "action.devices.commands.OnOff", + "params": {"on": False}, + } + ], "source": SOURCE_CLOUD, }, ), @@ -63,10 +67,8 @@ async def test_humanify_command_received(hass): assert event1["name"] == "Google Assistant" assert event1["domain"] == DOMAIN - assert event1["message"] == "sent command OnOff for The Kitchen Lights (via local)" - assert event1["entity_id"] == "light.kitchen" + assert event1["message"] == "sent command OnOff (via local)" assert event2["name"] == "Google Assistant" assert event2["domain"] == DOMAIN - assert event2["message"] == "sent command OnOff for light.non_existing (via cloud)" - assert event2["entity_id"] == "light.non_existing" + assert event2["message"] == "sent command OnOff" diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 0dfa9e2a5e908..161b4a6f3cbbc 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -353,29 +353,16 @@ async def test_query_message(hass): await hass.async_block_till_done() - assert len(events) == 4 + assert len(events) == 1 assert events[0].event_type == EVENT_QUERY_RECEIVED assert events[0].data == { "request_id": REQ_ID, - "entity_id": "light.demo_light", - "source": "cloud", - } - assert events[1].event_type == EVENT_QUERY_RECEIVED - assert events[1].data == { - "request_id": REQ_ID, - "entity_id": "light.another_light", - "source": "cloud", - } - assert events[2].event_type == EVENT_QUERY_RECEIVED - assert events[2].data == { - "request_id": REQ_ID, - "entity_id": "light.color_temp_light", - "source": "cloud", - } - assert events[3].event_type == EVENT_QUERY_RECEIVED - assert events[3].data == { - "request_id": REQ_ID, - "entity_id": "light.non_existing", + "entity_id": [ + "light.demo_light", + "light.another_light", + "light.color_temp_light", + "light.non_existing", + ], "source": "cloud", } @@ -467,65 +454,25 @@ async def test_execute(hass): }, } - assert len(events) == 6 + assert len(events) == 1 assert events[0].event_type == EVENT_COMMAND_RECEIVED assert events[0].data == { "request_id": REQ_ID, - "entity_id": "light.non_existing", - "execution": { - "command": "action.devices.commands.OnOff", - "params": {"on": True}, - }, - "source": "cloud", - } - assert events[1].event_type == EVENT_COMMAND_RECEIVED - assert events[1].data == { - "request_id": REQ_ID, - "entity_id": "light.non_existing", - "execution": { - "command": "action.devices.commands.BrightnessAbsolute", - "params": {"brightness": 20}, - }, - "source": "cloud", - } - assert events[2].event_type == EVENT_COMMAND_RECEIVED - assert events[2].data == { - "request_id": REQ_ID, - "entity_id": "light.ceiling_lights", - "execution": { - "command": "action.devices.commands.OnOff", - "params": {"on": True}, - }, - "source": "cloud", - } - assert events[3].event_type == EVENT_COMMAND_RECEIVED - assert events[3].data == { - "request_id": REQ_ID, - "entity_id": "light.ceiling_lights", - "execution": { - "command": "action.devices.commands.BrightnessAbsolute", - "params": {"brightness": 20}, - }, - "source": "cloud", - } - assert events[4].event_type == EVENT_COMMAND_RECEIVED - assert events[4].data == { - "request_id": REQ_ID, - "entity_id": "light.kitchen_lights", - "execution": { - "command": "action.devices.commands.OnOff", - "params": {"on": True}, - }, - "source": "cloud", - } - assert events[5].event_type == EVENT_COMMAND_RECEIVED - assert events[5].data == { - "request_id": REQ_ID, - "entity_id": "light.kitchen_lights", - "execution": { - "command": "action.devices.commands.BrightnessAbsolute", - "params": {"brightness": 20}, - }, + "entity_id": [ + "light.non_existing", + "light.ceiling_lights", + "light.kitchen_lights", + ], + "execution": [ + { + "command": "action.devices.commands.OnOff", + "params": {"on": True}, + }, + { + "command": "action.devices.commands.BrightnessAbsolute", + "params": {"brightness": 20}, + }, + ], "source": "cloud", } @@ -543,9 +490,8 @@ async def test_execute(hass): "service": "turn_on", "service_data": {"brightness_pct": 20, "entity_id": "light.ceiling_lights"}, } - assert service_events[0].context == events[2].context - assert service_events[1].context == events[2].context - assert service_events[1].context == events[3].context + assert service_events[0].context == events[0].context + assert service_events[1].context == events[0].context assert service_events[2].data == { "domain": "light", "service": "turn_on", @@ -556,9 +502,8 @@ async def test_execute(hass): "service": "turn_on", "service_data": {"brightness_pct": 20, "entity_id": "light.kitchen_lights"}, } - assert service_events[2].context == events[4].context - assert service_events[3].context == events[4].context - assert service_events[3].context == events[5].context + assert service_events[2].context == events[0].context + assert service_events[3].context == events[0].context async def test_raising_error_trait(hass): @@ -618,11 +563,13 @@ async def test_raising_error_trait(hass): assert events[0].event_type == EVENT_COMMAND_RECEIVED assert events[0].data == { "request_id": REQ_ID, - "entity_id": "climate.bla", - "execution": { - "command": "action.devices.commands.ThermostatTemperatureSetpoint", - "params": {"thermostatTemperatureSetpoint": 10}, - }, + "entity_id": ["climate.bla"], + "execution": [ + { + "command": "action.devices.commands.ThermostatTemperatureSetpoint", + "params": {"thermostatTemperatureSetpoint": 10}, + } + ], "source": "cloud", } From 3b64c574e384dae5868e58ebc9fafd4e0a411a04 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 20 Apr 2021 20:20:57 +0200 Subject: [PATCH 0411/1317] Replace local listener implementation to using config_entry.on_unload in deCONZ (#49494) --- homeassistant/components/deconz/alarm_control_panel.py | 2 +- homeassistant/components/deconz/binary_sensor.py | 2 +- homeassistant/components/deconz/climate.py | 2 +- homeassistant/components/deconz/cover.py | 2 +- homeassistant/components/deconz/deconz_event.py | 2 +- homeassistant/components/deconz/fan.py | 2 +- homeassistant/components/deconz/gateway.py | 5 ----- homeassistant/components/deconz/light.py | 4 ++-- homeassistant/components/deconz/lock.py | 4 ++-- homeassistant/components/deconz/scene.py | 2 +- homeassistant/components/deconz/sensor.py | 2 +- homeassistant/components/deconz/switch.py | 2 +- 12 files changed, 13 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/deconz/alarm_control_panel.py b/homeassistant/components/deconz/alarm_control_panel.py index 4592a8014fc6b..7a6ed19bcd676 100644 --- a/homeassistant/components/deconz/alarm_control_panel.py +++ b/homeassistant/components/deconz/alarm_control_panel.py @@ -62,7 +62,7 @@ def async_add_alarm_control_panel(sensors=gateway.api.sensors.values()) -> None: if entities: async_add_entities(entities) - gateway.listeners.append( + config_entry.async_on_unload( async_dispatcher_connect( hass, gateway.async_signal_new_device(NEW_SENSOR), diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index 99f559eec3d83..7bf05ee6edd84 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -58,7 +58,7 @@ def async_add_sensor(sensors=gateway.api.sensors.values()): if entities: async_add_entities(entities) - gateway.listeners.append( + config_entry.async_on_unload( async_dispatcher_connect( hass, gateway.async_signal_new_device(NEW_SENSOR), async_add_sensor ) diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py index 49f0cc4d1499b..1ef881e9c9033 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -97,7 +97,7 @@ def async_add_climate(sensors=gateway.api.sensors.values()): if entities: async_add_entities(entities) - gateway.listeners.append( + config_entry.async_on_unload( async_dispatcher_connect( hass, gateway.async_signal_new_device(NEW_SENSOR), async_add_climate ) diff --git a/homeassistant/components/deconz/cover.py b/homeassistant/components/deconz/cover.py index 301d17535917e..68fb9527e8710 100644 --- a/homeassistant/components/deconz/cover.py +++ b/homeassistant/components/deconz/cover.py @@ -43,7 +43,7 @@ def async_add_cover(lights=gateway.api.lights.values()): if entities: async_add_entities(entities) - gateway.listeners.append( + config_entry.async_on_unload( async_dispatcher_connect( hass, gateway.async_signal_new_device(NEW_LIGHT), async_add_cover ) diff --git a/homeassistant/components/deconz/deconz_event.py b/homeassistant/components/deconz/deconz_event.py index da80e2e6bf2e2..11dbfa89c908c 100644 --- a/homeassistant/components/deconz/deconz_event.py +++ b/homeassistant/components/deconz/deconz_event.py @@ -65,7 +65,7 @@ def async_add_sensor(sensors=gateway.api.sensors.values()): gateway.hass.async_create_task(new_event.async_update_device_registry()) gateway.events.append(new_event) - gateway.listeners.append( + gateway.config_entry.async_on_unload( async_dispatcher_connect( gateway.hass, gateway.async_signal_new_device(NEW_SENSOR), async_add_sensor ) diff --git a/homeassistant/components/deconz/fan.py b/homeassistant/components/deconz/fan.py index aca92f893c7bd..dfb6802fd758d 100644 --- a/homeassistant/components/deconz/fan.py +++ b/homeassistant/components/deconz/fan.py @@ -45,7 +45,7 @@ def async_add_fan(lights=gateway.api.lights.values()) -> None: if entities: async_add_entities(entities) - gateway.listeners.append( + config_entry.async_on_unload( async_dispatcher_connect( hass, gateway.async_signal_new_device(NEW_LIGHT), async_add_fan ) diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index 93a0befa93704..fa674727a8075 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -53,7 +53,6 @@ def __init__(self, hass, config_entry) -> None: self.deconz_ids = {} self.entities = {} self.events = [] - self.listeners = [] @property def bridgeid(self) -> str: @@ -256,10 +255,6 @@ async def async_reset(self): self.config_entry, platform ) - for unsub_dispatcher in self.listeners: - unsub_dispatcher() - self.listeners = [] - async_unload_events(self) self.deconz_ids = {} diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index f7ae45781acc8..838e7639fc76f 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -61,7 +61,7 @@ def async_add_light(lights=gateway.api.lights.values()): if entities: async_add_entities(entities) - gateway.listeners.append( + config_entry.async_on_unload( async_dispatcher_connect( hass, gateway.async_signal_new_device(NEW_LIGHT), async_add_light ) @@ -87,7 +87,7 @@ def async_add_group(groups=gateway.api.groups.values()): if entities: async_add_entities(entities) - gateway.listeners.append( + config_entry.async_on_unload( async_dispatcher_connect( hass, gateway.async_signal_new_device(NEW_GROUP), async_add_group ) diff --git a/homeassistant/components/deconz/lock.py b/homeassistant/components/deconz/lock.py index 4b6da1e0b9719..6daa6cd153761 100644 --- a/homeassistant/components/deconz/lock.py +++ b/homeassistant/components/deconz/lock.py @@ -26,7 +26,7 @@ def async_add_lock_from_light(lights=gateway.api.lights.values()): if entities: async_add_entities(entities) - gateway.listeners.append( + config_entry.async_on_unload( async_dispatcher_connect( hass, gateway.async_signal_new_device(NEW_LIGHT), async_add_lock_from_light ) @@ -45,7 +45,7 @@ def async_add_lock_from_sensor(sensors=gateway.api.sensors.values()): if entities: async_add_entities(entities) - gateway.listeners.append( + config_entry.async_on_unload( async_dispatcher_connect( hass, gateway.async_signal_new_device(NEW_SENSOR), diff --git a/homeassistant/components/deconz/scene.py b/homeassistant/components/deconz/scene.py index 4fbc1bfe4536e..ecd363f121a23 100644 --- a/homeassistant/components/deconz/scene.py +++ b/homeassistant/components/deconz/scene.py @@ -21,7 +21,7 @@ def async_add_scene(scenes=gateway.api.scenes.values()): if entities: async_add_entities(entities) - gateway.listeners.append( + config_entry.async_on_unload( async_dispatcher_connect( hass, gateway.async_signal_new_device(NEW_SCENE), async_add_scene ) diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 311dd9be82cfa..92686892d6a47 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -117,7 +117,7 @@ def async_add_sensor(sensors=gateway.api.sensors.values()): if entities: async_add_entities(entities) - gateway.listeners.append( + config_entry.async_on_unload( async_dispatcher_connect( hass, gateway.async_signal_new_device(NEW_SENSOR), async_add_sensor ) diff --git a/homeassistant/components/deconz/switch.py b/homeassistant/components/deconz/switch.py index f497e06c7af7c..492872ecca0ac 100644 --- a/homeassistant/components/deconz/switch.py +++ b/homeassistant/components/deconz/switch.py @@ -37,7 +37,7 @@ def async_add_switch(lights=gateway.api.lights.values()): if entities: async_add_entities(entities) - gateway.listeners.append( + config_entry.async_on_unload( async_dispatcher_connect( hass, gateway.async_signal_new_device(NEW_LIGHT), async_add_switch ) From df66f2a9da2a30add9932fa688bdfb42cbb9877a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 20 Apr 2021 08:21:41 -1000 Subject: [PATCH 0412/1317] Cleanup history states tests that were converted to async tests (#49446) --- .../components/history_stats/sensor.py | 4 +- tests/components/history_stats/test_sensor.py | 200 ++++++++++-------- 2 files changed, 110 insertions(+), 94 deletions(-) diff --git a/homeassistant/components/history_stats/sensor.py b/homeassistant/components/history_stats/sensor.py index d6587f435d745..54ff8bf82524f 100644 --- a/homeassistant/components/history_stats/sensor.py +++ b/homeassistant/components/history_stats/sensor.py @@ -108,7 +108,6 @@ def __init__( self, hass, entity_id, entity_states, start, end, duration, sensor_type, name ): """Initialize the HistoryStats sensor.""" - self.hass = hass self._entity_id = entity_id self._entity_states = entity_states self._duration = duration @@ -356,5 +355,4 @@ def handle_template_exception(ex, field): # Common during HA startup - so just a warning _LOGGER.warning(ex) return - _LOGGER.error("Error parsing template for field %s", field) - _LOGGER.error(ex) + _LOGGER.error("Error parsing template for field %s", field, exc_info=ex) diff --git a/tests/components/history_stats/test_sensor.py b/tests/components/history_stats/test_sensor.py index 37dc27e9e91b8..06ba1f22f4704 100644 --- a/tests/components/history_stats/test_sensor.py +++ b/tests/components/history_stats/test_sensor.py @@ -17,7 +17,11 @@ from homeassistant.setup import async_setup_component, setup_component import homeassistant.util.dt as dt_util -from tests.common import get_test_home_assistant, init_recorder_component +from tests.common import ( + async_init_recorder_component, + get_test_home_assistant, + init_recorder_component, +) class TestHistoryStatsSensor(unittest.TestCase): @@ -228,9 +232,7 @@ def init_recorder(self): async def test_reload(hass): """Verify we can reload history_stats sensors.""" - await hass.async_add_executor_job( - init_recorder_component, hass - ) # force in memory db + await async_init_recorder_component(hass) hass.state = ha.CoreState.not_running hass.states.async_set("binary_sensor.test_id", "on") @@ -278,7 +280,9 @@ async def test_reload(hass): async def test_measure_multiple(hass): - """Test the history statistics sensor measure for multiple states.""" + """Test the history statistics sensor measure for multiple .""" + await async_init_recorder_component(hass) + t0 = dt_util.utcnow() - timedelta(minutes=40) t1 = t0 + timedelta(minutes=20) t2 = dt_util.utcnow() - timedelta(minutes=10) @@ -295,70 +299,63 @@ async def test_measure_multiple(hass): ] } - start = Template("{{ as_timestamp(now()) - 3600 }}", hass) - end = Template("{{ now() }}", hass) - - sensor1 = HistoryStatsSensor( - hass, - "input_select.test_id", - ["orange", "blue"], - start, - end, - None, - "time", - "Test", - ) - - sensor2 = HistoryStatsSensor( - hass, - "unknown.id", - ["orange", "blue"], - start, - end, - None, - "time", - "Test", - ) - - sensor3 = HistoryStatsSensor( - hass, - "input_select.test_id", - ["orange", "blue"], - start, - end, - None, - "count", - "test", - ) - - sensor4 = HistoryStatsSensor( + await async_setup_component( hass, - "input_select.test_id", - ["orange", "blue"], - start, - end, - None, - "ratio", - "test", + "sensor", + { + "sensor": [ + { + "platform": "history_stats", + "entity_id": "input_select.test_id", + "name": "sensor1", + "state": ["orange", "blue"], + "start": "{{ as_timestamp(now()) - 3600 }}", + "end": "{{ now() }}", + "type": "time", + }, + { + "platform": "history_stats", + "entity_id": "unknown.test_id", + "name": "sensor2", + "state": ["orange", "blue"], + "start": "{{ as_timestamp(now()) - 3600 }}", + "end": "{{ now() }}", + "type": "time", + }, + { + "platform": "history_stats", + "entity_id": "input_select.test_id", + "name": "sensor3", + "state": ["orange", "blue"], + "start": "{{ as_timestamp(now()) - 3600 }}", + "end": "{{ now() }}", + "type": "count", + }, + { + "platform": "history_stats", + "entity_id": "input_select.test_id", + "name": "sensor4", + "state": ["orange", "blue"], + "start": "{{ as_timestamp(now()) - 3600 }}", + "end": "{{ now() }}", + "type": "ratio", + }, + ] + }, ) - assert sensor1._type == "time" - assert sensor3._type == "count" - assert sensor4._type == "ratio" - with patch( "homeassistant.components.history.state_changes_during_period", return_value=fake_states, ), patch("homeassistant.components.history.get_state", return_value=None): - await sensor1.async_update() - await sensor2.async_update() - await sensor3.async_update() - await sensor4.async_update() + for i in range(1, 5): + await hass.helpers.entity_component.async_update_entity(f"sensor.sensor{i}") + await hass.async_block_till_done() - assert sensor1.state == 0.5 - assert sensor2.state is None - assert sensor3.state == 2 - assert sensor4.state == 50 + assert hass.states.get("sensor.sensor1").state == "0.5" + assert hass.states.get("sensor.sensor2").state == STATE_UNKNOWN + assert hass.states.get("sensor.sensor3").state == "2" + assert hass.states.get("sensor.sensor4").state == "50.0" async def async_test_measure(hass): @@ -379,42 +376,63 @@ async def async_test_measure(hass): ] } - start = Template("{{ as_timestamp(now()) - 3600 }}", hass) - end = Template("{{ now() }}", hass) - - sensor1 = HistoryStatsSensor( - hass, "binary_sensor.test_id", "on", start, end, None, "time", "Test" - ) - - sensor2 = HistoryStatsSensor( - hass, "unknown.id", "on", start, end, None, "time", "Test" - ) - - sensor3 = HistoryStatsSensor( - hass, "binary_sensor.test_id", "on", start, end, None, "count", "test" - ) - - sensor4 = HistoryStatsSensor( - hass, "binary_sensor.test_id", "on", start, end, None, "ratio", "test" + await async_setup_component( + hass, + "sensor", + { + "sensor": [ + { + "platform": "history_stats", + "entity_id": "binary_sensor.test_id", + "name": "sensor1", + "state": "on", + "start": "{{ as_timestamp(now()) - 3600 }}", + "end": "{{ now() }}", + "type": "time", + }, + { + "platform": "history_stats", + "entity_id": "binary_sensor.test_id", + "name": "sensor2", + "state": "on", + "start": "{{ as_timestamp(now()) - 3600 }}", + "end": "{{ now() }}", + "type": "time", + }, + { + "platform": "history_stats", + "entity_id": "binary_sensor.test_id", + "name": "sensor3", + "state": "on", + "start": "{{ as_timestamp(now()) - 3600 }}", + "end": "{{ now() }}", + "type": "count", + }, + { + "platform": "history_stats", + "entity_id": "binary_sensor.test_id", + "name": "sensor4", + "state": "on", + "start": "{{ as_timestamp(now()) - 3600 }}", + "end": "{{ now() }}", + "type": "ratio", + }, + ] + }, ) - assert sensor1._type == "time" - assert sensor3._type == "count" - assert sensor4._type == "ratio" - with patch( "homeassistant.components.history.state_changes_during_period", return_value=fake_states, ), patch("homeassistant.components.history.get_state", return_value=None): - await sensor1.async_update() - await sensor2.async_update() - await sensor3.async_update() - await sensor4.async_update() - - assert sensor1.state == 0.5 - assert sensor2.state is None - assert sensor3.state == 2 - assert sensor4.state == 50 + for i in range(1, 5): + await hass.helpers.entity_component.async_update_entity(f"sensor.sensor{i}") + await hass.async_block_till_done() + + assert hass.states.get("sensor.sensor1").state == "0.5" + assert hass.states.get("sensor.sensor2").state == STATE_UNKNOWN + assert hass.states.get("sensor.sensor3").state == "2" + assert hass.states.get("sensor.sensor4").state == "50.0" def _get_fixtures_base_path(): From f73d9fa572096d88cfcc2fae368068b5bc9d1011 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 20 Apr 2021 08:22:10 -1000 Subject: [PATCH 0413/1317] Reduce broadlink executor jobs at setup time (#49447) Co-authored-by: Martin Hjelmare Co-authored-by: Paulus Schoutsen --- homeassistant/components/broadlink/device.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/broadlink/device.py b/homeassistant/components/broadlink/device.py index 5b42205993c3a..fd9c6dcd9d30c 100644 --- a/homeassistant/components/broadlink/device.py +++ b/homeassistant/components/broadlink/device.py @@ -63,6 +63,13 @@ async def async_update(hass, entry): device_registry.async_update_device(device_entry.id, name=entry.title) await hass.config_entries.async_reload(entry.entry_id) + def _auth_fetch_firmware(self): + """Auth and fetch firmware.""" + self.api.auth() + with suppress(BroadlinkException, OSError): + return self.api.get_fwversion() + return None + async def async_setup(self): """Set up the device and related entities.""" config = self.config @@ -77,7 +84,9 @@ async def async_setup(self): self.api = api try: - await self.hass.async_add_executor_job(api.auth) + self.fw_version = await self.hass.async_add_executor_job( + self._auth_fetch_firmware + ) except AuthenticationError: await self._async_handle_auth_error() @@ -102,9 +111,6 @@ async def async_setup(self): self.hass.data[DOMAIN].devices[config.entry_id] = self self.reset_jobs.append(config.add_update_listener(self.async_update)) - with suppress(BroadlinkException, OSError): - self.fw_version = await self.hass.async_add_executor_job(api.get_fwversion) - # Forward entry setup to related domains. tasks = ( self.hass.config_entries.async_forward_entry_setup(config, domain) From d24b3e0a3c074b438f6c96700e1f510098387b50 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Tue, 20 Apr 2021 20:25:37 +0200 Subject: [PATCH 0414/1317] Test pymodbus (#49053) --- tests/components/modbus/test_init.py | 144 +++++++++++++++++++++++++ tests/components/modbus/test_modbus.py | 72 ------------- 2 files changed, 144 insertions(+), 72 deletions(-) delete mode 100644 tests/components/modbus/test_modbus.py diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index cd8edb656a091..393a9ce86da0d 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -1,8 +1,28 @@ """The tests for the Modbus init.""" +import logging +from unittest import mock + import pytest import voluptuous as vol from homeassistant.components.modbus import number +from homeassistant.components.modbus.const import ( + CONF_BAUDRATE, + CONF_BYTESIZE, + CONF_PARITY, + CONF_STOPBITS, + MODBUS_DOMAIN as DOMAIN, +) +from homeassistant.const import ( + CONF_DELAY, + CONF_HOST, + CONF_METHOD, + CONF_NAME, + CONF_PORT, + CONF_TIMEOUT, + CONF_TYPE, +) +from homeassistant.setup import async_setup_component @pytest.mark.parametrize( @@ -33,3 +53,127 @@ async def test_number_exception(): return pytest.fail("Number not throwing exception") + + +async def _config_helper(hass, do_config): + """Run test for modbus.""" + + config = {DOMAIN: do_config} + + with mock.patch( + "homeassistant.components.modbus.modbus.ModbusTcpClient" + ), mock.patch( + "homeassistant.components.modbus.modbus.ModbusSerialClient" + ), mock.patch( + "homeassistant.components.modbus.modbus.ModbusUdpClient" + ): + assert await async_setup_component(hass, DOMAIN, config) is True + await hass.async_block_till_done() + + +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_TYPE: "tcp", + CONF_HOST: "modbusTestHost", + CONF_PORT: 5501, + }, + { + CONF_TYPE: "tcp", + CONF_HOST: "modbusTestHost", + CONF_PORT: 5501, + CONF_NAME: "modbusTest", + CONF_TIMEOUT: 30, + CONF_DELAY: 10, + }, + { + CONF_TYPE: "udp", + CONF_HOST: "modbusTestHost", + CONF_PORT: 5501, + }, + { + CONF_TYPE: "udp", + CONF_HOST: "modbusTestHost", + CONF_PORT: 5501, + CONF_NAME: "modbusTest", + CONF_TIMEOUT: 30, + CONF_DELAY: 10, + }, + { + CONF_TYPE: "rtuovertcp", + CONF_HOST: "modbusTestHost", + CONF_PORT: 5501, + }, + { + CONF_TYPE: "rtuovertcp", + CONF_HOST: "modbusTestHost", + CONF_PORT: 5501, + CONF_NAME: "modbusTest", + CONF_TIMEOUT: 30, + CONF_DELAY: 10, + }, + { + CONF_TYPE: "serial", + CONF_BAUDRATE: 9600, + CONF_BYTESIZE: 8, + CONF_METHOD: "rtu", + CONF_PORT: "usb01", + CONF_PARITY: "E", + CONF_STOPBITS: 1, + }, + { + CONF_TYPE: "serial", + CONF_BAUDRATE: 9600, + CONF_BYTESIZE: 8, + CONF_METHOD: "rtu", + CONF_PORT: "usb01", + CONF_PARITY: "E", + CONF_STOPBITS: 1, + CONF_NAME: "modbusTest", + CONF_TIMEOUT: 30, + CONF_DELAY: 10, + }, + ], +) +async def test_config_modbus(hass, caplog, do_config): + """Run test for modbus.""" + + caplog.set_level(logging.ERROR) + await _config_helper(hass, do_config) + assert DOMAIN in hass.config.components + assert len(caplog.records) == 0 + + +async def test_config_multiple_modbus(hass, caplog): + """Run test for multiple modbus.""" + + do_config = [ + { + CONF_TYPE: "tcp", + CONF_HOST: "modbusTestHost", + CONF_PORT: 5501, + CONF_NAME: "modbusTest1", + }, + { + CONF_TYPE: "tcp", + CONF_HOST: "modbusTestHost", + CONF_PORT: 5501, + CONF_NAME: "modbusTest2", + }, + { + CONF_TYPE: "serial", + CONF_BAUDRATE: 9600, + CONF_BYTESIZE: 8, + CONF_METHOD: "rtu", + CONF_PORT: "usb01", + CONF_PARITY: "E", + CONF_STOPBITS: 1, + CONF_NAME: "modbusTest3", + }, + ] + + caplog.set_level(logging.ERROR) + await _config_helper(hass, do_config) + assert DOMAIN in hass.config.components + assert len(caplog.records) == 0 diff --git a/tests/components/modbus/test_modbus.py b/tests/components/modbus/test_modbus.py deleted file mode 100644 index 708519bbb440d..0000000000000 --- a/tests/components/modbus/test_modbus.py +++ /dev/null @@ -1,72 +0,0 @@ -"""The tests for the Modbus sensor component.""" -import pytest - -from homeassistant.components.modbus.const import ( - CONF_BAUDRATE, - CONF_BYTESIZE, - CONF_PARITY, - CONF_STOPBITS, - MODBUS_DOMAIN as DOMAIN, -) -from homeassistant.const import ( - CONF_DELAY, - CONF_HOST, - CONF_METHOD, - CONF_NAME, - CONF_PORT, - CONF_TIMEOUT, - CONF_TYPE, -) - -from .conftest import base_config_test - - -@pytest.mark.parametrize("do_discovery", [False, True]) -@pytest.mark.parametrize( - "do_options", - [ - {}, - { - CONF_NAME: "modbusTest", - CONF_TIMEOUT: 30, - CONF_DELAY: 10, - }, - ], -) -@pytest.mark.parametrize( - "do_config", - [ - { - CONF_TYPE: "tcp", - CONF_HOST: "modbusTestHost", - CONF_PORT: 5501, - }, - { - CONF_TYPE: "serial", - CONF_BAUDRATE: 9600, - CONF_BYTESIZE: 8, - CONF_METHOD: "rtu", - CONF_PORT: "usb01", - CONF_PARITY: "E", - CONF_STOPBITS: 1, - }, - ], -) -async def test_config_modbus(hass, do_discovery, do_options, do_config): - """Run test for modbus.""" - config = { - DOMAIN: { - **do_config, - **do_options, - } - } - await base_config_test( - hass, - None, - "", - DOMAIN, - None, - None, - method_discovery=do_discovery, - config_modbus=config, - ) From 45b6dfce68aa68074d063aa77c9e07e97b22d766 Mon Sep 17 00:00:00 2001 From: Caleb Mah Date: Wed, 21 Apr 2021 02:26:42 +0800 Subject: [PATCH 0415/1317] Bump yeelight dependency to 0.6.1 (#49490) --- homeassistant/components/yeelight/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 845d9314bda46..bfb195b91fc4e 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -2,7 +2,7 @@ "domain": "yeelight", "name": "Yeelight", "documentation": "https://www.home-assistant.io/integrations/yeelight", - "requirements": ["yeelight==0.6.0"], + "requirements": ["yeelight==0.6.1"], "codeowners": ["@rytilahti", "@zewelor", "@shenxn"], "config_flow": true, "iot_class": "local_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 7ceb300cc23a5..3dc929f4f38a3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2363,7 +2363,7 @@ yalesmartalarmclient==0.1.6 yalexs==1.1.10 # homeassistant.components.yeelight -yeelight==0.6.0 +yeelight==0.6.1 # homeassistant.components.yeelightsunflower yeelightsunflower==0.0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 64da38880f8d2..eade0d39106a5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1248,7 +1248,7 @@ xmltodict==0.12.0 yalexs==1.1.10 # homeassistant.components.yeelight -yeelight==0.6.0 +yeelight==0.6.1 # homeassistant.components.onvif zeep[async]==4.0.0 From 138226fa14c6e1e4185e99c219fcc4720b1e54d5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 20 Apr 2021 08:49:58 -1000 Subject: [PATCH 0416/1317] Bump codecov to 1.4.1 (#49497) --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 16665acc9cb08..0531d8555cac5 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -739,4 +739,4 @@ jobs: coverage report --fail-under=94 coverage xml - name: Upload coverage to Codecov - uses: codecov/codecov-action@v1.4.0 + uses: codecov/codecov-action@v1.4.1 From 63616a9e36e1780ddf1a7dd304a687cb39d6c777 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 20 Apr 2021 20:50:42 +0200 Subject: [PATCH 0417/1317] Use config_entry.on_unload rather than local listener implementation in UniFi (#49496) --- homeassistant/components/unifi/controller.py | 5 ----- homeassistant/components/unifi/device_tracker.py | 4 +++- homeassistant/components/unifi/sensor.py | 4 +++- homeassistant/components/unifi/switch.py | 4 +++- tests/components/unifi/test_controller.py | 3 --- 5 files changed, 9 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index e2ad9636d7acc..0d8848e29205b 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -101,7 +101,6 @@ def __init__(self, hass, config_entry): self.progress = None self.wireless_clients = None - self.listeners = [] self.site_id: str = "" self._site_name = None self._site_role = None @@ -466,10 +465,6 @@ async def async_reset(self): if not unload_ok: return False - for unsub_dispatcher in self.listeners: - unsub_dispatcher() - self.listeners = [] - if self._cancel_heartbeat_check: self._cancel_heartbeat_check() self._cancel_heartbeat_check = None diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index 9842184e2ee59..649636434473e 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -85,7 +85,9 @@ def items_added( add_device_entities(controller, async_add_entities, devices) for signal in (controller.signal_update, controller.signal_options_update): - controller.listeners.append(async_dispatcher_connect(hass, signal, items_added)) + config_entry.async_on_unload( + async_dispatcher_connect(hass, signal, items_added) + ) items_added() diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index 755d95a061bbe..c823860285664 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -41,7 +41,9 @@ def items_added( add_uptime_entities(controller, async_add_entities, clients) for signal in (controller.signal_update, controller.signal_options_update): - controller.listeners.append(async_dispatcher_connect(hass, signal, items_added)) + config_entry.async_on_unload( + async_dispatcher_connect(hass, signal, items_added) + ) items_added() diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index f04acaaec872e..59e8c9fa149d3 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -86,7 +86,9 @@ def items_added( add_dpi_entities(controller, async_add_entities, dpi_groups) for signal in (controller.signal_update, controller.signal_options_update): - controller.listeners.append(async_dispatcher_connect(hass, signal, items_added)) + config_entry.async_on_unload( + async_dispatcher_connect(hass, signal, items_added) + ) items_added() known_poe_clients.clear() diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index 50d464d23c0e1..ec666ff27b948 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -313,13 +313,10 @@ async def test_reset_after_successful_setup(hass, aioclient_mock): config_entry = await setup_unifi_integration(hass, aioclient_mock) controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] - assert len(controller.listeners) == 6 - result = await controller.async_reset() await hass.async_block_till_done() assert result is True - assert len(controller.listeners) == 0 async def test_reset_fails(hass, aioclient_mock): From 12a9695798e8bb975ef361877d0a7775feb7fc7d Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 20 Apr 2021 20:53:05 +0200 Subject: [PATCH 0418/1317] Use config_entry.on_unload rather than local listener implementation in Axis (#49495) --- homeassistant/components/axis/__init__.py | 2 +- homeassistant/components/axis/binary_sensor.py | 2 +- homeassistant/components/axis/device.py | 7 +------ homeassistant/components/axis/light.py | 2 +- homeassistant/components/axis/switch.py | 2 +- 5 files changed, 5 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/axis/__init__.py b/homeassistant/components/axis/__init__.py index acbdc2ca78254..e3c4d20fc045d 100644 --- a/homeassistant/components/axis/__init__.py +++ b/homeassistant/components/axis/__init__.py @@ -26,7 +26,7 @@ async def async_setup_entry(hass, config_entry): await device.async_update_device_registry() - device.listeners.append( + config_entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, device.shutdown) ) diff --git a/homeassistant/components/axis/binary_sensor.py b/homeassistant/components/axis/binary_sensor.py index 32d4afa328d8d..222a356d4f98d 100644 --- a/homeassistant/components/axis/binary_sensor.py +++ b/homeassistant/components/axis/binary_sensor.py @@ -53,7 +53,7 @@ def async_add_sensor(event_id): ): async_add_entities([AxisBinarySensor(event, device)]) - device.listeners.append( + config_entry.async_on_unload( async_dispatcher_connect(hass, device.signal_new_event, async_add_sensor) ) diff --git a/homeassistant/components/axis/device.py b/homeassistant/components/axis/device.py index b2af9e0efc6c1..cc9922b290c10 100644 --- a/homeassistant/components/axis/device.py +++ b/homeassistant/components/axis/device.py @@ -58,8 +58,6 @@ def __init__(self, hass, config_entry): self.fw_version = None self.product_type = None - self.listeners = [] - @property def host(self): """Return the host address of this device.""" @@ -190,7 +188,7 @@ async def use_mqtt(self, hass: HomeAssistant, component: str) -> None: status = {} if status.get("data", {}).get("status", {}).get("state") == "active": - self.listeners.append( + self.config_entry.async_on_unload( await mqtt.async_subscribe( hass, f"{self.api.vapix.serial_number}/#", self.mqtt_message ) @@ -279,9 +277,6 @@ async def async_reset(self): if not unload_ok: return False - for unsubscribe_listener in self.listeners: - unsubscribe_listener() - return True diff --git a/homeassistant/components/axis/light.py b/homeassistant/components/axis/light.py index 75a42b13cbfb6..e627d6ccdbdba 100644 --- a/homeassistant/components/axis/light.py +++ b/homeassistant/components/axis/light.py @@ -32,7 +32,7 @@ def async_add_sensor(event_id): if event.CLASS == CLASS_LIGHT and event.TYPE == "Light": async_add_entities([AxisLight(event, device)]) - device.listeners.append( + config_entry.async_on_unload( async_dispatcher_connect(hass, device.signal_new_event, async_add_sensor) ) diff --git a/homeassistant/components/axis/switch.py b/homeassistant/components/axis/switch.py index f3436b3eb8322..e509716fc1ff0 100644 --- a/homeassistant/components/axis/switch.py +++ b/homeassistant/components/axis/switch.py @@ -22,7 +22,7 @@ def async_add_switch(event_id): if event.CLASS == CLASS_OUTPUT: async_add_entities([AxisSwitch(event, device)]) - device.listeners.append( + config_entry.async_on_unload( async_dispatcher_connect(hass, device.signal_new_event, async_add_switch) ) From d517d7232f8ab712d2c012521ebe65b033467f65 Mon Sep 17 00:00:00 2001 From: dfigus Date: Tue, 20 Apr 2021 22:06:00 +0200 Subject: [PATCH 0419/1317] Fix HmIP-HAP attributes unit (#49476) --- homeassistant/components/homematic/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/homematic/sensor.py b/homeassistant/components/homematic/sensor.py index 4525d5a48fcbf..964ba15cd0abb 100644 --- a/homeassistant/components/homematic/sensor.py +++ b/homeassistant/components/homematic/sensor.py @@ -67,6 +67,8 @@ "FREQUENCY": FREQUENCY_HERTZ, "VALUE": "#", "VALVE_STATE": PERCENTAGE, + "CARRIER_SENSE_LEVEL": PERCENTAGE, + "DUTY_CYCLE_LEVEL": PERCENTAGE, } HM_DEVICE_CLASS_HA_CAST = { From ccda903c17ffd61270742cc3e80c51783bf72b17 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Tue, 20 Apr 2021 13:08:08 -0700 Subject: [PATCH 0420/1317] Upgrade to the latest hyperion-py (#49448) --- homeassistant/components/hyperion/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hyperion/manifest.json b/homeassistant/components/hyperion/manifest.json index 08b852f5302eb..4f247b3e937c2 100644 --- a/homeassistant/components/hyperion/manifest.json +++ b/homeassistant/components/hyperion/manifest.json @@ -5,7 +5,7 @@ "domain": "hyperion", "name": "Hyperion", "quality_scale": "platinum", - "requirements": ["hyperion-py==0.7.2"], + "requirements": ["hyperion-py==0.7.4"], "ssdp": [ { "manufacturer": "Hyperion Open Source Ambient Lighting", diff --git a/requirements_all.txt b/requirements_all.txt index 3dc929f4f38a3..47c2b9cdb9a68 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -793,7 +793,7 @@ huisbaasje-client==0.1.0 hydrawiser==0.2 # homeassistant.components.hyperion -hyperion-py==0.7.2 +hyperion-py==0.7.4 # homeassistant.components.bh1750 # homeassistant.components.bme280 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index eade0d39106a5..fa55cbca22260 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -448,7 +448,7 @@ huawei-lte-api==1.4.17 huisbaasje-client==0.1.0 # homeassistant.components.hyperion -hyperion-py==0.7.2 +hyperion-py==0.7.4 # homeassistant.components.iaqualink iaqualink==0.3.4 From 1c587d2e473e5445fa949744d6f965d0a7e9bcec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Tue, 20 Apr 2021 23:38:07 +0300 Subject: [PATCH 0421/1317] Fix and add some ScannerEntity property type hints (#49500) --- .../components/device_tracker/config_entry.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/device_tracker/config_entry.py b/homeassistant/components/device_tracker/config_entry.py index 05fa4b4a60d7f..9a8c77686a16f 100644 --- a/homeassistant/components/device_tracker/config_entry.py +++ b/homeassistant/components/device_tracker/config_entry.py @@ -134,29 +134,29 @@ class ScannerEntity(BaseTrackerEntity): """Base class for a tracked device that is on a scanned network.""" @property - def ip_address(self) -> str: + def ip_address(self) -> str | None: """Return the primary ip address of the device.""" return None @property - def mac_address(self) -> str: + def mac_address(self) -> str | None: """Return the mac address of the device.""" return None @property - def hostname(self) -> str: + def hostname(self) -> str | None: """Return hostname of the device.""" return None @property - def state(self): + def state(self) -> str: """Return the state of the device.""" if self.is_connected: return STATE_HOME return STATE_NOT_HOME @property - def is_connected(self): + def is_connected(self) -> bool: """Return true if the device is connected to the network.""" raise NotImplementedError From 208a17d0dc0a30598059ab0a70ab10d1b14f64e1 Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Tue, 20 Apr 2021 22:38:54 +0200 Subject: [PATCH 0422/1317] Add additional device classes to devolo Home Control (#49425) --- homeassistant/components/devolo_home_control/binary_sensor.py | 4 ++++ homeassistant/components/devolo_home_control/sensor.py | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/devolo_home_control/binary_sensor.py b/homeassistant/components/devolo_home_control/binary_sensor.py index c8007792857f7..e99c96832ae23 100644 --- a/homeassistant/components/devolo_home_control/binary_sensor.py +++ b/homeassistant/components/devolo_home_control/binary_sensor.py @@ -4,6 +4,8 @@ DEVICE_CLASS_HEAT, DEVICE_CLASS_MOISTURE, DEVICE_CLASS_MOTION, + DEVICE_CLASS_PROBLEM, + DEVICE_CLASS_SAFETY, DEVICE_CLASS_SMOKE, BinarySensorEntity, ) @@ -19,6 +21,7 @@ "Smoke Alarm": DEVICE_CLASS_SMOKE, "Heat Alarm": DEVICE_CLASS_HEAT, "door": DEVICE_CLASS_DOOR, + "overload": DEVICE_CLASS_SAFETY, } @@ -84,6 +87,7 @@ def __init__(self, homecontrol, device_instance, element_uid): self._value = self._binary_sensor_property.state if element_uid.startswith("devolo.WarningBinaryFI:"): + self._device_class = DEVICE_CLASS_PROBLEM self._enabled_default = False @property diff --git a/homeassistant/components/devolo_home_control/sensor.py b/homeassistant/components/devolo_home_control/sensor.py index 041eb7cae38e0..e309130537547 100644 --- a/homeassistant/components/devolo_home_control/sensor.py +++ b/homeassistant/components/devolo_home_control/sensor.py @@ -1,6 +1,7 @@ """Platform for sensor integration.""" from homeassistant.components.sensor import ( DEVICE_CLASS_BATTERY, + DEVICE_CLASS_ENERGY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_POWER, @@ -21,7 +22,7 @@ "light": DEVICE_CLASS_ILLUMINANCE, "humidity": DEVICE_CLASS_HUMIDITY, "current": DEVICE_CLASS_POWER, - "total": DEVICE_CLASS_POWER, + "total": DEVICE_CLASS_ENERGY, "voltage": DEVICE_CLASS_VOLTAGE, } From cf16e651cf3a31905343ef8f64c7069902bf7ff9 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 20 Apr 2021 17:44:26 -0400 Subject: [PATCH 0423/1317] Bump zwave_js dependency to 0.24.0 (#49445) * Bump zwave_js dependency to 0.24.0 * fix bug in schema * fix test --- homeassistant/components/zwave_js/manifest.json | 2 +- homeassistant/components/zwave_js/services.py | 10 ++++------ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/zwave_js/test_api.py | 6 +++--- 5 files changed, 10 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 0b780ef7da40c..e730b6ae9dbd4 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave JS", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave_js", - "requirements": ["zwave-js-server-python==0.23.1"], + "requirements": ["zwave-js-server-python==0.24.0"], "codeowners": ["@home-assistant/z-wave"], "dependencies": ["http", "websocket_api"], "iot_class": "local_push" diff --git a/homeassistant/components/zwave_js/services.py b/homeassistant/components/zwave_js/services.py index 513abd97318c1..16bf9c7eb94b7 100644 --- a/homeassistant/components/zwave_js/services.py +++ b/homeassistant/components/zwave_js/services.py @@ -94,15 +94,13 @@ def async_register(self) -> None: { vol.Optional(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]), vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(const.ATTR_CONFIG_PARAMETER): vol.Any( - vol.Coerce(int), cv.string - ), + vol.Required(const.ATTR_CONFIG_PARAMETER): vol.Coerce(int), vol.Required(const.ATTR_CONFIG_VALUE): vol.Any( vol.Coerce(int), { - vol.Any(vol.Coerce(int), BITMASK_SCHEMA): vol.Any( - vol.Coerce(int), cv.string - ) + vol.Any( + vol.Coerce(int), BITMASK_SCHEMA, cv.string + ): vol.Any(vol.Coerce(int), cv.string) }, ), }, diff --git a/requirements_all.txt b/requirements_all.txt index 47c2b9cdb9a68..644682ad29eae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2411,4 +2411,4 @@ zigpy==0.33.0 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.23.1 +zwave-js-server-python==0.24.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fa55cbca22260..f90dcf3ff6968 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1278,4 +1278,4 @@ zigpy-znp==0.4.0 zigpy==0.33.0 # homeassistant.components.zwave_js -zwave-js-server-python==0.23.1 +zwave-js-server-python==0.24.0 diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index eb198b01f82a6..ee718020b7aea 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -419,7 +419,7 @@ async def test_update_log_config(hass, client, integration, hass_ws_client): assert len(client.async_send_command.call_args_list) == 1 args = client.async_send_command.call_args[0][0] - assert args["command"] == "update_log_config" + assert args["command"] == "driver.update_log_config" assert args["config"] == {"level": "error"} client.async_send_command.reset_mock() @@ -439,7 +439,7 @@ async def test_update_log_config(hass, client, integration, hass_ws_client): assert len(client.async_send_command.call_args_list) == 1 args = client.async_send_command.call_args[0][0] - assert args["command"] == "update_log_config" + assert args["command"] == "driver.update_log_config" assert args["config"] == {"logToFile": True, "filename": "/test"} client.async_send_command.reset_mock() @@ -465,7 +465,7 @@ async def test_update_log_config(hass, client, integration, hass_ws_client): assert len(client.async_send_command.call_args_list) == 1 args = client.async_send_command.call_args[0][0] - assert args["command"] == "update_log_config" + assert args["command"] == "driver.update_log_config" assert args["config"] == { "level": "error", "logToFile": True, From c825f88888d670045d34f88f58322d3ec4339072 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 21 Apr 2021 01:26:09 +0300 Subject: [PATCH 0424/1317] Support wired clients in Huawei LTE device tracker (#48987) --- .../components/huawei_lte/__init__.py | 8 +- .../components/huawei_lte/config_flow.py | 8 + homeassistant/components/huawei_lte/const.py | 9 +- .../components/huawei_lte/device_tracker.py | 139 ++++++++++++++---- .../components/huawei_lte/strings.json | 3 +- 5 files changed, 137 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index 8a729b3c38ce7..ea3a909206d0b 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -64,6 +64,7 @@ KEY_DEVICE_INFORMATION, KEY_DEVICE_SIGNAL, KEY_DIALUP_MOBILE_DATASWITCH, + KEY_LAN_HOST_INFO, KEY_MONITORING_CHECK_NOTIFICATIONS, KEY_MONITORING_MONTH_STATISTICS, KEY_MONITORING_STATUS, @@ -130,6 +131,7 @@ class Router: """Class for router state.""" + config_entry: ConfigEntry = attr.ib() connection: Connection = attr.ib() url: str = attr.ib() mac: str = attr.ib() @@ -261,6 +263,10 @@ def update(self) -> None: self._get_data(KEY_NET_CURRENT_PLMN, self.client.net.current_plmn) self._get_data(KEY_NET_NET_MODE, self.client.net.net_mode) self._get_data(KEY_SMS_SMS_COUNT, self.client.sms.sms_count) + self._get_data(KEY_LAN_HOST_INFO, self.client.lan.host_info) + if self.data.get(KEY_LAN_HOST_INFO): + # LAN host info includes everything in WLAN host list + self.subscriptions.pop(KEY_WLAN_HOST_LIST, None) self._get_data(KEY_WLAN_HOST_LIST, self.client.wlan.host_list) self._get_data( KEY_WLAN_WIFI_FEATURE_SWITCH, self.client.wlan.wifi_feature_switch @@ -382,7 +388,7 @@ def signal_update() -> None: raise ConfigEntryNotReady from ex # Set up router and store reference to it - router = Router(connection, url, mac, signal_update) + router = Router(config_entry, connection, url, mac, signal_update) hass.data[DOMAIN].routers[url] = router # Do initial data update diff --git a/homeassistant/components/huawei_lte/config_flow.py b/homeassistant/components/huawei_lte/config_flow.py index cfd197e151507..c95131308d6ef 100644 --- a/homeassistant/components/huawei_lte/config_flow.py +++ b/homeassistant/components/huawei_lte/config_flow.py @@ -33,9 +33,11 @@ from homeassistant.helpers.typing import DiscoveryInfoType from .const import ( + CONF_TRACK_WIRED_CLIENTS, CONNECTION_TIMEOUT, DEFAULT_DEVICE_NAME, DEFAULT_NOTIFY_SERVICE_NAME, + DEFAULT_TRACK_WIRED_CLIENTS, DOMAIN, ) @@ -284,6 +286,12 @@ async def async_step_init( self.config_entry.options.get(CONF_RECIPIENT, []) ), ): str, + vol.Optional( + CONF_TRACK_WIRED_CLIENTS, + default=self.config_entry.options.get( + CONF_TRACK_WIRED_CLIENTS, DEFAULT_TRACK_WIRED_CLIENTS + ), + ): bool, } ) return self.async_show_form(step_id="init", data_schema=data_schema) diff --git a/homeassistant/components/huawei_lte/const.py b/homeassistant/components/huawei_lte/const.py index 519da09caee07..7e34b3dbd160d 100644 --- a/homeassistant/components/huawei_lte/const.py +++ b/homeassistant/components/huawei_lte/const.py @@ -2,8 +2,11 @@ DOMAIN = "huawei_lte" +CONF_TRACK_WIRED_CLIENTS = "track_wired_clients" + DEFAULT_DEVICE_NAME = "LTE" DEFAULT_NOTIFY_SERVICE_NAME = DOMAIN +DEFAULT_TRACK_WIRED_CLIENTS = True UPDATE_SIGNAL = f"{DOMAIN}_update" @@ -26,6 +29,7 @@ KEY_DEVICE_INFORMATION = "device_information" KEY_DEVICE_SIGNAL = "device_signal" KEY_DIALUP_MOBILE_DATASWITCH = "dialup_mobile_dataswitch" +KEY_LAN_HOST_INFO = "lan_host_info" KEY_MONITORING_CHECK_NOTIFICATIONS = "monitoring_check_notifications" KEY_MONITORING_MONTH_STATISTICS = "monitoring_month_statistics" KEY_MONITORING_STATUS = "monitoring_status" @@ -42,7 +46,10 @@ KEY_WLAN_WIFI_FEATURE_SWITCH, } -DEVICE_TRACKER_KEYS = {KEY_WLAN_HOST_LIST} +DEVICE_TRACKER_KEYS = { + KEY_LAN_HOST_INFO, + KEY_WLAN_HOST_LIST, +} SENSOR_KEYS = { KEY_DEVICE_INFORMATION, diff --git a/homeassistant/components/huawei_lte/device_tracker.py b/homeassistant/components/huawei_lte/device_tracker.py index 595221a3d8410..7e83369688abe 100644 --- a/homeassistant/components/huawei_lte/device_tracker.py +++ b/homeassistant/components/huawei_lte/device_tracker.py @@ -3,7 +3,7 @@ import logging import re -from typing import Any, Callable, cast +from typing import Any, Callable, Dict, List, cast import attr from stringcase import snakecase @@ -21,13 +21,35 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType -from . import HuaweiLteBaseEntity -from .const import DOMAIN, KEY_WLAN_HOST_LIST, UPDATE_SIGNAL +from . import HuaweiLteBaseEntity, Router +from .const import ( + CONF_TRACK_WIRED_CLIENTS, + DEFAULT_TRACK_WIRED_CLIENTS, + DOMAIN, + KEY_LAN_HOST_INFO, + KEY_WLAN_HOST_LIST, + UPDATE_SIGNAL, +) _LOGGER = logging.getLogger(__name__) _DEVICE_SCAN = f"{DEVICE_TRACKER_DOMAIN}/device_scan" +_HostType = Dict[str, Any] + + +def _get_hosts( + router: Router, ignore_subscriptions: bool = False +) -> list[_HostType] | None: + for key in KEY_LAN_HOST_INFO, KEY_WLAN_HOST_LIST: + if not ignore_subscriptions and key not in router.subscriptions: + continue + try: + return cast(List[_HostType], router.data[key]["Hosts"]["Host"]) + except KeyError: + _LOGGER.debug("%s[%s][%s] not in data", key, "Hosts", "Host") + return None + async def async_setup_entry( hass: HomeAssistantType, @@ -40,28 +62,36 @@ async def async_setup_entry( # us, i.e. if wlan host list is supported. Only set up a subscription and proceed # with adding and tracking entities if it is. router = hass.data[DOMAIN].routers[config_entry.data[CONF_URL]] - try: - _ = router.data[KEY_WLAN_HOST_LIST]["Hosts"]["Host"] - except KeyError: - _LOGGER.debug("%s[%s][%s] not in data", KEY_WLAN_HOST_LIST, "Hosts", "Host") + if (hosts := _get_hosts(router, True)) is None: return # Initialize already tracked entities tracked: set[str] = set() registry = await entity_registry.async_get_registry(hass) known_entities: list[Entity] = [] + track_wired_clients = router.config_entry.options.get( + CONF_TRACK_WIRED_CLIENTS, DEFAULT_TRACK_WIRED_CLIENTS + ) for entity in registry.entities.values(): if ( entity.domain == DEVICE_TRACKER_DOMAIN and entity.config_entry_id == config_entry.entry_id ): - tracked.add(entity.unique_id) - known_entities.append( - HuaweiLteScannerEntity(router, entity.unique_id.partition("-")[2]) - ) + mac = entity.unique_id.partition("-")[2] + # Do not add known wired clients if not tracking them (any more) + skip = False + if not track_wired_clients: + for host in hosts: + if host.get("MacAddress") == mac: + skip = not _is_wireless(host) + break + if not skip: + tracked.add(entity.unique_id) + known_entities.append(HuaweiLteScannerEntity(router, mac)) async_add_entities(known_entities, True) # Tell parent router to poll hosts list to gather new devices + router.subscriptions[KEY_LAN_HOST_INFO].add(_DEVICE_SCAN) router.subscriptions[KEY_WLAN_HOST_LIST].add(_DEVICE_SCAN) async def _async_maybe_add_new_entities(url: str) -> None: @@ -79,6 +109,24 @@ async def _async_maybe_add_new_entities(url: str) -> None: async_add_new_entities(hass, router.url, async_add_entities, tracked) +def _is_wireless(host: _HostType) -> bool: + # LAN host info entries have an "InterfaceType" property, "Ethernet" / "Wireless". + # WLAN host list ones don't, but they're expected to be all wireless. + return cast(str, host.get("InterfaceType", "Wireless")) != "Ethernet" + + +def _is_connected(host: _HostType | None) -> bool: + # LAN host info entries have an "Active" property, "1" or "0". + # WLAN host list ones don't, but that call appears to return active hosts only. + return False if host is None else cast(str, host.get("Active", "1")) != "0" + + +def _is_us(host: _HostType) -> bool: + """Try to determine if the host entry is us, the HA instance.""" + # LAN host info entries have an "isLocalDevice" property, "1" / "0"; WLAN host list ones don't. + return cast(str, host.get("isLocalDevice", "0")) == "1" + + @callback def async_add_new_entities( hass: HomeAssistantType, @@ -88,14 +136,23 @@ def async_add_new_entities( ) -> None: """Add new entities that are not already being tracked.""" router = hass.data[DOMAIN].routers[router_url] - try: - hosts = router.data[KEY_WLAN_HOST_LIST]["Hosts"]["Host"] - except KeyError: - _LOGGER.debug("%s[%s][%s] not in data", KEY_WLAN_HOST_LIST, "Hosts", "Host") + hosts = _get_hosts(router) + if not hosts: return + track_wired_clients = router.config_entry.options.get( + CONF_TRACK_WIRED_CLIENTS, DEFAULT_TRACK_WIRED_CLIENTS + ) + new_entities: list[Entity] = [] - for host in (x for x in hosts if x.get("MacAddress")): + for host in ( + x + for x in hosts + if not _is_us(x) + and _is_connected(x) + and x.get("MacAddress") + and (track_wired_clients or _is_wireless(x)) + ): entity = HuaweiLteScannerEntity(router, host["MacAddress"]) if entity.unique_id in tracked: continue @@ -124,29 +181,41 @@ def _better_snakecase(text: str) -> str: class HuaweiLteScannerEntity(HuaweiLteBaseEntity, ScannerEntity): """Huawei LTE router scanner entity.""" - mac: str = attr.ib() + _mac_address: str = attr.ib() + _ip_address: str | None = attr.ib(init=False, default=None) _is_connected: bool = attr.ib(init=False, default=False) _hostname: str | None = attr.ib(init=False, default=None) _extra_state_attributes: dict[str, Any] = attr.ib(init=False, factory=dict) - def __attrs_post_init__(self) -> None: - """Initialize internal state.""" - self._extra_state_attributes["mac_address"] = self.mac - @property def _entity_name(self) -> str: - return self._hostname or self.mac + return self.hostname or self.mac_address @property def _device_unique_id(self) -> str: - return self.mac + return self.mac_address @property def source_type(self) -> str: """Return SOURCE_TYPE_ROUTER.""" return SOURCE_TYPE_ROUTER + @property + def ip_address(self) -> str | None: + """Return the primary ip address of the device.""" + return self._ip_address + + @property + def mac_address(self) -> str: + """Return the mac address of the device.""" + return self._mac_address + + @property + def hostname(self) -> str | None: + """Return hostname of the device.""" + return self._hostname + @property def is_connected(self) -> bool: """Get whether the entity is connected.""" @@ -159,11 +228,27 @@ def extra_state_attributes(self) -> dict[str, Any]: async def async_update(self) -> None: """Update state.""" - hosts = self.router.data[KEY_WLAN_HOST_LIST]["Hosts"]["Host"] - host = next((x for x in hosts if x.get("MacAddress") == self.mac), None) - self._is_connected = host is not None + hosts = _get_hosts(self.router) + if hosts is None: + self._available = False + return + self._available = True + host = next( + (x for x in hosts if x.get("MacAddress") == self._mac_address), None + ) + self._is_connected = _is_connected(host) if host is not None: + # IpAddress can contain multiple semicolon separated addresses. + # Pick one for model sanity; e.g. the dhcp component to which it is fed, parses and expects to see just one. + self._ip_address = (host.get("IpAddress") or "").split(";", 2)[0] or None self._hostname = host.get("HostName") self._extra_state_attributes = { - _better_snakecase(k): v for k, v in host.items() if k != "HostName" + _better_snakecase(k): v + for k, v in host.items() + if k + in { + "AddressSource", + "AssociatedSsid", + "InterfaceType", + } } diff --git a/homeassistant/components/huawei_lte/strings.json b/homeassistant/components/huawei_lte/strings.json index 4aa0278faf4d9..5cff2165dc370 100644 --- a/homeassistant/components/huawei_lte/strings.json +++ b/homeassistant/components/huawei_lte/strings.json @@ -33,7 +33,8 @@ "init": { "data": { "name": "Notification service name (change requires restart)", - "recipient": "SMS notification recipients" + "recipient": "SMS notification recipients", + "track_wired_clients": "Track wired network clients" } } } From 020d456889edb4e2f80d48fed4429dd90b45dbf5 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Wed, 21 Apr 2021 00:03:47 +0000 Subject: [PATCH 0425/1317] [ci skip] Translation update --- .../components/abode/translations/es-419.json | 15 +++++++++++ .../accuweather/translations/es-419.json | 8 +++++- .../translations/sensor.es-419.json | 9 +++++++ .../components/adguard/translations/ko.json | 1 + .../components/adguard/translations/pl.json | 1 + .../components/aemet/translations/es-419.json | 12 +++++++++ .../components/airly/translations/es-419.json | 6 +++++ .../airnow/translations/es-419.json | 17 ++++++++++++ .../airvisual/translations/es-419.json | 7 ++++- .../alarmdecoder/translations/es-419.json | 3 ++- .../components/almond/translations/nl.json | 2 +- .../ambiclimate/translations/nl.json | 2 +- .../binary_sensor/translations/he.json | 14 ++++++++-- .../coronavirus/translations/ko.json | 3 ++- .../coronavirus/translations/pl.json | 3 ++- .../enphase_envoy/translations/pl.json | 3 ++- .../home_plus_control/translations/nl.json | 2 +- .../huawei_lte/translations/en.json | 3 ++- .../components/ialarm/translations/pl.json | 20 ++++++++++++++ .../litterrobot/translations/pl.json | 2 +- .../logi_circle/translations/nl.json | 2 +- .../components/lyric/translations/ko.json | 6 ++++- .../components/lyric/translations/nl.json | 2 +- .../components/lyric/translations/pl.json | 7 ++++- .../components/mysensors/translations/ca.json | 1 + .../components/mysensors/translations/et.json | 1 + .../components/mysensors/translations/ru.json | 1 + .../components/neato/translations/nl.json | 2 +- .../components/nest/translations/nl.json | 2 +- .../ondilo_ico/translations/nl.json | 2 +- .../components/point/translations/nl.json | 2 +- .../components/sma/translations/es.json | 11 ++++++++ .../components/sma/translations/pl.json | 27 +++++++++++++++++++ .../components/smappee/translations/nl.json | 2 +- .../components/somfy/translations/nl.json | 2 +- .../components/toon/translations/nl.json | 2 +- .../components/withings/translations/nl.json | 2 +- .../components/xbox/translations/nl.json | 2 +- 38 files changed, 183 insertions(+), 26 deletions(-) create mode 100644 homeassistant/components/accuweather/translations/sensor.es-419.json create mode 100644 homeassistant/components/aemet/translations/es-419.json create mode 100644 homeassistant/components/airnow/translations/es-419.json create mode 100644 homeassistant/components/ialarm/translations/pl.json create mode 100644 homeassistant/components/sma/translations/es.json create mode 100644 homeassistant/components/sma/translations/pl.json diff --git a/homeassistant/components/abode/translations/es-419.json b/homeassistant/components/abode/translations/es-419.json index 3a7ca7a8cab9f..9de6d9d185a2a 100644 --- a/homeassistant/components/abode/translations/es-419.json +++ b/homeassistant/components/abode/translations/es-419.json @@ -3,7 +3,22 @@ "abort": { "single_instance_allowed": "Solo se permite una \u00fanica configuraci\u00f3n de Abode." }, + "error": { + "invalid_mfa_code": "C\u00f3digo MFA no v\u00e1lido" + }, "step": { + "mfa": { + "data": { + "mfa_code": "C\u00f3digo MFA (6 d\u00edgitos)" + }, + "title": "Ingrese su c\u00f3digo MFA para Abode" + }, + "reauth_confirm": { + "data": { + "password": "Contrase\u00f1a" + }, + "title": "Complete su informaci\u00f3n de inicio de sesi\u00f3n de Abode" + }, "user": { "data": { "password": "Contrase\u00f1a", diff --git a/homeassistant/components/accuweather/translations/es-419.json b/homeassistant/components/accuweather/translations/es-419.json index 5af58867ebf92..92d5d5ef2c291 100644 --- a/homeassistant/components/accuweather/translations/es-419.json +++ b/homeassistant/components/accuweather/translations/es-419.json @@ -16,8 +16,14 @@ "data": { "forecast": "Pron\u00f3stico del tiempo" }, - "description": "Debido a las limitaciones de la versi\u00f3n gratuita de la clave API de AccuWeather, cuando habilita el pron\u00f3stico del tiempo, las actualizaciones de datos se realizar\u00e1n cada 64 minutos en lugar de cada 32 minutos." + "description": "Debido a las limitaciones de la versi\u00f3n gratuita de la clave API de AccuWeather, cuando habilita el pron\u00f3stico del tiempo, las actualizaciones de datos se realizar\u00e1n cada 64 minutos en lugar de cada 32 minutos.", + "title": "Opciones de AccuWeather" } } + }, + "system_health": { + "info": { + "remaining_requests": "Solicitudes permitidas restantes" + } } } \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/sensor.es-419.json b/homeassistant/components/accuweather/translations/sensor.es-419.json new file mode 100644 index 0000000000000..b411977726049 --- /dev/null +++ b/homeassistant/components/accuweather/translations/sensor.es-419.json @@ -0,0 +1,9 @@ +{ + "state": { + "accuweather__pressure_tendency": { + "falling": "Descendente", + "rising": "Creciente", + "steady": "Firme" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/adguard/translations/ko.json b/homeassistant/components/adguard/translations/ko.json index 6b1917cf73b2c..fa5b3254ad4a5 100644 --- a/homeassistant/components/adguard/translations/ko.json +++ b/homeassistant/components/adguard/translations/ko.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "existing_instance_updated": "\uae30\uc874 \uad6c\uc131\uc744 \uc5c5\ub370\uc774\ud2b8\ud588\uc2b5\ub2c8\ub2e4.", "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." }, diff --git a/homeassistant/components/adguard/translations/pl.json b/homeassistant/components/adguard/translations/pl.json index 50f442d793718..c194afb63dab7 100644 --- a/homeassistant/components/adguard/translations/pl.json +++ b/homeassistant/components/adguard/translations/pl.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana", "existing_instance_updated": "Zaktualizowano istniej\u0105c\u0105 konfiguracj\u0119", "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." }, diff --git a/homeassistant/components/aemet/translations/es-419.json b/homeassistant/components/aemet/translations/es-419.json new file mode 100644 index 0000000000000..4b3db0a8833bc --- /dev/null +++ b/homeassistant/components/aemet/translations/es-419.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Nombre de la integraci\u00f3n" + }, + "title": "AEMET OpenData" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airly/translations/es-419.json b/homeassistant/components/airly/translations/es-419.json index b4bd813d715e1..c7d1e388d67f8 100644 --- a/homeassistant/components/airly/translations/es-419.json +++ b/homeassistant/components/airly/translations/es-419.json @@ -18,5 +18,11 @@ "title": "Airly" } } + }, + "system_health": { + "info": { + "requests_per_day": "Solicitudes permitidas por d\u00eda", + "requests_remaining": "Solicitudes permitidas restantes" + } } } \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/es-419.json b/homeassistant/components/airnow/translations/es-419.json new file mode 100644 index 0000000000000..015d7242ef191 --- /dev/null +++ b/homeassistant/components/airnow/translations/es-419.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "invalid_location": "No se encontraron resultados para esa ubicaci\u00f3n" + }, + "step": { + "user": { + "data": { + "radius": "Radio de la estaci\u00f3n (millas; opcional)" + }, + "description": "Configure la integraci\u00f3n de la calidad del aire de AirNow. Para generar la clave de API, vaya a https://docs.airnowapi.org/account/request/", + "title": "AirNow" + } + } + }, + "title": "AirNow" +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/es-419.json b/homeassistant/components/airvisual/translations/es-419.json index 8c88c25923037..0cc07d27f171c 100644 --- a/homeassistant/components/airvisual/translations/es-419.json +++ b/homeassistant/components/airvisual/translations/es-419.json @@ -5,7 +5,8 @@ }, "error": { "general_error": "Se ha producido un error desconocido.", - "invalid_api_key": "Se proporciona una clave de API no v\u00e1lida." + "invalid_api_key": "Se proporciona una clave de API no v\u00e1lida.", + "location_not_found": "Ubicaci\u00f3n no encontrada" }, "step": { "geography": { @@ -17,6 +18,10 @@ "description": "Use la API de AirVisual para monitorear una ubicaci\u00f3n geogr\u00e1fica.", "title": "Configurar una geograf\u00eda" }, + "geography_by_coords": { + "description": "Utilice la API en la nube de AirVisual para monitorear una latitud / longitud.", + "title": "Configurar una geograf\u00eda" + }, "node_pro": { "data": { "ip_address": "Direcci\u00f3n IP/nombre de host de la unidad", diff --git a/homeassistant/components/alarmdecoder/translations/es-419.json b/homeassistant/components/alarmdecoder/translations/es-419.json index 39344beb289ea..2152084ea5658 100644 --- a/homeassistant/components/alarmdecoder/translations/es-419.json +++ b/homeassistant/components/alarmdecoder/translations/es-419.json @@ -14,7 +14,8 @@ "user": { "data": { "protocol": "Protocolo" - } + }, + "title": "Elija el protocolo AlarmDecoder" } } }, diff --git a/homeassistant/components/almond/translations/nl.json b/homeassistant/components/almond/translations/nl.json index dbf4c485d345d..4c507cfab69aa 100644 --- a/homeassistant/components/almond/translations/nl.json +++ b/homeassistant/components/almond/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "cannot_connect": "Kan geen verbinding maken", - "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen.", + "missing_configuration": "De component is niet geconfigureerd. Gelieve de documentatie volgen.", "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})", "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, diff --git a/homeassistant/components/ambiclimate/translations/nl.json b/homeassistant/components/ambiclimate/translations/nl.json index 4e6c5ebb202d7..6d3b382222438 100644 --- a/homeassistant/components/ambiclimate/translations/nl.json +++ b/homeassistant/components/ambiclimate/translations/nl.json @@ -3,7 +3,7 @@ "abort": { "access_token": "Onbekende fout bij het genereren van een toegangstoken.", "already_configured": "Account is al geconfigureerd", - "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen." + "missing_configuration": "De component is niet geconfigureerd. Gelieve de documentatie volgen." }, "create_entry": { "default": "Succesvol geauthenticeerd" diff --git a/homeassistant/components/binary_sensor/translations/he.json b/homeassistant/components/binary_sensor/translations/he.json index 9178d8ef64978..5f4fb949b344e 100644 --- a/homeassistant/components/binary_sensor/translations/he.json +++ b/homeassistant/components/binary_sensor/translations/he.json @@ -1,4 +1,14 @@ { + "device_automation": { + "condition_type": { + "is_cold": "{entity_name} \u05e7\u05e8", + "is_not_cold": "{entity_name} \u05dc\u05d0 \u05e7\u05e8" + }, + "trigger_type": { + "cold": "{entity_name} \u05e0\u05d4\u05d9\u05d4 \u05e7\u05e8", + "not_cold": "{entity_name} \u05e0\u05d4\u05d9\u05d4 \u05dc\u05d0 \u05e7\u05e8" + } + }, "state": { "_": { "off": "\u05db\u05d1\u05d5\u05d9", @@ -57,8 +67,8 @@ "on": "\u05e0\u05d5\u05db\u05d7" }, "problem": { - "off": "\u05d0\u05d5\u05e7\u05d9\u05d9", - "on": "\u05d1\u05e2\u05d9\u05d9\u05d4" + "off": "\u05ea\u05e7\u05d9\u05df", + "on": "\u05d1\u05e2\u05d9\u05d4" }, "safety": { "off": "\u05d1\u05d8\u05d5\u05d7", diff --git a/homeassistant/components/coronavirus/translations/ko.json b/homeassistant/components/coronavirus/translations/ko.json index 873aca88e30af..e9a3c29926468 100644 --- a/homeassistant/components/coronavirus/translations/ko.json +++ b/homeassistant/components/coronavirus/translations/ko.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" }, "step": { "user": { diff --git a/homeassistant/components/coronavirus/translations/pl.json b/homeassistant/components/coronavirus/translations/pl.json index f901f258682d7..410e0f4378db1 100644 --- a/homeassistant/components/coronavirus/translations/pl.json +++ b/homeassistant/components/coronavirus/translations/pl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana" + "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, "step": { "user": { diff --git a/homeassistant/components/enphase_envoy/translations/pl.json b/homeassistant/components/enphase_envoy/translations/pl.json index de961875c56c6..e35e215bffaeb 100644 --- a/homeassistant/components/enphase_envoy/translations/pl.json +++ b/homeassistant/components/enphase_envoy/translations/pl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", diff --git a/homeassistant/components/home_plus_control/translations/nl.json b/homeassistant/components/home_plus_control/translations/nl.json index 9d448e480a104..8f6df2fdad095 100644 --- a/homeassistant/components/home_plus_control/translations/nl.json +++ b/homeassistant/components/home_plus_control/translations/nl.json @@ -4,7 +4,7 @@ "already_configured": "Account is al geconfigureerd", "already_in_progress": "De configuratiestroom is al aan de gang", "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", - "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen.", + "missing_configuration": "De component is niet geconfigureerd. Gelieve de documentatie volgen.", "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})", "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." }, diff --git a/homeassistant/components/huawei_lte/translations/en.json b/homeassistant/components/huawei_lte/translations/en.json index 57f32fdd2dfd0..36e99b3420cc5 100644 --- a/homeassistant/components/huawei_lte/translations/en.json +++ b/homeassistant/components/huawei_lte/translations/en.json @@ -34,7 +34,8 @@ "data": { "name": "Notification service name (change requires restart)", "recipient": "SMS notification recipients", - "track_new_devices": "Track new devices" + "track_new_devices": "Track new devices", + "track_wired_clients": "Track wired network clients" } } } diff --git a/homeassistant/components/ialarm/translations/pl.json b/homeassistant/components/ialarm/translations/pl.json new file mode 100644 index 0000000000000..db52ec86612b2 --- /dev/null +++ b/homeassistant/components/ialarm/translations/pl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "pin": "Kod PIN", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/pl.json b/homeassistant/components/litterrobot/translations/pl.json index 8a08a06c69904..8558125c05752 100644 --- a/homeassistant/components/litterrobot/translations/pl.json +++ b/homeassistant/components/litterrobot/translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + "already_configured": "[%key::common::config_flow::abort::already_configured_account%]" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", diff --git a/homeassistant/components/logi_circle/translations/nl.json b/homeassistant/components/logi_circle/translations/nl.json index 8c4d81d120eb6..962310868306f 100644 --- a/homeassistant/components/logi_circle/translations/nl.json +++ b/homeassistant/components/logi_circle/translations/nl.json @@ -4,7 +4,7 @@ "already_configured": "Account is al geconfigureerd", "external_error": "Uitzondering opgetreden uit een andere stroom.", "external_setup": "Logi Circle is met succes geconfigureerd vanuit een andere stroom.", - "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen." + "missing_configuration": "De component is niet geconfigureerd. Gelieve de documentatie volgen." }, "error": { "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", diff --git a/homeassistant/components/lyric/translations/ko.json b/homeassistant/components/lyric/translations/ko.json index fa000ea1c06d1..37093d340df9b 100644 --- a/homeassistant/components/lyric/translations/ko.json +++ b/homeassistant/components/lyric/translations/ko.json @@ -2,7 +2,8 @@ "config": { "abort": { "authorize_url_timeout": "\uc778\uc99d URL \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694." + "missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.", + "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4" }, "create_entry": { "default": "\uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4" @@ -10,6 +11,9 @@ "step": { "pick_implementation": { "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd\ud558\uae30" + }, + "reauth_confirm": { + "title": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc7ac\uc778\uc99d\ud558\uae30" } } } diff --git a/homeassistant/components/lyric/translations/nl.json b/homeassistant/components/lyric/translations/nl.json index 0d766d1823ffd..0d1f9da12e802 100644 --- a/homeassistant/components/lyric/translations/nl.json +++ b/homeassistant/components/lyric/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", - "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen.", + "missing_configuration": "De component is niet geconfigureerd. Gelieve de documentatie volgen.", "reauth_successful": "Herauthenticatie was succesvol" }, "create_entry": { diff --git a/homeassistant/components/lyric/translations/pl.json b/homeassistant/components/lyric/translations/pl.json index 8c75c11dd7c4c..09ae3ba273ad4 100644 --- a/homeassistant/components/lyric/translations/pl.json +++ b/homeassistant/components/lyric/translations/pl.json @@ -2,7 +2,8 @@ "config": { "abort": { "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji", - "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105." + "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105.", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, "create_entry": { "default": "Pomy\u015blnie uwierzytelniono" @@ -10,6 +11,10 @@ "step": { "pick_implementation": { "title": "Wybierz metod\u0119 uwierzytelniania" + }, + "reauth_confirm": { + "description": "Integracja Lyric wymaga ponownego uwierzytelnienia Twojego konta.", + "title": "Ponownie uwierzytelnij integracj\u0119" } } } diff --git a/homeassistant/components/mysensors/translations/ca.json b/homeassistant/components/mysensors/translations/ca.json index 844d9e51da1de..6e20f0bcbeec4 100644 --- a/homeassistant/components/mysensors/translations/ca.json +++ b/homeassistant/components/mysensors/translations/ca.json @@ -33,6 +33,7 @@ "invalid_serial": "Port s\u00e8rie inv\u00e0lid", "invalid_subscribe_topic": "Topic de subscripci\u00f3 inv\u00e0lid", "invalid_version": "Versi\u00f3 de MySensors inv\u00e0lida", + "mqtt_required": "La integraci\u00f3 MQTT no est\u00e0 configurada", "not_a_number": "Introdueix un n\u00famero", "port_out_of_range": "El n\u00famero de port ha d'estar entre 1 i 65535", "same_topic": "Els topics de publicaci\u00f3 i subscripci\u00f3 son els mateixos", diff --git a/homeassistant/components/mysensors/translations/et.json b/homeassistant/components/mysensors/translations/et.json index 0682610be97fe..7aff6b1c3da72 100644 --- a/homeassistant/components/mysensors/translations/et.json +++ b/homeassistant/components/mysensors/translations/et.json @@ -33,6 +33,7 @@ "invalid_serial": "Sobimatu jadaport", "invalid_subscribe_topic": "Kehtetu tellimisteema", "invalid_version": "Sobimatu MySensors versioon", + "mqtt_required": "MQTT sidumine on loomata", "not_a_number": "Sisesta number", "port_out_of_range": "Pordi number peab olema v\u00e4hemalt 1 ja k\u00f5ige rohkem 65535", "same_topic": "Tellimise ja avaldamise teemad kattuvad", diff --git a/homeassistant/components/mysensors/translations/ru.json b/homeassistant/components/mysensors/translations/ru.json index 6267970901761..16f23f6efffa5 100644 --- a/homeassistant/components/mysensors/translations/ru.json +++ b/homeassistant/components/mysensors/translations/ru.json @@ -33,6 +33,7 @@ "invalid_serial": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043f\u043e\u0440\u0442.", "invalid_subscribe_topic": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u043e\u043f\u0438\u043a \u0434\u043b\u044f \u043f\u043e\u0434\u043f\u0438\u0441\u043a\u0438.", "invalid_version": "\u041d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u0430\u044f \u0432\u0435\u0440\u0441\u0438\u044f MySensors.", + "mqtt_required": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f MQTT \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u0430.", "not_a_number": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0447\u0438\u0441\u043b\u043e.", "port_out_of_range": "\u041d\u043e\u043c\u0435\u0440 \u043f\u043e\u0440\u0442\u0430 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u043e\u0442 1 \u0434\u043e 65535.", "same_topic": "\u0422\u043e\u043f\u0438\u043a\u0438 \u043f\u043e\u0434\u043f\u0438\u0441\u043a\u0438 \u0438 \u043f\u0443\u0431\u043b\u0438\u043a\u0430\u0446\u0438\u0438 \u0441\u043e\u0432\u043f\u0430\u0434\u0430\u044e\u0442.", diff --git a/homeassistant/components/neato/translations/nl.json b/homeassistant/components/neato/translations/nl.json index d03bc1d216a76..919928ad91ad3 100644 --- a/homeassistant/components/neato/translations/nl.json +++ b/homeassistant/components/neato/translations/nl.json @@ -4,7 +4,7 @@ "already_configured": "Apparaat is al geconfigureerd", "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", "invalid_auth": "Ongeldige authenticatie", - "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen.", + "missing_configuration": "De component is niet geconfigureerd. Gelieve de documentatie volgen.", "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})", "reauth_successful": "Herauthenticatie was succesvol" }, diff --git a/homeassistant/components/nest/translations/nl.json b/homeassistant/components/nest/translations/nl.json index b4a965f495574..a55f19d2a4229 100644 --- a/homeassistant/components/nest/translations/nl.json +++ b/homeassistant/components/nest/translations/nl.json @@ -3,7 +3,7 @@ "abort": { "authorize_url_fail": "Onbekende fout bij het genereren van een autoriseer-URL.", "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", - "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen.", + "missing_configuration": "De component is niet geconfigureerd. Gelieve de documentatie volgen.", "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})", "reauth_successful": "Herauthenticatie was succesvol", "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk.", diff --git a/homeassistant/components/ondilo_ico/translations/nl.json b/homeassistant/components/ondilo_ico/translations/nl.json index 8a91dff086f64..50d093405550e 100644 --- a/homeassistant/components/ondilo_ico/translations/nl.json +++ b/homeassistant/components/ondilo_ico/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", - "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen." + "missing_configuration": "De component is niet geconfigureerd. Gelieve de documentatie volgen." }, "create_entry": { "default": "Succesvol geauthenticeerd" diff --git a/homeassistant/components/point/translations/nl.json b/homeassistant/components/point/translations/nl.json index 8447ac6bbb2c3..59b50f1636b21 100644 --- a/homeassistant/components/point/translations/nl.json +++ b/homeassistant/components/point/translations/nl.json @@ -5,7 +5,7 @@ "authorize_url_fail": "Onbekende fout bij het genereren van een autoriseer-URL.", "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", "external_setup": "Punt succesvol geconfigureerd vanuit een andere stroom.", - "no_flows": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen.", + "no_flows": "De component is niet geconfigureerd. Gelieve de documentatie volgen.", "unknown_authorize_url_generation": "Onbekende fout bij het genereren van een autorisatie-URL." }, "create_entry": { diff --git a/homeassistant/components/sma/translations/es.json b/homeassistant/components/sma/translations/es.json new file mode 100644 index 0000000000000..abfce5c74a166 --- /dev/null +++ b/homeassistant/components/sma/translations/es.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "group": "Grupo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sma/translations/pl.json b/homeassistant/components/sma/translations/pl.json new file mode 100644 index 0000000000000..6fbf5d2f9510d --- /dev/null +++ b/homeassistant/components/sma/translations/pl.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "cannot_retrieve_device_info": "Po\u0142\u0105czono pomy\u015blnie, ale nie mo\u017cna pobra\u0107 informacji o urz\u0105dzeniu", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "group": "Grupa", + "host": "Nazwa hosta lub adres IP", + "password": "Has\u0142o", + "ssl": "Certyfikat SSL", + "verify_ssl": "Weryfikacja certyfikatu SSL" + }, + "description": "Wprowad\u017a informacje o urz\u0105dzeniu SMA.", + "title": "Konfiguracja SMA Solar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smappee/translations/nl.json b/homeassistant/components/smappee/translations/nl.json index 66ede5e8c14e6..0bc976e587c40 100644 --- a/homeassistant/components/smappee/translations/nl.json +++ b/homeassistant/components/smappee/translations/nl.json @@ -6,7 +6,7 @@ "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", "cannot_connect": "Kan geen verbinding maken", "invalid_mdns": "Niet-ondersteund apparaat voor de Smappee-integratie.", - "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen.", + "missing_configuration": "De component is niet geconfigureerd. Gelieve de documentatie volgen.", "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})" }, "flow_title": "Smappee: {name}", diff --git a/homeassistant/components/somfy/translations/nl.json b/homeassistant/components/somfy/translations/nl.json index 94305c7ae6f97..11b3de442dd02 100644 --- a/homeassistant/components/somfy/translations/nl.json +++ b/homeassistant/components/somfy/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", - "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen.", + "missing_configuration": "De component is niet geconfigureerd. Gelieve de documentatie volgen.", "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})", "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, diff --git a/homeassistant/components/toon/translations/nl.json b/homeassistant/components/toon/translations/nl.json index 687efce4a4235..82f224e25140f 100644 --- a/homeassistant/components/toon/translations/nl.json +++ b/homeassistant/components/toon/translations/nl.json @@ -4,7 +4,7 @@ "already_configured": "De geselecteerde overeenkomst is al geconfigureerd.", "authorize_url_fail": "Onbekende fout bij het genereren van een autorisatie-URL.", "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", - "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen.", + "missing_configuration": "De component is niet geconfigureerd. Gelieve de documentatie volgen.", "no_agreements": "Dit account heeft geen Toon schermen.", "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout [check the help section] ( {docs_url} )", "unknown_authorize_url_generation": "Onbekende fout bij het genereren van een autorisatie-URL." diff --git a/homeassistant/components/withings/translations/nl.json b/homeassistant/components/withings/translations/nl.json index 23e110a1d60bd..d5a8d62334905 100644 --- a/homeassistant/components/withings/translations/nl.json +++ b/homeassistant/components/withings/translations/nl.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Configuratie bijgewerkt voor profiel.", "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", - "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen.", + "missing_configuration": "De component is niet geconfigureerd. Gelieve de documentatie volgen.", "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})" }, "create_entry": { diff --git a/homeassistant/components/xbox/translations/nl.json b/homeassistant/components/xbox/translations/nl.json index 858fd264eaf99..1e567e954d24e 100644 --- a/homeassistant/components/xbox/translations/nl.json +++ b/homeassistant/components/xbox/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", - "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen.", + "missing_configuration": "De component is niet geconfigureerd. Gelieve de documentatie volgen.", "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." }, "create_entry": { From a90d3a051fc402957c24ec782fca1d2f6d9cf8dc Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 20 Apr 2021 17:41:36 -0700 Subject: [PATCH 0426/1317] prefer total_seconds over seconds (#49505) --- homeassistant/components/aqualogic/__init__.py | 2 +- homeassistant/components/august/activity.py | 6 ++++-- homeassistant/components/august/binary_sensor.py | 8 +++++--- homeassistant/components/broadlink/remote.py | 6 +++--- .../device_sun_light_trigger/__init__.py | 2 +- homeassistant/components/gdacs/config_flow.py | 2 +- .../components/geonetnz_quakes/config_flow.py | 2 +- .../components/geonetnz_volcano/config_flow.py | 2 +- homeassistant/components/gtfs/sensor.py | 4 ++-- homeassistant/components/keenetic_ndms2/const.py | 2 +- .../components/luftdaten/config_flow.py | 2 +- homeassistant/components/mikrotik/config_flow.py | 4 +++- homeassistant/components/mqtt_room/sensor.py | 2 +- homeassistant/components/mychevy/__init__.py | 4 ++-- homeassistant/components/nzbget/config_flow.py | 4 +++- homeassistant/components/rachio/switch.py | 4 +++- homeassistant/components/simplisafe/__init__.py | 16 +++++++++++----- homeassistant/components/snips/__init__.py | 2 +- .../components/speedtestdotnet/config_flow.py | 2 +- homeassistant/components/systemmonitor/sensor.py | 2 +- .../components/tellduslive/config_flow.py | 4 ++-- homeassistant/components/template/trigger.py | 2 +- .../components/transmission/config_flow.py | 4 +++- homeassistant/components/uk_transport/sensor.py | 2 +- homeassistant/components/upcloud/__init__.py | 5 +++-- homeassistant/components/upcloud/config_flow.py | 2 +- homeassistant/components/upnp/const.py | 2 +- homeassistant/components/upnp/sensor.py | 4 ++-- .../components/waterfurnace/__init__.py | 4 ++-- homeassistant/components/wemo/entity.py | 2 +- homeassistant/components/yeelight/__init__.py | 2 +- 31 files changed, 65 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/aqualogic/__init__.py b/homeassistant/components/aqualogic/__init__.py index 7ed38206a1190..0878419a792ec 100644 --- a/homeassistant/components/aqualogic/__init__.py +++ b/homeassistant/components/aqualogic/__init__.py @@ -82,7 +82,7 @@ def run(self): return _LOGGER.error("Connection to %s:%d lost", self._host, self._port) - time.sleep(RECONNECT_INTERVAL.seconds) + time.sleep(RECONNECT_INTERVAL.total_seconds()) @property def panel(self): diff --git a/homeassistant/components/august/activity.py b/homeassistant/components/august/activity.py index 18f390b4f8f5e..402852013f8aa 100644 --- a/homeassistant/components/august/activity.py +++ b/homeassistant/components/august/activity.py @@ -52,7 +52,7 @@ async def _async_update_house_id(): return Debouncer( self._hass, _LOGGER, - cooldown=ACTIVITY_UPDATE_INTERVAL.seconds, + cooldown=ACTIVITY_UPDATE_INTERVAL.total_seconds(), immediate=True, function=_async_update_house_id, ) @@ -121,7 +121,9 @@ async def _update_house_activities(_): # we catch the case where the lock operator is # not updated or the lock failed self._schedule_updates[house_id] = async_call_later( - self._hass, ACTIVITY_UPDATE_INTERVAL.seconds + 1, _update_house_activities + self._hass, + ACTIVITY_UPDATE_INTERVAL.total_seconds() + 1, + _update_house_activities, ) async def _async_update_house_id(self, house_id): diff --git a/homeassistant/components/august/binary_sensor.py b/homeassistant/components/august/binary_sensor.py index 6dccec57a09a8..bb2bcda39e6c8 100644 --- a/homeassistant/components/august/binary_sensor.py +++ b/homeassistant/components/august/binary_sensor.py @@ -21,8 +21,10 @@ _LOGGER = logging.getLogger(__name__) -TIME_TO_DECLARE_DETECTION = timedelta(seconds=ACTIVITY_UPDATE_INTERVAL.seconds) -TIME_TO_RECHECK_DETECTION = timedelta(seconds=ACTIVITY_UPDATE_INTERVAL.seconds * 3) +TIME_TO_DECLARE_DETECTION = timedelta(seconds=ACTIVITY_UPDATE_INTERVAL.total_seconds()) +TIME_TO_RECHECK_DETECTION = timedelta( + seconds=ACTIVITY_UPDATE_INTERVAL.total_seconds() * 3 +) def _retrieve_online_state(data, detail): @@ -257,7 +259,7 @@ def _scheduled_update(now): self.async_write_ha_state() self._check_for_off_update_listener = async_call_later( - self.hass, TIME_TO_RECHECK_DETECTION.seconds, _scheduled_update + self.hass, TIME_TO_RECHECK_DETECTION.total_seconds(), _scheduled_update ) def _cancel_any_pending_updates(self): diff --git a/homeassistant/components/broadlink/remote.py b/homeassistant/components/broadlink/remote.py index dff7ba6b2fdb4..291bf6a3d8bb6 100644 --- a/homeassistant/components/broadlink/remote.py +++ b/homeassistant/components/broadlink/remote.py @@ -387,7 +387,7 @@ async def _async_learn_ir_command(self, command): raise TimeoutError( "No infrared code received within " - f"{LEARNING_TIMEOUT.seconds} seconds" + f"{LEARNING_TIMEOUT.total_seconds()} seconds" ) finally: @@ -425,7 +425,7 @@ async def _async_learn_rf_command(self, command): ) raise TimeoutError( "No radiofrequency found within " - f"{LEARNING_TIMEOUT.seconds} seconds" + f"{LEARNING_TIMEOUT.total_seconds()} seconds" ) finally: @@ -460,7 +460,7 @@ async def _async_learn_rf_command(self, command): raise TimeoutError( "No radiofrequency code received within " - f"{LEARNING_TIMEOUT.seconds} seconds" + f"{LEARNING_TIMEOUT.total_seconds()} seconds" ) finally: diff --git a/homeassistant/components/device_sun_light_trigger/__init__.py b/homeassistant/components/device_sun_light_trigger/__init__.py index b1fc37e3ae312..5ae7e43b19a12 100644 --- a/homeassistant/components/device_sun_light_trigger/__init__.py +++ b/homeassistant/components/device_sun_light_trigger/__init__.py @@ -141,7 +141,7 @@ async def async_turn_on_before_sunset(light_id): SERVICE_TURN_ON, { ATTR_ENTITY_ID: light_id, - ATTR_TRANSITION: LIGHT_TRANSITION_TIME.seconds, + ATTR_TRANSITION: LIGHT_TRANSITION_TIME.total_seconds(), ATTR_PROFILE: light_profile, }, ) diff --git a/homeassistant/components/gdacs/config_flow.py b/homeassistant/components/gdacs/config_flow.py index b672b56ad9b9b..7255fd28de3a5 100644 --- a/homeassistant/components/gdacs/config_flow.py +++ b/homeassistant/components/gdacs/config_flow.py @@ -53,7 +53,7 @@ async def async_step_user(self, user_input=None): self._abort_if_unique_id_configured() scan_interval = user_input.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) - user_input[CONF_SCAN_INTERVAL] = scan_interval.seconds + user_input[CONF_SCAN_INTERVAL] = scan_interval.total_seconds() categories = user_input.get(CONF_CATEGORIES, []) user_input[CONF_CATEGORIES] = categories diff --git a/homeassistant/components/geonetnz_quakes/config_flow.py b/homeassistant/components/geonetnz_quakes/config_flow.py index 735c6cd6d9fe3..2bad1533fc707 100644 --- a/homeassistant/components/geonetnz_quakes/config_flow.py +++ b/homeassistant/components/geonetnz_quakes/config_flow.py @@ -66,7 +66,7 @@ async def async_step_user(self, user_input=None): self._abort_if_unique_id_configured() scan_interval = user_input.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) - user_input[CONF_SCAN_INTERVAL] = scan_interval.seconds + user_input[CONF_SCAN_INTERVAL] = scan_interval.total_seconds() minimum_magnitude = user_input.get( CONF_MINIMUM_MAGNITUDE, DEFAULT_MINIMUM_MAGNITUDE diff --git a/homeassistant/components/geonetnz_volcano/config_flow.py b/homeassistant/components/geonetnz_volcano/config_flow.py index 0658f07350354..7f47480dc34d7 100644 --- a/homeassistant/components/geonetnz_volcano/config_flow.py +++ b/homeassistant/components/geonetnz_volcano/config_flow.py @@ -65,6 +65,6 @@ async def async_step_user(self, user_input=None): user_input[CONF_UNIT_SYSTEM] = CONF_UNIT_SYSTEM_METRIC scan_interval = user_input.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) - user_input[CONF_SCAN_INTERVAL] = scan_interval.seconds + user_input[CONF_SCAN_INTERVAL] = scan_interval.total_seconds() return self.async_create_entry(title=identifier, data=user_input) diff --git a/homeassistant/components/gtfs/sensor.py b/homeassistant/components/gtfs/sensor.py index 46a31f464a1aa..61f0bb7d9c11c 100644 --- a/homeassistant/components/gtfs/sensor.py +++ b/homeassistant/components/gtfs/sensor.py @@ -527,7 +527,7 @@ def __init__( name: Any | None, origin: Any, destination: Any, - offset: cv.time_period, + offset: datetime.timedelta, include_tomorrow: bool, ) -> None: """Initialize the sensor.""" @@ -699,7 +699,7 @@ def update_attributes(self) -> None: del self._attributes[ATTR_LAST] # Add contextual information - self._attributes[ATTR_OFFSET] = self._offset.seconds / 60 + self._attributes[ATTR_OFFSET] = self._offset.total_seconds() / 60 if self._state is None: self._attributes[ATTR_INFO] = ( diff --git a/homeassistant/components/keenetic_ndms2/const.py b/homeassistant/components/keenetic_ndms2/const.py index 1818cfab6a661..c07fb0a0d1594 100644 --- a/homeassistant/components/keenetic_ndms2/const.py +++ b/homeassistant/components/keenetic_ndms2/const.py @@ -9,7 +9,7 @@ UNDO_UPDATE_LISTENER = "undo_update_listener" DEFAULT_TELNET_PORT = 23 DEFAULT_SCAN_INTERVAL = 120 -DEFAULT_CONSIDER_HOME = _DEFAULT_CONSIDER_HOME.seconds +DEFAULT_CONSIDER_HOME = _DEFAULT_CONSIDER_HOME.total_seconds() DEFAULT_INTERFACE = "Home" CONF_CONSIDER_HOME = "consider_home" diff --git a/homeassistant/components/luftdaten/config_flow.py b/homeassistant/components/luftdaten/config_flow.py index a77618f27f3e7..964f2ba187587 100644 --- a/homeassistant/components/luftdaten/config_flow.py +++ b/homeassistant/components/luftdaten/config_flow.py @@ -92,6 +92,6 @@ async def async_step_user(self, user_input=None): ) scan_interval = user_input.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) - user_input.update({CONF_SCAN_INTERVAL: scan_interval.seconds}) + user_input.update({CONF_SCAN_INTERVAL: scan_interval.total_seconds()}) return self.async_create_entry(title=str(sensor_id), data=user_input) diff --git a/homeassistant/components/mikrotik/config_flow.py b/homeassistant/components/mikrotik/config_flow.py index 91e0f366b4dc1..8c2c211169206 100644 --- a/homeassistant/components/mikrotik/config_flow.py +++ b/homeassistant/components/mikrotik/config_flow.py @@ -78,7 +78,9 @@ async def async_step_user(self, user_input=None): async def async_step_import(self, import_config): """Import Miktortik from config.""" - import_config[CONF_DETECTION_TIME] = import_config[CONF_DETECTION_TIME].seconds + import_config[CONF_DETECTION_TIME] = import_config[ + CONF_DETECTION_TIME + ].total_seconds() return await self.async_step_user(user_input=import_config) diff --git a/homeassistant/components/mqtt_room/sensor.py b/homeassistant/components/mqtt_room/sensor.py index e446ab8ba7a09..b40d550abf600 100644 --- a/homeassistant/components/mqtt_room/sensor.py +++ b/homeassistant/components/mqtt_room/sensor.py @@ -120,7 +120,7 @@ def message_received(msg): if ( device.get(ATTR_ROOM) == self._state or device.get(ATTR_DISTANCE) < self._distance - or timediff.seconds >= self._timeout + or timediff.total_seconds() >= self._timeout ): update_state(**device) diff --git a/homeassistant/components/mychevy/__init__.py b/homeassistant/components/mychevy/__init__.py index 2b8bd65dfe8a0..5ea5b142657b7 100644 --- a/homeassistant/components/mychevy/__init__.py +++ b/homeassistant/components/mychevy/__init__.py @@ -145,11 +145,11 @@ def run(self): _LOGGER.info("Starting mychevy loop") self.update() self.hass.helpers.dispatcher.dispatcher_send(UPDATE_TOPIC) - time.sleep(MIN_TIME_BETWEEN_UPDATES.seconds) + time.sleep(MIN_TIME_BETWEEN_UPDATES.total_seconds()) except Exception: # pylint: disable=broad-except _LOGGER.exception( "Error updating mychevy data. " "This probably means the OnStar link is down again" ) self.hass.helpers.dispatcher.dispatcher_send(ERROR_TOPIC) - time.sleep(ERROR_SLEEP_TIME.seconds) + time.sleep(ERROR_SLEEP_TIME.total_seconds()) diff --git a/homeassistant/components/nzbget/config_flow.py b/homeassistant/components/nzbget/config_flow.py index a352c4df6ed47..a5b24ad6dfef7 100644 --- a/homeassistant/components/nzbget/config_flow.py +++ b/homeassistant/components/nzbget/config_flow.py @@ -69,7 +69,9 @@ async def async_step_import( ) -> dict[str, Any]: """Handle a flow initiated by configuration file.""" if CONF_SCAN_INTERVAL in user_input: - user_input[CONF_SCAN_INTERVAL] = user_input[CONF_SCAN_INTERVAL].seconds + user_input[CONF_SCAN_INTERVAL] = user_input[ + CONF_SCAN_INTERVAL + ].total_seconds() return await self.async_step_user(user_input) diff --git a/homeassistant/components/rachio/switch.py b/homeassistant/components/rachio/switch.py index 30146cb44f689..41b253d97eea3 100644 --- a/homeassistant/components/rachio/switch.py +++ b/homeassistant/components/rachio/switch.py @@ -418,7 +418,9 @@ def turn_on(self, **kwargs) -> None: CONF_MANUAL_RUN_MINS, DEFAULT_MANUAL_RUN_MINS ) ) - self._controller.rachio.zone.start(self.zone_id, manual_run_time.seconds) + self._controller.rachio.zone.start( + self.zone_id, manual_run_time.total_seconds() + ) _LOGGER.debug( "Watering %s on %s for %s", self.name, diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 723c04caea027..6324df3311783 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -114,21 +114,27 @@ SERVICE_SET_SYSTEM_PROPERTIES_SCHEMA = SERVICE_BASE_SCHEMA.extend( { vol.Optional(ATTR_ALARM_DURATION): vol.All( - cv.time_period, lambda value: value.seconds, vol.Range(min=30, max=480) + cv.time_period, + lambda value: value.total_seconds(), + vol.Range(min=30, max=480), ), vol.Optional(ATTR_ALARM_VOLUME): vol.All(vol.Coerce(int), vol.In(VOLUMES)), vol.Optional(ATTR_CHIME_VOLUME): vol.All(vol.Coerce(int), vol.In(VOLUMES)), vol.Optional(ATTR_ENTRY_DELAY_AWAY): vol.All( - cv.time_period, lambda value: value.seconds, vol.Range(min=30, max=255) + cv.time_period, + lambda value: value.total_seconds(), + vol.Range(min=30, max=255), ), vol.Optional(ATTR_ENTRY_DELAY_HOME): vol.All( - cv.time_period, lambda value: value.seconds, vol.Range(max=255) + cv.time_period, lambda value: value.total_seconds(), vol.Range(max=255) ), vol.Optional(ATTR_EXIT_DELAY_AWAY): vol.All( - cv.time_period, lambda value: value.seconds, vol.Range(min=45, max=255) + cv.time_period, + lambda value: value.total_seconds(), + vol.Range(min=45, max=255), ), vol.Optional(ATTR_EXIT_DELAY_HOME): vol.All( - cv.time_period, lambda value: value.seconds, vol.Range(max=255) + cv.time_period, lambda value: value.total_seconds(), vol.Range(max=255) ), vol.Optional(ATTR_LIGHT): cv.boolean, vol.Optional(ATTR_VOICE_PROMPT_VOLUME): vol.All( diff --git a/homeassistant/components/snips/__init__.py b/homeassistant/components/snips/__init__.py index 11f732a3bd500..256a4ae8719e1 100644 --- a/homeassistant/components/snips/__init__.py +++ b/homeassistant/components/snips/__init__.py @@ -233,6 +233,6 @@ def resolve_slot_values(slot): minutes=slot["value"]["minutes"], seconds=slot["value"]["seconds"], ) - value = delta.seconds + value = delta.total_seconds() return value diff --git a/homeassistant/components/speedtestdotnet/config_flow.py b/homeassistant/components/speedtestdotnet/config_flow.py index 84d63ebc33bd1..280a7e391c34c 100644 --- a/homeassistant/components/speedtestdotnet/config_flow.py +++ b/homeassistant/components/speedtestdotnet/config_flow.py @@ -50,7 +50,7 @@ async def async_step_import(self, import_config): return self.async_abort(reason="wrong_server_id") import_config[CONF_SCAN_INTERVAL] = int( - import_config[CONF_SCAN_INTERVAL].seconds / 60 + import_config[CONF_SCAN_INTERVAL].total_seconds() / 60 ) import_config.pop(CONF_MONITORED_CONDITIONS) diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index 94f747014a46d..6d6e898908de2 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -431,7 +431,7 @@ def _update( state = round( (counter - data.value) / 1000 ** 2 - / (now - (data.update_time or now)).seconds, + / (now - (data.update_time or now)).total_seconds(), 3, ) else: diff --git a/homeassistant/components/tellduslive/config_flow.py b/homeassistant/components/tellduslive/config_flow.py index 33a02cd1f16ff..bd16993b57e11 100644 --- a/homeassistant/components/tellduslive/config_flow.py +++ b/homeassistant/components/tellduslive/config_flow.py @@ -87,7 +87,7 @@ async def async_step_auth(self, user_input=None): title=host, data={ CONF_HOST: host, - KEY_SCAN_INTERVAL: self._scan_interval.seconds, + KEY_SCAN_INTERVAL: self._scan_interval.total_seconds(), KEY_SESSION: session, }, ) @@ -152,7 +152,7 @@ async def async_step_import(self, user_input): title=host, data={ CONF_HOST: host, - KEY_SCAN_INTERVAL: self._scan_interval.seconds, + KEY_SCAN_INTERVAL: self._scan_interval.total_seconds(), KEY_SESSION: next(iter(conf.values())), }, ) diff --git a/homeassistant/components/template/trigger.py b/homeassistant/components/template/trigger.py index e631950a74ac4..998984e0b9a8b 100644 --- a/homeassistant/components/template/trigger.py +++ b/homeassistant/components/template/trigger.py @@ -130,7 +130,7 @@ def call_action(*_): trigger_variables["for"] = period - delay_cancel = async_call_later(hass, period.seconds, call_action) + delay_cancel = async_call_later(hass, period.total_seconds(), call_action) info = async_track_template_result( hass, diff --git a/homeassistant/components/transmission/config_flow.py b/homeassistant/components/transmission/config_flow.py index 890a0f3dfa921..f8b4003463801 100644 --- a/homeassistant/components/transmission/config_flow.py +++ b/homeassistant/components/transmission/config_flow.py @@ -86,7 +86,9 @@ async def async_step_user(self, user_input=None): async def async_step_import(self, import_config): """Import from Transmission client config.""" - import_config[CONF_SCAN_INTERVAL] = import_config[CONF_SCAN_INTERVAL].seconds + import_config[CONF_SCAN_INTERVAL] = import_config[ + CONF_SCAN_INTERVAL + ].total_seconds() return await self.async_step_user(user_input=import_config) diff --git a/homeassistant/components/uk_transport/sensor.py b/homeassistant/components/uk_transport/sensor.py index f5cb21edcf790..a0448230dd189 100644 --- a/homeassistant/components/uk_transport/sensor.py +++ b/homeassistant/components/uk_transport/sensor.py @@ -279,5 +279,5 @@ def _delta_mins(hhmm_time_str): if hhmm_datetime < now: hhmm_datetime += timedelta(days=1) - delta_mins = (hhmm_datetime - now).seconds // 60 + delta_mins = (hhmm_datetime - now).total_seconds() // 60 return delta_mins diff --git a/homeassistant/components/upcloud/__init__.py b/homeassistant/components/upcloud/__init__.py index c118f12954d29..f2484135be344 100644 --- a/homeassistant/components/upcloud/__init__.py +++ b/homeassistant/components/upcloud/__init__.py @@ -187,12 +187,13 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) ) if migrated_scan_interval and ( not config_entry.options.get(CONF_SCAN_INTERVAL) - or config_entry.options[CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL.seconds + or config_entry.options[CONF_SCAN_INTERVAL] + == DEFAULT_SCAN_INTERVAL.total_seconds() ): update_interval = migrated_scan_interval hass.config_entries.async_update_entry( config_entry, - options={CONF_SCAN_INTERVAL: update_interval.seconds}, + options={CONF_SCAN_INTERVAL: update_interval.total_seconds()}, ) elif config_entry.options.get(CONF_SCAN_INTERVAL): update_interval = timedelta(seconds=config_entry.options[CONF_SCAN_INTERVAL]) diff --git a/homeassistant/components/upcloud/config_flow.py b/homeassistant/components/upcloud/config_flow.py index 1a39b1898970b..81bc44ac957b6 100644 --- a/homeassistant/components/upcloud/config_flow.py +++ b/homeassistant/components/upcloud/config_flow.py @@ -104,7 +104,7 @@ async def async_step_init(self, user_input=None): vol.Optional( CONF_SCAN_INTERVAL, default=self.config_entry.options.get(CONF_SCAN_INTERVAL) - or DEFAULT_SCAN_INTERVAL.seconds, + or DEFAULT_SCAN_INTERVAL.total_seconds(), ): vol.All(vol.Coerce(int), vol.Range(min=30)), } ) diff --git a/homeassistant/components/upnp/const.py b/homeassistant/components/upnp/const.py index 142524ef9ca52..0611176350ada 100644 --- a/homeassistant/components/upnp/const.py +++ b/homeassistant/components/upnp/const.py @@ -31,4 +31,4 @@ CONFIG_ENTRY_ST = "st" CONFIG_ENTRY_UDN = "udn" CONFIG_ENTRY_HOSTNAME = "hostname" -DEFAULT_SCAN_INTERVAL = timedelta(seconds=30).seconds +DEFAULT_SCAN_INTERVAL = timedelta(seconds=30).total_seconds() diff --git a/homeassistant/components/upnp/sensor.py b/homeassistant/components/upnp/sensor.py index d144bd29299a2..d777b8104cdfd 100644 --- a/homeassistant/components/upnp/sensor.py +++ b/homeassistant/components/upnp/sensor.py @@ -232,10 +232,10 @@ def state(self) -> str | None: if self._sensor_type["unit"] == DATA_BYTES: delta_value /= KIBIBYTE delta_time = current_timestamp - self._last_timestamp - if delta_time.seconds == 0: + if delta_time.total_seconds() == 0: # Prevent division by 0. return None - derived = delta_value / delta_time.seconds + derived = delta_value / delta_time.total_seconds() # Store current values for future use. self._last_value = current_value diff --git a/homeassistant/components/waterfurnace/__init__.py b/homeassistant/components/waterfurnace/__init__.py index 8f237f2fc5a4f..9ae5836f69dd2 100644 --- a/homeassistant/components/waterfurnace/__init__.py +++ b/homeassistant/components/waterfurnace/__init__.py @@ -98,7 +98,7 @@ def _reconnect(self): # sleep first before the reconnect attempt _LOGGER.debug("Sleeping for fail # %s", self._fails) - time.sleep(self._fails * ERROR_INTERVAL.seconds) + time.sleep(self._fails * ERROR_INTERVAL.total_seconds()) try: self.client.login() @@ -149,4 +149,4 @@ def shutdown(event): else: self.hass.helpers.dispatcher.dispatcher_send(UPDATE_TOPIC) - time.sleep(SCAN_INTERVAL.seconds) + time.sleep(SCAN_INTERVAL.total_seconds()) diff --git a/homeassistant/components/wemo/entity.py b/homeassistant/components/wemo/entity.py index 904d62639ebfb..a315f9daf0222 100644 --- a/homeassistant/components/wemo/entity.py +++ b/homeassistant/components/wemo/entity.py @@ -94,7 +94,7 @@ async def async_update(self) -> None: try: async with async_timeout.timeout( - self.platform.scan_interval.seconds - 0.1 + self.platform.scan_interval.total_seconds() - 0.1 ) as timeout: await asyncio.shield(self._async_locked_update(True, timeout)) except asyncio.TimeoutError: diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index c1e0c555e0226..944e6e6bec250 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -319,7 +319,7 @@ async def _async_scan(self): if len(self._callbacks) == 0: self._async_stop_scan() - await asyncio.sleep(SCAN_INTERVAL.seconds) + await asyncio.sleep(SCAN_INTERVAL.total_seconds()) self._scan_task = self._hass.loop.create_task(self._async_scan()) @callback From 6e22251e1d3a4d9a01fea59cd4e4021fbe0226a6 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 20 Apr 2021 21:40:54 -0400 Subject: [PATCH 0427/1317] Add support to enable/disable zwave_js data collection (#49440) --- homeassistant/components/zwave_js/__init__.py | 10 +- homeassistant/components/zwave_js/api.py | 163 +++++++++++++++--- homeassistant/components/zwave_js/const.py | 1 + homeassistant/components/zwave_js/helpers.py | 18 +- tests/components/zwave_js/test_api.py | 75 +++++++- tests/components/zwave_js/test_init.py | 48 ++++++ 6 files changed, 287 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 37d85b81ebe5e..b7d95ab7bc708 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -50,6 +50,7 @@ ATTR_TYPE, ATTR_VALUE, ATTR_VALUE_RAW, + CONF_DATA_COLLECTION_OPTED_IN, CONF_INTEGRATION_CREATED_ADDON, CONF_NETWORK_KEY, CONF_USB_PATH, @@ -64,7 +65,7 @@ ZWAVE_JS_VALUE_NOTIFICATION_EVENT, ) from .discovery import async_discover_values -from .helpers import get_device_id +from .helpers import async_enable_statistics, get_device_id from .migrate import async_migrate_discovered_value from .services import ZWaveServices @@ -322,6 +323,13 @@ async def handle_ha_shutdown(event: Event) -> None: LOGGER.info("Connection to Zwave JS Server initialized") + # If opt in preference hasn't been specified yet, we do nothing, otherwise + # we apply the preference + if opted_in := entry.data.get(CONF_DATA_COLLECTION_OPTED_IN): + await async_enable_statistics(client) + elif opted_in is False: + await client.driver.async_disable_statistics() + # Check for nodes that no longer exist and remove them stored_devices = device_registry.async_entries_for_config_entry( dev_reg, entry.entry_id diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 2792fc6819b58..fc4f16bda33d2 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -2,11 +2,14 @@ from __future__ import annotations import dataclasses +from functools import wraps import json +from typing import Callable from aiohttp import hdrs, web, web_exceptions import voluptuous as vol from zwave_js_server import dump +from zwave_js_server.client import Client from zwave_js_server.const import LogLevel from zwave_js_server.exceptions import InvalidNewValue, NotFoundError, SetValueFailed from zwave_js_server.model.log_config import LogConfig @@ -20,6 +23,7 @@ ERR_NOT_SUPPORTED, ERR_UNKNOWN_ERROR, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv @@ -27,7 +31,13 @@ from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import DATA_CLIENT, DOMAIN, EVENT_DEVICE_ADDED_TO_REGISTRY +from .const import ( + CONF_DATA_COLLECTION_OPTED_IN, + DATA_CLIENT, + DOMAIN, + EVENT_DEVICE_ADDED_TO_REGISTRY, +) +from .helpers import async_enable_statistics, update_data_collection_preference # general API constants ID = "id" @@ -50,6 +60,26 @@ VALUE_ID = "value_id" STATUS = "status" +# constants for data collection +ENABLED = "enabled" +OPTED_IN = "opted_in" + + +def async_get_entry(orig_func: Callable) -> Callable: + """Decorate async function to get entry.""" + + @wraps(orig_func) + async def async_get_entry_func( + hass: HomeAssistant, connection: ActiveConnection, msg: dict + ) -> None: + """Provide user specific data and store to function.""" + entry_id = msg[ENTRY_ID] + entry = hass.config_entries.async_get_entry(entry_id) + client = hass.data[DOMAIN][entry_id][DATA_CLIENT] + await orig_func(hass, connection, msg, entry, client) + + return async_get_entry_func + @callback def async_register_api(hass: HomeAssistant) -> None: @@ -65,6 +95,10 @@ def async_register_api(hass: HomeAssistant) -> None: websocket_api.async_register_command(hass, websocket_get_log_config) websocket_api.async_register_command(hass, websocket_get_config_parameters) websocket_api.async_register_command(hass, websocket_set_config_parameter) + websocket_api.async_register_command( + hass, websocket_update_data_collection_preference + ) + websocket_api.async_register_command(hass, websocket_data_collection_status) hass.http.register_view(DumpView) # type: ignore @@ -140,12 +174,15 @@ def websocket_node_status( vol.Optional("secure", default=False): bool, } ) +@async_get_entry async def websocket_add_node( - hass: HomeAssistant, connection: ActiveConnection, msg: dict + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + entry: ConfigEntry, + client: Client, ) -> None: """Add a node to the Z-Wave network.""" - entry_id = msg[ENTRY_ID] - client = hass.data[DOMAIN][entry_id][DATA_CLIENT] controller = client.driver.controller include_non_secure = not msg["secure"] @@ -210,12 +247,15 @@ def device_registered(device: DeviceEntry) -> None: vol.Required(ENTRY_ID): str, } ) +@async_get_entry async def websocket_stop_inclusion( - hass: HomeAssistant, connection: ActiveConnection, msg: dict + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + entry: ConfigEntry, + client: Client, ) -> None: """Cancel adding a node to the Z-Wave network.""" - entry_id = msg[ENTRY_ID] - client = hass.data[DOMAIN][entry_id][DATA_CLIENT] controller = client.driver.controller result = await controller.async_stop_inclusion() connection.send_result( @@ -232,12 +272,15 @@ async def websocket_stop_inclusion( vol.Required(ENTRY_ID): str, } ) +@async_get_entry async def websocket_stop_exclusion( - hass: HomeAssistant, connection: ActiveConnection, msg: dict + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + entry: ConfigEntry, + client: Client, ) -> None: """Cancel removing a node from the Z-Wave network.""" - entry_id = msg[ENTRY_ID] - client = hass.data[DOMAIN][entry_id][DATA_CLIENT] controller = client.driver.controller result = await controller.async_stop_exclusion() connection.send_result( @@ -254,12 +297,15 @@ async def websocket_stop_exclusion( vol.Required(ENTRY_ID): str, } ) +@async_get_entry async def websocket_remove_node( - hass: HomeAssistant, connection: ActiveConnection, msg: dict + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + entry: ConfigEntry, + client: Client, ) -> None: """Remove a node from the Z-Wave network.""" - entry_id = msg[ENTRY_ID] - client = hass.data[DOMAIN][entry_id][DATA_CLIENT] controller = client.driver.controller @callback @@ -311,13 +357,16 @@ def node_removed(event: dict) -> None: vol.Required(NODE_ID): int, }, ) +@async_get_entry async def websocket_refresh_node_info( - hass: HomeAssistant, connection: ActiveConnection, msg: dict + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + entry: ConfigEntry, + client: Client, ) -> None: """Re-interview a node.""" - entry_id = msg[ENTRY_ID] node_id = msg[NODE_ID] - client = hass.data[DOMAIN][entry_id][DATA_CLIENT] node = client.driver.controller.nodes.get(node_id) if node is None: @@ -340,16 +389,19 @@ async def websocket_refresh_node_info( vol.Required(VALUE): int, } ) +@async_get_entry async def websocket_set_config_parameter( - hass: HomeAssistant, connection: ActiveConnection, msg: dict + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + entry: ConfigEntry, + client: Client, ) -> None: """Set a config parameter value for a Z-Wave node.""" - entry_id = msg[ENTRY_ID] node_id = msg[NODE_ID] property_ = msg[PROPERTY] property_key = msg.get(PROPERTY_KEY) value = msg[VALUE] - client = hass.data[DOMAIN][entry_id][DATA_CLIENT] node = client.driver.controller.nodes[node_id] try: zwave_value, cmd_status = await async_set_config_parameter( @@ -464,12 +516,15 @@ def filename_is_present_if_logging_to_file(obj: dict) -> dict: ), }, ) +@async_get_entry async def websocket_update_log_config( - hass: HomeAssistant, connection: ActiveConnection, msg: dict + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + entry: ConfigEntry, + client: Client, ) -> None: """Update the driver log config.""" - entry_id = msg[ENTRY_ID] - client = hass.data[DOMAIN][entry_id][DATA_CLIENT] await client.driver.async_update_log_config(LogConfig(**msg[CONFIG])) connection.send_result( msg[ID], @@ -484,12 +539,15 @@ async def websocket_update_log_config( vol.Required(ENTRY_ID): str, }, ) +@async_get_entry async def websocket_get_log_config( - hass: HomeAssistant, connection: ActiveConnection, msg: dict + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + entry: ConfigEntry, + client: Client, ) -> None: """Get log configuration for the Z-Wave JS driver.""" - entry_id = msg[ENTRY_ID] - client = hass.data[DOMAIN][entry_id][DATA_CLIENT] result = await client.driver.async_get_log_config() connection.send_result( msg[ID], @@ -497,6 +555,61 @@ async def websocket_get_log_config( ) +@websocket_api.require_admin # type: ignore +@websocket_api.async_response +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/update_data_collection_preference", + vol.Required(ENTRY_ID): str, + vol.Required(OPTED_IN): bool, + }, +) +@async_get_entry +async def websocket_update_data_collection_preference( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + entry: ConfigEntry, + client: Client, +) -> None: + """Update preference for data collection and enable/disable collection.""" + opted_in = msg[OPTED_IN] + update_data_collection_preference(hass, entry, opted_in) + + if opted_in: + await async_enable_statistics(client) + else: + await client.driver.async_disable_statistics() + + connection.send_result( + msg[ID], + ) + + +@websocket_api.require_admin # type: ignore +@websocket_api.async_response +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/data_collection_status", + vol.Required(ENTRY_ID): str, + }, +) +@async_get_entry +async def websocket_data_collection_status( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + entry: ConfigEntry, + client: Client, +) -> None: + """Return data collection preference and status.""" + result = { + OPTED_IN: entry.data.get(CONF_DATA_COLLECTION_OPTED_IN), + ENABLED: await client.driver.async_is_statistics_enabled(), + } + connection.send_result(msg[ID], result) + + class DumpView(HomeAssistantView): """View to dump the state of the Z-Wave JS server.""" diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index afd899e0ee06a..54c1ca78e30f3 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -7,6 +7,7 @@ CONF_NETWORK_KEY = "network_key" CONF_USB_PATH = "usb_path" CONF_USE_ADDON = "use_addon" +CONF_DATA_COLLECTION_OPTED_IN = "data_collection_opted_in" DOMAIN = "zwave_js" DATA_CLIENT = "client" diff --git a/homeassistant/components/zwave_js/helpers.py b/homeassistant/components/zwave_js/helpers.py index d535a22394cd9..98c308ea58c5a 100644 --- a/homeassistant/components/zwave_js/helpers.py +++ b/homeassistant/components/zwave_js/helpers.py @@ -7,11 +7,27 @@ from zwave_js_server.model.node import Node as ZwaveNode from homeassistant.config_entries import ConfigEntry +from homeassistant.const import __version__ as HA_VERSION from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import async_get as async_get_dev_reg from homeassistant.helpers.entity_registry import async_get as async_get_ent_reg -from .const import DATA_CLIENT, DOMAIN +from .const import CONF_DATA_COLLECTION_OPTED_IN, DATA_CLIENT, DOMAIN + + +async def async_enable_statistics(client: ZwaveClient) -> None: + """Enable statistics on the driver.""" + await client.driver.async_enable_statistics("Home Assistant", HA_VERSION) + + +@callback +def update_data_collection_preference( + hass: HomeAssistant, entry: ConfigEntry, preference: bool +) -> None: + """Update data collection preference on config entry.""" + new_data = entry.data.copy() + new_data[CONF_DATA_COLLECTION_OPTED_IN] = preference + hass.config_entries.async_update_entry(entry, data=new_data) @callback diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index ee718020b7aea..edd711b07d1c8 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -17,12 +17,16 @@ LEVEL, LOG_TO_FILE, NODE_ID, + OPTED_IN, PROPERTY, PROPERTY_KEY, TYPE, VALUE, ) -from homeassistant.components.zwave_js.const import DOMAIN +from homeassistant.components.zwave_js.const import ( + CONF_DATA_COLLECTION_OPTED_IN, + DOMAIN, +) from homeassistant.helpers import device_registry as dr @@ -552,3 +556,72 @@ async def test_get_log_config(hass, client, integration, hass_ws_client): assert log_config["log_to_file"] is False assert log_config["filename"] == "/test.txt" assert log_config["force_console"] is False + + +async def test_data_collection(hass, client, integration, hass_ws_client): + """Test that the data collection WS API commands work.""" + entry = integration + ws_client = await hass_ws_client(hass) + + client.async_send_command.return_value = {"statisticsEnabled": False} + await ws_client.send_json( + { + ID: 1, + TYPE: "zwave_js/data_collection_status", + ENTRY_ID: entry.entry_id, + } + ) + msg = await ws_client.receive_json() + result = msg["result"] + assert result == {"opted_in": None, "enabled": False} + + assert len(client.async_send_command.call_args_list) == 1 + assert client.async_send_command.call_args[0][0] == { + "command": "driver.is_statistics_enabled" + } + + assert CONF_DATA_COLLECTION_OPTED_IN not in entry.data + + client.async_send_command.reset_mock() + + client.async_send_command.return_value = {} + await ws_client.send_json( + { + ID: 2, + TYPE: "zwave_js/update_data_collection_preference", + ENTRY_ID: entry.entry_id, + OPTED_IN: True, + } + ) + msg = await ws_client.receive_json() + result = msg["result"] + assert result is None + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "driver.enable_statistics" + assert args["applicationName"] == "Home Assistant" + assert entry.data[CONF_DATA_COLLECTION_OPTED_IN] + + client.async_send_command.reset_mock() + + client.async_send_command.return_value = {} + await ws_client.send_json( + { + ID: 3, + TYPE: "zwave_js/update_data_collection_preference", + ENTRY_ID: entry.entry_id, + OPTED_IN: False, + } + ) + msg = await ws_client.receive_json() + result = msg["result"] + assert result is None + + assert len(client.async_send_command.call_args_list) == 1 + assert client.async_send_command.call_args[0][0] == { + "command": "driver.disable_statistics" + } + assert not entry.data[CONF_DATA_COLLECTION_OPTED_IN] + + client.async_send_command.reset_mock() diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index 32fcdbcc84a7d..f9784e0f9b8cb 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -66,6 +66,54 @@ async def test_initialized_timeout(hass, client, connect_timeout): assert entry.state == ENTRY_STATE_SETUP_RETRY +async def test_enabled_statistics(hass, client): + """Test that we enabled statistics if the entry is opted in.""" + entry = MockConfigEntry( + domain="zwave_js", + data={"url": "ws://test.org", "data_collection_opted_in": True}, + ) + entry.add_to_hass(hass) + + with patch( + "zwave_js_server.model.driver.Driver.async_enable_statistics" + ) as mock_cmd: + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert mock_cmd.called + + +async def test_disabled_statistics(hass, client): + """Test that we diisabled statistics if the entry is opted out.""" + entry = MockConfigEntry( + domain="zwave_js", + data={"url": "ws://test.org", "data_collection_opted_in": False}, + ) + entry.add_to_hass(hass) + + with patch( + "zwave_js_server.model.driver.Driver.async_disable_statistics" + ) as mock_cmd: + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert mock_cmd.called + + +async def test_noop_statistics(hass, client): + """Test that we don't make any statistics calls if user hasn't provided preference.""" + entry = MockConfigEntry(domain="zwave_js", data={"url": "ws://test.org"}) + entry.add_to_hass(hass) + + with patch( + "zwave_js_server.model.driver.Driver.async_enable_statistics" + ) as mock_cmd1, patch( + "zwave_js_server.model.driver.Driver.async_disable_statistics" + ) as mock_cmd2: + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert not mock_cmd1.called + assert not mock_cmd2.called + + @pytest.mark.parametrize("error", [BaseZwaveJSServerError("Boom"), Exception("Boom")]) async def test_listen_failure(hass, client, error): """Test we handle errors during client listen.""" From c9bdc9609cfeeff8328fb90b04e915c778419a09 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Wed, 21 Apr 2021 11:46:40 +0200 Subject: [PATCH 0428/1317] Do not close non existing clients in modbus (#49489) * Only close if _client is present. * Remove del. --- homeassistant/components/modbus/modbus.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index 6784357f1e8f8..44dd330f6ef94 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -209,11 +209,11 @@ def close(self): """Disconnect client.""" with self._lock: try: - self._client.close() - del self._client - self._client = None + if self._client: + self._client.close() + self._client = None except ModbusException as exception_error: - self._log_error(exception_error, error_state=False) + self._log_error(exception_error) return def connect(self): From 77ae4abc6ea9014772d12962cd35a83914bd0345 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 21 Apr 2021 11:57:23 +0200 Subject: [PATCH 0429/1317] Upgrade isort to 5.8.0 (#49516) --- .pre-commit-config.yaml | 2 +- requirements_test_pre_commit.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1e257b537c2b2..a99f1d7de337b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -44,7 +44,7 @@ repos: - --configfile=tests/bandit.yaml files: ^(homeassistant|script|tests)/.+\.py$ - repo: https://github.com/PyCQA/isort - rev: 5.7.0 + rev: 5.8.0 hooks: - id: isort - repo: https://github.com/pre-commit/pre-commit-hooks diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 01115cbede8e5..92920c91549b7 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -7,7 +7,7 @@ flake8-comprehensions==3.4.0 flake8-docstrings==1.6.0 flake8-noqa==1.1.0 flake8==3.9.1 -isort==5.7.0 +isort==5.8.0 pycodestyle==2.7.0 pydocstyle==6.0.0 pyflakes==2.3.1 From 168b3c100c00a8127e86965fd864042f3e9f43ba Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 21 Apr 2021 12:18:42 +0200 Subject: [PATCH 0430/1317] Remove HomeAssistantType alias - Part 4 (#49515) --- homeassistant/components/elgato/light.py | 4 ++-- homeassistant/components/esphome/__init__.py | 19 +++++++++---------- homeassistant/components/esphome/camera.py | 4 ++-- homeassistant/components/esphome/cover.py | 4 ++-- .../components/esphome/entry_data.py | 15 +++++++-------- homeassistant/components/esphome/fan.py | 4 ++-- homeassistant/components/esphome/light.py | 4 ++-- homeassistant/components/esphome/sensor.py | 4 ++-- homeassistant/components/esphome/switch.py | 4 ++-- homeassistant/components/evohome/__init__.py | 8 ++++---- homeassistant/components/evohome/climate.py | 5 +++-- .../components/evohome/water_heater.py | 5 +++-- homeassistant/components/ffmpeg/__init__.py | 5 ++--- .../fireservicerota/binary_sensor.py | 4 ++-- .../components/fireservicerota/sensor.py | 5 ++--- .../components/fireservicerota/switch.py | 5 ++--- homeassistant/components/flo/device.py | 6 +++--- homeassistant/components/freebox/__init__.py | 6 +++--- .../components/freebox/device_tracker.py | 5 ++--- homeassistant/components/freebox/router.py | 6 +++--- homeassistant/components/freebox/sensor.py | 5 ++--- homeassistant/components/freebox/switch.py | 4 ++-- 22 files changed, 63 insertions(+), 68 deletions(-) diff --git a/homeassistant/components/elgato/light.py b/homeassistant/components/elgato/light.py index ae3d827428142..e52e98500d266 100644 --- a/homeassistant/components/elgato/light.py +++ b/homeassistant/components/elgato/light.py @@ -16,8 +16,8 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_NAME, ATTR_TEMPERATURE +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import HomeAssistantType from .const import ( ATTR_IDENTIFIERS, @@ -36,7 +36,7 @@ async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable[[list[Entity], bool], None], ) -> None: diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 4cd9744a2f802..8edb6d79bcd68 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -30,7 +30,7 @@ CONF_PORT, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.core import Event, State, callback +from homeassistant.core import Event, HomeAssistant, State, callback from homeassistant.exceptions import TemplateError from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv @@ -42,7 +42,6 @@ from homeassistant.helpers.service import async_set_service_schema from homeassistant.helpers.storage import Store from homeassistant.helpers.template import Template -from homeassistant.helpers.typing import HomeAssistantType # Import config flow so that it's added to the registry from .entry_data import RuntimeEntryData @@ -56,7 +55,7 @@ CONFIG_SCHEMA = vol.Schema({}, extra=vol.ALLOW_EXTRA) -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up the esphome component.""" hass.data.setdefault(DOMAIN, {}) @@ -222,7 +221,7 @@ class ReconnectLogic(RecordUpdateListener): def __init__( self, - hass: HomeAssistantType, + hass: HomeAssistant, cli: APIClient, entry: ConfigEntry, host: str, @@ -452,7 +451,7 @@ def update_record(self, zc: Zeroconf, now: float, record: DNSRecord) -> None: async def _async_setup_device_registry( - hass: HomeAssistantType, entry: ConfigEntry, device_info: DeviceInfo + hass: HomeAssistant, entry: ConfigEntry, device_info: DeviceInfo ): """Set up device registry feature for a particular config entry.""" sw_version = device_info.esphome_version @@ -471,7 +470,7 @@ async def _async_setup_device_registry( async def _register_service( - hass: HomeAssistantType, entry_data: RuntimeEntryData, service: UserService + hass: HomeAssistant, entry_data: RuntimeEntryData, service: UserService ): service_name = f"{entry_data.device_info.name.replace('-', '_')}_{service.name}" schema = {} @@ -549,7 +548,7 @@ async def execute_service(call): async def _setup_services( - hass: HomeAssistantType, entry_data: RuntimeEntryData, services: list[UserService] + hass: HomeAssistant, entry_data: RuntimeEntryData, services: list[UserService] ): old_services = entry_data.services.copy() to_unregister = [] @@ -580,7 +579,7 @@ async def _setup_services( async def _cleanup_instance( - hass: HomeAssistantType, entry: ConfigEntry + hass: HomeAssistant, entry: ConfigEntry ) -> RuntimeEntryData: """Cleanup the esphome client if it exists.""" data: RuntimeEntryData = hass.data[DOMAIN].pop(entry.entry_id) @@ -592,7 +591,7 @@ async def _cleanup_instance( return data -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload an esphome config entry.""" entry_data = await _cleanup_instance(hass, entry) tasks = [] @@ -604,7 +603,7 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> boo async def platform_async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, async_add_entities, *, diff --git a/homeassistant/components/esphome/camera.py b/homeassistant/components/esphome/camera.py index c868d7b320a73..105d77637a7cf 100644 --- a/homeassistant/components/esphome/camera.py +++ b/homeassistant/components/esphome/camera.py @@ -8,14 +8,14 @@ from homeassistant.components import camera from homeassistant.components.camera import Camera from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.typing import HomeAssistantType from . import EsphomeBaseEntity, platform_async_setup_entry async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up esphome cameras based on a config entry.""" await platform_async_setup_entry( diff --git a/homeassistant/components/esphome/cover.py b/homeassistant/components/esphome/cover.py index 294689d075a19..3f4bd29198cfd 100644 --- a/homeassistant/components/esphome/cover.py +++ b/homeassistant/components/esphome/cover.py @@ -16,13 +16,13 @@ CoverEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from . import EsphomeEntity, esphome_state_property, platform_async_setup_entry async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up ESPHome covers based on a config entry.""" await platform_async_setup_entry( diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index 34ed6ffee4634..fdaa50bb09c4c 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -23,10 +23,9 @@ import attr from homeassistant.config_entries import ConfigEntry -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.storage import Store -from homeassistant.helpers.typing import HomeAssistantType if TYPE_CHECKING: from . import APIClient @@ -73,7 +72,7 @@ class RuntimeEntryData: @callback def async_update_entity( - self, hass: HomeAssistantType, component_key: str, key: int + self, hass: HomeAssistant, component_key: str, key: int ) -> None: """Schedule the update of an entity.""" signal = f"esphome_{self.entry_id}_update_{component_key}_{key}" @@ -81,14 +80,14 @@ def async_update_entity( @callback def async_remove_entity( - self, hass: HomeAssistantType, component_key: str, key: int + self, hass: HomeAssistant, component_key: str, key: int ) -> None: """Schedule the removal of an entity.""" signal = f"esphome_{self.entry_id}_remove_{component_key}_{key}" async_dispatcher_send(hass, signal) async def _ensure_platforms_loaded( - self, hass: HomeAssistantType, entry: ConfigEntry, platforms: set[str] + self, hass: HomeAssistant, entry: ConfigEntry, platforms: set[str] ): async with self.platform_load_lock: needed = platforms - self.loaded_platforms @@ -102,7 +101,7 @@ async def _ensure_platforms_loaded( self.loaded_platforms |= needed async def async_update_static_infos( - self, hass: HomeAssistantType, entry: ConfigEntry, infos: list[EntityInfo] + self, hass: HomeAssistant, entry: ConfigEntry, infos: list[EntityInfo] ) -> None: """Distribute an update of static infos to all platforms.""" # First, load all platforms @@ -119,13 +118,13 @@ async def async_update_static_infos( async_dispatcher_send(hass, signal, infos) @callback - def async_update_state(self, hass: HomeAssistantType, state: EntityState) -> None: + def async_update_state(self, hass: HomeAssistant, state: EntityState) -> None: """Distribute an update of state information to all platforms.""" signal = f"esphome_{self.entry_id}_on_state" async_dispatcher_send(hass, signal, state) @callback - def async_update_device_state(self, hass: HomeAssistantType) -> None: + def async_update_device_state(self, hass: HomeAssistant) -> None: """Distribute an update of a core device state like availability.""" signal = f"esphome_{self.entry_id}_on_device_update" async_dispatcher_send(hass, signal) diff --git a/homeassistant/components/esphome/fan.py b/homeassistant/components/esphome/fan.py index 5d7cf24f2c518..5272cdef5f127 100644 --- a/homeassistant/components/esphome/fan.py +++ b/homeassistant/components/esphome/fan.py @@ -14,7 +14,7 @@ FanEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from homeassistant.util.percentage import ( ordered_list_item_to_percentage, percentage_to_ordered_list_item, @@ -33,7 +33,7 @@ async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up ESPHome fans based on a config entry.""" await platform_async_setup_entry( diff --git a/homeassistant/components/esphome/light.py b/homeassistant/components/esphome/light.py index 29fd969d479e4..d1f567c3c8efa 100644 --- a/homeassistant/components/esphome/light.py +++ b/homeassistant/components/esphome/light.py @@ -23,7 +23,7 @@ LightEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant import homeassistant.util.color as color_util from . import EsphomeEntity, esphome_state_property, platform_async_setup_entry @@ -32,7 +32,7 @@ async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up ESPHome lights based on a config entry.""" await platform_async_setup_entry( diff --git a/homeassistant/components/esphome/sensor.py b/homeassistant/components/esphome/sensor.py index d751109c15975..045f74d3e4ab4 100644 --- a/homeassistant/components/esphome/sensor.py +++ b/homeassistant/components/esphome/sensor.py @@ -8,14 +8,14 @@ from homeassistant.components.sensor import DEVICE_CLASSES, SensorEntity from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import HomeAssistantType from . import EsphomeEntity, esphome_state_property, platform_async_setup_entry async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up esphome sensors based on a config entry.""" await platform_async_setup_entry( diff --git a/homeassistant/components/esphome/switch.py b/homeassistant/components/esphome/switch.py index 992f014e829f4..341068b05ad38 100644 --- a/homeassistant/components/esphome/switch.py +++ b/homeassistant/components/esphome/switch.py @@ -5,13 +5,13 @@ from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from . import EsphomeEntity, esphome_state_property, platform_async_setup_entry async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up ESPHome switches based on a config entry.""" await platform_async_setup_entry( diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 8c83308a8b767..cadeefa3c3a71 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -23,7 +23,7 @@ HTTP_TOO_MANY_REQUESTS, TEMP_CELSIUS, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_load_platform @@ -33,7 +33,7 @@ ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.service import verify_domain_control -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util from .const import DOMAIN, GWS, STORAGE_KEY, STORAGE_VER, TCS, UTC_OFFSET @@ -175,7 +175,7 @@ def _handle_exception(err) -> bool: raise # we don't expect/handle any other Exceptions -async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Create a (EMEA/EU-based) Honeywell TCC system.""" async def load_auth_tokens(store) -> tuple[dict, dict | None]: @@ -264,7 +264,7 @@ async def load_auth_tokens(store) -> tuple[dict, dict | None]: @callback -def setup_service_functions(hass: HomeAssistantType, broker): +def setup_service_functions(hass: HomeAssistant, broker): """Set up the service handlers for the system/zone operating modes. Not all Honeywell TCC-compatible systems support all operating modes. In addition, diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index f291fcd9cb308..8021ad6ba246a 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -17,7 +17,8 @@ SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import PRECISION_TENTHS -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util from . import ( @@ -75,7 +76,7 @@ async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None ) -> None: """Create the evohome Controller, and its Zones, if any.""" if discovery_info is None: diff --git a/homeassistant/components/evohome/water_heater.py b/homeassistant/components/evohome/water_heater.py index 4e05c5534616e..692c4dbbc49be 100644 --- a/homeassistant/components/evohome/water_heater.py +++ b/homeassistant/components/evohome/water_heater.py @@ -9,7 +9,8 @@ WaterHeaterEntity, ) from homeassistant.const import PRECISION_TENTHS, PRECISION_WHOLE, STATE_OFF, STATE_ON -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util from . import EvoChild @@ -26,7 +27,7 @@ async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None ) -> None: """Create a DHW controller.""" if discovery_info is None: diff --git a/homeassistant/components/ffmpeg/__init__.py b/homeassistant/components/ffmpeg/__init__.py index 4bf8de91edc18..55e34a547e35f 100644 --- a/homeassistant/components/ffmpeg/__init__.py +++ b/homeassistant/components/ffmpeg/__init__.py @@ -13,14 +13,13 @@ EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import HomeAssistantType DOMAIN = "ffmpeg" @@ -91,7 +90,7 @@ async def async_service_handle(service): async def async_get_image( - hass: HomeAssistantType, + hass: HomeAssistant, input_source: str, output_format: str = IMAGE_JPEG, extra_cmd: str | None = None, diff --git a/homeassistant/components/fireservicerota/binary_sensor.py b/homeassistant/components/fireservicerota/binary_sensor.py index 29fc97ae50300..ef7ef9daa5118 100644 --- a/homeassistant/components/fireservicerota/binary_sensor.py +++ b/homeassistant/components/fireservicerota/binary_sensor.py @@ -1,7 +1,7 @@ """Binary Sensor platform for FireServiceRota integration.""" from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -11,7 +11,7 @@ async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up FireServiceRota binary sensor based on a config entry.""" diff --git a/homeassistant/components/fireservicerota/sensor.py b/homeassistant/components/fireservicerota/sensor.py index 04d8c97a4a5e7..58b3239331ce5 100644 --- a/homeassistant/components/fireservicerota/sensor.py +++ b/homeassistant/components/fireservicerota/sensor.py @@ -3,10 +3,9 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.typing import HomeAssistantType from .const import DATA_CLIENT, DOMAIN as FIRESERVICEROTA_DOMAIN @@ -14,7 +13,7 @@ async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up FireServiceRota sensor based on a config entry.""" client = hass.data[FIRESERVICEROTA_DOMAIN][entry.entry_id][DATA_CLIENT] diff --git a/homeassistant/components/fireservicerota/switch.py b/homeassistant/components/fireservicerota/switch.py index e2385f02e5c9b..f54e3bc1fa25e 100644 --- a/homeassistant/components/fireservicerota/switch.py +++ b/homeassistant/components/fireservicerota/switch.py @@ -3,9 +3,8 @@ from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.typing import HomeAssistantType from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN as FIRESERVICEROTA_DOMAIN @@ -13,7 +12,7 @@ async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up FireServiceRota switch based on a config entry.""" client = hass.data[FIRESERVICEROTA_DOMAIN][entry.entry_id][DATA_CLIENT] diff --git a/homeassistant/components/flo/device.py b/homeassistant/components/flo/device.py index 50e0ccda87f47..e955c784ae4e4 100644 --- a/homeassistant/components/flo/device.py +++ b/homeassistant/components/flo/device.py @@ -9,7 +9,7 @@ from aioflo.errors import RequestError from async_timeout import timeout -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed import homeassistant.util.dt as dt_util @@ -20,10 +20,10 @@ class FloDeviceDataUpdateCoordinator(DataUpdateCoordinator): """Flo device object.""" def __init__( - self, hass: HomeAssistantType, api_client: API, location_id: str, device_id: str + self, hass: HomeAssistant, api_client: API, location_id: str, device_id: str ): """Initialize the device.""" - self.hass: HomeAssistantType = hass + self.hass: HomeAssistant = hass self.api_client: API = api_client self._flo_location_id: str = location_id self._flo_device_id: str = device_id diff --git a/homeassistant/components/freebox/__init__.py b/homeassistant/components/freebox/__init__.py index a54f34b4d1292..976041721c361 100644 --- a/homeassistant/components/freebox/__init__.py +++ b/homeassistant/components/freebox/__init__.py @@ -6,8 +6,8 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.typing import HomeAssistantType from .const import DOMAIN, PLATFORMS, SERVICE_REBOOT from .router import FreeboxRouter @@ -37,7 +37,7 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Freebox entry.""" router = FreeboxRouter(hass, entry) await router.setup() @@ -68,7 +68,7 @@ async def async_close_connection(event): return True -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" unload_ok = all( await asyncio.gather( diff --git a/homeassistant/components/freebox/device_tracker.py b/homeassistant/components/freebox/device_tracker.py index 6510a29bbfc77..7485c9da85676 100644 --- a/homeassistant/components/freebox/device_tracker.py +++ b/homeassistant/components/freebox/device_tracker.py @@ -6,17 +6,16 @@ from homeassistant.components.device_tracker import SOURCE_TYPE_ROUTER from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.typing import HomeAssistantType from .const import DEFAULT_DEVICE_NAME, DEVICE_ICONS, DOMAIN from .router import FreeboxRouter async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up device tracker for Freebox component.""" router = hass.data[DOMAIN][entry.unique_id] diff --git a/homeassistant/components/freebox/router.py b/homeassistant/components/freebox/router.py index fbeca869d1dd0..3f5a4e5352813 100644 --- a/homeassistant/components/freebox/router.py +++ b/homeassistant/components/freebox/router.py @@ -13,11 +13,11 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import slugify from .const import ( @@ -34,7 +34,7 @@ SCAN_INTERVAL = timedelta(seconds=30) -async def get_api(hass: HomeAssistantType, host: str) -> Freepybox: +async def get_api(hass: HomeAssistant, host: str) -> Freepybox: """Get the Freebox API.""" freebox_path = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY).path @@ -49,7 +49,7 @@ async def get_api(hass: HomeAssistantType, host: str) -> Freepybox: class FreeboxRouter: """Representation of a Freebox router.""" - def __init__(self, hass: HomeAssistantType, entry: ConfigEntry) -> None: + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: """Initialize a Freebox router.""" self.hass = hass self._entry = entry diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py index fd0685f76676f..c121974f1faa4 100644 --- a/homeassistant/components/freebox/sensor.py +++ b/homeassistant/components/freebox/sensor.py @@ -6,9 +6,8 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import DATA_RATE_KILOBYTES_PER_SECOND -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.typing import HomeAssistantType import homeassistant.util.dt as dt_util from .const import ( @@ -28,7 +27,7 @@ async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up the sensors.""" router = hass.data[DOMAIN][entry.unique_id] diff --git a/homeassistant/components/freebox/switch.py b/homeassistant/components/freebox/switch.py index a15a86f46d879..f309524ceb44e 100644 --- a/homeassistant/components/freebox/switch.py +++ b/homeassistant/components/freebox/switch.py @@ -7,7 +7,7 @@ from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .const import DOMAIN from .router import FreeboxRouter @@ -16,7 +16,7 @@ async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up the switch.""" router = hass.data[DOMAIN][entry.unique_id] From 5c6744d97890adab3ef8803fd680c7b53c2d2769 Mon Sep 17 00:00:00 2001 From: Brynley McDonald Date: Wed, 21 Apr 2021 22:21:32 +1200 Subject: [PATCH 0431/1317] Fix typo in tuya config_flow (#49517) --- homeassistant/components/tuya/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tuya/strings.json b/homeassistant/components/tuya/strings.json index 23958349b66cc..4ce021395294e 100644 --- a/homeassistant/components/tuya/strings.json +++ b/homeassistant/components/tuya/strings.json @@ -4,11 +4,11 @@ "step": { "user": { "title": "Tuya", - "description": "Enter your Tuya credential.", + "description": "Enter your Tuya credentials.", "data": { "country_code": "Your account country code (e.g., 1 for USA or 86 for China)", "password": "[%key:common::config_flow::data::password%]", - "platform": "The app where your account register", + "platform": "The app where your account is registered", "username": "[%key:common::config_flow::data::username%]" } } From cad281b3265c2a023f3a5c1f49805bc2723c6c80 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Wed, 21 Apr 2021 07:35:16 -0400 Subject: [PATCH 0432/1317] Add subscription for Z-Wave JS node re-interview status (#49024) * Add subscription for interview status * update test * forward stage completed event * add additional test * additional tests * return earlier --- homeassistant/components/zwave_js/api.py | 35 ++++++++++++++++++-- tests/components/zwave_js/test_api.py | 42 ++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index fc4f16bda33d2..9a30d78a07cc3 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -367,14 +367,43 @@ async def websocket_refresh_node_info( ) -> None: """Re-interview a node.""" node_id = msg[NODE_ID] - node = client.driver.controller.nodes.get(node_id) + controller = client.driver.controller + node = controller.nodes.get(node_id) if node is None: connection.send_error(msg[ID], ERR_NOT_FOUND, f"Node {node_id} not found") return - await node.async_refresh_info() - connection.send_result(msg[ID]) + @callback + def async_cleanup() -> None: + """Remove signal listeners.""" + for unsub in unsubs: + unsub() + + @callback + def forward_event(event: dict) -> None: + connection.send_message( + websocket_api.event_message(msg[ID], {"event": event["event"]}) + ) + + @callback + def forward_stage(event: dict) -> None: + connection.send_message( + websocket_api.event_message( + msg[ID], {"event": event["event"], "stage": event["stageName"]} + ) + ) + + connection.subscriptions[msg["id"]] = async_cleanup + unsubs = [ + node.on("interview started", forward_event), + node.on("interview completed", forward_event), + node.on("interview stage completed", forward_stage), + node.on("interview failed", forward_event), + ] + + result = await node.async_refresh_info() + connection.send_result(msg[ID], result) @websocket_api.require_admin # type:ignore diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index edd711b07d1c8..525f97da68128 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -250,6 +250,48 @@ async def test_refresh_node_info( assert args["command"] == "node.refresh_info" assert args["nodeId"] == 52 + event = Event( + type="interview started", + data={"source": "node", "event": "interview started", "nodeId": 52}, + ) + client.driver.receive_event(event) + + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "interview started" + + event = Event( + type="interview stage completed", + data={ + "source": "node", + "event": "interview stage completed", + "stageName": "NodeInfo", + "nodeId": 52, + }, + ) + client.driver.receive_event(event) + + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "interview stage completed" + assert msg["event"]["stage"] == "NodeInfo" + + event = Event( + type="interview completed", + data={"source": "node", "event": "interview completed", "nodeId": 52}, + ) + client.driver.receive_event(event) + + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "interview completed" + + event = Event( + type="interview failed", + data={"source": "node", "event": "interview failed", "nodeId": 52}, + ) + client.driver.receive_event(event) + + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "interview failed" + client.async_send_command_no_wait.reset_mock() await ws_client.send_json( From 99c5087c1e6868a3d7e819e1084728c5dcc558ac Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 21 Apr 2021 07:37:35 -0400 Subject: [PATCH 0433/1317] Add WS API command to capture zwave_js logs from server (#49444) * Add WS API commands to capture zwave_js logs from server * register commands * create a task * Update homeassistant/components/zwave_js/api.py Co-authored-by: Paulus Schoutsen * Update homeassistant/components/zwave_js/api.py Co-authored-by: Paulus Schoutsen * fix * fixes and add test * fix PR on rebase Co-authored-by: Paulus Schoutsen --- homeassistant/components/zwave_js/api.py | 49 ++++++++++++++++++++++++ tests/components/zwave_js/test_api.py | 42 ++++++++++++++++++++ 2 files changed, 91 insertions(+) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 9a30d78a07cc3..be4386e529ef1 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -13,6 +13,7 @@ from zwave_js_server.const import LogLevel from zwave_js_server.exceptions import InvalidNewValue, NotFoundError, SetValueFailed from zwave_js_server.model.log_config import LogConfig +from zwave_js_server.model.log_message import LogMessage from zwave_js_server.util.node import async_set_config_parameter from homeassistant.components import websocket_api @@ -91,6 +92,7 @@ def async_register_api(hass: HomeAssistant) -> None: websocket_api.async_register_command(hass, websocket_remove_node) websocket_api.async_register_command(hass, websocket_stop_exclusion) websocket_api.async_register_command(hass, websocket_refresh_node_info) + websocket_api.async_register_command(hass, websocket_subscribe_logs) websocket_api.async_register_command(hass, websocket_update_log_config) websocket_api.async_register_command(hass, websocket_get_log_config) websocket_api.async_register_command(hass, websocket_get_config_parameters) @@ -517,6 +519,53 @@ def filename_is_present_if_logging_to_file(obj: dict) -> dict: return obj +@websocket_api.require_admin # type: ignore +@websocket_api.async_response +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/subscribe_logs", + vol.Required(ENTRY_ID): str, + } +) +@async_get_entry +async def websocket_subscribe_logs( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + entry: ConfigEntry, + client: Client, +) -> None: + """Subscribe to log message events from the server.""" + driver = client.driver + + @callback + def async_cleanup() -> None: + """Remove signal listeners.""" + hass.async_create_task(driver.async_stop_listening_logs()) + unsub() + + @callback + def forward_event(event: dict) -> None: + log_msg: LogMessage = event["log_message"] + connection.send_message( + websocket_api.event_message( + msg[ID], + { + "timestamp": log_msg.timestamp, + "level": log_msg.level, + "primary_tags": log_msg.primary_tags, + "message": log_msg.formatted_message, + }, + ) + ) + + unsub = driver.on("logging", forward_event) + connection.subscriptions[msg["id"]] = async_cleanup + + await driver.async_start_listening_logs() + connection.send_result(msg[ID]) + + @websocket_api.require_admin # type: ignore @websocket_api.async_response @websocket_api.websocket_command( diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index 525f97da68128..3fb57a366aa8f 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -445,6 +445,48 @@ async def test_dump_view_invalid_entry_id(integration, hass_client): assert resp.status == 400 +async def test_subscribe_logs(hass, integration, client, hass_ws_client): + """Test the subscribe_logs websocket command.""" + entry = integration + ws_client = await hass_ws_client(hass) + + client.async_send_command.return_value = {} + + await ws_client.send_json( + {ID: 1, TYPE: "zwave_js/subscribe_logs", ENTRY_ID: entry.entry_id} + ) + + msg = await ws_client.receive_json() + assert msg["success"] + + event = Event( + type="logging", + data={ + "source": "driver", + "event": "logging", + "message": "test", + "formattedMessage": "test", + "direction": ">", + "level": "debug", + "primaryTags": "tag", + "secondaryTags": "tag2", + "secondaryTagPadding": 0, + "multiline": False, + "timestamp": "time", + "label": "label", + }, + ) + client.driver.receive_event(event) + + msg = await ws_client.receive_json() + assert msg["event"] == { + "message": ["test"], + "level": "debug", + "primary_tags": "tag", + "timestamp": "time", + } + + async def test_update_log_config(hass, client, integration, hass_ws_client): """Test that the update_log_config WS API call works and that schema validation works.""" entry = integration From dc24ce491bdba6d2ec1511ceb06ba4c33751322a Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Wed, 21 Apr 2021 11:45:50 -0700 Subject: [PATCH 0434/1317] Add Screenlogic set_color_mode service (#49366) --- .coveragerc | 1 + .../components/screenlogic/__init__.py | 8 +- homeassistant/components/screenlogic/const.py | 9 ++ .../components/screenlogic/manifest.json | 2 +- .../components/screenlogic/services.py | 89 +++++++++++++++++++ .../components/screenlogic/services.yaml | 38 ++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 8 files changed, 147 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/screenlogic/services.py create mode 100644 homeassistant/components/screenlogic/services.yaml diff --git a/.coveragerc b/.coveragerc index 0078882e167f5..86b129f636c9f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -866,6 +866,7 @@ omit = homeassistant/components/screenlogic/binary_sensor.py homeassistant/components/screenlogic/climate.py homeassistant/components/screenlogic/sensor.py + homeassistant/components/screenlogic/services.py homeassistant/components/screenlogic/switch.py homeassistant/components/scsgate/* homeassistant/components/scsgate/cover.py diff --git a/homeassistant/components/screenlogic/__init__.py b/homeassistant/components/screenlogic/__init__.py index c5c082cd509d0..cb747b3ed84b9 100644 --- a/homeassistant/components/screenlogic/__init__.py +++ b/homeassistant/components/screenlogic/__init__.py @@ -25,6 +25,7 @@ from .config_flow import async_discover_gateways_by_unique_id, name_for_mac from .const import DEFAULT_SCAN_INTERVAL, DISCOVERED_GATEWAYS, DOMAIN +from .services import async_load_screenlogic_services, async_unload_screenlogic_services _LOGGER = logging.getLogger(__name__) @@ -68,10 +69,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass, config_entry=entry, gateway=gateway, api_lock=api_lock ) - device_data = defaultdict(list) + async_load_screenlogic_services(hass) await coordinator.async_config_entry_first_refresh() + device_data = defaultdict(list) + for circuit in coordinator.data["circuits"]: device_data["switch"].append(circuit) @@ -120,6 +123,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) + async_unload_screenlogic_services(hass) + return unload_ok @@ -137,6 +142,7 @@ def __init__(self, hass, *, config_entry, gateway, api_lock): self.gateway = gateway self.api_lock = api_lock self.screenlogic_data = {} + interval = timedelta( seconds=config_entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) ) diff --git a/homeassistant/components/screenlogic/const.py b/homeassistant/components/screenlogic/const.py index d777dc6ddc503..49a57b8d46e8e 100644 --- a/homeassistant/components/screenlogic/const.py +++ b/homeassistant/components/screenlogic/const.py @@ -1,7 +1,16 @@ """Constants for the ScreenLogic integration.""" +from screenlogicpy.const import COLOR_MODE + +from homeassistant.util import slugify DOMAIN = "screenlogic" DEFAULT_SCAN_INTERVAL = 30 MIN_SCAN_INTERVAL = 10 +SERVICE_SET_COLOR_MODE = "set_color_mode" +ATTR_COLOR_MODE = "color_mode" +SUPPORTED_COLOR_MODES = { + slugify(name): num for num, name in COLOR_MODE.NAME_FOR_NUM.items() +} + DISCOVERED_GATEWAYS = "_discovered_gateways" diff --git a/homeassistant/components/screenlogic/manifest.json b/homeassistant/components/screenlogic/manifest.json index e62c5ba1f8ae7..e4d1be9bfb4fd 100644 --- a/homeassistant/components/screenlogic/manifest.json +++ b/homeassistant/components/screenlogic/manifest.json @@ -3,7 +3,7 @@ "name": "Pentair ScreenLogic", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/screenlogic", - "requirements": ["screenlogicpy==0.2.1"], + "requirements": ["screenlogicpy==0.3.0"], "codeowners": ["@dieselrabbit"], "dhcp": [ { diff --git a/homeassistant/components/screenlogic/services.py b/homeassistant/components/screenlogic/services.py new file mode 100644 index 0000000000000..7ca2bb6912923 --- /dev/null +++ b/homeassistant/components/screenlogic/services.py @@ -0,0 +1,89 @@ +"""Services for ScreenLogic integration.""" + +import logging + +from screenlogicpy import ScreenLogicError +import voluptuous as vol + +from homeassistant.core import ServiceCall, callback +from homeassistant.exceptions import HomeAssistantError +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.service import async_extract_config_entry_ids +from homeassistant.helpers.typing import HomeAssistantType + +from .const import ( + ATTR_COLOR_MODE, + DOMAIN, + SERVICE_SET_COLOR_MODE, + SUPPORTED_COLOR_MODES, +) + +_LOGGER = logging.getLogger(__name__) + +SET_COLOR_MODE_SCHEMA = cv.make_entity_service_schema( + { + vol.Required(ATTR_COLOR_MODE): vol.In(SUPPORTED_COLOR_MODES), + }, +) + + +@callback +def async_load_screenlogic_services(hass: HomeAssistantType): + """Set up services for the ScreenLogic integration.""" + if hass.services.has_service(DOMAIN, SERVICE_SET_COLOR_MODE): + # Integration-level services have already been added. Return. + return + + async def extract_screenlogic_config_entry_ids(service_call: ServiceCall): + return [ + entry_id + for entry_id in await async_extract_config_entry_ids(hass, service_call) + if hass.config_entries.async_get_entry(entry_id).domain == DOMAIN + ] + + async def async_set_color_mode(service_call: ServiceCall): + if not ( + screenlogic_entry_ids := await extract_screenlogic_config_entry_ids( + service_call + ) + ): + raise HomeAssistantError( + f"Failed to call service '{SERVICE_SET_COLOR_MODE}'. Config entry for target not found" + ) + color_num = SUPPORTED_COLOR_MODES[service_call.data[ATTR_COLOR_MODE]] + for entry_id in screenlogic_entry_ids: + coordinator = hass.data[DOMAIN][entry_id]["coordinator"] + _LOGGER.debug( + "Service %s called on %s with mode %s", + SERVICE_SET_COLOR_MODE, + coordinator.gateway.name, + color_num, + ) + try: + async with coordinator.api_lock: + if not await hass.async_add_executor_job( + coordinator.gateway.set_color_lights, color_num + ): + raise HomeAssistantError( + f"Failed to call service '{SERVICE_SET_COLOR_MODE}'" + ) + except ScreenLogicError as error: + raise HomeAssistantError(error) from error + + hass.services.async_register( + DOMAIN, SERVICE_SET_COLOR_MODE, async_set_color_mode, SET_COLOR_MODE_SCHEMA + ) + + +@callback +def async_unload_screenlogic_services(hass: HomeAssistantType): + """Unload services for the ScreenLogic integration.""" + if hass.data[DOMAIN]: + # There is still another config entry for this domain, don't remove services. + return + + if not hass.services.has_service(DOMAIN, SERVICE_SET_COLOR_MODE): + return + + _LOGGER.info("Unloading ScreenLogic Services") + hass.services.async_remove(domain=DOMAIN, service=SERVICE_SET_COLOR_MODE) diff --git a/homeassistant/components/screenlogic/services.yaml b/homeassistant/components/screenlogic/services.yaml new file mode 100644 index 0000000000000..7b54b9541d21a --- /dev/null +++ b/homeassistant/components/screenlogic/services.yaml @@ -0,0 +1,38 @@ +# ScreenLogic Services +set_color_mode: + name: Set Color Mode + description: Sets the color mode for all color-capable lights attached to this ScreenLogic gateway. + target: + device: + integration: screenlogic + fields: + color_mode: + name: Color Mode + description: The ScreenLogic color mode to set + required: true + example: "romance" + selector: + select: + options: + - all_off + - all_on + - color_set + - color_sync + - color_swim + - party + - romance + - caribbean + - american + - sunset + - royal + - save + - recall + - blue + - green + - red + - white + - magenta + - thumper + - next_mode + - reset + - hold diff --git a/requirements_all.txt b/requirements_all.txt index 644682ad29eae..b52b8415755a7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2024,7 +2024,7 @@ scapy==2.4.4 schiene==0.23 # homeassistant.components.screenlogic -screenlogicpy==0.2.1 +screenlogicpy==0.3.0 # homeassistant.components.scsgate scsgate==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f90dcf3ff6968..b0b64af715998 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1073,7 +1073,7 @@ samsungtvws==1.6.0 scapy==2.4.4 # homeassistant.components.screenlogic -screenlogicpy==0.2.1 +screenlogicpy==0.3.0 # homeassistant.components.emulated_kasa # homeassistant.components.sense From 69c1721c2a0fe124b2bd05e03e76b836c11e66d4 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 22 Apr 2021 00:02:50 +0000 Subject: [PATCH 0435/1317] [ci skip] Translation update --- .../components/adguard/translations/es.json | 1 + .../components/august/translations/es.json | 7 ++- .../components/cast/translations/es.json | 4 +- .../components/climacell/translations/es.json | 3 ++ .../coronavirus/translations/es.json | 3 +- .../components/emonitor/translations/es.json | 23 ++++++++ .../enphase_envoy/translations/es.json | 23 ++++++++ .../components/ezviz/translations/es.json | 52 +++++++++++++++++++ .../fritzbox_callmonitor/translations/es.json | 4 +- .../google_travel_time/translations/es.json | 22 ++++++-- .../components/habitica/translations/es.json | 5 ++ .../components/hive/translations/es.json | 13 ++++- .../home_plus_control/translations/es.json | 21 ++++++++ .../huawei_lte/translations/ca.json | 3 +- .../huawei_lte/translations/es.json | 3 +- .../huawei_lte/translations/et.json | 3 +- .../huawei_lte/translations/it.json | 3 +- .../huawei_lte/translations/nl.json | 3 +- .../huawei_lte/translations/no.json | 3 +- .../huawei_lte/translations/ru.json | 3 +- .../huawei_lte/translations/zh-Hant.json | 3 +- .../huisbaasje/translations/es.json | 1 + .../components/hyperion/translations/es.json | 1 + .../components/ialarm/translations/es.json | 20 +++++++ .../components/kmtronic/translations/es.json | 2 + .../kostal_plenticore/translations/es.json | 21 ++++++++ .../components/litejet/translations/es.json | 3 ++ .../components/lyric/translations/es.json | 7 ++- .../components/mazda/translations/es.json | 10 +++- .../components/met/translations/es.json | 3 ++ .../met_eireann/translations/es.json | 19 +++++++ .../components/mullvad/translations/es.json | 1 + .../components/mysensors/translations/es.json | 7 ++- .../components/mysensors/translations/it.json | 1 + .../components/mysensors/translations/nl.json | 1 + .../components/mysensors/translations/no.json | 1 + .../mysensors/translations/zh-Hant.json | 1 + .../components/nuki/translations/es.json | 10 ++++ .../components/nut/translations/es.json | 4 ++ .../opentherm_gw/translations/es.json | 3 +- .../philips_js/translations/es.json | 10 +++- .../components/powerwall/translations/es.json | 7 ++- .../translations/nl.json | 2 +- .../components/roku/translations/es.json | 1 + .../components/roomba/translations/es.json | 3 +- .../screenlogic/translations/es.json | 10 ++++ .../components/sma/translations/es.json | 20 ++++++- .../components/smarttub/translations/es.json | 12 +++++ .../components/subaru/translations/es.json | 9 +++- .../components/tesla/translations/es.json | 4 ++ .../totalconnect/translations/es.json | 6 ++- .../components/tuya/translations/ca.json | 2 +- .../components/tuya/translations/en.json | 4 +- .../components/tuya/translations/it.json | 2 +- .../components/tuya/translations/ru.json | 2 +- .../components/unifi/translations/ru.json | 8 +-- .../components/verisure/translations/es.json | 16 +++++- .../water_heater/translations/es.json | 1 + .../waze_travel_time/translations/es.json | 29 ++++++++++- .../xiaomi_miio/translations/es.json | 4 +- 60 files changed, 428 insertions(+), 45 deletions(-) create mode 100644 homeassistant/components/emonitor/translations/es.json create mode 100644 homeassistant/components/enphase_envoy/translations/es.json create mode 100644 homeassistant/components/ezviz/translations/es.json create mode 100644 homeassistant/components/home_plus_control/translations/es.json create mode 100644 homeassistant/components/ialarm/translations/es.json create mode 100644 homeassistant/components/kostal_plenticore/translations/es.json create mode 100644 homeassistant/components/met_eireann/translations/es.json diff --git a/homeassistant/components/adguard/translations/es.json b/homeassistant/components/adguard/translations/es.json index 3ffdb6b9eb0b8..fa12995ea5985 100644 --- a/homeassistant/components/adguard/translations/es.json +++ b/homeassistant/components/adguard/translations/es.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "El servicio ya est\u00e1 configurado", "existing_instance_updated": "Se ha actualizado la configuraci\u00f3n existente.", "single_instance_allowed": "S\u00f3lo se permite una \u00fanica configuraci\u00f3n de AdGuard Home." }, diff --git a/homeassistant/components/august/translations/es.json b/homeassistant/components/august/translations/es.json index bb343e6da97d8..d30db423db64b 100644 --- a/homeassistant/components/august/translations/es.json +++ b/homeassistant/components/august/translations/es.json @@ -11,6 +11,9 @@ }, "step": { "reauth_validate": { + "data": { + "password": "Contrase\u00f1a" + }, "description": "Introduzca la contrase\u00f1a de {username}.", "title": "Reautorizar una cuenta de August" }, @@ -26,7 +29,9 @@ }, "user_validate": { "data": { - "login_method": "M\u00e9todo de inicio de sesi\u00f3n" + "login_method": "M\u00e9todo de inicio de sesi\u00f3n", + "password": "Contrase\u00f1a", + "username": "Usuario" }, "description": "Si el m\u00e9todo de inicio de sesi\u00f3n es \"correo electr\u00f3nico\", el nombre de usuario es la direcci\u00f3n de correo electr\u00f3nico. Si el m\u00e9todo de inicio de sesi\u00f3n es \"tel\u00e9fono\", el nombre de usuario es el n\u00famero de tel\u00e9fono en el formato \"+NNNNNNN\".", "title": "Configurar una cuenta de August" diff --git a/homeassistant/components/cast/translations/es.json b/homeassistant/components/cast/translations/es.json index 07b090634e0a6..17b0ff4c2c44f 100644 --- a/homeassistant/components/cast/translations/es.json +++ b/homeassistant/components/cast/translations/es.json @@ -27,7 +27,9 @@ "step": { "options": { "data": { - "known_hosts": "Lista opcional de hosts conocidos si el descubrimiento mDNS no funciona." + "ignore_cec": "Lista opcional que se pasar\u00e1 a pychromecast.IGNORE_CEC.", + "known_hosts": "Lista opcional de hosts conocidos si el descubrimiento mDNS no funciona.", + "uuid": "Lista opcional de UUIDs. Los cast que no aparezcan en la lista no se a\u00f1adir\u00e1n." }, "description": "Introduce la configuraci\u00f3n de Google Cast." } diff --git a/homeassistant/components/climacell/translations/es.json b/homeassistant/components/climacell/translations/es.json index 52fd5d21166da..ec3bfd1596785 100644 --- a/homeassistant/components/climacell/translations/es.json +++ b/homeassistant/components/climacell/translations/es.json @@ -2,12 +2,15 @@ "config": { "error": { "cannot_connect": "Fallo al conectar", + "invalid_api_key": "Clave API no v\u00e1lida", "rate_limited": "Actualmente la tarifa est\u00e1 limitada, por favor int\u00e9ntelo m\u00e1s tarde.", "unknown": "Error inesperado" }, "step": { "user": { "data": { + "api_key": "Clave API", + "api_version": "Versi\u00f3n del API", "latitude": "Latitud", "longitude": "Longitud", "name": "Nombre" diff --git a/homeassistant/components/coronavirus/translations/es.json b/homeassistant/components/coronavirus/translations/es.json index 8363007d85abd..160bdc219a643 100644 --- a/homeassistant/components/coronavirus/translations/es.json +++ b/homeassistant/components/coronavirus/translations/es.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "El servicio ya est\u00e1 configurado" + "already_configured": "El servicio ya est\u00e1 configurado", + "cannot_connect": "No se pudo conectar" }, "step": { "user": { diff --git a/homeassistant/components/emonitor/translations/es.json b/homeassistant/components/emonitor/translations/es.json new file mode 100644 index 0000000000000..bef4b3b23293e --- /dev/null +++ b/homeassistant/components/emonitor/translations/es.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "unknown": "Error inesperado" + }, + "flow_title": "SiteSage {name}", + "step": { + "confirm": { + "description": "\u00bfQuieres configurar {name} ({host})?", + "title": "Configurar SiteSage Emonitor" + }, + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/es.json b/homeassistant/components/enphase_envoy/translations/es.json new file mode 100644 index 0000000000000..a8166b2c71f38 --- /dev/null +++ b/homeassistant/components/enphase_envoy/translations/es.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" + }, + "flow_title": "Envoy {serial} ({host})", + "step": { + "user": { + "data": { + "host": "Host", + "password": "Contrase\u00f1a", + "username": "Usuario" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ezviz/translations/es.json b/homeassistant/components/ezviz/translations/es.json new file mode 100644 index 0000000000000..a0f624bf8df14 --- /dev/null +++ b/homeassistant/components/ezviz/translations/es.json @@ -0,0 +1,52 @@ +{ + "config": { + "abort": { + "already_configured_account": "La cuenta ya ha sido configurada", + "ezviz_cloud_account_missing": "Falta la cuenta de Ezviz Cloud. Por favor, reconfigura la cuenta de Ezviz Cloud", + "unknown": "Error inesperado" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "invalid_host": "Nombre de host o direcci\u00f3n IP no v\u00e1lidos" + }, + "flow_title": "{serial}", + "step": { + "confirm": { + "data": { + "password": "Contrase\u00f1a", + "username": "Usuario" + }, + "description": "Introduce las credenciales RTSP para la c\u00e1mara Ezviz {serial} con IP {ip_address}", + "title": "Descubierta c\u00e1mara Ezviz" + }, + "user": { + "data": { + "password": "Contrase\u00f1a", + "url": "URL", + "username": "Usuario" + }, + "title": "Conectar con Ezviz Cloud" + }, + "user_custom_url": { + "data": { + "password": "Contrase\u00f1a", + "url": "URL", + "username": "Usuario" + }, + "description": "Especificar manualmente la URL de tu regi\u00f3n", + "title": "Conectar con la URL personalizada de Ezviz" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ffmpeg_arguments": "Par\u00e1metros pasados a ffmpeg para c\u00e1maras", + "timeout": "Tiempo de espera de la solicitud (segundos)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox_callmonitor/translations/es.json b/homeassistant/components/fritzbox_callmonitor/translations/es.json index 4d4aa4cd86b84..d6891db5ef9f7 100644 --- a/homeassistant/components/fritzbox_callmonitor/translations/es.json +++ b/homeassistant/components/fritzbox_callmonitor/translations/es.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "insufficient_permissions": "El usuario no tiene permisos suficientes para acceder a la configuraci\u00f3n de AVM FRITZ! Box y sus agendas telef\u00f3nicas." + "already_configured": "El dispositivo ya est\u00e1 configurado", + "insufficient_permissions": "El usuario no tiene permisos suficientes para acceder a la configuraci\u00f3n de AVM FRITZ! Box y sus agendas telef\u00f3nicas.", + "no_devices_found": "No se encontraron dispositivos en la red" }, "error": { "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" diff --git a/homeassistant/components/google_travel_time/translations/es.json b/homeassistant/components/google_travel_time/translations/es.json index 8224454237516..1c59ce079978e 100644 --- a/homeassistant/components/google_travel_time/translations/es.json +++ b/homeassistant/components/google_travel_time/translations/es.json @@ -1,11 +1,19 @@ { "config": { + "abort": { + "already_configured": "La ubicaci\u00f3n ya est\u00e1 configurada" + }, + "error": { + "cannot_connect": "No se pudo conectar" + }, "step": { "user": { "data": { + "api_key": "Clave API", "destination": "Destino", "origin": "Origen" - } + }, + "description": "Al especificar el origen y el destino, puedes proporcionar una o m\u00e1s ubicaciones separadas por el car\u00e1cter de barra vertical, en forma de una direcci\u00f3n, coordenadas de latitud/longitud o un ID de lugar de Google. Al especificar la ubicaci\u00f3n utilizando un ID de lugar de Google, el ID debe tener el prefijo `place_id:`." } } }, @@ -13,10 +21,18 @@ "step": { "init": { "data": { + "avoid": "Evitar", "language": "Idioma", + "mode": "Modo de viaje", + "time": "Hora", + "time_type": "Tipo de tiempo", + "transit_mode": "Modo de tr\u00e1nsito", + "transit_routing_preference": "Preferencia de enrutamiento de tr\u00e1nsito", "units": "Unidades" - } + }, + "description": "Opcionalmente, puedes especificar una hora de salida o una hora de llegada. Si especifica una hora de salida, puedes introducir `ahora`, una marca de tiempo Unix o una cadena de tiempo 24 horas como `08:00:00`. Si especifica una hora de llegada, puede usar una marca de tiempo Unix o una cadena de tiempo 24 horas como `08:00:00`" } } - } + }, + "title": "Tiempo de viaje de Google Maps" } \ No newline at end of file diff --git a/homeassistant/components/habitica/translations/es.json b/homeassistant/components/habitica/translations/es.json index afdbb6666ad14..6850c903b9911 100644 --- a/homeassistant/components/habitica/translations/es.json +++ b/homeassistant/components/habitica/translations/es.json @@ -1,8 +1,13 @@ { "config": { + "error": { + "invalid_credentials": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" + }, "step": { "user": { "data": { + "api_key": "Clave API", "api_user": "ID de usuario de la API de Habitica", "name": "Anular el nombre de usuario de Habitica. Se utilizar\u00e1 para llamadas de servicio.", "url": "URL" diff --git a/homeassistant/components/hive/translations/es.json b/homeassistant/components/hive/translations/es.json index eb5ef0fd6eb01..727a33ec66e5b 100644 --- a/homeassistant/components/hive/translations/es.json +++ b/homeassistant/components/hive/translations/es.json @@ -1,13 +1,16 @@ { "config": { "abort": { + "already_configured": "La cuenta ya ha sido configurada", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", "unknown_entry": "No se puede encontrar una entrada existente." }, "error": { "invalid_code": "No se ha podido iniciar la sesi\u00f3n en Hive. Tu c\u00f3digo de autenticaci\u00f3n de dos factores era incorrecto.", "invalid_password": "No se ha podido iniciar la sesi\u00f3n en Hive. Contrase\u00f1a incorrecta, por favor, int\u00e9ntelo de nuevo.", "invalid_username": "No se ha podido iniciar la sesi\u00f3n en Hive. No se reconoce su direcci\u00f3n de correo electr\u00f3nico.", - "no_internet_available": "Se requiere una conexi\u00f3n a Internet para conectarse a Hive." + "no_internet_available": "Se requiere una conexi\u00f3n a Internet para conectarse a Hive.", + "unknown": "Error inesperado" }, "step": { "2fa": { @@ -18,12 +21,18 @@ "title": "Autenticaci\u00f3n de dos factores de Hive." }, "reauth": { + "data": { + "password": "Contrase\u00f1a", + "username": "Usuario" + }, "description": "Vuelva a introducir sus datos de acceso a Hive.", "title": "Inicio de sesi\u00f3n en Hive" }, "user": { "data": { - "scan_interval": "Intervalo de exploraci\u00f3n (segundos)" + "password": "Contrase\u00f1a", + "scan_interval": "Intervalo de exploraci\u00f3n (segundos)", + "username": "Usuario" }, "description": "Ingrese su configuraci\u00f3n e informaci\u00f3n de inicio de sesi\u00f3n de Hive.", "title": "Inicio de sesi\u00f3n en Hive" diff --git a/homeassistant/components/home_plus_control/translations/es.json b/homeassistant/components/home_plus_control/translations/es.json new file mode 100644 index 0000000000000..3c471ffc75eac --- /dev/null +++ b/homeassistant/components/home_plus_control/translations/es.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "La cuenta ya ha sido configurada", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n.", + "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", + "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", + "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." + }, + "create_entry": { + "default": "Autenticado correctamente" + }, + "step": { + "pick_implementation": { + "title": "Selecciona el m\u00e9todo de autenticaci\u00f3n" + } + } + }, + "title": "Legrand Home+ Control" +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/translations/ca.json b/homeassistant/components/huawei_lte/translations/ca.json index 86e48641a574f..73c5bc9b8e1cf 100644 --- a/homeassistant/components/huawei_lte/translations/ca.json +++ b/homeassistant/components/huawei_lte/translations/ca.json @@ -34,7 +34,8 @@ "data": { "name": "Nom del servei de notificacions (reinici necessari si canvia)", "recipient": "Destinataris de notificacions SMS", - "track_new_devices": "Segueix dispositius nous" + "track_new_devices": "Segueix dispositius nous", + "track_wired_clients": "Segueix els clients connectats a la xarxa per cable" } } } diff --git a/homeassistant/components/huawei_lte/translations/es.json b/homeassistant/components/huawei_lte/translations/es.json index d283073281db5..00564d7282a97 100644 --- a/homeassistant/components/huawei_lte/translations/es.json +++ b/homeassistant/components/huawei_lte/translations/es.json @@ -34,7 +34,8 @@ "data": { "name": "Nombre del servicio de notificaci\u00f3n (el cambio requiere reiniciar)", "recipient": "Destinatarios de notificaciones por SMS", - "track_new_devices": "Rastrea nuevos dispositivos" + "track_new_devices": "Rastrea nuevos dispositivos", + "track_wired_clients": "Seguir clientes de red cableados" } } } diff --git a/homeassistant/components/huawei_lte/translations/et.json b/homeassistant/components/huawei_lte/translations/et.json index 17647ef687750..3c674c0344c7c 100644 --- a/homeassistant/components/huawei_lte/translations/et.json +++ b/homeassistant/components/huawei_lte/translations/et.json @@ -34,7 +34,8 @@ "data": { "name": "Teavitusteenuse nimi (muudatus n\u00f5uab taask\u00e4ivitamist)", "recipient": "SMS teavituse saajad", - "track_new_devices": "Uute seadmete j\u00e4lgimine" + "track_new_devices": "Uute seadmete j\u00e4lgimine", + "track_wired_clients": "J\u00e4lgi juhtmega v\u00f5rgukliente" } } } diff --git a/homeassistant/components/huawei_lte/translations/it.json b/homeassistant/components/huawei_lte/translations/it.json index 675cc7ad9693e..545d3b35daf67 100644 --- a/homeassistant/components/huawei_lte/translations/it.json +++ b/homeassistant/components/huawei_lte/translations/it.json @@ -34,7 +34,8 @@ "data": { "name": "Nome del servizio di notifica (la modifica richiede il riavvio)", "recipient": "Destinatari della notifica SMS", - "track_new_devices": "Traccia nuovi dispositivi" + "track_new_devices": "Traccia nuovi dispositivi", + "track_wired_clients": "Tieni traccia dei client di rete cablata" } } } diff --git a/homeassistant/components/huawei_lte/translations/nl.json b/homeassistant/components/huawei_lte/translations/nl.json index 799a9ce50af1e..11d450abc3b2a 100644 --- a/homeassistant/components/huawei_lte/translations/nl.json +++ b/homeassistant/components/huawei_lte/translations/nl.json @@ -34,7 +34,8 @@ "data": { "name": "Naam meldingsservice (wijziging vereist opnieuw opstarten)", "recipient": "Ontvangers van sms-berichten", - "track_new_devices": "Volg nieuwe apparaten" + "track_new_devices": "Volg nieuwe apparaten", + "track_wired_clients": "Volg bekabelde netwerkclients" } } } diff --git a/homeassistant/components/huawei_lte/translations/no.json b/homeassistant/components/huawei_lte/translations/no.json index 9cd5e164464b6..4a9966c9339d3 100644 --- a/homeassistant/components/huawei_lte/translations/no.json +++ b/homeassistant/components/huawei_lte/translations/no.json @@ -34,7 +34,8 @@ "data": { "name": "Navn p\u00e5 varslingstjeneste (endring krever omstart)", "recipient": "Mottakere av SMS-varsling", - "track_new_devices": "Spor nye enheter" + "track_new_devices": "Spor nye enheter", + "track_wired_clients": "Spor kablede nettverksklienter" } } } diff --git a/homeassistant/components/huawei_lte/translations/ru.json b/homeassistant/components/huawei_lte/translations/ru.json index d3f95e3fbf1bb..d679f8d28616e 100644 --- a/homeassistant/components/huawei_lte/translations/ru.json +++ b/homeassistant/components/huawei_lte/translations/ru.json @@ -34,7 +34,8 @@ "data": { "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0441\u043b\u0443\u0436\u0431\u044b \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0439 (\u043f\u043e\u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u043a)", "recipient": "\u041f\u043e\u043b\u0443\u0447\u0430\u0442\u0435\u043b\u0438 SMS-\u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0439", - "track_new_devices": "\u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0442\u044c \u043d\u043e\u0432\u044b\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" + "track_new_devices": "\u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0442\u044c \u043d\u043e\u0432\u044b\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", + "track_wired_clients": "\u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0442\u044c \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u043f\u0440\u043e\u0432\u043e\u0434\u043d\u043e\u0439 \u0441\u0435\u0442\u0438" } } } diff --git a/homeassistant/components/huawei_lte/translations/zh-Hant.json b/homeassistant/components/huawei_lte/translations/zh-Hant.json index 48b568b43d663..bc929fdcbba86 100644 --- a/homeassistant/components/huawei_lte/translations/zh-Hant.json +++ b/homeassistant/components/huawei_lte/translations/zh-Hant.json @@ -34,7 +34,8 @@ "data": { "name": "\u901a\u77e5\u670d\u52d9\u540d\u7a31\uff08\u8b8a\u66f4\u5f8c\u9700\u91cd\u555f\uff09", "recipient": "\u7c21\u8a0a\u901a\u77e5\u6536\u4ef6\u8005", - "track_new_devices": "\u8ffd\u8e64\u65b0\u88dd\u7f6e" + "track_new_devices": "\u8ffd\u8e64\u65b0\u88dd\u7f6e", + "track_wired_clients": "\u8ffd\u8e64\u6709\u7dda\u7db2\u8def\u5ba2\u6236\u7aef" } } } diff --git a/homeassistant/components/huisbaasje/translations/es.json b/homeassistant/components/huisbaasje/translations/es.json index def06b0941dec..a66da5b00d988 100644 --- a/homeassistant/components/huisbaasje/translations/es.json +++ b/homeassistant/components/huisbaasje/translations/es.json @@ -4,6 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { + "cannot_connect": "No se pudo conectar", "connection_exception": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unauthenticated_exception": "Autenticaci\u00f3n no v\u00e1lida", diff --git a/homeassistant/components/hyperion/translations/es.json b/homeassistant/components/hyperion/translations/es.json index db3aa75462a83..5b4534069dd85 100644 --- a/homeassistant/components/hyperion/translations/es.json +++ b/homeassistant/components/hyperion/translations/es.json @@ -45,6 +45,7 @@ "step": { "init": { "data": { + "effect_show_list": "Efectos de Hyperion a mostrar", "priority": "Prioridad de Hyperion a usar para colores y efectos" } } diff --git a/homeassistant/components/ialarm/translations/es.json b/homeassistant/components/ialarm/translations/es.json new file mode 100644 index 0000000000000..fcf028791ae14 --- /dev/null +++ b/homeassistant/components/ialarm/translations/es.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "host": "Host", + "pin": "C\u00f3digo PIN", + "port": "Puerto" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kmtronic/translations/es.json b/homeassistant/components/kmtronic/translations/es.json index f7c20f7805bad..822a37649fde4 100644 --- a/homeassistant/components/kmtronic/translations/es.json +++ b/homeassistant/components/kmtronic/translations/es.json @@ -5,11 +5,13 @@ }, "error": { "cannot_connect": "Fallo al conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, "step": { "user": { "data": { + "host": "Host", "password": "Contrase\u00f1a", "username": "Usuario" } diff --git a/homeassistant/components/kostal_plenticore/translations/es.json b/homeassistant/components/kostal_plenticore/translations/es.json new file mode 100644 index 0000000000000..e763acfbe4d28 --- /dev/null +++ b/homeassistant/components/kostal_plenticore/translations/es.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Contrase\u00f1a" + } + } + } + }, + "title": "Inversor solar Kostal Plenticore" +} \ No newline at end of file diff --git a/homeassistant/components/litejet/translations/es.json b/homeassistant/components/litejet/translations/es.json index b0641022bf01d..32d39e995e1f6 100644 --- a/homeassistant/components/litejet/translations/es.json +++ b/homeassistant/components/litejet/translations/es.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." + }, "error": { "open_failed": "No se puede abrir el puerto serie especificado." }, diff --git a/homeassistant/components/lyric/translations/es.json b/homeassistant/components/lyric/translations/es.json index db8d744d17620..404a812e67653 100644 --- a/homeassistant/components/lyric/translations/es.json +++ b/homeassistant/components/lyric/translations/es.json @@ -2,7 +2,8 @@ "config": { "abort": { "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n.", - "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n." + "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "create_entry": { "default": "Autenticado correctamente" @@ -10,6 +11,10 @@ "step": { "pick_implementation": { "title": "Selecciona el m\u00e9todo de autenticaci\u00f3n" + }, + "reauth_confirm": { + "description": "La integraci\u00f3n de Lyric necesita volver a autenticar tu cuenta.", + "title": "Volver a autenticar la integraci\u00f3n" } } } diff --git a/homeassistant/components/mazda/translations/es.json b/homeassistant/components/mazda/translations/es.json index 868ae0d770ea6..bfe1b43036573 100644 --- a/homeassistant/components/mazda/translations/es.json +++ b/homeassistant/components/mazda/translations/es.json @@ -1,11 +1,19 @@ { "config": { + "abort": { + "already_configured": "La cuenta ya ha sido configurada", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + }, "error": { - "account_locked": "Cuenta bloqueada. Por favor, int\u00e9ntelo de nuevo m\u00e1s tarde." + "account_locked": "Cuenta bloqueada. Por favor, int\u00e9ntelo de nuevo m\u00e1s tarde.", + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" }, "step": { "reauth": { "data": { + "email": "Correo electr\u00f3nico", "password": "Contrase\u00f1a", "region": "Regi\u00f3n" }, diff --git a/homeassistant/components/met/translations/es.json b/homeassistant/components/met/translations/es.json index 4c6c4aa19911c..b03e6636cf881 100644 --- a/homeassistant/components/met/translations/es.json +++ b/homeassistant/components/met/translations/es.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_home": "No se han establecido las coordenadas de casa en la configuraci\u00f3n de Home Assistant" + }, "error": { "already_configured": "El servicio ya est\u00e1 configurado" }, diff --git a/homeassistant/components/met_eireann/translations/es.json b/homeassistant/components/met_eireann/translations/es.json new file mode 100644 index 0000000000000..97b6518862ceb --- /dev/null +++ b/homeassistant/components/met_eireann/translations/es.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "already_configured": "El servicio ya est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "elevation": "Elevaci\u00f3n", + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Nombre" + }, + "description": "Introduce tu ubicaci\u00f3n para utilizar los datos meteorol\u00f3gicos de la API p\u00fablica de previsi\u00f3n meteorol\u00f3gica de Met \u00c9ireann", + "title": "Ubicaci\u00f3n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mullvad/translations/es.json b/homeassistant/components/mullvad/translations/es.json index 579726b061e52..7b64c9b11287f 100644 --- a/homeassistant/components/mullvad/translations/es.json +++ b/homeassistant/components/mullvad/translations/es.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "host": "Host", "password": "Contrase\u00f1a", "username": "Usuario" }, diff --git a/homeassistant/components/mysensors/translations/es.json b/homeassistant/components/mysensors/translations/es.json index 2a4b30910d17d..4bb5f5cfd1528 100644 --- a/homeassistant/components/mysensors/translations/es.json +++ b/homeassistant/components/mysensors/translations/es.json @@ -1,8 +1,11 @@ { "config": { "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "cannot_connect": "No se pudo conectar", "duplicate_persistence_file": "Archivo de persistencia ya en uso", "duplicate_topic": "Tema ya en uso", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "invalid_device": "Dispositivo no v\u00e1lido", "invalid_ip": "Direcci\u00f3n IP no v\u00e1lida", "invalid_persistence_file": "Archivo de persistencia no v\u00e1lido", @@ -13,7 +16,8 @@ "invalid_version": "Versi\u00f3n inv\u00e1lida de MySensors", "not_a_number": "Por favor, introduzca un n\u00famero", "port_out_of_range": "El n\u00famero de puerto debe ser como m\u00ednimo 1 y como m\u00e1ximo 65535", - "same_topic": "Los temas de suscripci\u00f3n y publicaci\u00f3n son los mismos" + "same_topic": "Los temas de suscripci\u00f3n y publicaci\u00f3n son los mismos", + "unknown": "Error inesperado" }, "error": { "already_configured": "El dispositivo ya est\u00e1 configurado", @@ -29,6 +33,7 @@ "invalid_serial": "Puerto serie no v\u00e1lido", "invalid_subscribe_topic": "Tema de suscripci\u00f3n no v\u00e1lido", "invalid_version": "Versi\u00f3n no v\u00e1lida de MySensors", + "mqtt_required": "La integraci\u00f3n MQTT no est\u00e1 configurada", "not_a_number": "Por favor, introduce un n\u00famero", "port_out_of_range": "El n\u00famero de puerto debe ser como m\u00ednimo 1 y como m\u00e1ximo 65535", "same_topic": "Los temas de suscripci\u00f3n y publicaci\u00f3n son los mismos", diff --git a/homeassistant/components/mysensors/translations/it.json b/homeassistant/components/mysensors/translations/it.json index f256ddb95eb27..8b13912015138 100644 --- a/homeassistant/components/mysensors/translations/it.json +++ b/homeassistant/components/mysensors/translations/it.json @@ -33,6 +33,7 @@ "invalid_serial": "Porta seriale non valida", "invalid_subscribe_topic": "Argomento di sottoscrizione non valido", "invalid_version": "Versione di MySensors non valida", + "mqtt_required": "L'integrazione MQTT non \u00e8 configurata", "not_a_number": "Per favore inserisci un numero", "port_out_of_range": "Il numero di porta deve essere almeno 1 e al massimo 65535", "same_topic": "Gli argomenti di sottoscrizione e pubblicazione sono gli stessi", diff --git a/homeassistant/components/mysensors/translations/nl.json b/homeassistant/components/mysensors/translations/nl.json index 49ddf987cef64..14055639f60be 100644 --- a/homeassistant/components/mysensors/translations/nl.json +++ b/homeassistant/components/mysensors/translations/nl.json @@ -33,6 +33,7 @@ "invalid_serial": "Ongeldige seri\u00eble poort", "invalid_subscribe_topic": "Ongeldig abonneer topic", "invalid_version": "Ongeldige MySensors-versie", + "mqtt_required": "De MQTT integratie is niet ingesteld", "not_a_number": "Voer een nummer in", "port_out_of_range": "Poortnummer moet minimaal 1 en maximaal 65535 zijn", "same_topic": "De topics abonneren en publiceren zijn hetzelfde", diff --git a/homeassistant/components/mysensors/translations/no.json b/homeassistant/components/mysensors/translations/no.json index 9d028260a7686..f0e307a1ab2e5 100644 --- a/homeassistant/components/mysensors/translations/no.json +++ b/homeassistant/components/mysensors/translations/no.json @@ -33,6 +33,7 @@ "invalid_serial": "Ugyldig serieport", "invalid_subscribe_topic": "Ugyldig abonnementsemne", "invalid_version": "Ugyldig MySensors-versjon", + "mqtt_required": "MQTT-integrasjonen er ikke satt opp", "not_a_number": "Vennligst skriv inn et nummer", "port_out_of_range": "Portnummer m\u00e5 v\u00e6re minst 1 og maksimalt 65535", "same_topic": "Abonner og publiser emner er de samme", diff --git a/homeassistant/components/mysensors/translations/zh-Hant.json b/homeassistant/components/mysensors/translations/zh-Hant.json index f70fc897b2283..234a2bd0b30dc 100644 --- a/homeassistant/components/mysensors/translations/zh-Hant.json +++ b/homeassistant/components/mysensors/translations/zh-Hant.json @@ -33,6 +33,7 @@ "invalid_serial": "\u5e8f\u5217\u57e0\u7121\u6548", "invalid_subscribe_topic": "\u8a02\u95b1\u4e3b\u984c\u7121\u6548", "invalid_version": "MySensors \u7248\u672c\u7121\u6548", + "mqtt_required": "MQTT \u6574\u5408\u5c1a\u672a\u8a2d\u5b9a", "not_a_number": "\u8acb\u8f38\u5165\u865f\u78bc", "port_out_of_range": "\u8acb\u8f38\u5165\u4ecb\u65bc 1 \u81f3 65535 \u4e4b\u9593\u7684\u865f\u78bc", "same_topic": "\u8a02\u95b1\u8207\u767c\u4f48\u4e3b\u984c\u76f8\u540c", diff --git a/homeassistant/components/nuki/translations/es.json b/homeassistant/components/nuki/translations/es.json index 8def4e2780d2e..33fe3f462df3f 100644 --- a/homeassistant/components/nuki/translations/es.json +++ b/homeassistant/components/nuki/translations/es.json @@ -1,11 +1,21 @@ { "config": { + "abort": { + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + }, "error": { "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, "step": { + "reauth_confirm": { + "data": { + "token": "Token de acceso" + }, + "description": "La integraci\u00f3n de Nuki debe volver a autenticarse con tu bridge.", + "title": "Volver a autenticar la integraci\u00f3n" + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/nut/translations/es.json b/homeassistant/components/nut/translations/es.json index c76fc0da7983a..234f34082e164 100644 --- a/homeassistant/components/nut/translations/es.json +++ b/homeassistant/components/nut/translations/es.json @@ -33,6 +33,10 @@ } }, "options": { + "error": { + "cannot_connect": "No se pudo conectar", + "unknown": "Error inesperado" + }, "step": { "init": { "data": { diff --git a/homeassistant/components/opentherm_gw/translations/es.json b/homeassistant/components/opentherm_gw/translations/es.json index 7a85b685e891d..e0799932a52a6 100644 --- a/homeassistant/components/opentherm_gw/translations/es.json +++ b/homeassistant/components/opentherm_gw/translations/es.json @@ -23,7 +23,8 @@ "floor_temperature": "Temperatura del suelo", "precision": "Precisi\u00f3n", "read_precision": "Leer precisi\u00f3n", - "set_precision": "Establecer precisi\u00f3n" + "set_precision": "Establecer precisi\u00f3n", + "temporary_override_mode": "Modo de anulaci\u00f3n temporal del punto de ajuste" }, "description": "Opciones para OpenTherm Gateway" } diff --git a/homeassistant/components/philips_js/translations/es.json b/homeassistant/components/philips_js/translations/es.json index c8d34e9ea9d6e..5cd00abc216b8 100644 --- a/homeassistant/components/philips_js/translations/es.json +++ b/homeassistant/components/philips_js/translations/es.json @@ -1,11 +1,19 @@ { "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, "error": { + "cannot_connect": "No se pudo conectar", "invalid_pin": "PIN no v\u00e1lido", - "pairing_failure": "No se ha podido emparejar: {error_id}" + "pairing_failure": "No se ha podido emparejar: {error_id}", + "unknown": "Error inesperado" }, "step": { "pair": { + "data": { + "pin": "C\u00f3digo PIN" + }, "description": "Introduzca el PIN que se muestra en el televisor", "title": "Par" }, diff --git a/homeassistant/components/powerwall/translations/es.json b/homeassistant/components/powerwall/translations/es.json index 81e3edab38772..f2beb19d5dac9 100644 --- a/homeassistant/components/powerwall/translations/es.json +++ b/homeassistant/components/powerwall/translations/es.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "El powerwall ya est\u00e1 configurado" + "already_configured": "El powerwall ya est\u00e1 configurado", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { "cannot_connect": "No se pudo conectar, por favor int\u00e9ntelo de nuevo", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado", "wrong_version": "Tu powerwall utiliza una versi\u00f3n de software que no es compatible. Considera actualizar o informar de este problema para que pueda resolverse." }, @@ -12,7 +14,8 @@ "step": { "user": { "data": { - "ip_address": "Direcci\u00f3n IP" + "ip_address": "Direcci\u00f3n IP", + "password": "Contrase\u00f1a" }, "description": "La contrase\u00f1a suele ser los \u00faltimos 5 caracteres del n\u00famero de serie del Backup Gateway y se puede encontrar en la aplicaci\u00f3n Telsa; o los \u00faltimos 5 caracteres de la contrase\u00f1a que se encuentran dentro de la puerta del Backup Gateway 2.", "title": "Conectarse al powerwall" diff --git a/homeassistant/components/rituals_perfume_genie/translations/nl.json b/homeassistant/components/rituals_perfume_genie/translations/nl.json index 432079cac257f..ddc5fcb062f87 100644 --- a/homeassistant/components/rituals_perfume_genie/translations/nl.json +++ b/homeassistant/components/rituals_perfume_genie/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Account is al geconfigureerd" + "already_configured": "Apparaat is al geconfigureerd" }, "error": { "cannot_connect": "Kan geen verbinding maken", diff --git a/homeassistant/components/roku/translations/es.json b/homeassistant/components/roku/translations/es.json index 95e42643379c1..189a4aec17994 100644 --- a/homeassistant/components/roku/translations/es.json +++ b/homeassistant/components/roku/translations/es.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", "unknown": "Error inesperado" }, "error": { diff --git a/homeassistant/components/roomba/translations/es.json b/homeassistant/components/roomba/translations/es.json index 29f0b47a655eb..c78b66bbb87ae 100644 --- a/homeassistant/components/roomba/translations/es.json +++ b/homeassistant/components/roomba/translations/es.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", "cannot_connect": "No se pudo conectar", - "not_irobot_device": "El dispositivo descubierto no es un dispositivo iRobot" + "not_irobot_device": "El dispositivo descubierto no es un dispositivo iRobot", + "short_blid": "El BLID ha sido truncado" }, "error": { "cannot_connect": "No se pudo conectar" diff --git a/homeassistant/components/screenlogic/translations/es.json b/homeassistant/components/screenlogic/translations/es.json index 8e9513d4f7530..c890d3bf10cac 100644 --- a/homeassistant/components/screenlogic/translations/es.json +++ b/homeassistant/components/screenlogic/translations/es.json @@ -1,8 +1,18 @@ { "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar" + }, "flow_title": "ScreenLogic {name}", "step": { "gateway_entry": { + "data": { + "ip_address": "Direcci\u00f3n IP", + "port": "Puerto" + }, "description": "Introduzca la informaci\u00f3n de su ScreenLogic Gateway.", "title": "ScreenLogic" }, diff --git a/homeassistant/components/sma/translations/es.json b/homeassistant/components/sma/translations/es.json index abfce5c74a166..76edc25241c7b 100644 --- a/homeassistant/components/sma/translations/es.json +++ b/homeassistant/components/sma/translations/es.json @@ -1,10 +1,26 @@ { "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "cannot_retrieve_device_info": "Conectado con \u00e9xito, pero no se puede recuperar la informaci\u00f3n del dispositivo", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" + }, "step": { "user": { "data": { - "group": "Grupo" - } + "group": "Grupo", + "host": "Host", + "password": "Contrase\u00f1a", + "ssl": "Utiliza un certificado SSL", + "verify_ssl": "Verificar certificado SSL" + }, + "description": "Introduce la informaci\u00f3n de tu dispositivo SMA.", + "title": "Configurar SMA Solar" } } } diff --git a/homeassistant/components/smarttub/translations/es.json b/homeassistant/components/smarttub/translations/es.json index df5b4122bc444..f7c225b02a1f0 100644 --- a/homeassistant/components/smarttub/translations/es.json +++ b/homeassistant/components/smarttub/translations/es.json @@ -1,7 +1,19 @@ { "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + }, + "error": { + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" + }, "step": { "user": { + "data": { + "email": "Correo electr\u00f3nico", + "password": "Contrase\u00f1a" + }, "description": "Introduzca su direcci\u00f3n de correo electr\u00f3nico y contrase\u00f1a de SmartTub para iniciar sesi\u00f3n", "title": "Inicio de sesi\u00f3n" } diff --git a/homeassistant/components/subaru/translations/es.json b/homeassistant/components/subaru/translations/es.json index deccc23c75dbb..ff8c5720781e8 100644 --- a/homeassistant/components/subaru/translations/es.json +++ b/homeassistant/components/subaru/translations/es.json @@ -1,8 +1,15 @@ { "config": { + "abort": { + "already_configured": "La cuenta ya ha sido configurada", + "cannot_connect": "No se pudo conectar" + }, "error": { "bad_pin_format": "El PIN debe tener 4 d\u00edgitos", - "incorrect_pin": "PIN incorrecto" + "cannot_connect": "No se pudo conectar", + "incorrect_pin": "PIN incorrecto", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" }, "step": { "pin": { diff --git a/homeassistant/components/tesla/translations/es.json b/homeassistant/components/tesla/translations/es.json index e2c5ed3c0eec8..54fbfd1a21d35 100644 --- a/homeassistant/components/tesla/translations/es.json +++ b/homeassistant/components/tesla/translations/es.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "already_configured": "La cuenta ya ha sido configurada", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + }, "error": { "already_configured": "La cuenta ya ha sido configurada", "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/totalconnect/translations/es.json b/homeassistant/components/totalconnect/translations/es.json index 85797fa901ee1..07837760a444b 100644 --- a/homeassistant/components/totalconnect/translations/es.json +++ b/homeassistant/components/totalconnect/translations/es.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "La cuenta ya ha sido configurada" + "already_configured": "La cuenta ya ha sido configurada", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", @@ -16,7 +17,8 @@ "title": "C\u00f3digos de usuario de ubicaci\u00f3n" }, "reauth_confirm": { - "description": "Total Connect necesita volver a autentificar tu cuenta" + "description": "Total Connect necesita volver a autentificar tu cuenta", + "title": "Volver a autenticar la integraci\u00f3n" }, "user": { "data": { diff --git a/homeassistant/components/tuya/translations/ca.json b/homeassistant/components/tuya/translations/ca.json index a00d9683141f1..62fad2ad47ffe 100644 --- a/homeassistant/components/tuya/translations/ca.json +++ b/homeassistant/components/tuya/translations/ca.json @@ -17,7 +17,7 @@ "platform": "L'aplicaci\u00f3 on es registra el teu compte", "username": "Nom d'usuari" }, - "description": "Introdueix la teva credencial de Tuya.", + "description": "Introdueix les teves credencial de Tuya.", "title": "Tuya" } } diff --git a/homeassistant/components/tuya/translations/en.json b/homeassistant/components/tuya/translations/en.json index 7204d6072a96d..ee304ff30cd4e 100644 --- a/homeassistant/components/tuya/translations/en.json +++ b/homeassistant/components/tuya/translations/en.json @@ -14,10 +14,10 @@ "data": { "country_code": "Your account country code (e.g., 1 for USA or 86 for China)", "password": "Password", - "platform": "The app where your account register", + "platform": "The app where your account is registered", "username": "Username" }, - "description": "Enter your Tuya credential.", + "description": "Enter your Tuya credentials.", "title": "Tuya" } } diff --git a/homeassistant/components/tuya/translations/it.json b/homeassistant/components/tuya/translations/it.json index 729514d35416a..a2a8dc874732e 100644 --- a/homeassistant/components/tuya/translations/it.json +++ b/homeassistant/components/tuya/translations/it.json @@ -14,7 +14,7 @@ "data": { "country_code": "Prefisso internazionale del tuo account (ad es. 1 per gli Stati Uniti o 86 per la Cina)", "password": "Password", - "platform": "L'app in cui si registra il tuo account", + "platform": "L'app in cui \u00e8 registrato il tuo account", "username": "Nome utente" }, "description": "Inserisci le tue credenziali Tuya.", diff --git a/homeassistant/components/tuya/translations/ru.json b/homeassistant/components/tuya/translations/ru.json index f40071ba400b3..7b46689bc5035 100644 --- a/homeassistant/components/tuya/translations/ru.json +++ b/homeassistant/components/tuya/translations/ru.json @@ -14,7 +14,7 @@ "data": { "country_code": "\u041a\u043e\u0434 \u0441\u0442\u0440\u0430\u043d\u044b \u0430\u043a\u043a\u0430\u0443\u043d\u0442\u0430 (1 \u0434\u043b\u044f \u0421\u0428\u0410 \u0438\u043b\u0438 86 \u0434\u043b\u044f \u041a\u0438\u0442\u0430\u044f)", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "platform": "\u041f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435, \u0432 \u043a\u043e\u0442\u043e\u0440\u043e\u043c \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d \u0430\u043a\u043a\u0430\u0443\u043d\u0442", + "platform": "\u041f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435, \u0432 \u043a\u043e\u0442\u043e\u0440\u043e\u043c \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c", "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Tuya.", diff --git a/homeassistant/components/unifi/translations/ru.json b/homeassistant/components/unifi/translations/ru.json index 769287bb9751d..8c164272fdb10 100644 --- a/homeassistant/components/unifi/translations/ru.json +++ b/homeassistant/components/unifi/translations/ru.json @@ -41,8 +41,8 @@ "detection_time": "\u0412\u0440\u0435\u043c\u044f \u043e\u0442 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0435\u0433\u043e \u0441\u0435\u0430\u043d\u0441\u0430 \u0441\u0432\u044f\u0437\u0438 \u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c (\u0441\u0435\u043a.), \u043f\u043e \u0438\u0441\u0442\u0435\u0447\u0435\u043d\u0438\u044e \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u043e\u043b\u0443\u0447\u0438\u0442 \u0441\u0442\u0430\u0442\u0443\u0441 \"\u041d\u0435 \u0434\u043e\u043c\u0430\".", "ignore_wired_bug": "\u041e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043b\u043e\u0433\u0438\u043a\u0443 \u043e\u0448\u0438\u0431\u043a\u0438 \u0434\u043b\u044f \u043d\u0435 \u0431\u0435\u0441\u043f\u0440\u043e\u0432\u043e\u0434\u043d\u044b\u0445 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 UniFi", "ssid_filter": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 SSID \u0434\u043b\u044f \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u043d\u0438\u044f \u0431\u0435\u0441\u043f\u0440\u043e\u0432\u043e\u0434\u043d\u044b\u0445 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432", - "track_clients": "\u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u043d\u0438\u0435 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u0441\u0435\u0442\u0438", - "track_devices": "\u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u043d\u0438\u0435 \u0441\u0435\u0442\u0435\u0432\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 (\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Ubiquiti)", + "track_clients": "\u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0442\u044c \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u0441\u0435\u0442\u0438", + "track_devices": "\u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0442\u044c \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u0441\u0435\u0442\u0438 (\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Ubiquiti)", "track_wired_clients": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u043f\u0440\u043e\u0432\u043e\u0434\u043d\u043e\u0439 \u0441\u0435\u0442\u0438" }, "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u043d\u0438\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432", @@ -59,8 +59,8 @@ "simple_options": { "data": { "block_client": "\u041a\u043b\u0438\u0435\u043d\u0442\u044b \u0441 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0435\u043c \u0441\u0435\u0442\u0435\u0432\u043e\u0433\u043e \u0434\u043e\u0441\u0442\u0443\u043f\u0430", - "track_clients": "\u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u043d\u0438\u0435 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u0441\u0435\u0442\u0438", - "track_devices": "\u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u043d\u0438\u0435 \u0441\u0435\u0442\u0435\u0432\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 (\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Ubiquiti)" + "track_clients": "\u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0442\u044c \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u0441\u0435\u0442\u0438", + "track_devices": "\u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0442\u044c \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u0441\u0435\u0442\u0438 (\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Ubiquiti)" }, "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 UniFi." }, diff --git a/homeassistant/components/verisure/translations/es.json b/homeassistant/components/verisure/translations/es.json index 38605e4f86bc1..7ec2812d9647c 100644 --- a/homeassistant/components/verisure/translations/es.json +++ b/homeassistant/components/verisure/translations/es.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "La cuenta ya ha sido configurada", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + }, + "error": { + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" + }, "step": { "installation": { "data": { @@ -9,12 +17,16 @@ }, "reauth_confirm": { "data": { - "description": "Vuelva a autenticarse con su cuenta Verisure My Pages." + "description": "Vuelva a autenticarse con su cuenta Verisure My Pages.", + "email": "Correo electr\u00f3nico", + "password": "Contrase\u00f1a" } }, "user": { "data": { - "description": "Inicia sesi\u00f3n con tu cuenta Verisure My Pages." + "description": "Inicia sesi\u00f3n con tu cuenta Verisure My Pages.", + "email": "Correo electr\u00f3nico", + "password": "Contrase\u00f1a" } } } diff --git a/homeassistant/components/water_heater/translations/es.json b/homeassistant/components/water_heater/translations/es.json index f11f9592b8153..e30895d281eb8 100644 --- a/homeassistant/components/water_heater/translations/es.json +++ b/homeassistant/components/water_heater/translations/es.json @@ -12,6 +12,7 @@ "gas": "Gas", "heat_pump": "Bomba de calor", "high_demand": "Alta demanda", + "off": "Apagado", "performance": "Rendimiento" } } diff --git a/homeassistant/components/waze_travel_time/translations/es.json b/homeassistant/components/waze_travel_time/translations/es.json index 8b7235537f218..2ae07164fe766 100644 --- a/homeassistant/components/waze_travel_time/translations/es.json +++ b/homeassistant/components/waze_travel_time/translations/es.json @@ -1,13 +1,38 @@ { "config": { + "abort": { + "already_configured": "La ubicaci\u00f3n ya est\u00e1 configurada" + }, + "error": { + "cannot_connect": "No se pudo conectar" + }, "step": { "user": { "data": { "destination": "Destino", "origin": "Origen", "region": "Regi\u00f3n" - } + }, + "description": "En Origen y Destino, introduce la direcci\u00f3n de las coordenadas GPS de la ubicaci\u00f3n (las coordenadas GPS deben estar separadas por una coma). Tambi\u00e9n puedes escribir un id de entidad que proporcione esta informaci\u00f3n en su estado, un id de entidad con atributos de latitud y longitud o un nombre descriptivo de zona." } } - } + }, + "options": { + "step": { + "init": { + "data": { + "avoid_ferries": "\u00bfEvitar ferries?", + "avoid_subscription_roads": "\u00bfEvitar carreteras que necesitan vignette / suscripci\u00f3n?", + "avoid_toll_roads": "\u00bfEvitar las autopistas de peaje?", + "excl_filter": "Subcadena NO en la descripci\u00f3n de la ruta seleccionada", + "incl_filter": "Subcadena en descripci\u00f3n de la ruta seleccionada", + "realtime": "\u00bfTiempo de viaje en tiempo real?", + "units": "Unidades", + "vehicle_type": "Tipo de veh\u00edculo" + }, + "description": "Las entradas `subcadena` te permitir\u00e1n forzar a la integraci\u00f3n a utilizar una ruta concreta o a evitar una ruta concreta en el c\u00e1lculo del tiempo del recorrido." + } + } + }, + "title": "Waze Travel Time" } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/es.json b/homeassistant/components/xiaomi_miio/translations/es.json index 60a989ade0d6c..b5ec01007c0b8 100644 --- a/homeassistant/components/xiaomi_miio/translations/es.json +++ b/homeassistant/components/xiaomi_miio/translations/es.json @@ -13,8 +13,10 @@ "step": { "device": { "data": { + "host": "Direcci\u00f3n IP", "model": "Modelo de dispositivo (opcional)", - "name": "Nombre del dispositivo" + "name": "Nombre del dispositivo", + "token": "Token API" }, "description": "Necesitar\u00e1 la clave de 32 caracteres Token API, consulte https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token para obtener instrucciones. Tenga en cuenta que esta Token API es diferente de la clave utilizada por la integraci\u00f3n de Xiaomi Aqara.", "title": "Con\u00e9ctese a un dispositivo Xiaomi Miio o Xiaomi Gateway" From 6a4f414236c7b791eb9320412359ec46354730f8 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 22 Apr 2021 03:53:06 +0200 Subject: [PATCH 0436/1317] Change HomeAssistantType to HomeAssistant (#49522) --- tests/components/canary/__init__.py | 4 +- tests/components/cast/test_media_player.py | 30 +++++++-------- .../components/climacell/test_config_flow.py | 18 ++++----- tests/components/climacell/test_init.py | 8 ++-- tests/components/climacell/test_sensor.py | 13 +++---- tests/components/climacell/test_weather.py | 11 +++--- .../command_line/test_binary_sensor.py | 12 +++--- tests/components/command_line/test_cover.py | 18 ++++----- tests/components/command_line/test_notify.py | 20 ++++------ tests/components/command_line/test_sensor.py | 30 +++++++-------- tests/components/command_line/test_switch.py | 28 +++++++------- tests/components/device_tracker/common.py | 6 +-- tests/components/directv/__init__.py | 4 +- tests/components/directv/test_config_flow.py | 28 +++++++------- tests/components/directv/test_init.py | 6 +-- tests/components/directv/test_media_player.py | 38 ++++++++----------- tests/components/directv/test_remote.py | 10 ++--- tests/components/ezviz/__init__.py | 4 +- tests/components/freebox/test_config_flow.py | 14 +++---- tests/components/freebox/test_init.py | 8 ++-- .../components/fritzbox/test_binary_sensor.py | 12 +++--- tests/components/fritzbox/test_climate.py | 30 +++++++-------- tests/components/fritzbox/test_config_flow.py | 34 ++++++++--------- tests/components/fritzbox/test_init.py | 8 ++-- tests/components/fritzbox/test_sensor.py | 10 ++--- tests/components/fritzbox/test_switch.py | 14 +++---- 26 files changed, 197 insertions(+), 221 deletions(-) diff --git a/tests/components/canary/__init__.py b/tests/components/canary/__init__.py index 27cec31b9e98c..b327fb0ebcb78 100644 --- a/tests/components/canary/__init__.py +++ b/tests/components/canary/__init__.py @@ -10,7 +10,7 @@ DOMAIN, ) from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -51,7 +51,7 @@ def _patch_async_setup_entry(return_value=True): async def init_integration( - hass: HomeAssistantType, + hass: HomeAssistant, *, data: dict = ENTRY_CONFIG, options: dict = ENTRY_OPTIONS, diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 4b1978e8da5ba..959d53184a436 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -28,9 +28,9 @@ ) from homeassistant.config import async_process_ha_core_config from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, assert_setup_component @@ -158,7 +158,7 @@ def remove_chromecast(service_name: str, info: ChromecastInfo) -> None: return discover_chromecast, remove_chromecast, add_entities -async def async_setup_media_player_cast(hass: HomeAssistantType, info: ChromecastInfo): +async def async_setup_media_player_cast(hass: HomeAssistant, info: ChromecastInfo): """Set up the cast platform with async_setup_component.""" browser = MagicMock(devices={}, zc={}) chromecast = get_fake_chromecast(info) @@ -549,7 +549,7 @@ async def test_update_cast_chromecasts(hass): assert add_dev1.call_count == 1 -async def test_entity_availability(hass: HomeAssistantType): +async def test_entity_availability(hass: HomeAssistant): """Test handling of connection status.""" entity_id = "media_player.speaker" info = get_fake_chromecast_info() @@ -575,7 +575,7 @@ async def test_entity_availability(hass: HomeAssistantType): assert state.state == "unavailable" -async def test_entity_cast_status(hass: HomeAssistantType): +async def test_entity_cast_status(hass: HomeAssistant): """Test handling of cast status.""" entity_id = "media_player.speaker" reg = er.async_get(hass) @@ -644,7 +644,7 @@ async def test_entity_cast_status(hass: HomeAssistantType): ) -async def test_entity_play_media(hass: HomeAssistantType): +async def test_entity_play_media(hass: HomeAssistant): """Test playing media.""" entity_id = "media_player.speaker" reg = er.async_get(hass) @@ -673,7 +673,7 @@ async def test_entity_play_media(hass: HomeAssistantType): chromecast.media_controller.play_media.assert_called_once_with("best.mp3", "audio") -async def test_entity_play_media_cast(hass: HomeAssistantType, quick_play_mock): +async def test_entity_play_media_cast(hass: HomeAssistant, quick_play_mock): """Test playing media with cast special features.""" entity_id = "media_player.speaker" reg = er.async_get(hass) @@ -752,7 +752,7 @@ async def test_entity_play_media_cast_invalid(hass, caplog, quick_play_mock): assert "App unknown not supported" in caplog.text -async def test_entity_play_media_sign_URL(hass: HomeAssistantType): +async def test_entity_play_media_sign_URL(hass: HomeAssistant): """Test playing media.""" entity_id = "media_player.speaker" @@ -779,7 +779,7 @@ async def test_entity_play_media_sign_URL(hass: HomeAssistantType): ) -async def test_entity_media_content_type(hass: HomeAssistantType): +async def test_entity_media_content_type(hass: HomeAssistant): """Test various content types.""" entity_id = "media_player.speaker" reg = er.async_get(hass) @@ -833,7 +833,7 @@ async def test_entity_media_content_type(hass: HomeAssistantType): assert state.attributes.get("media_content_type") == "movie" -async def test_entity_control(hass: HomeAssistantType): +async def test_entity_control(hass: HomeAssistant): """Test various device and media controls.""" entity_id = "media_player.speaker" reg = er.async_get(hass) @@ -942,7 +942,7 @@ async def test_entity_control(hass: HomeAssistantType): chromecast.media_controller.seek.assert_called_once_with(123) -async def test_entity_media_states(hass: HomeAssistantType): +async def test_entity_media_states(hass: HomeAssistant): """Test various entity media states.""" entity_id = "media_player.speaker" reg = er.async_get(hass) @@ -1242,7 +1242,7 @@ async def test_failed_cast_tts_base_url(hass, caplog): ) -async def test_disconnect_on_stop(hass: HomeAssistantType): +async def test_disconnect_on_stop(hass: HomeAssistant): """Test cast device disconnects socket on stop.""" info = get_fake_chromecast_info() @@ -1253,7 +1253,7 @@ async def test_disconnect_on_stop(hass: HomeAssistantType): assert chromecast.disconnect.call_count == 1 -async def test_entry_setup_no_config(hass: HomeAssistantType): +async def test_entry_setup_no_config(hass: HomeAssistant): """Test deprecated empty yaml config..""" await async_setup_component(hass, "cast", {}) await hass.async_block_till_done() @@ -1261,7 +1261,7 @@ async def test_entry_setup_no_config(hass: HomeAssistantType): assert not hass.config_entries.async_entries("cast") -async def test_entry_setup_empty_config(hass: HomeAssistantType): +async def test_entry_setup_empty_config(hass: HomeAssistant): """Test deprecated empty yaml config..""" await async_setup_component(hass, "cast", {"cast": {}}) await hass.async_block_till_done() @@ -1271,7 +1271,7 @@ async def test_entry_setup_empty_config(hass: HomeAssistantType): assert config_entry.data["ignore_cec"] == [] -async def test_entry_setup_single_config(hass: HomeAssistantType, pycast_mock): +async def test_entry_setup_single_config(hass: HomeAssistant, pycast_mock): """Test deprecated yaml config with a single config media_player.""" await async_setup_component( hass, "cast", {"cast": {"media_player": {"uuid": "bla", "ignore_cec": "cast1"}}} @@ -1285,7 +1285,7 @@ async def test_entry_setup_single_config(hass: HomeAssistantType, pycast_mock): assert pycast_mock.IGNORE_CEC == ["cast1"] -async def test_entry_setup_list_config(hass: HomeAssistantType, pycast_mock): +async def test_entry_setup_list_config(hass: HomeAssistant, pycast_mock): """Test deprecated yaml config with multiple media_players.""" await async_setup_component( hass, diff --git a/tests/components/climacell/test_config_flow.py b/tests/components/climacell/test_config_flow.py index 6cd5fb8579437..faa3748be692d 100644 --- a/tests/components/climacell/test_config_flow.py +++ b/tests/components/climacell/test_config_flow.py @@ -28,7 +28,7 @@ CONF_LONGITUDE, CONF_NAME, ) -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .const import API_KEY, MIN_CONFIG @@ -37,7 +37,7 @@ _LOGGER = logging.getLogger(__name__) -async def test_user_flow_minimum_fields(hass: HomeAssistantType) -> None: +async def test_user_flow_minimum_fields(hass: HomeAssistant) -> None: """Test user config flow with minimum fields.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -59,7 +59,7 @@ async def test_user_flow_minimum_fields(hass: HomeAssistantType) -> None: assert result["data"][CONF_LONGITUDE] == hass.config.longitude -async def test_user_flow_v3(hass: HomeAssistantType) -> None: +async def test_user_flow_v3(hass: HomeAssistant) -> None: """Test user config flow with v3 API.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -84,7 +84,7 @@ async def test_user_flow_v3(hass: HomeAssistantType) -> None: assert result["data"][CONF_LONGITUDE] == hass.config.longitude -async def test_user_flow_same_unique_ids(hass: HomeAssistantType) -> None: +async def test_user_flow_same_unique_ids(hass: HomeAssistant) -> None: """Test user config flow with the same unique ID as an existing entry.""" user_input = _get_config_schema(hass, MIN_CONFIG)(MIN_CONFIG) MockConfigEntry( @@ -105,7 +105,7 @@ async def test_user_flow_same_unique_ids(hass: HomeAssistantType) -> None: assert result["reason"] == "already_configured" -async def test_user_flow_cannot_connect(hass: HomeAssistantType) -> None: +async def test_user_flow_cannot_connect(hass: HomeAssistant) -> None: """Test user config flow when ClimaCell can't connect.""" with patch( "homeassistant.components.climacell.config_flow.ClimaCellV4.realtime", @@ -121,7 +121,7 @@ async def test_user_flow_cannot_connect(hass: HomeAssistantType) -> None: assert result["errors"] == {"base": "cannot_connect"} -async def test_user_flow_invalid_api(hass: HomeAssistantType) -> None: +async def test_user_flow_invalid_api(hass: HomeAssistant) -> None: """Test user config flow when API key is invalid.""" with patch( "homeassistant.components.climacell.config_flow.ClimaCellV4.realtime", @@ -137,7 +137,7 @@ async def test_user_flow_invalid_api(hass: HomeAssistantType) -> None: assert result["errors"] == {CONF_API_KEY: "invalid_api_key"} -async def test_user_flow_rate_limited(hass: HomeAssistantType) -> None: +async def test_user_flow_rate_limited(hass: HomeAssistant) -> None: """Test user config flow when API key is rate limited.""" with patch( "homeassistant.components.climacell.config_flow.ClimaCellV4.realtime", @@ -153,7 +153,7 @@ async def test_user_flow_rate_limited(hass: HomeAssistantType) -> None: assert result["errors"] == {CONF_API_KEY: "rate_limited"} -async def test_user_flow_unknown_exception(hass: HomeAssistantType) -> None: +async def test_user_flow_unknown_exception(hass: HomeAssistant) -> None: """Test user config flow when unknown error occurs.""" with patch( "homeassistant.components.climacell.config_flow.ClimaCellV4.realtime", @@ -169,7 +169,7 @@ async def test_user_flow_unknown_exception(hass: HomeAssistantType) -> None: assert result["errors"] == {"base": "unknown"} -async def test_options_flow(hass: HomeAssistantType) -> None: +async def test_options_flow(hass: HomeAssistant) -> None: """Test options config flow for climacell.""" user_config = _get_config_schema(hass)(MIN_CONFIG) entry = MockConfigEntry( diff --git a/tests/components/climacell/test_init.py b/tests/components/climacell/test_init.py index 33a18d553f34a..d90a0c0018149 100644 --- a/tests/components/climacell/test_init.py +++ b/tests/components/climacell/test_init.py @@ -10,7 +10,7 @@ from homeassistant.components.climacell.const import CONF_TIMESTEP, DOMAIN from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN from homeassistant.const import CONF_API_VERSION -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .const import API_V3_ENTRY_DATA, MIN_CONFIG, V1_ENTRY_DATA @@ -20,7 +20,7 @@ async def test_load_and_unload( - hass: HomeAssistantType, + hass: HomeAssistant, climacell_config_entry_update: pytest.fixture, ) -> None: """Test loading and unloading entry.""" @@ -42,7 +42,7 @@ async def test_load_and_unload( async def test_v3_load_and_unload( - hass: HomeAssistantType, + hass: HomeAssistant, climacell_config_entry_update: pytest.fixture, ) -> None: """Test loading and unloading v3 entry.""" @@ -67,7 +67,7 @@ async def test_v3_load_and_unload( "old_timestep, new_timestep", [(2, 1), (7, 5), (20, 15), (21, 30)] ) async def test_migrate_timestep( - hass: HomeAssistantType, + hass: HomeAssistant, climacell_config_entry_update: pytest.fixture, old_timestep: int, new_timestep: int, diff --git a/tests/components/climacell/test_sensor.py b/tests/components/climacell/test_sensor.py index d82a70964cf1e..7757fe208d39e 100644 --- a/tests/components/climacell/test_sensor.py +++ b/tests/components/climacell/test_sensor.py @@ -16,9 +16,8 @@ from homeassistant.components.climacell.const import ATTRIBUTION, DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import ATTR_ATTRIBUTION -from homeassistant.core import State, callback +from homeassistant.core import HomeAssistant, State, callback from homeassistant.helpers.entity_registry import async_get -from homeassistant.helpers.typing import HomeAssistantType from .const import API_V3_ENTRY_DATA, API_V4_ENTRY_DATA @@ -45,7 +44,7 @@ @callback -def _enable_entity(hass: HomeAssistantType, entity_name: str) -> None: +def _enable_entity(hass: HomeAssistant, entity_name: str) -> None: """Enable disabled entity.""" ent_reg = async_get(hass) entry = ent_reg.async_get(entity_name) @@ -56,7 +55,7 @@ def _enable_entity(hass: HomeAssistantType, entity_name: str) -> None: assert updated_entry.disabled is False -async def _setup(hass: HomeAssistantType, config: dict[str, Any]) -> State: +async def _setup(hass: HomeAssistant, config: dict[str, Any]) -> State: """Set up entry and return entity state.""" with patch( "homeassistant.util.dt.utcnow", @@ -94,7 +93,7 @@ async def _setup(hass: HomeAssistantType, config: dict[str, Any]) -> State: assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 15 -def check_sensor_state(hass: HomeAssistantType, entity_name: str, value: str): +def check_sensor_state(hass: HomeAssistant, entity_name: str, value: str): """Check the state of a ClimaCell sensor.""" state = hass.states.get(CC_SENSOR_ENTITY_ID.format(entity_name)) assert state @@ -103,7 +102,7 @@ def check_sensor_state(hass: HomeAssistantType, entity_name: str, value: str): async def test_v3_sensor( - hass: HomeAssistantType, + hass: HomeAssistant, climacell_config_entry_update: pytest.fixture, ) -> None: """Test v3 sensor data.""" @@ -126,7 +125,7 @@ async def test_v3_sensor( async def test_v4_sensor( - hass: HomeAssistantType, + hass: HomeAssistant, climacell_config_entry_update: pytest.fixture, ) -> None: """Test v4 sensor data.""" diff --git a/tests/components/climacell/test_weather.py b/tests/components/climacell/test_weather.py index 43515d6aa6620..02aa65a350e65 100644 --- a/tests/components/climacell/test_weather.py +++ b/tests/components/climacell/test_weather.py @@ -44,9 +44,8 @@ DOMAIN as WEATHER_DOMAIN, ) from homeassistant.const import ATTR_ATTRIBUTION, ATTR_FRIENDLY_NAME -from homeassistant.core import State, callback +from homeassistant.core import HomeAssistant, State, callback from homeassistant.helpers.entity_registry import async_get -from homeassistant.helpers.typing import HomeAssistantType from .const import API_V3_ENTRY_DATA, API_V4_ENTRY_DATA @@ -56,7 +55,7 @@ @callback -def _enable_entity(hass: HomeAssistantType, entity_name: str) -> None: +def _enable_entity(hass: HomeAssistant, entity_name: str) -> None: """Enable disabled entity.""" ent_reg = async_get(hass) entry = ent_reg.async_get(entity_name) @@ -67,7 +66,7 @@ def _enable_entity(hass: HomeAssistantType, entity_name: str) -> None: assert updated_entry.disabled is False -async def _setup(hass: HomeAssistantType, config: dict[str, Any]) -> State: +async def _setup(hass: HomeAssistant, config: dict[str, Any]) -> State: """Set up entry and return entity state.""" with patch( "homeassistant.util.dt.utcnow", @@ -92,7 +91,7 @@ async def _setup(hass: HomeAssistantType, config: dict[str, Any]) -> State: async def test_v3_weather( - hass: HomeAssistantType, + hass: HomeAssistant, climacell_config_entry_update: pytest.fixture, ) -> None: """Test v3 weather data.""" @@ -235,7 +234,7 @@ async def test_v3_weather( async def test_v4_weather( - hass: HomeAssistantType, + hass: HomeAssistant, climacell_config_entry_update: pytest.fixture, ) -> None: """Test v4 weather data.""" diff --git a/tests/components/command_line/test_binary_sensor.py b/tests/components/command_line/test_binary_sensor.py index aa6395096c3d6..2749fff812762 100644 --- a/tests/components/command_line/test_binary_sensor.py +++ b/tests/components/command_line/test_binary_sensor.py @@ -6,12 +6,10 @@ from homeassistant import setup from homeassistant.components.binary_sensor import DOMAIN from homeassistant.const import STATE_OFF, STATE_ON -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant -async def setup_test_entity( - hass: HomeAssistantType, config_dict: dict[str, Any] -) -> None: +async def setup_test_entity(hass: HomeAssistant, config_dict: dict[str, Any]) -> None: """Set up a test command line binary_sensor entity.""" assert await setup.async_setup_component( hass, @@ -21,7 +19,7 @@ async def setup_test_entity( await hass.async_block_till_done() -async def test_setup(hass: HomeAssistantType) -> None: +async def test_setup(hass: HomeAssistant) -> None: """Test sensor setup.""" await setup_test_entity( hass, @@ -38,7 +36,7 @@ async def test_setup(hass: HomeAssistantType) -> None: assert entity_state.name == "Test" -async def test_template(hass: HomeAssistantType) -> None: +async def test_template(hass: HomeAssistant) -> None: """Test setting the state with a template.""" await setup_test_entity( @@ -55,7 +53,7 @@ async def test_template(hass: HomeAssistantType) -> None: assert entity_state.state == STATE_ON -async def test_sensor_off(hass: HomeAssistantType) -> None: +async def test_sensor_off(hass: HomeAssistant) -> None: """Test setting the state with a template.""" await setup_test_entity( hass, diff --git a/tests/components/command_line/test_cover.py b/tests/components/command_line/test_cover.py index 8ee69e8b5cbc4..d0b36d31c37d4 100644 --- a/tests/components/command_line/test_cover.py +++ b/tests/components/command_line/test_cover.py @@ -15,15 +15,13 @@ SERVICE_RELOAD, SERVICE_STOP_COVER, ) -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant import homeassistant.util.dt as dt_util from tests.common import async_fire_time_changed -async def setup_test_entity( - hass: HomeAssistantType, config_dict: dict[str, Any] -) -> None: +async def setup_test_entity(hass: HomeAssistant, config_dict: dict[str, Any]) -> None: """Set up a test command line notify service.""" assert await setup.async_setup_component( hass, @@ -37,7 +35,7 @@ async def setup_test_entity( await hass.async_block_till_done() -async def test_no_covers(caplog: Any, hass: HomeAssistantType) -> None: +async def test_no_covers(caplog: Any, hass: HomeAssistant) -> None: """Test that the cover does not polls when there's no state command.""" with patch( @@ -48,7 +46,7 @@ async def test_no_covers(caplog: Any, hass: HomeAssistantType) -> None: assert "No covers added" in caplog.text -async def test_no_poll_when_cover_has_no_command_state(hass: HomeAssistantType) -> None: +async def test_no_poll_when_cover_has_no_command_state(hass: HomeAssistant) -> None: """Test that the cover does not polls when there's no state command.""" with patch( @@ -61,7 +59,7 @@ async def test_no_poll_when_cover_has_no_command_state(hass: HomeAssistantType) assert not check_output.called -async def test_poll_when_cover_has_command_state(hass: HomeAssistantType) -> None: +async def test_poll_when_cover_has_command_state(hass: HomeAssistant) -> None: """Test that the cover polls when there's a state command.""" with patch( @@ -76,7 +74,7 @@ async def test_poll_when_cover_has_command_state(hass: HomeAssistantType) -> Non ) -async def test_state_value(hass: HomeAssistantType) -> None: +async def test_state_value(hass: HomeAssistant) -> None: """Test with state value.""" with tempfile.TemporaryDirectory() as tempdirname: path = os.path.join(tempdirname, "cover_status") @@ -119,7 +117,7 @@ async def test_state_value(hass: HomeAssistantType) -> None: assert entity_state.state == "closed" -async def test_reload(hass: HomeAssistantType) -> None: +async def test_reload(hass: HomeAssistant) -> None: """Verify we can reload command_line covers.""" await setup_test_entity( @@ -155,7 +153,7 @@ async def test_reload(hass: HomeAssistantType) -> None: assert hass.states.get("cover.from_yaml") -async def test_move_cover_failure(caplog: Any, hass: HomeAssistantType) -> None: +async def test_move_cover_failure(caplog: Any, hass: HomeAssistant) -> None: """Test with state value.""" await setup_test_entity( diff --git a/tests/components/command_line/test_notify.py b/tests/components/command_line/test_notify.py index b22b0323aadc0..5fef385bf81dc 100644 --- a/tests/components/command_line/test_notify.py +++ b/tests/components/command_line/test_notify.py @@ -9,12 +9,10 @@ from homeassistant import setup from homeassistant.components.notify import DOMAIN -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant -async def setup_test_service( - hass: HomeAssistantType, config_dict: dict[str, Any] -) -> None: +async def setup_test_service(hass: HomeAssistant, config_dict: dict[str, Any]) -> None: """Set up a test command line notify service.""" assert await setup.async_setup_component( hass, @@ -28,19 +26,19 @@ async def setup_test_service( await hass.async_block_till_done() -async def test_setup(hass: HomeAssistantType) -> None: +async def test_setup(hass: HomeAssistant) -> None: """Test sensor setup.""" await setup_test_service(hass, {"command": "exit 0"}) assert hass.services.has_service(DOMAIN, "test") -async def test_bad_config(hass: HomeAssistantType) -> None: +async def test_bad_config(hass: HomeAssistant) -> None: """Test set up the platform with bad/missing configuration.""" await setup_test_service(hass, {}) assert not hass.services.has_service(DOMAIN, "test") -async def test_command_line_output(hass: HomeAssistantType) -> None: +async def test_command_line_output(hass: HomeAssistant) -> None: """Test the command line output.""" with tempfile.TemporaryDirectory() as tempdirname: filename = os.path.join(tempdirname, "message.txt") @@ -62,9 +60,7 @@ async def test_command_line_output(hass: HomeAssistantType) -> None: assert message == handle.read() -async def test_error_for_none_zero_exit_code( - caplog: Any, hass: HomeAssistantType -) -> None: +async def test_error_for_none_zero_exit_code(caplog: Any, hass: HomeAssistant) -> None: """Test if an error is logged for non zero exit codes.""" await setup_test_service( hass, @@ -79,7 +75,7 @@ async def test_error_for_none_zero_exit_code( assert "Command failed" in caplog.text -async def test_timeout(caplog: Any, hass: HomeAssistantType) -> None: +async def test_timeout(caplog: Any, hass: HomeAssistant) -> None: """Test blocking is not forever.""" await setup_test_service( hass, @@ -94,7 +90,7 @@ async def test_timeout(caplog: Any, hass: HomeAssistantType) -> None: assert "Timeout" in caplog.text -async def test_subprocess_exceptions(caplog: Any, hass: HomeAssistantType) -> None: +async def test_subprocess_exceptions(caplog: Any, hass: HomeAssistant) -> None: """Test that notify subprocess exceptions are handled correctly.""" with patch( diff --git a/tests/components/command_line/test_sensor.py b/tests/components/command_line/test_sensor.py index 7e1f7707ca136..be897fa2408f9 100644 --- a/tests/components/command_line/test_sensor.py +++ b/tests/components/command_line/test_sensor.py @@ -6,12 +6,10 @@ from homeassistant import setup from homeassistant.components.sensor import DOMAIN -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant -async def setup_test_entities( - hass: HomeAssistantType, config_dict: dict[str, Any] -) -> None: +async def setup_test_entities(hass: HomeAssistant, config_dict: dict[str, Any]) -> None: """Set up a test command line sensor entity.""" assert await setup.async_setup_component( hass, @@ -33,7 +31,7 @@ async def setup_test_entities( await hass.async_block_till_done() -async def test_setup(hass: HomeAssistantType) -> None: +async def test_setup(hass: HomeAssistant) -> None: """Test sensor setup.""" await setup_test_entities( hass, @@ -49,7 +47,7 @@ async def test_setup(hass: HomeAssistantType) -> None: assert entity_state.attributes["unit_of_measurement"] == "in" -async def test_template(hass: HomeAssistantType) -> None: +async def test_template(hass: HomeAssistant) -> None: """Test command sensor with template.""" await setup_test_entities( hass, @@ -64,7 +62,7 @@ async def test_template(hass: HomeAssistantType) -> None: assert float(entity_state.state) == 5 -async def test_template_render(hass: HomeAssistantType) -> None: +async def test_template_render(hass: HomeAssistant) -> None: """Ensure command with templates get rendered properly.""" await setup_test_entities( @@ -78,7 +76,7 @@ async def test_template_render(hass: HomeAssistantType) -> None: assert entity_state.state == "template_value" -async def test_template_render_with_quote(hass: HomeAssistantType) -> None: +async def test_template_render_with_quote(hass: HomeAssistant) -> None: """Ensure command with templates and quotes get rendered properly.""" with patch( @@ -99,7 +97,7 @@ async def test_template_render_with_quote(hass: HomeAssistantType) -> None: ) -async def test_bad_template_render(caplog: Any, hass: HomeAssistantType) -> None: +async def test_bad_template_render(caplog: Any, hass: HomeAssistant) -> None: """Test rendering a broken template.""" await setup_test_entities( @@ -112,7 +110,7 @@ async def test_bad_template_render(caplog: Any, hass: HomeAssistantType) -> None assert "Error rendering command template" in caplog.text -async def test_bad_command(hass: HomeAssistantType) -> None: +async def test_bad_command(hass: HomeAssistant) -> None: """Test bad command.""" await setup_test_entities( hass, @@ -125,7 +123,7 @@ async def test_bad_command(hass: HomeAssistantType) -> None: assert entity_state.state == "unknown" -async def test_update_with_json_attrs(hass: HomeAssistantType) -> None: +async def test_update_with_json_attrs(hass: HomeAssistant) -> None: """Test attributes get extracted from a JSON result.""" await setup_test_entities( hass, @@ -142,7 +140,7 @@ async def test_update_with_json_attrs(hass: HomeAssistantType) -> None: assert entity_state.attributes["key_three"] == "value_three" -async def test_update_with_json_attrs_no_data(caplog, hass: HomeAssistantType) -> None: # type: ignore[no-untyped-def] +async def test_update_with_json_attrs_no_data(caplog, hass: HomeAssistant) -> None: # type: ignore[no-untyped-def] """Test attributes when no JSON result fetched.""" await setup_test_entities( @@ -158,7 +156,7 @@ async def test_update_with_json_attrs_no_data(caplog, hass: HomeAssistantType) - assert "Empty reply found when expecting JSON data" in caplog.text -async def test_update_with_json_attrs_not_dict(caplog, hass: HomeAssistantType) -> None: # type: ignore[no-untyped-def] +async def test_update_with_json_attrs_not_dict(caplog, hass: HomeAssistant) -> None: # type: ignore[no-untyped-def] """Test attributes when the return value not a dict.""" await setup_test_entities( @@ -174,7 +172,7 @@ async def test_update_with_json_attrs_not_dict(caplog, hass: HomeAssistantType) assert "JSON result was not a dictionary" in caplog.text -async def test_update_with_json_attrs_bad_json(caplog, hass: HomeAssistantType) -> None: # type: ignore[no-untyped-def] +async def test_update_with_json_attrs_bad_json(caplog, hass: HomeAssistant) -> None: # type: ignore[no-untyped-def] """Test attributes when the return value is invalid JSON.""" await setup_test_entities( @@ -190,7 +188,7 @@ async def test_update_with_json_attrs_bad_json(caplog, hass: HomeAssistantType) assert "Unable to parse output as JSON" in caplog.text -async def test_update_with_missing_json_attrs(caplog, hass: HomeAssistantType) -> None: # type: ignore[no-untyped-def] +async def test_update_with_missing_json_attrs(caplog, hass: HomeAssistant) -> None: # type: ignore[no-untyped-def] """Test attributes when an expected key is missing.""" await setup_test_entities( @@ -209,7 +207,7 @@ async def test_update_with_missing_json_attrs(caplog, hass: HomeAssistantType) - assert "missing_key" not in entity_state.attributes -async def test_update_with_unnecessary_json_attrs(caplog, hass: HomeAssistantType) -> None: # type: ignore[no-untyped-def] +async def test_update_with_unnecessary_json_attrs(caplog, hass: HomeAssistant) -> None: # type: ignore[no-untyped-def] """Test attributes when an expected key is missing.""" await setup_test_entities( diff --git a/tests/components/command_line/test_switch.py b/tests/components/command_line/test_switch.py index 4439e6fdcb5e4..3eeded7278b86 100644 --- a/tests/components/command_line/test_switch.py +++ b/tests/components/command_line/test_switch.py @@ -17,15 +17,13 @@ STATE_OFF, STATE_ON, ) -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant import homeassistant.util.dt as dt_util from tests.common import async_fire_time_changed -async def setup_test_entity( - hass: HomeAssistantType, config_dict: dict[str, Any] -) -> None: +async def setup_test_entity(hass: HomeAssistant, config_dict: dict[str, Any]) -> None: """Set up a test command line switch entity.""" assert await setup.async_setup_component( hass, @@ -39,7 +37,7 @@ async def setup_test_entity( await hass.async_block_till_done() -async def test_state_none(hass: HomeAssistantType) -> None: +async def test_state_none(hass: HomeAssistant) -> None: """Test with none state.""" with tempfile.TemporaryDirectory() as tempdirname: path = os.path.join(tempdirname, "switch_status") @@ -80,7 +78,7 @@ async def test_state_none(hass: HomeAssistantType) -> None: assert entity_state.state == STATE_OFF -async def test_state_value(hass: HomeAssistantType) -> None: +async def test_state_value(hass: HomeAssistant) -> None: """Test with state value.""" with tempfile.TemporaryDirectory() as tempdirname: path = os.path.join(tempdirname, "switch_status") @@ -123,7 +121,7 @@ async def test_state_value(hass: HomeAssistantType) -> None: assert entity_state.state == STATE_OFF -async def test_state_json_value(hass: HomeAssistantType) -> None: +async def test_state_json_value(hass: HomeAssistant) -> None: """Test with state JSON value.""" with tempfile.TemporaryDirectory() as tempdirname: path = os.path.join(tempdirname, "switch_status") @@ -169,7 +167,7 @@ async def test_state_json_value(hass: HomeAssistantType) -> None: assert entity_state.state == STATE_OFF -async def test_state_code(hass: HomeAssistantType) -> None: +async def test_state_code(hass: HomeAssistant) -> None: """Test with state code.""" with tempfile.TemporaryDirectory() as tempdirname: path = os.path.join(tempdirname, "switch_status") @@ -212,7 +210,7 @@ async def test_state_code(hass: HomeAssistantType) -> None: async def test_assumed_state_should_be_true_if_command_state_is_none( - hass: HomeAssistantType, + hass: HomeAssistant, ) -> None: """Test with state value.""" @@ -231,7 +229,7 @@ async def test_assumed_state_should_be_true_if_command_state_is_none( async def test_assumed_state_should_absent_if_command_state_present( - hass: HomeAssistantType, + hass: HomeAssistant, ) -> None: """Test with state value.""" @@ -250,7 +248,7 @@ async def test_assumed_state_should_absent_if_command_state_present( assert "assumed_state" not in entity_state.attributes -async def test_name_is_set_correctly(hass: HomeAssistantType) -> None: +async def test_name_is_set_correctly(hass: HomeAssistant) -> None: """Test that name is set correctly.""" await setup_test_entity( hass, @@ -267,7 +265,7 @@ async def test_name_is_set_correctly(hass: HomeAssistantType) -> None: assert entity_state.name == "Test friendly name!" -async def test_switch_command_state_fail(caplog: Any, hass: HomeAssistantType) -> None: +async def test_switch_command_state_fail(caplog: Any, hass: HomeAssistant) -> None: """Test that switch failures are handled correctly.""" await setup_test_entity( hass, @@ -301,7 +299,7 @@ async def test_switch_command_state_fail(caplog: Any, hass: HomeAssistantType) - async def test_switch_command_state_code_exceptions( - caplog: Any, hass: HomeAssistantType + caplog: Any, hass: HomeAssistant ) -> None: """Test that switch state code exceptions are handled correctly.""" @@ -334,7 +332,7 @@ async def test_switch_command_state_code_exceptions( async def test_switch_command_state_value_exceptions( - caplog: Any, hass: HomeAssistantType + caplog: Any, hass: HomeAssistant ) -> None: """Test that switch state value exceptions are handled correctly.""" @@ -367,7 +365,7 @@ async def test_switch_command_state_value_exceptions( assert "Error trying to exec command" in caplog.text -async def test_no_switches(caplog: Any, hass: HomeAssistantType) -> None: +async def test_no_switches(caplog: Any, hass: HomeAssistant) -> None: """Test with no switches.""" await setup_test_entity(hass, {}) diff --git a/tests/components/device_tracker/common.py b/tests/components/device_tracker/common.py index 1326174c6be13..dfb3f9e8462b8 100644 --- a/tests/components/device_tracker/common.py +++ b/tests/components/device_tracker/common.py @@ -15,15 +15,15 @@ DOMAIN, SERVICE_SEE, ) -from homeassistant.core import callback -from homeassistant.helpers.typing import GPSType, HomeAssistantType +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.typing import GPSType from homeassistant.loader import bind_hass @callback @bind_hass def async_see( - hass: HomeAssistantType, + hass: HomeAssistant, mac: str = None, dev_id: str = None, host_name: str = None, diff --git a/tests/components/directv/__init__.py b/tests/components/directv/__init__.py index 9f09c377bd403..1bdfbeea82373 100644 --- a/tests/components/directv/__init__.py +++ b/tests/components/directv/__init__.py @@ -7,7 +7,7 @@ HTTP_FORBIDDEN, HTTP_INTERNAL_SERVER_ERROR, ) -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry, load_fixture from tests.test_util.aiohttp import AiohttpClientMocker @@ -99,7 +99,7 @@ def mock_connection(aioclient_mock: AiohttpClientMocker) -> None: async def setup_integration( - hass: HomeAssistantType, + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, skip_entry_setup: bool = False, setup_error: bool = False, diff --git a/tests/components/directv/test_config_flow.py b/tests/components/directv/test_config_flow.py index 8c2d190f01429..646b863a1149b 100644 --- a/tests/components/directv/test_config_flow.py +++ b/tests/components/directv/test_config_flow.py @@ -7,12 +7,12 @@ from homeassistant.components.ssdp import ATTR_UPNP_SERIAL from homeassistant.config_entries import SOURCE_SSDP, SOURCE_USER from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SOURCE +from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import ( RESULT_TYPE_ABORT, RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM, ) -from homeassistant.helpers.typing import HomeAssistantType from tests.components.directv import ( HOST, @@ -26,7 +26,7 @@ from tests.test_util.aiohttp import AiohttpClientMocker -async def test_show_user_form(hass: HomeAssistantType) -> None: +async def test_show_user_form(hass: HomeAssistant) -> None: """Test that the user set up form is served.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -38,7 +38,7 @@ async def test_show_user_form(hass: HomeAssistantType) -> None: async def test_show_ssdp_form( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test that the ssdp confirmation form is served.""" mock_connection(aioclient_mock) @@ -54,7 +54,7 @@ async def test_show_ssdp_form( async def test_cannot_connect( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test we show user form on connection error.""" aioclient_mock.get("http://127.0.0.1:8080/info/getVersion", exc=HTTPClientError) @@ -72,7 +72,7 @@ async def test_cannot_connect( async def test_ssdp_cannot_connect( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test we abort SSDP flow on connection error.""" aioclient_mock.get("http://127.0.0.1:8080/info/getVersion", exc=HTTPClientError) @@ -89,7 +89,7 @@ async def test_ssdp_cannot_connect( async def test_ssdp_confirm_cannot_connect( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test we abort SSDP flow on connection error.""" aioclient_mock.get("http://127.0.0.1:8080/info/getVersion", exc=HTTPClientError) @@ -106,7 +106,7 @@ async def test_ssdp_confirm_cannot_connect( async def test_user_device_exists_abort( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test we abort user flow if DirecTV receiver already configured.""" await setup_integration(hass, aioclient_mock, skip_entry_setup=True) @@ -123,7 +123,7 @@ async def test_user_device_exists_abort( async def test_ssdp_device_exists_abort( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test we abort SSDP flow if DirecTV receiver already configured.""" await setup_integration(hass, aioclient_mock, skip_entry_setup=True) @@ -140,7 +140,7 @@ async def test_ssdp_device_exists_abort( async def test_ssdp_with_receiver_id_device_exists_abort( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test we abort SSDP flow if DirecTV receiver already configured.""" await setup_integration(hass, aioclient_mock, skip_entry_setup=True) @@ -158,7 +158,7 @@ async def test_ssdp_with_receiver_id_device_exists_abort( async def test_unknown_error( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test we show user form on unknown error.""" user_input = MOCK_USER_INPUT.copy() @@ -177,7 +177,7 @@ async def test_unknown_error( async def test_ssdp_unknown_error( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test we abort SSDP flow on unknown error.""" discovery_info = MOCK_SSDP_DISCOVERY_INFO.copy() @@ -196,7 +196,7 @@ async def test_ssdp_unknown_error( async def test_ssdp_confirm_unknown_error( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test we abort SSDP flow on unknown error.""" discovery_info = MOCK_SSDP_DISCOVERY_INFO.copy() @@ -215,7 +215,7 @@ async def test_ssdp_confirm_unknown_error( async def test_full_user_flow_implementation( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test the full manual user flow from start to finish.""" mock_connection(aioclient_mock) @@ -244,7 +244,7 @@ async def test_full_user_flow_implementation( async def test_full_ssdp_flow_implementation( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test the full SSDP flow from start to finish.""" mock_connection(aioclient_mock) diff --git a/tests/components/directv/test_init.py b/tests/components/directv/test_init.py index b56070f0e7eaa..96fd27a30eb63 100644 --- a/tests/components/directv/test_init.py +++ b/tests/components/directv/test_init.py @@ -5,7 +5,7 @@ ENTRY_STATE_NOT_LOADED, ENTRY_STATE_SETUP_RETRY, ) -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from tests.components.directv import setup_integration from tests.test_util.aiohttp import AiohttpClientMocker @@ -14,7 +14,7 @@ async def test_config_entry_not_ready( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test the DirecTV configuration entry not ready.""" entry = await setup_integration(hass, aioclient_mock, setup_error=True) @@ -23,7 +23,7 @@ async def test_config_entry_not_ready( async def test_unload_config_entry( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test the DirecTV configuration entry unloading.""" entry = await setup_integration(hass, aioclient_mock) diff --git a/tests/components/directv/test_media_player.py b/tests/components/directv/test_media_player.py index 8e7fad62c8943..14bc121bf8679 100644 --- a/tests/components/directv/test_media_player.py +++ b/tests/components/directv/test_media_player.py @@ -54,8 +54,8 @@ STATE_PLAYING, STATE_UNAVAILABLE, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import dt as dt_util from tests.components.directv import setup_integration @@ -78,44 +78,38 @@ def mock_now() -> datetime: return dt_util.utcnow() -async def async_turn_on(hass: HomeAssistantType, entity_id: str | None = None) -> None: +async def async_turn_on(hass: HomeAssistant, entity_id: str | None = None) -> None: """Turn on specified media player or all.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} await hass.services.async_call(MP_DOMAIN, SERVICE_TURN_ON, data) -async def async_turn_off(hass: HomeAssistantType, entity_id: str | None = None) -> None: +async def async_turn_off(hass: HomeAssistant, entity_id: str | None = None) -> None: """Turn off specified media player or all.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} await hass.services.async_call(MP_DOMAIN, SERVICE_TURN_OFF, data) -async def async_media_pause( - hass: HomeAssistantType, entity_id: str | None = None -) -> None: +async def async_media_pause(hass: HomeAssistant, entity_id: str | None = None) -> None: """Send the media player the command for pause.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} await hass.services.async_call(MP_DOMAIN, SERVICE_MEDIA_PAUSE, data) -async def async_media_play( - hass: HomeAssistantType, entity_id: str | None = None -) -> None: +async def async_media_play(hass: HomeAssistant, entity_id: str | None = None) -> None: """Send the media player the command for play/pause.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} await hass.services.async_call(MP_DOMAIN, SERVICE_MEDIA_PLAY, data) -async def async_media_stop( - hass: HomeAssistantType, entity_id: str | None = None -) -> None: +async def async_media_stop(hass: HomeAssistant, entity_id: str | None = None) -> None: """Send the media player the command for stop.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} await hass.services.async_call(MP_DOMAIN, SERVICE_MEDIA_STOP, data) async def async_media_next_track( - hass: HomeAssistantType, entity_id: str | None = None + hass: HomeAssistant, entity_id: str | None = None ) -> None: """Send the media player the command for next track.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} @@ -123,7 +117,7 @@ async def async_media_next_track( async def async_media_previous_track( - hass: HomeAssistantType, entity_id: str | None = None + hass: HomeAssistant, entity_id: str | None = None ) -> None: """Send the media player the command for prev track.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} @@ -131,7 +125,7 @@ async def async_media_previous_track( async def async_play_media( - hass: HomeAssistantType, + hass: HomeAssistant, media_type: str, media_id: str, entity_id: str | None = None, @@ -149,9 +143,7 @@ async def async_play_media( await hass.services.async_call(MP_DOMAIN, SERVICE_PLAY_MEDIA, data) -async def test_setup( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker -) -> None: +async def test_setup(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) -> None: """Test setup with basic config.""" await setup_integration(hass, aioclient_mock) assert hass.states.get(MAIN_ENTITY_ID) @@ -160,7 +152,7 @@ async def test_setup( async def test_unique_id( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test unique id.""" await setup_integration(hass, aioclient_mock) @@ -181,7 +173,7 @@ async def test_unique_id( async def test_supported_features( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test supported features.""" await setup_integration(hass, aioclient_mock) @@ -214,7 +206,7 @@ async def test_supported_features( async def test_check_attributes( - hass: HomeAssistantType, + hass: HomeAssistant, mock_now: dt_util.dt.datetime, aioclient_mock: AiohttpClientMocker, ) -> None: @@ -321,7 +313,7 @@ async def test_check_attributes( async def test_attributes_paused( - hass: HomeAssistantType, + hass: HomeAssistant, mock_now: dt_util.dt.datetime, aioclient_mock: AiohttpClientMocker, ): @@ -345,7 +337,7 @@ async def test_attributes_paused( async def test_main_services( - hass: HomeAssistantType, + hass: HomeAssistant, mock_now: dt_util.dt.datetime, aioclient_mock: AiohttpClientMocker, ) -> None: diff --git a/tests/components/directv/test_remote.py b/tests/components/directv/test_remote.py index 92bcd6af014d2..37eec1324c0d9 100644 --- a/tests/components/directv/test_remote.py +++ b/tests/components/directv/test_remote.py @@ -7,8 +7,8 @@ SERVICE_SEND_COMMAND, ) from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON +from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.typing import HomeAssistantType from tests.components.directv import setup_integration from tests.test_util.aiohttp import AiohttpClientMocker @@ -21,9 +21,7 @@ # pylint: disable=redefined-outer-name -async def test_setup( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker -) -> None: +async def test_setup(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) -> None: """Test setup with basic config.""" await setup_integration(hass, aioclient_mock) assert hass.states.get(MAIN_ENTITY_ID) @@ -32,7 +30,7 @@ async def test_setup( async def test_unique_id( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test unique id.""" await setup_integration(hass, aioclient_mock) @@ -50,7 +48,7 @@ async def test_unique_id( async def test_main_services( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test the different services.""" await setup_integration(hass, aioclient_mock) diff --git a/tests/components/ezviz/__init__.py b/tests/components/ezviz/__init__.py index 9a133a6f50bd3..b8dc04ef79043 100644 --- a/tests/components/ezviz/__init__.py +++ b/tests/components/ezviz/__init__.py @@ -19,7 +19,7 @@ CONF_URL, CONF_USERNAME, ) -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -101,7 +101,7 @@ def _patch_async_setup_entry(return_value=True): async def init_integration( - hass: HomeAssistantType, + hass: HomeAssistant, *, data: dict = ENTRY_CONFIG, options: dict = ENTRY_OPTIONS, diff --git a/tests/components/freebox/test_config_flow.py b/tests/components/freebox/test_config_flow.py index 565387d3fb489..2d86ee1de205f 100644 --- a/tests/components/freebox/test_config_flow.py +++ b/tests/components/freebox/test_config_flow.py @@ -11,7 +11,7 @@ from homeassistant.components.freebox.const import DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_PORT -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .const import MOCK_HOST, MOCK_PORT @@ -37,7 +37,7 @@ } -async def test_user(hass: HomeAssistantType): +async def test_user(hass: HomeAssistant): """Test user config.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -55,7 +55,7 @@ async def test_user(hass: HomeAssistantType): assert result["step_id"] == "link" -async def test_import(hass: HomeAssistantType): +async def test_import(hass: HomeAssistant): """Test import step.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -66,7 +66,7 @@ async def test_import(hass: HomeAssistantType): assert result["step_id"] == "link" -async def test_zeroconf(hass: HomeAssistantType): +async def test_zeroconf(hass: HomeAssistant): """Test zeroconf step.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -77,7 +77,7 @@ async def test_zeroconf(hass: HomeAssistantType): assert result["step_id"] == "link" -async def test_link(hass: HomeAssistantType, router: Mock): +async def test_link(hass: HomeAssistant, router: Mock): """Test linking.""" with patch( "homeassistant.components.freebox.async_setup", return_value=True @@ -102,7 +102,7 @@ async def test_link(hass: HomeAssistantType, router: Mock): assert len(mock_setup_entry.mock_calls) == 1 -async def test_abort_if_already_setup(hass: HomeAssistantType): +async def test_abort_if_already_setup(hass: HomeAssistant): """Test we abort if component is already setup.""" MockConfigEntry( domain=DOMAIN, @@ -129,7 +129,7 @@ async def test_abort_if_already_setup(hass: HomeAssistantType): assert result["reason"] == "already_configured" -async def test_on_link_failed(hass: HomeAssistantType): +async def test_on_link_failed(hass: HomeAssistant): """Test when we have errors during linking the router.""" result = await hass.config_entries.flow.async_init( DOMAIN, diff --git a/tests/components/freebox/test_init.py b/tests/components/freebox/test_init.py index aae5f911e1079..6b5589ac6477f 100644 --- a/tests/components/freebox/test_init.py +++ b/tests/components/freebox/test_init.py @@ -7,7 +7,7 @@ from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED from homeassistant.const import CONF_HOST, CONF_PORT, STATE_UNAVAILABLE -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from .const import MOCK_HOST, MOCK_PORT @@ -15,7 +15,7 @@ from tests.common import MockConfigEntry -async def test_setup(hass: HomeAssistantType, router: Mock): +async def test_setup(hass: HomeAssistant, router: Mock): """Test setup of integration.""" entry = MockConfigEntry( domain=DOMAIN, @@ -44,7 +44,7 @@ async def test_setup(hass: HomeAssistantType, router: Mock): mock_service.assert_called_once() -async def test_setup_import(hass: HomeAssistantType, router: Mock): +async def test_setup_import(hass: HomeAssistant, router: Mock): """Test setup of integration from import.""" await async_setup_component(hass, "persistent_notification", {}) @@ -66,7 +66,7 @@ async def test_setup_import(hass: HomeAssistantType, router: Mock): assert hass.services.has_service(DOMAIN, SERVICE_REBOOT) -async def test_unload_remove(hass: HomeAssistantType, router: Mock): +async def test_unload_remove(hass: HomeAssistant, router: Mock): """Test unload and remove of integration.""" entity_id_dt = f"{DT_DOMAIN}.freebox_server_r2" entity_id_sensor = f"{SENSOR_DOMAIN}.freebox_download_speed" diff --git a/tests/components/fritzbox/test_binary_sensor.py b/tests/components/fritzbox/test_binary_sensor.py index 89c1dea170440..0d29db2f7b19b 100644 --- a/tests/components/fritzbox/test_binary_sensor.py +++ b/tests/components/fritzbox/test_binary_sensor.py @@ -13,7 +13,7 @@ STATE_OFF, STATE_ON, ) -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -24,13 +24,13 @@ ENTITY_ID = f"{DOMAIN}.fake_name" -async def setup_fritzbox(hass: HomeAssistantType, config: dict): +async def setup_fritzbox(hass: HomeAssistant, config: dict): """Set up mock AVM Fritz!Box.""" assert await async_setup_component(hass, FB_DOMAIN, config) await hass.async_block_till_done() -async def test_setup(hass: HomeAssistantType, fritz: Mock): +async def test_setup(hass: HomeAssistant, fritz: Mock): """Test setup of platform.""" device = FritzDeviceBinarySensorMock() fritz().get_devices.return_value = [device] @@ -44,7 +44,7 @@ async def test_setup(hass: HomeAssistantType, fritz: Mock): assert state.attributes[ATTR_DEVICE_CLASS] == "window" -async def test_is_off(hass: HomeAssistantType, fritz: Mock): +async def test_is_off(hass: HomeAssistant, fritz: Mock): """Test state of platform.""" device = FritzDeviceBinarySensorMock() device.present = False @@ -57,7 +57,7 @@ async def test_is_off(hass: HomeAssistantType, fritz: Mock): assert state.state == STATE_OFF -async def test_update(hass: HomeAssistantType, fritz: Mock): +async def test_update(hass: HomeAssistant, fritz: Mock): """Test update with error.""" device = FritzDeviceBinarySensorMock() fritz().get_devices.return_value = [device] @@ -75,7 +75,7 @@ async def test_update(hass: HomeAssistantType, fritz: Mock): assert fritz().login.call_count == 1 -async def test_update_error(hass: HomeAssistantType, fritz: Mock): +async def test_update_error(hass: HomeAssistant, fritz: Mock): """Test update with error.""" device = FritzDeviceBinarySensorMock() device.update.side_effect = [mock.DEFAULT, HTTPError("Boom")] diff --git a/tests/components/fritzbox/test_climate.py b/tests/components/fritzbox/test_climate.py index 627eae5da9185..5453f93609e69 100644 --- a/tests/components/fritzbox/test_climate.py +++ b/tests/components/fritzbox/test_climate.py @@ -36,7 +36,7 @@ ATTR_FRIENDLY_NAME, ATTR_TEMPERATURE, ) -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -47,13 +47,13 @@ ENTITY_ID = f"{DOMAIN}.fake_name" -async def setup_fritzbox(hass: HomeAssistantType, config: dict): +async def setup_fritzbox(hass: HomeAssistant, config: dict): """Set up mock AVM Fritz!Box.""" assert await async_setup_component(hass, FB_DOMAIN, config) is True await hass.async_block_till_done() -async def test_setup(hass: HomeAssistantType, fritz: Mock): +async def test_setup(hass: HomeAssistant, fritz: Mock): """Test setup of platform.""" device = FritzDeviceClimateMock() fritz().get_devices.return_value = [device] @@ -80,7 +80,7 @@ async def test_setup(hass: HomeAssistantType, fritz: Mock): assert state.state == HVAC_MODE_HEAT -async def test_target_temperature_on(hass: HomeAssistantType, fritz: Mock): +async def test_target_temperature_on(hass: HomeAssistant, fritz: Mock): """Test turn device on.""" device = FritzDeviceClimateMock() fritz().get_devices.return_value = [device] @@ -92,7 +92,7 @@ async def test_target_temperature_on(hass: HomeAssistantType, fritz: Mock): assert state.attributes[ATTR_TEMPERATURE] == 30 -async def test_target_temperature_off(hass: HomeAssistantType, fritz: Mock): +async def test_target_temperature_off(hass: HomeAssistant, fritz: Mock): """Test turn device on.""" device = FritzDeviceClimateMock() fritz().get_devices.return_value = [device] @@ -104,7 +104,7 @@ async def test_target_temperature_off(hass: HomeAssistantType, fritz: Mock): assert state.attributes[ATTR_TEMPERATURE] == 0 -async def test_update(hass: HomeAssistantType, fritz: Mock): +async def test_update(hass: HomeAssistant, fritz: Mock): """Test update with error.""" device = FritzDeviceClimateMock() fritz().get_devices.return_value = [device] @@ -132,7 +132,7 @@ async def test_update(hass: HomeAssistantType, fritz: Mock): assert state.attributes[ATTR_TEMPERATURE] == 20 -async def test_update_error(hass: HomeAssistantType, fritz: Mock): +async def test_update_error(hass: HomeAssistant, fritz: Mock): """Test update with error.""" device = FritzDeviceClimateMock() device.update.side_effect = HTTPError("Boom") @@ -150,7 +150,7 @@ async def test_update_error(hass: HomeAssistantType, fritz: Mock): assert fritz().login.call_count == 2 -async def test_set_temperature_temperature(hass: HomeAssistantType, fritz: Mock): +async def test_set_temperature_temperature(hass: HomeAssistant, fritz: Mock): """Test setting temperature by temperature.""" device = FritzDeviceClimateMock() fritz().get_devices.return_value = [device] @@ -166,7 +166,7 @@ async def test_set_temperature_temperature(hass: HomeAssistantType, fritz: Mock) assert device.set_target_temperature.call_args_list == [call(123)] -async def test_set_temperature_mode_off(hass: HomeAssistantType, fritz: Mock): +async def test_set_temperature_mode_off(hass: HomeAssistant, fritz: Mock): """Test setting temperature by mode.""" device = FritzDeviceClimateMock() fritz().get_devices.return_value = [device] @@ -186,7 +186,7 @@ async def test_set_temperature_mode_off(hass: HomeAssistantType, fritz: Mock): assert device.set_target_temperature.call_args_list == [call(0)] -async def test_set_temperature_mode_heat(hass: HomeAssistantType, fritz: Mock): +async def test_set_temperature_mode_heat(hass: HomeAssistant, fritz: Mock): """Test setting temperature by mode.""" device = FritzDeviceClimateMock() fritz().get_devices.return_value = [device] @@ -206,7 +206,7 @@ async def test_set_temperature_mode_heat(hass: HomeAssistantType, fritz: Mock): assert device.set_target_temperature.call_args_list == [call(22)] -async def test_set_hvac_mode_off(hass: HomeAssistantType, fritz: Mock): +async def test_set_hvac_mode_off(hass: HomeAssistant, fritz: Mock): """Test setting hvac mode.""" device = FritzDeviceClimateMock() fritz().get_devices.return_value = [device] @@ -222,7 +222,7 @@ async def test_set_hvac_mode_off(hass: HomeAssistantType, fritz: Mock): assert device.set_target_temperature.call_args_list == [call(0)] -async def test_set_hvac_mode_heat(hass: HomeAssistantType, fritz: Mock): +async def test_set_hvac_mode_heat(hass: HomeAssistant, fritz: Mock): """Test setting hvac mode.""" device = FritzDeviceClimateMock() fritz().get_devices.return_value = [device] @@ -238,7 +238,7 @@ async def test_set_hvac_mode_heat(hass: HomeAssistantType, fritz: Mock): assert device.set_target_temperature.call_args_list == [call(22)] -async def test_set_preset_mode_comfort(hass: HomeAssistantType, fritz: Mock): +async def test_set_preset_mode_comfort(hass: HomeAssistant, fritz: Mock): """Test setting preset mode.""" device = FritzDeviceClimateMock() fritz().get_devices.return_value = [device] @@ -254,7 +254,7 @@ async def test_set_preset_mode_comfort(hass: HomeAssistantType, fritz: Mock): assert device.set_target_temperature.call_args_list == [call(22)] -async def test_set_preset_mode_eco(hass: HomeAssistantType, fritz: Mock): +async def test_set_preset_mode_eco(hass: HomeAssistant, fritz: Mock): """Test setting preset mode.""" device = FritzDeviceClimateMock() fritz().get_devices.return_value = [device] @@ -270,7 +270,7 @@ async def test_set_preset_mode_eco(hass: HomeAssistantType, fritz: Mock): assert device.set_target_temperature.call_args_list == [call(16)] -async def test_preset_mode_update(hass: HomeAssistantType, fritz: Mock): +async def test_preset_mode_update(hass: HomeAssistant, fritz: Mock): """Test preset mode.""" device = FritzDeviceClimateMock() device.comfort_temperature = 98 diff --git a/tests/components/fritzbox/test_config_flow.py b/tests/components/fritzbox/test_config_flow.py index 5d3fcc181ce6a..2ffa14003f01c 100644 --- a/tests/components/fritzbox/test_config_flow.py +++ b/tests/components/fritzbox/test_config_flow.py @@ -14,12 +14,12 @@ ) from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_SSDP, SOURCE_USER from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import ( RESULT_TYPE_ABORT, RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM, ) -from homeassistant.helpers.typing import HomeAssistantType from . import MOCK_CONFIG @@ -40,7 +40,7 @@ def fritz_fixture() -> Mock: yield fritz -async def test_user(hass: HomeAssistantType, fritz: Mock): +async def test_user(hass: HomeAssistant, fritz: Mock): """Test starting a flow by user.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -59,7 +59,7 @@ async def test_user(hass: HomeAssistantType, fritz: Mock): assert not result["result"].unique_id -async def test_user_auth_failed(hass: HomeAssistantType, fritz: Mock): +async def test_user_auth_failed(hass: HomeAssistant, fritz: Mock): """Test starting a flow by user with authentication failure.""" fritz().login.side_effect = [LoginError("Boom"), mock.DEFAULT] @@ -71,7 +71,7 @@ async def test_user_auth_failed(hass: HomeAssistantType, fritz: Mock): assert result["errors"]["base"] == "invalid_auth" -async def test_user_not_successful(hass: HomeAssistantType, fritz: Mock): +async def test_user_not_successful(hass: HomeAssistant, fritz: Mock): """Test starting a flow by user but no connection found.""" fritz().login.side_effect = OSError("Boom") @@ -82,7 +82,7 @@ async def test_user_not_successful(hass: HomeAssistantType, fritz: Mock): assert result["reason"] == "no_devices_found" -async def test_user_already_configured(hass: HomeAssistantType, fritz: Mock): +async def test_user_already_configured(hass: HomeAssistant, fritz: Mock): """Test starting a flow by user when already configured.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_DATA @@ -97,7 +97,7 @@ async def test_user_already_configured(hass: HomeAssistantType, fritz: Mock): assert result["reason"] == "already_configured" -async def test_reauth_success(hass: HomeAssistantType, fritz: Mock): +async def test_reauth_success(hass: HomeAssistant, fritz: Mock): """Test starting a reauthentication flow.""" mock_config = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) mock_config.add_to_hass(hass) @@ -124,7 +124,7 @@ async def test_reauth_success(hass: HomeAssistantType, fritz: Mock): assert mock_config.data[CONF_PASSWORD] == "other_fake_password" -async def test_reauth_auth_failed(hass: HomeAssistantType, fritz: Mock): +async def test_reauth_auth_failed(hass: HomeAssistant, fritz: Mock): """Test starting a reauthentication flow with authentication failure.""" fritz().login.side_effect = LoginError("Boom") @@ -152,7 +152,7 @@ async def test_reauth_auth_failed(hass: HomeAssistantType, fritz: Mock): assert result["errors"]["base"] == "invalid_auth" -async def test_reauth_not_successful(hass: HomeAssistantType, fritz: Mock): +async def test_reauth_not_successful(hass: HomeAssistant, fritz: Mock): """Test starting a reauthentication flow but no connection found.""" fritz().login.side_effect = OSError("Boom") @@ -179,7 +179,7 @@ async def test_reauth_not_successful(hass: HomeAssistantType, fritz: Mock): assert result["reason"] == "no_devices_found" -async def test_import(hass: HomeAssistantType, fritz: Mock): +async def test_import(hass: HomeAssistant, fritz: Mock): """Test starting a flow by import.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "import"}, data=MOCK_USER_DATA @@ -192,7 +192,7 @@ async def test_import(hass: HomeAssistantType, fritz: Mock): assert not result["result"].unique_id -async def test_ssdp(hass: HomeAssistantType, fritz: Mock): +async def test_ssdp(hass: HomeAssistant, fritz: Mock): """Test starting a flow from discovery.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA @@ -212,7 +212,7 @@ async def test_ssdp(hass: HomeAssistantType, fritz: Mock): assert result["result"].unique_id == "only-a-test" -async def test_ssdp_no_friendly_name(hass: HomeAssistantType, fritz: Mock): +async def test_ssdp_no_friendly_name(hass: HomeAssistant, fritz: Mock): """Test starting a flow from discovery without friendly name.""" MOCK_NO_NAME = MOCK_SSDP_DATA.copy() del MOCK_NO_NAME[ATTR_UPNP_FRIENDLY_NAME] @@ -234,7 +234,7 @@ async def test_ssdp_no_friendly_name(hass: HomeAssistantType, fritz: Mock): assert result["result"].unique_id == "only-a-test" -async def test_ssdp_auth_failed(hass: HomeAssistantType, fritz: Mock): +async def test_ssdp_auth_failed(hass: HomeAssistant, fritz: Mock): """Test starting a flow from discovery with authentication failure.""" fritz().login.side_effect = LoginError("Boom") @@ -254,7 +254,7 @@ async def test_ssdp_auth_failed(hass: HomeAssistantType, fritz: Mock): assert result["errors"]["base"] == "invalid_auth" -async def test_ssdp_not_successful(hass: HomeAssistantType, fritz: Mock): +async def test_ssdp_not_successful(hass: HomeAssistant, fritz: Mock): """Test starting a flow from discovery but no device found.""" fritz().login.side_effect = OSError("Boom") @@ -272,7 +272,7 @@ async def test_ssdp_not_successful(hass: HomeAssistantType, fritz: Mock): assert result["reason"] == "no_devices_found" -async def test_ssdp_not_supported(hass: HomeAssistantType, fritz: Mock): +async def test_ssdp_not_supported(hass: HomeAssistant, fritz: Mock): """Test starting a flow from discovery with unsupported device.""" fritz().get_device_elements.side_effect = HTTPError("Boom") @@ -290,7 +290,7 @@ async def test_ssdp_not_supported(hass: HomeAssistantType, fritz: Mock): assert result["reason"] == "not_supported" -async def test_ssdp_already_in_progress_unique_id(hass: HomeAssistantType, fritz: Mock): +async def test_ssdp_already_in_progress_unique_id(hass: HomeAssistant, fritz: Mock): """Test starting a flow from discovery twice.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA @@ -305,7 +305,7 @@ async def test_ssdp_already_in_progress_unique_id(hass: HomeAssistantType, fritz assert result["reason"] == "already_in_progress" -async def test_ssdp_already_in_progress_host(hass: HomeAssistantType, fritz: Mock): +async def test_ssdp_already_in_progress_host(hass: HomeAssistant, fritz: Mock): """Test starting a flow from discovery twice.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA @@ -322,7 +322,7 @@ async def test_ssdp_already_in_progress_host(hass: HomeAssistantType, fritz: Moc assert result["reason"] == "already_in_progress" -async def test_ssdp_already_configured(hass: HomeAssistantType, fritz: Mock): +async def test_ssdp_already_configured(hass: HomeAssistant, fritz: Mock): """Test starting a flow from discovery when already configured.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_DATA diff --git a/tests/components/fritzbox/test_init.py b/tests/components/fritzbox/test_init.py index dafb873fb8a87..bb5faa2c4d9d1 100644 --- a/tests/components/fritzbox/test_init.py +++ b/tests/components/fritzbox/test_init.py @@ -17,7 +17,7 @@ CONF_USERNAME, STATE_UNAVAILABLE, ) -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from . import MOCK_CONFIG, FritzDeviceSwitchMock @@ -25,7 +25,7 @@ from tests.common import MockConfigEntry -async def test_setup(hass: HomeAssistantType, fritz: Mock): +async def test_setup(hass: HomeAssistant, fritz: Mock): """Test setup of integration.""" assert await async_setup_component(hass, FB_DOMAIN, MOCK_CONFIG) await hass.async_block_till_done() @@ -40,7 +40,7 @@ async def test_setup(hass: HomeAssistantType, fritz: Mock): ] -async def test_setup_duplicate_config(hass: HomeAssistantType, fritz: Mock, caplog): +async def test_setup_duplicate_config(hass: HomeAssistant, fritz: Mock, caplog): """Test duplicate config of integration.""" DUPLICATE = { FB_DOMAIN: { @@ -57,7 +57,7 @@ async def test_setup_duplicate_config(hass: HomeAssistantType, fritz: Mock, capl assert "duplicate host entries found" in caplog.text -async def test_unload_remove(hass: HomeAssistantType, fritz: Mock): +async def test_unload_remove(hass: HomeAssistant, fritz: Mock): """Test unload and remove of integration.""" fritz().get_devices.return_value = [FritzDeviceSwitchMock()] entity_id = f"{SWITCH_DOMAIN}.fake_name" diff --git a/tests/components/fritzbox/test_sensor.py b/tests/components/fritzbox/test_sensor.py index 00c9923bbeaae..d26f2b935e980 100644 --- a/tests/components/fritzbox/test_sensor.py +++ b/tests/components/fritzbox/test_sensor.py @@ -16,7 +16,7 @@ PERCENTAGE, TEMP_CELSIUS, ) -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -27,13 +27,13 @@ ENTITY_ID = f"{DOMAIN}.fake_name" -async def setup_fritzbox(hass: HomeAssistantType, config: dict): +async def setup_fritzbox(hass: HomeAssistant, config: dict): """Set up mock AVM Fritz!Box.""" assert await async_setup_component(hass, FB_DOMAIN, config) await hass.async_block_till_done() -async def test_setup(hass: HomeAssistantType, fritz: Mock): +async def test_setup(hass: HomeAssistant, fritz: Mock): """Test setup of platform.""" device = FritzDeviceSensorMock() fritz().get_devices.return_value = [device] @@ -56,7 +56,7 @@ async def test_setup(hass: HomeAssistantType, fritz: Mock): assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE -async def test_update(hass: HomeAssistantType, fritz: Mock): +async def test_update(hass: HomeAssistant, fritz: Mock): """Test update with error.""" device = FritzDeviceSensorMock() fritz().get_devices.return_value = [device] @@ -73,7 +73,7 @@ async def test_update(hass: HomeAssistantType, fritz: Mock): assert fritz().login.call_count == 1 -async def test_update_error(hass: HomeAssistantType, fritz: Mock): +async def test_update_error(hass: HomeAssistant, fritz: Mock): """Test update with error.""" device = FritzDeviceSensorMock() device.update.side_effect = HTTPError("Boom") diff --git a/tests/components/fritzbox/test_switch.py b/tests/components/fritzbox/test_switch.py index 1c0f7b3f37a9d..31198aa950db3 100644 --- a/tests/components/fritzbox/test_switch.py +++ b/tests/components/fritzbox/test_switch.py @@ -23,7 +23,7 @@ STATE_ON, TEMP_CELSIUS, ) -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -34,13 +34,13 @@ ENTITY_ID = f"{DOMAIN}.fake_name" -async def setup_fritzbox(hass: HomeAssistantType, config: dict): +async def setup_fritzbox(hass: HomeAssistant, config: dict): """Set up mock AVM Fritz!Box.""" assert await async_setup_component(hass, FB_DOMAIN, config) await hass.async_block_till_done() -async def test_setup(hass: HomeAssistantType, fritz: Mock): +async def test_setup(hass: HomeAssistant, fritz: Mock): """Test setup of platform.""" device = FritzDeviceSwitchMock() fritz().get_devices.return_value = [device] @@ -60,7 +60,7 @@ async def test_setup(hass: HomeAssistantType, fritz: Mock): assert state.attributes[ATTR_TOTAL_CONSUMPTION_UNIT] == ENERGY_KILO_WATT_HOUR -async def test_turn_on(hass: HomeAssistantType, fritz: Mock): +async def test_turn_on(hass: HomeAssistant, fritz: Mock): """Test turn device on.""" device = FritzDeviceSwitchMock() fritz().get_devices.return_value = [device] @@ -73,7 +73,7 @@ async def test_turn_on(hass: HomeAssistantType, fritz: Mock): assert device.set_switch_state_on.call_count == 1 -async def test_turn_off(hass: HomeAssistantType, fritz: Mock): +async def test_turn_off(hass: HomeAssistant, fritz: Mock): """Test turn device off.""" device = FritzDeviceSwitchMock() fritz().get_devices.return_value = [device] @@ -86,7 +86,7 @@ async def test_turn_off(hass: HomeAssistantType, fritz: Mock): assert device.set_switch_state_off.call_count == 1 -async def test_update(hass: HomeAssistantType, fritz: Mock): +async def test_update(hass: HomeAssistant, fritz: Mock): """Test update with error.""" device = FritzDeviceSwitchMock() fritz().get_devices.return_value = [device] @@ -103,7 +103,7 @@ async def test_update(hass: HomeAssistantType, fritz: Mock): assert fritz().login.call_count == 1 -async def test_update_error(hass: HomeAssistantType, fritz: Mock): +async def test_update_error(hass: HomeAssistant, fritz: Mock): """Test update with error.""" device = FritzDeviceSwitchMock() device.update.side_effect = HTTPError("Boom") From 9003dbfdf35f2b695ade570508a6db4280a91615 Mon Sep 17 00:00:00 2001 From: MarBra <16831559+MarBra@users.noreply.github.com> Date: Thu, 22 Apr 2021 03:55:30 +0200 Subject: [PATCH 0437/1317] Add denonavr DynamicEQ and Audyssey service (#48694) * denonavr: Add DynamicEQ and Audyssey service * Remove debug print * Syntax sugar * Apply suggestions from code review Co-authored-by: J. Nick Koston * Update homeassistant/components/denonavr/services.yaml Co-authored-by: J. Nick Koston * Remove trailing whitespaces Co-authored-by: J. Nick Koston --- homeassistant/components/denonavr/__init__.py | 5 ++ .../components/denonavr/config_flow.py | 8 +++ .../components/denonavr/media_player.py | 64 +++++++++++++++++-- .../components/denonavr/services.yaml | 19 ++++++ .../components/denonavr/strings.json | 3 +- .../components/denonavr/translations/en.json | 3 +- tests/components/denonavr/test_config_flow.py | 4 ++ .../components/denonavr/test_media_player.py | 41 ++++++++++++ 8 files changed, 140 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/denonavr/__init__.py b/homeassistant/components/denonavr/__init__.py index fa4d161269731..853ade1f8a634 100644 --- a/homeassistant/components/denonavr/__init__.py +++ b/homeassistant/components/denonavr/__init__.py @@ -11,10 +11,12 @@ from .config_flow import ( CONF_SHOW_ALL_SOURCES, + CONF_UPDATE_AUDYSSEY, CONF_ZONE2, CONF_ZONE3, DEFAULT_SHOW_SOURCES, DEFAULT_TIMEOUT, + DEFAULT_UPDATE_AUDYSSEY, DEFAULT_ZONE2, DEFAULT_ZONE3, DOMAIN, @@ -53,6 +55,9 @@ async def async_setup_entry( hass.data[DOMAIN][entry.entry_id] = { CONF_RECEIVER: receiver, + CONF_UPDATE_AUDYSSEY: entry.options.get( + CONF_UPDATE_AUDYSSEY, DEFAULT_UPDATE_AUDYSSEY + ), UNDO_UPDATE_LISTENER: undo_listener, } diff --git a/homeassistant/components/denonavr/config_flow.py b/homeassistant/components/denonavr/config_flow.py index adcd4e26b6f79..695c323e1f799 100644 --- a/homeassistant/components/denonavr/config_flow.py +++ b/homeassistant/components/denonavr/config_flow.py @@ -30,11 +30,13 @@ CONF_MODEL = "model" CONF_MANUFACTURER = "manufacturer" CONF_SERIAL_NUMBER = "serial_number" +CONF_UPDATE_AUDYSSEY = "update_audyssey" DEFAULT_SHOW_SOURCES = False DEFAULT_TIMEOUT = 5 DEFAULT_ZONE2 = False DEFAULT_ZONE3 = False +DEFAULT_UPDATE_AUDYSSEY = False CONFIG_SCHEMA = vol.Schema({vol.Optional(CONF_HOST): str}) @@ -67,6 +69,12 @@ async def async_step_init(self, user_input: dict[str, Any] | None = None): CONF_ZONE3, default=self.config_entry.options.get(CONF_ZONE3, DEFAULT_ZONE3), ): bool, + vol.Optional( + CONF_UPDATE_AUDYSSEY, + default=self.config_entry.options.get( + CONF_UPDATE_AUDYSSEY, DEFAULT_UPDATE_AUDYSSEY + ), + ): bool, } ) diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index d7e0f8510dd51..254b7ffb02cd3 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -45,12 +45,15 @@ CONF_MODEL, CONF_SERIAL_NUMBER, CONF_TYPE, + CONF_UPDATE_AUDYSSEY, + DEFAULT_UPDATE_AUDYSSEY, DOMAIN, ) _LOGGER = logging.getLogger(__name__) ATTR_SOUND_MODE_RAW = "sound_mode_raw" +ATTR_DYNAMIC_EQ = "dynamic_eq" SUPPORT_DENON = ( SUPPORT_VOLUME_STEP @@ -75,6 +78,8 @@ # Services SERVICE_GET_COMMAND = "get_command" +SERVICE_SET_DYNAMIC_EQ = "set_dynamic_eq" +SERVICE_UPDATE_AUDYSSEY = "update_audyssey" async def async_setup_entry( @@ -84,14 +89,23 @@ async def async_setup_entry( ): """Set up the DenonAVR receiver from a config entry.""" entities = [] - receiver = hass.data[DOMAIN][config_entry.entry_id][CONF_RECEIVER] + data = hass.data[DOMAIN][config_entry.entry_id] + receiver = data[CONF_RECEIVER] + update_audyssey = data.get(CONF_UPDATE_AUDYSSEY, DEFAULT_UPDATE_AUDYSSEY) for receiver_zone in receiver.zones.values(): if config_entry.data[CONF_SERIAL_NUMBER] is not None: unique_id = f"{config_entry.unique_id}-{receiver_zone.zone}" else: unique_id = f"{config_entry.entry_id}-{receiver_zone.zone}" await receiver_zone.async_setup() - entities.append(DenonDevice(receiver_zone, unique_id, config_entry)) + entities.append( + DenonDevice( + receiver_zone, + unique_id, + config_entry, + update_audyssey, + ) + ) _LOGGER.debug( "%s receiver at host %s initialized", receiver.manufacturer, receiver.host ) @@ -103,6 +117,16 @@ async def async_setup_entry( {vol.Required(ATTR_COMMAND): cv.string}, f"async_{SERVICE_GET_COMMAND}", ) + platform.async_register_entity_service( + SERVICE_SET_DYNAMIC_EQ, + {vol.Required(ATTR_DYNAMIC_EQ): cv.boolean}, + f"async_{SERVICE_SET_DYNAMIC_EQ}", + ) + platform.async_register_entity_service( + SERVICE_UPDATE_AUDYSSEY, + {}, + f"async_{SERVICE_UPDATE_AUDYSSEY}", + ) async_add_entities(entities, update_before_add=True) @@ -115,11 +139,13 @@ def __init__( receiver: DenonAVR, unique_id: str, config_entry: config_entries.ConfigEntry, + update_audyssey: bool, ): """Initialize the device.""" self._receiver = receiver self._unique_id = unique_id self._config_entry = config_entry + self._update_audyssey = update_audyssey self._supported_features_base = SUPPORT_DENON self._supported_features_base |= ( @@ -194,6 +220,8 @@ async def wrapper(self, *args, **kwargs): async def async_update(self) -> None: """Get the latest status information from device.""" await self._receiver.async_update() + if self._update_audyssey: + await self._receiver.async_update_audyssey() @property def available(self): @@ -350,13 +378,22 @@ def media_episode(self): @property def extra_state_attributes(self): """Return device specific state attributes.""" + if self._receiver.power != POWER_ON: + return {} + state_attributes = {} if ( self._receiver.sound_mode_raw is not None and self._receiver.support_sound_mode - and self._receiver.power == POWER_ON ): - return {ATTR_SOUND_MODE_RAW: self._receiver.sound_mode_raw} - return {} + state_attributes[ATTR_SOUND_MODE_RAW] = self._receiver.sound_mode_raw + if self._receiver.dynamic_eq is not None: + state_attributes[ATTR_DYNAMIC_EQ] = self._receiver.dynamic_eq + return state_attributes + + @property + def dynamic_eq(self): + """Status of DynamicEQ.""" + return self._receiver.dynamic_eq @async_log_errors async def async_media_play_pause(self): @@ -436,6 +473,23 @@ async def async_get_command(self, command: str, **kwargs): """Send generic command.""" return await self._receiver.async_get_command(command) + @async_log_errors + async def async_update_audyssey(self): + """Get the latest audyssey information from device.""" + await self._receiver.async_update_audyssey() + + @async_log_errors + async def async_set_dynamic_eq(self, dynamic_eq: bool): + """Turn DynamicEQ on or off.""" + if dynamic_eq: + result = await self._receiver.async_dynamic_eq_on() + else: + result = await self._receiver.async_dynamic_eq_off() + + if self._update_audyssey: + await self._receiver.async_update_audyssey() + return result + # Decorator defined before is a staticmethod async_log_errors = staticmethod( # pylint: disable=no-staticmethod-decorator async_log_errors diff --git a/homeassistant/components/denonavr/services.yaml b/homeassistant/components/denonavr/services.yaml index 62157426bb221..d79652dd1f8ad 100644 --- a/homeassistant/components/denonavr/services.yaml +++ b/homeassistant/components/denonavr/services.yaml @@ -9,3 +9,22 @@ get_command: command: description: Endpoint of the command, including associated parameters. example: "/goform/formiPhoneAppDirect.xml?RCKSK0410370" +set_dynamic_eq: + description: "Enable or disable DynamicEQ." + target: + entity: + integration: denonavr + domain: media_player + fields: + dynamic_eq: + description: "True/false for enable/disable." + default: true + example: true + selector: + boolean: +update_audyssey: + description: "Update Audyssey settings." + target: + entity: + integration: denonavr + domain: media_player diff --git a/homeassistant/components/denonavr/strings.json b/homeassistant/components/denonavr/strings.json index c754c90606262..5e5c7665a47d9 100644 --- a/homeassistant/components/denonavr/strings.json +++ b/homeassistant/components/denonavr/strings.json @@ -40,7 +40,8 @@ "data": { "show_all_sources": "Show all sources", "zone2": "Set up Zone 2", - "zone3": "Set up Zone 3" + "zone3": "Set up Zone 3", + "update_audyssey": "Update Audyssey settings" } } } diff --git a/homeassistant/components/denonavr/translations/en.json b/homeassistant/components/denonavr/translations/en.json index 8c8f26d9b8cd4..b39a5608f81c5 100644 --- a/homeassistant/components/denonavr/translations/en.json +++ b/homeassistant/components/denonavr/translations/en.json @@ -38,7 +38,8 @@ "data": { "show_all_sources": "Show all sources", "zone2": "Set up Zone 2", - "zone3": "Set up Zone 3" + "zone3": "Set up Zone 3", + "update_audyssey": "Update Audyssey settings" }, "description": "Specify optional settings", "title": "Denon AVR Network Receivers" diff --git a/tests/components/denonavr/test_config_flow.py b/tests/components/denonavr/test_config_flow.py index 74ce77f1db760..b38c43775f933 100644 --- a/tests/components/denonavr/test_config_flow.py +++ b/tests/components/denonavr/test_config_flow.py @@ -11,6 +11,7 @@ CONF_SERIAL_NUMBER, CONF_SHOW_ALL_SOURCES, CONF_TYPE, + CONF_UPDATE_AUDYSSEY, CONF_ZONE2, CONF_ZONE3, DOMAIN, @@ -28,6 +29,7 @@ TEST_RECEIVER_TYPE = "avr-x" TEST_SERIALNUMBER = "123456789" TEST_MANUFACTURER = "Denon" +TEST_UPDATE_AUDYSSEY = False TEST_SSDP_LOCATION = f"http://{TEST_HOST}/" TEST_UNIQUE_ID = f"{TEST_MODEL}-{TEST_SERIALNUMBER}" TEST_DISCOVER_1_RECEIVER = [{CONF_HOST: TEST_HOST}] @@ -397,6 +399,7 @@ async def test_options_flow(hass): CONF_TYPE: TEST_RECEIVER_TYPE, CONF_MANUFACTURER: TEST_MANUFACTURER, CONF_SERIAL_NUMBER: TEST_SERIALNUMBER, + CONF_UPDATE_AUDYSSEY: TEST_UPDATE_AUDYSSEY, }, title=TEST_NAME, ) @@ -420,6 +423,7 @@ async def test_options_flow(hass): CONF_SHOW_ALL_SOURCES: True, CONF_ZONE2: True, CONF_ZONE3: True, + CONF_UPDATE_AUDYSSEY: False, } diff --git a/tests/components/denonavr/test_media_player.py b/tests/components/denonavr/test_media_player.py index 71c873a2b9d72..0607e7d42f7fc 100644 --- a/tests/components/denonavr/test_media_player.py +++ b/tests/components/denonavr/test_media_player.py @@ -13,7 +13,10 @@ ) from homeassistant.components.denonavr.media_player import ( ATTR_COMMAND, + ATTR_DYNAMIC_EQ, SERVICE_GET_COMMAND, + SERVICE_SET_DYNAMIC_EQ, + SERVICE_UPDATE_AUDYSSEY, ) from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST @@ -94,3 +97,41 @@ async def test_get_command(hass, client): await hass.async_block_till_done() client.async_get_command.assert_awaited_with("test_command") + + +async def test_dynamic_eq(hass, client): + """Test that dynamic eq method works.""" + await setup_denonavr(hass) + + data = { + ATTR_ENTITY_ID: ENTITY_ID, + ATTR_DYNAMIC_EQ: True, + } + # Verify on call + await hass.services.async_call(DOMAIN, SERVICE_SET_DYNAMIC_EQ, data) + await hass.async_block_till_done() + + # Verify off call + data[ATTR_DYNAMIC_EQ] = False + await hass.services.async_call(DOMAIN, SERVICE_SET_DYNAMIC_EQ, data) + await hass.async_block_till_done() + + client.async_dynamic_eq_on.assert_called_once() + client.async_dynamic_eq_off.assert_called_once() + + +async def test_update_audyssey(hass, client): + """Test that dynamic eq method works.""" + await setup_denonavr(hass) + + # Verify call + await hass.services.async_call( + DOMAIN, + SERVICE_UPDATE_AUDYSSEY, + { + ATTR_ENTITY_ID: ENTITY_ID, + }, + ) + await hass.async_block_till_done() + + client.async_update_audyssey.assert_called_once() From cb4558c0885e590242db92cc6f45f91c1bdb68c7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 21 Apr 2021 19:10:34 -1000 Subject: [PATCH 0438/1317] Autodetect zeroconf interface selection when not set (#49529) --- homeassistant/components/zeroconf/__init__.py | 61 ++++++++++++-- .../components/zeroconf/manifest.json | 2 +- homeassistant/package_constraints.txt | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/zeroconf/test_init.py | 80 +++++++++++++++++++ 6 files changed, 144 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index d2eaa6ca76622..7b13c7fd75385 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -5,10 +5,12 @@ import fnmatch from functools import partial import ipaddress +from ipaddress import ip_address import logging import socket -from typing import Any, TypedDict +from typing import Any, Iterable, TypedDict, cast +from pyroute2 import IPRoute import voluptuous as vol from zeroconf import ( Error as ZeroconfError, @@ -32,6 +34,7 @@ from homeassistant.helpers.network import NoURLAvailableError, get_url from homeassistant.helpers.singleton import singleton from homeassistant.loader import async_get_homekit, async_get_zeroconf +from homeassistant.util.network import is_loopback from .models import HaServiceBrowser, HaZeroconf from .usage import install_multiple_zeroconf_catcher @@ -55,6 +58,8 @@ HOMEKIT_PAIRED_STATUS_FLAG = "sf" HOMEKIT_MODEL = "md" +MDNS_TARGET_IP = "224.0.0.251" + # Property key=value has a max length of 255 # so we use 230 to leave space for key= MAX_PROPERTY_VALUE_LEN = 230 @@ -66,9 +71,7 @@ { DOMAIN: vol.Schema( { - vol.Optional( - CONF_DEFAULT_INTERFACE, default=DEFAULT_DEFAULT_INTERFACE - ): cv.boolean, + vol.Optional(CONF_DEFAULT_INTERFACE): cv.boolean, vol.Optional(CONF_IPV6, default=DEFAULT_IPV6): cv.boolean, } ) @@ -110,11 +113,59 @@ def _stop_zeroconf(_event: Event) -> None: return zeroconf +def _get_ip_route(dst_ip: str) -> Any: + """Get ip next hop.""" + return IPRoute().route("get", dst=dst_ip) + + +def _first_ip_nexthop_from_route(routes: Iterable) -> None | str: + """Find the first RTA_PREFSRC in the routes.""" + _LOGGER.debug("Routes: %s", routes) + for route in routes: + for key, value in route["attrs"]: + if key == "RTA_PREFSRC": + return cast(str, value) + return None + + +async def async_detect_interfaces_setting(hass: HomeAssistant) -> InterfaceChoice: + """Auto detect the interfaces setting when unset.""" + routes = [] + try: + routes = await hass.async_add_executor_job(_get_ip_route, MDNS_TARGET_IP) + except Exception as ex: # pylint: disable=broad-except + _LOGGER.debug( + "The system could not auto detect routing data on your operating system; Zeroconf will broadcast on all interfaces", + exc_info=ex, + ) + return InterfaceChoice.All + + if not (first_ip := _first_ip_nexthop_from_route(routes)): + _LOGGER.debug( + "The system could not auto detect the nexthop for %s on your operating system; Zeroconf will broadcast on all interfaces", + MDNS_TARGET_IP, + ) + return InterfaceChoice.All + + if is_loopback(ip_address(first_ip)): + _LOGGER.debug( + "The next hop for %s is %s; Zeroconf will broadcast on all interfaces", + MDNS_TARGET_IP, + first_ip, + ) + return InterfaceChoice.All + + return InterfaceChoice.Default + + async def async_setup(hass: HomeAssistant, config: dict) -> bool: """Set up Zeroconf and make Home Assistant discoverable.""" zc_config = config.get(DOMAIN, {}) zc_args: dict = {} - if zc_config.get(CONF_DEFAULT_INTERFACE, DEFAULT_DEFAULT_INTERFACE): + + if CONF_DEFAULT_INTERFACE not in zc_config: + zc_args["interfaces"] = await async_detect_interfaces_setting(hass) + elif zc_config[CONF_DEFAULT_INTERFACE]: zc_args["interfaces"] = InterfaceChoice.Default if not zc_config.get(CONF_IPV6, DEFAULT_IPV6): zc_args["ip_version"] = IPVersion.V4Only diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 149033c4acb6c..6e0c50e068368 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -2,7 +2,7 @@ "domain": "zeroconf", "name": "Zero-configuration networking (zeroconf)", "documentation": "https://www.home-assistant.io/integrations/zeroconf", - "requirements": ["zeroconf==0.29.0"], + "requirements": ["zeroconf==0.29.0","pyroute2==0.5.18"], "dependencies": ["api"], "codeowners": ["@bdraco"], "quality_scale": "internal", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 2f6700ff2efc0..1761a9de4f673 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -23,6 +23,7 @@ netdisco==2.8.2 paho-mqtt==1.5.1 pillow==8.1.2 pip>=8.0.3,<20.3 +pyroute2==0.5.18 python-slugify==4.0.1 pytz>=2021.1 pyyaml==5.4.1 diff --git a/requirements_all.txt b/requirements_all.txt index b52b8415755a7..b06589bac1b8c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1672,6 +1672,9 @@ pyrisco==0.3.1 # homeassistant.components.rituals_perfume_genie pyrituals==0.0.2 +# homeassistant.components.zeroconf +pyroute2==0.5.18 + # homeassistant.components.ruckus_unleashed pyruckus==0.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b0b64af715998..33f110af521fe 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -920,6 +920,9 @@ pyrisco==0.3.1 # homeassistant.components.rituals_perfume_genie pyrituals==0.0.2 +# homeassistant.components.zeroconf +pyroute2==0.5.18 + # homeassistant.components.ruckus_unleashed pyruckus==0.12 diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index e7a30abc73f0b..61b444a57843a 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -31,6 +31,27 @@ HOMEKIT_STATUS_UNPAIRED = b"1" HOMEKIT_STATUS_PAIRED = b"0" +_ROUTE_NO_LOOPBACK = ( + { + "attrs": [ + ("RTA_TABLE", 254), + ("RTA_DST", "224.0.0.251"), + ("RTA_OIF", 4), + ("RTA_PREFSRC", "192.168.1.5"), + ], + }, +) +_ROUTE_LOOPBACK = ( + { + "attrs": [ + ("RTA_TABLE", 254), + ("RTA_DST", "224.0.0.251"), + ("RTA_OIF", 4), + ("RTA_PREFSRC", "127.0.0.1"), + ], + }, +) + def service_update_mock(zeroconf, services, handlers, *, limit_service=None): """Call service update handler.""" @@ -611,3 +632,62 @@ def service_update_mock(zeroconf, services, handlers): assert len(mock_zeroconf.get_service_info.mock_calls) == 2 assert mock_zeroconf.get_service_info.mock_calls[0][1][0] == "_service.added" assert mock_zeroconf.get_service_info.mock_calls[1][1][0] == "_service.updated" + + +async def test_async_detect_interfaces_setting_non_loopback_route(hass, mock_zeroconf): + """Test without default interface config and the route returns a non-loopback address.""" + with patch.object(hass.config_entries.flow, "async_init"), patch.object( + zeroconf, "HaServiceBrowser", side_effect=service_update_mock + ), patch( + "homeassistant.components.zeroconf.IPRoute.route", + return_value=_ROUTE_NO_LOOPBACK, + ): + mock_zeroconf.get_service_info.side_effect = get_service_info_mock + assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert mock_zeroconf.called_with(interface_choice=InterfaceChoice.Default) + + +async def test_async_detect_interfaces_setting_loopback_route(hass, mock_zeroconf): + """Test without default interface config and the route returns a loopback address.""" + with patch.object(hass.config_entries.flow, "async_init"), patch.object( + zeroconf, "HaServiceBrowser", side_effect=service_update_mock + ), patch( + "homeassistant.components.zeroconf.IPRoute.route", return_value=_ROUTE_LOOPBACK + ): + mock_zeroconf.get_service_info.side_effect = get_service_info_mock + assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert mock_zeroconf.called_with(interface_choice=InterfaceChoice.All) + + +async def test_async_detect_interfaces_setting_empty_route(hass, mock_zeroconf): + """Test without default interface config and the route returns nothing.""" + with patch.object(hass.config_entries.flow, "async_init"), patch.object( + zeroconf, "HaServiceBrowser", side_effect=service_update_mock + ), patch("homeassistant.components.zeroconf.IPRoute.route", return_value=[]): + mock_zeroconf.get_service_info.side_effect = get_service_info_mock + assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert mock_zeroconf.called_with(interface_choice=InterfaceChoice.All) + + +async def test_async_detect_interfaces_setting_exception(hass, mock_zeroconf): + """Test without default interface config and the route throws an exception.""" + with patch.object(hass.config_entries.flow, "async_init"), patch.object( + zeroconf, "HaServiceBrowser", side_effect=service_update_mock + ), patch( + "homeassistant.components.zeroconf.IPRoute.route", side_effect=AttributeError + ): + mock_zeroconf.get_service_info.side_effect = get_service_info_mock + assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert mock_zeroconf.called_with(interface_choice=InterfaceChoice.All) From 303ab36c544c813221cc67326c15343a99a3e3ab Mon Sep 17 00:00:00 2001 From: corneyl Date: Thu, 22 Apr 2021 07:21:56 +0200 Subject: [PATCH 0439/1317] Add Picnic integration (#47507) Co-authored-by: Paulus Schoutsen Co-authored-by: @tkdrob --- CODEOWNERS | 1 + homeassistant/components/picnic/__init__.py | 59 +++ .../components/picnic/config_flow.py | 119 +++++ homeassistant/components/picnic/const.py | 118 +++++ .../components/picnic/coordinator.py | 151 +++++++ homeassistant/components/picnic/manifest.json | 9 + homeassistant/components/picnic/sensor.py | 109 +++++ homeassistant/components/picnic/strings.json | 22 + .../components/picnic/translations/en.json | 22 + .../components/picnic/translations/nl.json | 22 + homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/picnic/__init__.py | 1 + tests/components/picnic/test_config_flow.py | 124 ++++++ tests/components/picnic/test_sensor.py | 407 ++++++++++++++++++ 16 files changed, 1171 insertions(+) create mode 100644 homeassistant/components/picnic/__init__.py create mode 100644 homeassistant/components/picnic/config_flow.py create mode 100644 homeassistant/components/picnic/const.py create mode 100644 homeassistant/components/picnic/coordinator.py create mode 100644 homeassistant/components/picnic/manifest.json create mode 100644 homeassistant/components/picnic/sensor.py create mode 100644 homeassistant/components/picnic/strings.json create mode 100644 homeassistant/components/picnic/translations/en.json create mode 100644 homeassistant/components/picnic/translations/nl.json create mode 100644 tests/components/picnic/__init__.py create mode 100644 tests/components/picnic/test_config_flow.py create mode 100644 tests/components/picnic/test_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index a2ab0082cac26..6d044f4d06b6a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -356,6 +356,7 @@ homeassistant/components/persistent_notification/* @home-assistant/core homeassistant/components/philips_js/* @elupus homeassistant/components/pi4ioe5v9xxxx/* @antonverburg homeassistant/components/pi_hole/* @fabaff @johnluetke @shenxn +homeassistant/components/picnic/* @corneyl homeassistant/components/pilight/* @trekky12 homeassistant/components/plaato/* @JohNan homeassistant/components/plex/* @jjlawren diff --git a/homeassistant/components/picnic/__init__.py b/homeassistant/components/picnic/__init__.py new file mode 100644 index 0000000000000..003111088e101 --- /dev/null +++ b/homeassistant/components/picnic/__init__.py @@ -0,0 +1,59 @@ +"""The Picnic integration.""" +import asyncio + +from python_picnic_api import PicnicAPI + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.core import HomeAssistant + +from .const import CONF_API, CONF_COORDINATOR, CONF_COUNTRY_CODE, DOMAIN +from .coordinator import PicnicUpdateCoordinator + +PLATFORMS = ["sensor"] + + +def create_picnic_client(entry: ConfigEntry): + """Create an instance of the PicnicAPI client.""" + return PicnicAPI( + auth_token=entry.data.get(CONF_ACCESS_TOKEN), + country_code=entry.data.get(CONF_COUNTRY_CODE), + ) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Picnic from a config entry.""" + picnic_client = await hass.async_add_executor_job(create_picnic_client, entry) + picnic_coordinator = PicnicUpdateCoordinator(hass, picnic_client, entry) + + # Fetch initial data so we have data when entities subscribe + await picnic_coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = { + CONF_API: picnic_client, + CONF_COORDINATOR: picnic_coordinator, + } + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/picnic/config_flow.py b/homeassistant/components/picnic/config_flow.py new file mode 100644 index 0000000000000..0252e7caca533 --- /dev/null +++ b/homeassistant/components/picnic/config_flow.py @@ -0,0 +1,119 @@ +"""Config flow for Picnic integration.""" +import logging +from typing import Tuple + +from python_picnic_api import PicnicAPI +from python_picnic_api.session import PicnicAuthError +import requests +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_PASSWORD, CONF_USERNAME + +from .const import ( # pylint: disable=unused-import + CONF_COUNTRY_CODE, + COUNTRY_CODES, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_COUNTRY_CODE, default=COUNTRY_CODES[0]): vol.In( + COUNTRY_CODES + ), + } +) + + +class PicnicHub: + """Hub class to test user authentication.""" + + @staticmethod + def authenticate(username, password, country_code) -> Tuple[str, dict]: + """Test if we can authenticate with the Picnic API.""" + picnic = PicnicAPI(username, password, country_code) + return picnic.session.auth_token, picnic.get_user() + + +async def validate_input(hass: core.HomeAssistant, data): + """Validate the user input allows us to connect. + + Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. + """ + hub = PicnicHub() + + try: + auth_token, user_data = await hass.async_add_executor_job( + hub.authenticate, + data[CONF_USERNAME], + data[CONF_PASSWORD], + data[CONF_COUNTRY_CODE], + ) + except requests.exceptions.ConnectionError as error: + raise CannotConnect from error + except PicnicAuthError as error: + raise InvalidAuth from error + + # Return the validation result + address = ( + f'{user_data["address"]["street"]} {user_data["address"]["house_number"]}' + + f'{user_data["address"]["house_number_ext"]}' + ) + return auth_token, { + "title": address, + "unique_id": user_data["user_id"], + } + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Picnic.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + if user_input is None: + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA + ) + + errors = {} + + try: + auth_token, info = await validate_input(self.hass, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + # Set the unique id and abort if it already exists + await self.async_set_unique_id(info["unique_id"]) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=info["title"], + data={ + CONF_ACCESS_TOKEN: auth_token, + CONF_COUNTRY_CODE: user_input[CONF_COUNTRY_CODE], + }, + ) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/picnic/const.py b/homeassistant/components/picnic/const.py new file mode 100644 index 0000000000000..18a625897322d --- /dev/null +++ b/homeassistant/components/picnic/const.py @@ -0,0 +1,118 @@ +"""Constants for the Picnic integration.""" +from homeassistant.const import CURRENCY_EURO, DEVICE_CLASS_TIMESTAMP + +DOMAIN = "picnic" + +CONF_API = "api" +CONF_COORDINATOR = "coordinator" +CONF_COUNTRY_CODE = "country_code" + +COUNTRY_CODES = ["NL", "DE", "BE"] +ATTRIBUTION = "Data provided by Picnic" +ADDRESS = "address" +CART_DATA = "cart_data" +SLOT_DATA = "slot_data" +LAST_ORDER_DATA = "last_order_data" + +SENSOR_CART_ITEMS_COUNT = "cart_items_count" +SENSOR_CART_TOTAL_PRICE = "cart_total_price" +SENSOR_SELECTED_SLOT_START = "selected_slot_start" +SENSOR_SELECTED_SLOT_END = "selected_slot_end" +SENSOR_SELECTED_SLOT_MAX_ORDER_TIME = "selected_slot_max_order_time" +SENSOR_SELECTED_SLOT_MIN_ORDER_VALUE = "selected_slot_min_order_value" +SENSOR_LAST_ORDER_SLOT_START = "last_order_slot_start" +SENSOR_LAST_ORDER_SLOT_END = "last_order_slot_end" +SENSOR_LAST_ORDER_STATUS = "last_order_status" +SENSOR_LAST_ORDER_ETA_START = "last_order_eta_start" +SENSOR_LAST_ORDER_ETA_END = "last_order_eta_end" +SENSOR_LAST_ORDER_DELIVERY_TIME = "last_order_delivery_time" +SENSOR_LAST_ORDER_TOTAL_PRICE = "last_order_total_price" + +SENSOR_TYPES = { + SENSOR_CART_ITEMS_COUNT: { + "icon": "mdi:format-list-numbered", + "data_type": CART_DATA, + "state": lambda cart: cart.get("total_count", 0), + }, + SENSOR_CART_TOTAL_PRICE: { + "unit": CURRENCY_EURO, + "icon": "mdi:currency-eur", + "default_enabled": True, + "data_type": CART_DATA, + "state": lambda cart: cart.get("total_price", 0) / 100, + }, + SENSOR_SELECTED_SLOT_START: { + "class": DEVICE_CLASS_TIMESTAMP, + "icon": "mdi:calendar-start", + "default_enabled": True, + "data_type": SLOT_DATA, + "state": lambda slot: slot.get("window_start"), + }, + SENSOR_SELECTED_SLOT_END: { + "class": DEVICE_CLASS_TIMESTAMP, + "icon": "mdi:calendar-end", + "default_enabled": True, + "data_type": SLOT_DATA, + "state": lambda slot: slot.get("window_end"), + }, + SENSOR_SELECTED_SLOT_MAX_ORDER_TIME: { + "class": DEVICE_CLASS_TIMESTAMP, + "icon": "mdi:clock-alert-outline", + "default_enabled": True, + "data_type": SLOT_DATA, + "state": lambda slot: slot.get("cut_off_time"), + }, + SENSOR_SELECTED_SLOT_MIN_ORDER_VALUE: { + "unit": CURRENCY_EURO, + "icon": "mdi:currency-eur", + "default_enabled": True, + "data_type": SLOT_DATA, + "state": lambda slot: slot["minimum_order_value"] / 100 + if slot.get("minimum_order_value") + else None, + }, + SENSOR_LAST_ORDER_SLOT_START: { + "class": DEVICE_CLASS_TIMESTAMP, + "icon": "mdi:calendar-start", + "data_type": LAST_ORDER_DATA, + "state": lambda last_order: last_order.get("slot", {}).get("window_start"), + }, + SENSOR_LAST_ORDER_SLOT_END: { + "class": DEVICE_CLASS_TIMESTAMP, + "icon": "mdi:calendar-end", + "data_type": LAST_ORDER_DATA, + "state": lambda last_order: last_order.get("slot", {}).get("window_end"), + }, + SENSOR_LAST_ORDER_STATUS: { + "icon": "mdi:list-status", + "data_type": LAST_ORDER_DATA, + "state": lambda last_order: last_order.get("status"), + }, + SENSOR_LAST_ORDER_ETA_START: { + "class": DEVICE_CLASS_TIMESTAMP, + "icon": "mdi:clock-start", + "default_enabled": True, + "data_type": LAST_ORDER_DATA, + "state": lambda last_order: last_order.get("eta", {}).get("start"), + }, + SENSOR_LAST_ORDER_ETA_END: { + "class": DEVICE_CLASS_TIMESTAMP, + "icon": "mdi:clock-end", + "default_enabled": True, + "data_type": LAST_ORDER_DATA, + "state": lambda last_order: last_order.get("eta", {}).get("end"), + }, + SENSOR_LAST_ORDER_DELIVERY_TIME: { + "class": DEVICE_CLASS_TIMESTAMP, + "icon": "mdi:timeline-clock", + "default_enabled": True, + "data_type": LAST_ORDER_DATA, + "state": lambda last_order: last_order.get("delivery_time", {}).get("start"), + }, + SENSOR_LAST_ORDER_TOTAL_PRICE: { + "unit": CURRENCY_EURO, + "icon": "mdi:cash-marker", + "data_type": LAST_ORDER_DATA, + "state": lambda last_order: last_order.get("total_price", 0) / 100, + }, +} diff --git a/homeassistant/components/picnic/coordinator.py b/homeassistant/components/picnic/coordinator.py new file mode 100644 index 0000000000000..a4660344aaf20 --- /dev/null +++ b/homeassistant/components/picnic/coordinator.py @@ -0,0 +1,151 @@ +"""Coordinator to fetch data from the Picnic API.""" +import copy +from datetime import timedelta +import logging + +import async_timeout +from python_picnic_api import PicnicAPI +from python_picnic_api.session import PicnicAuthError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import ADDRESS, CART_DATA, LAST_ORDER_DATA, SLOT_DATA + + +class PicnicUpdateCoordinator(DataUpdateCoordinator): + """The coordinator to fetch data from the Picnic API at a set interval.""" + + def __init__( + self, + hass: HomeAssistant, + picnic_api_client: PicnicAPI, + config_entry: ConfigEntry, + ): + """Initialize the coordinator with the given Picnic API client.""" + self.picnic_api_client = picnic_api_client + self.config_entry = config_entry + self._user_address = None + + logger = logging.getLogger(__name__) + super().__init__( + hass, + logger, + name="Picnic coordinator", + update_interval=timedelta(minutes=30), + ) + + async def _async_update_data(self) -> dict: + """Fetch data from API endpoint.""" + try: + # Note: asyncio.TimeoutError and aiohttp.ClientError are already + # handled by the data update coordinator. + async with async_timeout.timeout(10): + data = await self.hass.async_add_executor_job(self.fetch_data) + + # Update the auth token in the config entry if applicable + self._update_auth_token() + + # Return the fetched data + return data + except ValueError as error: + raise UpdateFailed(f"API response was malformed: {error}") from error + except PicnicAuthError as error: + raise ConfigEntryAuthFailed from error + + def fetch_data(self): + """Fetch the data from the Picnic API and return a flat dict with only needed sensor data.""" + # Fetch from the API and pre-process the data + cart = self.picnic_api_client.get_cart() + last_order = self._get_last_order() + + if not cart or not last_order: + raise UpdateFailed("API response doesn't contain expected data.") + + slot_data = self._get_slot_data(cart) + + return { + ADDRESS: self._get_address(), + CART_DATA: cart, + SLOT_DATA: slot_data, + LAST_ORDER_DATA: last_order, + } + + def _get_address(self): + """Get the address that identifies the Picnic service.""" + if self._user_address is None: + address = self.picnic_api_client.get_user()["address"] + self._user_address = f'{address["street"]} {address["house_number"]}{address["house_number_ext"]}' + + return self._user_address + + @staticmethod + def _get_slot_data(cart: dict) -> dict: + """Get the selected slot, if it's explicitly selected.""" + selected_slot = cart.get("selected_slot", {}) + available_slots = cart.get("delivery_slots", []) + + if selected_slot.get("state") == "EXPLICIT": + slot_data = filter( + lambda slot: slot.get("slot_id") == selected_slot.get("slot_id"), + available_slots, + ) + if slot_data: + return next(slot_data) + + return {} + + def _get_last_order(self) -> dict: + """Get data of the last order from the list of deliveries.""" + # Get the deliveries + deliveries = self.picnic_api_client.get_deliveries(summary=True) + if not deliveries: + return {} + + # Determine the last order + last_order = copy.deepcopy(deliveries[0]) + + # Get the position details if the order is not delivered yet + delivery_position = {} + if not last_order.get("delivery_time"): + try: + delivery_position = self.picnic_api_client.get_delivery_position( + last_order["delivery_id"] + ) + except ValueError: + # No information yet can mean an empty response + pass + + # Determine the ETA, if available, the one from the delivery position API is more precise + # but it's only available shortly before the actual delivery. + last_order["eta"] = delivery_position.get( + "eta_window", last_order.get("eta2", {}) + ) + + # Determine the total price by adding up the total price of all sub-orders + total_price = 0 + for order in last_order.get("orders", []): + total_price += order.get("total_price", 0) + + # Sanitise the object + last_order["total_price"] = total_price + last_order.setdefault("delivery_time", {}) + if "eta2" in last_order: + del last_order["eta2"] + + # Make a copy because some references are local + return last_order + + @callback + def _update_auth_token(self): + """Set the updated authentication token.""" + updated_token = self.picnic_api_client.session.auth_token + if self.config_entry.data.get(CONF_ACCESS_TOKEN) != updated_token: + # Create an updated data dict + data = {**self.config_entry.data, CONF_ACCESS_TOKEN: updated_token} + + # Update the config entry + self.hass.config_entries.async_update_entry(self.config_entry, data=data) diff --git a/homeassistant/components/picnic/manifest.json b/homeassistant/components/picnic/manifest.json new file mode 100644 index 0000000000000..757f2ef24ad8b --- /dev/null +++ b/homeassistant/components/picnic/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "picnic", + "name": "Picnic", + "config_flow": true, + "iot_class": "cloud_polling", + "documentation": "https://www.home-assistant.io/integrations/picnic", + "requirements": ["python-picnic-api==1.1.0"], + "codeowners": ["@corneyl"] +} \ No newline at end of file diff --git a/homeassistant/components/picnic/sensor.py b/homeassistant/components/picnic/sensor.py new file mode 100644 index 0000000000000..d3778003646fd --- /dev/null +++ b/homeassistant/components/picnic/sensor.py @@ -0,0 +1,109 @@ +"""Definition of Picnic sensors.""" + +from typing import Any, Optional + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_ATTRIBUTION +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import StateType +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from .const import ADDRESS, ATTRIBUTION, CONF_COORDINATOR, DOMAIN, SENSOR_TYPES + + +async def async_setup_entry( + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities +): + """Set up Picnic sensor entries.""" + picnic_coordinator = hass.data[DOMAIN][config_entry.entry_id][CONF_COORDINATOR] + + # Add an entity for each sensor type + async_add_entities( + PicnicSensor(picnic_coordinator, config_entry, sensor_type, props) + for sensor_type, props in SENSOR_TYPES.items() + ) + + return True + + +class PicnicSensor(CoordinatorEntity): + """The CoordinatorEntity subclass representing Picnic sensors.""" + + def __init__( + self, + coordinator: DataUpdateCoordinator[Any], + config_entry: ConfigEntry, + sensor_type, + properties, + ): + """Init a Picnic sensor.""" + super().__init__(coordinator) + + self.sensor_type = sensor_type + self.properties = properties + self.entity_id = f"sensor.picnic_{sensor_type}" + self._service_unique_id = config_entry.unique_id + + @property + def unit_of_measurement(self) -> Optional[str]: + """Return the unit this state is expressed in.""" + return self.properties.get("unit") + + @property + def unique_id(self) -> Optional[str]: + """Return a unique ID.""" + return f"{self._service_unique_id}.{self.sensor_type}" + + @property + def name(self) -> Optional[str]: + """Return the name of the entity.""" + return self._to_capitalized_name(self.sensor_type) + + @property + def state(self) -> StateType: + """Return the state of the entity.""" + data_set = self.coordinator.data.get(self.properties["data_type"], {}) + return self.properties["state"](data_set) + + @property + def device_class(self) -> Optional[str]: + """Return the class of this device, from component DEVICE_CLASSES.""" + return self.properties.get("class") + + @property + def icon(self) -> Optional[str]: + """Return the icon to use in the frontend, if any.""" + return self.properties["icon"] + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self.coordinator.last_update_success and self.state is not None + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + return self.properties.get("default_enabled", False) + + @property + def extra_state_attributes(self): + """Return the sensor specific state attributes.""" + return {ATTR_ATTRIBUTION: ATTRIBUTION} + + @property + def device_info(self): + """Return device info.""" + return { + "identifiers": {(DOMAIN, self._service_unique_id)}, + "manufacturer": "Picnic", + "model": self._service_unique_id, + "name": f"Picnic: {self.coordinator.data[ADDRESS]}", + "entry_type": "service", + } + + @staticmethod + def _to_capitalized_name(name: str) -> str: + return name.replace("_", " ").capitalize() diff --git a/homeassistant/components/picnic/strings.json b/homeassistant/components/picnic/strings.json new file mode 100644 index 0000000000000..d43a91fbb0cb6 --- /dev/null +++ b/homeassistant/components/picnic/strings.json @@ -0,0 +1,22 @@ +{ + "title": "Picnic", + "config": { + "step": { + "user": { + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "country_code": "Country code" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/en.json b/homeassistant/components/picnic/translations/en.json new file mode 100644 index 0000000000000..2732abe8adc61 --- /dev/null +++ b/homeassistant/components/picnic/translations/en.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Picnic integration is already configured" + }, + "error": { + "cannot_connect": "Failed to connect to Picnic server", + "invalid_auth": "Invalid credentials", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Username", + "country_code": "County code" + } + } + } + }, + "title": "Picnic" +} \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/nl.json b/homeassistant/components/picnic/translations/nl.json new file mode 100644 index 0000000000000..78879f10b616c --- /dev/null +++ b/homeassistant/components/picnic/translations/nl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Picnic integratie is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan niet verbinden met Picnic server", + "invalid_auth": "Verkeerde gebruikersnaam/wachtwoord", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam", + "country_code": "Landcode" + } + } + } + }, + "title": "Picnic" +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 151b95a8f2030..f4bb23d698c2b 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -179,6 +179,7 @@ "panasonic_viera", "philips_js", "pi_hole", + "picnic", "plaato", "plex", "plugwise", diff --git a/requirements_all.txt b/requirements_all.txt index b06589bac1b8c..4fcb1250572b6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1830,6 +1830,9 @@ python-nmap==0.6.1 # homeassistant.components.ozw python-openzwave-mqtt[mqtt-client]==1.4.0 +# homeassistant.components.picnic +python-picnic-api==1.1.0 + # homeassistant.components.qbittorrent python-qbittorrent==0.4.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 33f110af521fe..4cfa722c5d565 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -985,6 +985,9 @@ python-nest==4.1.0 # homeassistant.components.ozw python-openzwave-mqtt[mqtt-client]==1.4.0 +# homeassistant.components.picnic +python-picnic-api==1.1.0 + # homeassistant.components.smarttub python-smarttub==0.0.23 diff --git a/tests/components/picnic/__init__.py b/tests/components/picnic/__init__.py new file mode 100644 index 0000000000000..fe6e65cbd2b3f --- /dev/null +++ b/tests/components/picnic/__init__.py @@ -0,0 +1 @@ +"""Tests for the Picnic integration.""" diff --git a/tests/components/picnic/test_config_flow.py b/tests/components/picnic/test_config_flow.py new file mode 100644 index 0000000000000..7cdc04e4a3976 --- /dev/null +++ b/tests/components/picnic/test_config_flow.py @@ -0,0 +1,124 @@ +"""Test the Picnic config flow.""" +from unittest.mock import patch + +from python_picnic_api.session import PicnicAuthError +import requests + +from homeassistant import config_entries, setup +from homeassistant.components.picnic.const import CONF_COUNTRY_CODE, DOMAIN +from homeassistant.const import CONF_ACCESS_TOKEN + + +async def test_form(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] is None + + auth_token = "af3wh738j3fa28l9fa23lhiufahu7l" + auth_data = { + "user_id": "f29-2a6-o32n", + "address": { + "street": "Teststreet", + "house_number": 123, + "house_number_ext": "b", + }, + } + with patch( + "homeassistant.components.picnic.config_flow.PicnicAPI", + ) as mock_picnic, patch( + "homeassistant.components.picnic.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + mock_picnic().session.auth_token = auth_token + mock_picnic().get_user.return_value = auth_data + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + "country_code": "NL", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "Teststreet 123b" + assert result2["data"] == { + CONF_ACCESS_TOKEN: auth_token, + CONF_COUNTRY_CODE: "NL", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_invalid_auth(hass): + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.picnic.config_flow.PicnicHub.authenticate", + side_effect=PicnicAuthError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + "country_code": "NL", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_cannot_connect(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.picnic.config_flow.PicnicHub.authenticate", + side_effect=requests.exceptions.ConnectionError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + "country_code": "NL", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_exception(hass): + """Test we handle random exceptions.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.picnic.config_flow.PicnicHub.authenticate", + side_effect=Exception, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + "country_code": "NL", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/picnic/test_sensor.py b/tests/components/picnic/test_sensor.py new file mode 100644 index 0000000000000..08a2e0282c036 --- /dev/null +++ b/tests/components/picnic/test_sensor.py @@ -0,0 +1,407 @@ +"""The tests for the Picnic sensor platform.""" +import copy +from datetime import timedelta +import unittest +from unittest.mock import patch + +import pytest +import requests + +from homeassistant import config_entries +from homeassistant.components.picnic import const +from homeassistant.components.picnic.const import CONF_COUNTRY_CODE, SENSOR_TYPES +from homeassistant.config_entries import CONN_CLASS_CLOUD_POLL +from homeassistant.const import ( + CONF_ACCESS_TOKEN, + CURRENCY_EURO, + DEVICE_CLASS_TIMESTAMP, + STATE_UNAVAILABLE, +) +from homeassistant.util import dt + +from tests.common import ( + MockConfigEntry, + async_fire_time_changed, + async_test_home_assistant, +) + +DEFAULT_USER_RESPONSE = { + "user_id": "295-6y3-1nf4", + "firstname": "User", + "lastname": "Name", + "address": { + "house_number": 123, + "house_number_ext": "a", + "postcode": "4321 AB", + "street": "Commonstreet", + "city": "Somewhere", + }, + "total_deliveries": 123, + "completed_deliveries": 112, +} +DEFAULT_CART_RESPONSE = { + "items": [], + "delivery_slots": [ + { + "slot_id": "611a3b074872b23576bef456a", + "window_start": "2021-03-03T14:45:00.000+01:00", + "window_end": "2021-03-03T15:45:00.000+01:00", + "cut_off_time": "2021-03-02T22:00:00.000+01:00", + "minimum_order_value": 3500, + }, + ], + "selected_slot": {"slot_id": "611a3b074872b23576bef456a", "state": "EXPLICIT"}, + "total_count": 10, + "total_price": 2535, +} +DEFAULT_DELIVERY_RESPONSE = { + "delivery_id": "z28fjso23e", + "creation_time": "2021-02-24T21:48:46.395+01:00", + "slot": { + "slot_id": "602473859a40dc24c6b65879", + "hub_id": "AMS", + "window_start": "2021-02-26T20:15:00.000+01:00", + "window_end": "2021-02-26T21:15:00.000+01:00", + "cut_off_time": "2021-02-25T22:00:00.000+01:00", + "minimum_order_value": 3500, + }, + "eta2": { + "start": "2021-02-26T20:54:00.000+01:00", + "end": "2021-02-26T21:14:00.000+01:00", + }, + "status": "COMPLETED", + "delivery_time": { + "start": "2021-02-26T20:54:05.221+01:00", + "end": "2021-02-26T20:58:31.802+01:00", + }, + "orders": [ + { + "creation_time": "2021-02-24T21:48:46.418+01:00", + "total_price": 3597, + }, + { + "creation_time": "2021-02-25T17:10:26.816+01:00", + "total_price": 536, + }, + ], +} + + +@pytest.mark.usefixtures("hass_storage") +class TestPicnicSensor(unittest.IsolatedAsyncioTestCase): + """Test the Picnic sensor.""" + + async def asyncSetUp(self): + """Set up things to be run when tests are started.""" + self.hass = await async_test_home_assistant(None) + self.entity_registry = ( + await self.hass.helpers.entity_registry.async_get_registry() + ) + + # Patch the api client + self.picnic_patcher = patch("homeassistant.components.picnic.PicnicAPI") + self.picnic_mock = self.picnic_patcher.start() + + # Add a config entry and setup the integration + config_data = { + CONF_ACCESS_TOKEN: "x-original-picnic-auth-token", + CONF_COUNTRY_CODE: "NL", + } + self.config_entry = MockConfigEntry( + domain=const.DOMAIN, + data=config_data, + connection_class=CONN_CLASS_CLOUD_POLL, + unique_id="295-6y3-1nf4", + ) + self.config_entry.add_to_hass(self.hass) + + async def asyncTearDown(self): + """Tear down the test setup, stop hass/patchers.""" + await self.hass.async_stop(force=True) + self.picnic_patcher.stop() + + @property + def _coordinator(self): + return self.hass.data[const.DOMAIN][self.config_entry.entry_id][ + const.CONF_COORDINATOR + ] + + def _assert_sensor(self, name, state=None, cls=None, unit=None, disabled=False): + sensor = self.hass.states.get(name) + if disabled: + assert sensor is None + return + + assert sensor.state == state + if cls: + assert sensor.attributes["device_class"] == cls + if unit: + assert sensor.attributes["unit_of_measurement"] == unit + + async def _setup_platform( + self, use_default_responses=False, enable_all_sensors=True + ): + """Set up the Picnic sensor platform.""" + if use_default_responses: + self.picnic_mock().get_user.return_value = copy.deepcopy( + DEFAULT_USER_RESPONSE + ) + self.picnic_mock().get_cart.return_value = copy.deepcopy( + DEFAULT_CART_RESPONSE + ) + self.picnic_mock().get_deliveries.return_value = [ + copy.deepcopy(DEFAULT_DELIVERY_RESPONSE) + ] + self.picnic_mock().get_delivery_position.return_value = {} + + await self.hass.config_entries.async_setup(self.config_entry.entry_id) + await self.hass.async_block_till_done() + + if enable_all_sensors: + await self._enable_all_sensors() + + async def _enable_all_sensors(self): + """Enable all sensors of the Picnic integration.""" + # Enable the sensors + for sensor_type in SENSOR_TYPES.keys(): + updated_entry = self.entity_registry.async_update_entity( + f"sensor.picnic_{sensor_type}", disabled_by=None + ) + assert updated_entry.disabled is False + await self.hass.async_block_till_done() + + # Trigger a reload of the data + async_fire_time_changed( + self.hass, + dt.utcnow() + + timedelta(seconds=config_entries.RELOAD_AFTER_UPDATE_DELAY + 1), + ) + await self.hass.async_block_till_done() + + async def test_sensor_setup_platform_not_available(self): + """Test the set-up of the sensor platform if API is not available.""" + # Configure mock requests to yield exceptions + self.picnic_mock().get_user.side_effect = requests.exceptions.ConnectionError + self.picnic_mock().get_cart.side_effect = requests.exceptions.ConnectionError + self.picnic_mock().get_deliveries.side_effect = ( + requests.exceptions.ConnectionError + ) + self.picnic_mock().get_delivery_position.side_effect = ( + requests.exceptions.ConnectionError + ) + await self._setup_platform(enable_all_sensors=False) + + # Assert that sensors are not set up + assert ( + self.hass.states.get("sensor.picnic_selected_slot_max_order_time") is None + ) + assert self.hass.states.get("sensor.picnic_last_order_status") is None + assert self.hass.states.get("sensor.picnic_last_order_total_price") is None + + async def test_sensors_setup(self): + """Test the default sensor setup behaviour.""" + await self._setup_platform(use_default_responses=True) + + self._assert_sensor("sensor.picnic_cart_items_count", "10") + self._assert_sensor( + "sensor.picnic_cart_total_price", "25.35", unit=CURRENCY_EURO + ) + self._assert_sensor( + "sensor.picnic_selected_slot_start", + "2021-03-03T14:45:00.000+01:00", + cls=DEVICE_CLASS_TIMESTAMP, + ) + self._assert_sensor( + "sensor.picnic_selected_slot_end", + "2021-03-03T15:45:00.000+01:00", + cls=DEVICE_CLASS_TIMESTAMP, + ) + self._assert_sensor( + "sensor.picnic_selected_slot_max_order_time", + "2021-03-02T22:00:00.000+01:00", + cls=DEVICE_CLASS_TIMESTAMP, + ) + self._assert_sensor("sensor.picnic_selected_slot_min_order_value", "35.0") + self._assert_sensor( + "sensor.picnic_last_order_slot_start", + "2021-02-26T20:15:00.000+01:00", + cls=DEVICE_CLASS_TIMESTAMP, + ) + self._assert_sensor( + "sensor.picnic_last_order_slot_end", + "2021-02-26T21:15:00.000+01:00", + cls=DEVICE_CLASS_TIMESTAMP, + ) + self._assert_sensor("sensor.picnic_last_order_status", "COMPLETED") + self._assert_sensor( + "sensor.picnic_last_order_eta_start", + "2021-02-26T20:54:00.000+01:00", + cls=DEVICE_CLASS_TIMESTAMP, + ) + self._assert_sensor( + "sensor.picnic_last_order_eta_end", + "2021-02-26T21:14:00.000+01:00", + cls=DEVICE_CLASS_TIMESTAMP, + ) + self._assert_sensor( + "sensor.picnic_last_order_delivery_time", + "2021-02-26T20:54:05.221+01:00", + cls=DEVICE_CLASS_TIMESTAMP, + ) + self._assert_sensor( + "sensor.picnic_last_order_total_price", "41.33", unit=CURRENCY_EURO + ) + + async def test_sensors_setup_disabled_by_default(self): + """Test that some sensors are disabled by default.""" + await self._setup_platform(use_default_responses=True, enable_all_sensors=False) + + self._assert_sensor("sensor.picnic_cart_items_count", disabled=True) + self._assert_sensor("sensor.picnic_last_order_slot_start", disabled=True) + self._assert_sensor("sensor.picnic_last_order_slot_end", disabled=True) + self._assert_sensor("sensor.picnic_last_order_status", disabled=True) + self._assert_sensor("sensor.picnic_last_order_total_price", disabled=True) + + async def test_sensors_no_selected_time_slot(self): + """Test sensor states with no explicit selected time slot.""" + # Adjust cart response + cart_response = copy.deepcopy(DEFAULT_CART_RESPONSE) + cart_response["selected_slot"]["state"] = "IMPLICIT" + + # Set mock responses + self.picnic_mock().get_user.return_value = copy.deepcopy(DEFAULT_USER_RESPONSE) + self.picnic_mock().get_cart.return_value = cart_response + self.picnic_mock().get_deliveries.return_value = [ + copy.deepcopy(DEFAULT_DELIVERY_RESPONSE) + ] + self.picnic_mock().get_delivery_position.return_value = {} + await self._setup_platform() + + # Assert sensors are unknown + self._assert_sensor("sensor.picnic_selected_slot_start", STATE_UNAVAILABLE) + self._assert_sensor("sensor.picnic_selected_slot_end", STATE_UNAVAILABLE) + self._assert_sensor( + "sensor.picnic_selected_slot_max_order_time", STATE_UNAVAILABLE + ) + self._assert_sensor( + "sensor.picnic_selected_slot_min_order_value", STATE_UNAVAILABLE + ) + + async def test_sensors_last_order_in_future(self): + """Test sensor states when last order is not yet delivered.""" + # Adjust default delivery response + delivery_response = copy.deepcopy(DEFAULT_DELIVERY_RESPONSE) + del delivery_response["delivery_time"] + + # Set mock responses + self.picnic_mock().get_user.return_value = copy.deepcopy(DEFAULT_USER_RESPONSE) + self.picnic_mock().get_cart.return_value = copy.deepcopy(DEFAULT_CART_RESPONSE) + self.picnic_mock().get_deliveries.return_value = [delivery_response] + self.picnic_mock().get_delivery_position.return_value = {} + await self._setup_platform() + + # Assert delivery time is not available, but eta is + self._assert_sensor("sensor.picnic_last_order_delivery_time", STATE_UNAVAILABLE) + self._assert_sensor( + "sensor.picnic_last_order_eta_start", "2021-02-26T20:54:00.000+01:00" + ) + self._assert_sensor( + "sensor.picnic_last_order_eta_end", "2021-02-26T21:14:00.000+01:00" + ) + + async def test_sensors_use_detailed_eta_if_available(self): + """Test sensor states when last order is not yet delivered.""" + # Set-up platform with default mock responses + await self._setup_platform(use_default_responses=True) + + # Provide a delivery position response with different ETA and remove delivery time from response + delivery_response = copy.deepcopy(DEFAULT_DELIVERY_RESPONSE) + del delivery_response["delivery_time"] + self.picnic_mock().get_deliveries.return_value = [delivery_response] + self.picnic_mock().get_delivery_position.return_value = { + "eta_window": { + "start": "2021-03-05T11:19:20.452+01:00", + "end": "2021-03-05T11:39:20.452+01:00", + } + } + await self._coordinator.async_refresh() + + # Assert detailed ETA is used + self.picnic_mock().get_delivery_position.assert_called_with( + delivery_response["delivery_id"] + ) + self._assert_sensor( + "sensor.picnic_last_order_eta_start", "2021-03-05T11:19:20.452+01:00" + ) + self._assert_sensor( + "sensor.picnic_last_order_eta_end", "2021-03-05T11:39:20.452+01:00" + ) + + async def test_sensors_no_data(self): + """Test sensor states when the api only returns empty objects.""" + # Setup platform with default responses + await self._setup_platform(use_default_responses=True) + + # Change mock responses to empty data and refresh the coordinator + self.picnic_mock().get_user.return_value = {} + self.picnic_mock().get_cart.return_value = None + self.picnic_mock().get_deliveries.return_value = None + self.picnic_mock().get_delivery_position.side_effect = ValueError + await self._coordinator.async_refresh() + + # Assert all default-enabled sensors have STATE_UNAVAILABLE because the last update failed + assert self._coordinator.last_update_success is False + self._assert_sensor("sensor.picnic_cart_total_price", STATE_UNAVAILABLE) + self._assert_sensor("sensor.picnic_selected_slot_start", STATE_UNAVAILABLE) + self._assert_sensor("sensor.picnic_selected_slot_end", STATE_UNAVAILABLE) + self._assert_sensor( + "sensor.picnic_selected_slot_max_order_time", STATE_UNAVAILABLE + ) + self._assert_sensor( + "sensor.picnic_selected_slot_min_order_value", STATE_UNAVAILABLE + ) + self._assert_sensor("sensor.picnic_last_order_eta_start", STATE_UNAVAILABLE) + self._assert_sensor("sensor.picnic_last_order_eta_end", STATE_UNAVAILABLE) + self._assert_sensor("sensor.picnic_last_order_delivery_time", STATE_UNAVAILABLE) + + async def test_sensors_malformed_response(self): + """Test coordinator update fails when API yields ValueError.""" + # Setup platform with default responses + await self._setup_platform(use_default_responses=True) + + # Change mock responses to empty data and refresh the coordinator + self.picnic_mock().get_user.side_effect = ValueError + self.picnic_mock().get_cart.side_effect = ValueError + await self._coordinator.async_refresh() + + # Assert coordinator update failed + assert self._coordinator.last_update_success is False + + async def test_device_registry_entry(self): + """Test if device registry entry is populated correctly.""" + # Setup platform and default mock responses + await self._setup_platform(use_default_responses=True) + + device_registry = await self.hass.helpers.device_registry.async_get_registry() + picnic_service = device_registry.async_get_device( + identifiers={(const.DOMAIN, DEFAULT_USER_RESPONSE["user_id"])} + ) + assert picnic_service.model == DEFAULT_USER_RESPONSE["user_id"] + assert picnic_service.name == "Picnic: Commonstreet 123a" + assert picnic_service.entry_type == "service" + + async def test_auth_token_is_saved_on_update(self): + """Test that auth-token changes in the session object are reflected by the config entry.""" + # Setup platform and default mock responses + await self._setup_platform(use_default_responses=True) + + # Set a different auth token in the session mock + updated_auth_token = "x-updated-picnic-auth-token" + self.picnic_mock().session.auth_token = updated_auth_token + + # Verify the updated auth token is not set and fetch data using the coordinator + assert self.config_entry.data.get(CONF_ACCESS_TOKEN) != updated_auth_token + await self._coordinator.async_refresh() + + # Verify that the updated auth token is saved in the config entry + assert self.config_entry.data.get(CONF_ACCESS_TOKEN) == updated_auth_token From c10836fcee7b052500279fa76f0cfdf3bb0cbcd7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 21 Apr 2021 20:29:36 -1000 Subject: [PATCH 0440/1317] Upgrade to sqlalchemy 1.4.11 (#49538) --- .github/workflows/ci.yaml | 1 + .../components/recorder/manifest.json | 2 +- .../components/recorder/migration.py | 109 +++++++++--------- homeassistant/components/recorder/util.py | 2 +- homeassistant/components/sql/manifest.json | 2 +- homeassistant/components/sql/sensor.py | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/recorder/models_original.py | 2 +- tests/components/recorder/test_migrate.py | 26 +++-- tests/components/recorder/test_util.py | 6 +- 12 files changed, 84 insertions(+), 74 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 0531d8555cac5..ae341f9aff191 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -13,6 +13,7 @@ env: CACHE_VERSION: 1 DEFAULT_PYTHON: 3.8 PRE_COMMIT_CACHE: ~/.cache/pre-commit + SQLALCHEMY_WARN_20: 1 jobs: # Separate job to pre-populate the base dependency cache diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json index e943e61d5c0b4..a79a79fbc4a7f 100644 --- a/homeassistant/components/recorder/manifest.json +++ b/homeassistant/components/recorder/manifest.json @@ -2,7 +2,7 @@ "domain": "recorder", "name": "Recorder", "documentation": "https://www.home-assistant.io/integrations/recorder", - "requirements": ["sqlalchemy==1.3.23"], + "requirements": ["sqlalchemy==1.4.11"], "codeowners": [], "quality_scale": "internal", "iot_class": "local_push" diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 5f138d01f176c..6c84e110f473e 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -1,8 +1,8 @@ """Schema migration helpers.""" import logging +import sqlalchemy from sqlalchemy import ForeignKeyConstraint, MetaData, Table, text -from sqlalchemy.engine import reflection from sqlalchemy.exc import ( InternalError, OperationalError, @@ -50,13 +50,13 @@ def migrate_schema(instance, current_version): for version in range(current_version, SCHEMA_VERSION): new_version = version + 1 _LOGGER.info("Upgrading recorder db schema to version %s", new_version) - _apply_update(instance.engine, new_version, current_version) + _apply_update(instance.engine, session, new_version, current_version) session.add(SchemaChanges(schema_version=new_version)) _LOGGER.info("Upgrade to version %s done", new_version) -def _create_index(engine, table_name, index_name): +def _create_index(connection, table_name, index_name): """Create an index for the specified table. The index name should match the name given for the index @@ -78,7 +78,7 @@ def _create_index(engine, table_name, index_name): index_name, ) try: - index.create(engine) + index.create(connection) except (InternalError, ProgrammingError, OperationalError) as err: lower_err_str = str(err).lower() @@ -92,7 +92,7 @@ def _create_index(engine, table_name, index_name): _LOGGER.debug("Finished creating %s", index_name) -def _drop_index(engine, table_name, index_name): +def _drop_index(connection, table_name, index_name): """Drop an index from a specified table. There is no universal way to do something like `DROP INDEX IF EXISTS` @@ -108,7 +108,7 @@ def _drop_index(engine, table_name, index_name): # Engines like DB2/Oracle try: - engine.execute(text(f"DROP INDEX {index_name}")) + connection.execute(text(f"DROP INDEX {index_name}")) except SQLAlchemyError: pass else: @@ -117,7 +117,7 @@ def _drop_index(engine, table_name, index_name): # Engines like SQLite, SQL Server if not success: try: - engine.execute( + connection.execute( text( "DROP INDEX {table}.{index}".format( index=index_name, table=table_name @@ -132,7 +132,7 @@ def _drop_index(engine, table_name, index_name): if not success: # Engines like MySQL, MS Access try: - engine.execute( + connection.execute( text( "DROP INDEX {index} ON {table}".format( index=index_name, table=table_name @@ -163,7 +163,7 @@ def _drop_index(engine, table_name, index_name): ) -def _add_columns(engine, table_name, columns_def): +def _add_columns(connection, table_name, columns_def): """Add columns to a table.""" _LOGGER.warning( "Adding columns %s to table %s. Note: this can take several " @@ -176,7 +176,7 @@ def _add_columns(engine, table_name, columns_def): columns_def = [f"ADD {col_def}" for col_def in columns_def] try: - engine.execute( + connection.execute( text( "ALTER TABLE {table} {columns_def}".format( table=table_name, columns_def=", ".join(columns_def) @@ -191,7 +191,7 @@ def _add_columns(engine, table_name, columns_def): for column_def in columns_def: try: - engine.execute( + connection.execute( text( "ALTER TABLE {table} {column_def}".format( table=table_name, column_def=column_def @@ -209,7 +209,7 @@ def _add_columns(engine, table_name, columns_def): ) -def _modify_columns(engine, table_name, columns_def): +def _modify_columns(connection, engine, table_name, columns_def): """Modify columns in a table.""" if engine.dialect.name == "sqlite": _LOGGER.debug( @@ -242,7 +242,7 @@ def _modify_columns(engine, table_name, columns_def): columns_def = [f"MODIFY {col_def}" for col_def in columns_def] try: - engine.execute( + connection.execute( text( "ALTER TABLE {table} {columns_def}".format( table=table_name, columns_def=", ".join(columns_def) @@ -255,7 +255,7 @@ def _modify_columns(engine, table_name, columns_def): for column_def in columns_def: try: - engine.execute( + connection.execute( text( "ALTER TABLE {table} {column_def}".format( table=table_name, column_def=column_def @@ -268,9 +268,9 @@ def _modify_columns(engine, table_name, columns_def): ) -def _update_states_table_with_foreign_key_options(engine): +def _update_states_table_with_foreign_key_options(connection, engine): """Add the options to foreign key constraints.""" - inspector = reflection.Inspector.from_engine(engine) + inspector = sqlalchemy.inspect(engine) alters = [] for foreign_key in inspector.get_foreign_keys(TABLE_STATES): if foreign_key["name"] and ( @@ -297,25 +297,26 @@ def _update_states_table_with_foreign_key_options(engine): for alter in alters: try: - engine.execute(DropConstraint(alter["old_fk"])) + connection.execute(DropConstraint(alter["old_fk"])) for fkc in states_key_constraints: if fkc.column_keys == alter["columns"]: - engine.execute(AddConstraint(fkc)) + connection.execute(AddConstraint(fkc)) except (InternalError, OperationalError): _LOGGER.exception( "Could not update foreign options in %s table", TABLE_STATES ) -def _apply_update(engine, new_version, old_version): +def _apply_update(engine, session, new_version, old_version): """Perform operations to bring schema up to date.""" + connection = session.connection() if new_version == 1: - _create_index(engine, "events", "ix_events_time_fired") + _create_index(connection, "events", "ix_events_time_fired") elif new_version == 2: # Create compound start/end index for recorder_runs - _create_index(engine, "recorder_runs", "ix_recorder_runs_start_end") + _create_index(connection, "recorder_runs", "ix_recorder_runs_start_end") # Create indexes for states - _create_index(engine, "states", "ix_states_last_updated") + _create_index(connection, "states", "ix_states_last_updated") elif new_version == 3: # There used to be a new index here, but it was removed in version 4. pass @@ -325,41 +326,41 @@ def _apply_update(engine, new_version, old_version): if old_version == 3: # Remove index that was added in version 3 - _drop_index(engine, "states", "ix_states_created_domain") + _drop_index(connection, "states", "ix_states_created_domain") if old_version == 2: # Remove index that was added in version 2 - _drop_index(engine, "states", "ix_states_entity_id_created") + _drop_index(connection, "states", "ix_states_entity_id_created") # Remove indexes that were added in version 0 - _drop_index(engine, "states", "states__state_changes") - _drop_index(engine, "states", "states__significant_changes") - _drop_index(engine, "states", "ix_states_entity_id_created") + _drop_index(connection, "states", "states__state_changes") + _drop_index(connection, "states", "states__significant_changes") + _drop_index(connection, "states", "ix_states_entity_id_created") - _create_index(engine, "states", "ix_states_entity_id_last_updated") + _create_index(connection, "states", "ix_states_entity_id_last_updated") elif new_version == 5: # Create supporting index for States.event_id foreign key - _create_index(engine, "states", "ix_states_event_id") + _create_index(connection, "states", "ix_states_event_id") elif new_version == 6: _add_columns( - engine, + session, "events", ["context_id CHARACTER(36)", "context_user_id CHARACTER(36)"], ) - _create_index(engine, "events", "ix_events_context_id") - _create_index(engine, "events", "ix_events_context_user_id") + _create_index(connection, "events", "ix_events_context_id") + _create_index(connection, "events", "ix_events_context_user_id") _add_columns( - engine, + connection, "states", ["context_id CHARACTER(36)", "context_user_id CHARACTER(36)"], ) - _create_index(engine, "states", "ix_states_context_id") - _create_index(engine, "states", "ix_states_context_user_id") + _create_index(connection, "states", "ix_states_context_id") + _create_index(connection, "states", "ix_states_context_user_id") elif new_version == 7: - _create_index(engine, "states", "ix_states_entity_id") + _create_index(connection, "states", "ix_states_entity_id") elif new_version == 8: - _add_columns(engine, "events", ["context_parent_id CHARACTER(36)"]) - _add_columns(engine, "states", ["old_state_id INTEGER"]) - _create_index(engine, "events", "ix_events_context_parent_id") + _add_columns(connection, "events", ["context_parent_id CHARACTER(36)"]) + _add_columns(connection, "states", ["old_state_id INTEGER"]) + _create_index(connection, "events", "ix_events_context_parent_id") elif new_version == 9: # We now get the context from events with a join # since its always there on state_changed events @@ -369,32 +370,36 @@ def _apply_update(engine, new_version, old_version): # and we would have to move to something like # sqlalchemy alembic to make that work # - _drop_index(engine, "states", "ix_states_context_id") - _drop_index(engine, "states", "ix_states_context_user_id") + _drop_index(connection, "states", "ix_states_context_id") + _drop_index(connection, "states", "ix_states_context_user_id") # This index won't be there if they were not running # nightly but we don't treat that as a critical issue - _drop_index(engine, "states", "ix_states_context_parent_id") + _drop_index(connection, "states", "ix_states_context_parent_id") # Redundant keys on composite index: # We already have ix_states_entity_id_last_updated - _drop_index(engine, "states", "ix_states_entity_id") - _create_index(engine, "events", "ix_events_event_type_time_fired") - _drop_index(engine, "events", "ix_events_event_type") + _drop_index(connection, "states", "ix_states_entity_id") + _create_index(connection, "events", "ix_events_event_type_time_fired") + _drop_index(connection, "events", "ix_events_event_type") elif new_version == 10: # Now done in step 11 pass elif new_version == 11: - _create_index(engine, "states", "ix_states_old_state_id") - _update_states_table_with_foreign_key_options(engine) + _create_index(connection, "states", "ix_states_old_state_id") + _update_states_table_with_foreign_key_options(connection, engine) elif new_version == 12: if engine.dialect.name == "mysql": - _modify_columns(engine, "events", ["event_data LONGTEXT"]) - _modify_columns(engine, "states", ["attributes LONGTEXT"]) + _modify_columns(connection, engine, "events", ["event_data LONGTEXT"]) + _modify_columns(connection, engine, "states", ["attributes LONGTEXT"]) elif new_version == 13: if engine.dialect.name == "mysql": _modify_columns( - engine, "events", ["time_fired DATETIME(6)", "created DATETIME(6)"] + connection, + engine, + "events", + ["time_fired DATETIME(6)", "created DATETIME(6)"], ) _modify_columns( + connection, engine, "states", [ @@ -404,7 +409,7 @@ def _apply_update(engine, new_version, old_version): ], ) elif new_version == 14: - _modify_columns(engine, "events", ["event_type VARCHAR(64)"]) + _modify_columns(connection, engine, "events", ["event_type VARCHAR(64)"]) else: raise ValueError(f"No schema migration defined for version {new_version}") @@ -418,7 +423,7 @@ def _inspect_schema_version(engine, session): version 1 are present to make the determination. Eventually this logic can be removed and we can assume a new db is being created. """ - inspector = reflection.Inspector.from_engine(engine) + inspector = sqlalchemy.inspect(engine) indexes = inspector.get_indexes("events") for index in indexes: diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index c18ff0a9830aa..e4e691bec915f 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -49,7 +49,7 @@ def session_scope( need_rollback = False try: yield session - if session.transaction: + if session.get_transaction(): need_rollback = True session.commit() except Exception as err: diff --git a/homeassistant/components/sql/manifest.json b/homeassistant/components/sql/manifest.json index 3eb1308c7f6fa..1716664c12990 100644 --- a/homeassistant/components/sql/manifest.json +++ b/homeassistant/components/sql/manifest.json @@ -2,7 +2,7 @@ "domain": "sql", "name": "SQL", "documentation": "https://www.home-assistant.io/integrations/sql", - "requirements": ["sqlalchemy==1.3.23"], + "requirements": ["sqlalchemy==1.4.11"], "codeowners": ["@dgomes"], "iot_class": "local_polling" } diff --git a/homeassistant/components/sql/sensor.py b/homeassistant/components/sql/sensor.py index b90ce2f8e594b..4c1c29b82a6df 100644 --- a/homeassistant/components/sql/sensor.py +++ b/homeassistant/components/sql/sensor.py @@ -151,7 +151,7 @@ def update(self): self._state = None return - for res in result: + for res in result.mappings(): _LOGGER.debug("result = %s", res.items()) data = res[self._column_name] for key, value in res.items(): diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1761a9de4f673..dfcf3e81c9a98 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -30,7 +30,7 @@ pyyaml==5.4.1 requests==2.25.1 ruamel.yaml==0.15.100 scapy==2.4.4 -sqlalchemy==1.3.23 +sqlalchemy==1.4.11 voluptuous-serialize==2.4.0 voluptuous==0.12.1 yarl==1.6.3 diff --git a/requirements_all.txt b/requirements_all.txt index 4fcb1250572b6..71ab25877f002 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2136,7 +2136,7 @@ spotipy==2.18.0 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.3.23 +sqlalchemy==1.4.11 # homeassistant.components.srp_energy srpenergy==1.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4cfa722c5d565..2209fc39d4798 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1138,7 +1138,7 @@ spotipy==2.18.0 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.3.23 +sqlalchemy==1.4.11 # homeassistant.components.srp_energy srpenergy==1.3.2 diff --git a/tests/components/recorder/models_original.py b/tests/components/recorder/models_original.py index 25978ef6d5567..4c9880d9257d8 100644 --- a/tests/components/recorder/models_original.py +++ b/tests/components/recorder/models_original.py @@ -19,7 +19,7 @@ Text, distinct, ) -from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import declarative_base from homeassistant.core import Event, EventOrigin, State, split_entity_id from homeassistant.helpers.json import JSONEncoder diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index ab5c7d54a28e3..59695b631e1ec 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -2,16 +2,17 @@ # pylint: disable=protected-access import datetime import sqlite3 -from unittest.mock import Mock, PropertyMock, call, patch +from unittest.mock import ANY, Mock, PropertyMock, call, patch import pytest -from sqlalchemy import create_engine +from sqlalchemy import create_engine, text from sqlalchemy.exc import ( DatabaseError, InternalError, OperationalError, ProgrammingError, ) +from sqlalchemy.orm import Session from sqlalchemy.pool import StaticPool from homeassistant.bootstrap import async_setup_component @@ -64,7 +65,7 @@ async def test_schema_update_calls(hass): assert await recorder.async_migration_in_progress(hass) is False update.assert_has_calls( [ - call(hass.data[DATA_INSTANCE].engine, version + 1, 0) + call(hass.data[DATA_INSTANCE].engine, ANY, version + 1, 0) for version in range(0, models.SCHEMA_VERSION) ] ) @@ -259,7 +260,7 @@ def _mock_setup_run(self): def test_invalid_update(): """Test that an invalid new version raises an exception.""" with pytest.raises(ValueError): - migration._apply_update(None, -1, 0) + migration._apply_update(Mock(), Mock(), -1, 0) @pytest.mark.parametrize( @@ -273,28 +274,31 @@ def test_invalid_update(): ) def test_modify_column(engine_type, substr): """Test that modify column generates the expected query.""" + connection = Mock() engine = Mock() engine.dialect.name = engine_type - migration._modify_columns(engine, "events", ["event_type VARCHAR(64)"]) + migration._modify_columns(connection, engine, "events", ["event_type VARCHAR(64)"]) if substr: - assert substr in engine.execute.call_args[0][0].text + assert substr in connection.execute.call_args[0][0].text else: - assert not engine.execute.called + assert not connection.execute.called def test_forgiving_add_column(): """Test that add column will continue if column exists.""" engine = create_engine("sqlite://", poolclass=StaticPool) - engine.execute("CREATE TABLE hello (id int)") - migration._add_columns(engine, "hello", ["context_id CHARACTER(36)"]) - migration._add_columns(engine, "hello", ["context_id CHARACTER(36)"]) + with Session(engine) as session: + session.execute(text("CREATE TABLE hello (id int)")) + migration._add_columns(session, "hello", ["context_id CHARACTER(36)"]) + migration._add_columns(session, "hello", ["context_id CHARACTER(36)"]) def test_forgiving_add_index(): """Test that add index will continue if index exists.""" engine = create_engine("sqlite://", poolclass=StaticPool) models.Base.metadata.create_all(engine) - migration._create_index(engine, "states", "ix_states_context_id") + with Session(engine) as session: + migration._create_index(session, "states", "ix_states_context_id") @pytest.mark.parametrize( diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index e4d942246c5d0..b5c5b68fe3f29 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -5,12 +5,12 @@ from unittest.mock import MagicMock, patch import pytest +from sqlalchemy import text from homeassistant.components.recorder import run_information_with_session, util from homeassistant.components.recorder.const import DATA_INSTANCE, SQLITE_URL_PREFIX from homeassistant.components.recorder.models import RecorderRuns from homeassistant.components.recorder.util import end_incomplete_runs, session_scope -from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.util import dt as dt_util from .common import corrupt_db_file @@ -55,7 +55,7 @@ def test_recorder_bad_commit(hass_recorder): def work(session): """Bad work.""" - session.execute("select * from notthere") + session.execute(text("select * from notthere")) with patch( "homeassistant.components.recorder.time.sleep" @@ -122,7 +122,7 @@ async def test_last_run_was_recently_clean(hass): is False ) - hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_add_executor_job(hass.data[DATA_INSTANCE]._end_session) await hass.async_block_till_done() assert ( From 8c52dfa1c5814cca5d6ff2eed1d91a6a3a7d2728 Mon Sep 17 00:00:00 2001 From: Matt Zimmerman Date: Thu, 22 Apr 2021 00:28:24 -0700 Subject: [PATCH 0441/1317] Implement reauth for smarttub (#47628) Co-authored-by: J. Nick Koston --- .../components/smarttub/config_flow.py | 84 +++++++++---- homeassistant/components/smarttub/const.py | 2 + .../components/smarttub/controller.py | 7 +- .../components/smarttub/strings.json | 6 +- .../components/smarttub/translations/en.json | 9 +- tests/components/smarttub/test_config_flow.py | 112 ++++++++++++++---- tests/components/smarttub/test_init.py | 17 ++- 7 files changed, 181 insertions(+), 56 deletions(-) diff --git a/homeassistant/components/smarttub/config_flow.py b/homeassistant/components/smarttub/config_flow.py index d3349060a0761..933f5a92367f1 100644 --- a/homeassistant/components/smarttub/config_flow.py +++ b/homeassistant/components/smarttub/config_flow.py @@ -10,44 +10,84 @@ from .const import DOMAIN from .controller import SmartTubController +_LOGGER = logging.getLogger(__name__) + + DATA_SCHEMA = vol.Schema( {vol.Required(CONF_EMAIL): str, vol.Required(CONF_PASSWORD): str} ) -_LOGGER = logging.getLogger(__name__) - - class SmartTubConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """SmartTub configuration flow.""" VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + def __init__(self) -> None: + """Instantiate config flow.""" + super().__init__() + self._reauth_input = None + self._reauth_entry = None + async def async_step_user(self, user_input=None): """Handle a flow initiated by the user.""" errors = {} - if user_input is None: - return self.async_show_form( - step_id="user", data_schema=DATA_SCHEMA, errors=errors - ) + if user_input is not None: + controller = SmartTubController(self.hass) + try: + account = await controller.login( + user_input[CONF_EMAIL], user_input[CONF_PASSWORD] + ) + + except LoginFailed: + errors["base"] = "invalid_auth" + else: + await self.async_set_unique_id(account.id) + + if self._reauth_input is None: + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=user_input[CONF_EMAIL], data=user_input + ) + + # this is a reauth attempt + if self._reauth_entry.unique_id != self.unique_id: + # there is a config entry matching this account, but it is not the one we were trying to reauth + return self.async_abort(reason="already_configured") + self.hass.config_entries.async_update_entry( + self._reauth_entry, data=user_input + ) + await self.hass.config_entries.async_reload(self._reauth_entry.entry_id) + return self.async_abort(reason="reauth_successful") + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) - controller = SmartTubController(self.hass) - try: - account = await controller.login( - user_input[CONF_EMAIL], user_input[CONF_PASSWORD] + async def async_step_reauth(self, user_input=None): + """Get new credentials if the current ones don't work anymore.""" + self._reauth_input = dict(user_input) + self._reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm(self, user_input=None): + """Dialog that informs the user that reauth is required.""" + if user_input is None: + # same as DATA_SCHEMA but with default email + data_schema = vol.Schema( + { + vol.Required( + CONF_EMAIL, default=self._reauth_input.get(CONF_EMAIL) + ): str, + vol.Required(CONF_PASSWORD): str, + } ) - except LoginFailed: - errors["base"] = "invalid_auth" return self.async_show_form( - step_id="user", data_schema=DATA_SCHEMA, errors=errors + step_id="reauth_confirm", + data_schema=data_schema, ) - - existing_entry = await self.async_set_unique_id(account.id) - if existing_entry: - self.hass.config_entries.async_update_entry(existing_entry, data=user_input) - await self.hass.config_entries.async_reload(existing_entry.entry_id) - return self.async_abort(reason="reauth_successful") - - return self.async_create_entry(title=user_input[CONF_EMAIL], data=user_input) + return await self.async_step_user(user_input) diff --git a/homeassistant/components/smarttub/const.py b/homeassistant/components/smarttub/const.py index 23bd8bd8ec0da..ad737bcd63a20 100644 --- a/homeassistant/components/smarttub/const.py +++ b/homeassistant/components/smarttub/const.py @@ -25,3 +25,5 @@ ATTR_PUMPS = "pumps" ATTR_REMINDERS = "reminders" ATTR_STATUS = "status" + +CONF_CONFIG_ENTRY = "config_entry" diff --git a/homeassistant/components/smarttub/controller.py b/homeassistant/components/smarttub/controller.py index 8139c72ab6e95..0b395a10fe58d 100644 --- a/homeassistant/components/smarttub/controller.py +++ b/homeassistant/components/smarttub/controller.py @@ -10,7 +10,7 @@ from smarttub.api import Account from homeassistant.const import CONF_EMAIL, CONF_PASSWORD -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -52,10 +52,9 @@ async def async_setup_entry(self, entry): self._account = await self.login( entry.data[CONF_EMAIL], entry.data[CONF_PASSWORD] ) - except LoginFailed: + except LoginFailed as ex: # credentials were changed or invalidated, we need new ones - - return False + raise ConfigEntryAuthFailed from ex except ( asyncio.TimeoutError, client_exceptions.ClientOSError, diff --git a/homeassistant/components/smarttub/strings.json b/homeassistant/components/smarttub/strings.json index 8ba888a9ffb3a..25528b8a374d6 100644 --- a/homeassistant/components/smarttub/strings.json +++ b/homeassistant/components/smarttub/strings.json @@ -8,13 +8,17 @@ "email": "[%key:common::config_flow::data::email%]", "password": "[%key:common::config_flow::data::password%]" } + }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "The SmartTub integration needs to re-authenticate your account" } }, "error": { "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } } diff --git a/homeassistant/components/smarttub/translations/en.json b/homeassistant/components/smarttub/translations/en.json index 4cf930918875b..752faa76b95f3 100644 --- a/homeassistant/components/smarttub/translations/en.json +++ b/homeassistant/components/smarttub/translations/en.json @@ -1,14 +1,17 @@ { "config": { "abort": { - "already_configured": "Device is already configured", + "already_configured": "Account is already configured", "reauth_successful": "Re-authentication was successful" }, "error": { - "invalid_auth": "Invalid authentication", - "unknown": "Unexpected error" + "invalid_auth": "Invalid authentication" }, "step": { + "reauth_confirm": { + "description": "The SmartTub integration needs to re-authenticate your account", + "title": "Reauthenticate Integration" + }, "user": { "data": { "email": "Email", diff --git a/tests/components/smarttub/test_config_flow.py b/tests/components/smarttub/test_config_flow.py index 8e4d575119e7a..c6170afc30e45 100644 --- a/tests/components/smarttub/test_config_flow.py +++ b/tests/components/smarttub/test_config_flow.py @@ -3,8 +3,11 @@ from smarttub import LoginFailed -from homeassistant import config_entries +from homeassistant import config_entries, data_entry_flow from homeassistant.components.smarttub.const import DOMAIN +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD + +from tests.common import MockConfigEntry async def test_form(hass): @@ -19,43 +22,104 @@ async def test_form(hass): "homeassistant.components.smarttub.async_setup_entry", return_value=True, ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], - {"email": "test-email", "password": "test-password"}, + {CONF_EMAIL: "test-email", CONF_PASSWORD: "test-password"}, ) - assert result2["type"] == "create_entry" - assert result2["title"] == "test-email" - assert result2["data"] == { - "email": "test-email", - "password": "test-password", - } - await hass.async_block_till_done() - mock_setup_entry.assert_called_once() + assert result["type"] == "create_entry" + assert result["title"] == "test-email" + assert result["data"] == { + CONF_EMAIL: "test-email", + CONF_PASSWORD: "test-password", + } + await hass.async_block_till_done() + mock_setup_entry.assert_called_once() + +async def test_form_invalid_auth(hass, smarttub_api): + """Test we handle invalid auth.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {"email": "test-email2", "password": "test-password2"} + smarttub_api.login.side_effect = LoginFailed + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_EMAIL: "test-email", CONF_PASSWORD: "test-password"}, ) - assert result2["type"] == "abort" - assert result2["reason"] == "reauth_successful" + assert result["type"] == "form" + assert result["errors"] == {"base": "invalid_auth"} + + +async def test_reauth_success(hass, smarttub_api, account): + """Test reauthentication flow.""" + mock_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_EMAIL: "test-email", CONF_PASSWORD: "test-password"}, + unique_id=account.id, + ) + mock_entry.add_to_hass(hass) -async def test_form_invalid_auth(hass, smarttub_api): - """Test we handle invalid auth.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": mock_entry.unique_id, + "entry_id": mock_entry.entry_id, + }, + data=mock_entry.data, ) - smarttub_api.login.side_effect = LoginFailed + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "reauth_confirm" - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"email": "test-email", "password": "test-password"}, + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_EMAIL: "test-email3", CONF_PASSWORD: "test-password3"} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "reauth_successful" + assert mock_entry.data[CONF_EMAIL] == "test-email3" + assert mock_entry.data[CONF_PASSWORD] == "test-password3" + + +async def test_reauth_wrong_account(hass, smarttub_api, account): + """Test reauthentication flow if the user enters credentials for a different already-configured account.""" + mock_entry1 = MockConfigEntry( + domain=DOMAIN, + data={CONF_EMAIL: "test-email1", CONF_PASSWORD: "test-password1"}, + unique_id=account.id, + ) + mock_entry1.add_to_hass(hass) + + mock_entry2 = MockConfigEntry( + domain=DOMAIN, + data={CONF_EMAIL: "test-email2", CONF_PASSWORD: "test-password2"}, + unique_id="mockaccount2", + ) + mock_entry2.add_to_hass(hass) + + # we try to reauth account #2, and the user successfully authenticates to account #1 + account.id = mock_entry1.unique_id + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": mock_entry2.unique_id, + "entry_id": mock_entry2.entry_id, + }, + data=mock_entry2.data, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_EMAIL: "test-email1", CONF_PASSWORD: "test-password1"} ) - assert result2["type"] == "form" - assert result2["errors"] == {"base": "invalid_auth"} + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" diff --git a/tests/components/smarttub/test_init.py b/tests/components/smarttub/test_init.py index 01989818d3be6..df44edb3da35e 100644 --- a/tests/components/smarttub/test_init.py +++ b/tests/components/smarttub/test_init.py @@ -1,13 +1,16 @@ """Test smarttub setup process.""" import asyncio +from unittest.mock import patch from smarttub import LoginFailed from homeassistant.components import smarttub +from homeassistant.components.smarttub.const import DOMAIN from homeassistant.config_entries import ( ENTRY_STATE_SETUP_ERROR, ENTRY_STATE_SETUP_RETRY, + SOURCE_REAUTH, ) from homeassistant.setup import async_setup_component @@ -35,8 +38,18 @@ async def test_setup_auth_failed(setup_component, hass, config_entry, smarttub_a smarttub_api.login.side_effect = LoginFailed config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ENTRY_STATE_SETUP_ERROR + with patch.object(hass.config_entries.flow, "async_init") as mock_flow_init: + await hass.config_entries.async_setup(config_entry.entry_id) + assert config_entry.state == ENTRY_STATE_SETUP_ERROR + mock_flow_init.assert_called_with( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "entry_id": config_entry.entry_id, + "unique_id": config_entry.unique_id, + }, + data=config_entry.data, + ) async def test_config_passed_to_config_entry(hass, config_entry, config_data): From 8b08134850a88d7ee67e3f46321e27fba72a5f2d Mon Sep 17 00:00:00 2001 From: bsmappee <58250533+bsmappee@users.noreply.github.com> Date: Thu, 22 Apr 2021 10:12:13 +0200 Subject: [PATCH 0442/1317] Support local Smappee Genius device (#48627) Co-authored-by: J. Nick Koston --- homeassistant/components/smappee/__init__.py | 19 +++- .../components/smappee/config_flow.py | 52 ++++++--- homeassistant/components/smappee/const.py | 2 +- .../components/smappee/manifest.json | 12 ++- homeassistant/components/smappee/sensor.py | 5 + homeassistant/generated/zeroconf.py | 4 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/smappee/test_config_flow.py | 100 +++++++++++++++++- 9 files changed, 172 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/smappee/__init__.py b/homeassistant/components/smappee/__init__.py index f803f38b8ea03..9c867b7d17f2b 100644 --- a/homeassistant/components/smappee/__init__.py +++ b/homeassistant/components/smappee/__init__.py @@ -1,7 +1,7 @@ """The Smappee integration.""" import asyncio -from pysmappee import Smappee +from pysmappee import Smappee, helper, mqtt import voluptuous as vol from homeassistant.config_entries import ConfigEntry @@ -75,8 +75,21 @@ async def async_setup(hass: HomeAssistant, config: dict): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Smappee from a zeroconf or config entry.""" if CONF_IP_ADDRESS in entry.data: - smappee_api = api.api.SmappeeLocalApi(ip=entry.data[CONF_IP_ADDRESS]) - smappee = Smappee(api=smappee_api, serialnumber=entry.data[CONF_SERIALNUMBER]) + if helper.is_smappee_genius(entry.data[CONF_SERIALNUMBER]): + # next generation: local mqtt broker + smappee_mqtt = mqtt.SmappeeLocalMqtt( + serial_number=entry.data[CONF_SERIALNUMBER] + ) + await hass.async_add_executor_job(smappee_mqtt.start_and_wait_for_config) + smappee = Smappee( + api=smappee_mqtt, serialnumber=entry.data[CONF_SERIALNUMBER] + ) + else: + # legacy devices through local api + smappee_api = api.api.SmappeeLocalApi(ip=entry.data[CONF_IP_ADDRESS]) + smappee = Smappee( + api=smappee_api, serialnumber=entry.data[CONF_SERIALNUMBER] + ) await hass.async_add_executor_job(smappee.load_local_service_location) else: implementation = ( diff --git a/homeassistant/components/smappee/config_flow.py b/homeassistant/components/smappee/config_flow.py index 450874b3f35ae..caa1bbf58f775 100644 --- a/homeassistant/components/smappee/config_flow.py +++ b/homeassistant/components/smappee/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Smappee.""" import logging +from pysmappee import helper, mqtt import voluptuous as vol from homeassistant import config_entries @@ -41,7 +42,6 @@ async def async_step_zeroconf(self, discovery_info): """Handle zeroconf discovery.""" if not discovery_info[CONF_HOSTNAME].startswith(SUPPORTED_LOCAL_DEVICES): - # We currently only support Energy and Solar models (legacy) return self.async_abort(reason="invalid_mdns") serial_number = ( @@ -86,10 +86,18 @@ async def async_step_zeroconf_confirm(self, user_input=None): serial_number = self.context.get(CONF_SERIALNUMBER) # Attempt to make a connection to the local device - smappee_api = api.api.SmappeeLocalApi(ip=ip_address) - logon = await self.hass.async_add_executor_job(smappee_api.logon) - if logon is None: - return self.async_abort(reason="cannot_connect") + if helper.is_smappee_genius(serial_number): + # next generation device, attempt connect to the local mqtt broker + smappee_mqtt = mqtt.SmappeeLocalMqtt(serial_number=serial_number) + connect = await self.hass.async_add_executor_job(smappee_mqtt.start_attempt) + if not connect: + return self.async_abort(reason="cannot_connect") + else: + # legacy devices, without local mqtt broker, try api access + smappee_api = api.api.SmappeeLocalApi(ip=ip_address) + logon = await self.hass.async_add_executor_job(smappee_api.logon) + if logon is None: + return self.async_abort(reason="cannot_connect") return self.async_create_entry( title=f"{DOMAIN}{serial_number}", @@ -141,23 +149,35 @@ async def async_step_local(self, user_input=None): ) # In a LOCAL setup we still need to resolve the host to serial number ip_address = user_input["host"] + serial_number = None + + # Attempt 1: try to use the local api (older generation) to resolve host to serialnumber smappee_api = api.api.SmappeeLocalApi(ip=ip_address) logon = await self.hass.async_add_executor_job(smappee_api.logon) - if logon is None: - return self.async_abort(reason="cannot_connect") - - advanced_config = await self.hass.async_add_executor_job( - smappee_api.load_advanced_config - ) - serial_number = None - for config_item in advanced_config: - if config_item["key"] == "mdnsHostName": - serial_number = config_item["value"] + if logon is not None: + advanced_config = await self.hass.async_add_executor_job( + smappee_api.load_advanced_config + ) + for config_item in advanced_config: + if config_item["key"] == "mdnsHostName": + serial_number = config_item["value"] + else: + # Attempt 2: try to use the local mqtt broker (newer generation) to resolve host to serialnumber + smappee_mqtt = mqtt.SmappeeLocalMqtt() + connect = await self.hass.async_add_executor_job(smappee_mqtt.start_attempt) + if not connect: + return self.async_abort(reason="cannot_connect") + + serial_number = await self.hass.async_add_executor_job( + smappee_mqtt.start_and_wait_for_config + ) + await self.hass.async_add_executor_job(smappee_mqtt.stop) + if serial_number is None: + return self.async_abort(reason="cannot_connect") if serial_number is None or not serial_number.startswith( SUPPORTED_LOCAL_DEVICES ): - # We currently only support Energy and Solar models (legacy) return self.async_abort(reason="invalid_mdns") serial_number = serial_number.replace("Smappee", "") diff --git a/homeassistant/components/smappee/const.py b/homeassistant/components/smappee/const.py index fc059509ced65..1abfc3a9b027f 100644 --- a/homeassistant/components/smappee/const.py +++ b/homeassistant/components/smappee/const.py @@ -14,7 +14,7 @@ PLATFORMS = ["binary_sensor", "sensor", "switch"] -SUPPORTED_LOCAL_DEVICES = ("Smappee1", "Smappee2") +SUPPORTED_LOCAL_DEVICES = ("Smappee1", "Smappee2", "Smappee50") MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=20) diff --git a/homeassistant/components/smappee/manifest.json b/homeassistant/components/smappee/manifest.json index cf693b8061c68..d6e9cc69f6f62 100644 --- a/homeassistant/components/smappee/manifest.json +++ b/homeassistant/components/smappee/manifest.json @@ -4,8 +4,12 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/smappee", "dependencies": ["http"], - "requirements": ["pysmappee==0.2.17"], - "codeowners": ["@bsmappee"], + "requirements": [ + "pysmappee==0.2.24" + ], + "codeowners": [ + "@bsmappee" + ], "zeroconf": [ { "type": "_ssh._tcp.local.", @@ -14,6 +18,10 @@ { "type": "_ssh._tcp.local.", "name": "smappee2*" + }, + { + "type": "_ssh._tcp.local.", + "name": "smappee50*" } ], "iot_class": "cloud_polling" diff --git a/homeassistant/components/smappee/sensor.py b/homeassistant/components/smappee/sensor.py index 43483dbdb1ecb..024845a08fc3c 100644 --- a/homeassistant/components/smappee/sensor.py +++ b/homeassistant/components/smappee/sensor.py @@ -205,6 +205,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if service_location.has_voltage_values: for sensor_name, sensor in VOLTAGE_SENSORS.items(): if service_location.phase_type in sensor[5]: + if ( + sensor_name.startswith("line_") + and service_location.local_polling + ): + continue entities.append( SmappeeSensor( smappee_base=smappee_base, diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 03f06fbc4c127..f1485bc6e8735 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -157,6 +157,10 @@ { "domain": "smappee", "name": "smappee2*" + }, + { + "domain": "smappee", + "name": "smappee50*" } ], "_touch-able._tcp.local.": [ diff --git a/requirements_all.txt b/requirements_all.txt index 71ab25877f002..f7ae5b4a14107 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1714,7 +1714,7 @@ pyskyqhub==0.1.3 pysma==0.4.3 # homeassistant.components.smappee -pysmappee==0.2.17 +pysmappee==0.2.24 # homeassistant.components.smartthings pysmartapp==0.3.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2209fc39d4798..2bdab44dc49c6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -941,7 +941,7 @@ pysignalclirestapi==0.3.4 pysma==0.4.3 # homeassistant.components.smappee -pysmappee==0.2.17 +pysmappee==0.2.24 # homeassistant.components.smartthings pysmartapp==0.3.3 diff --git a/tests/components/smappee/test_config_flow.py b/tests/components/smappee/test_config_flow.py index cba962d3e4448..bc9175a3b46d2 100644 --- a/tests/components/smappee/test_config_flow.py +++ b/tests/components/smappee/test_config_flow.py @@ -77,9 +77,69 @@ async def test_show_zeroconf_connection_error_form(hass): assert len(hass.config_entries.async_entries(DOMAIN)) == 0 +async def test_show_zeroconf_connection_error_form_next_generation(hass): + """Test that the zeroconf confirmation form is served.""" + with patch("pysmappee.mqtt.SmappeeLocalMqtt.start_attempt", return_value=False): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data={ + "host": "1.2.3.4", + "port": 22, + CONF_HOSTNAME: "Smappee5001000212.local.", + "type": "_ssh._tcp.local.", + "name": "Smappee5001000212._ssh._tcp.local.", + "properties": {"_raw": {}}, + }, + ) + + assert result["description_placeholders"] == {CONF_SERIALNUMBER: "5001000212"} + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "zeroconf_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"host": "1.2.3.4"} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "cannot_connect" + assert len(hass.config_entries.async_entries(DOMAIN)) == 0 + + async def test_connection_error(hass): """Test we show user form on Smappee connection error.""" - with patch("pysmappee.api.SmappeeLocalApi.logon", return_value=None): + with patch("pysmappee.api.SmappeeLocalApi.logon", return_value=None), patch( + "pysmappee.mqtt.SmappeeLocalMqtt.start_attempt", return_value=None + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + assert result["step_id"] == "environment" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"environment": ENV_LOCAL} + ) + assert result["step_id"] == ENV_LOCAL + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"host": "1.2.3.4"} + ) + assert result["reason"] == "cannot_connect" + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + + +async def test_user_local_connection_error(hass): + """Test we show user form on Smappee connection error in local next generation option.""" + with patch("pysmappee.api.SmappeeLocalApi.logon", return_value=None), patch( + "pysmappee.mqtt.SmappeeLocalMqtt.start_attempt", return_value=True + ), patch("pysmappee.mqtt.SmappeeLocalMqtt.start", return_value=True), patch( + "pysmappee.mqtt.SmappeeLocalMqtt.stop", return_value=True + ), patch( + "pysmappee.mqtt.SmappeeLocalMqtt.is_config_ready", return_value=None + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, @@ -123,7 +183,7 @@ async def test_full_user_wrong_mdns(hass): """Test we abort user flow if unsupported mDNS name got resolved.""" with patch("pysmappee.api.SmappeeLocalApi.logon", return_value={}), patch( "pysmappee.api.SmappeeLocalApi.load_advanced_config", - return_value=[{"key": "mdnsHostName", "value": "Smappee5010000001"}], + return_value=[{"key": "mdnsHostName", "value": "Smappee5100000001"}], ), patch( "pysmappee.api.SmappeeLocalApi.load_command_control_config", return_value=[] ), patch( @@ -464,3 +524,39 @@ async def test_full_user_local_flow(hass): entry = hass.config_entries.async_entries(DOMAIN)[0] assert entry.unique_id == "1006000212" + + +async def test_full_zeroconf_flow_next_generation(hass): + """Test the full zeroconf flow.""" + with patch( + "pysmappee.mqtt.SmappeeLocalMqtt.start_attempt", return_value=True + ), patch("pysmappee.mqtt.SmappeeLocalMqtt.start", return_value=None,), patch( + "pysmappee.mqtt.SmappeeLocalMqtt.is_config_ready", + return_value=None, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data={ + "host": "1.2.3.4", + "port": 22, + CONF_HOSTNAME: "Smappee5001000212.local.", + "type": "_ssh._tcp.local.", + "name": "Smappee5001000212._ssh._tcp.local.", + "properties": {"_raw": {}}, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "zeroconf_confirm" + assert result["description_placeholders"] == {CONF_SERIALNUMBER: "5001000212"} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"host": "1.2.3.4"} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "smappee5001000212" + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + entry = hass.config_entries.async_entries(DOMAIN)[0] + assert entry.unique_id == "5001000212" From f67c0ce8bb006e29f955b82c20a414e0dbe3f35b Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 22 Apr 2021 11:54:40 +0200 Subject: [PATCH 0443/1317] Secure 100% test coverage for modbus, binary_sensor and sensor (#49521) * Secure 100% test coverage for modbus/binary_sensor. * Test that class constructor is called. --- .coveragerc | 3 - homeassistant/components/modbus/sensor.py | 21 +-- tests/components/modbus/conftest.py | 25 ++- tests/components/modbus/test_init.py | 155 ++++++++++++++++++ .../modbus/test_modbus_binary_sensor.py | 5 + tests/components/modbus/test_modbus_sensor.py | 134 ++++++++++++--- 6 files changed, 290 insertions(+), 53 deletions(-) diff --git a/.coveragerc b/.coveragerc index 86b129f636c9f..26d49395164c7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -616,10 +616,7 @@ omit = homeassistant/components/mochad/* homeassistant/components/modbus/climate.py homeassistant/components/modbus/cover.py - homeassistant/components/modbus/modbus.py homeassistant/components/modbus/switch.py - homeassistant/components/modbus/sensor.py - homeassistant/components/modbus/binary_sensor.py homeassistant/components/modem_callerid/sensor.py homeassistant/components/motion_blinds/__init__.py homeassistant/components/motion_blinds/const.py diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index 89c68947d3fc8..254bfe6e0fbfe 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -4,7 +4,6 @@ from datetime import timedelta import logging import struct -from typing import Any import voluptuous as vol @@ -31,6 +30,7 @@ from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from . import number from .const import ( CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT, @@ -58,25 +58,6 @@ _LOGGER = logging.getLogger(__name__) -def number(value: Any) -> int | float: - """Coerce a value to number without losing precision.""" - if isinstance(value, int): - return value - - if isinstance(value, str): - try: - value = int(value) - return value - except (TypeError, ValueError): - pass - - try: - value = float(value) - return value - except (TypeError, ValueError) as err: - raise vol.Invalid(f"invalid number {value}") from err - - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_REGISTERS): [ diff --git a/tests/components/modbus/conftest.py b/tests/components/modbus/conftest.py index 761f2c7e1413e..cbfddb4488be4 100644 --- a/tests/components/modbus/conftest.py +++ b/tests/components/modbus/conftest.py @@ -3,6 +3,7 @@ import logging from unittest import mock +from pymodbus.exceptions import ModbusException import pytest from homeassistant.components.modbus.const import DEFAULT_HUB, MODBUS_DOMAIN as DOMAIN @@ -69,11 +70,23 @@ async def base_test( ): # Setup inputs for the sensor - read_result = ReadResult(register_words) - mock_sync.read_coils.return_value = read_result - mock_sync.read_discrete_inputs.return_value = read_result - mock_sync.read_input_registers.return_value = read_result - mock_sync.read_holding_registers.return_value = read_result + if register_words is None: + mock_sync.read_coils.side_effect = ModbusException("fail read_coils") + mock_sync.read_discrete_inputs.side_effect = ModbusException( + "fail read_coils" + ) + mock_sync.read_input_registers.side_effect = ModbusException( + "fail read_coils" + ) + mock_sync.read_holding_registers.side_effect = ModbusException( + "fail read_coils" + ) + else: + read_result = ReadResult(register_words) + mock_sync.read_coils.return_value = read_result + mock_sync.read_discrete_inputs.return_value = read_result + mock_sync.read_input_registers.return_value = read_result + mock_sync.read_holding_registers.return_value = read_result # mock timer and add old/new config now = dt_util.utcnow() @@ -104,7 +117,7 @@ async def base_test( assert await async_setup_component(hass, entity_domain, config_device) await hass.async_block_till_done() - assert DOMAIN in hass.data + assert DOMAIN in hass.config.components if config_device is not None: entity_id = f"{entity_domain}.{device_name}" device = hass.states.get(entity_id) diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index 393a9ce86da0d..2da3a75350504 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -2,16 +2,24 @@ import logging from unittest import mock +from pymodbus.exceptions import ModbusException import pytest import voluptuous as vol from homeassistant.components.modbus import number from homeassistant.components.modbus.const import ( + ATTR_ADDRESS, + ATTR_HUB, + ATTR_STATE, + ATTR_UNIT, + ATTR_VALUE, CONF_BAUDRATE, CONF_BYTESIZE, CONF_PARITY, CONF_STOPBITS, MODBUS_DOMAIN as DOMAIN, + SERVICE_WRITE_COIL, + SERVICE_WRITE_REGISTER, ) from homeassistant.const import ( CONF_DELAY, @@ -177,3 +185,150 @@ async def test_config_multiple_modbus(hass, caplog): await _config_helper(hass, do_config) assert DOMAIN in hass.config.components assert len(caplog.records) == 0 + + +async def test_pb_service_write_register(hass): + """Run test for service write_register.""" + + conf_name = "myModbus" + config = { + DOMAIN: [ + { + CONF_TYPE: "tcp", + CONF_HOST: "modbusTestHost", + CONF_PORT: 5501, + CONF_NAME: conf_name, + } + ] + } + + mock_pb = mock.MagicMock() + with mock.patch( + "homeassistant.components.modbus.modbus.ModbusTcpClient", return_value=mock_pb + ): + assert await async_setup_component(hass, DOMAIN, config) is True + await hass.async_block_till_done() + + data = {ATTR_HUB: conf_name, ATTR_UNIT: 17, ATTR_ADDRESS: 16, ATTR_VALUE: 15} + await hass.services.async_call( + DOMAIN, SERVICE_WRITE_REGISTER, data, blocking=True + ) + assert mock_pb.write_register.called + assert mock_pb.write_register.call_args[0] == ( + data[ATTR_ADDRESS], + data[ATTR_VALUE], + ) + mock_pb.write_register.side_effect = ModbusException("fail write_") + await hass.services.async_call( + DOMAIN, SERVICE_WRITE_REGISTER, data, blocking=True + ) + + data[ATTR_VALUE] = [1, 2, 3] + await hass.services.async_call( + DOMAIN, SERVICE_WRITE_REGISTER, data, blocking=True + ) + assert mock_pb.write_registers.called + assert mock_pb.write_registers.call_args[0] == ( + data[ATTR_ADDRESS], + data[ATTR_VALUE], + ) + mock_pb.write_registers.side_effect = ModbusException("fail write_") + await hass.services.async_call( + DOMAIN, SERVICE_WRITE_REGISTER, data, blocking=True + ) + + +async def test_pb_service_write_coil(hass, caplog): + """Run test for service write_coil.""" + + conf_name = "myModbus" + config = { + DOMAIN: [ + { + CONF_TYPE: "tcp", + CONF_HOST: "modbusTestHost", + CONF_PORT: 5501, + CONF_NAME: conf_name, + } + ] + } + + mock_pb = mock.MagicMock() + with mock.patch( + "homeassistant.components.modbus.modbus.ModbusTcpClient", return_value=mock_pb + ): + assert await async_setup_component(hass, DOMAIN, config) is True + await hass.async_block_till_done() + + data = {ATTR_HUB: conf_name, ATTR_UNIT: 17, ATTR_ADDRESS: 16, ATTR_STATE: False} + await hass.services.async_call(DOMAIN, SERVICE_WRITE_COIL, data, blocking=True) + assert mock_pb.write_coil.called + assert mock_pb.write_coil.call_args[0] == ( + data[ATTR_ADDRESS], + data[ATTR_STATE], + ) + mock_pb.write_coil.side_effect = ModbusException("fail write_") + await hass.services.async_call(DOMAIN, SERVICE_WRITE_COIL, data, blocking=True) + + data[ATTR_STATE] = [True, False, True] + await hass.services.async_call(DOMAIN, SERVICE_WRITE_COIL, data, blocking=True) + assert mock_pb.write_coils.called + assert mock_pb.write_coils.call_args[0] == ( + data[ATTR_ADDRESS], + data[ATTR_STATE], + ) + + caplog.set_level(logging.DEBUG) + caplog.clear + mock_pb.write_coils.side_effect = ModbusException("fail write_") + await hass.services.async_call(DOMAIN, SERVICE_WRITE_COIL, data, blocking=True) + assert caplog.records[-1].levelname == "ERROR" + await hass.services.async_call(DOMAIN, SERVICE_WRITE_COIL, data, blocking=True) + assert caplog.records[-1].levelname == "DEBUG" + + +async def test_pymodbus_constructor_fail(hass, caplog): + """Run test for failing pymodbus constructor.""" + config = { + DOMAIN: [ + { + CONF_TYPE: "tcp", + CONF_HOST: "modbusTestHost", + CONF_PORT: 5501, + } + ] + } + with mock.patch( + "homeassistant.components.modbus.modbus.ModbusTcpClient" + ) as mock_pb: + caplog.set_level(logging.ERROR) + mock_pb.side_effect = ModbusException("test no class") + assert await async_setup_component(hass, DOMAIN, config) is True + await hass.async_block_till_done() + assert len(caplog.records) == 1 + assert caplog.records[0].levelname == "ERROR" + assert mock_pb.called + + +async def test_pymodbus_connect_fail(hass, caplog): + """Run test for failing pymodbus constructor.""" + config = { + DOMAIN: [ + { + CONF_TYPE: "tcp", + CONF_HOST: "modbusTestHost", + CONF_PORT: 5501, + } + ] + } + mock_pb = mock.MagicMock() + with mock.patch( + "homeassistant.components.modbus.modbus.ModbusTcpClient", return_value=mock_pb + ): + caplog.set_level(logging.ERROR) + mock_pb.connect.side_effect = ModbusException("test connect fail") + mock_pb.close.side_effect = ModbusException("test connect fail") + assert await async_setup_component(hass, DOMAIN, config) is True + await hass.async_block_till_done() + assert len(caplog.records) == 1 + assert caplog.records[0].levelname == "ERROR" diff --git a/tests/components/modbus/test_modbus_binary_sensor.py b/tests/components/modbus/test_modbus_binary_sensor.py index 5c4e71cd66936..4ce423b2f16aa 100644 --- a/tests/components/modbus/test_modbus_binary_sensor.py +++ b/tests/components/modbus/test_modbus_binary_sensor.py @@ -16,6 +16,7 @@ CONF_SLAVE, STATE_OFF, STATE_ON, + STATE_UNAVAILABLE, ) from .conftest import base_config_test, base_test @@ -76,6 +77,10 @@ async def test_config_binary_sensor(hass, do_discovery, do_options): [0xFE], STATE_OFF, ), + ( + None, + STATE_UNAVAILABLE, + ), ], ) async def test_all_binary_sensor(hass, do_type, regs, expected): diff --git a/tests/components/modbus/test_modbus_sensor.py b/tests/components/modbus/test_modbus_sensor.py index b81cc9c4c1ef1..59bb81f8baa19 100644 --- a/tests/components/modbus/test_modbus_sensor.py +++ b/tests/components/modbus/test_modbus_sensor.py @@ -28,6 +28,7 @@ CONF_SENSORS, CONF_SLAVE, CONF_STRUCTURE, + STATE_UNAVAILABLE, ) from .conftest import base_config_test, base_test @@ -128,6 +129,50 @@ async def test_config_sensor(hass, do_discovery, do_config): ) +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_ADDRESS: 1234, + CONF_COUNT: 8, + CONF_PRECISION: 2, + CONF_DATA_TYPE: DATA_TYPE_INT, + }, + { + CONF_ADDRESS: 1234, + CONF_COUNT: 8, + CONF_PRECISION: 2, + CONF_DATA_TYPE: DATA_TYPE_CUSTOM, + CONF_STRUCTURE: ">no struct", + }, + { + CONF_ADDRESS: 1234, + CONF_COUNT: 2, + CONF_PRECISION: 2, + CONF_DATA_TYPE: DATA_TYPE_CUSTOM, + CONF_STRUCTURE: ">4f", + }, + ], +) +async def test_config_wrong_struct_sensor(hass, do_config): + """Run test for sensor with wrong struct.""" + + sensor_name = "test_sensor" + config_sensor = { + CONF_NAME: sensor_name, + **do_config, + } + await base_config_test( + hass, + config_sensor, + sensor_name, + SENSOR_DOMAIN, + CONF_SENSORS, + None, + method_discovery=True, + ) + + @pytest.mark.parametrize( "cfg,regs,expected", [ @@ -336,6 +381,30 @@ async def test_config_sensor(hass, do_discovery, do_config): [0x3037, 0x2D30, 0x352D, 0x3230, 0x3230, 0x2031, 0x343A, 0x3335], "07-05-2020 14:35", ), + ( + { + CONF_COUNT: 8, + CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING, + CONF_DATA_TYPE: DATA_TYPE_STRING, + CONF_SCALE: 1, + CONF_OFFSET: 0, + CONF_PRECISION: 0, + }, + None, + STATE_UNAVAILABLE, + ), + ( + { + CONF_COUNT: 2, + CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT, + CONF_DATA_TYPE: DATA_TYPE_UINT, + CONF_SCALE: 1, + CONF_OFFSET: 0, + CONF_PRECISION: 0, + }, + None, + STATE_UNAVAILABLE, + ), ], ) async def test_all_sensor(hass, cfg, regs, expected): @@ -357,39 +426,56 @@ async def test_all_sensor(hass, cfg, regs, expected): assert state == expected -async def test_struct_sensor(hass): +@pytest.mark.parametrize( + "cfg,regs,expected", + [ + ( + { + CONF_COUNT: 8, + CONF_PRECISION: 2, + CONF_DATA_TYPE: DATA_TYPE_CUSTOM, + CONF_STRUCTURE: ">4f", + }, + # floats: 7.931250095367432, 10.600000381469727, + # 1.000879611487865e-28, 10.566553115844727 + [0x40FD, 0xCCCD, 0x4129, 0x999A, 0x10FD, 0xC0CD, 0x4129, 0x109A], + "7.93,10.60,0.00,10.57", + ), + ( + { + CONF_COUNT: 4, + CONF_PRECISION: 0, + CONF_DATA_TYPE: DATA_TYPE_CUSTOM, + CONF_STRUCTURE: ">2i", + }, + [0x0000, 0x0100, 0x0000, 0x0032], + "256,50", + ), + ( + { + CONF_COUNT: 1, + CONF_PRECISION: 0, + CONF_DATA_TYPE: DATA_TYPE_INT, + }, + [0x0101], + "257", + ), + ], +) +async def test_struct_sensor(hass, cfg, regs, expected): """Run test for sensor struct.""" sensor_name = "modbus_test_sensor" - # floats: 7.931250095367432, 10.600000381469727, - # 1.000879611487865e-28, 10.566553115844727 - expected = "7.93,10.60,0.00,10.57" state = await base_test( hass, - { - CONF_NAME: sensor_name, - CONF_REGISTER: 1234, - CONF_COUNT: 8, - CONF_PRECISION: 2, - CONF_DATA_TYPE: DATA_TYPE_CUSTOM, - CONF_STRUCTURE: ">4f", - }, + {CONF_NAME: sensor_name, CONF_ADDRESS: 1234, **cfg}, sensor_name, SENSOR_DOMAIN, CONF_SENSORS, - CONF_REGISTERS, - [ - 0x40FD, - 0xCCCD, - 0x4129, - 0x999A, - 0x10FD, - 0xC0CD, - 0x4129, - 0x109A, - ], + None, + regs, expected, - method_discovery=False, + method_discovery=True, scan_interval=5, ) assert state == expected From e75233b27920c8694508cc69b14e08ce24e9e47a Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 22 Apr 2021 13:20:14 +0200 Subject: [PATCH 0444/1317] Bump `brother` library to version 1.0.0 (#49547) * Bump brother library * Improve attributes generation --- homeassistant/components/brother/__init__.py | 4 +- homeassistant/components/brother/const.py | 20 +++++++ .../components/brother/manifest.json | 2 +- homeassistant/components/brother/sensor.py | 59 ++++--------------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/brother/test_sensor.py | 6 +- 7 files changed, 43 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/brother/__init__.py b/homeassistant/components/brother/__init__.py index cd5a8b444b38d..f3c7678f3e33f 100644 --- a/homeassistant/components/brother/__init__.py +++ b/homeassistant/components/brother/__init__.py @@ -81,7 +81,7 @@ def __init__(self, hass, host, kind, snmp_engine): async def _async_update_data(self): """Update data via library.""" try: - await self.brother.async_update() + data = await self.brother.async_update() except (ConnectionError, SnmpError, UnsupportedModel) as error: raise UpdateFailed(error) from error - return self.brother.data + return data diff --git a/homeassistant/components/brother/const.py b/homeassistant/components/brother/const.py index 07843b0f3d0b1..2df14031f94c4 100644 --- a/homeassistant/components/brother/const.py +++ b/homeassistant/components/brother/const.py @@ -50,6 +50,26 @@ SNMP = "snmp" +ATTRS_MAP = { + ATTR_DRUM_REMAINING_LIFE: (ATTR_DRUM_REMAINING_PAGES, ATTR_DRUM_COUNTER), + ATTR_BLACK_DRUM_REMAINING_LIFE: ( + ATTR_BLACK_DRUM_REMAINING_PAGES, + ATTR_BLACK_DRUM_COUNTER, + ), + ATTR_CYAN_DRUM_REMAINING_LIFE: ( + ATTR_CYAN_DRUM_REMAINING_PAGES, + ATTR_CYAN_DRUM_COUNTER, + ), + ATTR_MAGENTA_DRUM_REMAINING_LIFE: ( + ATTR_MAGENTA_DRUM_REMAINING_PAGES, + ATTR_MAGENTA_DRUM_COUNTER, + ), + ATTR_YELLOW_DRUM_REMAINING_LIFE: ( + ATTR_YELLOW_DRUM_REMAINING_PAGES, + ATTR_YELLOW_DRUM_COUNTER, + ), +} + SENSOR_TYPES = { ATTR_STATUS: { ATTR_ICON: "mdi:printer", diff --git a/homeassistant/components/brother/manifest.json b/homeassistant/components/brother/manifest.json index dd33046a065d1..e2c1d4e9aff81 100644 --- a/homeassistant/components/brother/manifest.json +++ b/homeassistant/components/brother/manifest.json @@ -3,7 +3,7 @@ "name": "Brother Printer", "documentation": "https://www.home-assistant.io/integrations/brother", "codeowners": ["@bieniu"], - "requirements": ["brother==0.2.2"], + "requirements": ["brother==1.0.0"], "zeroconf": [ { "type": "_printer._tcp.local.", diff --git a/homeassistant/components/brother/sensor.py b/homeassistant/components/brother/sensor.py index 0b614ffa5825e..ca76932cd959e 100644 --- a/homeassistant/components/brother/sensor.py +++ b/homeassistant/components/brother/sensor.py @@ -4,37 +4,20 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( - ATTR_BLACK_DRUM_COUNTER, - ATTR_BLACK_DRUM_REMAINING_LIFE, - ATTR_BLACK_DRUM_REMAINING_PAGES, - ATTR_CYAN_DRUM_COUNTER, - ATTR_CYAN_DRUM_REMAINING_LIFE, - ATTR_CYAN_DRUM_REMAINING_PAGES, - ATTR_DRUM_COUNTER, - ATTR_DRUM_REMAINING_LIFE, - ATTR_DRUM_REMAINING_PAGES, ATTR_ENABLED, ATTR_ICON, ATTR_LABEL, - ATTR_MAGENTA_DRUM_COUNTER, - ATTR_MAGENTA_DRUM_REMAINING_LIFE, - ATTR_MAGENTA_DRUM_REMAINING_PAGES, ATTR_MANUFACTURER, ATTR_UNIT, ATTR_UPTIME, - ATTR_YELLOW_DRUM_COUNTER, - ATTR_YELLOW_DRUM_REMAINING_LIFE, - ATTR_YELLOW_DRUM_REMAINING_PAGES, + ATTRS_MAP, DATA_CONFIG_ENTRY, DOMAIN, SENSOR_TYPES, ) ATTR_COUNTER = "counter" -ATTR_FIRMWARE = "firmware" -ATTR_MODEL = "model" ATTR_REMAINING_PAGES = "remaining_pages" -ATTR_SERIAL = "serial" async def async_setup_entry(hass, config_entry, async_add_entities): @@ -44,11 +27,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): sensors = [] device_info = { - "identifiers": {(DOMAIN, coordinator.data[ATTR_SERIAL])}, - "name": coordinator.data[ATTR_MODEL], + "identifiers": {(DOMAIN, coordinator.data.serial)}, + "name": coordinator.data.model, "manufacturer": ATTR_MANUFACTURER, - "model": coordinator.data[ATTR_MODEL], - "sw_version": coordinator.data.get(ATTR_FIRMWARE), + "model": coordinator.data.model, + "sw_version": getattr(coordinator.data, "firmware", None), } for sensor in SENSOR_TYPES: @@ -63,8 +46,8 @@ class BrotherPrinterSensor(CoordinatorEntity, SensorEntity): def __init__(self, coordinator, kind, device_info): """Initialize.""" super().__init__(coordinator) - self._name = f"{coordinator.data[ATTR_MODEL]} {SENSOR_TYPES[kind][ATTR_LABEL]}" - self._unique_id = f"{coordinator.data[ATTR_SERIAL].lower()}_{kind}" + self._name = f"{coordinator.data.model} {SENSOR_TYPES[kind][ATTR_LABEL]}" + self._unique_id = f"{coordinator.data.serial.lower()}_{kind}" self._device_info = device_info self.kind = kind self._attrs = {} @@ -78,8 +61,8 @@ def name(self): def state(self): """Return the state.""" if self.kind == ATTR_UPTIME: - return self.coordinator.data.get(self.kind).isoformat() - return self.coordinator.data.get(self.kind) + return getattr(self.coordinator.data, self.kind).isoformat() + return getattr(self.coordinator.data, self.kind) @property def device_class(self): @@ -91,28 +74,12 @@ def device_class(self): @property def extra_state_attributes(self): """Return the state attributes.""" - remaining_pages = None - drum_counter = None - if self.kind == ATTR_DRUM_REMAINING_LIFE: - remaining_pages = ATTR_DRUM_REMAINING_PAGES - drum_counter = ATTR_DRUM_COUNTER - elif self.kind == ATTR_BLACK_DRUM_REMAINING_LIFE: - remaining_pages = ATTR_BLACK_DRUM_REMAINING_PAGES - drum_counter = ATTR_BLACK_DRUM_COUNTER - elif self.kind == ATTR_CYAN_DRUM_REMAINING_LIFE: - remaining_pages = ATTR_CYAN_DRUM_REMAINING_PAGES - drum_counter = ATTR_CYAN_DRUM_COUNTER - elif self.kind == ATTR_MAGENTA_DRUM_REMAINING_LIFE: - remaining_pages = ATTR_MAGENTA_DRUM_REMAINING_PAGES - drum_counter = ATTR_MAGENTA_DRUM_COUNTER - elif self.kind == ATTR_YELLOW_DRUM_REMAINING_LIFE: - remaining_pages = ATTR_YELLOW_DRUM_REMAINING_PAGES - drum_counter = ATTR_YELLOW_DRUM_COUNTER + remaining_pages, drum_counter = ATTRS_MAP.get(self.kind, (None, None)) if remaining_pages and drum_counter: - self._attrs[ATTR_REMAINING_PAGES] = self.coordinator.data.get( - remaining_pages + self._attrs[ATTR_REMAINING_PAGES] = getattr( + self.coordinator.data, remaining_pages ) - self._attrs[ATTR_COUNTER] = self.coordinator.data.get(drum_counter) + self._attrs[ATTR_COUNTER] = getattr(self.coordinator.data, drum_counter) return self._attrs @property diff --git a/requirements_all.txt b/requirements_all.txt index f7ae5b4a14107..c78bd27229206 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -390,7 +390,7 @@ bravia-tv==1.0.8 broadlink==0.17.0 # homeassistant.components.brother -brother==0.2.2 +brother==1.0.0 # homeassistant.components.brottsplatskartan brottsplatskartan==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2bdab44dc49c6..a7260aadb2537 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -223,7 +223,7 @@ bravia-tv==1.0.8 broadlink==0.17.0 # homeassistant.components.brother -brother==0.2.2 +brother==1.0.0 # homeassistant.components.bsblan bsblan==0.4.0 diff --git a/tests/components/brother/test_sensor.py b/tests/components/brother/test_sensor.py index ab48721dec545..49f7340a37ac9 100644 --- a/tests/components/brother/test_sensor.py +++ b/tests/components/brother/test_sensor.py @@ -289,8 +289,12 @@ async def test_manual_update_entity(hass): """Test manual update entity via service homeasasistant/update_entity.""" await init_integration(hass) + data = json.loads(load_fixture("brother_printer_data.json")) + await async_setup_component(hass, "homeassistant", {}) - with patch("homeassistant.components.brother.Brother.async_update") as mock_update: + with patch( + "homeassistant.components.brother.Brother.async_update", return_value=data + ) as mock_update: await hass.services.async_call( "homeassistant", "update_entity", From c4c8c67a03bae4910dc7ddd04dab8c1e2711260d Mon Sep 17 00:00:00 2001 From: D3v01dZA Date: Thu, 22 Apr 2021 09:46:48 -0400 Subject: [PATCH 0445/1317] Bump snapcast to 2.1.3 (#49553) --- homeassistant/components/snapcast/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/snapcast/manifest.json b/homeassistant/components/snapcast/manifest.json index 32162c062dd9a..2e3249f4551d7 100644 --- a/homeassistant/components/snapcast/manifest.json +++ b/homeassistant/components/snapcast/manifest.json @@ -2,7 +2,7 @@ "domain": "snapcast", "name": "Snapcast", "documentation": "https://www.home-assistant.io/integrations/snapcast", - "requirements": ["snapcast==2.1.2"], + "requirements": ["snapcast==2.1.3"], "codeowners": [], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index c78bd27229206..43f93c0b48f98 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2099,7 +2099,7 @@ smarthab==0.21 smhi-pkg==1.0.13 # homeassistant.components.snapcast -snapcast==2.1.2 +snapcast==2.1.3 # homeassistant.components.solaredge_local solaredge-local==0.2.0 From 2e084f260e64f8f508d7b0b7f896c8ac97505274 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 22 Apr 2021 16:21:38 +0200 Subject: [PATCH 0446/1317] =?UTF-8?q?Rename=20HomeAssistantType=20?= =?UTF-8?q?=E2=80=94>=20HomeAssistant,=20integrations=20s*=20-=20t*=20(#49?= =?UTF-8?q?550)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- homeassistant/components/solarlog/__init__.py | 4 +-- homeassistant/components/soma/__init__.py | 6 ++-- homeassistant/components/somfy/__init__.py | 7 ++--- homeassistant/components/sonarr/__init__.py | 8 ++--- .../components/sonarr/config_flow.py | 6 ++-- homeassistant/components/sonarr/sensor.py | 4 +-- homeassistant/components/songpal/__init__.py | 8 ++--- .../components/songpal/media_player.py | 6 ++-- homeassistant/components/stt/__init__.py | 7 ++--- .../components/switcher_kis/__init__.py | 6 ++-- .../components/switcher_kis/switch.py | 5 ++-- homeassistant/components/syncthru/__init__.py | 6 ++-- .../components/synology_dsm/__init__.py | 13 ++++---- .../components/synology_dsm/binary_sensor.py | 4 +-- .../components/synology_dsm/camera.py | 4 +-- .../components/synology_dsm/sensor.py | 4 +-- .../components/synology_dsm/switch.py | 4 +-- .../components/tasmota/device_trigger.py | 4 +-- homeassistant/components/tasmota/discovery.py | 6 ++-- homeassistant/components/timer/__init__.py | 6 ++-- .../components/timer/reproduce_state.py | 7 ++--- .../components/toon/binary_sensor.py | 4 +-- homeassistant/components/toon/climate.py | 4 +-- homeassistant/components/toon/switch.py | 4 +-- tests/components/sonarr/__init__.py | 4 +-- tests/components/sonarr/test_config_flow.py | 16 +++++----- tests/components/sonarr/test_sensor.py | 8 ++--- tests/components/switcher_kis/test_init.py | 15 +++++----- .../synology_dsm/test_config_flow.py | 30 ++++++++----------- tests/components/synology_dsm/test_init.py | 4 +-- 30 files changed, 103 insertions(+), 111 deletions(-) diff --git a/homeassistant/components/solarlog/__init__.py b/homeassistant/components/solarlog/__init__.py index 51aa21eb3151c..5db2e15f12160 100644 --- a/homeassistant/components/solarlog/__init__.py +++ b/homeassistant/components/solarlog/__init__.py @@ -1,9 +1,9 @@ """Solar-Log integration.""" from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up a config entry for solarlog.""" hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, "sensor") diff --git a/homeassistant/components/soma/__init__.py b/homeassistant/components/soma/__init__.py index 3f15199c162be..7c4d252208abf 100644 --- a/homeassistant/components/soma/__init__.py +++ b/homeassistant/components/soma/__init__.py @@ -7,9 +7,9 @@ from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import HomeAssistantType from .const import API, DOMAIN, HOST, PORT @@ -43,7 +43,7 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Soma from a config entry.""" hass.data[DOMAIN] = {} hass.data[DOMAIN][API] = SomaApi(entry.data[HOST], entry.data[PORT]) @@ -58,7 +58,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): return True -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" unload_ok = all( await asyncio.gather( diff --git a/homeassistant/components/somfy/__init__.py b/homeassistant/components/somfy/__init__.py index 9d67675f10e28..e7a8d71824723 100644 --- a/homeassistant/components/somfy/__init__.py +++ b/homeassistant/components/somfy/__init__.py @@ -10,14 +10,13 @@ from homeassistant.components.somfy import config_flow from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_OPTIMISTIC -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import ( config_entry_oauth2_flow, config_validation as cv, device_registry as dr, ) from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -73,7 +72,7 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Somfy from a config entry.""" # Backwards compat if "auth_implementation" not in entry.data: @@ -142,7 +141,7 @@ async def _update_all_devices(): return True -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" hass.data[DOMAIN].pop(API, None) await asyncio.gather( diff --git a/homeassistant/components/sonarr/__init__.py b/homeassistant/components/sonarr/__init__.py index ad5b0299f3eb5..12fe47f80c714 100644 --- a/homeassistant/components/sonarr/__init__.py +++ b/homeassistant/components/sonarr/__init__.py @@ -17,10 +17,10 @@ CONF_SSL, CONF_VERIFY_SSL, ) +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import HomeAssistantType from .const import ( ATTR_IDENTIFIERS, @@ -41,7 +41,7 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Sonarr from a config entry.""" if not entry.options: options = { @@ -89,7 +89,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool return True -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = all( await asyncio.gather( @@ -108,7 +108,7 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> boo return unload_ok -async def _async_update_listener(hass: HomeAssistantType, entry: ConfigEntry) -> None: +async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: """Handle options update.""" await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/sonarr/config_flow.py b/homeassistant/components/sonarr/config_flow.py index fe4cdd13454fd..acee381591c7a 100644 --- a/homeassistant/components/sonarr/config_flow.py +++ b/homeassistant/components/sonarr/config_flow.py @@ -15,9 +15,9 @@ CONF_SSL, CONF_VERIFY_SSL, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from .const import ( CONF_BASE_PATH, @@ -35,7 +35,7 @@ _LOGGER = logging.getLogger(__name__) -async def validate_input(hass: HomeAssistantType, data: dict) -> dict[str, Any]: +async def validate_input(hass: HomeAssistant, data: dict) -> dict[str, Any]: """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. diff --git a/homeassistant/components/sonarr/sensor.py b/homeassistant/components/sonarr/sensor.py index 3446130433e81..e7ec3e7844c2a 100644 --- a/homeassistant/components/sonarr/sensor.py +++ b/homeassistant/components/sonarr/sensor.py @@ -10,8 +10,8 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import DATA_GIGABYTES +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import HomeAssistantType import homeassistant.util.dt as dt_util from . import SonarrEntity @@ -21,7 +21,7 @@ async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable[[list[Entity], bool], None], ) -> None: diff --git a/homeassistant/components/songpal/__init__.py b/homeassistant/components/songpal/__init__.py index d6e31fb9a1cf3..b5d87e29c4539 100644 --- a/homeassistant/components/songpal/__init__.py +++ b/homeassistant/components/songpal/__init__.py @@ -5,8 +5,8 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.typing import HomeAssistantType from .const import CONF_ENDPOINT, DOMAIN @@ -20,7 +20,7 @@ ) -async def async_setup(hass: HomeAssistantType, config: OrderedDict) -> bool: +async def async_setup(hass: HomeAssistant, config: OrderedDict) -> bool: """Set up songpal environment.""" conf = config.get(DOMAIN) if conf is None: @@ -36,7 +36,7 @@ async def async_setup(hass: HomeAssistantType, config: OrderedDict) -> bool: return True -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up songpal media player.""" hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, "media_player") @@ -44,6 +44,6 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool return True -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload songpal media player.""" return await hass.config_entries.async_forward_entry_unload(entry, "media_player") diff --git a/homeassistant/components/songpal/media_player.py b/homeassistant/components/songpal/media_player.py index 2a0bde306b7fb..5cc1f9b542a6d 100644 --- a/homeassistant/components/songpal/media_player.py +++ b/homeassistant/components/songpal/media_player.py @@ -25,13 +25,13 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, EVENT_HOMEASSISTANT_STOP, STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import ( config_validation as cv, device_registry as dr, entity_platform, ) -from homeassistant.helpers.typing import HomeAssistantType from .const import CONF_ENDPOINT, DOMAIN, SET_SOUND_SETTING @@ -53,7 +53,7 @@ async def async_setup_platform( - hass: HomeAssistantType, config: dict, async_add_entities, discovery_info=None + hass: HomeAssistant, config: dict, async_add_entities, discovery_info=None ) -> None: """Set up from legacy configuration file. Obsolete.""" _LOGGER.error( @@ -62,7 +62,7 @@ async def async_setup_platform( async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up songpal media player.""" name = config_entry.data[CONF_NAME] diff --git a/homeassistant/components/stt/__init__.py b/homeassistant/components/stt/__init__.py index 5c45e5e3d4457..694ddeff9987b 100644 --- a/homeassistant/components/stt/__init__.py +++ b/homeassistant/components/stt/__init__.py @@ -15,9 +15,8 @@ import attr from homeassistant.components.http import HomeAssistantView -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_per_platform, discovery -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.setup import async_prepare_setup_platform from .const import ( @@ -35,7 +34,7 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistantType, config): +async def async_setup(hass: HomeAssistant, config): """Set up STT.""" providers = {} @@ -104,7 +103,7 @@ class SpeechResult: class Provider(ABC): """Represent a single STT provider.""" - hass: HomeAssistantType | None = None + hass: HomeAssistant | None = None name: str | None = None @property diff --git a/homeassistant/components/switcher_kis/__init__.py b/homeassistant/components/switcher_kis/__init__.py index 8d39182dcc32f..5483ad88c2d9f 100644 --- a/homeassistant/components/switcher_kis/__init__.py +++ b/homeassistant/components/switcher_kis/__init__.py @@ -10,12 +10,12 @@ from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.const import CONF_DEVICE_ID, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.typing import EventType, HomeAssistantType +from homeassistant.helpers.typing import EventType _LOGGER = logging.getLogger(__name__) @@ -46,7 +46,7 @@ ) -async def async_setup(hass: HomeAssistantType, config: dict) -> bool: +async def async_setup(hass: HomeAssistant, config: dict) -> bool: """Set up the switcher component.""" phone_id = config[DOMAIN][CONF_PHONE_ID] device_id = config[DOMAIN][CONF_DEVICE_ID] diff --git a/homeassistant/components/switcher_kis/switch.py b/homeassistant/components/switcher_kis/switch.py index 61297142716cb..5bad50a798590 100644 --- a/homeassistant/components/switcher_kis/switch.py +++ b/homeassistant/components/switcher_kis/switch.py @@ -16,9 +16,10 @@ import voluptuous as vol from homeassistant.components.switch import ATTR_CURRENT_POWER_W, SwitchEntity +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.typing import HomeAssistantType, ServiceCallType +from homeassistant.helpers.typing import ServiceCallType from . import ( ATTR_AUTO_OFF_SET, @@ -53,7 +54,7 @@ async def async_setup_platform( - hass: HomeAssistantType, + hass: HomeAssistant, config: dict, async_add_entities: Callable, discovery_info: dict, diff --git a/homeassistant/components/syncthru/__init__.py b/homeassistant/components/syncthru/__init__.py index 888dd22c0908e..b09f799df366c 100644 --- a/homeassistant/components/syncthru/__init__.py +++ b/homeassistant/components/syncthru/__init__.py @@ -8,16 +8,16 @@ from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_URL +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, device_registry as dr -from homeassistant.helpers.typing import HomeAssistantType from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up config entry.""" session = aiohttp_client.async_get_clientsession(hass) @@ -53,7 +53,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool return True -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload the config entry.""" await hass.config_entries.async_forward_entry_unload(entry, SENSOR_DOMAIN) hass.data[DOMAIN].pop(entry.entry_id, None) diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index 16b531b9ee3df..3c9461f6ca3ee 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -36,11 +36,10 @@ CONF_USERNAME, CONF_VERIFY_SSL, ) -from homeassistant.core import ServiceCall, callback +from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -119,7 +118,7 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Synology DSM sensors.""" # Migrate old unique_id @@ -294,7 +293,7 @@ async def async_coordinator_update_data_switches(): return True -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload Synology DSM sensors.""" unload_ok = all( await asyncio.gather( @@ -314,12 +313,12 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): return unload_ok -async def _async_update_listener(hass: HomeAssistantType, entry: ConfigEntry): +async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry): """Handle options update.""" await hass.config_entries.async_reload(entry.entry_id) -async def _async_setup_services(hass: HomeAssistantType): +async def _async_setup_services(hass: HomeAssistant): """Service handler setup.""" async def service_handler(call: ServiceCall): @@ -358,7 +357,7 @@ async def service_handler(call: ServiceCall): class SynoApi: """Class to interface with Synology DSM API.""" - def __init__(self, hass: HomeAssistantType, entry: ConfigEntry): + def __init__(self, hass: HomeAssistant, entry: ConfigEntry): """Initialize the API wrapper class.""" self._hass = hass self._entry = entry diff --git a/homeassistant/components/synology_dsm/binary_sensor.py b/homeassistant/components/synology_dsm/binary_sensor.py index fb8ed5a23cdb5..587f89cf16a64 100644 --- a/homeassistant/components/synology_dsm/binary_sensor.py +++ b/homeassistant/components/synology_dsm/binary_sensor.py @@ -4,7 +4,7 @@ from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DISKS -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from . import SynologyDSMBaseEntity, SynologyDSMDeviceEntity from .const import ( @@ -18,7 +18,7 @@ async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up the Synology NAS binary sensor.""" diff --git a/homeassistant/components/synology_dsm/camera.py b/homeassistant/components/synology_dsm/camera.py index 67052543569e3..cdd4b88186a93 100644 --- a/homeassistant/components/synology_dsm/camera.py +++ b/homeassistant/components/synology_dsm/camera.py @@ -11,7 +11,7 @@ from homeassistant.components.camera import SUPPORT_STREAM, Camera from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import SynoApi, SynologyDSMBaseEntity @@ -30,7 +30,7 @@ async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up the Synology NAS cameras.""" diff --git a/homeassistant/components/synology_dsm/sensor.py b/homeassistant/components/synology_dsm/sensor.py index 22f41601e7baa..d4a9b0bb7fc77 100644 --- a/homeassistant/components/synology_dsm/sensor.py +++ b/homeassistant/components/synology_dsm/sensor.py @@ -13,8 +13,8 @@ PRECISION_TENTHS, TEMP_CELSIUS, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers.temperature import display_temp -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.util.dt import utcnow @@ -34,7 +34,7 @@ async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up the Synology NAS Sensor.""" diff --git a/homeassistant/components/synology_dsm/switch.py b/homeassistant/components/synology_dsm/switch.py index f9883b0c9162f..3b71e481d6e46 100644 --- a/homeassistant/components/synology_dsm/switch.py +++ b/homeassistant/components/synology_dsm/switch.py @@ -7,7 +7,7 @@ from homeassistant.components.switch import ToggleEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import SynoApi, SynologyDSMBaseEntity @@ -17,7 +17,7 @@ async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up the Synology NAS switch.""" diff --git a/homeassistant/components/tasmota/device_trigger.py b/homeassistant/components/tasmota/device_trigger.py index ae4a528efc666..d4aca9b07caa5 100644 --- a/homeassistant/components/tasmota/device_trigger.py +++ b/homeassistant/components/tasmota/device_trigger.py @@ -17,7 +17,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from .const import DOMAIN, TASMOTA_EVENT from .discovery import TASMOTA_DISCOVERY_ENTITY_UPDATED, clear_discovery_hash @@ -82,7 +82,7 @@ class Trigger: device_id: str = attr.ib() discovery_hash: dict = attr.ib() - hass: HomeAssistantType = attr.ib() + hass: HomeAssistant = attr.ib() remove_update_signal: Callable[[], None] = attr.ib() subtype: str = attr.ib() tasmota_trigger: TasmotaTrigger = attr.ib() diff --git a/homeassistant/components/tasmota/discovery.py b/homeassistant/components/tasmota/discovery.py index 22824e9cd7160..600b2fd293e44 100644 --- a/homeassistant/components/tasmota/discovery.py +++ b/homeassistant/components/tasmota/discovery.py @@ -12,9 +12,9 @@ ) import homeassistant.components.sensor as sensor +from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity_registry import async_entries_for_device -from homeassistant.helpers.typing import HomeAssistantType from .const import DOMAIN, PLATFORMS @@ -40,7 +40,7 @@ def set_discovery_hash(hass, discovery_hash): async def async_start( - hass: HomeAssistantType, discovery_topic, config_entry, tasmota_mqtt, setup_device + hass: HomeAssistant, discovery_topic, config_entry, tasmota_mqtt, setup_device ) -> bool: """Start Tasmota device discovery.""" @@ -168,7 +168,7 @@ async def async_sensors_discovered(sensors, mac): hass.data[TASMOTA_DISCOVERY_INSTANCE] = tasmota_discovery -async def async_stop(hass: HomeAssistantType) -> bool: +async def async_stop(hass: HomeAssistant) -> bool: """Stop Tasmota device discovery.""" hass.data.pop(ALREADY_DISCOVERED) tasmota_discovery = hass.data.pop(TASMOTA_DISCOVERY_INSTANCE) diff --git a/homeassistant/components/timer/__init__.py b/homeassistant/components/timer/__init__.py index 2ff408dcd819e..9a2b053a8e3da 100644 --- a/homeassistant/components/timer/__init__.py +++ b/homeassistant/components/timer/__init__.py @@ -13,7 +13,7 @@ CONF_NAME, SERVICE_RELOAD, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import collection import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_component import EntityComponent @@ -21,7 +21,7 @@ from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.helpers.service from homeassistant.helpers.storage import Store -from homeassistant.helpers.typing import ConfigType, HomeAssistantType, ServiceCallType +from homeassistant.helpers.typing import ConfigType, ServiceCallType import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -100,7 +100,7 @@ def _none_to_empty_dict(value): RELOAD_SERVICE_SCHEMA = vol.Schema({}) -async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up an input select.""" component = EntityComponent(_LOGGER, DOMAIN, hass) id_manager = collection.IDManager() diff --git a/homeassistant/components/timer/reproduce_state.py b/homeassistant/components/timer/reproduce_state.py index 3ab7d4815cf30..33aed933a0637 100644 --- a/homeassistant/components/timer/reproduce_state.py +++ b/homeassistant/components/timer/reproduce_state.py @@ -7,8 +7,7 @@ from typing import Any from homeassistant.const import ATTR_ENTITY_ID -from homeassistant.core import Context, State -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import Context, HomeAssistant, State from . import ( ATTR_DURATION, @@ -27,7 +26,7 @@ async def _async_reproduce_state( - hass: HomeAssistantType, + hass: HomeAssistant, state: State, *, context: Context | None = None, @@ -69,7 +68,7 @@ async def _async_reproduce_state( async def async_reproduce_states( - hass: HomeAssistantType, + hass: HomeAssistant, states: Iterable[State], *, context: Context | None = None, diff --git a/homeassistant/components/toon/binary_sensor.py b/homeassistant/components/toon/binary_sensor.py index 6651806a21c7c..4a55911dcfc55 100644 --- a/homeassistant/components/toon/binary_sensor.py +++ b/homeassistant/components/toon/binary_sensor.py @@ -3,7 +3,7 @@ from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .const import ( ATTR_DEFAULT_ENABLED, @@ -26,7 +26,7 @@ async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up a Toon binary sensor based on a config entry.""" coordinator = hass.data[DOMAIN][entry.entry_id] diff --git a/homeassistant/components/toon/climate.py b/homeassistant/components/toon/climate.py index db2bed47f5161..1c7bde7d9e5ca 100644 --- a/homeassistant/components/toon/climate.py +++ b/homeassistant/components/toon/climate.py @@ -24,7 +24,7 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .const import DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, DOMAIN from .helpers import toon_exception_handler @@ -32,7 +32,7 @@ async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up a Toon binary sensors based on a config entry.""" coordinator = hass.data[DOMAIN][entry.entry_id] diff --git a/homeassistant/components/toon/switch.py b/homeassistant/components/toon/switch.py index d529dd07075fe..b830f53179ef7 100644 --- a/homeassistant/components/toon/switch.py +++ b/homeassistant/components/toon/switch.py @@ -10,7 +10,7 @@ from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .const import ( ATTR_DEFAULT_ENABLED, @@ -28,7 +28,7 @@ async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up a Toon switches based on a config entry.""" coordinator = hass.data[DOMAIN][entry.entry_id] diff --git a/tests/components/sonarr/__init__.py b/tests/components/sonarr/__init__.py index c1d4fc30736f6..e3ae6bfa83745 100644 --- a/tests/components/sonarr/__init__.py +++ b/tests/components/sonarr/__init__.py @@ -18,7 +18,7 @@ CONF_VERIFY_SSL, CONTENT_TYPE_JSON, ) -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry, load_fixture from tests.test_util.aiohttp import AiohttpClientMocker @@ -176,7 +176,7 @@ def mock_connection_server_error( async def setup_integration( - hass: HomeAssistantType, + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, host: str = HOST, port: str = PORT, diff --git a/tests/components/sonarr/test_config_flow.py b/tests/components/sonarr/test_config_flow.py index 71ec142024407..c1896061f79a2 100644 --- a/tests/components/sonarr/test_config_flow.py +++ b/tests/components/sonarr/test_config_flow.py @@ -10,12 +10,12 @@ ) from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_SOURCE, CONF_VERIFY_SSL +from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import ( RESULT_TYPE_ABORT, RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM, ) -from homeassistant.helpers.typing import HomeAssistantType from tests.components.sonarr import ( HOST, @@ -30,7 +30,7 @@ from tests.test_util.aiohttp import AiohttpClientMocker -async def test_show_user_form(hass: HomeAssistantType) -> None: +async def test_show_user_form(hass: HomeAssistant) -> None: """Test that the user set up form is served.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -42,7 +42,7 @@ async def test_show_user_form(hass: HomeAssistantType) -> None: async def test_cannot_connect( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test we show user form on connection error.""" mock_connection_error(aioclient_mock) @@ -60,7 +60,7 @@ async def test_cannot_connect( async def test_invalid_auth( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test we show user form on invalid auth.""" mock_connection_invalid_auth(aioclient_mock) @@ -78,7 +78,7 @@ async def test_invalid_auth( async def test_unknown_error( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test we show user form on unknown error.""" user_input = MOCK_USER_INPUT.copy() @@ -97,7 +97,7 @@ async def test_unknown_error( async def test_full_reauth_flow_implementation( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test the manual reauth flow from start to finish.""" entry = await setup_integration( @@ -137,7 +137,7 @@ async def test_full_reauth_flow_implementation( async def test_full_user_flow_implementation( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test the full manual user flow from start to finish.""" mock_connection(aioclient_mock) @@ -166,7 +166,7 @@ async def test_full_user_flow_implementation( async def test_full_user_flow_advanced_options( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test the full manual user flow with advanced options.""" mock_connection(aioclient_mock) diff --git a/tests/components/sonarr/test_sensor.py b/tests/components/sonarr/test_sensor.py index 3a11688a56f9d..3f99325c3ef7f 100644 --- a/tests/components/sonarr/test_sensor.py +++ b/tests/components/sonarr/test_sensor.py @@ -12,8 +12,8 @@ DATA_GIGABYTES, STATE_UNAVAILABLE, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import dt as dt_util from tests.common import async_fire_time_changed @@ -24,7 +24,7 @@ async def test_sensors( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test the creation and values of the sensors.""" entry = await setup_integration(hass, aioclient_mock, skip_entry_setup=True) @@ -104,7 +104,7 @@ async def test_sensors( ), ) async def test_disabled_by_default_sensors( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker, entity_id: str + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, entity_id: str ) -> None: """Test the disabled by default sensors.""" await setup_integration(hass, aioclient_mock) @@ -121,7 +121,7 @@ async def test_disabled_by_default_sensors( async def test_availability( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test entity availability.""" now = dt_util.utcnow() diff --git a/tests/components/switcher_kis/test_init.py b/tests/components/switcher_kis/test_init.py index 394f48d001a45..14eb2a1a16e14 100644 --- a/tests/components/switcher_kis/test_init.py +++ b/tests/components/switcher_kis/test_init.py @@ -19,11 +19,10 @@ SERVICE_TURN_ON_WITH_TIMER_NAME, ) from homeassistant.const import CONF_ENTITY_ID -from homeassistant.core import Context, callback +from homeassistant.core import Context, HomeAssistant, callback from homeassistant.exceptions import UnknownUser from homeassistant.helpers.config_validation import time_period_str from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.setup import async_setup_component from homeassistant.util import dt @@ -47,21 +46,21 @@ async def test_failed_config( - hass: HomeAssistantType, mock_failed_bridge: Generator[None, Any, None] + hass: HomeAssistant, mock_failed_bridge: Generator[None, Any, None] ) -> None: """Test failed configuration.""" assert await async_setup_component(hass, DOMAIN, MANDATORY_CONFIGURATION) is False async def test_minimal_config( - hass: HomeAssistantType, mock_bridge: Generator[None, Any, None] + hass: HomeAssistant, mock_bridge: Generator[None, Any, None] ) -> None: """Test setup with configuration minimal entries.""" assert await async_setup_component(hass, DOMAIN, MANDATORY_CONFIGURATION) async def test_discovery_data_bucket( - hass: HomeAssistantType, mock_bridge: Generator[None, Any, None] + hass: HomeAssistant, mock_bridge: Generator[None, Any, None] ) -> None: """Test the event send with the updated device.""" assert await async_setup_component(hass, DOMAIN, MANDATORY_CONFIGURATION) @@ -82,7 +81,7 @@ async def test_discovery_data_bucket( async def test_set_auto_off_service( - hass: HomeAssistantType, + hass: HomeAssistant, mock_bridge: Generator[None, Any, None], mock_api: Generator[None, Any, None], hass_owner_user: MockUser, @@ -130,7 +129,7 @@ async def test_set_auto_off_service( async def test_turn_on_with_timer_service( - hass: HomeAssistantType, + hass: HomeAssistant, mock_bridge: Generator[None, Any, None], mock_api: Generator[None, Any, None], hass_owner_user: MockUser, @@ -184,7 +183,7 @@ async def test_turn_on_with_timer_service( async def test_signal_dispatcher( - hass: HomeAssistantType, mock_bridge: Generator[None, Any, None] + hass: HomeAssistant, mock_bridge: Generator[None, Any, None] ) -> None: """Test signal dispatcher dispatching device updates every 4 seconds.""" assert await async_setup_component(hass, DOMAIN, MANDATORY_CONFIGURATION) diff --git a/tests/components/synology_dsm/test_config_flow.py b/tests/components/synology_dsm/test_config_flow.py index 85ed02a7a524e..9c89ec64666a4 100644 --- a/tests/components/synology_dsm/test_config_flow.py +++ b/tests/components/synology_dsm/test_config_flow.py @@ -36,7 +36,7 @@ CONF_USERNAME, CONF_VERIFY_SSL, ) -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .consts import ( DEVICE_TOKEN, @@ -114,7 +114,7 @@ def mock_controller_service_failed(): yield service_mock -async def test_user(hass: HomeAssistantType, service: MagicMock): +async def test_user(hass: HomeAssistant, service: MagicMock): """Test user config.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=None @@ -177,7 +177,7 @@ async def test_user(hass: HomeAssistantType, service: MagicMock): assert result["data"].get(CONF_VOLUMES) is None -async def test_user_2sa(hass: HomeAssistantType, service_2sa: MagicMock): +async def test_user_2sa(hass: HomeAssistant, service_2sa: MagicMock): """Test user with 2sa authentication config.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -220,7 +220,7 @@ async def test_user_2sa(hass: HomeAssistantType, service_2sa: MagicMock): assert result["data"].get(CONF_VOLUMES) is None -async def test_user_vdsm(hass: HomeAssistantType, service_vdsm: MagicMock): +async def test_user_vdsm(hass: HomeAssistant, service_vdsm: MagicMock): """Test user config.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=None @@ -256,7 +256,7 @@ async def test_user_vdsm(hass: HomeAssistantType, service_vdsm: MagicMock): assert result["data"].get(CONF_VOLUMES) is None -async def test_import(hass: HomeAssistantType, service: MagicMock): +async def test_import(hass: HomeAssistant, service: MagicMock): """Test import step.""" # import with minimum setup result = await hass.config_entries.flow.async_init( @@ -309,7 +309,7 @@ async def test_import(hass: HomeAssistantType, service: MagicMock): assert result["data"][CONF_VOLUMES] == ["volume_1"] -async def test_abort_if_already_setup(hass: HomeAssistantType, service: MagicMock): +async def test_abort_if_already_setup(hass: HomeAssistant, service: MagicMock): """Test we abort if the account is already setup.""" MockConfigEntry( domain=DOMAIN, @@ -336,7 +336,7 @@ async def test_abort_if_already_setup(hass: HomeAssistantType, service: MagicMoc assert result["reason"] == "already_configured" -async def test_login_failed(hass: HomeAssistantType, service: MagicMock): +async def test_login_failed(hass: HomeAssistant, service: MagicMock): """Test when we have errors during login.""" service.return_value.login = Mock( side_effect=(SynologyDSMLoginInvalidException(USERNAME)) @@ -351,7 +351,7 @@ async def test_login_failed(hass: HomeAssistantType, service: MagicMock): assert result["errors"] == {CONF_USERNAME: "invalid_auth"} -async def test_connection_failed(hass: HomeAssistantType, service: MagicMock): +async def test_connection_failed(hass: HomeAssistant, service: MagicMock): """Test when we have errors during connection.""" service.return_value.login = Mock( side_effect=SynologyDSMRequestException(IOError("arg")) @@ -367,7 +367,7 @@ async def test_connection_failed(hass: HomeAssistantType, service: MagicMock): assert result["errors"] == {CONF_HOST: "cannot_connect"} -async def test_unknown_failed(hass: HomeAssistantType, service: MagicMock): +async def test_unknown_failed(hass: HomeAssistant, service: MagicMock): """Test when we have an unknown error.""" service.return_value.login = Mock(side_effect=SynologyDSMException(None, None)) @@ -381,9 +381,7 @@ async def test_unknown_failed(hass: HomeAssistantType, service: MagicMock): assert result["errors"] == {"base": "unknown"} -async def test_missing_data_after_login( - hass: HomeAssistantType, service_failed: MagicMock -): +async def test_missing_data_after_login(hass: HomeAssistant, service_failed: MagicMock): """Test when we have errors during connection.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -394,9 +392,7 @@ async def test_missing_data_after_login( assert result["errors"] == {"base": "missing_data"} -async def test_form_ssdp_already_configured( - hass: HomeAssistantType, service: MagicMock -): +async def test_form_ssdp_already_configured(hass: HomeAssistant, service: MagicMock): """Test ssdp abort when the serial number is already configured.""" await setup.async_setup_component(hass, "persistent_notification", {}) @@ -423,7 +419,7 @@ async def test_form_ssdp_already_configured( assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT -async def test_form_ssdp(hass: HomeAssistantType, service: MagicMock): +async def test_form_ssdp(hass: HomeAssistant, service: MagicMock): """Test we can setup from ssdp.""" await setup.async_setup_component(hass, "persistent_notification", {}) @@ -459,7 +455,7 @@ async def test_form_ssdp(hass: HomeAssistantType, service: MagicMock): assert result["data"].get(CONF_VOLUMES) is None -async def test_options_flow(hass: HomeAssistantType, service: MagicMock): +async def test_options_flow(hass: HomeAssistant, service: MagicMock): """Test config flow options.""" config_entry = MockConfigEntry( domain=DOMAIN, diff --git a/tests/components/synology_dsm/test_init.py b/tests/components/synology_dsm/test_init.py index 59864c5652328..891296d97ea6e 100644 --- a/tests/components/synology_dsm/test_init.py +++ b/tests/components/synology_dsm/test_init.py @@ -12,7 +12,7 @@ CONF_SSL, CONF_USERNAME, ) -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .consts import HOST, MACS, PASSWORD, PORT, USE_SSL, USERNAME @@ -20,7 +20,7 @@ @pytest.mark.no_bypass_setup -async def test_services_registered(hass: HomeAssistantType): +async def test_services_registered(hass: HomeAssistant): """Test if all services are registered.""" with patch( "homeassistant.components.synology_dsm.SynoApi.async_setup", return_value=True From 6992e24263ba77dafcb0168943c4d6e49360363e Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 22 Apr 2021 16:53:57 +0200 Subject: [PATCH 0447/1317] =?UTF-8?q?Rename=20HomeAssistantType=20?= =?UTF-8?q?=E2=80=94>=20HomeAssistant,=20integrations=20t*=20-=20v*=20(#49?= =?UTF-8?q?544)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Integration vizio: HomeAssistantType -> HomeAssistant. * Integration velbus: HomeAssistantType -> HomeAssistant. * Integration vacuum: HomeAssistantType -> HomeAssistant. * Integration upnp: HomeAssistantType -> HomeAssistant. * Integration upcloud: HomeAssistantType -> HomeAssistant. * Integration twinkly: HomeAssistantType -> HomeAssistant. * Integration tts: HomeAssistantType -> HomeAssistant. * Integration tradfri: HomeAssistantType -> HomeAssistant. * Integration traccar: HomeAssistantType -> HomeAssistant. * Integration tplink: HomeAssistantType -> HomeAssistant. --- homeassistant/components/tplink/__init__.py | 5 +- homeassistant/components/tplink/common.py | 4 +- homeassistant/components/tplink/light.py | 4 +- homeassistant/components/tplink/switch.py | 4 +- .../components/traccar/device_tracker.py | 5 +- homeassistant/components/tradfri/__init__.py | 9 +-- homeassistant/components/tts/__init__.py | 5 +- homeassistant/components/twinkly/__init__.py | 8 +-- homeassistant/components/twinkly/light.py | 6 +- homeassistant/components/upcloud/__init__.py | 11 ++- homeassistant/components/upnp/__init__.py | 13 ++-- homeassistant/components/upnp/device.py | 10 +-- homeassistant/components/upnp/sensor.py | 6 +- homeassistant/components/vacuum/group.py | 5 +- .../components/vacuum/reproduce_state.py | 7 +- homeassistant/components/velbus/__init__.py | 6 +- homeassistant/components/vizio/__init__.py | 13 ++-- .../components/vizio/media_player.py | 7 +- tests/components/upnp/test_config_flow.py | 18 ++--- tests/components/upnp/test_init.py | 6 +- tests/components/vizio/test_config_flow.py | 70 +++++++++---------- tests/components/vizio/test_init.py | 8 +-- tests/components/vizio/test_media_player.py | 60 ++++++++-------- 23 files changed, 141 insertions(+), 149 deletions(-) diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index 764060135a213..17b58569c7eb4 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -5,8 +5,9 @@ from homeassistant import config_entries from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from .common import ( ATTR_CONFIG, @@ -68,7 +69,7 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigType): +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigType): """Set up TPLink from a config entry.""" config_data = hass.data[DOMAIN].get(ATTR_CONFIG) diff --git a/homeassistant/components/tplink/common.py b/homeassistant/components/tplink/common.py index b9318cf3fdd21..4129a80f83ccd 100644 --- a/homeassistant/components/tplink/common.py +++ b/homeassistant/components/tplink/common.py @@ -12,7 +12,7 @@ SmartStrip, ) -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .const import DOMAIN as TPLINK_DOMAIN @@ -67,7 +67,7 @@ def discover(): async def async_discover_devices( - hass: HomeAssistantType, existing_devices: SmartDevices + hass: HomeAssistant, existing_devices: SmartDevices ) -> SmartDevices: """Get devices through discovery.""" _LOGGER.debug("Discovering devices") diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index 8880373955f05..0d9db7ba108ce 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -19,9 +19,9 @@ SUPPORT_COLOR_TEMP, LightEntity, ) +from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, PlatformNotReady import homeassistant.helpers.device_registry as dr -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util.color import ( color_temperature_kelvin_to_mired as kelvin_to_mired, color_temperature_mired_to_kelvin as mired_to_kelvin, @@ -77,7 +77,7 @@ FALLBACK_MAX_COLOR = 5000 -async def async_setup_entry(hass: HomeAssistantType, config_entry, async_add_entities): +async def async_setup_entry(hass: HomeAssistant, config_entry, async_add_entities): """Set up lights.""" entities = await hass.async_add_executor_job( add_available_devices, hass, CONF_LIGHT, TPLinkSmartBulb diff --git a/homeassistant/components/tplink/switch.py b/homeassistant/components/tplink/switch.py index 11b86d6254f0c..ab8b3290b306c 100644 --- a/homeassistant/components/tplink/switch.py +++ b/homeassistant/components/tplink/switch.py @@ -12,9 +12,9 @@ SwitchEntity, ) from homeassistant.const import ATTR_VOLTAGE +from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.device_registry as dr -from homeassistant.helpers.typing import HomeAssistantType from . import CONF_SWITCH, DOMAIN as TPLINK_DOMAIN from .common import add_available_devices @@ -30,7 +30,7 @@ SLEEP_TIME = 2 -async def async_setup_entry(hass: HomeAssistantType, config_entry, async_add_entities): +async def async_setup_entry(hass: HomeAssistant, config_entry, async_add_entities): """Set up switches.""" entities = await hass.async_add_executor_job( add_available_devices, hass, CONF_SWITCH, SmartPlugSwitch diff --git a/homeassistant/components/traccar/device_tracker.py b/homeassistant/components/traccar/device_tracker.py index d558129e323be..aebbb8b3b6c39 100644 --- a/homeassistant/components/traccar/device_tracker.py +++ b/homeassistant/components/traccar/device_tracker.py @@ -19,14 +19,13 @@ CONF_USERNAME, CONF_VERIFY_SSL, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import slugify from . import DOMAIN, TRACKER_UPDATE @@ -114,7 +113,7 @@ ) -async def async_setup_entry(hass: HomeAssistantType, entry, async_add_entities): +async def async_setup_entry(hass: HomeAssistant, entry, async_add_entities): """Configure a dispatcher connection based on a config entry.""" @callback diff --git a/homeassistant/components/tradfri/__init__.py b/homeassistant/components/tradfri/__init__.py index 3323c54d9c276..13d6d5713008a 100644 --- a/homeassistant/components/tradfri/__init__.py +++ b/homeassistant/components/tradfri/__init__.py @@ -10,10 +10,11 @@ from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from homeassistant.util.json import load_json from .const import ( @@ -55,7 +56,7 @@ ) -async def async_setup(hass: HomeAssistantType, config: ConfigType): +async def async_setup(hass: HomeAssistant, config: ConfigType): """Set up the Tradfri component.""" conf = config.get(DOMAIN) @@ -100,7 +101,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): return True -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Create a gateway.""" # host, identity, key, allow_tradfri_groups tradfri_data = hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {} @@ -169,7 +170,7 @@ async def async_keep_alive(now): return True -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" unload_ok = all( await asyncio.gather( diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index 5922392f17d97..f2d72dbe4adb1 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -33,13 +33,12 @@ HTTP_NOT_FOUND, PLATFORM_FORMAT, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_per_platform, discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.network import get_url from homeassistant.helpers.service import async_set_service_schema -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.loader import async_get_integration from homeassistant.setup import async_prepare_setup_platform from homeassistant.util.yaml import load_yaml @@ -519,7 +518,7 @@ def write_tags(filename, data, provider, message, language, options): class Provider: """Represent a single TTS provider.""" - hass: HomeAssistantType | None = None + hass: HomeAssistant | None = None name: str | None = None @property diff --git a/homeassistant/components/twinkly/__init__.py b/homeassistant/components/twinkly/__init__.py index 2b60510460983..876d02bd698c8 100644 --- a/homeassistant/components/twinkly/__init__.py +++ b/homeassistant/components/twinkly/__init__.py @@ -3,19 +3,19 @@ import twinkly_client from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.typing import HomeAssistantType from .const import CONF_ENTRY_HOST, CONF_ENTRY_ID, DOMAIN -async def async_setup(hass: HomeAssistantType, config: dict): +async def async_setup(hass: HomeAssistant, config: dict): """Set up the twinkly integration.""" return True -async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): """Set up entries from config flow.""" # We setup the client here so if at some point we add any other entity for this device, @@ -33,7 +33,7 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry): return True -async def async_unload_entry(hass: HomeAssistantType, config_entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry): """Remove a twinkly entry.""" # For now light entries don't have unload method, so we don't have to async_forward_entry_unload diff --git a/homeassistant/components/twinkly/light.py b/homeassistant/components/twinkly/light.py index 4353aa2707b8f..1918839b4b213 100644 --- a/homeassistant/components/twinkly/light.py +++ b/homeassistant/components/twinkly/light.py @@ -13,7 +13,7 @@ LightEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .const import ( ATTR_HOST, @@ -31,7 +31,7 @@ async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities ) -> None: """Setups an entity from a config entry (UI config flow).""" @@ -46,7 +46,7 @@ class TwinklyLight(LightEntity): def __init__( self, conf: ConfigEntry, - hass: HomeAssistantType, + hass: HomeAssistant, ): """Initialize a TwinklyLight entity.""" self._id = conf.data[CONF_ENTRY_ID] diff --git a/homeassistant/components/upcloud/__init__.py b/homeassistant/components/upcloud/__init__.py index f2484135be344..4f13aaa546062 100644 --- a/homeassistant/components/upcloud/__init__.py +++ b/homeassistant/components/upcloud/__init__.py @@ -21,14 +21,13 @@ STATE_ON, STATE_PROBLEM, ) -from homeassistant.core import CALLBACK_TYPE +from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -81,7 +80,7 @@ class UpCloudDataUpdateCoordinator( def __init__( self, - hass: HomeAssistantType, + hass: HomeAssistant, *, cloud_manager: upcloud_api.CloudManager, update_interval: timedelta, @@ -119,7 +118,7 @@ class UpCloudHassData: scan_interval_migrations: dict[str, int] = dataclasses.field(default_factory=dict) -async def async_setup(hass: HomeAssistantType, config) -> bool: +async def async_setup(hass: HomeAssistant, config) -> bool: """Set up UpCloud component.""" domain_config = config.get(DOMAIN) if not domain_config: @@ -155,7 +154,7 @@ def _config_entry_update_signal_name(config_entry: ConfigEntry) -> str: async def _async_signal_options_update( - hass: HomeAssistantType, config_entry: ConfigEntry + hass: HomeAssistant, config_entry: ConfigEntry ) -> None: """Signal config entry options update.""" async_dispatcher_send( @@ -163,7 +162,7 @@ async def _async_signal_options_update( ) -async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up the UpCloud config entry.""" manager = upcloud_api.CloudManager( diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index 439c3a8760b5e..3b4672a8fe50f 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -6,9 +6,10 @@ from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv, device_registry as dr -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from homeassistant.util import get_local_ip from .const import ( @@ -42,7 +43,7 @@ ) -async def async_construct_device(hass: HomeAssistantType, udn: str, st: str) -> Device: +async def async_construct_device(hass: HomeAssistant, udn: str, st: str) -> Device: """Discovery devices and construct a Device for one.""" # pylint: disable=invalid-name _LOGGER.debug("Constructing device: %s::%s", udn, st) @@ -66,7 +67,7 @@ async def async_construct_device(hass: HomeAssistantType, udn: str, st: str) -> return await Device.async_create_device(hass, location) -async def async_setup(hass: HomeAssistantType, config: ConfigType): +async def async_setup(hass: HomeAssistant, config: ConfigType): """Set up UPnP component.""" _LOGGER.debug("async_setup, config: %s", config) conf_default = CONFIG_SCHEMA({DOMAIN: {}})[DOMAIN] @@ -89,7 +90,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): return True -async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up UPnP/IGD device from a config entry.""" _LOGGER.debug("Setting up config entry: %s", config_entry.unique_id) @@ -153,9 +154,7 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) return True -async def async_unload_entry( - hass: HomeAssistantType, config_entry: ConfigEntry -) -> bool: +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload a UPnP/IGD device from a config entry.""" _LOGGER.debug("Unloading config entry: %s", config_entry.unique_id) diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index c116e64ca7f35..e5b6099e9f385 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -11,8 +11,8 @@ from async_upnp_client.device_updater import DeviceUpdater from async_upnp_client.profiles.igd import IgdDevice +from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator import homeassistant.util.dt as dt_util @@ -36,7 +36,7 @@ ) -def _get_local_ip(hass: HomeAssistantType) -> IPv4Address | None: +def _get_local_ip(hass: HomeAssistant) -> IPv4Address | None: """Get the configured local ip.""" if DOMAIN in hass.data and DOMAIN_CONFIG in hass.data[DOMAIN]: local_ip = hass.data[DOMAIN][DOMAIN_CONFIG].get(CONF_LOCAL_IP) @@ -55,7 +55,7 @@ def __init__(self, igd_device: IgdDevice, device_updater: DeviceUpdater) -> None self.coordinator: DataUpdateCoordinator = None @classmethod - async def async_discover(cls, hass: HomeAssistantType) -> list[Mapping]: + async def async_discover(cls, hass: HomeAssistant) -> list[Mapping]: """Discover UPnP/IGD devices.""" _LOGGER.debug("Discovering UPnP/IGD devices") local_ip = _get_local_ip(hass) @@ -73,7 +73,7 @@ async def async_discover(cls, hass: HomeAssistantType) -> list[Mapping]: @classmethod async def async_supplement_discovery( - cls, hass: HomeAssistantType, discovery: Mapping + cls, hass: HomeAssistant, discovery: Mapping ) -> Mapping: """Get additional data from device and supplement discovery.""" location = discovery[DISCOVERY_LOCATION] @@ -86,7 +86,7 @@ async def async_supplement_discovery( @classmethod async def async_create_device( - cls, hass: HomeAssistantType, ssdp_location: str + cls, hass: HomeAssistant, ssdp_location: str ) -> Device: """Create UPnP/IGD device.""" # Build async_upnp_client requester. diff --git a/homeassistant/components/upnp/sensor.py b/homeassistant/components/upnp/sensor.py index d777b8104cdfd..3ffcb8d74265a 100644 --- a/homeassistant/components/upnp/sensor.py +++ b/homeassistant/components/upnp/sensor.py @@ -7,8 +7,8 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import DATA_BYTES, DATA_RATE_KIBIBYTES_PER_SECOND +from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -73,7 +73,7 @@ async def async_setup_platform( - hass: HomeAssistantType, config, async_add_entities, discovery_info=None + hass: HomeAssistant, config, async_add_entities, discovery_info=None ) -> None: """Old way of setting up UPnP/IGD sensors.""" _LOGGER.debug( @@ -82,7 +82,7 @@ async def async_setup_platform( async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable ) -> None: """Set up the UPnP/IGD sensors.""" udn = config_entry.data[CONFIG_ENTRY_UDN] diff --git a/homeassistant/components/vacuum/group.py b/homeassistant/components/vacuum/group.py index 0219ecdf79579..e5a1734420f89 100644 --- a/homeassistant/components/vacuum/group.py +++ b/homeassistant/components/vacuum/group.py @@ -3,15 +3,14 @@ from homeassistant.components.group import GroupIntegrationRegistry from homeassistant.const import STATE_OFF, STATE_ON -from homeassistant.core import callback -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant, callback from . import STATE_CLEANING, STATE_ERROR, STATE_RETURNING @callback def async_describe_on_off_states( - hass: HomeAssistantType, registry: GroupIntegrationRegistry + hass: HomeAssistant, registry: GroupIntegrationRegistry ) -> None: """Describe group on off states.""" registry.on_off_states( diff --git a/homeassistant/components/vacuum/reproduce_state.py b/homeassistant/components/vacuum/reproduce_state.py index 4d5a9baf46e55..f8d718c99795e 100644 --- a/homeassistant/components/vacuum/reproduce_state.py +++ b/homeassistant/components/vacuum/reproduce_state.py @@ -15,8 +15,7 @@ STATE_ON, STATE_PAUSED, ) -from homeassistant.core import Context, State -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import Context, HomeAssistant, State from . import ( ATTR_FAN_SPEED, @@ -44,7 +43,7 @@ async def _async_reproduce_state( - hass: HomeAssistantType, + hass: HomeAssistant, state: State, *, context: Context | None = None, @@ -99,7 +98,7 @@ async def _async_reproduce_state( async def async_reproduce_states( - hass: HomeAssistantType, + hass: HomeAssistant, states: Iterable[State], *, context: Context | None = None, diff --git a/homeassistant/components/velbus/__init__.py b/homeassistant/components/velbus/__init__.py index a15b0a641ef1e..6d5e741a3ce34 100644 --- a/homeassistant/components/velbus/__init__.py +++ b/homeassistant/components/velbus/__init__.py @@ -7,10 +7,10 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_PORT +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import HomeAssistantType from .const import CONF_MEMO_TEXT, DOMAIN, SERVICE_SET_MEMO_TEXT @@ -44,7 +44,7 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Establish connection with velbus.""" hass.data.setdefault(DOMAIN, {}) @@ -109,7 +109,7 @@ def set_memo_text(service): return True -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Remove the velbus connection.""" await asyncio.wait( [ diff --git a/homeassistant/components/vizio/__init__.py b/homeassistant/components/vizio/__init__.py index 3719ada27aef5..b8afba7d69e7d 100644 --- a/homeassistant/components/vizio/__init__.py +++ b/homeassistant/components/vizio/__init__.py @@ -12,9 +12,10 @@ from homeassistant.components.media_player import DEVICE_CLASS_TV from homeassistant.config_entries import ENTRY_STATE_LOADED, SOURCE_IMPORT, ConfigEntry +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import CONF_APPS, CONF_DEVICE_CLASS, DOMAIN, VIZIO_SCHEMA @@ -43,7 +44,7 @@ def validate_apps(config: ConfigType) -> ConfigType: PLATFORMS = ["media_player"] -async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Component setup, run import config flow for each entry in config.""" if DOMAIN in config: for entry in config[DOMAIN]: @@ -56,7 +57,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: return True -async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Load the saved entities.""" hass.data.setdefault(DOMAIN, {}) @@ -76,9 +77,7 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) return True -async def async_unload_entry( - hass: HomeAssistantType, config_entry: ConfigEntry -) -> bool: +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = all( await asyncio.gather( @@ -107,7 +106,7 @@ async def async_unload_entry( class VizioAppsDataUpdateCoordinator(DataUpdateCoordinator): """Define an object to hold Vizio app config data.""" - def __init__(self, hass: HomeAssistantType) -> None: + def __init__(self, hass: HomeAssistant) -> None: """Initialize.""" super().__init__( hass, diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py index fc955d48158b9..57d770b26aea3 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -26,7 +26,7 @@ STATE_OFF, STATE_ON, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_platform from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import ( @@ -34,7 +34,6 @@ async_dispatcher_send, ) from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import ( @@ -64,7 +63,7 @@ async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable[[list[Entity], bool], None], ) -> None: @@ -284,7 +283,7 @@ def _get_additional_app_names(self) -> list[dict[str, Any]]: @staticmethod async def _async_send_update_options_signal( - hass: HomeAssistantType, config_entry: ConfigEntry + hass: HomeAssistant, config_entry: ConfigEntry ) -> None: """Send update event when Vizio config entry is updated.""" # Move this method to component level if another entity ever gets added for a single config entry. diff --git a/tests/components/upnp/test_config_flow.py b/tests/components/upnp/test_config_flow.py index facc5f057012f..93f21911c78b5 100644 --- a/tests/components/upnp/test_config_flow.py +++ b/tests/components/upnp/test_config_flow.py @@ -21,7 +21,7 @@ DOMAIN, ) from homeassistant.components.upnp.device import Device -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from homeassistant.util import dt @@ -30,7 +30,7 @@ from tests.common import MockConfigEntry, async_fire_time_changed -async def test_flow_ssdp_discovery(hass: HomeAssistantType): +async def test_flow_ssdp_discovery(hass: HomeAssistant): """Test config flow: discovered + configured through ssdp.""" udn = "uuid:device_1" location = "dummy" @@ -82,7 +82,7 @@ async def test_flow_ssdp_discovery(hass: HomeAssistantType): } -async def test_flow_ssdp_incomplete_discovery(hass: HomeAssistantType): +async def test_flow_ssdp_incomplete_discovery(hass: HomeAssistant): """Test config flow: incomplete discovery through ssdp.""" udn = "uuid:device_1" location = "dummy" @@ -103,7 +103,7 @@ async def test_flow_ssdp_incomplete_discovery(hass: HomeAssistantType): assert result["reason"] == "incomplete_discovery" -async def test_flow_ssdp_discovery_ignored(hass: HomeAssistantType): +async def test_flow_ssdp_discovery_ignored(hass: HomeAssistant): """Test config flow: discovery through ssdp, but ignored.""" udn = "uuid:device_random_1" location = "dummy" @@ -151,7 +151,7 @@ async def test_flow_ssdp_discovery_ignored(hass: HomeAssistantType): assert result["reason"] == "discovery_ignored" -async def test_flow_user(hass: HomeAssistantType): +async def test_flow_user(hass: HomeAssistant): """Test config flow: discovered + configured through user.""" udn = "uuid:device_1" location = "dummy" @@ -197,7 +197,7 @@ async def test_flow_user(hass: HomeAssistantType): } -async def test_flow_import(hass: HomeAssistantType): +async def test_flow_import(hass: HomeAssistant): """Test config flow: discovered + configured through configuration.yaml.""" udn = "uuid:device_1" mock_device = MockDevice(udn) @@ -235,7 +235,7 @@ async def test_flow_import(hass: HomeAssistantType): } -async def test_flow_import_already_configured(hass: HomeAssistantType): +async def test_flow_import_already_configured(hass: HomeAssistant): """Test config flow: discovered, but already configured.""" udn = "uuid:device_1" mock_device = MockDevice(udn) @@ -261,7 +261,7 @@ async def test_flow_import_already_configured(hass: HomeAssistantType): assert result["reason"] == "already_configured" -async def test_flow_import_incomplete(hass: HomeAssistantType): +async def test_flow_import_incomplete(hass: HomeAssistant): """Test config flow: incomplete discovery, configured through configuration.yaml.""" udn = "uuid:device_1" mock_device = MockDevice(udn) @@ -288,7 +288,7 @@ async def test_flow_import_incomplete(hass: HomeAssistantType): assert result["reason"] == "incomplete_discovery" -async def test_options_flow(hass: HomeAssistantType): +async def test_options_flow(hass: HomeAssistant): """Test options flow.""" # Set up config entry. udn = "uuid:device_1" diff --git a/tests/components/upnp/test_init.py b/tests/components/upnp/test_init.py index 086fbd677abed..e6e37ca52fbdf 100644 --- a/tests/components/upnp/test_init.py +++ b/tests/components/upnp/test_init.py @@ -15,7 +15,7 @@ DOMAIN, ) from homeassistant.components.upnp.device import Device -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from .mock_device import MockDevice @@ -23,7 +23,7 @@ from tests.common import MockConfigEntry -async def test_async_setup_entry_default(hass: HomeAssistantType): +async def test_async_setup_entry_default(hass: HomeAssistant): """Test async_setup_entry.""" udn = "uuid:device_1" location = "http://192.168.1.1/desc.xml" @@ -69,7 +69,7 @@ async def test_async_setup_entry_default(hass: HomeAssistantType): async_create_device.assert_called_with(hass, discoveries[0][DISCOVERY_LOCATION]) -async def test_sync_setup_entry_multiple_discoveries(hass: HomeAssistantType): +async def test_sync_setup_entry_multiple_discoveries(hass: HomeAssistant): """Test async_setup_entry.""" udn_0 = "uuid:device_1" location_0 = "http://192.168.1.1/desc.xml" diff --git a/tests/components/vizio/test_config_flow.py b/tests/components/vizio/test_config_flow.py index 5f33aa2be4adc..544ad2b38cd05 100644 --- a/tests/components/vizio/test_config_flow.py +++ b/tests/components/vizio/test_config_flow.py @@ -29,7 +29,7 @@ CONF_PIN, CONF_PORT, ) -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .const import ( ACCESS_TOKEN, @@ -56,7 +56,7 @@ async def test_user_flow_minimum_fields( - hass: HomeAssistantType, + hass: HomeAssistant, vizio_connect: pytest.fixture, vizio_bypass_setup: pytest.fixture, ) -> None: @@ -80,7 +80,7 @@ async def test_user_flow_minimum_fields( async def test_user_flow_all_fields( - hass: HomeAssistantType, + hass: HomeAssistant, vizio_connect: pytest.fixture, vizio_bypass_setup: pytest.fixture, ) -> None: @@ -107,7 +107,7 @@ async def test_user_flow_all_fields( async def test_speaker_options_flow( - hass: HomeAssistantType, + hass: HomeAssistant, vizio_connect: pytest.fixture, vizio_bypass_update: pytest.fixture, ) -> None: @@ -135,7 +135,7 @@ async def test_speaker_options_flow( async def test_tv_options_flow_no_apps( - hass: HomeAssistantType, + hass: HomeAssistant, vizio_connect: pytest.fixture, vizio_bypass_update: pytest.fixture, ) -> None: @@ -166,7 +166,7 @@ async def test_tv_options_flow_no_apps( async def test_tv_options_flow_with_apps( - hass: HomeAssistantType, + hass: HomeAssistant, vizio_connect: pytest.fixture, vizio_bypass_update: pytest.fixture, ) -> None: @@ -198,7 +198,7 @@ async def test_tv_options_flow_with_apps( async def test_tv_options_flow_start_with_volume( - hass: HomeAssistantType, + hass: HomeAssistant, vizio_connect: pytest.fixture, vizio_bypass_update: pytest.fixture, ) -> None: @@ -240,7 +240,7 @@ async def test_tv_options_flow_start_with_volume( async def test_user_host_already_configured( - hass: HomeAssistantType, + hass: HomeAssistant, vizio_connect: pytest.fixture, vizio_bypass_setup: pytest.fixture, ) -> None: @@ -264,7 +264,7 @@ async def test_user_host_already_configured( async def test_user_serial_number_already_exists( - hass: HomeAssistantType, + hass: HomeAssistant, vizio_connect: pytest.fixture, vizio_bypass_setup: pytest.fixture, ) -> None: @@ -288,7 +288,7 @@ async def test_user_serial_number_already_exists( async def test_user_error_on_could_not_connect( - hass: HomeAssistantType, vizio_no_unique_id: pytest.fixture + hass: HomeAssistant, vizio_no_unique_id: pytest.fixture ) -> None: """Test with could_not_connect during user setup due to no connectivity.""" result = await hass.config_entries.flow.async_init( @@ -300,7 +300,7 @@ async def test_user_error_on_could_not_connect( async def test_user_error_on_could_not_connect_invalid_token( - hass: HomeAssistantType, vizio_cant_connect: pytest.fixture + hass: HomeAssistant, vizio_cant_connect: pytest.fixture ) -> None: """Test with could_not_connect during user setup due to invalid token.""" result = await hass.config_entries.flow.async_init( @@ -312,7 +312,7 @@ async def test_user_error_on_could_not_connect_invalid_token( async def test_user_tv_pairing_no_apps( - hass: HomeAssistantType, + hass: HomeAssistant, vizio_connect: pytest.fixture, vizio_bypass_setup: pytest.fixture, vizio_complete_pairing: pytest.fixture, @@ -343,7 +343,7 @@ async def test_user_tv_pairing_no_apps( async def test_user_start_pairing_failure( - hass: HomeAssistantType, + hass: HomeAssistant, vizio_connect: pytest.fixture, vizio_bypass_setup: pytest.fixture, vizio_start_pairing_failure: pytest.fixture, @@ -359,7 +359,7 @@ async def test_user_start_pairing_failure( async def test_user_invalid_pin( - hass: HomeAssistantType, + hass: HomeAssistant, vizio_connect: pytest.fixture, vizio_bypass_setup: pytest.fixture, vizio_invalid_pin_failure: pytest.fixture, @@ -382,7 +382,7 @@ async def test_user_invalid_pin( async def test_user_ignore( - hass: HomeAssistantType, + hass: HomeAssistant, vizio_connect: pytest.fixture, vizio_bypass_setup: pytest.fixture, ) -> None: @@ -402,7 +402,7 @@ async def test_user_ignore( async def test_import_flow_minimum_fields( - hass: HomeAssistantType, + hass: HomeAssistant, vizio_connect: pytest.fixture, vizio_bypass_setup: pytest.fixture, ) -> None: @@ -424,7 +424,7 @@ async def test_import_flow_minimum_fields( async def test_import_flow_all_fields( - hass: HomeAssistantType, + hass: HomeAssistant, vizio_connect: pytest.fixture, vizio_bypass_setup: pytest.fixture, ) -> None: @@ -445,7 +445,7 @@ async def test_import_flow_all_fields( async def test_import_entity_already_configured( - hass: HomeAssistantType, + hass: HomeAssistant, vizio_connect: pytest.fixture, vizio_bypass_setup: pytest.fixture, ) -> None: @@ -467,7 +467,7 @@ async def test_import_entity_already_configured( async def test_import_flow_update_options( - hass: HomeAssistantType, + hass: HomeAssistant, vizio_connect: pytest.fixture, vizio_bypass_update: pytest.fixture, ) -> None: @@ -498,7 +498,7 @@ async def test_import_flow_update_options( async def test_import_flow_update_name_and_apps( - hass: HomeAssistantType, + hass: HomeAssistant, vizio_connect: pytest.fixture, vizio_bypass_update: pytest.fixture, ) -> None: @@ -532,7 +532,7 @@ async def test_import_flow_update_name_and_apps( async def test_import_flow_update_remove_apps( - hass: HomeAssistantType, + hass: HomeAssistant, vizio_connect: pytest.fixture, vizio_bypass_update: pytest.fixture, ) -> None: @@ -565,7 +565,7 @@ async def test_import_flow_update_remove_apps( async def test_import_needs_pairing( - hass: HomeAssistantType, + hass: HomeAssistant, vizio_connect: pytest.fixture, vizio_bypass_setup: pytest.fixture, vizio_complete_pairing: pytest.fixture, @@ -602,7 +602,7 @@ async def test_import_needs_pairing( async def test_import_with_apps_needs_pairing( - hass: HomeAssistantType, + hass: HomeAssistant, vizio_connect: pytest.fixture, vizio_bypass_setup: pytest.fixture, vizio_complete_pairing: pytest.fixture, @@ -645,7 +645,7 @@ async def test_import_with_apps_needs_pairing( async def test_import_flow_additional_configs( - hass: HomeAssistantType, + hass: HomeAssistant, vizio_connect: pytest.fixture, vizio_bypass_update: pytest.fixture, ) -> None: @@ -665,7 +665,7 @@ async def test_import_flow_additional_configs( async def test_import_error( - hass: HomeAssistantType, + hass: HomeAssistant, vizio_connect: pytest.fixture, vizio_bypass_setup: pytest.fixture, caplog: pytest.fixture, @@ -699,7 +699,7 @@ async def test_import_error( async def test_import_ignore( - hass: HomeAssistantType, + hass: HomeAssistant, vizio_connect: pytest.fixture, vizio_bypass_setup: pytest.fixture, ) -> None: @@ -722,7 +722,7 @@ async def test_import_ignore( async def test_zeroconf_flow( - hass: HomeAssistantType, + hass: HomeAssistant, vizio_connect: pytest.fixture, vizio_bypass_setup: pytest.fixture, vizio_guess_device_type: pytest.fixture, @@ -753,7 +753,7 @@ async def test_zeroconf_flow( async def test_zeroconf_flow_already_configured( - hass: HomeAssistantType, + hass: HomeAssistant, vizio_connect: pytest.fixture, vizio_bypass_setup: pytest.fixture, vizio_guess_device_type: pytest.fixture, @@ -779,7 +779,7 @@ async def test_zeroconf_flow_already_configured( async def test_zeroconf_flow_with_port_in_host( - hass: HomeAssistantType, + hass: HomeAssistant, vizio_connect: pytest.fixture, vizio_bypass_setup: pytest.fixture, vizio_guess_device_type: pytest.fixture, @@ -808,7 +808,7 @@ async def test_zeroconf_flow_with_port_in_host( async def test_zeroconf_dupe_fail( - hass: HomeAssistantType, + hass: HomeAssistant, vizio_connect: pytest.fixture, vizio_bypass_setup: pytest.fixture, vizio_guess_device_type: pytest.fixture, @@ -834,7 +834,7 @@ async def test_zeroconf_dupe_fail( async def test_zeroconf_ignore( - hass: HomeAssistantType, + hass: HomeAssistant, vizio_connect: pytest.fixture, vizio_bypass_setup: pytest.fixture, vizio_guess_device_type: pytest.fixture, @@ -857,7 +857,7 @@ async def test_zeroconf_ignore( async def test_zeroconf_no_unique_id( - hass: HomeAssistantType, + hass: HomeAssistant, vizio_guess_device_type: pytest.fixture, vizio_no_unique_id: pytest.fixture, ) -> None: @@ -873,7 +873,7 @@ async def test_zeroconf_no_unique_id( async def test_zeroconf_abort_when_ignored( - hass: HomeAssistantType, + hass: HomeAssistant, vizio_connect: pytest.fixture, vizio_bypass_setup: pytest.fixture, vizio_guess_device_type: pytest.fixture, @@ -898,7 +898,7 @@ async def test_zeroconf_abort_when_ignored( async def test_zeroconf_flow_already_configured_hostname( - hass: HomeAssistantType, + hass: HomeAssistant, vizio_connect: pytest.fixture, vizio_bypass_setup: pytest.fixture, vizio_hostname_check: pytest.fixture, @@ -927,7 +927,7 @@ async def test_zeroconf_flow_already_configured_hostname( async def test_import_flow_already_configured_hostname( - hass: HomeAssistantType, + hass: HomeAssistant, vizio_connect: pytest.fixture, vizio_bypass_setup: pytest.fixture, vizio_hostname_check: pytest.fixture, diff --git a/tests/components/vizio/test_init.py b/tests/components/vizio/test_init.py index b223202d5b18f..16e2a5bb769ad 100644 --- a/tests/components/vizio/test_init.py +++ b/tests/components/vizio/test_init.py @@ -4,7 +4,7 @@ from homeassistant.components.media_player.const import DOMAIN as MP_DOMAIN from homeassistant.components.vizio.const import DOMAIN from homeassistant.const import STATE_UNAVAILABLE -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from .const import MOCK_SPEAKER_CONFIG, MOCK_USER_VALID_TV_CONFIG, UNIQUE_ID @@ -13,7 +13,7 @@ async def test_setup_component( - hass: HomeAssistantType, + hass: HomeAssistant, vizio_connect: pytest.fixture, vizio_update: pytest.fixture, ) -> None: @@ -26,7 +26,7 @@ async def test_setup_component( async def test_tv_load_and_unload( - hass: HomeAssistantType, + hass: HomeAssistant, vizio_connect: pytest.fixture, vizio_update: pytest.fixture, ) -> None: @@ -50,7 +50,7 @@ async def test_tv_load_and_unload( async def test_speaker_load_and_unload( - hass: HomeAssistantType, + hass: HomeAssistant, vizio_connect: pytest.fixture, vizio_update: pytest.fixture, ) -> None: diff --git a/tests/components/vizio/test_media_player.py b/tests/components/vizio/test_media_player.py index 48a1b5b464fd6..c137f112976dc 100644 --- a/tests/components/vizio/test_media_player.py +++ b/tests/components/vizio/test_media_player.py @@ -49,7 +49,7 @@ VIZIO_SCHEMA, ) from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, STATE_UNAVAILABLE -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util from .const import ( @@ -82,7 +82,7 @@ async def _add_config_entry_to_hass( - hass: HomeAssistantType, config_entry: MockConfigEntry + hass: HomeAssistant, config_entry: MockConfigEntry ) -> None: config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) @@ -112,7 +112,7 @@ def _assert_sources_and_volume(attr: dict[str, Any], vizio_device_class: str) -> def _get_attr_and_assert_base_attr( - hass: HomeAssistantType, device_class: str, power_state: str + hass: HomeAssistant, device_class: str, power_state: str ) -> dict[str, Any]: """Return entity attributes after asserting name, device class, and power state.""" attr = hass.states.get(ENTITY_ID).attributes @@ -141,9 +141,7 @@ async def _cm_for_test_setup_without_apps( yield -async def _test_setup_tv( - hass: HomeAssistantType, vizio_power_state: bool | None -) -> None: +async def _test_setup_tv(hass: HomeAssistant, vizio_power_state: bool | None) -> None: """Test Vizio TV entity setup.""" ha_power_state = _get_ha_power_state(vizio_power_state) @@ -166,7 +164,7 @@ async def _test_setup_tv( async def _test_setup_speaker( - hass: HomeAssistantType, vizio_power_state: bool | None + hass: HomeAssistant, vizio_power_state: bool | None ) -> None: """Test Vizio Speaker entity setup.""" ha_power_state = _get_ha_power_state(vizio_power_state) @@ -203,7 +201,7 @@ async def _test_setup_speaker( @asynccontextmanager async def _cm_for_test_setup_tv_with_apps( - hass: HomeAssistantType, device_config: dict[str, Any], app_config: dict[str, Any] + hass: HomeAssistant, device_config: dict[str, Any], app_config: dict[str, Any] ) -> None: """Context manager to setup test for Vizio TV with support for apps.""" config_entry = MockConfigEntry( @@ -242,7 +240,7 @@ def _assert_source_list_with_apps( async def _test_service( - hass: HomeAssistantType, + hass: HomeAssistant, domain: str, vizio_func_name: str, ha_service_name: str, @@ -272,7 +270,7 @@ async def _test_service( async def test_speaker_on( - hass: HomeAssistantType, + hass: HomeAssistant, vizio_connect: pytest.fixture, vizio_update: pytest.fixture, ) -> None: @@ -281,7 +279,7 @@ async def test_speaker_on( async def test_speaker_off( - hass: HomeAssistantType, + hass: HomeAssistant, vizio_connect: pytest.fixture, vizio_update: pytest.fixture, ) -> None: @@ -290,7 +288,7 @@ async def test_speaker_off( async def test_speaker_unavailable( - hass: HomeAssistantType, + hass: HomeAssistant, vizio_connect: pytest.fixture, vizio_update: pytest.fixture, ) -> None: @@ -299,7 +297,7 @@ async def test_speaker_unavailable( async def test_init_tv_on( - hass: HomeAssistantType, + hass: HomeAssistant, vizio_connect: pytest.fixture, vizio_update: pytest.fixture, ) -> None: @@ -308,7 +306,7 @@ async def test_init_tv_on( async def test_init_tv_off( - hass: HomeAssistantType, + hass: HomeAssistant, vizio_connect: pytest.fixture, vizio_update: pytest.fixture, ) -> None: @@ -317,7 +315,7 @@ async def test_init_tv_off( async def test_init_tv_unavailable( - hass: HomeAssistantType, + hass: HomeAssistant, vizio_connect: pytest.fixture, vizio_update: pytest.fixture, ) -> None: @@ -326,7 +324,7 @@ async def test_init_tv_unavailable( async def test_setup_unavailable_speaker( - hass: HomeAssistantType, vizio_cant_connect: pytest.fixture + hass: HomeAssistant, vizio_cant_connect: pytest.fixture ) -> None: """Test speaker entity sets up as unavailable.""" config_entry = MockConfigEntry( @@ -338,7 +336,7 @@ async def test_setup_unavailable_speaker( async def test_setup_unavailable_tv( - hass: HomeAssistantType, vizio_cant_connect: pytest.fixture + hass: HomeAssistant, vizio_cant_connect: pytest.fixture ) -> None: """Test TV entity sets up as unavailable.""" config_entry = MockConfigEntry( @@ -350,7 +348,7 @@ async def test_setup_unavailable_tv( async def test_services( - hass: HomeAssistantType, + hass: HomeAssistant, vizio_connect: pytest.fixture, vizio_update: pytest.fixture, ) -> None: @@ -439,7 +437,7 @@ async def test_services( async def test_options_update( - hass: HomeAssistantType, + hass: HomeAssistant, vizio_connect: pytest.fixture, vizio_update: pytest.fixture, ) -> None: @@ -461,7 +459,7 @@ async def test_options_update( async def _test_update_availability_switch( - hass: HomeAssistantType, + hass: HomeAssistant, initial_power_state: bool | None, final_power_state: bool | None, caplog: pytest.fixture, @@ -504,7 +502,7 @@ async def _test_update_availability_switch( async def test_update_unavailable_to_available( - hass: HomeAssistantType, + hass: HomeAssistant, vizio_connect: pytest.fixture, vizio_update: pytest.fixture, caplog: pytest.fixture, @@ -514,7 +512,7 @@ async def test_update_unavailable_to_available( async def test_update_available_to_unavailable( - hass: HomeAssistantType, + hass: HomeAssistant, vizio_connect: pytest.fixture, vizio_update: pytest.fixture, caplog: pytest.fixture, @@ -524,7 +522,7 @@ async def test_update_available_to_unavailable( async def test_setup_with_apps( - hass: HomeAssistantType, + hass: HomeAssistant, vizio_connect: pytest.fixture, vizio_update_with_apps: pytest.fixture, caplog: pytest.fixture, @@ -552,7 +550,7 @@ async def test_setup_with_apps( async def test_setup_with_apps_include( - hass: HomeAssistantType, + hass: HomeAssistant, vizio_connect: pytest.fixture, vizio_update_with_apps: pytest.fixture, caplog: pytest.fixture, @@ -570,7 +568,7 @@ async def test_setup_with_apps_include( async def test_setup_with_apps_exclude( - hass: HomeAssistantType, + hass: HomeAssistant, vizio_connect: pytest.fixture, vizio_update_with_apps: pytest.fixture, caplog: pytest.fixture, @@ -588,7 +586,7 @@ async def test_setup_with_apps_exclude( async def test_setup_with_apps_additional_apps_config( - hass: HomeAssistantType, + hass: HomeAssistant, vizio_connect: pytest.fixture, vizio_update_with_apps: pytest.fixture, caplog: pytest.fixture, @@ -654,7 +652,7 @@ async def test_setup_with_apps_additional_apps_config( assert not service_call2.called -def test_invalid_apps_config(hass: HomeAssistantType): +def test_invalid_apps_config(hass: HomeAssistant): """Test that schema validation fails on certain conditions.""" with raises(vol.Invalid): vol.Schema(vol.All(VIZIO_SCHEMA, validate_apps))(MOCK_TV_APPS_FAILURE) @@ -664,7 +662,7 @@ def test_invalid_apps_config(hass: HomeAssistantType): async def test_setup_with_unknown_app_config( - hass: HomeAssistantType, + hass: HomeAssistant, vizio_connect: pytest.fixture, vizio_update_with_apps: pytest.fixture, caplog: pytest.fixture, @@ -681,7 +679,7 @@ async def test_setup_with_unknown_app_config( async def test_setup_with_no_running_app( - hass: HomeAssistantType, + hass: HomeAssistant, vizio_connect: pytest.fixture, vizio_update_with_apps: pytest.fixture, caplog: pytest.fixture, @@ -698,7 +696,7 @@ async def test_setup_with_no_running_app( async def test_setup_tv_without_mute( - hass: HomeAssistantType, + hass: HomeAssistant, vizio_connect: pytest.fixture, vizio_update: pytest.fixture, ) -> None: @@ -722,7 +720,7 @@ async def test_setup_tv_without_mute( async def test_apps_update( - hass: HomeAssistantType, + hass: HomeAssistant, vizio_connect: pytest.fixture, vizio_update_with_apps: pytest.fixture, caplog: pytest.fixture, From 9879b7becfa03e8ff5b256276cb3fc5177b52a20 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 22 Apr 2021 17:04:28 +0200 Subject: [PATCH 0448/1317] Rename HomeAssistantType to HomeAssistant, integrations w* - z* (#49543) * Integration zha: HomeAssistantType -> HomeAssistant. * Integration zerproc: HomeAssistantType -> HomeAssistant. * Integration xbox: HomeAssistantType -> HomeAssistant. * Integration wunderground: HomeAssistantType -> HomeAssistant. * Integration wled: HomeAssistantType -> HomeAssistant. * Integration water_heater: HomeAssistantType -> HomeAssistant. * Integration websocket_api: HomeAssistantType -> HomeAssistant. * Integration wilight: HomeAssistantType -> HomeAssistant. --- homeassistant/components/water_heater/group.py | 5 ++--- .../components/water_heater/reproduce_state.py | 7 +++---- homeassistant/components/wled/light.py | 5 ++--- homeassistant/components/wled/sensor.py | 4 ++-- homeassistant/components/wled/switch.py | 4 ++-- homeassistant/components/wunderground/sensor.py | 7 ++++--- homeassistant/components/xbox/__init__.py | 3 +-- homeassistant/components/xbox/binary_sensor.py | 5 ++--- homeassistant/components/xbox/media_source.py | 9 ++++----- homeassistant/components/xbox/sensor.py | 5 ++--- homeassistant/components/zerproc/light.py | 3 +-- homeassistant/components/zha/__init__.py | 6 +++--- homeassistant/components/zha/core/device.py | 7 +++---- homeassistant/components/zha/core/discovery.py | 9 ++++----- homeassistant/components/zha/core/group.py | 6 +++--- homeassistant/components/zha/sensor.py | 6 +++--- tests/components/websocket_api/test_commands.py | 5 ++--- tests/components/wilight/__init__.py | 4 ++-- tests/components/wilight/test_config_flow.py | 16 ++++++++-------- tests/components/wilight/test_cover.py | 6 +++--- tests/components/wilight/test_fan.py | 10 +++++----- tests/components/wilight/test_init.py | 8 +++----- tests/components/wilight/test_light.py | 10 +++++----- 23 files changed, 69 insertions(+), 81 deletions(-) diff --git a/homeassistant/components/water_heater/group.py b/homeassistant/components/water_heater/group.py index f4ec0ecbc2654..59d5478b1ab6e 100644 --- a/homeassistant/components/water_heater/group.py +++ b/homeassistant/components/water_heater/group.py @@ -3,8 +3,7 @@ from homeassistant.components.group import GroupIntegrationRegistry from homeassistant.const import STATE_OFF -from homeassistant.core import callback -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant, callback from . import ( STATE_ECO, @@ -18,7 +17,7 @@ @callback def async_describe_on_off_states( - hass: HomeAssistantType, registry: GroupIntegrationRegistry + hass: HomeAssistant, registry: GroupIntegrationRegistry ) -> None: """Describe group on off states.""" registry.on_off_states( diff --git a/homeassistant/components/water_heater/reproduce_state.py b/homeassistant/components/water_heater/reproduce_state.py index 235eac5cd57db..513b365e67af3 100644 --- a/homeassistant/components/water_heater/reproduce_state.py +++ b/homeassistant/components/water_heater/reproduce_state.py @@ -13,8 +13,7 @@ STATE_OFF, STATE_ON, ) -from homeassistant.core import Context, State -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import Context, HomeAssistant, State from . import ( ATTR_AWAY_MODE, @@ -47,7 +46,7 @@ async def _async_reproduce_state( - hass: HomeAssistantType, + hass: HomeAssistant, state: State, *, context: Context | None = None, @@ -124,7 +123,7 @@ async def _async_reproduce_state( async def async_reproduce_states( - hass: HomeAssistantType, + hass: HomeAssistant, states: Iterable[State], *, context: Context | None = None, diff --git a/homeassistant/components/wled/light.py b/homeassistant/components/wled/light.py index 9de7eafc042d1..9d25a1bcbcf27 100644 --- a/homeassistant/components/wled/light.py +++ b/homeassistant/components/wled/light.py @@ -22,13 +22,12 @@ LightEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_registry import ( async_get_registry as async_get_entity_registry, ) -from homeassistant.helpers.typing import HomeAssistantType import homeassistant.util.color as color_util from . import WLEDDataUpdateCoordinator, WLEDDeviceEntity, wled_exception_handler @@ -51,7 +50,7 @@ async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable[[list[Entity], bool], None], ) -> None: diff --git a/homeassistant/components/wled/sensor.py b/homeassistant/components/wled/sensor.py index 7e91f81dea086..96c79452790f4 100644 --- a/homeassistant/components/wled/sensor.py +++ b/homeassistant/components/wled/sensor.py @@ -13,8 +13,8 @@ PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util.dt import utcnow from . import WLEDDataUpdateCoordinator, WLEDDeviceEntity @@ -22,7 +22,7 @@ async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable[[list[Entity], bool], None], ) -> None: diff --git a/homeassistant/components/wled/switch.py b/homeassistant/components/wled/switch.py index 5902cd246a018..f262b5a3fa41e 100644 --- a/homeassistant/components/wled/switch.py +++ b/homeassistant/components/wled/switch.py @@ -5,8 +5,8 @@ from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import HomeAssistantType from . import WLEDDataUpdateCoordinator, WLEDDeviceEntity, wled_exception_handler from .const import ( @@ -21,7 +21,7 @@ async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable[[list[Entity], bool], None], ) -> None: diff --git a/homeassistant/components/wunderground/sensor.py b/homeassistant/components/wunderground/sensor.py index 358e305dc4774..67eab97b4c3d1 100644 --- a/homeassistant/components/wunderground/sensor.py +++ b/homeassistant/components/wunderground/sensor.py @@ -33,10 +33,11 @@ TEMP_CELSIUS, TEMP_FAHRENHEIT, ) +from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from homeassistant.util import Throttle _RESOURCE = "http://api.wunderground.com/api/{}/{}/{}/q/" @@ -1084,7 +1085,7 @@ def _get_attributes(rest): async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None ): """Set up the WUnderground sensor.""" latitude = config.get(CONF_LATITUDE, hass.config.latitude) @@ -1119,7 +1120,7 @@ async def async_setup_platform( class WUndergroundSensor(SensorEntity): """Implementing the WUnderground sensor.""" - def __init__(self, hass: HomeAssistantType, rest, condition, unique_id_base: str): + def __init__(self, hass: HomeAssistant, rest, condition, unique_id_base: str): """Initialize the sensor.""" self.rest = rest self._condition = condition diff --git a/homeassistant/components/xbox/__init__.py b/homeassistant/components/xbox/__init__.py index d287e515cefd4..2484c99b63821 100644 --- a/homeassistant/components/xbox/__init__.py +++ b/homeassistant/components/xbox/__init__.py @@ -29,7 +29,6 @@ config_entry_oauth2_flow, config_validation as cv, ) -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import api, config_flow @@ -168,7 +167,7 @@ class XboxUpdateCoordinator(DataUpdateCoordinator): def __init__( self, - hass: HomeAssistantType, + hass: HomeAssistant, client: XboxLiveClient, consoles: SmartglassConsoleList, ) -> None: diff --git a/homeassistant/components/xbox/binary_sensor.py b/homeassistant/components/xbox/binary_sensor.py index 98e0625714631..32a3126de1e22 100644 --- a/homeassistant/components/xbox/binary_sensor.py +++ b/homeassistant/components/xbox/binary_sensor.py @@ -4,11 +4,10 @@ from functools import partial from homeassistant.components.binary_sensor import BinarySensorEntity -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_registry import ( async_get_registry as async_get_entity_registry, ) -from homeassistant.helpers.typing import HomeAssistantType from . import XboxUpdateCoordinator from .base_sensor import XboxBaseSensorEntity @@ -17,7 +16,7 @@ PRESENCE_ATTRIBUTES = ["online", "in_party", "in_game", "in_multiplayer"] -async def async_setup_entry(hass: HomeAssistantType, config_entry, async_add_entities): +async def async_setup_entry(hass: HomeAssistant, config_entry, async_add_entities): """Set up Xbox Live friends.""" coordinator: XboxUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id][ "coordinator" diff --git a/homeassistant/components/xbox/media_source.py b/homeassistant/components/xbox/media_source.py index 64a16e2c21d7a..aeaa233a6ed17 100644 --- a/homeassistant/components/xbox/media_source.py +++ b/homeassistant/components/xbox/media_source.py @@ -24,8 +24,7 @@ MediaSourceItem, PlayMedia, ) -from homeassistant.core import callback -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant, callback from homeassistant.util import dt as dt_util from .browse_media import _find_media_image @@ -42,7 +41,7 @@ } -async def async_get_media_source(hass: HomeAssistantType): +async def async_get_media_source(hass: HomeAssistant): """Set up Xbox media source.""" entry = hass.config_entries.async_entries(DOMAIN)[0] client = hass.data[DOMAIN][entry.entry_id]["client"] @@ -75,11 +74,11 @@ class XboxSource(MediaSource): name: str = "Xbox Game Media" - def __init__(self, hass: HomeAssistantType, client: XboxLiveClient): + def __init__(self, hass: HomeAssistant, client: XboxLiveClient): """Initialize Xbox source.""" super().__init__(DOMAIN) - self.hass: HomeAssistantType = hass + self.hass: HomeAssistant = hass self.client: XboxLiveClient = client async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia: diff --git a/homeassistant/components/xbox/sensor.py b/homeassistant/components/xbox/sensor.py index ac19a4be1934c..9aa0de4a727f6 100644 --- a/homeassistant/components/xbox/sensor.py +++ b/homeassistant/components/xbox/sensor.py @@ -4,11 +4,10 @@ from functools import partial from homeassistant.components.sensor import SensorEntity -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_registry import ( async_get_registry as async_get_entity_registry, ) -from homeassistant.helpers.typing import HomeAssistantType from . import XboxUpdateCoordinator from .base_sensor import XboxBaseSensorEntity @@ -17,7 +16,7 @@ SENSOR_ATTRIBUTES = ["status", "gamer_score", "account_tier", "gold_tenure"] -async def async_setup_entry(hass: HomeAssistantType, config_entry, async_add_entities): +async def async_setup_entry(hass: HomeAssistant, config_entry, async_add_entities): """Set up Xbox Live friends.""" coordinator: XboxUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id][ "coordinator" diff --git a/homeassistant/components/zerproc/light.py b/homeassistant/components/zerproc/light.py index 627358ab97188..d4bf6a98c7033 100644 --- a/homeassistant/components/zerproc/light.py +++ b/homeassistant/components/zerproc/light.py @@ -19,7 +19,6 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.typing import HomeAssistantType import homeassistant.util.color as color_util from .const import DATA_ADDRESSES, DATA_DISCOVERY_SUBSCRIPTION, DOMAIN @@ -51,7 +50,7 @@ async def discover_entities(hass: HomeAssistant) -> list[Entity]: async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable[[list[Entity], bool], None], ) -> None: diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 4c8b73686bf11..801dedae0b6bb 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -8,10 +8,10 @@ from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH from homeassistant import config_entries, const as ha_const +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.typing import HomeAssistantType from . import api from .core import ZHAGateway @@ -156,7 +156,7 @@ async def async_unload_entry(hass, config_entry): return True -async def async_load_entities(hass: HomeAssistantType) -> None: +async def async_load_entities(hass: HomeAssistant) -> None: """Load entities after integration was setup.""" await hass.data[DATA_ZHA][DATA_ZHA_GATEWAY].async_initialize_devices_and_entities() to_setup = hass.data[DATA_ZHA][DATA_ZHA_PLATFORM_LOADED] @@ -168,7 +168,7 @@ async def async_load_entities(hass: HomeAssistantType) -> None: async def async_migrate_entry( - hass: HomeAssistantType, config_entry: config_entries.ConfigEntry + hass: HomeAssistant, config_entry: config_entries.ConfigEntry ): """Migrate old entry.""" _LOGGER.debug("Migrating from version %s", config_entry.version) diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index ab3c9b3b9e6d3..c8866990cd924 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -17,13 +17,12 @@ import zigpy.zdo.types as zdo_types from homeassistant.const import ATTR_COMMAND, ATTR_NAME -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.typing import HomeAssistantType from . import channels, typing as zha_typing from .const import ( @@ -88,7 +87,7 @@ class ZHADevice(LogMixin): def __init__( self, - hass: HomeAssistantType, + hass: HomeAssistant, zigpy_device: zha_typing.ZigpyDeviceType, zha_gateway: zha_typing.ZhaGatewayType, ): @@ -288,7 +287,7 @@ def zigbee_signature(self) -> dict[str, Any]: @classmethod def new( cls, - hass: HomeAssistantType, + hass: HomeAssistant, zigpy_dev: zha_typing.ZigpyDeviceType, gateway: zha_typing.ZhaGatewayType, restored: bool = False, diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py index 338796acffe0a..b12d6efbcf821 100644 --- a/homeassistant/components/zha/core/discovery.py +++ b/homeassistant/components/zha/core/discovery.py @@ -6,13 +6,12 @@ from typing import Callable from homeassistant import const as ha_const -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) from homeassistant.helpers.entity_registry import async_entries_for_device -from homeassistant.helpers.typing import HomeAssistantType from . import const as zha_const, registries as zha_regs, typing as zha_typing from .. import ( # noqa: F401 pylint: disable=unused-import, @@ -159,7 +158,7 @@ def handle_on_off_output_cluster_exception( channel = channel_class(cluster, ep_channels) self.probe_single_cluster(component, channel, ep_channels) - def initialize(self, hass: HomeAssistantType) -> None: + def initialize(self, hass: HomeAssistant) -> None: """Update device overrides config.""" zha_config = hass.data[zha_const.DATA_ZHA].get(zha_const.DATA_ZHA_CONFIG, {}) overrides = zha_config.get(zha_const.CONF_DEVICE_CONFIG) @@ -175,7 +174,7 @@ def __init__(self): self._hass = None self._unsubs = [] - def initialize(self, hass: HomeAssistantType) -> None: + def initialize(self, hass: HomeAssistant) -> None: """Initialize the group probe.""" self._hass = hass self._unsubs.append( @@ -235,7 +234,7 @@ def discover_group_entities(self, group: zha_typing.ZhaGroupType) -> None: @staticmethod def determine_entity_domains( - hass: HomeAssistantType, group: zha_typing.ZhaGroupType + hass: HomeAssistant, group: zha_typing.ZhaGroupType ) -> list[str]: """Determine the entity domains for this group.""" entity_domains: list[str] = [] diff --git a/homeassistant/components/zha/core/group.py b/homeassistant/components/zha/core/group.py index beaebbe876703..90dcb6fffc381 100644 --- a/homeassistant/components/zha/core/group.py +++ b/homeassistant/components/zha/core/group.py @@ -8,8 +8,8 @@ import zigpy.exceptions +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_registry import async_entries_for_device -from homeassistant.helpers.typing import HomeAssistantType from .helpers import LogMixin from .typing import ( @@ -113,12 +113,12 @@ class ZHAGroup(LogMixin): def __init__( self, - hass: HomeAssistantType, + hass: HomeAssistant, zha_gateway: ZhaGatewayType, zigpy_group: ZigpyGroupType, ): """Initialize the group.""" - self.hass: HomeAssistantType = hass + self.hass: HomeAssistant = hass self._zigpy_group: ZigpyGroupType = zigpy_group self._zha_gateway: ZhaGatewayType = zha_gateway diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index aa7a1649b14a7..d40638ecd71cd 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -26,9 +26,9 @@ PRESSURE_HPA, TEMP_CELSIUS, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.typing import HomeAssistantType, StateType +from homeassistant.helpers.typing import StateType from .core import discovery from .core.const import ( @@ -72,7 +72,7 @@ async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable ) -> None: """Set up the Zigbee Home Automation sensor from config entry.""" entities_to_create = hass.data[DATA_ZHA][DOMAIN] diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 3ec021c3e3b5f..bb74bbe8ca860 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -14,11 +14,10 @@ TYPE_AUTH_REQUIRED, ) from homeassistant.components.websocket_api.const import URL -from homeassistant.core import Context, callback +from homeassistant.core import Context, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.loader import async_get_integration from homeassistant.setup import DATA_SETUP_TIME, async_setup_component @@ -233,7 +232,7 @@ async def serv_handler(call): async def test_call_service_schema_validation_error( - hass: HomeAssistantType, websocket_client + hass: HomeAssistant, websocket_client ): """Test call service command with invalid service data.""" diff --git a/tests/components/wilight/__init__.py b/tests/components/wilight/__init__.py index 7ee7f0119a460..d16b4d083e85e 100644 --- a/tests/components/wilight/__init__.py +++ b/tests/components/wilight/__init__.py @@ -14,7 +14,7 @@ CONF_SERIAL_NUMBER, ) from homeassistant.const import CONF_HOST -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -58,7 +58,7 @@ async def setup_integration( - hass: HomeAssistantType, + hass: HomeAssistant, ) -> MockConfigEntry: """Mock ConfigEntry in Home Assistant.""" diff --git a/tests/components/wilight/test_config_flow.py b/tests/components/wilight/test_config_flow.py index 9888dbe3ef945..42f6aa592b057 100644 --- a/tests/components/wilight/test_config_flow.py +++ b/tests/components/wilight/test_config_flow.py @@ -10,12 +10,12 @@ ) from homeassistant.config_entries import SOURCE_SSDP from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SOURCE +from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import ( RESULT_TYPE_ABORT, RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM, ) -from homeassistant.helpers.typing import HomeAssistantType from tests.common import MockConfigEntry from tests.components.wilight import ( @@ -52,7 +52,7 @@ def mock_dummy_get_components_from_model_wrong(): yield components -async def test_show_ssdp_form(hass: HomeAssistantType) -> None: +async def test_show_ssdp_form(hass: HomeAssistant) -> None: """Test that the ssdp confirmation form is served.""" discovery_info = MOCK_SSDP_DISCOVERY_INFO_P_B.copy() @@ -68,7 +68,7 @@ async def test_show_ssdp_form(hass: HomeAssistantType) -> None: } -async def test_ssdp_not_wilight_abort_1(hass: HomeAssistantType) -> None: +async def test_ssdp_not_wilight_abort_1(hass: HomeAssistant) -> None: """Test that the ssdp aborts not_wilight.""" discovery_info = MOCK_SSDP_DISCOVERY_INFO_WRONG_MANUFACTORER.copy() @@ -80,7 +80,7 @@ async def test_ssdp_not_wilight_abort_1(hass: HomeAssistantType) -> None: assert result["reason"] == "not_wilight_device" -async def test_ssdp_not_wilight_abort_2(hass: HomeAssistantType) -> None: +async def test_ssdp_not_wilight_abort_2(hass: HomeAssistant) -> None: """Test that the ssdp aborts not_wilight.""" discovery_info = MOCK_SSDP_DISCOVERY_INFO_MISSING_MANUFACTORER.copy() @@ -93,7 +93,7 @@ async def test_ssdp_not_wilight_abort_2(hass: HomeAssistantType) -> None: async def test_ssdp_not_wilight_abort_3( - hass: HomeAssistantType, dummy_get_components_from_model_clear + hass: HomeAssistant, dummy_get_components_from_model_clear ) -> None: """Test that the ssdp aborts not_wilight.""" @@ -107,7 +107,7 @@ async def test_ssdp_not_wilight_abort_3( async def test_ssdp_not_supported_abort( - hass: HomeAssistantType, dummy_get_components_from_model_wrong + hass: HomeAssistant, dummy_get_components_from_model_wrong ) -> None: """Test that the ssdp aborts not_supported.""" @@ -120,7 +120,7 @@ async def test_ssdp_not_supported_abort( assert result["reason"] == "not_supported_device" -async def test_ssdp_device_exists_abort(hass: HomeAssistantType) -> None: +async def test_ssdp_device_exists_abort(hass: HomeAssistant) -> None: """Test abort SSDP flow if WiLight already configured.""" entry = MockConfigEntry( domain=DOMAIN, @@ -145,7 +145,7 @@ async def test_ssdp_device_exists_abort(hass: HomeAssistantType) -> None: assert result["reason"] == "already_configured" -async def test_full_ssdp_flow_implementation(hass: HomeAssistantType) -> None: +async def test_full_ssdp_flow_implementation(hass: HomeAssistant) -> None: """Test the full SSDP flow from start to finish.""" discovery_info = MOCK_SSDP_DISCOVERY_INFO_P_B.copy() diff --git a/tests/components/wilight/test_cover.py b/tests/components/wilight/test_cover.py index 8b058d9583644..ce0a65ca29a98 100644 --- a/tests/components/wilight/test_cover.py +++ b/tests/components/wilight/test_cover.py @@ -20,8 +20,8 @@ STATE_OPEN, STATE_OPENING, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.typing import HomeAssistantType from . import ( HOST, @@ -56,7 +56,7 @@ def mock_dummy_device_from_host_light_fan(): async def test_loading_cover( - hass: HomeAssistantType, + hass: HomeAssistant, dummy_device_from_host_cover, ) -> None: """Test the WiLight configuration entry loading.""" @@ -78,7 +78,7 @@ async def test_loading_cover( async def test_open_close_cover_state( - hass: HomeAssistantType, dummy_device_from_host_cover + hass: HomeAssistant, dummy_device_from_host_cover ) -> None: """Test the change of state of the cover.""" await setup_integration(hass) diff --git a/tests/components/wilight/test_fan.py b/tests/components/wilight/test_fan.py index 0ad7789c52cfe..dc3ad57e11f83 100644 --- a/tests/components/wilight/test_fan.py +++ b/tests/components/wilight/test_fan.py @@ -20,8 +20,8 @@ STATE_OFF, STATE_ON, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.typing import HomeAssistantType from . import ( HOST, @@ -56,7 +56,7 @@ def mock_dummy_device_from_host_light_fan(): async def test_loading_light_fan( - hass: HomeAssistantType, + hass: HomeAssistant, dummy_device_from_host_light_fan, ) -> None: """Test the WiLight configuration entry loading.""" @@ -78,7 +78,7 @@ async def test_loading_light_fan( async def test_on_off_fan_state( - hass: HomeAssistantType, dummy_device_from_host_light_fan + hass: HomeAssistant, dummy_device_from_host_light_fan ) -> None: """Test the change of state of the fan switches.""" await setup_integration(hass) @@ -125,7 +125,7 @@ async def test_on_off_fan_state( async def test_speed_fan_state( - hass: HomeAssistantType, dummy_device_from_host_light_fan + hass: HomeAssistant, dummy_device_from_host_light_fan ) -> None: """Test the change of speed of the fan switches.""" await setup_integration(hass) @@ -171,7 +171,7 @@ async def test_speed_fan_state( async def test_direction_fan_state( - hass: HomeAssistantType, dummy_device_from_host_light_fan + hass: HomeAssistant, dummy_device_from_host_light_fan ) -> None: """Test the change of direction of the fan switches.""" await setup_integration(hass) diff --git a/tests/components/wilight/test_init.py b/tests/components/wilight/test_init.py index 1441564b640d4..4f6654d34363a 100644 --- a/tests/components/wilight/test_init.py +++ b/tests/components/wilight/test_init.py @@ -10,7 +10,7 @@ ENTRY_STATE_NOT_LOADED, ENTRY_STATE_SETUP_RETRY, ) -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from tests.components.wilight import ( HOST, @@ -43,16 +43,14 @@ def mock_dummy_device_from_host(): yield device -async def test_config_entry_not_ready(hass: HomeAssistantType) -> None: +async def test_config_entry_not_ready(hass: HomeAssistant) -> None: """Test the WiLight configuration entry not ready.""" entry = await setup_integration(hass) assert entry.state == ENTRY_STATE_SETUP_RETRY -async def test_unload_config_entry( - hass: HomeAssistantType, dummy_device_from_host -) -> None: +async def test_unload_config_entry(hass: HomeAssistant, dummy_device_from_host) -> None: """Test the WiLight configuration entry unloading.""" entry = await setup_integration(hass) diff --git a/tests/components/wilight/test_light.py b/tests/components/wilight/test_light.py index 9abe17ce9e56f..2255840d01cb8 100644 --- a/tests/components/wilight/test_light.py +++ b/tests/components/wilight/test_light.py @@ -16,8 +16,8 @@ STATE_OFF, STATE_ON, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.typing import HomeAssistantType from tests.components.wilight import ( HOST, @@ -129,7 +129,7 @@ def mock_dummy_device_from_host_color(): async def test_loading_light( - hass: HomeAssistantType, + hass: HomeAssistant, dummy_device_from_host_light_fan, dummy_get_components_from_model_light, ) -> None: @@ -154,7 +154,7 @@ async def test_loading_light( async def test_on_off_light_state( - hass: HomeAssistantType, dummy_device_from_host_pb + hass: HomeAssistant, dummy_device_from_host_pb ) -> None: """Test the change of state of the light switches.""" await setup_integration(hass) @@ -187,7 +187,7 @@ async def test_on_off_light_state( async def test_dimmer_light_state( - hass: HomeAssistantType, dummy_device_from_host_dimmer + hass: HomeAssistant, dummy_device_from_host_dimmer ) -> None: """Test the change of state of the light switches.""" await setup_integration(hass) @@ -257,7 +257,7 @@ async def test_dimmer_light_state( async def test_color_light_state( - hass: HomeAssistantType, dummy_device_from_host_color + hass: HomeAssistant, dummy_device_from_host_color ) -> None: """Test the change of state of the light switches.""" await setup_integration(hass) From 9fe0c96474cdd543fb6faf6b014b9304bc97c1ed Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 22 Apr 2021 10:29:11 -0700 Subject: [PATCH 0449/1317] Fix Hue activate scene (#49556) --- homeassistant/components/hue/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index 68f48e47550f0..6bbe3d9ebddac 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -153,7 +153,7 @@ async def hue_activate_scene(call, skip_reload=True): # Call the set scene function on each bridge tasks = [ bridge.hue_activate_scene( - call.data, updated=skip_reload, hide_warnings=skip_reload + call.data, skip_reload=skip_reload, hide_warnings=skip_reload ) for bridge in hass.data[DOMAIN].values() if isinstance(bridge, HueBridge) From c351098f04c7f9b0f3f6a202d4e7eac18a4e2c62 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 22 Apr 2021 19:58:02 +0200 Subject: [PATCH 0450/1317] =?UTF-8?q?HomeAssistantType=20=E2=80=94>=20Home?= =?UTF-8?q?Assistant=20for=20Integrations=20p*=20-=20s*=20(#49558)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- homeassistant/components/ps4/__init__.py | 11 +++--- homeassistant/components/recorder/util.py | 4 +- homeassistant/components/remote/__init__.py | 11 +++--- homeassistant/components/remote/group.py | 5 +-- .../components/remote/reproduce_state.py | 7 ++-- homeassistant/components/roku/__init__.py | 8 ++-- homeassistant/components/roku/config_flow.py | 5 +-- homeassistant/components/roku/remote.py | 4 +- .../ruckus_unleashed/device_tracker.py | 5 +-- .../components/screenlogic/services.py | 7 ++-- .../components/shell_command/__init__.py | 6 +-- homeassistant/components/slack/notify.py | 14 +++---- homeassistant/components/smarthab/__init__.py | 6 +-- .../components/smartthings/__init__.py | 15 +++---- .../components/smartthings/smartapp.py | 30 +++++++------- homeassistant/components/zha/core/store.py | 9 ++--- tests/components/recorder/common.py | 16 ++++---- tests/components/recorder/conftest.py | 5 ++- tests/components/recorder/test_init.py | 7 ++-- tests/components/recorder/test_purge.py | 39 ++++++++++--------- tests/components/roku/__init__.py | 4 +- tests/components/roku/test_config_flow.py | 24 ++++++------ tests/components/roku/test_init.py | 6 +-- tests/components/roku/test_media_player.py | 36 ++++++++--------- tests/components/roku/test_remote.py | 10 ++--- tests/components/slack/test_notify.py | 8 ++-- 26 files changed, 144 insertions(+), 158 deletions(-) diff --git a/homeassistant/components/ps4/__init__.py b/homeassistant/components/ps4/__init__.py index 11d271be543a0..51583b5f4bc64 100644 --- a/homeassistant/components/ps4/__init__.py +++ b/homeassistant/components/ps4/__init__.py @@ -18,10 +18,9 @@ CONF_REGION, CONF_TOKEN, ) -from homeassistant.core import split_entity_id +from homeassistant.core import HomeAssistant, split_entity_id from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, entity_registry -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import location from homeassistant.util.json import load_json, save_json @@ -157,7 +156,7 @@ def format_unique_id(creds, mac_address): return f"{mac_address}_{suffix}" -def load_games(hass: HomeAssistantType, unique_id: str) -> dict: +def load_games(hass: HomeAssistant, unique_id: str) -> dict: """Load games for sources.""" g_file = hass.config.path(GAMES_FILE.format(unique_id)) try: @@ -176,7 +175,7 @@ def load_games(hass: HomeAssistantType, unique_id: str) -> dict: return games -def save_games(hass: HomeAssistantType, games: dict, unique_id: str): +def save_games(hass: HomeAssistant, games: dict, unique_id: str): """Save games to file.""" g_file = hass.config.path(GAMES_FILE.format(unique_id)) try: @@ -185,7 +184,7 @@ def save_games(hass: HomeAssistantType, games: dict, unique_id: str): _LOGGER.error("Could not save game list, %s", error) -def _reformat_data(hass: HomeAssistantType, games: dict, unique_id: str) -> dict: +def _reformat_data(hass: HomeAssistant, games: dict, unique_id: str) -> dict: """Reformat data to correct format.""" data_reformatted = False @@ -208,7 +207,7 @@ def _reformat_data(hass: HomeAssistantType, games: dict, unique_id: str) -> dict return games -def service_handle(hass: HomeAssistantType): +def service_handle(hass: HomeAssistant): """Handle for services.""" async def async_service_command(call): diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index e4e691bec915f..9f99dc2bf4592 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -11,7 +11,7 @@ from sqlalchemy.exc import OperationalError, SQLAlchemyError from sqlalchemy.orm.session import Session -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant import homeassistant.util.dt as dt_util from .const import DATA_INSTANCE, SQLITE_URL_PREFIX @@ -37,7 +37,7 @@ @contextmanager def session_scope( - *, hass: HomeAssistantType | None = None, session: Session | None = None + *, hass: HomeAssistant | None = None, session: Session | None = None ) -> Generator[Session, None, None]: """Provide a transactional scope around a series of operations.""" if session is None and hass is not None: diff --git a/homeassistant/components/remote/__init__.py b/homeassistant/components/remote/__init__.py index 94c54dd323d93..fef0da4dae635 100644 --- a/homeassistant/components/remote/__init__.py +++ b/homeassistant/components/remote/__init__.py @@ -17,6 +17,7 @@ SERVICE_TURN_ON, STATE_ON, ) +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, @@ -25,7 +26,7 @@ ) from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs @@ -69,12 +70,12 @@ @bind_hass -def is_on(hass: HomeAssistantType, entity_id: str) -> bool: +def is_on(hass: HomeAssistant, entity_id: str) -> bool: """Return if the remote is on based on the statemachine.""" return hass.states.is_state(entity_id, STATE_ON) -async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Track states and offer events for remotes.""" component = hass.data[DOMAIN] = EntityComponent( _LOGGER, DOMAIN, hass, SCAN_INTERVAL @@ -131,12 +132,12 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: return True -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" return await cast(EntityComponent, hass.data[DOMAIN]).async_setup_entry(entry) -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" return await cast(EntityComponent, hass.data[DOMAIN]).async_unload_entry(entry) diff --git a/homeassistant/components/remote/group.py b/homeassistant/components/remote/group.py index 1636054663dc6..234883ffd5a04 100644 --- a/homeassistant/components/remote/group.py +++ b/homeassistant/components/remote/group.py @@ -3,13 +3,12 @@ from homeassistant.components.group import GroupIntegrationRegistry from homeassistant.const import STATE_OFF, STATE_ON -from homeassistant.core import callback -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant, callback @callback def async_describe_on_off_states( - hass: HomeAssistantType, registry: GroupIntegrationRegistry + hass: HomeAssistant, registry: GroupIntegrationRegistry ) -> None: """Describe group on off states.""" registry.on_off_states({STATE_ON}, STATE_OFF) diff --git a/homeassistant/components/remote/reproduce_state.py b/homeassistant/components/remote/reproduce_state.py index 24f748d4a0279..cc9685dee2fb3 100644 --- a/homeassistant/components/remote/reproduce_state.py +++ b/homeassistant/components/remote/reproduce_state.py @@ -13,8 +13,7 @@ STATE_OFF, STATE_ON, ) -from homeassistant.core import Context, State -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import Context, HomeAssistant, State from . import DOMAIN @@ -24,7 +23,7 @@ async def _async_reproduce_state( - hass: HomeAssistantType, + hass: HomeAssistant, state: State, *, context: Context | None = None, @@ -60,7 +59,7 @@ async def _async_reproduce_state( async def async_reproduce_states( - hass: HomeAssistantType, + hass: HomeAssistant, states: Iterable[State], *, context: Context | None = None, diff --git a/homeassistant/components/roku/__init__.py b/homeassistant/components/roku/__init__.py index 3a12de51b06d2..d7bf30593742e 100644 --- a/homeassistant/components/roku/__init__.py +++ b/homeassistant/components/roku/__init__.py @@ -13,9 +13,9 @@ from homeassistant.components.remote import DOMAIN as REMOTE_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_NAME, CONF_HOST +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -39,7 +39,7 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Roku from a config entry.""" hass.data.setdefault(DOMAIN, {}) coordinator = hass.data[DOMAIN].get(entry.entry_id) @@ -57,7 +57,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool return True -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = all( await asyncio.gather( @@ -95,7 +95,7 @@ class RokuDataUpdateCoordinator(DataUpdateCoordinator[Device]): def __init__( self, - hass: HomeAssistantType, + hass: HomeAssistant, *, host: str, ): diff --git a/homeassistant/components/roku/config_flow.py b/homeassistant/components/roku/config_flow.py index 8424850fe6c6d..d10e22cd1bc1a 100644 --- a/homeassistant/components/roku/config_flow.py +++ b/homeassistant/components/roku/config_flow.py @@ -15,9 +15,8 @@ ) from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL, ConfigFlow from homeassistant.const import CONF_HOST, CONF_NAME -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.typing import HomeAssistantType from .const import DOMAIN @@ -29,7 +28,7 @@ _LOGGER = logging.getLogger(__name__) -async def validate_input(hass: HomeAssistantType, data: dict) -> dict: +async def validate_input(hass: HomeAssistant, data: dict) -> dict: """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. diff --git a/homeassistant/components/roku/remote.py b/homeassistant/components/roku/remote.py index da57866757866..a4f35294fd58c 100644 --- a/homeassistant/components/roku/remote.py +++ b/homeassistant/components/roku/remote.py @@ -5,14 +5,14 @@ from homeassistant.components.remote import ATTR_NUM_REPEATS, RemoteEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from . import RokuDataUpdateCoordinator, RokuEntity, roku_exception_handler from .const import DOMAIN async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable[[list, bool], None], ) -> bool: diff --git a/homeassistant/components/ruckus_unleashed/device_tracker.py b/homeassistant/components/ruckus_unleashed/device_tracker.py index 90a848b663b3a..a5bc266f04563 100644 --- a/homeassistant/components/ruckus_unleashed/device_tracker.py +++ b/homeassistant/components/ruckus_unleashed/device_tracker.py @@ -4,10 +4,9 @@ from homeassistant.components.device_tracker import SOURCE_TYPE_ROUTER from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( @@ -22,7 +21,7 @@ async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up device tracker for Ruckus Unleashed component.""" coordinator = hass.data[DOMAIN][entry.entry_id][COORDINATOR] diff --git a/homeassistant/components/screenlogic/services.py b/homeassistant/components/screenlogic/services.py index 7ca2bb6912923..31e35788f44f7 100644 --- a/homeassistant/components/screenlogic/services.py +++ b/homeassistant/components/screenlogic/services.py @@ -5,11 +5,10 @@ from screenlogicpy import ScreenLogicError import voluptuous as vol -from homeassistant.core import ServiceCall, callback +from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.service import async_extract_config_entry_ids -from homeassistant.helpers.typing import HomeAssistantType from .const import ( ATTR_COLOR_MODE, @@ -28,7 +27,7 @@ @callback -def async_load_screenlogic_services(hass: HomeAssistantType): +def async_load_screenlogic_services(hass: HomeAssistant): """Set up services for the ScreenLogic integration.""" if hass.services.has_service(DOMAIN, SERVICE_SET_COLOR_MODE): # Integration-level services have already been added. Return. @@ -76,7 +75,7 @@ async def async_set_color_mode(service_call: ServiceCall): @callback -def async_unload_screenlogic_services(hass: HomeAssistantType): +def async_unload_screenlogic_services(hass: HomeAssistant): """Unload services for the ScreenLogic integration.""" if hass.data[DOMAIN]: # There is still another config entry for this domain, don't remove services. diff --git a/homeassistant/components/shell_command/__init__.py b/homeassistant/components/shell_command/__init__.py index 089dc36b1a822..a86bf5b356658 100644 --- a/homeassistant/components/shell_command/__init__.py +++ b/homeassistant/components/shell_command/__init__.py @@ -6,10 +6,10 @@ import voluptuous as vol -from homeassistant.core import ServiceCall +from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import TemplateError from homeassistant.helpers import config_validation as cv, template -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType DOMAIN = "shell_command" @@ -22,7 +22,7 @@ ) -async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the shell_command component.""" conf = config.get(DOMAIN, {}) diff --git a/homeassistant/components/slack/notify.py b/homeassistant/components/slack/notify.py index f1e293773bd3a..c2ca834b5654c 100644 --- a/homeassistant/components/slack/notify.py +++ b/homeassistant/components/slack/notify.py @@ -21,14 +21,10 @@ BaseNotificationService, ) from homeassistant.const import ATTR_ICON, CONF_API_KEY, CONF_ICON, CONF_USERNAME -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import aiohttp_client, config_validation as cv import homeassistant.helpers.template as template -from homeassistant.helpers.typing import ( - ConfigType, - DiscoveryInfoType, - HomeAssistantType, -) +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType _LOGGER = logging.getLogger(__name__) @@ -109,7 +105,7 @@ class MessageT(TypedDict, total=False): async def async_get_service( - hass: HomeAssistantType, + hass: HomeAssistant, config: ConfigType, discovery_info: DiscoveryInfoType | None = None, ) -> SlackNotificationService | None: @@ -152,7 +148,7 @@ def _async_sanitize_channel_names(channel_list: list[str]) -> list[str]: @callback -def _async_templatize_blocks(hass: HomeAssistantType, value: Any) -> Any: +def _async_templatize_blocks(hass: HomeAssistant, value: Any) -> Any: """Recursive template creator helper function.""" if isinstance(value, list): return [_async_templatize_blocks(hass, item) for item in value] @@ -170,7 +166,7 @@ class SlackNotificationService(BaseNotificationService): def __init__( self, - hass: HomeAssistantType, + hass: HomeAssistant, client: WebClient, default_channel: str, username: str | None, diff --git a/homeassistant/components/smarthab/__init__.py b/homeassistant/components/smarthab/__init__.py index c259ef71aab96..ba6e7a5e5e734 100644 --- a/homeassistant/components/smarthab/__init__.py +++ b/homeassistant/components/smarthab/__init__.py @@ -7,9 +7,9 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_EMAIL, CONF_PASSWORD +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import HomeAssistantType DOMAIN = "smarthab" DATA_HUB = "hub" @@ -50,7 +50,7 @@ async def async_setup(hass, config) -> bool: return True -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up config entry for SmartHab integration.""" # Assign configuration variables @@ -77,7 +77,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): return True -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload config entry from SmartHab integration.""" result = all( diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index d36739c955107..456857efc9b4c 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -18,6 +18,7 @@ HTTP_FORBIDDEN, HTTP_UNAUTHORIZED, ) +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import ( @@ -26,7 +27,7 @@ ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from .config_flow import SmartThingsFlowHandler # noqa: F401 from .const import ( @@ -55,13 +56,13 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistantType, config: ConfigType): +async def async_setup(hass: HomeAssistant, config: ConfigType): """Initialize the SmartThings platform.""" await setup_smartapp_endpoint(hass) return True -async def async_migrate_entry(hass: HomeAssistantType, entry: ConfigEntry): +async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry): """Handle migration of a previous version config entry. A config entry created under a previous version must go through the @@ -81,7 +82,7 @@ async def async_migrate_entry(hass: HomeAssistantType, entry: ConfigEntry): return False -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Initialize config entry which represents an installed SmartApp.""" # For backwards compat if entry.unique_id is None: @@ -208,7 +209,7 @@ async def async_get_entry_scenes(entry: ConfigEntry, api): return [] -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" broker = hass.data[DOMAIN][DATA_BROKERS].pop(entry.entry_id, None) if broker: @@ -221,7 +222,7 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): return all(await asyncio.gather(*tasks)) -async def async_remove_entry(hass: HomeAssistantType, entry: ConfigEntry) -> None: +async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: """Perform clean-up when entry is being removed.""" api = SmartThings(async_get_clientsession(hass), entry.data[CONF_ACCESS_TOKEN]) @@ -270,7 +271,7 @@ class DeviceBroker: def __init__( self, - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, token, smart_app, diff --git a/homeassistant/components/smartthings/smartapp.py b/homeassistant/components/smartthings/smartapp.py index 24d6e4ae18fb5..0225a17a62c3c 100644 --- a/homeassistant/components/smartthings/smartapp.py +++ b/homeassistant/components/smartthings/smartapp.py @@ -25,13 +25,13 @@ from homeassistant.components import webhook from homeassistant.const import CONF_WEBHOOK_ID +from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) from homeassistant.helpers.network import NoURLAvailableError, get_url -from homeassistant.helpers.typing import HomeAssistantType from .const import ( APP_NAME_PREFIX, @@ -60,7 +60,7 @@ def format_unique_id(app_id: str, location_id: str) -> str: return f"{app_id}_{location_id}" -async def find_app(hass: HomeAssistantType, api): +async def find_app(hass: HomeAssistant, api): """Find an existing SmartApp for this installation of hass.""" apps = await api.apps() for app in [app for app in apps if app.app_name.startswith(APP_NAME_PREFIX)]: @@ -92,7 +92,7 @@ async def validate_installed_app(api, installed_app_id: str): return installed_app -def validate_webhook_requirements(hass: HomeAssistantType) -> bool: +def validate_webhook_requirements(hass: HomeAssistant) -> bool: """Ensure Home Assistant is setup properly to receive webhooks.""" if hass.components.cloud.async_active_subscription(): return True @@ -101,7 +101,7 @@ def validate_webhook_requirements(hass: HomeAssistantType) -> bool: return get_webhook_url(hass).lower().startswith("https://") -def get_webhook_url(hass: HomeAssistantType) -> str: +def get_webhook_url(hass: HomeAssistant) -> str: """ Get the URL of the webhook. @@ -113,7 +113,7 @@ def get_webhook_url(hass: HomeAssistantType) -> str: return webhook.async_generate_url(hass, hass.data[DOMAIN][CONF_WEBHOOK_ID]) -def _get_app_template(hass: HomeAssistantType): +def _get_app_template(hass: HomeAssistant): try: endpoint = f"at {get_url(hass, allow_cloud=False, prefer_external=True)}" except NoURLAvailableError: @@ -135,7 +135,7 @@ def _get_app_template(hass: HomeAssistantType): } -async def create_app(hass: HomeAssistantType, api): +async def create_app(hass: HomeAssistant, api): """Create a SmartApp for this instance of hass.""" # Create app from template attributes template = _get_app_template(hass) @@ -163,7 +163,7 @@ async def create_app(hass: HomeAssistantType, api): return app, client -async def update_app(hass: HomeAssistantType, app): +async def update_app(hass: HomeAssistant, app): """Ensure the SmartApp is up-to-date and update if necessary.""" template = _get_app_template(hass) template.pop("app_name") # don't update this @@ -199,7 +199,7 @@ def setup_smartapp(hass, app): return smartapp -async def setup_smartapp_endpoint(hass: HomeAssistantType): +async def setup_smartapp_endpoint(hass: HomeAssistant): """ Configure the SmartApp webhook in hass. @@ -276,7 +276,7 @@ async def setup_smartapp_endpoint(hass: HomeAssistantType): ) -async def unload_smartapp_endpoint(hass: HomeAssistantType): +async def unload_smartapp_endpoint(hass: HomeAssistant): """Tear down the component configuration.""" if DOMAIN not in hass.data: return @@ -308,7 +308,7 @@ async def unload_smartapp_endpoint(hass: HomeAssistantType): async def smartapp_sync_subscriptions( - hass: HomeAssistantType, + hass: HomeAssistant, auth_token: str, location_id: str, installed_app_id: str, @@ -397,7 +397,7 @@ async def delete_subscription(sub: SubscriptionEntity): async def _continue_flow( - hass: HomeAssistantType, + hass: HomeAssistant, app_id: str, location_id: str, installed_app_id: str, @@ -429,7 +429,7 @@ async def _continue_flow( ) -async def smartapp_install(hass: HomeAssistantType, req, resp, app): +async def smartapp_install(hass: HomeAssistant, req, resp, app): """Handle a SmartApp installation and continue the config flow.""" await _continue_flow( hass, app.app_id, req.location_id, req.installed_app_id, req.refresh_token @@ -441,7 +441,7 @@ async def smartapp_install(hass: HomeAssistantType, req, resp, app): ) -async def smartapp_update(hass: HomeAssistantType, req, resp, app): +async def smartapp_update(hass: HomeAssistant, req, resp, app): """Handle a SmartApp update and either update the entry or continue the flow.""" entry = next( ( @@ -470,7 +470,7 @@ async def smartapp_update(hass: HomeAssistantType, req, resp, app): ) -async def smartapp_uninstall(hass: HomeAssistantType, req, resp, app): +async def smartapp_uninstall(hass: HomeAssistant, req, resp, app): """ Handle when a SmartApp is removed from a location by the user. @@ -496,7 +496,7 @@ async def smartapp_uninstall(hass: HomeAssistantType, req, resp, app): ) -async def smartapp_webhook(hass: HomeAssistantType, webhook_id: str, request): +async def smartapp_webhook(hass: HomeAssistant, webhook_id: str, request): """ Handle a smartapp lifecycle event callback from SmartThings. diff --git a/homeassistant/components/zha/core/store.py b/homeassistant/components/zha/core/store.py index c4bbca2567a6f..f1b2ee57aeeb7 100644 --- a/homeassistant/components/zha/core/store.py +++ b/homeassistant/components/zha/core/store.py @@ -9,8 +9,7 @@ import attr -from homeassistant.core import callback -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant, callback from homeassistant.loader import bind_hass from .typing import ZhaDeviceType @@ -35,9 +34,9 @@ class ZhaDeviceEntry: class ZhaStorage: """Class to hold a registry of zha devices.""" - def __init__(self, hass: HomeAssistantType) -> None: + def __init__(self, hass: HomeAssistant) -> None: """Initialize the zha device storage.""" - self.hass: HomeAssistantType = hass + self.hass: HomeAssistant = hass self.devices: MutableMapping[str, ZhaDeviceEntry] = {} self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) @@ -130,7 +129,7 @@ def _data_to_save(self) -> dict: @bind_hass -async def async_get_registry(hass: HomeAssistantType) -> ZhaStorage: +async def async_get_registry(hass: HomeAssistant) -> ZhaStorage: """Return zha device storage instance.""" task = hass.data.get(DATA_REGISTRY) diff --git a/tests/components/recorder/common.py b/tests/components/recorder/common.py index 0bc4cfbfeb943..7414548c864ad 100644 --- a/tests/components/recorder/common.py +++ b/tests/components/recorder/common.py @@ -3,7 +3,7 @@ from homeassistant import core as ha from homeassistant.components import recorder -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util from tests.common import async_fire_time_changed, fire_time_changed @@ -11,7 +11,7 @@ DEFAULT_PURGE_TASKS = 3 -def wait_recording_done(hass: HomeAssistantType) -> None: +def wait_recording_done(hass: HomeAssistant) -> None: """Block till recording is done.""" hass.block_till_done() trigger_db_commit(hass) @@ -20,12 +20,12 @@ def wait_recording_done(hass: HomeAssistantType) -> None: hass.block_till_done() -async def async_wait_recording_done_without_instance(hass: HomeAssistantType) -> None: +async def async_wait_recording_done_without_instance(hass: HomeAssistant) -> None: """Block till recording is done.""" await hass.loop.run_in_executor(None, wait_recording_done, hass) -def trigger_db_commit(hass: HomeAssistantType) -> None: +def trigger_db_commit(hass: HomeAssistant) -> None: """Force the recorder to commit.""" for _ in range(recorder.DEFAULT_COMMIT_INTERVAL): # We only commit on time change @@ -33,7 +33,7 @@ def trigger_db_commit(hass: HomeAssistantType) -> None: async def async_wait_recording_done( - hass: HomeAssistantType, + hass: HomeAssistant, instance: recorder.Recorder, ) -> None: """Async wait until recording is done.""" @@ -45,7 +45,7 @@ async def async_wait_recording_done( async def async_wait_purge_done( - hass: HomeAssistantType, instance: recorder.Recorder, max: int = None + hass: HomeAssistant, instance: recorder.Recorder, max: int = None ) -> None: """Wait for max number of purge events. @@ -61,14 +61,14 @@ async def async_wait_purge_done( @ha.callback -def async_trigger_db_commit(hass: HomeAssistantType) -> None: +def async_trigger_db_commit(hass: HomeAssistant) -> None: """Fore the recorder to commit. Async friendly.""" for _ in range(recorder.DEFAULT_COMMIT_INTERVAL): async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=1)) async def async_recorder_block_till_done( - hass: HomeAssistantType, + hass: HomeAssistant, instance: recorder.Recorder, ) -> None: """Non blocking version of recorder.block_till_done().""" diff --git a/tests/components/recorder/conftest.py b/tests/components/recorder/conftest.py index 6eadb1c62ed00..6b8c61d4d7dce 100644 --- a/tests/components/recorder/conftest.py +++ b/tests/components/recorder/conftest.py @@ -8,7 +8,8 @@ from homeassistant.components.recorder import Recorder from homeassistant.components.recorder.const import DATA_INSTANCE -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType from .common import async_recorder_block_till_done @@ -45,7 +46,7 @@ async def async_setup_recorder_instance() -> AsyncGenerator[ """Yield callable to setup recorder instance.""" async def async_setup_recorder( - hass: HomeAssistantType, config: ConfigType | None = None + hass: HomeAssistant, config: ConfigType | None = None ) -> Recorder: """Setup and return recorder instance.""" # noqa: D401 await async_init_recorder_component(hass, config) diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index d346408839410..dddba971aadc4 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -32,7 +32,6 @@ STATE_UNLOCKED, ) from homeassistant.core import Context, CoreState, HomeAssistant, callback -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.setup import async_setup_component, setup_component from homeassistant.util import dt as dt_util @@ -116,7 +115,7 @@ async def test_state_gets_saved_when_set_before_start_event( async def test_saving_state( - hass: HomeAssistantType, async_setup_recorder_instance: SetupRecorderInstanceT + hass: HomeAssistant, async_setup_recorder_instance: SetupRecorderInstanceT ): """Test saving and restoring a state.""" instance = await async_setup_recorder_instance(hass) @@ -139,7 +138,7 @@ async def test_saving_state( async def test_saving_many_states( - hass: HomeAssistantType, async_setup_recorder_instance: SetupRecorderInstanceT + hass: HomeAssistant, async_setup_recorder_instance: SetupRecorderInstanceT ): """Test we expire after many commits.""" instance = await async_setup_recorder_instance(hass) @@ -165,7 +164,7 @@ async def test_saving_many_states( async def test_saving_state_with_intermixed_time_changes( - hass: HomeAssistantType, async_setup_recorder_instance: SetupRecorderInstanceT + hass: HomeAssistant, async_setup_recorder_instance: SetupRecorderInstanceT ): """Test saving states with intermixed time changes.""" instance = await async_setup_recorder_instance(hass) diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index d1825663ccc5d..23164bd73f50c 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -12,7 +12,8 @@ from homeassistant.components.recorder.purge import purge_old_data from homeassistant.components.recorder.util import session_scope from homeassistant.const import EVENT_STATE_CHANGED -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType from homeassistant.util import dt as dt_util from .common import ( @@ -25,7 +26,7 @@ async def test_purge_old_states( - hass: HomeAssistantType, async_setup_recorder_instance: SetupRecorderInstanceT + hass: HomeAssistant, async_setup_recorder_instance: SetupRecorderInstanceT ): """Test deleting old states.""" instance = await async_setup_recorder_instance(hass) @@ -57,7 +58,7 @@ async def test_purge_old_states( async def test_purge_old_states_encouters_database_corruption( - hass: HomeAssistantType, async_setup_recorder_instance: SetupRecorderInstanceT + hass: HomeAssistant, async_setup_recorder_instance: SetupRecorderInstanceT ): """Test database image image is malformed while deleting old states.""" instance = await async_setup_recorder_instance(hass) @@ -89,7 +90,7 @@ async def test_purge_old_states_encouters_database_corruption( async def test_purge_old_states_encounters_temporary_mysql_error( - hass: HomeAssistantType, + hass: HomeAssistant, async_setup_recorder_instance: SetupRecorderInstanceT, caplog, ): @@ -122,7 +123,7 @@ async def test_purge_old_states_encounters_temporary_mysql_error( async def test_purge_old_states_encounters_operational_error( - hass: HomeAssistantType, + hass: HomeAssistant, async_setup_recorder_instance: SetupRecorderInstanceT, caplog, ): @@ -150,7 +151,7 @@ async def test_purge_old_states_encounters_operational_error( async def test_purge_old_events( - hass: HomeAssistantType, async_setup_recorder_instance: SetupRecorderInstanceT + hass: HomeAssistant, async_setup_recorder_instance: SetupRecorderInstanceT ): """Test deleting old events.""" instance = await async_setup_recorder_instance(hass) @@ -173,7 +174,7 @@ async def test_purge_old_events( async def test_purge_old_recorder_runs( - hass: HomeAssistantType, async_setup_recorder_instance: SetupRecorderInstanceT + hass: HomeAssistant, async_setup_recorder_instance: SetupRecorderInstanceT ): """Test deleting old recorder runs keeps current run.""" instance = await async_setup_recorder_instance(hass) @@ -195,7 +196,7 @@ async def test_purge_old_recorder_runs( async def test_purge_method( - hass: HomeAssistantType, + hass: HomeAssistant, async_setup_recorder_instance: SetupRecorderInstanceT, caplog, ): @@ -265,12 +266,12 @@ async def test_purge_method( async def test_purge_edge_case( - hass: HomeAssistantType, + hass: HomeAssistant, async_setup_recorder_instance: SetupRecorderInstanceT, ): """Test states and events are purged even if they occurred shortly before purge_before.""" - async def _add_db_entries(hass: HomeAssistantType, timestamp: datetime) -> None: + async def _add_db_entries(hass: HomeAssistant, timestamp: datetime) -> None: with recorder.session_scope(hass=hass) as session: session.add( Events( @@ -322,7 +323,7 @@ async def _add_db_entries(hass: HomeAssistantType, timestamp: datetime) -> None: async def test_purge_filtered_states( - hass: HomeAssistantType, + hass: HomeAssistant, async_setup_recorder_instance: SetupRecorderInstanceT, ): """Test filtered states are purged.""" @@ -330,7 +331,7 @@ async def test_purge_filtered_states( instance = await async_setup_recorder_instance(hass, config) assert instance.entity_filter("sensor.excluded") is False - def _add_db_entries(hass: HomeAssistantType) -> None: + def _add_db_entries(hass: HomeAssistant) -> None: with recorder.session_scope(hass=hass) as session: # Add states and state_changed events that should be purged for days in range(1, 4): @@ -467,14 +468,14 @@ def _add_db_entries(hass: HomeAssistantType) -> None: async def test_purge_filtered_events( - hass: HomeAssistantType, + hass: HomeAssistant, async_setup_recorder_instance: SetupRecorderInstanceT, ): """Test filtered events are purged.""" config: ConfigType = {"exclude": {"event_types": ["EVENT_PURGE"]}} instance = await async_setup_recorder_instance(hass, config) - def _add_db_entries(hass: HomeAssistantType) -> None: + def _add_db_entries(hass: HomeAssistant) -> None: with recorder.session_scope(hass=hass) as session: # Add events that should be purged for days in range(1, 4): @@ -548,7 +549,7 @@ def _add_db_entries(hass: HomeAssistantType) -> None: async def test_purge_filtered_events_state_changed( - hass: HomeAssistantType, + hass: HomeAssistant, async_setup_recorder_instance: SetupRecorderInstanceT, ): """Test filtered state_changed events are purged. This should also remove all states.""" @@ -557,7 +558,7 @@ async def test_purge_filtered_events_state_changed( # Assert entity_id is NOT excluded assert instance.entity_filter("sensor.excluded") is True - def _add_db_entries(hass: HomeAssistantType) -> None: + def _add_db_entries(hass: HomeAssistant) -> None: with recorder.session_scope(hass=hass) as session: # Add states and state_changed events that should be purged for days in range(1, 4): @@ -652,7 +653,7 @@ def _add_db_entries(hass: HomeAssistantType) -> None: assert session.query(States).get(63).old_state_id == 62 # should have been kept -async def _add_test_states(hass: HomeAssistantType, instance: recorder.Recorder): +async def _add_test_states(hass: HomeAssistant, instance: recorder.Recorder): """Add multiple states to the db for testing.""" utcnow = dt_util.utcnow() five_days_ago = utcnow - timedelta(days=5) @@ -700,7 +701,7 @@ async def _add_test_states(hass: HomeAssistantType, instance: recorder.Recorder) old_state_id = state.state_id -async def _add_test_events(hass: HomeAssistantType, instance: recorder.Recorder): +async def _add_test_events(hass: HomeAssistant, instance: recorder.Recorder): """Add a few events for testing.""" utcnow = dt_util.utcnow() five_days_ago = utcnow - timedelta(days=5) @@ -733,7 +734,7 @@ async def _add_test_events(hass: HomeAssistantType, instance: recorder.Recorder) ) -async def _add_test_recorder_runs(hass: HomeAssistantType, instance: recorder.Recorder): +async def _add_test_recorder_runs(hass: HomeAssistant, instance: recorder.Recorder): """Add a few recorder_runs for testing.""" utcnow = dt_util.utcnow() five_days_ago = utcnow - timedelta(days=5) diff --git a/tests/components/roku/__init__.py b/tests/components/roku/__init__.py index 4ab2991bd43a5..e9f2d5c85bf73 100644 --- a/tests/components/roku/__init__.py +++ b/tests/components/roku/__init__.py @@ -9,7 +9,7 @@ ATTR_UPNP_SERIAL, ) from homeassistant.const import CONF_HOST, CONF_ID, CONF_NAME -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry, load_fixture from tests.test_util.aiohttp import AiohttpClientMocker @@ -162,7 +162,7 @@ def mock_connection_server_error( async def setup_integration( - hass: HomeAssistantType, + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, device: str = "roku3", app: str = "roku", diff --git a/tests/components/roku/test_config_flow.py b/tests/components/roku/test_config_flow.py index ab0072377cd61..fde46b6621c25 100644 --- a/tests/components/roku/test_config_flow.py +++ b/tests/components/roku/test_config_flow.py @@ -4,12 +4,12 @@ from homeassistant.components.roku.const import DOMAIN from homeassistant.config_entries import SOURCE_HOMEKIT, SOURCE_SSDP, SOURCE_USER from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SOURCE +from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import ( RESULT_TYPE_ABORT, RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM, ) -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.setup import async_setup_component from tests.components.roku import ( @@ -26,7 +26,7 @@ async def test_duplicate_error( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test that errors are shown when duplicates are added.""" await setup_integration(hass, aioclient_mock, skip_entry_setup=True) @@ -57,9 +57,7 @@ async def test_duplicate_error( assert result["reason"] == "already_configured" -async def test_form( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker -) -> None: +async def test_form(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) -> None: """Test the user step.""" await async_setup_component(hass, "persistent_notification", {}) mock_connection(aioclient_mock) @@ -90,7 +88,7 @@ async def test_form( async def test_form_cannot_connect( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test we handle cannot connect roku error.""" mock_connection(aioclient_mock, error=True) @@ -107,7 +105,7 @@ async def test_form_cannot_connect( assert result["errors"] == {"base": "cannot_connect"} -async def test_form_unknown_error(hass: HomeAssistantType) -> None: +async def test_form_unknown_error(hass: HomeAssistant) -> None: """Test we handle unknown error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER} @@ -130,7 +128,7 @@ async def test_form_unknown_error(hass: HomeAssistantType) -> None: async def test_homekit_cannot_connect( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test we abort homekit flow on connection error.""" mock_connection( @@ -151,7 +149,7 @@ async def test_homekit_cannot_connect( async def test_homekit_unknown_error( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test we abort homekit flow on unknown error.""" discovery_info = MOCK_HOMEKIT_DISCOVERY_INFO.copy() @@ -170,7 +168,7 @@ async def test_homekit_unknown_error( async def test_homekit_discovery( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test the homekit discovery flow.""" mock_connection(aioclient_mock, device="rokutv", host=HOMEKIT_HOST) @@ -213,7 +211,7 @@ async def test_homekit_discovery( async def test_ssdp_cannot_connect( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test we abort SSDP flow on connection error.""" mock_connection(aioclient_mock, error=True) @@ -230,7 +228,7 @@ async def test_ssdp_cannot_connect( async def test_ssdp_unknown_error( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test we abort SSDP flow on unknown error.""" discovery_info = MOCK_SSDP_DISCOVERY_INFO.copy() @@ -249,7 +247,7 @@ async def test_ssdp_unknown_error( async def test_ssdp_discovery( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test the SSDP discovery flow.""" mock_connection(aioclient_mock) diff --git a/tests/components/roku/test_init.py b/tests/components/roku/test_init.py index a5f16c6071f63..be9131d5f9156 100644 --- a/tests/components/roku/test_init.py +++ b/tests/components/roku/test_init.py @@ -7,14 +7,14 @@ ENTRY_STATE_NOT_LOADED, ENTRY_STATE_SETUP_RETRY, ) -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from tests.components.roku import setup_integration from tests.test_util.aiohttp import AiohttpClientMocker async def test_config_entry_not_ready( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test the Roku configuration entry not ready.""" entry = await setup_integration(hass, aioclient_mock, error=True) @@ -23,7 +23,7 @@ async def test_config_entry_not_ready( async def test_unload_config_entry( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test the Roku configuration entry unloading.""" with patch( diff --git a/tests/components/roku/test_media_player.py b/tests/components/roku/test_media_player.py index dca336be076d0..0964343e45343 100644 --- a/tests/components/roku/test_media_player.py +++ b/tests/components/roku/test_media_player.py @@ -61,8 +61,8 @@ STATE_STANDBY, STATE_UNAVAILABLE, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import dt as dt_util from tests.common import async_fire_time_changed @@ -80,9 +80,7 @@ TV_SW_VERSION = "9.2.0" -async def test_setup( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker -) -> None: +async def test_setup(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) -> None: """Test setup with basic config.""" await setup_integration(hass, aioclient_mock) @@ -96,7 +94,7 @@ async def test_setup( async def test_idle_setup( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test setup with idle device.""" await setup_integration(hass, aioclient_mock, power=False) @@ -106,7 +104,7 @@ async def test_idle_setup( async def test_tv_setup( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test Roku TV setup.""" await setup_integration( @@ -128,7 +126,7 @@ async def test_tv_setup( async def test_availability( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test entity availability.""" now = dt_util.utcnow() @@ -153,7 +151,7 @@ async def test_availability( async def test_supported_features( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test supported features.""" await setup_integration(hass, aioclient_mock) @@ -177,7 +175,7 @@ async def test_supported_features( async def test_tv_supported_features( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test supported features for Roku TV.""" await setup_integration( @@ -207,7 +205,7 @@ async def test_tv_supported_features( async def test_attributes( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test attributes.""" await setup_integration(hass, aioclient_mock) @@ -222,7 +220,7 @@ async def test_attributes( async def test_attributes_app( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test attributes for app.""" await setup_integration(hass, aioclient_mock, app="netflix") @@ -237,7 +235,7 @@ async def test_attributes_app( async def test_attributes_app_media_playing( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test attributes for app with playing media.""" await setup_integration(hass, aioclient_mock, app="pluto", media_state="play") @@ -254,7 +252,7 @@ async def test_attributes_app_media_playing( async def test_attributes_app_media_paused( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test attributes for app with paused media.""" await setup_integration(hass, aioclient_mock, app="pluto", media_state="pause") @@ -271,7 +269,7 @@ async def test_attributes_app_media_paused( async def test_attributes_screensaver( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test attributes for app with screensaver.""" await setup_integration(hass, aioclient_mock, app="screensaver") @@ -286,7 +284,7 @@ async def test_attributes_screensaver( async def test_tv_attributes( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test attributes for Roku TV.""" await setup_integration( @@ -310,7 +308,7 @@ async def test_tv_attributes( async def test_tv_device_registry( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test device registered for Roku TV in the device registry.""" await setup_integration( @@ -333,7 +331,7 @@ async def test_tv_device_registry( async def test_services( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test the different media player services.""" await setup_integration(hass, aioclient_mock) @@ -448,7 +446,7 @@ async def test_services( async def test_tv_services( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test the media player services related to Roku TV.""" await setup_integration( @@ -691,7 +689,7 @@ async def test_media_browse_internal(hass, aioclient_mock, hass_ws_client): async def test_integration_services( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test integration services.""" await setup_integration(hass, aioclient_mock) diff --git a/tests/components/roku/test_remote.py b/tests/components/roku/test_remote.py index 363c7134bad6a..5b1c0509e1ff9 100644 --- a/tests/components/roku/test_remote.py +++ b/tests/components/roku/test_remote.py @@ -7,8 +7,8 @@ SERVICE_SEND_COMMAND, ) from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON +from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.typing import HomeAssistantType from tests.components.roku import UPNP_SERIAL, setup_integration from tests.test_util.aiohttp import AiohttpClientMocker @@ -18,16 +18,14 @@ # pylint: disable=redefined-outer-name -async def test_setup( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker -) -> None: +async def test_setup(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) -> None: """Test setup with basic config.""" await setup_integration(hass, aioclient_mock) assert hass.states.get(MAIN_ENTITY_ID) async def test_unique_id( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test unique id.""" await setup_integration(hass, aioclient_mock) @@ -39,7 +37,7 @@ async def test_unique_id( async def test_main_services( - hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test platform services.""" await setup_integration(hass, aioclient_mock) diff --git a/tests/components/slack/test_notify.py b/tests/components/slack/test_notify.py index 6c353cf8fc679..f10673cced4d3 100644 --- a/tests/components/slack/test_notify.py +++ b/tests/components/slack/test_notify.py @@ -22,7 +22,7 @@ CONF_PLATFORM, CONF_USERNAME, ) -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component MODULE_PATH = "homeassistant.components.slack.notify" @@ -47,7 +47,7 @@ def filter_log_records(caplog: LogCaptureFixture) -> list[logging.LogRecord]: ] -async def test_setup(hass: HomeAssistantType, caplog: LogCaptureFixture): +async def test_setup(hass: HomeAssistant, caplog: LogCaptureFixture): """Test setup slack notify.""" config = DEFAULT_CONFIG @@ -68,7 +68,7 @@ async def test_setup(hass: HomeAssistantType, caplog: LogCaptureFixture): client.auth_test.assert_called_once_with() -async def test_setup_clientError(hass: HomeAssistantType, caplog: LogCaptureFixture): +async def test_setup_clientError(hass: HomeAssistant, caplog: LogCaptureFixture): """Test setup slack notify with aiohttp.ClientError exception.""" config = copy.deepcopy(DEFAULT_CONFIG) config[notify.DOMAIN][0].update({CONF_USERNAME: "user", CONF_ICON: "icon"}) @@ -89,7 +89,7 @@ async def test_setup_clientError(hass: HomeAssistantType, caplog: LogCaptureFixt assert aiohttp.ClientError.__qualname__ in record.message -async def test_setup_slackApiError(hass: HomeAssistantType, caplog: LogCaptureFixture): +async def test_setup_slackApiError(hass: HomeAssistant, caplog: LogCaptureFixture): """Test setup slack notify with SlackApiError exception.""" config = DEFAULT_CONFIG From 34b258e8129e9407011f6cd6209394bb14ef9ca8 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 22 Apr 2021 20:23:19 +0200 Subject: [PATCH 0451/1317] =?UTF-8?q?Rename=20HomeAssistantType=20?= =?UTF-8?q?=E2=80=94>=20HomeAssistant=20for=20integrations=20n*=20-=20p*?= =?UTF-8?q?=20(#49559)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- homeassistant/components/nest/binary_sensor.py | 4 ++-- homeassistant/components/nest/camera.py | 4 ++-- homeassistant/components/nest/camera_sdm.py | 4 ++-- homeassistant/components/nest/climate.py | 4 ++-- homeassistant/components/nest/climate_sdm.py | 4 ++-- homeassistant/components/nest/sensor.py | 4 ++-- homeassistant/components/nest/sensor_sdm.py | 4 ++-- .../nsw_rural_fire_service_feed/geo_location.py | 6 +++--- homeassistant/components/number/__init__.py | 9 +++++---- homeassistant/components/number/reproduce_state.py | 7 +++---- homeassistant/components/nws/weather.py | 6 +++--- homeassistant/components/nzbget/__init__.py | 12 ++++++------ homeassistant/components/nzbget/config_flow.py | 6 +++--- homeassistant/components/nzbget/coordinator.py | 4 ++-- homeassistant/components/nzbget/sensor.py | 4 ++-- homeassistant/components/nzbget/switch.py | 4 ++-- homeassistant/components/onewire/__init__.py | 6 +++--- homeassistant/components/onewire/config_flow.py | 8 ++++---- homeassistant/components/onewire/onewirehub.py | 4 ++-- homeassistant/components/ovo_energy/__init__.py | 7 ++++--- homeassistant/components/ovo_energy/sensor.py | 4 ++-- homeassistant/components/person/__init__.py | 8 ++++---- homeassistant/components/person/group.py | 5 ++--- homeassistant/components/philips_js/__init__.py | 3 +-- homeassistant/components/philips_js/media_player.py | 5 ++--- homeassistant/components/plant/group.py | 5 ++--- homeassistant/components/point/__init__.py | 10 +++++----- 27 files changed, 74 insertions(+), 77 deletions(-) diff --git a/homeassistant/components/nest/binary_sensor.py b/homeassistant/components/nest/binary_sensor.py index d49ec8535cc96..0bf65f2163cca 100644 --- a/homeassistant/components/nest/binary_sensor.py +++ b/homeassistant/components/nest/binary_sensor.py @@ -1,14 +1,14 @@ """Support for Nest binary sensors that dispatches between API versions.""" from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .const import DATA_SDM from .legacy.binary_sensor import async_setup_legacy_entry async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up the binary sensors.""" assert DATA_SDM not in entry.data diff --git a/homeassistant/components/nest/camera.py b/homeassistant/components/nest/camera.py index f0e0b8e05fa11..ca117f0cbf17a 100644 --- a/homeassistant/components/nest/camera.py +++ b/homeassistant/components/nest/camera.py @@ -1,7 +1,7 @@ """Support for Nest cameras that dispatches between API versions.""" from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .camera_sdm import async_setup_sdm_entry from .const import DATA_SDM @@ -9,7 +9,7 @@ async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up the cameras.""" if DATA_SDM not in entry.data: diff --git a/homeassistant/components/nest/camera_sdm.py b/homeassistant/components/nest/camera_sdm.py index ce6ff897a2fac..66568907aa0f4 100644 --- a/homeassistant/components/nest/camera_sdm.py +++ b/homeassistant/components/nest/camera_sdm.py @@ -16,9 +16,9 @@ from homeassistant.components.camera import SUPPORT_STREAM, Camera from homeassistant.components.ffmpeg import async_get_image from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.event import async_track_point_in_utc_time -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util.dt import utcnow from .const import DATA_SUBSCRIBER, DOMAIN @@ -31,7 +31,7 @@ async def async_setup_sdm_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up the cameras.""" diff --git a/homeassistant/components/nest/climate.py b/homeassistant/components/nest/climate.py index a74a50b0f3615..1644cc46004b7 100644 --- a/homeassistant/components/nest/climate.py +++ b/homeassistant/components/nest/climate.py @@ -1,7 +1,7 @@ """Support for Nest climate that dispatches between API versions.""" from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .climate_sdm import async_setup_sdm_entry from .const import DATA_SDM @@ -9,7 +9,7 @@ async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up the climate platform.""" if DATA_SDM not in entry.data: diff --git a/homeassistant/components/nest/climate_sdm.py b/homeassistant/components/nest/climate_sdm.py index e02ebcd2dee58..a90fa06ce1fe1 100644 --- a/homeassistant/components/nest/climate_sdm.py +++ b/homeassistant/components/nest/climate_sdm.py @@ -35,8 +35,8 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS +from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.typing import HomeAssistantType from .const import DATA_SUBSCRIBER, DOMAIN from .device_info import DeviceInfo @@ -78,7 +78,7 @@ async def async_setup_sdm_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up the client entities.""" diff --git a/homeassistant/components/nest/sensor.py b/homeassistant/components/nest/sensor.py index 0dcc89e2262cc..c58ad26112d89 100644 --- a/homeassistant/components/nest/sensor.py +++ b/homeassistant/components/nest/sensor.py @@ -1,7 +1,7 @@ """Support for Nest sensors that dispatches between API versions.""" from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .const import DATA_SDM from .legacy.sensor import async_setup_legacy_entry @@ -9,7 +9,7 @@ async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up the sensors.""" if DATA_SDM not in entry.data: diff --git a/homeassistant/components/nest/sensor_sdm.py b/homeassistant/components/nest/sensor_sdm.py index 06e2b68d7cf60..b70d6cd5c57c3 100644 --- a/homeassistant/components/nest/sensor_sdm.py +++ b/homeassistant/components/nest/sensor_sdm.py @@ -15,8 +15,8 @@ PERCENTAGE, TEMP_CELSIUS, ) +from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.typing import HomeAssistantType from .const import DATA_SUBSCRIBER, DOMAIN from .device_info import DeviceInfo @@ -33,7 +33,7 @@ async def async_setup_sdm_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up the sensors.""" diff --git a/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py b/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py index 08e62e6c6a376..8df3520e24229 100644 --- a/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py +++ b/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py @@ -19,14 +19,14 @@ EVENT_HOMEASSISTANT_STOP, LENGTH_KILOMETERS, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) @@ -66,7 +66,7 @@ async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None ): """Set up the NSW Rural Fire Service Feed platform.""" scan_interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index e61398f6582f3..046895ac29c57 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -9,13 +9,14 @@ import voluptuous as vol from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from .const import ( ATTR_MAX, @@ -38,7 +39,7 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up Number entities.""" component = hass.data[DOMAIN] = EntityComponent( _LOGGER, DOMAIN, hass, SCAN_INTERVAL @@ -54,12 +55,12 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: return True -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" return await hass.data[DOMAIN].async_setup_entry(entry) # type: ignore -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" return await hass.data[DOMAIN].async_unload_entry(entry) # type: ignore diff --git a/homeassistant/components/number/reproduce_state.py b/homeassistant/components/number/reproduce_state.py index d628db825ca1d..dbf4af1f860e7 100644 --- a/homeassistant/components/number/reproduce_state.py +++ b/homeassistant/components/number/reproduce_state.py @@ -7,8 +7,7 @@ from typing import Any from homeassistant.const import ATTR_ENTITY_ID -from homeassistant.core import Context, State -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import Context, HomeAssistant, State from . import ATTR_VALUE, DOMAIN, SERVICE_SET_VALUE @@ -16,7 +15,7 @@ async def _async_reproduce_state( - hass: HomeAssistantType, + hass: HomeAssistant, state: State, *, context: Context | None = None, @@ -50,7 +49,7 @@ async def _async_reproduce_state( async def async_reproduce_states( - hass: HomeAssistantType, + hass: HomeAssistant, states: Iterable[State], *, context: Context | None = None, diff --git a/homeassistant/components/nws/weather.py b/homeassistant/components/nws/weather.py index c84d1b78ea2b7..a8f3e55c2700c 100644 --- a/homeassistant/components/nws/weather.py +++ b/homeassistant/components/nws/weather.py @@ -22,8 +22,8 @@ TEMP_CELSIUS, TEMP_FAHRENHEIT, ) -from homeassistant.core import callback -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.typing import ConfigType from homeassistant.util.distance import convert as convert_distance from homeassistant.util.dt import utcnow from homeassistant.util.pressure import convert as convert_pressure @@ -78,7 +78,7 @@ def convert_condition(time, weather): async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigType, async_add_entities + hass: HomeAssistant, entry: ConfigType, async_add_entities ) -> None: """Set up the NWS weather platform.""" hass_data = hass.data[DOMAIN][entry.entry_id] diff --git a/homeassistant/components/nzbget/__init__.py b/homeassistant/components/nzbget/__init__.py index 48abe597f5aeb..3e85839e5d7ba 100644 --- a/homeassistant/components/nzbget/__init__.py +++ b/homeassistant/components/nzbget/__init__.py @@ -13,8 +13,8 @@ CONF_SSL, CONF_USERNAME, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( @@ -59,7 +59,7 @@ ) -async def async_setup(hass: HomeAssistantType, config: dict) -> bool: +async def async_setup(hass: HomeAssistant, config: dict) -> bool: """Set up the NZBGet integration.""" hass.data.setdefault(DOMAIN, {}) @@ -78,7 +78,7 @@ async def async_setup(hass: HomeAssistantType, config: dict) -> bool: return True -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up NZBGet from a config entry.""" if not entry.options: options = { @@ -113,7 +113,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool return True -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = all( await asyncio.gather( @@ -132,7 +132,7 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> boo def _async_register_services( - hass: HomeAssistantType, + hass: HomeAssistant, coordinator: NZBGetDataUpdateCoordinator, ) -> None: """Register integration-level services.""" @@ -156,7 +156,7 @@ def set_speed(call) -> None: ) -async def _async_update_listener(hass: HomeAssistantType, entry: ConfigEntry) -> None: +async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: """Handle options update.""" await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/nzbget/config_flow.py b/homeassistant/components/nzbget/config_flow.py index a5b24ad6dfef7..980fbc1b2f9f8 100644 --- a/homeassistant/components/nzbget/config_flow.py +++ b/homeassistant/components/nzbget/config_flow.py @@ -17,8 +17,8 @@ CONF_USERNAME, CONF_VERIFY_SSL, ) -from homeassistant.core import callback -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.typing import ConfigType from .const import ( DEFAULT_NAME, @@ -33,7 +33,7 @@ _LOGGER = logging.getLogger(__name__) -def validate_input(hass: HomeAssistantType, data: dict) -> dict[str, Any]: +def validate_input(hass: HomeAssistant, data: dict) -> dict[str, Any]: """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. diff --git a/homeassistant/components/nzbget/coordinator.py b/homeassistant/components/nzbget/coordinator.py index 9a76d802bdd3a..57e0b9fc3956f 100644 --- a/homeassistant/components/nzbget/coordinator.py +++ b/homeassistant/components/nzbget/coordinator.py @@ -14,7 +14,7 @@ CONF_USERNAME, CONF_VERIFY_SSL, ) -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN @@ -25,7 +25,7 @@ class NZBGetDataUpdateCoordinator(DataUpdateCoordinator): """Class to manage fetching NZBGet data.""" - def __init__(self, hass: HomeAssistantType, *, config: dict, options: dict): + def __init__(self, hass: HomeAssistant, *, config: dict, options: dict): """Initialize global NZBGet data updater.""" self.nzbget = NZBGetAPI( config[CONF_HOST], diff --git a/homeassistant/components/nzbget/sensor.py b/homeassistant/components/nzbget/sensor.py index 54a88c89f5311..6ddac8b977e79 100644 --- a/homeassistant/components/nzbget/sensor.py +++ b/homeassistant/components/nzbget/sensor.py @@ -13,8 +13,8 @@ DATA_RATE_MEGABYTES_PER_SECOND, DEVICE_CLASS_TIMESTAMP, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util.dt import utcnow from . import NZBGetEntity @@ -42,7 +42,7 @@ async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable[[list[Entity], bool], None], ) -> None: diff --git a/homeassistant/components/nzbget/switch.py b/homeassistant/components/nzbget/switch.py index 4f0eae17c23dc..811f3233bb796 100644 --- a/homeassistant/components/nzbget/switch.py +++ b/homeassistant/components/nzbget/switch.py @@ -6,8 +6,8 @@ from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import HomeAssistantType from . import NZBGetEntity from .const import DATA_COORDINATOR, DOMAIN @@ -15,7 +15,7 @@ async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable[[list[Entity], bool], None], ) -> None: diff --git a/homeassistant/components/onewire/__init__.py b/homeassistant/components/onewire/__init__.py index 848cfc9086dc5..cd6d594fafb49 100644 --- a/homeassistant/components/onewire/__init__.py +++ b/homeassistant/components/onewire/__init__.py @@ -3,9 +3,9 @@ import logging from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr, entity_registry as er -from homeassistant.helpers.typing import HomeAssistantType from .const import DOMAIN, PLATFORMS from .onewirehub import CannotConnect, OneWireHub @@ -13,7 +13,7 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): """Set up a 1-Wire proxy for a config entry.""" hass.data.setdefault(DOMAIN, {}) @@ -65,7 +65,7 @@ async def start_platforms() -> None: return True -async def async_unload_entry(hass: HomeAssistantType, config_entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry): """Unload a config entry.""" unload_ok = all( await asyncio.gather( diff --git a/homeassistant/components/onewire/config_flow.py b/homeassistant/components/onewire/config_flow.py index fbb1d5debefeb..bcf30e17fe460 100644 --- a/homeassistant/components/onewire/config_flow.py +++ b/homeassistant/components/onewire/config_flow.py @@ -3,7 +3,7 @@ from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL, ConfigFlow from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .const import ( CONF_MOUNT_DIR, @@ -33,7 +33,7 @@ ) -async def validate_input_owserver(hass: HomeAssistantType, data): +async def validate_input_owserver(hass: HomeAssistant, data): """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA_OWSERVER with values provided by the user. @@ -50,7 +50,7 @@ async def validate_input_owserver(hass: HomeAssistantType, data): return {"title": host} -def is_duplicate_owserver_entry(hass: HomeAssistantType, user_input): +def is_duplicate_owserver_entry(hass: HomeAssistant, user_input): """Check existing entries for matching host and port.""" for config_entry in hass.config_entries.async_entries(DOMAIN): if ( @@ -62,7 +62,7 @@ def is_duplicate_owserver_entry(hass: HomeAssistantType, user_input): return False -async def validate_input_mount_dir(hass: HomeAssistantType, data): +async def validate_input_mount_dir(hass: HomeAssistant, data): """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA_MOUNTDIR with values provided by the user. diff --git a/homeassistant/components/onewire/onewirehub.py b/homeassistant/components/onewire/onewirehub.py index 09a3235377dcf..5f9e3bfff773f 100644 --- a/homeassistant/components/onewire/onewirehub.py +++ b/homeassistant/components/onewire/onewirehub.py @@ -6,8 +6,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE +from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.typing import HomeAssistantType from .const import CONF_MOUNT_DIR, CONF_TYPE_OWSERVER, CONF_TYPE_SYSBUS @@ -20,7 +20,7 @@ class OneWireHub: """Hub to communicate with SysBus or OWServer.""" - def __init__(self, hass: HomeAssistantType): + def __init__(self, hass: HomeAssistant): """Initialize.""" self.hass = hass self.type: str = None diff --git a/homeassistant/components/ovo_energy/__init__.py b/homeassistant/components/ovo_energy/__init__.py index 84e1182b38150..749f7b7e249f3 100644 --- a/homeassistant/components/ovo_energy/__init__.py +++ b/homeassistant/components/ovo_energy/__init__.py @@ -12,8 +12,9 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -25,7 +26,7 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up OVO Energy from a config entry.""" client = OVOEnergy() @@ -81,7 +82,7 @@ async def async_update_data() -> OVODailyUsage: return True -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigType) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigType) -> bool: """Unload OVO Energy config entry.""" # Unload sensors await hass.config_entries.async_forward_entry_unload(entry, "sensor") diff --git a/homeassistant/components/ovo_energy/sensor.py b/homeassistant/components/ovo_energy/sensor.py index d03f7c49f96aa..adc62906e651c 100644 --- a/homeassistant/components/ovo_energy/sensor.py +++ b/homeassistant/components/ovo_energy/sensor.py @@ -6,7 +6,7 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import OVOEnergyDeviceEntity @@ -17,7 +17,7 @@ async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up OVO Energy sensor based on a config entry.""" coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index 1eb9d4eda7a40..86f50367fd46d 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -49,7 +49,7 @@ from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.storage import Store -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass _LOGGER = logging.getLogger(__name__) @@ -259,7 +259,7 @@ async def _validate_user_id(self, user_id): raise ValueError("User already taken") -async def filter_yaml_data(hass: HomeAssistantType, persons: list[dict]) -> list[dict]: +async def filter_yaml_data(hass: HomeAssistant, persons: list[dict]) -> list[dict]: """Validate YAML data that we can't validate via schema.""" filtered = [] person_invalid_user = [] @@ -293,7 +293,7 @@ async def filter_yaml_data(hass: HomeAssistantType, persons: list[dict]) -> list return filtered -async def async_setup(hass: HomeAssistantType, config: ConfigType): +async def async_setup(hass: HomeAssistant, config: ConfigType): """Set up the person component.""" entity_component = EntityComponent(_LOGGER, DOMAIN, hass) id_manager = collection.IDManager() @@ -514,7 +514,7 @@ def _parse_source_state(self, state): @websocket_api.websocket_command({vol.Required(CONF_TYPE): "person/list"}) def ws_list_person( - hass: HomeAssistantType, connection: websocket_api.ActiveConnection, msg + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg ): """List persons.""" yaml, storage = hass.data[DOMAIN] diff --git a/homeassistant/components/person/group.py b/homeassistant/components/person/group.py index 07ec2cfe985ed..9bd2c991678c3 100644 --- a/homeassistant/components/person/group.py +++ b/homeassistant/components/person/group.py @@ -3,13 +3,12 @@ from homeassistant.components.group import GroupIntegrationRegistry from homeassistant.const import STATE_HOME, STATE_NOT_HOME -from homeassistant.core import callback -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant, callback @callback def async_describe_on_off_states( - hass: HomeAssistantType, registry: GroupIntegrationRegistry + hass: HomeAssistant, registry: GroupIntegrationRegistry ) -> None: """Describe group on off states.""" registry.on_off_states({STATE_HOME}, STATE_NOT_HOME) diff --git a/homeassistant/components/philips_js/__init__.py b/homeassistant/components/philips_js/__init__.py index 836c5392f9fd5..bf17284d77705 100644 --- a/homeassistant/components/philips_js/__init__.py +++ b/homeassistant/components/philips_js/__init__.py @@ -18,7 +18,6 @@ ) from homeassistant.core import CALLBACK_TYPE, Context, HassJob, HomeAssistant, callback from homeassistant.helpers.debounce import Debouncer -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN @@ -96,7 +95,7 @@ def _remove(): return _remove - async def async_run(self, hass: HomeAssistantType, context: Context | None = None): + async def async_run(self, hass: HomeAssistant, context: Context | None = None): """Run all turn on triggers.""" for job, variables in self._actions.values(): hass.async_run_hass_job(job, variables, context) diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index 7376d34e308af..60862d8ededbd 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -43,9 +43,8 @@ STATE_OFF, STATE_ON, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import LOGGER as _LOGGER, PhilipsTVDataUpdateCoordinator @@ -104,7 +103,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, config_entry: config_entries.ConfigEntry, async_add_entities, ): diff --git a/homeassistant/components/plant/group.py b/homeassistant/components/plant/group.py index 5d6edfa2b9ab3..90e894abb0f1d 100644 --- a/homeassistant/components/plant/group.py +++ b/homeassistant/components/plant/group.py @@ -3,13 +3,12 @@ from homeassistant.components.group import GroupIntegrationRegistry from homeassistant.const import STATE_OK, STATE_PROBLEM -from homeassistant.core import callback -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant, callback @callback def async_describe_on_off_states( - hass: HomeAssistantType, registry: GroupIntegrationRegistry + hass: HomeAssistant, registry: GroupIntegrationRegistry ) -> None: """Describe group on off states.""" registry.on_off_states({STATE_PROBLEM}, STATE_OK) diff --git a/homeassistant/components/point/__init__.py b/homeassistant/components/point/__init__.py index e5c209004de14..38561d42abc3f 100644 --- a/homeassistant/components/point/__init__.py +++ b/homeassistant/components/point/__init__.py @@ -13,6 +13,7 @@ CONF_TOKEN, CONF_WEBHOOK_ID, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, @@ -20,7 +21,6 @@ ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util.dt import as_local, parse_datetime, utc_from_timestamp from . import config_flow @@ -74,7 +74,7 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Point from a config entry.""" async def token_saver(token, **kwargs): @@ -107,7 +107,7 @@ async def token_saver(token, **kwargs): return True -async def async_setup_webhook(hass: HomeAssistantType, entry: ConfigEntry, session): +async def async_setup_webhook(hass: HomeAssistant, entry: ConfigEntry, session): """Set up a webhook to handle binary sensor events.""" if CONF_WEBHOOK_ID not in entry.data: webhook_id = hass.components.webhook.async_generate_id() @@ -133,7 +133,7 @@ async def async_setup_webhook(hass: HomeAssistantType, entry: ConfigEntry, sessi ) -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID]) session = hass.data[DOMAIN].pop(entry.entry_id) @@ -165,7 +165,7 @@ async def handle_webhook(hass, webhook_id, request): class MinutPointClient: """Get the latest data and update the states.""" - def __init__(self, hass: HomeAssistantType, config_entry: ConfigEntry, session): + def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry, session): """Initialize the Minut data object.""" self._known_devices = set() self._known_homes = set() From 77372d9094c48cf0f0c67b6631444d0665564ee6 Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Thu, 22 Apr 2021 20:38:56 +0200 Subject: [PATCH 0452/1317] Add zeroconf detection to devolo Home Control (#47934) Co-authored-by: Markus Bong <2Fake1987@gmail.com> --- .../devolo_home_control/__init__.py | 14 ++- .../devolo_home_control/config_flow.py | 54 +++++++---- .../components/devolo_home_control/const.py | 1 + .../devolo_home_control/manifest.json | 3 +- .../devolo_home_control/strings.json | 11 ++- .../devolo_home_control/translations/en.json | 8 +- homeassistant/generated/zeroconf.py | 5 + tests/components/devolo_home_control/const.py | 22 +++++ .../devolo_home_control/test_config_flow.py | 96 ++++++++++++++----- 9 files changed, 161 insertions(+), 53 deletions(-) create mode 100644 tests/components/devolo_home_control/const.py diff --git a/homeassistant/components/devolo_home_control/__init__.py b/homeassistant/components/devolo_home_control/__init__.py index e9620f1955107..a6918e819987d 100644 --- a/homeassistant/components/devolo_home_control/__init__.py +++ b/homeassistant/components/devolo_home_control/__init__.py @@ -12,14 +12,20 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from .const import CONF_MYDEVOLO, DOMAIN, GATEWAY_SERIAL_PATTERN, PLATFORMS +from .const import ( + CONF_MYDEVOLO, + DEFAULT_MYDEVOLO, + DOMAIN, + GATEWAY_SERIAL_PATTERN, + PLATFORMS, +) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up the devolo account from a config entry.""" hass.data.setdefault(DOMAIN, {}) - mydevolo = _mydevolo(entry.data) + mydevolo = configure_mydevolo(entry.data) credentials_valid = await hass.async_add_executor_job(mydevolo.credentials_valid) @@ -92,10 +98,10 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload -def _mydevolo(conf: dict) -> Mydevolo: +def configure_mydevolo(conf: dict) -> Mydevolo: """Configure mydevolo.""" mydevolo = Mydevolo() mydevolo.user = conf[CONF_USERNAME] mydevolo.password = conf[CONF_PASSWORD] - mydevolo.url = conf[CONF_MYDEVOLO] + mydevolo.url = conf.get(CONF_MYDEVOLO, DEFAULT_MYDEVOLO) return mydevolo diff --git a/homeassistant/components/devolo_home_control/config_flow.py b/homeassistant/components/devolo_home_control/config_flow.py index d6dbd331d5fec..43bacfed6390a 100644 --- a/homeassistant/components/devolo_home_control/config_flow.py +++ b/homeassistant/components/devolo_home_control/config_flow.py @@ -1,14 +1,20 @@ """Config flow to configure the devolo home control integration.""" import logging -from devolo_home_control_api.mydevolo import Mydevolo import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback +from homeassistant.helpers.typing import DiscoveryInfoType -from .const import CONF_MYDEVOLO, DEFAULT_MYDEVOLO, DOMAIN +from . import configure_mydevolo +from .const import ( # pylint:disable=unused-import + CONF_MYDEVOLO, + DEFAULT_MYDEVOLO, + DOMAIN, + SUPPORTED_MODEL_TYPES, +) _LOGGER = logging.getLogger(__name__) @@ -29,22 +35,30 @@ def __init__(self): async def async_step_user(self, user_input=None): """Handle a flow initiated by the user.""" if self.show_advanced_options: - self.data_schema = { - vol.Required(CONF_USERNAME): str, - vol.Required(CONF_PASSWORD): str, - vol.Required(CONF_MYDEVOLO, default=DEFAULT_MYDEVOLO): str, - } + self.data_schema[ + vol.Required(CONF_MYDEVOLO, default=DEFAULT_MYDEVOLO) + ] = str if user_input is None: return self._show_form(user_input) - user = user_input[CONF_USERNAME] - password = user_input[CONF_PASSWORD] - mydevolo = Mydevolo() - mydevolo.user = user - mydevolo.password = password - if self.show_advanced_options: - mydevolo.url = user_input[CONF_MYDEVOLO] - else: - mydevolo.url = DEFAULT_MYDEVOLO + return await self._connect_mydevolo(user_input) + + async def async_step_zeroconf(self, discovery_info: DiscoveryInfoType): + """Handle zeroconf discovery.""" + # Check if it is a gateway + if discovery_info.get("properties", {}).get("MT") in SUPPORTED_MODEL_TYPES: + await self._async_handle_discovery_without_unique_id() + return await self.async_step_zeroconf_confirm() + return self.async_abort(reason="Not a devolo Home Control gateway.") + + async def async_step_zeroconf_confirm(self, user_input=None): + """Handle a flow initiated by zeroconf.""" + if user_input is None: + return self._show_form(step_id="zeroconf_confirm") + return await self._connect_mydevolo(user_input) + + async def _connect_mydevolo(self, user_input): + """Connect to mydevolo.""" + mydevolo = configure_mydevolo(conf=user_input) credentials_valid = await self.hass.async_add_executor_job( mydevolo.credentials_valid ) @@ -58,17 +72,17 @@ async def async_step_user(self, user_input=None): return self.async_create_entry( title="devolo Home Control", data={ - CONF_PASSWORD: password, - CONF_USERNAME: user, + CONF_PASSWORD: mydevolo.password, + CONF_USERNAME: mydevolo.user, CONF_MYDEVOLO: mydevolo.url, }, ) @callback - def _show_form(self, errors=None): + def _show_form(self, errors=None, step_id="user"): """Show the form to the user.""" return self.async_show_form( - step_id="user", + step_id=step_id, data_schema=vol.Schema(self.data_schema), errors=errors if errors else {}, ) diff --git a/homeassistant/components/devolo_home_control/const.py b/homeassistant/components/devolo_home_control/const.py index 3a7d26435ff7c..b15c0acf62292 100644 --- a/homeassistant/components/devolo_home_control/const.py +++ b/homeassistant/components/devolo_home_control/const.py @@ -6,3 +6,4 @@ PLATFORMS = ["binary_sensor", "climate", "cover", "light", "sensor", "switch"] CONF_MYDEVOLO = "mydevolo_url" GATEWAY_SERIAL_PATTERN = re.compile(r"\d{16}") +SUPPORTED_MODEL_TYPES = ["2600", "2601"] diff --git a/homeassistant/components/devolo_home_control/manifest.json b/homeassistant/components/devolo_home_control/manifest.json index 832eb8025bc83..5886c1d0fe258 100644 --- a/homeassistant/components/devolo_home_control/manifest.json +++ b/homeassistant/components/devolo_home_control/manifest.json @@ -7,5 +7,6 @@ "config_flow": true, "codeowners": ["@2Fake", "@Shutgun"], "quality_scale": "silver", - "iot_class": "local_push" + "iot_class": "local_push", + "zeroconf": ["_dvl-deviceapi._tcp.local."] } diff --git a/homeassistant/components/devolo_home_control/strings.json b/homeassistant/components/devolo_home_control/strings.json index 7624beb531cd8..cbc911fcd18e6 100644 --- a/homeassistant/components/devolo_home_control/strings.json +++ b/homeassistant/components/devolo_home_control/strings.json @@ -11,10 +11,17 @@ "data": { "username": "[%key:common::config_flow::data::email%] / devolo ID", "password": "[%key:common::config_flow::data::password%]", - "mydevolo_url": "mydevolo [%key:common::config_flow::data::url%]", - "home_control_url": "Home Control [%key:common::config_flow::data::url%]" + "mydevolo_url": "mydevolo [%key:common::config_flow::data::url%]" + } + }, + "zeroconf_confirm": { + "data": { + "username": "[%key:common::config_flow::data::email%] / devolo ID", + "password": "[%key:common::config_flow::data::password%]", + "mydevolo_url": "mydevolo [%key:common::config_flow::data::url%]" } } } } } + diff --git a/homeassistant/components/devolo_home_control/translations/en.json b/homeassistant/components/devolo_home_control/translations/en.json index 10485c94b6f39..d1b8645072f1c 100644 --- a/homeassistant/components/devolo_home_control/translations/en.json +++ b/homeassistant/components/devolo_home_control/translations/en.json @@ -9,7 +9,13 @@ "step": { "user": { "data": { - "home_control_url": "Home Control URL", + "mydevolo_url": "mydevolo URL", + "password": "Password", + "username": "Email / devolo ID" + } + }, + "zeroconf_confirm": { + "data": { "mydevolo_url": "mydevolo URL", "password": "Password", "username": "Email / devolo ID" diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index f1485bc6e8735..4c017b07628a3 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -49,6 +49,11 @@ "domain": "daikin" } ], + "_dvl-deviceapi._tcp.local.": [ + { + "domain": "devolo_home_control" + } + ], "_elg._tcp.local.": [ { "domain": "elgato" diff --git a/tests/components/devolo_home_control/const.py b/tests/components/devolo_home_control/const.py new file mode 100644 index 0000000000000..33a98a15e2dae --- /dev/null +++ b/tests/components/devolo_home_control/const.py @@ -0,0 +1,22 @@ +"""Constants used for mocking data.""" + +DISCOVERY_INFO = { + "host": "192.168.0.1", + "port": 14791, + "hostname": "test.local.", + "type": "_dvl-deviceapi._tcp.local.", + "name": "dvl-deviceapi", + "properties": { + "Path": "/deviceapi", + "Version": "v0", + "Features": "", + "MT": "2600", + "SN": "1234567890", + "FirmwareVersion": "8.90.4", + "PlcMacAddress": "AA:BB:CC:DD:EE:FF", + }, +} + +DISCOVERY_INFO_WRONG_DEVOLO_DEVICE = {"properties": {"MT": "2700"}} + +DISCOVERY_INFO_WRONG_DEVICE = {"properties": {"Features": ""}} diff --git a/tests/components/devolo_home_control/test_config_flow.py b/tests/components/devolo_home_control/test_config_flow.py index 370d86c7c9409..0b02cb9f4a17c 100644 --- a/tests/components/devolo_home_control/test_config_flow.py +++ b/tests/components/devolo_home_control/test_config_flow.py @@ -4,9 +4,15 @@ import pytest from homeassistant import config_entries, data_entry_flow, setup -from homeassistant.components.devolo_home_control.const import DOMAIN +from homeassistant.components.devolo_home_control.const import DEFAULT_MYDEVOLO, DOMAIN from homeassistant.config_entries import SOURCE_USER +from .const import ( + DISCOVERY_INFO, + DISCOVERY_INFO_WRONG_DEVICE, + DISCOVERY_INFO_WRONG_DEVOLO_DEVICE, +) + from tests.common import MockConfigEntry @@ -19,28 +25,7 @@ async def test_form(hass): assert result["type"] == "form" assert result["errors"] == {} - with patch( - "homeassistant.components.devolo_home_control.async_setup_entry", - return_value=True, - ) as mock_setup_entry, patch( - "homeassistant.components.devolo_home_control.config_flow.Mydevolo.uuid", - return_value="123456", - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"username": "test-username", "password": "test-password"}, - ) - await hass.async_block_till_done() - - assert result2["type"] == "create_entry" - assert result2["title"] == "devolo Home Control" - assert result2["data"] == { - "username": "test-username", - "password": "test-password", - "mydevolo_url": "https://www.mydevolo.com", - } - - assert len(mock_setup_entry.mock_calls) == 1 + await _setup(hass, result) @pytest.mark.credentials_invalid @@ -64,7 +49,7 @@ async def test_form_invalid_credentials(hass): async def test_form_already_configured(hass): """Test if we get the error message on already configured.""" with patch( - "homeassistant.components.devolo_home_control.config_flow.Mydevolo.uuid", + "homeassistant.components.devolo_home_control.Mydevolo.uuid", return_value="123456", ): MockConfigEntry(domain=DOMAIN, unique_id="123456", data={}).add_to_hass(hass) @@ -89,7 +74,7 @@ async def test_form_advanced_options(hass): "homeassistant.components.devolo_home_control.async_setup_entry", return_value=True, ) as mock_setup_entry, patch( - "homeassistant.components.devolo_home_control.config_flow.Mydevolo.uuid", + "homeassistant.components.devolo_home_control.Mydevolo.uuid", return_value="123456", ): result2 = await hass.config_entries.flow.async_configure( @@ -111,3 +96,64 @@ async def test_form_advanced_options(hass): } assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_show_zeroconf_form(hass): + """Test that the zeroconf confirmation form is served.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=DISCOVERY_INFO, + ) + + assert result["step_id"] == "zeroconf_confirm" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + await _setup(hass, result) + + +async def test_zeroconf_wrong_device(hass): + """Test that the zeroconf ignores wrong devices.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=DISCOVERY_INFO_WRONG_DEVOLO_DEVICE, + ) + + assert result["reason"] == "Not a devolo Home Control gateway." + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=DISCOVERY_INFO_WRONG_DEVICE, + ) + + assert result["reason"] == "Not a devolo Home Control gateway." + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + + +async def _setup(hass, result): + """Finish configuration steps.""" + with patch( + "homeassistant.components.devolo_home_control.async_setup_entry", + return_value=True, + ) as mock_setup_entry, patch( + "homeassistant.components.devolo_home_control.Mydevolo.uuid", + return_value="123456", + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"username": "test-username", "password": "test-password"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "devolo Home Control" + assert result2["data"] == { + "username": "test-username", + "password": "test-password", + "mydevolo_url": DEFAULT_MYDEVOLO, + } + + assert len(mock_setup_entry.mock_calls) == 1 From c3d9aaa896327b8a1f8a5f6ff255b101261bfe4e Mon Sep 17 00:00:00 2001 From: tkdrob Date: Thu, 22 Apr 2021 14:41:43 -0400 Subject: [PATCH 0453/1317] Clean plex services.yaml (#49535) --- homeassistant/components/plex/services.yaml | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/homeassistant/components/plex/services.yaml b/homeassistant/components/plex/services.yaml index 366acb43a5bb0..5412a4180e637 100644 --- a/homeassistant/components/plex/services.yaml +++ b/homeassistant/components/plex/services.yaml @@ -1,17 +1,3 @@ -play_on_sonos: - description: Play music hosted on a Plex server on a linked Sonos speaker. - fields: - entity_id: - description: Entity ID of a media_player from the Sonos integration. - example: "media_player.sonos_living_room" - media_content_id: - description: The ID of the content to play. See https://www.home-assistant.io/integrations/plex/#music for details. - example: >- - '{ "library_name": "Music", "artist_name": "Stevie Wonder" }' - media_content_type: - description: The type of content to play. Must be "music". - example: "music" - refresh_library: description: Refresh a Plex library to scan for new and updated media. fields: From d76993034e3cbdec286d9c47544bfff412a3780b Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 22 Apr 2021 22:23:36 +0200 Subject: [PATCH 0454/1317] Replace HomeAssistantType with HomeAssistant for integrations m* - n* (#49566) * Integration neato: rename HomeAssistantType to HomeAssistant. * Integration mysensors: rename HomeAssistantType to HomeAssistant. * Integration mobile_app: rename HomeAssistantType to HomeAssistant. * Integration minecraft_server: rename HomeAssistantType to HomeAssistant. * Clean up Co-authored-by: Martin Hjelmare --- .../components/minecraft_server/__init__.py | 12 +++++----- .../minecraft_server/binary_sensor.py | 4 ++-- .../components/minecraft_server/helpers.py | 4 ++-- .../components/minecraft_server/sensor.py | 4 ++-- .../components/mobile_app/__init__.py | 5 +++-- .../components/mobile_app/helpers.py | 5 ++--- .../components/mobile_app/webhook.py | 5 ++--- .../components/mysensors/__init__.py | 8 +++---- .../components/mysensors/binary_sensor.py | 5 ++--- homeassistant/components/mysensors/climate.py | 4 ++-- homeassistant/components/mysensors/cover.py | 4 ++-- .../components/mysensors/device_tracker.py | 6 ++--- homeassistant/components/mysensors/gateway.py | 21 ++++++++---------- homeassistant/components/mysensors/handler.py | 21 ++++++++---------- homeassistant/components/mysensors/helpers.py | 3 +-- homeassistant/components/mysensors/light.py | 5 ++--- homeassistant/components/mysensors/sensor.py | 4 ++-- homeassistant/components/mysensors/switch.py | 4 ++-- homeassistant/components/neato/__init__.py | 13 ++++++----- .../minecraft_server/test_config_flow.py | 22 +++++++++---------- .../components/mysensors/test_config_flow.py | 20 ++++++++--------- tests/components/mysensors/test_gateway.py | 4 ++-- tests/components/mysensors/test_init.py | 5 +++-- tests/components/neato/test_config_flow.py | 6 ++--- 24 files changed, 92 insertions(+), 102 deletions(-) diff --git a/homeassistant/components/minecraft_server/__init__.py b/homeassistant/components/minecraft_server/__init__.py index f466988cda4af..e887f31ae0f9b 100644 --- a/homeassistant/components/minecraft_server/__init__.py +++ b/homeassistant/components/minecraft_server/__init__.py @@ -10,14 +10,14 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from . import helpers from .const import DOMAIN, MANUFACTURER, SCAN_INTERVAL, SIGNAL_NAME_PREFIX @@ -27,7 +27,7 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up Minecraft Server from a config entry.""" domain_data = hass.data.setdefault(DOMAIN, {}) @@ -52,9 +52,7 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) return True -async def async_unload_entry( - hass: HomeAssistantType, config_entry: ConfigEntry -) -> bool: +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload Minecraft Server config entry.""" unique_id = config_entry.unique_id server = hass.data[DOMAIN][unique_id] @@ -81,7 +79,7 @@ class MinecraftServer: _MAX_RETRIES_STATUS = 3 def __init__( - self, hass: HomeAssistantType, unique_id: str, config_data: ConfigType + self, hass: HomeAssistant, unique_id: str, config_data: ConfigType ) -> None: """Initialize server instance.""" self._hass = hass diff --git a/homeassistant/components/minecraft_server/binary_sensor.py b/homeassistant/components/minecraft_server/binary_sensor.py index aadcba44e8571..79325f9c90c32 100644 --- a/homeassistant/components/minecraft_server/binary_sensor.py +++ b/homeassistant/components/minecraft_server/binary_sensor.py @@ -5,14 +5,14 @@ BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from . import MinecraftServer, MinecraftServerEntity from .const import DOMAIN, ICON_STATUS, NAME_STATUS async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the Minecraft Server binary sensor platform.""" server = hass.data[DOMAIN][config_entry.unique_id] diff --git a/homeassistant/components/minecraft_server/helpers.py b/homeassistant/components/minecraft_server/helpers.py index f6409ce525de8..13ec4cd1afb71 100644 --- a/homeassistant/components/minecraft_server/helpers.py +++ b/homeassistant/components/minecraft_server/helpers.py @@ -6,12 +6,12 @@ import aiodns from homeassistant.const import CONF_HOST, CONF_PORT -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .const import SRV_RECORD_PREFIX -async def async_check_srv_record(hass: HomeAssistantType, host: str) -> dict[str, Any]: +async def async_check_srv_record(hass: HomeAssistant, host: str) -> dict[str, Any]: """Check if the given host is a valid Minecraft SRV record.""" # Check if 'host' is a valid SRV record. return_value = None diff --git a/homeassistant/components/minecraft_server/sensor.py b/homeassistant/components/minecraft_server/sensor.py index 3d77d9e27727f..651c2762c5599 100644 --- a/homeassistant/components/minecraft_server/sensor.py +++ b/homeassistant/components/minecraft_server/sensor.py @@ -6,7 +6,7 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import TIME_MILLISECONDS -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from . import MinecraftServer, MinecraftServerEntity from .const import ( @@ -30,7 +30,7 @@ async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the Minecraft Server sensor platform.""" server = hass.data[DOMAIN][config_entry.unique_id] diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py index e63698d3eb587..1321818b91fec 100644 --- a/homeassistant/components/mobile_app/__init__.py +++ b/homeassistant/components/mobile_app/__init__.py @@ -8,8 +8,9 @@ async_unregister as webhook_unregister, ) from homeassistant.const import ATTR_DEVICE_ID, CONF_WEBHOOK_ID +from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, discovery -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from .const import ( ATTR_DEVICE_NAME, @@ -32,7 +33,7 @@ PLATFORMS = "sensor", "binary_sensor", "device_tracker" -async def async_setup(hass: HomeAssistantType, config: ConfigType): +async def async_setup(hass: HomeAssistant, config: ConfigType): """Set up the mobile app component.""" store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) app_config = await store.async_load() diff --git a/homeassistant/components/mobile_app/helpers.py b/homeassistant/components/mobile_app/helpers.py index 63d638cd9e557..7fe4bb5ecd6a2 100644 --- a/homeassistant/components/mobile_app/helpers.py +++ b/homeassistant/components/mobile_app/helpers.py @@ -15,9 +15,8 @@ HTTP_BAD_REQUEST, HTTP_OK, ) -from homeassistant.core import Context +from homeassistant.core import Context, HomeAssistant from homeassistant.helpers.json import JSONEncoder -from homeassistant.helpers.typing import HomeAssistantType from .const import ( ATTR_APP_DATA, @@ -139,7 +138,7 @@ def safe_registration(registration: dict) -> dict: } -def savable_state(hass: HomeAssistantType) -> dict: +def savable_state(hass: HomeAssistant) -> dict: """Return a clean object containing things that should be saved.""" return { DATA_DELETED_IDS: hass.data[DOMAIN][DATA_DELETED_IDS], diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index 6be39f34f00fa..64f10d5616a5f 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -33,7 +33,7 @@ HTTP_BAD_REQUEST, HTTP_CREATED, ) -from homeassistant.core import EventOrigin +from homeassistant.core import EventOrigin, HomeAssistant from homeassistant.exceptions import HomeAssistantError, ServiceNotFound from homeassistant.helpers import ( config_validation as cv, @@ -42,7 +42,6 @@ template, ) from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util.decorator import Registry from .const import ( @@ -145,7 +144,7 @@ async def validate_and_run(hass, config_entry, data): async def handle_webhook( - hass: HomeAssistantType, webhook_id: str, request: Request + hass: HomeAssistant, webhook_id: str, request: Request ) -> Response: """Handle webhook callback.""" if webhook_id in hass.data[DOMAIN][DATA_DELETED_IDS]: diff --git a/homeassistant/components/mysensors/__init__.py b/homeassistant/components/mysensors/__init__.py index c9ad496762dce..c5ed31326a353 100644 --- a/homeassistant/components/mysensors/__init__.py +++ b/homeassistant/components/mysensors/__init__.py @@ -19,7 +19,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from .const import ( ATTR_DEVICES, @@ -142,7 +142,7 @@ def validator(config): ) -async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the MySensors component.""" hass.data[DOMAIN] = {DATA_HASS_CONFIG: config} @@ -182,7 +182,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: return True -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up an instance of the MySensors integration. Every instance has a connection to exactly one Gateway. @@ -234,7 +234,7 @@ async def finish() -> None: return True -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Remove an instance of the MySensors integration.""" gateway = get_mysensors_gateway(hass, entry.entry_id) diff --git a/homeassistant/components/mysensors/binary_sensor.py b/homeassistant/components/mysensors/binary_sensor.py index c4e12d170c01e..161f5cab8c780 100644 --- a/homeassistant/components/mysensors/binary_sensor.py +++ b/homeassistant/components/mysensors/binary_sensor.py @@ -16,9 +16,8 @@ from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_ON -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.typing import HomeAssistantType SENSORS = { "S_DOOR": "door", @@ -33,7 +32,7 @@ async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable ): """Set up this platform for a specific ConfigEntry(==Gateway).""" diff --git a/homeassistant/components/mysensors/climate.py b/homeassistant/components/mysensors/climate.py index b1916fc4ed104..a3104677fa2a3 100644 --- a/homeassistant/components/mysensors/climate.py +++ b/homeassistant/components/mysensors/climate.py @@ -19,8 +19,8 @@ from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.typing import HomeAssistantType DICT_HA_TO_MYS = { HVAC_MODE_AUTO: "AutoChangeOver", @@ -40,7 +40,7 @@ async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable ): """Set up this platform for a specific ConfigEntry(==Gateway).""" diff --git a/homeassistant/components/mysensors/cover.py b/homeassistant/components/mysensors/cover.py index 33393f08defab..bade01f42d879 100644 --- a/homeassistant/components/mysensors/cover.py +++ b/homeassistant/components/mysensors/cover.py @@ -9,8 +9,8 @@ from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.typing import HomeAssistantType _LOGGER = logging.getLogger(__name__) @@ -26,7 +26,7 @@ class CoverState(Enum): async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable ): """Set up this platform for a specific ConfigEntry(==Gateway).""" diff --git a/homeassistant/components/mysensors/device_tracker.py b/homeassistant/components/mysensors/device_tracker.py index 068029af9602f..45416ff7ae7e7 100644 --- a/homeassistant/components/mysensors/device_tracker.py +++ b/homeassistant/components/mysensors/device_tracker.py @@ -3,13 +3,13 @@ from homeassistant.components.device_tracker import DOMAIN from homeassistant.components.mysensors import DevId, on_unload from homeassistant.components.mysensors.const import ATTR_GATEWAY_ID, GatewayId +from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import slugify async def async_setup_scanner( - hass: HomeAssistantType, config, async_see, discovery_info=None + hass: HomeAssistant, config, async_see, discovery_info=None ): """Set up the MySensors device scanner.""" if not discovery_info: @@ -53,7 +53,7 @@ async def async_setup_scanner( class MySensorsDeviceScanner(mysensors.device.MySensorsDevice): """Represent a MySensors scanner.""" - def __init__(self, hass: HomeAssistantType, async_see, *args): + def __init__(self, hass: HomeAssistant, async_see, *args): """Set up instance.""" super().__init__(*args) self.async_see = async_see diff --git a/homeassistant/components/mysensors/gateway.py b/homeassistant/components/mysensors/gateway.py index 0d800e0215e85..ec403e6e34b26 100644 --- a/homeassistant/components/mysensors/gateway.py +++ b/homeassistant/components/mysensors/gateway.py @@ -16,9 +16,8 @@ from homeassistant.components.mqtt import DOMAIN as MQTT_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.core import Event, callback +from homeassistant.core import Event, HomeAssistant, callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import HomeAssistantType from .const import ( CONF_BAUD_RATE, @@ -67,7 +66,7 @@ def is_socket_address(value): raise vol.Invalid("Device is not a valid domain name or ip address") from err -async def try_connect(hass: HomeAssistantType, user_input: dict[str, str]) -> bool: +async def try_connect(hass: HomeAssistant, user_input: dict[str, str]) -> bool: """Try to connect to a gateway and report if it worked.""" if user_input[CONF_DEVICE] == MQTT_COMPONENT: return True # dont validate mqtt. mqtt gateways dont send ready messages :( @@ -113,7 +112,7 @@ def on_conn_made(_: BaseAsyncGateway) -> None: def get_mysensors_gateway( - hass: HomeAssistantType, gateway_id: GatewayId + hass: HomeAssistant, gateway_id: GatewayId ) -> BaseAsyncGateway | None: """Return the Gateway for a given GatewayId.""" if MYSENSORS_GATEWAYS not in hass.data[DOMAIN]: @@ -123,7 +122,7 @@ def get_mysensors_gateway( async def setup_gateway( - hass: HomeAssistantType, entry: ConfigEntry + hass: HomeAssistant, entry: ConfigEntry ) -> BaseAsyncGateway | None: """Set up the Gateway for the given ConfigEntry.""" @@ -145,7 +144,7 @@ async def setup_gateway( async def _get_gateway( - hass: HomeAssistantType, + hass: HomeAssistant, device: str, version: str, event_callback: Callable[[Message], None], @@ -233,7 +232,7 @@ def internal_callback(msg): async def finish_setup( - hass: HomeAssistantType, entry: ConfigEntry, gateway: BaseAsyncGateway + hass: HomeAssistant, entry: ConfigEntry, gateway: BaseAsyncGateway ): """Load any persistent devices and platforms and start gateway.""" discover_tasks = [] @@ -248,7 +247,7 @@ async def finish_setup( async def _discover_persistent_devices( - hass: HomeAssistantType, entry: ConfigEntry, gateway: BaseAsyncGateway + hass: HomeAssistant, entry: ConfigEntry, gateway: BaseAsyncGateway ): """Discover platforms for devices loaded via persistence file.""" tasks = [] @@ -278,9 +277,7 @@ async def gw_stop(hass, entry: ConfigEntry, gateway: BaseAsyncGateway): await gateway.stop() -async def _gw_start( - hass: HomeAssistantType, entry: ConfigEntry, gateway: BaseAsyncGateway -): +async def _gw_start(hass: HomeAssistant, entry: ConfigEntry, gateway: BaseAsyncGateway): """Start the gateway.""" gateway_ready = asyncio.Event() @@ -319,7 +316,7 @@ async def stop_this_gw(_: Event): def _gw_callback_factory( - hass: HomeAssistantType, gateway_id: GatewayId + hass: HomeAssistant, gateway_id: GatewayId ) -> Callable[[Message], None]: """Return a new callback for the gateway.""" diff --git a/homeassistant/components/mysensors/handler.py b/homeassistant/components/mysensors/handler.py index d21140701f97c..8558cd01f4261 100644 --- a/homeassistant/components/mysensors/handler.py +++ b/homeassistant/components/mysensors/handler.py @@ -3,9 +3,8 @@ from mysensors import Message -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import decorator from .const import CHILD_CALLBACK, NODE_CALLBACK, DevId, GatewayId @@ -16,9 +15,7 @@ @HANDLERS.register("set") -async def handle_set( - hass: HomeAssistantType, gateway_id: GatewayId, msg: Message -) -> None: +async def handle_set(hass: HomeAssistant, gateway_id: GatewayId, msg: Message) -> None: """Handle a mysensors set message.""" validated = validate_set_msg(gateway_id, msg) _handle_child_update(hass, gateway_id, validated) @@ -26,7 +23,7 @@ async def handle_set( @HANDLERS.register("internal") async def handle_internal( - hass: HomeAssistantType, gateway_id: GatewayId, msg: Message + hass: HomeAssistant, gateway_id: GatewayId, msg: Message ) -> None: """Handle a mysensors internal message.""" internal = msg.gateway.const.Internal(msg.sub_type) @@ -38,7 +35,7 @@ async def handle_internal( @HANDLERS.register("I_BATTERY_LEVEL") async def handle_battery_level( - hass: HomeAssistantType, gateway_id: GatewayId, msg: Message + hass: HomeAssistant, gateway_id: GatewayId, msg: Message ) -> None: """Handle an internal battery level message.""" _handle_node_update(hass, gateway_id, msg) @@ -46,7 +43,7 @@ async def handle_battery_level( @HANDLERS.register("I_HEARTBEAT_RESPONSE") async def handle_heartbeat( - hass: HomeAssistantType, gateway_id: GatewayId, msg: Message + hass: HomeAssistant, gateway_id: GatewayId, msg: Message ) -> None: """Handle an heartbeat.""" _handle_node_update(hass, gateway_id, msg) @@ -54,7 +51,7 @@ async def handle_heartbeat( @HANDLERS.register("I_SKETCH_NAME") async def handle_sketch_name( - hass: HomeAssistantType, gateway_id: GatewayId, msg: Message + hass: HomeAssistant, gateway_id: GatewayId, msg: Message ) -> None: """Handle an internal sketch name message.""" _handle_node_update(hass, gateway_id, msg) @@ -62,7 +59,7 @@ async def handle_sketch_name( @HANDLERS.register("I_SKETCH_VERSION") async def handle_sketch_version( - hass: HomeAssistantType, gateway_id: GatewayId, msg: Message + hass: HomeAssistant, gateway_id: GatewayId, msg: Message ) -> None: """Handle an internal sketch version message.""" _handle_node_update(hass, gateway_id, msg) @@ -70,7 +67,7 @@ async def handle_sketch_version( @callback def _handle_child_update( - hass: HomeAssistantType, gateway_id: GatewayId, validated: dict[str, list[DevId]] + hass: HomeAssistant, gateway_id: GatewayId, validated: dict[str, list[DevId]] ): """Handle a child update.""" signals: list[str] = [] @@ -94,7 +91,7 @@ def _handle_child_update( @callback -def _handle_node_update(hass: HomeAssistantType, gateway_id: GatewayId, msg: Message): +def _handle_node_update(hass: HomeAssistant, gateway_id: GatewayId, msg: Message): """Handle a node update.""" signal = NODE_CALLBACK.format(gateway_id, msg.node_id) async_dispatcher_send(hass, signal) diff --git a/homeassistant/components/mysensors/helpers.py b/homeassistant/components/mysensors/helpers.py index 71ea97bc3719d..9a35f67d49b41 100644 --- a/homeassistant/components/mysensors/helpers.py +++ b/homeassistant/components/mysensors/helpers.py @@ -15,7 +15,6 @@ from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util.decorator import Registry from .const import ( @@ -37,7 +36,7 @@ async def on_unload( - hass: HomeAssistantType, entry: ConfigEntry | GatewayId, fnct: Callable + hass: HomeAssistant, entry: ConfigEntry | GatewayId, fnct: Callable ) -> None: """Register a callback to be called when entry is unloaded. diff --git a/homeassistant/components/mysensors/light.py b/homeassistant/components/mysensors/light.py index f90f9c5c81c77..3262487d18e2c 100644 --- a/homeassistant/components/mysensors/light.py +++ b/homeassistant/components/mysensors/light.py @@ -16,9 +16,8 @@ from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_ON -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.typing import HomeAssistantType import homeassistant.util.color as color_util from homeassistant.util.color import rgb_hex_to_rgb_list @@ -26,7 +25,7 @@ async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable ): """Set up this platform for a specific ConfigEntry(==Gateway).""" device_class_map = { diff --git a/homeassistant/components/mysensors/sensor.py b/homeassistant/components/mysensors/sensor.py index 1a5f7330ddffa..a63f143f1d7e2 100644 --- a/homeassistant/components/mysensors/sensor.py +++ b/homeassistant/components/mysensors/sensor.py @@ -25,8 +25,8 @@ VOLT, VOLUME_CUBIC_METERS, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.typing import HomeAssistantType SENSORS = { "V_TEMP": [None, "mdi:thermometer"], @@ -64,7 +64,7 @@ async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable ): """Set up this platform for a specific ConfigEntry(==Gateway).""" diff --git a/homeassistant/components/mysensors/switch.py b/homeassistant/components/mysensors/switch.py index 14911e11090fb..a410cc64df4a6 100644 --- a/homeassistant/components/mysensors/switch.py +++ b/homeassistant/components/mysensors/switch.py @@ -6,12 +6,12 @@ from homeassistant.components import mysensors from homeassistant.components.switch import DOMAIN, SwitchEntity from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from . import on_unload from ...config_entries import ConfigEntry from ...helpers.dispatcher import async_dispatcher_connect -from ...helpers.typing import HomeAssistantType from .const import DOMAIN as MYSENSORS_DOMAIN, MYSENSORS_DISCOVERY, SERVICE_SEND_IR_CODE ATTR_IR_CODE = "V_IR_SEND" @@ -22,7 +22,7 @@ async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable ): """Set up this platform for a specific ConfigEntry(==Gateway).""" device_class_map = { diff --git a/homeassistant/components/neato/__init__.py b/homeassistant/components/neato/__init__.py index 9413ff77236fe..036d91534f404 100644 --- a/homeassistant/components/neato/__init__.py +++ b/homeassistant/components/neato/__init__.py @@ -9,9 +9,10 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_TOKEN +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from homeassistant.util import Throttle from . import api, config_flow @@ -42,7 +43,7 @@ PLATFORMS = ["camera", "vacuum", "switch", "sensor"] -async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Neato component.""" hass.data[NEATO_DOMAIN] = {} @@ -66,7 +67,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: return True -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up config entry.""" if CONF_TOKEN not in entry.data: raise ConfigEntryAuthFailed @@ -99,7 +100,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool return True -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigType) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigType) -> bool: """Unload config entry.""" unload_functions = ( hass.config_entries.async_forward_entry_unload(entry, platform) @@ -116,9 +117,9 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigType) -> bool class NeatoHub: """A My Neato hub wrapper class.""" - def __init__(self, hass: HomeAssistantType, neato: Account): + def __init__(self, hass: HomeAssistant, neato: Account): """Initialize the Neato hub.""" - self._hass: HomeAssistantType = hass + self._hass = hass self.my_neato: Account = neato @Throttle(timedelta(minutes=1)) diff --git a/tests/components/minecraft_server/test_config_flow.py b/tests/components/minecraft_server/test_config_flow.py index 9fcea3261ee62..9717fa0052b9a 100644 --- a/tests/components/minecraft_server/test_config_flow.py +++ b/tests/components/minecraft_server/test_config_flow.py @@ -13,12 +13,12 @@ ) from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT +from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import ( RESULT_TYPE_ABORT, RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM, ) -from homeassistant.helpers.typing import HomeAssistantType from tests.common import MockConfigEntry @@ -80,7 +80,7 @@ def __init__(self): SRV_RECORDS.set_result([QueryMock()]) -async def test_show_config_form(hass: HomeAssistantType) -> None: +async def test_show_config_form(hass: HomeAssistant) -> None: """Test if initial configuration form is shown.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -90,7 +90,7 @@ async def test_show_config_form(hass: HomeAssistantType) -> None: assert result["step_id"] == "user" -async def test_invalid_ip(hass: HomeAssistantType) -> None: +async def test_invalid_ip(hass: HomeAssistant) -> None: """Test error in case of an invalid IP address.""" with patch("getmac.get_mac_address", return_value=None): result = await hass.config_entries.flow.async_init( @@ -101,7 +101,7 @@ async def test_invalid_ip(hass: HomeAssistantType) -> None: assert result["errors"] == {"base": "invalid_ip"} -async def test_same_host(hass: HomeAssistantType) -> None: +async def test_same_host(hass: HomeAssistant) -> None: """Test abort in case of same host name.""" with patch("aiodns.DNSResolver.query", side_effect=aiodns.error.DNSError,), patch( "mcstatus.server.MinecraftServer.status", @@ -126,7 +126,7 @@ async def test_same_host(hass: HomeAssistantType) -> None: assert result["reason"] == "already_configured" -async def test_port_too_small(hass: HomeAssistantType) -> None: +async def test_port_too_small(hass: HomeAssistant) -> None: """Test error in case of a too small port.""" with patch( "aiodns.DNSResolver.query", @@ -140,7 +140,7 @@ async def test_port_too_small(hass: HomeAssistantType) -> None: assert result["errors"] == {"base": "invalid_port"} -async def test_port_too_large(hass: HomeAssistantType) -> None: +async def test_port_too_large(hass: HomeAssistant) -> None: """Test error in case of a too large port.""" with patch( "aiodns.DNSResolver.query", @@ -154,7 +154,7 @@ async def test_port_too_large(hass: HomeAssistantType) -> None: assert result["errors"] == {"base": "invalid_port"} -async def test_connection_failed(hass: HomeAssistantType) -> None: +async def test_connection_failed(hass: HomeAssistant) -> None: """Test error in case of a failed connection.""" with patch( "aiodns.DNSResolver.query", @@ -168,7 +168,7 @@ async def test_connection_failed(hass: HomeAssistantType) -> None: assert result["errors"] == {"base": "cannot_connect"} -async def test_connection_succeeded_with_srv_record(hass: HomeAssistantType) -> None: +async def test_connection_succeeded_with_srv_record(hass: HomeAssistant) -> None: """Test config entry in case of a successful connection with a SRV record.""" with patch("aiodns.DNSResolver.query", return_value=SRV_RECORDS,), patch( "mcstatus.server.MinecraftServer.status", @@ -184,7 +184,7 @@ async def test_connection_succeeded_with_srv_record(hass: HomeAssistantType) -> assert result["data"][CONF_HOST] == USER_INPUT_SRV[CONF_HOST] -async def test_connection_succeeded_with_host(hass: HomeAssistantType) -> None: +async def test_connection_succeeded_with_host(hass: HomeAssistant) -> None: """Test config entry in case of a successful connection with a host name.""" with patch("aiodns.DNSResolver.query", side_effect=aiodns.error.DNSError,), patch( "mcstatus.server.MinecraftServer.status", @@ -200,7 +200,7 @@ async def test_connection_succeeded_with_host(hass: HomeAssistantType) -> None: assert result["data"][CONF_HOST] == "mc.dummyserver.com" -async def test_connection_succeeded_with_ip4(hass: HomeAssistantType) -> None: +async def test_connection_succeeded_with_ip4(hass: HomeAssistant) -> None: """Test config entry in case of a successful connection with an IPv4 address.""" with patch("getmac.get_mac_address", return_value="01:23:45:67:89:ab"), patch( "aiodns.DNSResolver.query", @@ -219,7 +219,7 @@ async def test_connection_succeeded_with_ip4(hass: HomeAssistantType) -> None: assert result["data"][CONF_HOST] == "1.1.1.1" -async def test_connection_succeeded_with_ip6(hass: HomeAssistantType) -> None: +async def test_connection_succeeded_with_ip6(hass: HomeAssistant) -> None: """Test config entry in case of a successful connection with an IPv6 address.""" with patch("getmac.get_mac_address", return_value="01:23:45:67:89:ab"), patch( "aiodns.DNSResolver.query", diff --git a/tests/components/mysensors/test_config_flow.py b/tests/components/mysensors/test_config_flow.py index a91159e439562..dfad2b50558f4 100644 --- a/tests/components/mysensors/test_config_flow.py +++ b/tests/components/mysensors/test_config_flow.py @@ -23,13 +23,13 @@ DOMAIN, ConfGatewayType, ) -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry async def get_form( - hass: HomeAssistantType, gatway_type: ConfGatewayType, expected_step_id: str + hass: HomeAssistant, gatway_type: ConfGatewayType, expected_step_id: str ): """Get a form for the given gateway type.""" await setup.async_setup_component(hass, "persistent_notification", {}) @@ -50,7 +50,7 @@ async def get_form( return result -async def test_config_mqtt(hass: HomeAssistantType, mqtt: None) -> None: +async def test_config_mqtt(hass: HomeAssistant, mqtt: None) -> None: """Test configuring a mqtt gateway.""" step = await get_form(hass, CONF_GATEWAY_TYPE_MQTT, "gw_mqtt") flow_id = step["flow_id"] @@ -88,7 +88,7 @@ async def test_config_mqtt(hass: HomeAssistantType, mqtt: None) -> None: assert len(mock_setup_entry.mock_calls) == 1 -async def test_missing_mqtt(hass: HomeAssistantType) -> None: +async def test_missing_mqtt(hass: HomeAssistant) -> None: """Test configuring a mqtt gateway without mqtt integration setup.""" await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( @@ -106,7 +106,7 @@ async def test_missing_mqtt(hass: HomeAssistantType) -> None: assert result["errors"] == {"base": "mqtt_required"} -async def test_config_serial(hass: HomeAssistantType): +async def test_config_serial(hass: HomeAssistant): """Test configuring a gateway via serial.""" step = await get_form(hass, CONF_GATEWAY_TYPE_SERIAL, "gw_serial") flow_id = step["flow_id"] @@ -146,7 +146,7 @@ async def test_config_serial(hass: HomeAssistantType): assert len(mock_setup_entry.mock_calls) == 1 -async def test_config_tcp(hass: HomeAssistantType): +async def test_config_tcp(hass: HomeAssistant): """Test configuring a gateway via tcp.""" step = await get_form(hass, CONF_GATEWAY_TYPE_TCP, "gw_tcp") flow_id = step["flow_id"] @@ -183,7 +183,7 @@ async def test_config_tcp(hass: HomeAssistantType): assert len(mock_setup_entry.mock_calls) == 1 -async def test_fail_to_connect(hass: HomeAssistantType): +async def test_fail_to_connect(hass: HomeAssistant): """Test configuring a gateway via tcp.""" step = await get_form(hass, CONF_GATEWAY_TYPE_TCP, "gw_tcp") flow_id = step["flow_id"] @@ -365,7 +365,7 @@ async def test_fail_to_connect(hass: HomeAssistantType): ], ) async def test_config_invalid( - hass: HomeAssistantType, + hass: HomeAssistant, mqtt: config_entries.ConfigEntry, gateway_type: ConfGatewayType, expected_step_id: str, @@ -440,7 +440,7 @@ async def test_config_invalid( }, ], ) -async def test_import(hass: HomeAssistantType, mqtt: None, user_input: dict) -> None: +async def test_import(hass: HomeAssistant, mqtt: None, user_input: dict) -> None: """Test importing a gateway.""" await setup.async_setup_component(hass, "persistent_notification", {}) @@ -731,7 +731,7 @@ async def test_import(hass: HomeAssistantType, mqtt: None, user_input: dict) -> ], ) async def test_duplicate( - hass: HomeAssistantType, + hass: HomeAssistant, mqtt: None, first_input: dict, second_input: dict, diff --git a/tests/components/mysensors/test_gateway.py b/tests/components/mysensors/test_gateway.py index d3e360e0b9f8a..f2e7aa77c8c93 100644 --- a/tests/components/mysensors/test_gateway.py +++ b/tests/components/mysensors/test_gateway.py @@ -5,7 +5,7 @@ import voluptuous as vol from homeassistant.components.mysensors.gateway import is_serial_port -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant @pytest.mark.parametrize( @@ -18,7 +18,7 @@ ("/dev/ttyACM0", False), ], ) -def test_is_serial_port_windows(hass: HomeAssistantType, port: str, expect_valid: bool): +def test_is_serial_port_windows(hass: HomeAssistant, port: str, expect_valid: bool): """Test windows serial port.""" with patch("sys.platform", "win32"): diff --git a/tests/components/mysensors/test_init.py b/tests/components/mysensors/test_init.py index 780621112abab..30fbf3ea686bb 100644 --- a/tests/components/mysensors/test_init.py +++ b/tests/components/mysensors/test_init.py @@ -25,7 +25,8 @@ CONF_TOPIC_IN_PREFIX, CONF_TOPIC_OUT_PREFIX, ) -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType from homeassistant.setup import async_setup_component @@ -226,7 +227,7 @@ ], ) async def test_import( - hass: HomeAssistantType, + hass: HomeAssistant, mqtt: None, config: ConfigType, expected_calls: int, diff --git a/tests/components/neato/test_config_flow.py b/tests/components/neato/test_config_flow.py index 7c7e25f2e0c9e..3650dae8a5ca0 100644 --- a/tests/components/neato/test_config_flow.py +++ b/tests/components/neato/test_config_flow.py @@ -5,8 +5,8 @@ from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.neato.const import NEATO_DOMAIN +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_entry_oauth2_flow -from homeassistant.helpers.typing import HomeAssistantType from tests.common import MockConfigEntry @@ -74,7 +74,7 @@ async def test_full_flow( assert len(mock_setup.mock_calls) == 1 -async def test_abort_if_already_setup(hass: HomeAssistantType): +async def test_abort_if_already_setup(hass: HomeAssistant): """Test we abort if Neato is already setup.""" entry = MockConfigEntry( domain=NEATO_DOMAIN, @@ -91,7 +91,7 @@ async def test_abort_if_already_setup(hass: HomeAssistantType): async def test_reauth( - hass: HomeAssistantType, aiohttp_client, aioclient_mock, current_request_with_host + hass: HomeAssistant, aiohttp_client, aioclient_mock, current_request_with_host ): """Test initialization of the reauth flow.""" assert await setup.async_setup_component( From d4329e01efc184c9d05cc401e75536a1c7c5f2de Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 22 Apr 2021 10:32:38 -1000 Subject: [PATCH 0455/1317] Fix deadlock in async_get_integration_with_requirements after failed dep pip install (#49540) --- homeassistant/requirements.py | 53 ++++++++++++++-------- tests/test_requirements.py | 82 +++++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+), 19 deletions(-) diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py index cc4ce32d8082c..59321a1032e2a 100644 --- a/homeassistant/requirements.py +++ b/homeassistant/requirements.py @@ -65,6 +65,7 @@ async def async_get_integration_with_requirements( if isinstance(int_or_evt, asyncio.Event): await int_or_evt.wait() + int_or_evt = cache.get(domain, UNDEFINED) # When we have waited and it's UNDEFINED, it doesn't exist @@ -78,6 +79,22 @@ async def async_get_integration_with_requirements( event = cache[domain] = asyncio.Event() + try: + await _async_process_integration(hass, integration, done) + except Exception: # pylint: disable=broad-except + del cache[domain] + event.set() + raise + + cache[domain] = integration + event.set() + return integration + + +async def _async_process_integration( + hass: HomeAssistant, integration: Integration, done: set[str] +) -> None: + """Process an integration and requirements.""" if integration.requirements: await async_process_requirements( hass, integration.domain, integration.requirements @@ -97,26 +114,24 @@ async def async_get_integration_with_requirements( ): deps_to_check.append(check_domain) - if deps_to_check: - results = await asyncio.gather( - *[ - async_get_integration_with_requirements(hass, dep, done) - for dep in deps_to_check - ], - return_exceptions=True, - ) - for result in results: - if not isinstance(result, BaseException): - continue - if not isinstance(result, IntegrationNotFound) or not ( - not integration.is_built_in - and result.domain in integration.after_dependencies - ): - raise result + if not deps_to_check: + return - cache[domain] = integration - event.set() - return integration + results = await asyncio.gather( + *[ + async_get_integration_with_requirements(hass, dep, done) + for dep in deps_to_check + ], + return_exceptions=True, + ) + for result in results: + if not isinstance(result, BaseException): + continue + if not isinstance(result, IntegrationNotFound) or not ( + not integration.is_built_in + and result.domain in integration.after_dependencies + ): + raise result async def async_process_requirements( diff --git a/tests/test_requirements.py b/tests/test_requirements.py index acc83afeec29a..ff3f5bcab8767 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -139,6 +139,88 @@ async def test_get_integration_with_requirements(hass): ] +async def test_get_integration_with_requirements_pip_install_fails_two_passes(hass): + """Check getting an integration with loaded requirements and the pip install fails two passes.""" + hass.config.skip_pip = False + mock_integration( + hass, MockModule("test_component_dep", requirements=["test-comp-dep==1.0.0"]) + ) + mock_integration( + hass, + MockModule( + "test_component_after_dep", requirements=["test-comp-after-dep==1.0.0"] + ), + ) + mock_integration( + hass, + MockModule( + "test_component", + requirements=["test-comp==1.0.0"], + dependencies=["test_component_dep"], + partial_manifest={"after_dependencies": ["test_component_after_dep"]}, + ), + ) + + def _mock_install_package(package, **kwargs): + if package == "test-comp==1.0.0": + return True + return False + + # 1st pass + with pytest.raises(RequirementsNotFound), patch( + "homeassistant.util.package.is_installed", return_value=False + ) as mock_is_installed, patch( + "homeassistant.util.package.install_package", side_effect=_mock_install_package + ) as mock_inst: + + integration = await async_get_integration_with_requirements( + hass, "test_component" + ) + assert integration + assert integration.domain == "test_component" + + assert len(mock_is_installed.mock_calls) == 3 + assert sorted(mock_call[1][0] for mock_call in mock_is_installed.mock_calls) == [ + "test-comp-after-dep==1.0.0", + "test-comp-dep==1.0.0", + "test-comp==1.0.0", + ] + + assert len(mock_inst.mock_calls) == 3 + assert sorted(mock_call[1][0] for mock_call in mock_inst.mock_calls) == [ + "test-comp-after-dep==1.0.0", + "test-comp-dep==1.0.0", + "test-comp==1.0.0", + ] + + # 2nd pass + with pytest.raises(RequirementsNotFound), patch( + "homeassistant.util.package.is_installed", return_value=False + ) as mock_is_installed, patch( + "homeassistant.util.package.install_package", side_effect=_mock_install_package + ) as mock_inst: + + integration = await async_get_integration_with_requirements( + hass, "test_component" + ) + assert integration + assert integration.domain == "test_component" + + assert len(mock_is_installed.mock_calls) == 3 + assert sorted(mock_call[1][0] for mock_call in mock_is_installed.mock_calls) == [ + "test-comp-after-dep==1.0.0", + "test-comp-dep==1.0.0", + "test-comp==1.0.0", + ] + + assert len(mock_inst.mock_calls) == 3 + assert sorted(mock_call[1][0] for mock_call in mock_inst.mock_calls) == [ + "test-comp-after-dep==1.0.0", + "test-comp-dep==1.0.0", + "test-comp==1.0.0", + ] + + async def test_get_integration_with_missing_dependencies(hass): """Check getting an integration with missing dependencies.""" hass.config.skip_pip = False From 90ede05c82df744303a126951c43f441d280d1e9 Mon Sep 17 00:00:00 2001 From: tikismoke Date: Thu, 22 Apr 2021 22:34:31 +0200 Subject: [PATCH 0456/1317] Bump pyvlx to 0.2.19 (#49533) * Update manifest.json https://github.com/Julius2342/pyvlx/pull/59#issuecomment-824291298 * Update requirements_all.txt --- homeassistant/components/velux/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/velux/manifest.json b/homeassistant/components/velux/manifest.json index 43be9b424a846..c72e25d42eb7c 100644 --- a/homeassistant/components/velux/manifest.json +++ b/homeassistant/components/velux/manifest.json @@ -2,7 +2,7 @@ "domain": "velux", "name": "Velux", "documentation": "https://www.home-assistant.io/integrations/velux", - "requirements": ["pyvlx==0.2.18"], + "requirements": ["pyvlx==0.2.19"], "codeowners": ["@Julius2342"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 43f93c0b48f98..c3f0f0e42dea1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1919,7 +1919,7 @@ pyvesync==1.3.1 pyvizio==0.1.57 # homeassistant.components.velux -pyvlx==0.2.18 +pyvlx==0.2.19 # homeassistant.components.volumio pyvolumio==0.1.3 From 9410aefd0d6f7d827accf31584838310ae71d5c3 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 22 Apr 2021 23:53:37 +0200 Subject: [PATCH 0457/1317] Integrations m*: Rename HomeAssistantType to HomeAssistant. (#49567) --- homeassistant/components/melcloud/__init__.py | 6 ++-- homeassistant/components/melcloud/climate.py | 4 +-- .../components/melcloud/water_heater.py | 4 +-- .../components/meteo_france/weather.py | 4 +-- homeassistant/components/metoffice/sensor.py | 6 ++-- homeassistant/components/metoffice/weather.py | 6 ++-- homeassistant/components/mqtt/__init__.py | 29 ++++++++++++------- .../components/mqtt/alarm_control_panel.py | 6 ++-- .../components/mqtt/binary_sensor.py | 6 ++-- homeassistant/components/mqtt/camera.py | 6 ++-- homeassistant/components/mqtt/climate.py | 6 ++-- homeassistant/components/mqtt/cover.py | 6 ++-- homeassistant/components/mqtt/debug_info.py | 4 +-- .../components/mqtt/device_trigger.py | 4 +-- homeassistant/components/mqtt/discovery.py | 8 ++--- homeassistant/components/mqtt/fan.py | 6 ++-- homeassistant/components/mqtt/lock.py | 6 ++-- homeassistant/components/mqtt/number.py | 6 ++-- homeassistant/components/mqtt/scene.py | 5 ++-- homeassistant/components/mqtt/sensor.py | 6 ++-- homeassistant/components/mqtt/subscription.py | 8 ++--- homeassistant/components/mqtt/switch.py | 6 ++-- .../meteo_france/test_config_flow.py | 4 +-- 23 files changed, 79 insertions(+), 73 deletions(-) diff --git a/homeassistant/components/melcloud/__init__.py b/homeassistant/components/melcloud/__init__.py index 0f48db96bf846..528854308d6ff 100644 --- a/homeassistant/components/melcloud/__init__.py +++ b/homeassistant/components/melcloud/__init__.py @@ -13,10 +13,10 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_TOKEN, CONF_USERNAME +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import Throttle from .const import DOMAIN @@ -41,7 +41,7 @@ ) -async def async_setup(hass: HomeAssistantType, config: ConfigEntry): +async def async_setup(hass: HomeAssistant, config: ConfigEntry): """Establish connection with MELCloud.""" if DOMAIN not in config: return True @@ -58,7 +58,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigEntry): return True -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Establish connection with MELClooud.""" conf = entry.data mel_devices = await mel_devices_setup(hass, conf[CONF_TOKEN]) diff --git a/homeassistant/components/melcloud/climate.py b/homeassistant/components/melcloud/climate.py index 8e45cc3d9a4c1..d8bc89a45f077 100644 --- a/homeassistant/components/melcloud/climate.py +++ b/homeassistant/components/melcloud/climate.py @@ -30,8 +30,8 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import TEMP_CELSIUS +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform -from homeassistant.helpers.typing import HomeAssistantType from . import MelCloudDevice from .const import ( @@ -67,7 +67,7 @@ async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ): """Set up MelCloud device climate based on config_entry.""" mel_devices = hass.data[DOMAIN][entry.entry_id] diff --git a/homeassistant/components/melcloud/water_heater.py b/homeassistant/components/melcloud/water_heater.py index e01d78a527068..8de0a88c84153 100644 --- a/homeassistant/components/melcloud/water_heater.py +++ b/homeassistant/components/melcloud/water_heater.py @@ -15,14 +15,14 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import TEMP_CELSIUS -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from . import DOMAIN, MelCloudDevice from .const import ATTR_STATUS async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ): """Set up MelCloud device climate based on config_entry.""" mel_devices = hass.data[DOMAIN][entry.entry_id] diff --git a/homeassistant/components/meteo_france/weather.py b/homeassistant/components/meteo_france/weather.py index 08d5c1c4f6ad1..f45893ed7caf4 100644 --- a/homeassistant/components/meteo_france/weather.py +++ b/homeassistant/components/meteo_france/weather.py @@ -14,7 +14,7 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_MODE, TEMP_CELSIUS -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -44,7 +44,7 @@ def format_condition(condition: str): async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up the Meteo-France weather platform.""" coordinator = hass.data[DOMAIN][entry.entry_id][COORDINATOR_FORECAST] diff --git a/homeassistant/components/metoffice/sensor.py b/homeassistant/components/metoffice/sensor.py index 6a7cf5254a63e..a437ecd1fea02 100644 --- a/homeassistant/components/metoffice/sensor.py +++ b/homeassistant/components/metoffice/sensor.py @@ -10,8 +10,8 @@ TEMP_CELSIUS, UV_INDEX, ) -from homeassistant.core import callback -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.typing import ConfigType from .const import ( ATTRIBUTION, @@ -78,7 +78,7 @@ async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigType, async_add_entities + hass: HomeAssistant, entry: ConfigType, async_add_entities ) -> None: """Set up the Met Office weather sensor platform.""" hass_data = hass.data[DOMAIN][entry.entry_id] diff --git a/homeassistant/components/metoffice/weather.py b/homeassistant/components/metoffice/weather.py index 351065226af5d..5962300bb85cf 100644 --- a/homeassistant/components/metoffice/weather.py +++ b/homeassistant/components/metoffice/weather.py @@ -1,8 +1,8 @@ """Support for UK Met Office weather service.""" from homeassistant.components.weather import WeatherEntity from homeassistant.const import LENGTH_KILOMETERS, TEMP_CELSIUS -from homeassistant.core import callback -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.typing import ConfigType from .const import ( ATTRIBUTION, @@ -18,7 +18,7 @@ async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigType, async_add_entities + hass: HomeAssistant, entry: ConfigType, async_add_entities ) -> None: """Set up the Met Office weather sensor platform.""" hass_data = hass.data[DOMAIN][entry.entry_id] diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index ce2d413e1b69c..16379aa79235e 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -31,11 +31,18 @@ EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.core import CoreState, Event, HassJob, ServiceCall, callback +from homeassistant.core import ( + CoreState, + Event, + HassJob, + HomeAssistant, + ServiceCall, + callback, +) from homeassistant.exceptions import HomeAssistantError, Unauthorized from homeassistant.helpers import config_validation as cv, event, template from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send -from homeassistant.helpers.typing import ConfigType, HomeAssistantType, ServiceDataType +from homeassistant.helpers.typing import ConfigType, ServiceDataType from homeassistant.loader import bind_hass from homeassistant.util import dt as dt_util from homeassistant.util.async_ import run_callback_threadsafe @@ -245,7 +252,7 @@ def _build_publish_data(topic: Any, qos: int, retain: bool) -> ServiceDataType: @bind_hass -def publish(hass: HomeAssistantType, topic, payload, qos=None, retain=None) -> None: +def publish(hass: HomeAssistant, topic, payload, qos=None, retain=None) -> None: """Publish message to an MQTT topic.""" hass.add_job(async_publish, hass, topic, payload, qos, retain) @@ -253,7 +260,7 @@ def publish(hass: HomeAssistantType, topic, payload, qos=None, retain=None) -> N @callback @bind_hass def async_publish( - hass: HomeAssistantType, topic: Any, payload, qos=None, retain=None + hass: HomeAssistant, topic: Any, payload, qos=None, retain=None ) -> None: """Publish message to an MQTT topic.""" data = _build_publish_data(topic, qos, retain) @@ -263,7 +270,7 @@ def async_publish( @bind_hass def publish_template( - hass: HomeAssistantType, topic, payload_template, qos=None, retain=None + hass: HomeAssistant, topic, payload_template, qos=None, retain=None ) -> None: """Publish message to an MQTT topic.""" hass.add_job(async_publish_template, hass, topic, payload_template, qos, retain) @@ -271,7 +278,7 @@ def publish_template( @bind_hass def async_publish_template( - hass: HomeAssistantType, topic, payload_template, qos=None, retain=None + hass: HomeAssistant, topic, payload_template, qos=None, retain=None ) -> None: """Publish message to an MQTT topic using a template payload.""" data = _build_publish_data(topic, qos, retain) @@ -308,7 +315,7 @@ def wrapper(msg: Any) -> None: @bind_hass async def async_subscribe( - hass: HomeAssistantType, + hass: HomeAssistant, topic: str, msg_callback: MessageCallbackType, qos: int = DEFAULT_QOS, @@ -353,7 +360,7 @@ async def async_subscribe( @bind_hass def subscribe( - hass: HomeAssistantType, + hass: HomeAssistant, topic: str, msg_callback: MessageCallbackType, qos: int = DEFAULT_QOS, @@ -372,7 +379,7 @@ def remove(): async def _async_setup_discovery( - hass: HomeAssistantType, conf: ConfigType, config_entry + hass: HomeAssistant, conf: ConfigType, config_entry ) -> bool: """Try to start the discovery of MQTT devices. @@ -385,7 +392,7 @@ async def _async_setup_discovery( return success -async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Start the MQTT protocol service.""" conf: ConfigType | None = config.get(DOMAIN) @@ -542,7 +549,7 @@ class MQTT: def __init__( self, - hass: HomeAssistantType, + hass: HomeAssistant, config_entry, conf, ) -> None: diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index 0f10e91e41ca9..1e7ccf5bb4c42 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -26,10 +26,10 @@ STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.reload import async_setup_reload_service -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from . import ( CONF_COMMAND_TOPIC, @@ -87,7 +87,7 @@ async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None ): """Set up MQTT alarm control panel through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index fbd5e7535c519..e24abc2702877 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -18,12 +18,12 @@ CONF_PAYLOAD_ON, CONF_VALUE_TEMPLATE, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv import homeassistant.helpers.event as evt from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.reload import async_setup_reload_service -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from homeassistant.util import dt as dt_util from . import CONF_QOS, CONF_STATE_TOPIC, DOMAIN, PLATFORMS, subscription @@ -59,7 +59,7 @@ async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None ): """Set up MQTT binary sensor through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index 0a1a35b2ddd4a..0a9f37ac9ea30 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -6,10 +6,10 @@ from homeassistant.components import camera from homeassistant.components.camera import Camera from homeassistant.const import CONF_NAME -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.reload import async_setup_reload_service -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from . import CONF_QOS, DOMAIN, PLATFORMS, subscription from .. import mqtt @@ -28,7 +28,7 @@ async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None ): """Set up MQTT camera through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 8ab7a9ca3cfb6..dd766ef2035a4 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -46,10 +46,10 @@ PRECISION_WHOLE, STATE_ON, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.reload import async_setup_reload_service -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from . import ( CONF_QOS, @@ -251,7 +251,7 @@ async def async_setup_platform( - hass: HomeAssistantType, async_add_entities, config: ConfigType, discovery_info=None + hass: HomeAssistant, async_add_entities, config: ConfigType, discovery_info=None ): """Set up MQTT climate device through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 6de5050833fd1..3bcc7d4f2a914 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -30,10 +30,10 @@ STATE_OPENING, STATE_UNKNOWN, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.reload import async_setup_reload_service -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from . import ( CONF_COMMAND_TOPIC, @@ -184,7 +184,7 @@ def validate_options(value): async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None ): """Set up MQTT cover through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) diff --git a/homeassistant/components/mqtt/debug_info.py b/homeassistant/components/mqtt/debug_info.py index 52aeb20e3aa4b..e8f0b5784ee9e 100644 --- a/homeassistant/components/mqtt/debug_info.py +++ b/homeassistant/components/mqtt/debug_info.py @@ -3,7 +3,7 @@ from functools import wraps from typing import Any -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .const import ATTR_DISCOVERY_PAYLOAD, ATTR_DISCOVERY_TOPIC from .models import MessageCallbackType @@ -12,7 +12,7 @@ STORED_MESSAGES = 10 -def log_messages(hass: HomeAssistantType, entity_id: str) -> MessageCallbackType: +def log_messages(hass: HomeAssistant, entity_id: str) -> MessageCallbackType: """Wrap an MQTT message callback to support message logging.""" def _log_message(msg): diff --git a/homeassistant/components/mqtt/device_trigger.py b/homeassistant/components/mqtt/device_trigger.py index 1e058162bc3fb..038e6e9152347 100644 --- a/homeassistant/components/mqtt/device_trigger.py +++ b/homeassistant/components/mqtt/device_trigger.py @@ -24,7 +24,7 @@ async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from . import CONF_PAYLOAD, CONF_QOS, DOMAIN, debug_info, trigger as mqtt_trigger from .. import mqtt @@ -120,7 +120,7 @@ class Trigger: device_id: str = attr.ib() discovery_data: dict = attr.ib() - hass: HomeAssistantType = attr.ib() + hass: HomeAssistant = attr.ib() payload: str = attr.ib() qos: int = attr.ib() remove_signal: Callable[[], None] = attr.ib() diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 347166fdb82ce..1d3f5034ff25a 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -8,11 +8,11 @@ import time from homeassistant.const import CONF_DEVICE, CONF_PLATFORM +from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.loader import async_get_mqtt from .. import mqtt @@ -79,9 +79,7 @@ class MQTTConfig(dict): """Dummy class to allow adding attributes.""" -async def async_start( - hass: HomeAssistantType, discovery_topic, config_entry=None -) -> bool: +async def async_start(hass: HomeAssistant, discovery_topic, config_entry=None) -> bool: """Start MQTT Discovery.""" mqtt_integrations = {} @@ -295,7 +293,7 @@ async def async_integration_message_received(integration, msg): return True -async def async_stop(hass: HomeAssistantType) -> bool: +async def async_stop(hass: HomeAssistant) -> bool: """Stop MQTT Discovery.""" if DISCOVERY_UNSUBSCRIBE in hass.data: for unsub in hass.data[DISCOVERY_UNSUBSCRIBE]: diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 24c4c805dfda2..35393ea819c0e 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -28,10 +28,10 @@ CONF_PAYLOAD_ON, CONF_STATE, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.reload import async_setup_reload_service -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from homeassistant.util.percentage import ( int_states_in_range, ordered_list_item_to_percentage, @@ -181,7 +181,7 @@ def valid_speed_range_configuration(config): async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None ): """Set up MQTT fan through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index cdfa51015480a..24d58b148fa23 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -6,10 +6,10 @@ from homeassistant.components import lock from homeassistant.components.lock import LockEntity from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.reload import async_setup_reload_service -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from . import ( CONF_COMMAND_TOPIC, @@ -50,7 +50,7 @@ async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None ): """Set up MQTT lock panel through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index e7839f8e483bd..dd4cfb47acb6f 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -7,11 +7,11 @@ from homeassistant.components import number from homeassistant.components.number import NumberEntity from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from . import ( CONF_COMMAND_TOPIC, @@ -40,7 +40,7 @@ async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None ): """Set up MQTT number through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) diff --git a/homeassistant/components/mqtt/scene.py b/homeassistant/components/mqtt/scene.py index c6d9140af616a..1d84d1cecae73 100644 --- a/homeassistant/components/mqtt/scene.py +++ b/homeassistant/components/mqtt/scene.py @@ -6,9 +6,10 @@ from homeassistant.components import scene from homeassistant.components.scene import Scene from homeassistant.const import CONF_ICON, CONF_NAME, CONF_PAYLOAD_ON, CONF_UNIQUE_ID +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.reload import async_setup_reload_service -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from . import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, DOMAIN, PLATFORMS from .. import mqtt @@ -35,7 +36,7 @@ async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None ): """Set up MQTT scene through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 65c9e0550e0ba..2dcdce9e019f6 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -15,11 +15,11 @@ CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.reload import async_setup_reload_service -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from homeassistant.util import dt as dt_util from . import CONF_QOS, CONF_STATE_TOPIC, DOMAIN, PLATFORMS, subscription @@ -48,7 +48,7 @@ async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None ): """Set up MQTT sensors through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) diff --git a/homeassistant/components/mqtt/subscription.py b/homeassistant/components/mqtt/subscription.py index e6c99c09fd5a4..6c711600b2ca9 100644 --- a/homeassistant/components/mqtt/subscription.py +++ b/homeassistant/components/mqtt/subscription.py @@ -5,7 +5,7 @@ import attr -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from homeassistant.loader import bind_hass from . import debug_info @@ -18,7 +18,7 @@ class EntitySubscription: """Class to hold data about an active entity topic subscription.""" - hass: HomeAssistantType = attr.ib() + hass: HomeAssistant = attr.ib() topic: str = attr.ib() message_callback: MessageCallbackType = attr.ib() unsubscribe_callback: Callable[[], None] | None = attr.ib() @@ -63,7 +63,7 @@ def _should_resubscribe(self, other): @bind_hass async def async_subscribe_topics( - hass: HomeAssistantType, + hass: HomeAssistant, new_state: dict[str, EntitySubscription] | None, topics: dict[str, Any], ): @@ -106,6 +106,6 @@ async def async_subscribe_topics( @bind_hass -async def async_unsubscribe_topics(hass: HomeAssistantType, sub_state: dict): +async def async_unsubscribe_topics(hass: HomeAssistant, sub_state: dict): """Unsubscribe from all MQTT topics managed by async_subscribe_topics.""" return await async_subscribe_topics(hass, sub_state, {}) diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index 2b272b0f9be98..d07f639f41da2 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -13,11 +13,11 @@ CONF_VALUE_TEMPLATE, STATE_ON, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from . import ( CONF_COMMAND_TOPIC, @@ -52,7 +52,7 @@ async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None ): """Set up MQTT switch through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) diff --git a/tests/components/meteo_france/test_config_flow.py b/tests/components/meteo_france/test_config_flow.py index 0fbe1b5e13599..29c08e41d1d95 100644 --- a/tests/components/meteo_france/test_config_flow.py +++ b/tests/components/meteo_france/test_config_flow.py @@ -13,7 +13,7 @@ ) from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -214,7 +214,7 @@ async def test_abort_if_already_setup(hass, client_single): assert result["reason"] == "already_configured" -async def test_options_flow(hass: HomeAssistantType): +async def test_options_flow(hass: HomeAssistant): """Test config flow options.""" config_entry = MockConfigEntry( domain=DOMAIN, From 1016d4ea28831b7688e3e4d3ce21677311dcbd0d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 22 Apr 2021 14:54:28 -0700 Subject: [PATCH 0458/1317] Support trigger-based template binary sensors (#49504) Co-authored-by: Martin Hjelmare --- .../components/template/binary_sensor.py | 105 +++++++++++++- homeassistant/components/template/config.py | 50 +++++-- .../components/template/trigger_entity.py | 15 +- homeassistant/helpers/template.py | 3 + .../components/template/test_binary_sensor.py | 130 +++++++++++++++++- tests/components/template/test_init.py | 8 +- 6 files changed, 289 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index 42f23b23336f3..83c31406c4a4d 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -1,14 +1,19 @@ """Support for exposing a templated binary sensor.""" from __future__ import annotations +from datetime import timedelta +import logging + import voluptuous as vol from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA, + DOMAIN as BINARY_SENSOR_DOMAIN, ENTITY_ID_FORMAT, PLATFORM_SCHEMA, BinarySensorEntity, ) +from homeassistant.components.template import TriggerUpdateCoordinator from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, @@ -40,6 +45,7 @@ CONF_PICTURE, ) from .template_entity import TemplateEntity +from .trigger_entity import TriggerEntity CONF_DELAY_ON = "delay_on" CONF_DELAY_OFF = "delay_off" @@ -168,8 +174,23 @@ def _async_create_template_tracking_entities(async_add_entities, hass, definitio async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the template binary sensors.""" + if discovery_info is None: + _async_create_template_tracking_entities( + async_add_entities, + hass, + rewrite_legacy_to_modern_conf(config[CONF_SENSORS]), + ) + return + + if "coordinator" in discovery_info: + async_add_entities( + TriggerBinarySensorEntity(hass, discovery_info["coordinator"], config) + for config in discovery_info["entities"] + ) + return + _async_create_template_tracking_entities( - async_add_entities, hass, rewrite_legacy_to_modern_conf(config[CONF_SENSORS]) + async_add_entities, hass, discovery_info["entities"] ) @@ -283,7 +304,7 @@ def _set_state(_): self._state = state self.async_write_ha_state() - delay = (self._delay_on if state else self._delay_off).seconds + delay = (self._delay_on if state else self._delay_off).total_seconds() # state with delay. Cancelled if template result changes. self._delay_cancel = async_call_later(self.hass, delay, _set_state) @@ -306,3 +327,83 @@ def is_on(self): def device_class(self): """Return the sensor class of the binary sensor.""" return self._device_class + + +class TriggerBinarySensorEntity(TriggerEntity, BinarySensorEntity): + """Sensor entity based on trigger data.""" + + domain = BINARY_SENSOR_DOMAIN + extra_template_keys = (CONF_STATE,) + + def __init__( + self, + hass: HomeAssistant, + coordinator: TriggerUpdateCoordinator, + config: dict, + ) -> None: + """Initialize the entity.""" + super().__init__(hass, coordinator, config) + + if isinstance(config.get(CONF_DELAY_ON), template.Template): + self._to_render.append(CONF_DELAY_ON) + self._parse_result.add(CONF_DELAY_ON) + + if isinstance(config.get(CONF_DELAY_OFF), template.Template): + self._to_render.append(CONF_DELAY_OFF) + self._parse_result.add(CONF_DELAY_OFF) + + self._delay_cancel = None + self._state = False + + @property + def is_on(self) -> bool: + """Return state of the sensor.""" + return self._state + + @callback + def _handle_coordinator_update(self) -> None: + """Handle update of the data.""" + self._process_data() + + if self._delay_cancel: + self._delay_cancel() + self._delay_cancel = None + + if not self.available: + return + + raw = self._rendered.get(CONF_STATE) + state = template.result_as_boolean(raw) + + if state == self._state: + return + + key = CONF_DELAY_ON if state else CONF_DELAY_OFF + delay = self._rendered.get(key) or self._config.get(key) + + # state without delay. None means rendering failed. + if state is None or delay is None: + self._state = state + self.async_write_ha_state() + return + + if not isinstance(delay, timedelta): + try: + delay = cv.positive_time_period(delay) + except vol.Invalid as err: + logging.getLogger(__name__).warning( + "Error rendering %s template: %s", key, err + ) + return + + @callback + def _set_state(_): + """Set state of template binary sensor.""" + self._state = state + self.async_set_context(self.coordinator.data["context"]) + self.async_write_ha_state() + + # state with delay. Cancelled if new trigger received + self._delay_cancel = async_call_later( + self.hass, delay.total_seconds(), _set_state + ) diff --git a/homeassistant/components/template/config.py b/homeassistant/components/template/config.py index 8c015d70f1a5e..007f40a6d0a04 100644 --- a/homeassistant/components/template/config.py +++ b/homeassistant/components/template/config.py @@ -3,13 +3,14 @@ import voluptuous as vol +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config import async_log_exception, config_without_domain -from homeassistant.const import CONF_SENSORS, CONF_UNIQUE_ID +from homeassistant.const import CONF_BINARY_SENSORS, CONF_SENSORS, CONF_UNIQUE_ID from homeassistant.helpers import config_validation as cv from homeassistant.helpers.trigger import async_validate_trigger_config -from . import sensor as sensor_platform +from . import binary_sensor as binary_sensor_platform, sensor as sensor_platform from .const import CONF_TRIGGER, DOMAIN CONFIG_SECTION_SCHEMA = vol.Schema( @@ -22,6 +23,12 @@ vol.Optional(CONF_SENSORS): cv.schema_with_slug_keys( sensor_platform.LEGACY_SENSOR_SCHEMA ), + vol.Optional(BINARY_SENSOR_DOMAIN): vol.All( + cv.ensure_list, [binary_sensor_platform.BINARY_SENSOR_SCHEMA] + ), + vol.Optional(CONF_BINARY_SENSORS): cv.schema_with_slug_keys( + binary_sensor_platform.LEGACY_BINARY_SENSOR_SCHEMA + ), } ) @@ -45,17 +52,34 @@ async def async_validate_config(hass, config): async_log_exception(err, DOMAIN, cfg, hass) continue - if CONF_SENSORS in cfg: - logging.getLogger(__name__).warning( - "The entity definition format under template: differs from the platform " - "configuration format. See " - "https://www.home-assistant.io/integrations/template#configuration-for-trigger-based-template-sensors" - ) - sensors = list(cfg[SENSOR_DOMAIN]) if SENSOR_DOMAIN in cfg else [] - sensors.extend( - sensor_platform.rewrite_legacy_to_modern_conf(cfg[CONF_SENSORS]) - ) - cfg = {**cfg, "sensor": sensors} + legacy_warn_printed = False + + for old_key, new_key, transform in ( + ( + CONF_SENSORS, + SENSOR_DOMAIN, + sensor_platform.rewrite_legacy_to_modern_conf, + ), + ( + CONF_BINARY_SENSORS, + BINARY_SENSOR_DOMAIN, + binary_sensor_platform.rewrite_legacy_to_modern_conf, + ), + ): + if old_key not in cfg: + continue + + if not legacy_warn_printed: + legacy_warn_printed = True + logging.getLogger(__name__).warning( + "The entity definition format under template: differs from the platform " + "configuration format. See " + "https://www.home-assistant.io/integrations/template#configuration-for-trigger-based-template-sensors" + ) + + definitions = list(cfg[new_key]) if new_key in cfg else [] + definitions.extend(transform(cfg[old_key])) + cfg = {**cfg, new_key: definitions} config_sections.append(cfg) diff --git a/homeassistant/components/template/trigger_entity.py b/homeassistant/components/template/trigger_entity.py index 418fa976304d7..4ba2a549e6336 100644 --- a/homeassistant/components/template/trigger_entity.py +++ b/homeassistant/components/template/trigger_entity.py @@ -64,6 +64,7 @@ def __init__( # We make a copy so our initial render is 'unknown' and not 'unavailable' self._rendered = dict(self._static_rendered) + self._parse_result = set() @property def name(self): @@ -115,17 +116,18 @@ async def async_added_to_hass(self) -> None: template.attach(self.hass, self._config) await super().async_added_to_hass() if self.coordinator.data is not None: - self._handle_coordinator_update() + self._process_data() @callback - def _handle_coordinator_update(self) -> None: - """Handle updated data from the coordinator.""" + def _process_data(self) -> None: + """Process new data.""" try: rendered = dict(self._static_rendered) for key in self._to_render: rendered[key] = self._config[key].async_render( - self.coordinator.data["run_variables"], parse_result=False + self.coordinator.data["run_variables"], + parse_result=key in self._parse_result, ) if CONF_ATTRIBUTES in self._config: @@ -142,4 +144,9 @@ def _handle_coordinator_update(self) -> None: self._rendered = self._static_rendered self.async_set_context(self.coordinator.data["context"]) + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._process_data() self.async_write_ha_state() diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 053ab2947dd1b..b8721ef91d3fd 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -858,6 +858,9 @@ def result_as_boolean(template_result: str | None) -> bool: False/0/None/'0'/'false'/'no'/'off'/'disable' are considered falsy """ + if template_result is None: + return False + try: # Import here, not at top-level to avoid circular import from homeassistant.helpers import ( # pylint: disable=import-outside-toplevel diff --git a/tests/components/template/test_binary_sensor.py b/tests/components/template/test_binary_sensor.py index e6bdf83e2ff8f..3c38b184418e9 100644 --- a/tests/components/template/test_binary_sensor.py +++ b/tests/components/template/test_binary_sensor.py @@ -12,13 +12,14 @@ STATE_ON, STATE_UNAVAILABLE, ) -from homeassistant.core import CoreState +from homeassistant.core import Context, CoreState +from homeassistant.helpers import entity_registry import homeassistant.util.dt as dt_util from tests.common import async_fire_time_changed -async def test_setup(hass): +async def test_setup_legacy(hass): """Test the setup.""" config = { "binary_sensor": { @@ -906,3 +907,128 @@ async def test_template_validation_error(hass, caplog): state = hass.states.get("binary_sensor.test") assert state.attributes.get("icon") is None + + +async def test_trigger_entity(hass): + """Test trigger entity works.""" + assert await setup.async_setup_component( + hass, + "template", + { + "template": [ + {"invalid": "config"}, + # Config after invalid should still be set up + { + "unique_id": "listening-test-event", + "trigger": {"platform": "event", "event_type": "test_event"}, + "binary_sensors": { + "hello": { + "friendly_name": "Hello Name", + "unique_id": "hello_name-id", + "device_class": "battery", + "value_template": "{{ trigger.event.data.beer == 2 }}", + "entity_picture_template": "{{ '/local/dogs.png' }}", + "icon_template": "{{ 'mdi:pirate' }}", + "attribute_templates": { + "plus_one": "{{ trigger.event.data.beer + 1 }}" + }, + }, + }, + "binary_sensor": [ + { + "name": "via list", + "unique_id": "via_list-id", + "device_class": "battery", + "state": "{{ trigger.event.data.beer == 2 }}", + "picture": "{{ '/local/dogs.png' }}", + "icon": "{{ 'mdi:pirate' }}", + "attributes": { + "plus_one": "{{ trigger.event.data.beer + 1 }}" + }, + } + ], + }, + { + "trigger": [], + "binary_sensors": { + "bare_minimum": { + "value_template": "{{ trigger.event.data.beer == 1 }}" + }, + }, + }, + ], + }, + ) + + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.hello_name") + assert state is not None + assert state.state == "off" + + state = hass.states.get("binary_sensor.bare_minimum") + assert state is not None + assert state.state == "off" + + context = Context() + hass.bus.async_fire("test_event", {"beer": 2}, context=context) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.hello_name") + assert state.state == "on" + assert state.attributes.get("device_class") == "battery" + assert state.attributes.get("icon") == "mdi:pirate" + assert state.attributes.get("entity_picture") == "/local/dogs.png" + assert state.attributes.get("plus_one") == 3 + assert state.context is context + + ent_reg = entity_registry.async_get(hass) + assert len(ent_reg.entities) == 2 + assert ( + ent_reg.entities["binary_sensor.hello_name"].unique_id + == "listening-test-event-hello_name-id" + ) + assert ( + ent_reg.entities["binary_sensor.via_list"].unique_id + == "listening-test-event-via_list-id" + ) + + state = hass.states.get("binary_sensor.via_list") + assert state.state == "on" + assert state.attributes.get("device_class") == "battery" + assert state.attributes.get("icon") == "mdi:pirate" + assert state.attributes.get("entity_picture") == "/local/dogs.png" + assert state.attributes.get("plus_one") == 3 + assert state.context is context + + +async def test_template_with_trigger_templated_delay_on(hass): + """Test binary sensor template with template delay on.""" + config = { + "template": { + "trigger": {"platform": "event", "event_type": "test_event"}, + "binary_sensor": { + "name": "test", + "state": "{{ trigger.event.data.beer == 2 }}", + "device_class": "motion", + "delay_on": '{{ ({ "seconds": 6 / 2 }) }}', + }, + } + } + await setup.async_setup_component(hass, "template", config) + await hass.async_block_till_done() + await hass.async_start() + + state = hass.states.get("binary_sensor.test") + assert state.state == "off" + + context = Context() + hass.bus.async_fire("test_event", {"beer": 2}, context=context) + await hass.async_block_till_done() + + future = dt_util.utcnow() + timedelta(seconds=3) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == "on" diff --git a/tests/components/template/test_init.py b/tests/components/template/test_init.py index 3c098a0729ff1..ddbb165e5094e 100644 --- a/tests/components/template/test_init.py +++ b/tests/components/template/test_init.py @@ -41,6 +41,10 @@ async def test_reloadable(hass): "name": "top level state", "state": "{{ states.sensor.top_level.state }} + 2", }, + "binary_sensor": { + "name": "top level state", + "state": "{{ states.sensor.top_level.state == 'init' }}", + }, }, ], }, @@ -50,15 +54,17 @@ async def test_reloadable(hass): await hass.async_start() await hass.async_block_till_done() assert hass.states.get("sensor.top_level_state").state == "unknown + 2" + assert hass.states.get("binary_sensor.top_level_state").state == "off" hass.bus.async_fire("event_1", {"source": "init"}) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 4 + assert len(hass.states.async_all()) == 5 assert hass.states.get("sensor.state").state == "mytest" assert hass.states.get("sensor.top_level").state == "init" await hass.async_block_till_done() assert hass.states.get("sensor.top_level_state").state == "init + 2" + assert hass.states.get("binary_sensor.top_level_state").state == "on" yaml_path = path.join( _get_fixtures_base_path(), From a9065f381d9fc5adbb9fdedb17980f2521ca67de Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 23 Apr 2021 01:42:28 +0200 Subject: [PATCH 0459/1317] Use supported_color_modes in emulated_hue (#49175) --- .../components/emulated_hue/hue_api.py | 39 +++++++------------ tests/components/emulated_hue/test_hue_api.py | 7 ++-- 2 files changed, 17 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index f97636a46c038..0bb6b82f81393 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -45,9 +45,6 @@ ATTR_HS_COLOR, ATTR_TRANSITION, ATTR_XY_COLOR, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, SUPPORT_TRANSITION, ) from homeassistant.components.media_player.const import ( @@ -356,6 +353,8 @@ async def put(self, request, username, entity_number): # Get the entity's supported features entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + if entity.domain == light.DOMAIN: + color_modes = entity.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES, []) # Parse the request parsed = { @@ -401,7 +400,7 @@ async def put(self, request, username, entity_number): if HUE_API_STATE_BRI in request_json: if entity.domain == light.DOMAIN: - if entity_features & SUPPORT_BRIGHTNESS: + if light.brightness_supported(color_modes): parsed[STATE_ON] = parsed[STATE_BRIGHTNESS] > 0 else: parsed[STATE_BRIGHTNESS] = None @@ -440,14 +439,14 @@ async def put(self, request, username, entity_number): if entity.domain == light.DOMAIN: if parsed[STATE_ON]: if ( - entity_features & SUPPORT_BRIGHTNESS + light.brightness_supported(color_modes) and parsed[STATE_BRIGHTNESS] is not None ): data[ATTR_BRIGHTNESS] = hue_brightness_to_hass( parsed[STATE_BRIGHTNESS] ) - if entity_features & SUPPORT_COLOR: + if light.color_supported(color_modes): if any((parsed[STATE_HUE], parsed[STATE_SATURATION])): if parsed[STATE_HUE] is not None: hue = parsed[STATE_HUE] @@ -469,7 +468,7 @@ async def put(self, request, username, entity_number): data[ATTR_XY_COLOR] = parsed[STATE_XY] if ( - entity_features & SUPPORT_COLOR_TEMP + light.color_temp_supported(color_modes) and parsed[STATE_COLOR_TEMP] is not None ): data[ATTR_COLOR_TEMP] = parsed[STATE_COLOR_TEMP] @@ -671,13 +670,7 @@ def get_entity_state(config, entity): data[STATE_SATURATION] = 0 data[STATE_COLOR_TEMP] = 0 - # Get the entity's supported features - entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - - if entity.domain == light.DOMAIN: - if entity_features & SUPPORT_BRIGHTNESS: - pass - elif entity.domain == climate.DOMAIN: + if entity.domain == climate.DOMAIN: temperature = entity.attributes.get(ATTR_TEMPERATURE, 0) # Convert 0-100 to 0-254 data[STATE_BRIGHTNESS] = round(temperature * HUE_API_STATE_BRI_MAX / 100) @@ -736,6 +729,7 @@ def get_entity_state(config, entity): def entity_to_json(config, entity): """Convert an entity to its Hue bridge JSON representation.""" entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + color_modes = entity.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES, []) unique_id = hashlib.md5(entity.entity_id.encode()).hexdigest() unique_id = f"00:{unique_id[0:2]}:{unique_id[2:4]}:{unique_id[4:6]}:{unique_id[6:8]}:{unique_id[8:10]}:{unique_id[10:12]}:{unique_id[12:14]}-{unique_id[14:16]}" @@ -753,11 +747,7 @@ def entity_to_json(config, entity): "swversion": "123", } - if ( - (entity_features & SUPPORT_BRIGHTNESS) - and (entity_features & SUPPORT_COLOR) - and (entity_features & SUPPORT_COLOR_TEMP) - ): + if light.color_supported(color_modes) and light.color_temp_supported(color_modes): # Extended Color light (Zigbee Device ID: 0x0210) # Same as Color light, but which supports additional setting of color temperature retval["type"] = "Extended color light" @@ -775,7 +765,7 @@ def entity_to_json(config, entity): retval["state"][HUE_API_STATE_COLORMODE] = "hs" else: retval["state"][HUE_API_STATE_COLORMODE] = "ct" - elif (entity_features & SUPPORT_BRIGHTNESS) and (entity_features & SUPPORT_COLOR): + elif light.color_supported(color_modes): # Color light (Zigbee Device ID: 0x0200) # Supports on/off, dimming and color control (hue/saturation, enhanced hue, color loop and XY) retval["type"] = "Color light" @@ -789,9 +779,7 @@ def entity_to_json(config, entity): HUE_API_STATE_EFFECT: "none", } ) - elif (entity_features & SUPPORT_BRIGHTNESS) and ( - entity_features & SUPPORT_COLOR_TEMP - ): + elif light.color_temp_supported(color_modes): # Color temperature light (Zigbee Device ID: 0x0220) # Supports groups, scenes, on/off, dimming, and setting of a color temperature retval["type"] = "Color temperature light" @@ -804,12 +792,11 @@ def entity_to_json(config, entity): } ) elif entity_features & ( - SUPPORT_BRIGHTNESS - | SUPPORT_SET_POSITION + SUPPORT_SET_POSITION | SUPPORT_SET_SPEED | SUPPORT_VOLUME_SET | SUPPORT_TARGET_TEMPERATURE - ): + ) or light.brightness_supported(color_modes): # Dimmable light (Zigbee Device ID: 0x0100) # Supports groups, scenes, on/off and dimming retval["type"] = "Dimmable light" diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index f10786d36de7f..38288270a1b13 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -742,9 +742,10 @@ async def test_put_light_state(hass, hass_hue, hue_client): ) # mock light.turn_on call - hass.states.async_set( - "light.ceiling_lights", STATE_ON, {ATTR_SUPPORTED_FEATURES: 55} - ) + attributes = hass.states.get("light.ceiling_lights").attributes + supported_features = attributes[ATTR_SUPPORTED_FEATURES] | light.SUPPORT_TRANSITION + attributes = {**attributes, ATTR_SUPPORTED_FEATURES: supported_features} + hass.states.async_set("light.ceiling_lights", STATE_ON, attributes) call_turn_on = async_mock_service(hass, "light", "turn_on") # update light state through api From d28b959a0995d0deba63343b73de320b755ca3eb Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 23 Apr 2021 01:46:27 +0200 Subject: [PATCH 0460/1317] Improve sun condition trace (#49551) --- homeassistant/helpers/condition.py | 36 +- tests/components/sun/test_trigger.py | 692 ---------------- tests/helpers/test_condition.py | 1110 +++++++++++++++++++++++++- 3 files changed, 1124 insertions(+), 714 deletions(-) diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index 138fa81947c5f..b6030c61a1cdb 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -97,7 +97,7 @@ def condition_trace_set_result(result: bool, **kwargs: Any) -> None: node.set_result(result=result, **kwargs) -def condition_trace_update_result(result: bool, **kwargs: Any) -> None: +def condition_trace_update_result(**kwargs: Any) -> None: """Update the result of TraceElement at the top of the stack.""" node = trace_stack_top(trace_stack_cv) @@ -106,7 +106,7 @@ def condition_trace_update_result(result: bool, **kwargs: Any) -> None: if not node: return - node.update_result(result=result, **kwargs) + node.update_result(**kwargs) @contextmanager @@ -131,7 +131,7 @@ def wrapper(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool: """Trace condition.""" with trace_condition(variables): result = condition(hass, variables) - condition_trace_update_result(result) + condition_trace_update_result(result=result) return result return wrapper @@ -607,23 +607,37 @@ def sun( if sunrise is None and SUN_EVENT_SUNRISE in (before, after): # There is no sunrise today + condition_trace_set_result(False, message="no sunrise today") return False if sunset is None and SUN_EVENT_SUNSET in (before, after): # There is no sunset today + condition_trace_set_result(False, message="no sunset today") return False - if before == SUN_EVENT_SUNRISE and utcnow > cast(datetime, sunrise) + before_offset: - return False + if before == SUN_EVENT_SUNRISE: + wanted_time_before = cast(datetime, sunrise) + before_offset + condition_trace_update_result(wanted_time_before=wanted_time_before) + if utcnow > wanted_time_before: + return False - if before == SUN_EVENT_SUNSET and utcnow > cast(datetime, sunset) + before_offset: - return False + if before == SUN_EVENT_SUNSET: + wanted_time_before = cast(datetime, sunset) + before_offset + condition_trace_update_result(wanted_time_before=wanted_time_before) + if utcnow > wanted_time_before: + return False - if after == SUN_EVENT_SUNRISE and utcnow < cast(datetime, sunrise) + after_offset: - return False + if after == SUN_EVENT_SUNRISE: + wanted_time_after = cast(datetime, sunrise) + after_offset + condition_trace_update_result(wanted_time_after=wanted_time_after) + if utcnow < wanted_time_after: + return False - if after == SUN_EVENT_SUNSET and utcnow < cast(datetime, sunset) + after_offset: - return False + if after == SUN_EVENT_SUNSET: + wanted_time_after = cast(datetime, sunset) + after_offset + condition_trace_update_result(wanted_time_after=wanted_time_after) + if utcnow < wanted_time_after: + return False return True diff --git a/tests/components/sun/test_trigger.py b/tests/components/sun/test_trigger.py index 54dcef96e28e0..6e6a1f11ef9bd 100644 --- a/tests/components/sun/test_trigger.py +++ b/tests/components/sun/test_trigger.py @@ -168,695 +168,3 @@ async def test_sunrise_trigger_with_offset(hass, calls, legacy_patchable_time): async_fire_time_changed(hass, trigger_time) await hass.async_block_till_done() assert len(calls) == 1 - - -async def test_if_action_before_sunrise_no_offset(hass, calls): - """ - Test if action was before sunrise. - - Before sunrise is true from midnight until sunset, local time. - """ - await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: { - "trigger": {"platform": "event", "event_type": "test_event"}, - "condition": {"condition": "sun", "before": SUN_EVENT_SUNRISE}, - "action": {"service": "test.automation"}, - } - }, - ) - - # sunrise: 2015-09-16 06:33:18 local, sunset: 2015-09-16 18:53:45 local - # sunrise: 2015-09-16 13:33:18 UTC, sunset: 2015-09-17 01:53:45 UTC - # now = sunrise + 1s -> 'before sunrise' not true - now = datetime(2015, 9, 16, 13, 33, 19, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 0 - - # now = sunrise -> 'before sunrise' true - now = datetime(2015, 9, 16, 13, 33, 18, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 1 - - # now = local midnight -> 'before sunrise' true - now = datetime(2015, 9, 16, 7, 0, 0, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 2 - - # now = local midnight - 1s -> 'before sunrise' not true - now = datetime(2015, 9, 17, 6, 59, 59, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 2 - - -async def test_if_action_after_sunrise_no_offset(hass, calls): - """ - Test if action was after sunrise. - - After sunrise is true from sunrise until midnight, local time. - """ - await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: { - "trigger": {"platform": "event", "event_type": "test_event"}, - "condition": {"condition": "sun", "after": SUN_EVENT_SUNRISE}, - "action": {"service": "test.automation"}, - } - }, - ) - - # sunrise: 2015-09-16 06:33:18 local, sunset: 2015-09-16 18:53:45 local - # sunrise: 2015-09-16 13:33:18 UTC, sunset: 2015-09-17 01:53:45 UTC - # now = sunrise - 1s -> 'after sunrise' not true - now = datetime(2015, 9, 16, 13, 33, 17, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 0 - - # now = sunrise + 1s -> 'after sunrise' true - now = datetime(2015, 9, 16, 13, 33, 19, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 1 - - # now = local midnight -> 'after sunrise' not true - now = datetime(2015, 9, 16, 7, 0, 0, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 1 - - # now = local midnight - 1s -> 'after sunrise' true - now = datetime(2015, 9, 17, 6, 59, 59, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 2 - - -async def test_if_action_before_sunrise_with_offset(hass, calls): - """ - Test if action was before sunrise with offset. - - Before sunrise is true from midnight until sunset, local time. - """ - await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: { - "trigger": {"platform": "event", "event_type": "test_event"}, - "condition": { - "condition": "sun", - "before": SUN_EVENT_SUNRISE, - "before_offset": "+1:00:00", - }, - "action": {"service": "test.automation"}, - } - }, - ) - - # sunrise: 2015-09-16 06:33:18 local, sunset: 2015-09-16 18:53:45 local - # sunrise: 2015-09-16 13:33:18 UTC, sunset: 2015-09-17 01:53:45 UTC - # now = sunrise + 1s + 1h -> 'before sunrise' with offset +1h not true - now = datetime(2015, 9, 16, 14, 33, 19, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 0 - - # now = sunrise + 1h -> 'before sunrise' with offset +1h true - now = datetime(2015, 9, 16, 14, 33, 18, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 1 - - # now = UTC midnight -> 'before sunrise' with offset +1h not true - now = datetime(2015, 9, 17, 0, 0, 0, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 1 - - # now = UTC midnight - 1s -> 'before sunrise' with offset +1h not true - now = datetime(2015, 9, 16, 23, 59, 59, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 1 - - # now = local midnight -> 'before sunrise' with offset +1h true - now = datetime(2015, 9, 16, 7, 0, 0, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 2 - - # now = local midnight - 1s -> 'before sunrise' with offset +1h not true - now = datetime(2015, 9, 17, 6, 59, 59, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 2 - - # now = sunset -> 'before sunrise' with offset +1h not true - now = datetime(2015, 9, 17, 1, 53, 45, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 2 - - # now = sunset -1s -> 'before sunrise' with offset +1h not true - now = datetime(2015, 9, 17, 1, 53, 44, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 2 - - -async def test_if_action_before_sunset_with_offset(hass, calls): - """ - Test if action was before sunset with offset. - - Before sunset is true from midnight until sunset, local time. - """ - await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: { - "trigger": {"platform": "event", "event_type": "test_event"}, - "condition": { - "condition": "sun", - "before": "sunset", - "before_offset": "+1:00:00", - }, - "action": {"service": "test.automation"}, - } - }, - ) - - # sunrise: 2015-09-16 06:33:18 local, sunset: 2015-09-16 18:53:45 local - # sunrise: 2015-09-16 13:33:18 UTC, sunset: 2015-09-17 01:53:45 UTC - # now = local midnight -> 'before sunset' with offset +1h true - now = datetime(2015, 9, 16, 7, 0, 0, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 1 - - # now = sunset + 1s + 1h -> 'before sunset' with offset +1h not true - now = datetime(2015, 9, 17, 2, 53, 46, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 1 - - # now = sunset + 1h -> 'before sunset' with offset +1h true - now = datetime(2015, 9, 17, 2, 53, 44, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 2 - - # now = UTC midnight -> 'before sunset' with offset +1h true - now = datetime(2015, 9, 17, 0, 0, 0, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 3 - - # now = UTC midnight - 1s -> 'before sunset' with offset +1h true - now = datetime(2015, 9, 16, 23, 59, 59, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 4 - - # now = sunrise -> 'before sunset' with offset +1h true - now = datetime(2015, 9, 16, 13, 33, 18, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 5 - - # now = sunrise -1s -> 'before sunset' with offset +1h true - now = datetime(2015, 9, 16, 13, 33, 17, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 6 - - # now = local midnight-1s -> 'after sunrise' with offset +1h not true - now = datetime(2015, 9, 17, 6, 59, 59, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 6 - - -async def test_if_action_after_sunrise_with_offset(hass, calls): - """ - Test if action was after sunrise with offset. - - After sunrise is true from sunrise until midnight, local time. - """ - await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: { - "trigger": {"platform": "event", "event_type": "test_event"}, - "condition": { - "condition": "sun", - "after": SUN_EVENT_SUNRISE, - "after_offset": "+1:00:00", - }, - "action": {"service": "test.automation"}, - } - }, - ) - - # sunrise: 2015-09-16 06:33:18 local, sunset: 2015-09-16 18:53:45 local - # sunrise: 2015-09-16 13:33:18 UTC, sunset: 2015-09-17 01:53:45 UTC - # now = sunrise - 1s + 1h -> 'after sunrise' with offset +1h not true - now = datetime(2015, 9, 16, 14, 33, 17, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 0 - - # now = sunrise + 1h -> 'after sunrise' with offset +1h true - now = datetime(2015, 9, 16, 14, 33, 58, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 1 - - # now = UTC noon -> 'after sunrise' with offset +1h not true - now = datetime(2015, 9, 16, 12, 0, 0, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 1 - - # now = UTC noon - 1s -> 'after sunrise' with offset +1h not true - now = datetime(2015, 9, 16, 11, 59, 59, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 1 - - # now = local noon -> 'after sunrise' with offset +1h true - now = datetime(2015, 9, 16, 19, 1, 0, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 2 - - # now = local noon - 1s -> 'after sunrise' with offset +1h true - now = datetime(2015, 9, 16, 18, 59, 59, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 3 - - # now = sunset -> 'after sunrise' with offset +1h true - now = datetime(2015, 9, 17, 1, 53, 45, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 4 - - # now = sunset + 1s -> 'after sunrise' with offset +1h true - now = datetime(2015, 9, 17, 1, 53, 45, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 5 - - # now = local midnight-1s -> 'after sunrise' with offset +1h true - now = datetime(2015, 9, 17, 6, 59, 59, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 6 - - # now = local midnight -> 'after sunrise' with offset +1h not true - now = datetime(2015, 9, 17, 7, 0, 0, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 6 - - -async def test_if_action_after_sunset_with_offset(hass, calls): - """ - Test if action was after sunset with offset. - - After sunset is true from sunset until midnight, local time. - """ - await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: { - "trigger": {"platform": "event", "event_type": "test_event"}, - "condition": { - "condition": "sun", - "after": "sunset", - "after_offset": "+1:00:00", - }, - "action": {"service": "test.automation"}, - } - }, - ) - - # sunrise: 2015-09-16 06:33:18 local, sunset: 2015-09-16 18:53:45 local - # sunrise: 2015-09-16 13:33:18 UTC, sunset: 2015-09-17 01:53:45 UTC - # now = sunset - 1s + 1h -> 'after sunset' with offset +1h not true - now = datetime(2015, 9, 17, 2, 53, 44, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 0 - - # now = sunset + 1h -> 'after sunset' with offset +1h true - now = datetime(2015, 9, 17, 2, 53, 45, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 1 - - # now = midnight-1s -> 'after sunset' with offset +1h true - now = datetime(2015, 9, 16, 6, 59, 59, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 2 - - # now = midnight -> 'after sunset' with offset +1h not true - now = datetime(2015, 9, 16, 7, 0, 0, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 2 - - -async def test_if_action_before_and_after_during(hass, calls): - """ - Test if action was after sunset and before sunrise. - - This is true from sunrise until sunset. - """ - await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: { - "trigger": {"platform": "event", "event_type": "test_event"}, - "condition": { - "condition": "sun", - "after": SUN_EVENT_SUNRISE, - "before": SUN_EVENT_SUNSET, - }, - "action": {"service": "test.automation"}, - } - }, - ) - - # sunrise: 2015-09-16 06:33:18 local, sunset: 2015-09-16 18:53:45 local - # sunrise: 2015-09-16 13:33:18 UTC, sunset: 2015-09-17 01:53:45 UTC - # now = sunrise - 1s -> 'after sunrise' + 'before sunset' not true - now = datetime(2015, 9, 16, 13, 33, 17, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 0 - - # now = sunset + 1s -> 'after sunrise' + 'before sunset' not true - now = datetime(2015, 9, 17, 1, 53, 46, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 0 - - # now = sunrise + 1s -> 'after sunrise' + 'before sunset' true - now = datetime(2015, 9, 16, 13, 33, 19, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 1 - - # now = sunset - 1s -> 'after sunrise' + 'before sunset' true - now = datetime(2015, 9, 17, 1, 53, 44, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 2 - - # now = 9AM local -> 'after sunrise' + 'before sunset' true - now = datetime(2015, 9, 16, 16, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 3 - - -async def test_if_action_before_sunrise_no_offset_kotzebue(hass, calls): - """ - Test if action was before sunrise. - - Local timezone: Alaska time - Location: Kotzebue, which has a very skewed local timezone with sunrise - at 7 AM and sunset at 3AM during summer - After sunrise is true from sunrise until midnight, local time. - """ - tz = dt_util.get_time_zone("America/Anchorage") - dt_util.set_default_time_zone(tz) - hass.config.latitude = 66.5 - hass.config.longitude = 162.4 - await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: { - "trigger": {"platform": "event", "event_type": "test_event"}, - "condition": {"condition": "sun", "before": SUN_EVENT_SUNRISE}, - "action": {"service": "test.automation"}, - } - }, - ) - - # sunrise: 2015-07-24 07:21:12 local, sunset: 2015-07-25 03:13:33 local - # sunrise: 2015-07-24 15:21:12 UTC, sunset: 2015-07-25 11:13:33 UTC - # now = sunrise + 1s -> 'before sunrise' not true - now = datetime(2015, 7, 24, 15, 21, 13, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 0 - - # now = sunrise - 1h -> 'before sunrise' true - now = datetime(2015, 7, 24, 14, 21, 12, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 1 - - # now = local midnight -> 'before sunrise' true - now = datetime(2015, 7, 24, 8, 0, 0, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 2 - - # now = local midnight - 1s -> 'before sunrise' not true - now = datetime(2015, 7, 24, 7, 59, 59, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 2 - - -async def test_if_action_after_sunrise_no_offset_kotzebue(hass, calls): - """ - Test if action was after sunrise. - - Local timezone: Alaska time - Location: Kotzebue, which has a very skewed local timezone with sunrise - at 7 AM and sunset at 3AM during summer - Before sunrise is true from midnight until sunrise, local time. - """ - tz = dt_util.get_time_zone("America/Anchorage") - dt_util.set_default_time_zone(tz) - hass.config.latitude = 66.5 - hass.config.longitude = 162.4 - await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: { - "trigger": {"platform": "event", "event_type": "test_event"}, - "condition": {"condition": "sun", "after": SUN_EVENT_SUNRISE}, - "action": {"service": "test.automation"}, - } - }, - ) - - # sunrise: 2015-07-24 07:21:12 local, sunset: 2015-07-25 03:13:33 local - # sunrise: 2015-07-24 15:21:12 UTC, sunset: 2015-07-25 11:13:33 UTC - # now = sunrise -> 'after sunrise' true - now = datetime(2015, 7, 24, 15, 21, 12, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 1 - - # now = sunrise - 1h -> 'after sunrise' not true - now = datetime(2015, 7, 24, 14, 21, 12, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 1 - - # now = local midnight -> 'after sunrise' not true - now = datetime(2015, 7, 24, 8, 0, 1, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 1 - - # now = local midnight - 1s -> 'after sunrise' true - now = datetime(2015, 7, 24, 7, 59, 59, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 2 - - -async def test_if_action_before_sunset_no_offset_kotzebue(hass, calls): - """ - Test if action was before sunrise. - - Local timezone: Alaska time - Location: Kotzebue, which has a very skewed local timezone with sunrise - at 7 AM and sunset at 3AM during summer - Before sunset is true from midnight until sunset, local time. - """ - tz = dt_util.get_time_zone("America/Anchorage") - dt_util.set_default_time_zone(tz) - hass.config.latitude = 66.5 - hass.config.longitude = 162.4 - await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: { - "trigger": {"platform": "event", "event_type": "test_event"}, - "condition": {"condition": "sun", "before": SUN_EVENT_SUNSET}, - "action": {"service": "test.automation"}, - } - }, - ) - - # sunrise: 2015-07-24 07:21:12 local, sunset: 2015-07-25 03:13:33 local - # sunrise: 2015-07-24 15:21:12 UTC, sunset: 2015-07-25 11:13:33 UTC - # now = sunset + 1s -> 'before sunset' not true - now = datetime(2015, 7, 25, 11, 13, 34, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 0 - - # now = sunset - 1h-> 'before sunset' true - now = datetime(2015, 7, 25, 10, 13, 33, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 1 - - # now = local midnight -> 'before sunrise' true - now = datetime(2015, 7, 24, 8, 0, 0, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 2 - - # now = local midnight - 1s -> 'before sunrise' not true - now = datetime(2015, 7, 24, 7, 59, 59, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 2 - - -async def test_if_action_after_sunset_no_offset_kotzebue(hass, calls): - """ - Test if action was after sunrise. - - Local timezone: Alaska time - Location: Kotzebue, which has a very skewed local timezone with sunrise - at 7 AM and sunset at 3AM during summer - After sunset is true from sunset until midnight, local time. - """ - tz = dt_util.get_time_zone("America/Anchorage") - dt_util.set_default_time_zone(tz) - hass.config.latitude = 66.5 - hass.config.longitude = 162.4 - await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: { - "trigger": {"platform": "event", "event_type": "test_event"}, - "condition": {"condition": "sun", "after": SUN_EVENT_SUNSET}, - "action": {"service": "test.automation"}, - } - }, - ) - - # sunrise: 2015-07-24 07:21:12 local, sunset: 2015-07-25 03:13:33 local - # sunrise: 2015-07-24 15:21:12 UTC, sunset: 2015-07-25 11:13:33 UTC - # now = sunset -> 'after sunset' true - now = datetime(2015, 7, 25, 11, 13, 33, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 1 - - # now = sunset - 1s -> 'after sunset' not true - now = datetime(2015, 7, 25, 11, 13, 32, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 1 - - # now = local midnight -> 'after sunset' not true - now = datetime(2015, 7, 24, 8, 0, 1, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 1 - - # now = local midnight - 1s -> 'after sunset' true - now = datetime(2015, 7, 24, 7, 59, 59, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.utcnow", return_value=now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 2 diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index d46c343dfb166..11705502e7721 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -1,13 +1,41 @@ """Test the condition helper.""" +from datetime import datetime from unittest.mock import patch import pytest +from homeassistant.components import sun +import homeassistant.components.automation as automation +from homeassistant.const import SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET from homeassistant.exceptions import ConditionError, HomeAssistantError from homeassistant.helpers import condition, trace from homeassistant.helpers.template import Template from homeassistant.setup import async_setup_component -from homeassistant.util import dt +import homeassistant.util.dt as dt_util + +from tests.common import async_mock_service + +ORIG_TIME_ZONE = dt_util.DEFAULT_TIME_ZONE + + +@pytest.fixture +def calls(hass): + """Track calls to a mock service.""" + return async_mock_service(hass, "test", "automation") + + +@pytest.fixture(autouse=True) +def setup_comp(hass): + """Initialize components.""" + dt_util.set_default_time_zone(hass.config.time_zone) + hass.loop.run_until_complete( + async_setup_component(hass, sun.DOMAIN, {sun.DOMAIN: {sun.CONF_ELEVATION: 0}}) + ) + + +def teardown(): + """Restore.""" + dt_util.set_default_time_zone(ORIG_TIME_ZONE) def assert_element(trace_element, expected_element, path): @@ -651,28 +679,28 @@ async def test_time_window(hass): with patch( "homeassistant.helpers.condition.dt_util.now", - return_value=dt.now().replace(hour=3), + return_value=dt_util.now().replace(hour=3), ): assert not test1(hass) assert test2(hass) with patch( "homeassistant.helpers.condition.dt_util.now", - return_value=dt.now().replace(hour=9), + return_value=dt_util.now().replace(hour=9), ): assert test1(hass) assert not test2(hass) with patch( "homeassistant.helpers.condition.dt_util.now", - return_value=dt.now().replace(hour=15), + return_value=dt_util.now().replace(hour=15), ): assert test1(hass) assert not test2(hass) with patch( "homeassistant.helpers.condition.dt_util.now", - return_value=dt.now().replace(hour=21), + return_value=dt_util.now().replace(hour=21), ): assert not test1(hass) assert test2(hass) @@ -697,7 +725,7 @@ async def test_time_using_input_datetime(hass): { "entity_id": "input_datetime.am", "datetime": str( - dt.now() + dt_util.now() .replace(hour=6, minute=0, second=0, microsecond=0) .replace(tzinfo=None) ), @@ -711,7 +739,7 @@ async def test_time_using_input_datetime(hass): { "entity_id": "input_datetime.pm", "datetime": str( - dt.now() + dt_util.now() .replace(hour=18, minute=0, second=0, microsecond=0) .replace(tzinfo=None) ), @@ -721,7 +749,7 @@ async def test_time_using_input_datetime(hass): with patch( "homeassistant.helpers.condition.dt_util.now", - return_value=dt.now().replace(hour=3), + return_value=dt_util.now().replace(hour=3), ): assert not condition.time( hass, after="input_datetime.am", before="input_datetime.pm" @@ -732,7 +760,7 @@ async def test_time_using_input_datetime(hass): with patch( "homeassistant.helpers.condition.dt_util.now", - return_value=dt.now().replace(hour=9), + return_value=dt_util.now().replace(hour=9), ): assert condition.time( hass, after="input_datetime.am", before="input_datetime.pm" @@ -743,7 +771,7 @@ async def test_time_using_input_datetime(hass): with patch( "homeassistant.helpers.condition.dt_util.now", - return_value=dt.now().replace(hour=15), + return_value=dt_util.now().replace(hour=15), ): assert condition.time( hass, after="input_datetime.am", before="input_datetime.pm" @@ -754,7 +782,7 @@ async def test_time_using_input_datetime(hass): with patch( "homeassistant.helpers.condition.dt_util.now", - return_value=dt.now().replace(hour=21), + return_value=dt_util.now().replace(hour=21), ): assert not condition.time( hass, after="input_datetime.am", before="input_datetime.pm" @@ -1628,3 +1656,1063 @@ async def test_condition_template_invalid_results(hass): hass, {"condition": "template", "value_template": "{{ [1, 2, 3] }}"} ) assert not test(hass) + + +def _find_run_id(traces, trace_type, item_id): + """Find newest run_id for a script or automation.""" + for _trace in reversed(traces): + if _trace["domain"] == trace_type and _trace["item_id"] == item_id: + return _trace["run_id"] + + return None + + +async def assert_automation_condition_trace(hass_ws_client, automation_id, expected): + """Test the result of automation condition.""" + id = 1 + + def next_id(): + nonlocal id + id += 1 + return id + + client = await hass_ws_client() + + # List traces + await client.send_json( + {"id": next_id(), "type": "trace/list", "domain": "automation"} + ) + response = await client.receive_json() + assert response["success"] + run_id = _find_run_id(response["result"], "automation", automation_id) + + # Get trace + await client.send_json( + { + "id": next_id(), + "type": "trace/get", + "domain": "automation", + "item_id": "sun", + "run_id": run_id, + } + ) + response = await client.receive_json() + assert response["success"] + trace = response["result"] + assert len(trace["trace"]["condition/0"]) == 1 + condition_trace = trace["trace"]["condition/0"][0]["result"] + assert condition_trace == expected + + +async def test_if_action_before_sunrise_no_offset(hass, hass_ws_client, calls): + """ + Test if action was before sunrise. + + Before sunrise is true from midnight until sunset, local time. + """ + await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "id": "sun", + "trigger": {"platform": "event", "event_type": "test_event"}, + "condition": {"condition": "sun", "before": SUN_EVENT_SUNRISE}, + "action": {"service": "test.automation"}, + } + }, + ) + + # sunrise: 2015-09-16 06:33:18 local, sunset: 2015-09-16 18:53:45 local + # sunrise: 2015-09-16 13:33:18 UTC, sunset: 2015-09-17 01:53:45 UTC + # now = sunrise + 1s -> 'before sunrise' not true + now = datetime(2015, 9, 16, 13, 33, 19, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 0 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": False, "wanted_time_before": "2015-09-16T13:33:18.342542+00:00"}, + ) + + # now = sunrise -> 'before sunrise' true + now = datetime(2015, 9, 16, 13, 33, 18, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 1 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": True, "wanted_time_before": "2015-09-16T13:33:18.342542+00:00"}, + ) + + # now = local midnight -> 'before sunrise' true + now = datetime(2015, 9, 16, 7, 0, 0, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 2 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": True, "wanted_time_before": "2015-09-16T13:33:18.342542+00:00"}, + ) + + # now = local midnight - 1s -> 'before sunrise' not true + now = datetime(2015, 9, 17, 6, 59, 59, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 2 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": False, "wanted_time_before": "2015-09-16T13:33:18.342542+00:00"}, + ) + + +async def test_if_action_after_sunrise_no_offset(hass, hass_ws_client, calls): + """ + Test if action was after sunrise. + + After sunrise is true from sunrise until midnight, local time. + """ + await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "id": "sun", + "trigger": {"platform": "event", "event_type": "test_event"}, + "condition": {"condition": "sun", "after": SUN_EVENT_SUNRISE}, + "action": {"service": "test.automation"}, + } + }, + ) + + # sunrise: 2015-09-16 06:33:18 local, sunset: 2015-09-16 18:53:45 local + # sunrise: 2015-09-16 13:33:18 UTC, sunset: 2015-09-17 01:53:45 UTC + # now = sunrise - 1s -> 'after sunrise' not true + now = datetime(2015, 9, 16, 13, 33, 17, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 0 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": False, "wanted_time_after": "2015-09-16T13:33:18.342542+00:00"}, + ) + + # now = sunrise + 1s -> 'after sunrise' true + now = datetime(2015, 9, 16, 13, 33, 19, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 1 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": True, "wanted_time_after": "2015-09-16T13:33:18.342542+00:00"}, + ) + + # now = local midnight -> 'after sunrise' not true + now = datetime(2015, 9, 16, 7, 0, 0, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 1 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": False, "wanted_time_after": "2015-09-16T13:33:18.342542+00:00"}, + ) + + # now = local midnight - 1s -> 'after sunrise' true + now = datetime(2015, 9, 17, 6, 59, 59, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 2 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": True, "wanted_time_after": "2015-09-16T13:33:18.342542+00:00"}, + ) + + +async def test_if_action_before_sunrise_with_offset(hass, hass_ws_client, calls): + """ + Test if action was before sunrise with offset. + + Before sunrise is true from midnight until sunset, local time. + """ + await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "id": "sun", + "trigger": {"platform": "event", "event_type": "test_event"}, + "condition": { + "condition": "sun", + "before": SUN_EVENT_SUNRISE, + "before_offset": "+1:00:00", + }, + "action": {"service": "test.automation"}, + } + }, + ) + + # sunrise: 2015-09-16 06:33:18 local, sunset: 2015-09-16 18:53:45 local + # sunrise: 2015-09-16 13:33:18 UTC, sunset: 2015-09-17 01:53:45 UTC + # now = sunrise + 1s + 1h -> 'before sunrise' with offset +1h not true + now = datetime(2015, 9, 16, 14, 33, 19, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 0 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": False, "wanted_time_before": "2015-09-16T14:33:18.342542+00:00"}, + ) + + # now = sunrise + 1h -> 'before sunrise' with offset +1h true + now = datetime(2015, 9, 16, 14, 33, 18, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 1 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": True, "wanted_time_before": "2015-09-16T14:33:18.342542+00:00"}, + ) + + # now = UTC midnight -> 'before sunrise' with offset +1h not true + now = datetime(2015, 9, 17, 0, 0, 0, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 1 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": False, "wanted_time_before": "2015-09-16T14:33:18.342542+00:00"}, + ) + + # now = UTC midnight - 1s -> 'before sunrise' with offset +1h not true + now = datetime(2015, 9, 16, 23, 59, 59, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 1 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": False, "wanted_time_before": "2015-09-16T14:33:18.342542+00:00"}, + ) + + # now = local midnight -> 'before sunrise' with offset +1h true + now = datetime(2015, 9, 16, 7, 0, 0, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 2 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": True, "wanted_time_before": "2015-09-16T14:33:18.342542+00:00"}, + ) + + # now = local midnight - 1s -> 'before sunrise' with offset +1h not true + now = datetime(2015, 9, 17, 6, 59, 59, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 2 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": False, "wanted_time_before": "2015-09-16T14:33:18.342542+00:00"}, + ) + + # now = sunset -> 'before sunrise' with offset +1h not true + now = datetime(2015, 9, 17, 1, 53, 45, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 2 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": False, "wanted_time_before": "2015-09-16T14:33:18.342542+00:00"}, + ) + + # now = sunset -1s -> 'before sunrise' with offset +1h not true + now = datetime(2015, 9, 17, 1, 53, 44, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 2 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": False, "wanted_time_before": "2015-09-16T14:33:18.342542+00:00"}, + ) + + +async def test_if_action_before_sunset_with_offset(hass, hass_ws_client, calls): + """ + Test if action was before sunset with offset. + + Before sunset is true from midnight until sunset, local time. + """ + await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "id": "sun", + "trigger": {"platform": "event", "event_type": "test_event"}, + "condition": { + "condition": "sun", + "before": "sunset", + "before_offset": "+1:00:00", + }, + "action": {"service": "test.automation"}, + } + }, + ) + + # sunrise: 2015-09-16 06:33:18 local, sunset: 2015-09-16 18:53:45 local + # sunrise: 2015-09-16 13:33:18 UTC, sunset: 2015-09-17 01:53:45 UTC + # now = local midnight -> 'before sunset' with offset +1h true + now = datetime(2015, 9, 16, 7, 0, 0, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 1 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": True, "wanted_time_before": "2015-09-17T02:53:44.723614+00:00"}, + ) + + # now = sunset + 1s + 1h -> 'before sunset' with offset +1h not true + now = datetime(2015, 9, 17, 2, 53, 46, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 1 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": False, "wanted_time_before": "2015-09-17T02:53:44.723614+00:00"}, + ) + + # now = sunset + 1h -> 'before sunset' with offset +1h true + now = datetime(2015, 9, 17, 2, 53, 44, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 2 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": True, "wanted_time_before": "2015-09-17T02:53:44.723614+00:00"}, + ) + + # now = UTC midnight -> 'before sunset' with offset +1h true + now = datetime(2015, 9, 17, 0, 0, 0, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 3 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": True, "wanted_time_before": "2015-09-17T02:53:44.723614+00:00"}, + ) + + # now = UTC midnight - 1s -> 'before sunset' with offset +1h true + now = datetime(2015, 9, 16, 23, 59, 59, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 4 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": True, "wanted_time_before": "2015-09-17T02:53:44.723614+00:00"}, + ) + + # now = sunrise -> 'before sunset' with offset +1h true + now = datetime(2015, 9, 16, 13, 33, 18, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 5 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": True, "wanted_time_before": "2015-09-17T02:53:44.723614+00:00"}, + ) + + # now = sunrise -1s -> 'before sunset' with offset +1h true + now = datetime(2015, 9, 16, 13, 33, 17, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 6 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": True, "wanted_time_before": "2015-09-17T02:53:44.723614+00:00"}, + ) + + # now = local midnight-1s -> 'after sunrise' with offset +1h not true + now = datetime(2015, 9, 17, 6, 59, 59, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 6 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": False, "wanted_time_before": "2015-09-17T02:53:44.723614+00:00"}, + ) + + +async def test_if_action_after_sunrise_with_offset(hass, hass_ws_client, calls): + """ + Test if action was after sunrise with offset. + + After sunrise is true from sunrise until midnight, local time. + """ + await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "id": "sun", + "trigger": {"platform": "event", "event_type": "test_event"}, + "condition": { + "condition": "sun", + "after": SUN_EVENT_SUNRISE, + "after_offset": "+1:00:00", + }, + "action": {"service": "test.automation"}, + } + }, + ) + + # sunrise: 2015-09-16 06:33:18 local, sunset: 2015-09-16 18:53:45 local + # sunrise: 2015-09-16 13:33:18 UTC, sunset: 2015-09-17 01:53:45 UTC + # now = sunrise - 1s + 1h -> 'after sunrise' with offset +1h not true + now = datetime(2015, 9, 16, 14, 33, 17, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 0 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": False, "wanted_time_after": "2015-09-16T14:33:18.342542+00:00"}, + ) + + # now = sunrise + 1h -> 'after sunrise' with offset +1h true + now = datetime(2015, 9, 16, 14, 33, 58, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 1 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": True, "wanted_time_after": "2015-09-16T14:33:18.342542+00:00"}, + ) + + # now = UTC noon -> 'after sunrise' with offset +1h not true + now = datetime(2015, 9, 16, 12, 0, 0, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 1 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": False, "wanted_time_after": "2015-09-16T14:33:18.342542+00:00"}, + ) + + # now = UTC noon - 1s -> 'after sunrise' with offset +1h not true + now = datetime(2015, 9, 16, 11, 59, 59, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 1 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": False, "wanted_time_after": "2015-09-16T14:33:18.342542+00:00"}, + ) + + # now = local noon -> 'after sunrise' with offset +1h true + now = datetime(2015, 9, 16, 19, 1, 0, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 2 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": True, "wanted_time_after": "2015-09-16T14:33:18.342542+00:00"}, + ) + + # now = local noon - 1s -> 'after sunrise' with offset +1h true + now = datetime(2015, 9, 16, 18, 59, 59, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 3 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": True, "wanted_time_after": "2015-09-16T14:33:18.342542+00:00"}, + ) + + # now = sunset -> 'after sunrise' with offset +1h true + now = datetime(2015, 9, 17, 1, 53, 45, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 4 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": True, "wanted_time_after": "2015-09-16T14:33:18.342542+00:00"}, + ) + + # now = sunset + 1s -> 'after sunrise' with offset +1h true + now = datetime(2015, 9, 17, 1, 53, 45, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 5 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": True, "wanted_time_after": "2015-09-16T14:33:18.342542+00:00"}, + ) + + # now = local midnight-1s -> 'after sunrise' with offset +1h true + now = datetime(2015, 9, 17, 6, 59, 59, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 6 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": True, "wanted_time_after": "2015-09-16T14:33:18.342542+00:00"}, + ) + + # now = local midnight -> 'after sunrise' with offset +1h not true + now = datetime(2015, 9, 17, 7, 0, 0, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 6 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": False, "wanted_time_after": "2015-09-17T14:33:57.053037+00:00"}, + ) + + +async def test_if_action_after_sunset_with_offset(hass, hass_ws_client, calls): + """ + Test if action was after sunset with offset. + + After sunset is true from sunset until midnight, local time. + """ + await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "id": "sun", + "trigger": {"platform": "event", "event_type": "test_event"}, + "condition": { + "condition": "sun", + "after": "sunset", + "after_offset": "+1:00:00", + }, + "action": {"service": "test.automation"}, + } + }, + ) + + # sunrise: 2015-09-16 06:33:18 local, sunset: 2015-09-16 18:53:45 local + # sunrise: 2015-09-16 13:33:18 UTC, sunset: 2015-09-17 01:53:45 UTC + # now = sunset - 1s + 1h -> 'after sunset' with offset +1h not true + now = datetime(2015, 9, 17, 2, 53, 44, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 0 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": False, "wanted_time_after": "2015-09-17T02:53:44.723614+00:00"}, + ) + + # now = sunset + 1h -> 'after sunset' with offset +1h true + now = datetime(2015, 9, 17, 2, 53, 45, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 1 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": True, "wanted_time_after": "2015-09-17T02:53:44.723614+00:00"}, + ) + + # now = midnight-1s -> 'after sunset' with offset +1h true + now = datetime(2015, 9, 16, 6, 59, 59, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 2 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": True, "wanted_time_after": "2015-09-16T02:55:06.099767+00:00"}, + ) + + # now = midnight -> 'after sunset' with offset +1h not true + now = datetime(2015, 9, 16, 7, 0, 0, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 2 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": False, "wanted_time_after": "2015-09-17T02:53:44.723614+00:00"}, + ) + + +async def test_if_action_before_and_after_during(hass, hass_ws_client, calls): + """ + Test if action was after sunset and before sunrise. + + This is true from sunrise until sunset. + """ + await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "id": "sun", + "trigger": {"platform": "event", "event_type": "test_event"}, + "condition": { + "condition": "sun", + "after": SUN_EVENT_SUNRISE, + "before": SUN_EVENT_SUNSET, + }, + "action": {"service": "test.automation"}, + } + }, + ) + + # sunrise: 2015-09-16 06:33:18 local, sunset: 2015-09-16 18:53:45 local + # sunrise: 2015-09-16 13:33:18 UTC, sunset: 2015-09-17 01:53:45 UTC + # now = sunrise - 1s -> 'after sunrise' + 'before sunset' not true + now = datetime(2015, 9, 16, 13, 33, 17, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 0 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + { + "result": False, + "wanted_time_before": "2015-09-17T01:53:44.723614+00:00", + "wanted_time_after": "2015-09-16T13:33:18.342542+00:00", + }, + ) + + # now = sunset + 1s -> 'after sunrise' + 'before sunset' not true + now = datetime(2015, 9, 17, 1, 53, 46, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 0 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": False, "wanted_time_before": "2015-09-17T01:53:44.723614+00:00"}, + ) + + # now = sunrise + 1s -> 'after sunrise' + 'before sunset' true + now = datetime(2015, 9, 16, 13, 33, 19, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 1 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + { + "result": True, + "wanted_time_before": "2015-09-17T01:53:44.723614+00:00", + "wanted_time_after": "2015-09-16T13:33:18.342542+00:00", + }, + ) + + # now = sunset - 1s -> 'after sunrise' + 'before sunset' true + now = datetime(2015, 9, 17, 1, 53, 44, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 2 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + { + "result": True, + "wanted_time_before": "2015-09-17T01:53:44.723614+00:00", + "wanted_time_after": "2015-09-16T13:33:18.342542+00:00", + }, + ) + + # now = 9AM local -> 'after sunrise' + 'before sunset' true + now = datetime(2015, 9, 16, 16, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 3 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + { + "result": True, + "wanted_time_before": "2015-09-17T01:53:44.723614+00:00", + "wanted_time_after": "2015-09-16T13:33:18.342542+00:00", + }, + ) + + +async def test_if_action_before_sunrise_no_offset_kotzebue(hass, hass_ws_client, calls): + """ + Test if action was before sunrise. + + Local timezone: Alaska time + Location: Kotzebue, which has a very skewed local timezone with sunrise + at 7 AM and sunset at 3AM during summer + After sunrise is true from sunrise until midnight, local time. + """ + tz = dt_util.get_time_zone("America/Anchorage") + dt_util.set_default_time_zone(tz) + hass.config.latitude = 66.5 + hass.config.longitude = 162.4 + await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "id": "sun", + "trigger": {"platform": "event", "event_type": "test_event"}, + "condition": {"condition": "sun", "before": SUN_EVENT_SUNRISE}, + "action": {"service": "test.automation"}, + } + }, + ) + + # sunrise: 2015-07-24 07:21:12 local, sunset: 2015-07-25 03:13:33 local + # sunrise: 2015-07-24 15:21:12 UTC, sunset: 2015-07-25 11:13:33 UTC + # now = sunrise + 1s -> 'before sunrise' not true + now = datetime(2015, 7, 24, 15, 21, 13, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 0 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": False, "wanted_time_before": "2015-07-24T15:16:46.975735+00:00"}, + ) + + # now = sunrise - 1h -> 'before sunrise' true + now = datetime(2015, 7, 24, 14, 21, 12, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 1 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": True, "wanted_time_before": "2015-07-24T15:16:46.975735+00:00"}, + ) + + # now = local midnight -> 'before sunrise' true + now = datetime(2015, 7, 24, 8, 0, 0, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 2 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": True, "wanted_time_before": "2015-07-24T15:16:46.975735+00:00"}, + ) + + # now = local midnight - 1s -> 'before sunrise' not true + now = datetime(2015, 7, 24, 7, 59, 59, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 2 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": False, "wanted_time_before": "2015-07-23T15:12:19.155123+00:00"}, + ) + + +async def test_if_action_after_sunrise_no_offset_kotzebue(hass, hass_ws_client, calls): + """ + Test if action was after sunrise. + + Local timezone: Alaska time + Location: Kotzebue, which has a very skewed local timezone with sunrise + at 7 AM and sunset at 3AM during summer + Before sunrise is true from midnight until sunrise, local time. + """ + tz = dt_util.get_time_zone("America/Anchorage") + dt_util.set_default_time_zone(tz) + hass.config.latitude = 66.5 + hass.config.longitude = 162.4 + await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "id": "sun", + "trigger": {"platform": "event", "event_type": "test_event"}, + "condition": {"condition": "sun", "after": SUN_EVENT_SUNRISE}, + "action": {"service": "test.automation"}, + } + }, + ) + + # sunrise: 2015-07-24 07:21:12 local, sunset: 2015-07-25 03:13:33 local + # sunrise: 2015-07-24 15:21:12 UTC, sunset: 2015-07-25 11:13:33 UTC + # now = sunrise -> 'after sunrise' true + now = datetime(2015, 7, 24, 15, 21, 12, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 1 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": True, "wanted_time_after": "2015-07-24T15:16:46.975735+00:00"}, + ) + + # now = sunrise - 1h -> 'after sunrise' not true + now = datetime(2015, 7, 24, 14, 21, 12, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 1 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": False, "wanted_time_after": "2015-07-24T15:16:46.975735+00:00"}, + ) + + # now = local midnight -> 'after sunrise' not true + now = datetime(2015, 7, 24, 8, 0, 1, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 1 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": False, "wanted_time_after": "2015-07-24T15:16:46.975735+00:00"}, + ) + + # now = local midnight - 1s -> 'after sunrise' true + now = datetime(2015, 7, 24, 7, 59, 59, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 2 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": True, "wanted_time_after": "2015-07-23T15:12:19.155123+00:00"}, + ) + + +async def test_if_action_before_sunset_no_offset_kotzebue(hass, hass_ws_client, calls): + """ + Test if action was before sunrise. + + Local timezone: Alaska time + Location: Kotzebue, which has a very skewed local timezone with sunrise + at 7 AM and sunset at 3AM during summer + Before sunset is true from midnight until sunset, local time. + """ + tz = dt_util.get_time_zone("America/Anchorage") + dt_util.set_default_time_zone(tz) + hass.config.latitude = 66.5 + hass.config.longitude = 162.4 + await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "id": "sun", + "trigger": {"platform": "event", "event_type": "test_event"}, + "condition": {"condition": "sun", "before": SUN_EVENT_SUNSET}, + "action": {"service": "test.automation"}, + } + }, + ) + + # sunrise: 2015-07-24 07:21:12 local, sunset: 2015-07-25 03:13:33 local + # sunrise: 2015-07-24 15:21:12 UTC, sunset: 2015-07-25 11:13:33 UTC + # now = sunset + 1s -> 'before sunset' not true + now = datetime(2015, 7, 25, 11, 13, 34, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 0 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": False, "wanted_time_before": "2015-07-25T11:13:32.501837+00:00"}, + ) + + # now = sunset - 1h-> 'before sunset' true + now = datetime(2015, 7, 25, 10, 13, 33, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 1 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": True, "wanted_time_before": "2015-07-25T11:13:32.501837+00:00"}, + ) + + # now = local midnight -> 'before sunrise' true + now = datetime(2015, 7, 24, 8, 0, 0, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 2 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": True, "wanted_time_before": "2015-07-24T11:17:54.446913+00:00"}, + ) + + # now = local midnight - 1s -> 'before sunrise' not true + now = datetime(2015, 7, 24, 7, 59, 59, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 2 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": False, "wanted_time_before": "2015-07-23T11:22:18.467277+00:00"}, + ) + + +async def test_if_action_after_sunset_no_offset_kotzebue(hass, hass_ws_client, calls): + """ + Test if action was after sunrise. + + Local timezone: Alaska time + Location: Kotzebue, which has a very skewed local timezone with sunrise + at 7 AM and sunset at 3AM during summer + After sunset is true from sunset until midnight, local time. + """ + tz = dt_util.get_time_zone("America/Anchorage") + dt_util.set_default_time_zone(tz) + hass.config.latitude = 66.5 + hass.config.longitude = 162.4 + await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "id": "sun", + "trigger": {"platform": "event", "event_type": "test_event"}, + "condition": {"condition": "sun", "after": SUN_EVENT_SUNSET}, + "action": {"service": "test.automation"}, + } + }, + ) + + # sunrise: 2015-07-24 07:21:12 local, sunset: 2015-07-25 03:13:33 local + # sunrise: 2015-07-24 15:21:12 UTC, sunset: 2015-07-25 11:13:33 UTC + # now = sunset -> 'after sunset' true + now = datetime(2015, 7, 25, 11, 13, 33, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 1 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": True, "wanted_time_after": "2015-07-25T11:13:32.501837+00:00"}, + ) + + # now = sunset - 1s -> 'after sunset' not true + now = datetime(2015, 7, 25, 11, 13, 32, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 1 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": False, "wanted_time_after": "2015-07-25T11:13:32.501837+00:00"}, + ) + + # now = local midnight -> 'after sunset' not true + now = datetime(2015, 7, 24, 8, 0, 1, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 1 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": False, "wanted_time_after": "2015-07-24T11:17:54.446913+00:00"}, + ) + + # now = local midnight - 1s -> 'after sunset' true + now = datetime(2015, 7, 24, 7, 59, 59, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 2 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": True, "wanted_time_after": "2015-07-23T11:22:18.467277+00:00"}, + ) From b3c9d854f5547f83acc295a11a91821766e46065 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 23 Apr 2021 01:47:33 +0200 Subject: [PATCH 0461/1317] Correct min and max mired for light with color_mode support (#49572) --- homeassistant/components/light/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 1b55aa51c4539..835c4fd2fa93b 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -635,17 +635,16 @@ def capability_attributes(self): """Return capability attributes.""" data = {} supported_features = self.supported_features + supported_color_modes = self._light_internal_supported_color_modes - if supported_features & SUPPORT_COLOR_TEMP: + if COLOR_MODE_COLOR_TEMP in supported_color_modes: data[ATTR_MIN_MIREDS] = self.min_mireds data[ATTR_MAX_MIREDS] = self.max_mireds if supported_features & SUPPORT_EFFECT: data[ATTR_EFFECT_LIST] = self.effect_list - data[ATTR_SUPPORTED_COLOR_MODES] = sorted( - self._light_internal_supported_color_modes - ) + data[ATTR_SUPPORTED_COLOR_MODES] = sorted(supported_color_modes) return data From 2502e7669c9fce7b4f1074a79713db968e85151c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 23 Apr 2021 01:59:41 +0200 Subject: [PATCH 0462/1317] Remove SUPPORT_WHITE_VALUE from ZHA light groups (#49569) --- homeassistant/components/zha/light.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 9a74a23fc2e15..2aadb1199a2f9 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -23,14 +23,12 @@ ATTR_HS_COLOR, ATTR_MAX_MIREDS, ATTR_MIN_MIREDS, - ATTR_WHITE_VALUE, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_TRANSITION, - SUPPORT_WHITE_VALUE, ) from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_UNAVAILABLE from homeassistant.core import State, callback @@ -90,7 +88,6 @@ | SUPPORT_FLASH | SUPPORT_COLOR | SUPPORT_TRANSITION - | SUPPORT_WHITE_VALUE ) @@ -131,7 +128,6 @@ def __init__(self, *args, **kwargs): self._color_temp: int | None = None self._min_mireds: int | None = 153 self._max_mireds: int | None = 500 - self._white_value: int | None = None self._effect_list: list[str] | None = None self._effect: str | None = None self._supported_features: int = 0 @@ -598,8 +594,6 @@ async def async_update(self) -> None: on_states, ATTR_HS_COLOR, reduce=helpers.mean_tuple ) - self._white_value = helpers.reduce_attribute(on_states, ATTR_WHITE_VALUE) - self._color_temp = helpers.reduce_attribute(on_states, ATTR_COLOR_TEMP) self._min_mireds = helpers.reduce_attribute( states, ATTR_MIN_MIREDS, default=153, reduce=min From 686c92097fb1ab44c6d9d377c54d70d987412adf Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Fri, 23 Apr 2021 00:03:48 +0000 Subject: [PATCH 0463/1317] [ci skip] Translation update --- .../components/denonavr/translations/ca.json | 1 + .../components/denonavr/translations/en.json | 4 ++-- .../components/denonavr/translations/et.json | 1 + .../components/denonavr/translations/nl.json | 1 + .../components/denonavr/translations/no.json | 1 + .../components/denonavr/translations/ru.json | 1 + .../denonavr/translations/zh-Hant.json | 1 + .../devolo_home_control/translations/en.json | 1 + .../components/picnic/translations/ca.json | 22 +++++++++++++++++++ .../components/picnic/translations/en.json | 10 ++++----- .../components/picnic/translations/et.json | 22 +++++++++++++++++++ .../components/picnic/translations/nl.json | 10 ++++----- .../components/picnic/translations/no.json | 22 +++++++++++++++++++ .../components/picnic/translations/ru.json | 22 +++++++++++++++++++ .../picnic/translations/zh-Hant.json | 22 +++++++++++++++++++ .../components/smarttub/translations/ca.json | 6 ++++- .../components/smarttub/translations/en.json | 3 ++- .../components/smarttub/translations/et.json | 6 ++++- .../components/smarttub/translations/no.json | 6 ++++- .../components/smarttub/translations/ru.json | 6 ++++- .../components/tuya/translations/no.json | 4 ++-- 21 files changed, 153 insertions(+), 19 deletions(-) create mode 100644 homeassistant/components/picnic/translations/ca.json create mode 100644 homeassistant/components/picnic/translations/et.json create mode 100644 homeassistant/components/picnic/translations/no.json create mode 100644 homeassistant/components/picnic/translations/ru.json create mode 100644 homeassistant/components/picnic/translations/zh-Hant.json diff --git a/homeassistant/components/denonavr/translations/ca.json b/homeassistant/components/denonavr/translations/ca.json index 01f91f894c2e9..3f0c846e10f16 100644 --- a/homeassistant/components/denonavr/translations/ca.json +++ b/homeassistant/components/denonavr/translations/ca.json @@ -37,6 +37,7 @@ "init": { "data": { "show_all_sources": "Mostra totes les fonts", + "update_audyssey": "Actualitza la configuraci\u00f3 d'Audyssey", "zone2": "Configura la Zona 2", "zone3": "Configura la Zona 3" }, diff --git a/homeassistant/components/denonavr/translations/en.json b/homeassistant/components/denonavr/translations/en.json index b39a5608f81c5..a538dad62b99e 100644 --- a/homeassistant/components/denonavr/translations/en.json +++ b/homeassistant/components/denonavr/translations/en.json @@ -37,9 +37,9 @@ "init": { "data": { "show_all_sources": "Show all sources", + "update_audyssey": "Update Audyssey settings", "zone2": "Set up Zone 2", - "zone3": "Set up Zone 3", - "update_audyssey": "Update Audyssey settings" + "zone3": "Set up Zone 3" }, "description": "Specify optional settings", "title": "Denon AVR Network Receivers" diff --git a/homeassistant/components/denonavr/translations/et.json b/homeassistant/components/denonavr/translations/et.json index 45869680bdaf7..edba2158e6912 100644 --- a/homeassistant/components/denonavr/translations/et.json +++ b/homeassistant/components/denonavr/translations/et.json @@ -37,6 +37,7 @@ "init": { "data": { "show_all_sources": "Kuva k\u00f5ik sisendid", + "update_audyssey": "Uuenda Audyssey s\u00e4tteid", "zone2": "Seadista tsoon 2", "zone3": "Seadista tsoon 3" }, diff --git a/homeassistant/components/denonavr/translations/nl.json b/homeassistant/components/denonavr/translations/nl.json index 2cf2ea7976877..fc4d17fe1048f 100644 --- a/homeassistant/components/denonavr/translations/nl.json +++ b/homeassistant/components/denonavr/translations/nl.json @@ -37,6 +37,7 @@ "init": { "data": { "show_all_sources": "Toon alle bronnen", + "update_audyssey": "Audyssey-instellingen bijwerken", "zone2": "Stel Zone 2 in", "zone3": "Stel Zone 3 in" }, diff --git a/homeassistant/components/denonavr/translations/no.json b/homeassistant/components/denonavr/translations/no.json index 93afb82e3c1d1..c2cac347e77d1 100644 --- a/homeassistant/components/denonavr/translations/no.json +++ b/homeassistant/components/denonavr/translations/no.json @@ -37,6 +37,7 @@ "init": { "data": { "show_all_sources": "Vis alle kilder", + "update_audyssey": "Oppdater Audyssey-innstillingene", "zone2": "Sett opp sone 2", "zone3": "Sett opp sone 3" }, diff --git a/homeassistant/components/denonavr/translations/ru.json b/homeassistant/components/denonavr/translations/ru.json index f914d83bfa2f7..6a3397023d35a 100644 --- a/homeassistant/components/denonavr/translations/ru.json +++ b/homeassistant/components/denonavr/translations/ru.json @@ -37,6 +37,7 @@ "init": { "data": { "show_all_sources": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u0432\u0441\u0435 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0438", + "update_audyssey": "\u041e\u0431\u043d\u043e\u0432\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Audyssey", "zone2": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0437\u043e\u043d\u044b 2", "zone3": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0437\u043e\u043d\u044b 3" }, diff --git a/homeassistant/components/denonavr/translations/zh-Hant.json b/homeassistant/components/denonavr/translations/zh-Hant.json index 053dd143e1620..96bf7b00f92f2 100644 --- a/homeassistant/components/denonavr/translations/zh-Hant.json +++ b/homeassistant/components/denonavr/translations/zh-Hant.json @@ -37,6 +37,7 @@ "init": { "data": { "show_all_sources": "\u986f\u793a\u6240\u6709\u4f86\u6e90", + "update_audyssey": "\u66f4\u65b0 Audyssey \u8a2d\u5b9a", "zone2": "\u8a2d\u5b9a\u5340\u57df 2", "zone3": "\u8a2d\u5b9a\u5340\u57df 3" }, diff --git a/homeassistant/components/devolo_home_control/translations/en.json b/homeassistant/components/devolo_home_control/translations/en.json index d1b8645072f1c..e358e47ef0b1b 100644 --- a/homeassistant/components/devolo_home_control/translations/en.json +++ b/homeassistant/components/devolo_home_control/translations/en.json @@ -9,6 +9,7 @@ "step": { "user": { "data": { + "home_control_url": "Home Control URL", "mydevolo_url": "mydevolo URL", "password": "Password", "username": "Email / devolo ID" diff --git a/homeassistant/components/picnic/translations/ca.json b/homeassistant/components/picnic/translations/ca.json new file mode 100644 index 0000000000000..c81d180aef006 --- /dev/null +++ b/homeassistant/components/picnic/translations/ca.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "country_code": "Codi de pa\u00eds", + "password": "Contrasenya", + "username": "Nom d'usuari" + } + } + } + }, + "title": "Picnic" +} \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/en.json b/homeassistant/components/picnic/translations/en.json index 2732abe8adc61..c7097df12a961 100644 --- a/homeassistant/components/picnic/translations/en.json +++ b/homeassistant/components/picnic/translations/en.json @@ -1,19 +1,19 @@ { "config": { "abort": { - "already_configured": "Picnic integration is already configured" + "already_configured": "Device is already configured" }, "error": { - "cannot_connect": "Failed to connect to Picnic server", - "invalid_auth": "Invalid credentials", + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, "step": { "user": { "data": { + "country_code": "Country code", "password": "Password", - "username": "Username", - "country_code": "County code" + "username": "Username" } } } diff --git a/homeassistant/components/picnic/translations/et.json b/homeassistant/components/picnic/translations/et.json new file mode 100644 index 0000000000000..11fc0f1fe881e --- /dev/null +++ b/homeassistant/components/picnic/translations/et.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "T\u00f5rge \u00fchendamisel", + "invalid_auth": "Tuvastamine nurjus", + "unknown": "Tundmatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "country_code": "Riigi kood", + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + } + } + } + }, + "title": "" +} \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/nl.json b/homeassistant/components/picnic/translations/nl.json index 78879f10b616c..210eebdf35717 100644 --- a/homeassistant/components/picnic/translations/nl.json +++ b/homeassistant/components/picnic/translations/nl.json @@ -1,19 +1,19 @@ { "config": { "abort": { - "already_configured": "Picnic integratie is al geconfigureerd" + "already_configured": "Apparaat is al geconfigureerd" }, "error": { - "cannot_connect": "Kan niet verbinden met Picnic server", - "invalid_auth": "Verkeerde gebruikersnaam/wachtwoord", + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" }, "step": { "user": { "data": { + "country_code": "Landcode", "password": "Wachtwoord", - "username": "Gebruikersnaam", - "country_code": "Landcode" + "username": "Gebruikersnaam" } } } diff --git a/homeassistant/components/picnic/translations/no.json b/homeassistant/components/picnic/translations/no.json new file mode 100644 index 0000000000000..45e3bcbb5487b --- /dev/null +++ b/homeassistant/components/picnic/translations/no.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "country_code": "Landskode", + "password": "Passord", + "username": "Brukernavn" + } + } + } + }, + "title": "Picnic" +} \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/ru.json b/homeassistant/components/picnic/translations/ru.json new file mode 100644 index 0000000000000..e754faf8a0e96 --- /dev/null +++ b/homeassistant/components/picnic/translations/ru.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "country_code": "\u041a\u043e\u0434 \u0441\u0442\u0440\u0430\u043d\u044b", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + } + } + } + }, + "title": "Picnic" +} \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/zh-Hant.json b/homeassistant/components/picnic/translations/zh-Hant.json new file mode 100644 index 0000000000000..2f72809d4fe91 --- /dev/null +++ b/homeassistant/components/picnic/translations/zh-Hant.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "country_code": "\u570b\u78bc", + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + } + } + }, + "title": "Picnic" +} \ No newline at end of file diff --git a/homeassistant/components/smarttub/translations/ca.json b/homeassistant/components/smarttub/translations/ca.json index 6d882abeee658..16ad53b1ff60d 100644 --- a/homeassistant/components/smarttub/translations/ca.json +++ b/homeassistant/components/smarttub/translations/ca.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "El dispositiu ja est\u00e0 configurat", + "already_configured": "El compte ja ha estat configurat", "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { @@ -9,6 +9,10 @@ "unknown": "Error inesperat" }, "step": { + "reauth_confirm": { + "description": "La integraci\u00f3 SmartTub ha de tornar a autenticar el teu compte", + "title": "Reautenticaci\u00f3 de la integraci\u00f3" + }, "user": { "data": { "email": "Correu electr\u00f2nic", diff --git a/homeassistant/components/smarttub/translations/en.json b/homeassistant/components/smarttub/translations/en.json index 752faa76b95f3..d7e1476ed33e4 100644 --- a/homeassistant/components/smarttub/translations/en.json +++ b/homeassistant/components/smarttub/translations/en.json @@ -5,7 +5,8 @@ "reauth_successful": "Re-authentication was successful" }, "error": { - "invalid_auth": "Invalid authentication" + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" }, "step": { "reauth_confirm": { diff --git a/homeassistant/components/smarttub/translations/et.json b/homeassistant/components/smarttub/translations/et.json index 676edee158421..2d39fee07b6e8 100644 --- a/homeassistant/components/smarttub/translations/et.json +++ b/homeassistant/components/smarttub/translations/et.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "already_configured": "Konto on juba h\u00e4\u00e4lestatud", "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { @@ -9,6 +9,10 @@ "unknown": "Ootamatu t\u00f5rge" }, "step": { + "reauth_confirm": { + "description": "SmartTubi sidumise konto tuleb taastuvastada", + "title": "Taastuvastamine" + }, "user": { "data": { "email": "E-posti aadress", diff --git a/homeassistant/components/smarttub/translations/no.json b/homeassistant/components/smarttub/translations/no.json index 7f1c5982d28ad..9fd441b57a6b0 100644 --- a/homeassistant/components/smarttub/translations/no.json +++ b/homeassistant/components/smarttub/translations/no.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Enheten er allerede konfigurert", + "already_configured": "Kontoen er allerede konfigurert", "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { @@ -9,6 +9,10 @@ "unknown": "Uventet feil" }, "step": { + "reauth_confirm": { + "description": "SmartTub-integrasjonen m\u00e5 godkjenne kontoen din p\u00e5 nytt", + "title": "Godkjenne integrering p\u00e5 nytt" + }, "user": { "data": { "email": "E-post", diff --git a/homeassistant/components/smarttub/translations/ru.json b/homeassistant/components/smarttub/translations/ru.json index 44f27877d9364..9a138fc043980 100644 --- a/homeassistant/components/smarttub/translations/ru.json +++ b/homeassistant/components/smarttub/translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, "error": { @@ -9,6 +9,10 @@ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { + "reauth_confirm": { + "description": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 SmartTub", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + }, "user": { "data": { "email": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b", diff --git a/homeassistant/components/tuya/translations/no.json b/homeassistant/components/tuya/translations/no.json index d02a88f4097c1..eedf24be696fe 100644 --- a/homeassistant/components/tuya/translations/no.json +++ b/homeassistant/components/tuya/translations/no.json @@ -14,10 +14,10 @@ "data": { "country_code": "Din landskode for kontoen din (f.eks. 1 for USA eller 86 for Kina)", "password": "Passord", - "platform": "Appen der kontoen din registreres", + "platform": "Appen der kontoen din er registrert", "username": "Brukernavn" }, - "description": "Skriv inn din Tuya-legitimasjon.", + "description": "Angi Tuya-legitimasjonen din.", "title": "" } } From 48695869f917a23595423331e9e754d226b0aa60 Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Fri, 23 Apr 2021 05:23:15 +0200 Subject: [PATCH 0464/1317] Change dict[str, Any] to FlowResultDict (#49546) --- .../components/bsblan/config_flow.py | 6 +++--- .../components/canary/config_flow.py | 5 +++-- .../components/climacell/config_flow.py | 5 +++-- .../components/coronavirus/config_flow.py | 3 ++- .../components/denonavr/config_flow.py | 9 +++++---- .../components/directv/config_flow.py | 9 +++++---- .../components/elgato/config_flow.py | 11 ++++++----- .../components/enphase_envoy/config_flow.py | 3 ++- .../homematicip_cloud/config_flow.py | 11 +++++------ homeassistant/components/hue/config_flow.py | 5 +++-- homeassistant/components/ipp/config_flow.py | 9 +++++---- .../components/litejet/config_flow.py | 3 ++- homeassistant/components/met/config_flow.py | 5 ++--- .../components/mysensors/config_flow.py | 3 ++- .../components/nzbget/config_flow.py | 5 +++-- .../components/plum_lightpad/config_flow.py | 6 +++--- homeassistant/components/roku/config_flow.py | 10 +++++----- .../components/rpi_power/config_flow.py | 3 ++- .../components/sentry/config_flow.py | 5 +++-- homeassistant/components/sma/config_flow.py | 5 +++-- .../components/solaredge/config_flow.py | 5 +++-- .../components/sonarr/config_flow.py | 9 +++++---- .../components/spotify/config_flow.py | 7 ++++--- homeassistant/components/toon/config_flow.py | 9 +++++---- .../components/twentemilieu/config_flow.py | 5 +++-- .../components/verisure/config_flow.py | 13 +++++++------ homeassistant/components/vizio/config_flow.py | 19 ++++++++++--------- homeassistant/components/wled/config_flow.py | 15 +++++++-------- .../config_flow/integration/config_flow.py | 3 ++- 29 files changed, 113 insertions(+), 93 deletions(-) diff --git a/homeassistant/components/bsblan/config_flow.py b/homeassistant/components/bsblan/config_flow.py index f5df1df043728..9ccad6089b2ed 100644 --- a/homeassistant/components/bsblan/config_flow.py +++ b/homeassistant/components/bsblan/config_flow.py @@ -2,13 +2,13 @@ from __future__ import annotations import logging -from typing import Any from bsblan import BSBLan, BSBLanError, Info import voluptuous as vol from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL, ConfigFlow from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME +from homeassistant.data_entry_flow import FlowResultDict from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import ConfigType @@ -25,7 +25,7 @@ class BSBLanFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: ConfigType | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle a flow initiated by the user.""" if user_input is None: return self._show_setup_form() @@ -57,7 +57,7 @@ async def async_step_user( }, ) - def _show_setup_form(self, errors: dict | None = None) -> dict[str, Any]: + def _show_setup_form(self, errors: dict | None = None) -> FlowResultDict: """Show the setup form to the user.""" return self.async_show_form( step_id="user", diff --git a/homeassistant/components/canary/config_flow.py b/homeassistant/components/canary/config_flow.py index d02be83a7eef2..f01c558024e72 100644 --- a/homeassistant/components/canary/config_flow.py +++ b/homeassistant/components/canary/config_flow.py @@ -11,6 +11,7 @@ from homeassistant.config_entries import CONN_CLASS_CLOUD_POLL, ConfigFlow, OptionsFlow from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResultDict from homeassistant.helpers.typing import ConfigType from .const import ( @@ -52,13 +53,13 @@ def async_get_options_flow(config_entry): async def async_step_import( self, user_input: ConfigType | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle a flow initiated by configuration file.""" return await self.async_step_user(user_input) async def async_step_user( self, user_input: ConfigType | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle a flow initiated by the user.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") diff --git a/homeassistant/components/climacell/config_flow.py b/homeassistant/components/climacell/config_flow.py index 69cf0c052a104..5c5bb86a4794b 100644 --- a/homeassistant/components/climacell/config_flow.py +++ b/homeassistant/components/climacell/config_flow.py @@ -22,6 +22,7 @@ CONF_NAME, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResultDict from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -89,7 +90,7 @@ def __init__(self, config_entry: config_entries.ConfigEntry) -> None: async def async_step_init( self, user_input: dict[str, Any] = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Manage the ClimaCell options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) @@ -122,7 +123,7 @@ def async_get_options_flow( async def async_step_user( self, user_input: dict[str, Any] = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle the initial step.""" errors = {} if user_input is not None: diff --git a/homeassistant/components/coronavirus/config_flow.py b/homeassistant/components/coronavirus/config_flow.py index 4f6e865fa37fb..4bf1dcd56b9ac 100644 --- a/homeassistant/components/coronavirus/config_flow.py +++ b/homeassistant/components/coronavirus/config_flow.py @@ -6,6 +6,7 @@ import voluptuous as vol from homeassistant import config_entries +from homeassistant.data_entry_flow import FlowResultDict from . import get_coordinator from .const import DOMAIN, OPTION_WORLDWIDE @@ -21,7 +22,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle the initial step.""" errors = {} diff --git a/homeassistant/components/denonavr/config_flow.py b/homeassistant/components/denonavr/config_flow.py index 695c323e1f799..190c5e55af96b 100644 --- a/homeassistant/components/denonavr/config_flow.py +++ b/homeassistant/components/denonavr/config_flow.py @@ -13,6 +13,7 @@ from homeassistant.components import ssdp from homeassistant.const import CONF_HOST, CONF_TYPE from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResultDict from homeassistant.helpers.httpx_client import get_async_client from .receiver import ConnectDenonAVR @@ -134,7 +135,7 @@ async def async_step_user(self, user_input: dict[str, Any] | None = None): async def async_step_select( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle multiple receivers found.""" errors = {} if user_input is not None: @@ -155,7 +156,7 @@ async def async_step_select( async def async_step_confirm( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Allow the user to confirm adding the device.""" if user_input is not None: return await self.async_step_connect() @@ -165,7 +166,7 @@ async def async_step_confirm( async def async_step_connect( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Connect to the receiver.""" connect_denonavr = ConnectDenonAVR( self.host, @@ -214,7 +215,7 @@ async def async_step_connect( }, ) - async def async_step_ssdp(self, discovery_info: dict[str, Any]) -> dict[str, Any]: + async def async_step_ssdp(self, discovery_info: dict[str, Any]) -> FlowResultDict: """Handle a discovered Denon AVR. This flow is triggered by the SSDP component. It will check if the diff --git a/homeassistant/components/directv/config_flow.py b/homeassistant/components/directv/config_flow.py index 3b8b591371618..78e44f2f1eed7 100644 --- a/homeassistant/components/directv/config_flow.py +++ b/homeassistant/components/directv/config_flow.py @@ -12,6 +12,7 @@ from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL, ConfigFlow from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultDict from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -47,7 +48,7 @@ def __init__(self): async def async_step_user( self, user_input: ConfigType | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle a flow initiated by the user.""" if user_input is None: return self._show_setup_form() @@ -69,7 +70,7 @@ async def async_step_user( async def async_step_ssdp( self, discovery_info: DiscoveryInfoType - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle SSDP discovery.""" host = urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname receiver_id = None @@ -102,7 +103,7 @@ async def async_step_ssdp( async def async_step_ssdp_confirm( self, user_input: ConfigType = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle a confirmation flow initiated by SSDP.""" if user_input is None: return self.async_show_form( @@ -116,7 +117,7 @@ async def async_step_ssdp_confirm( data=self.discovery_info, ) - def _show_setup_form(self, errors: dict | None = None) -> dict[str, Any]: + def _show_setup_form(self, errors: dict | None = None) -> FlowResultDict: """Show the setup form to the user.""" return self.async_show_form( step_id="user", diff --git a/homeassistant/components/elgato/config_flow.py b/homeassistant/components/elgato/config_flow.py index afdbe7e1cdc47..2f3e25d4fb58c 100644 --- a/homeassistant/components/elgato/config_flow.py +++ b/homeassistant/components/elgato/config_flow.py @@ -9,6 +9,7 @@ from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL, ConfigFlow from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResultDict from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import CONF_SERIAL_NUMBER, DOMAIN @@ -26,7 +27,7 @@ class ElgatoFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle a flow initiated by the user.""" if user_input is None: return self._async_show_setup_form() @@ -43,7 +44,7 @@ async def async_step_user( async def async_step_zeroconf( self, discovery_info: dict[str, Any] - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle zeroconf discovery.""" self.host = discovery_info[CONF_HOST] self.port = discovery_info[CONF_PORT] @@ -61,14 +62,14 @@ async def async_step_zeroconf( async def async_step_zeroconf_confirm( self, _: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle a flow initiated by zeroconf.""" return self._async_create_entry() @callback def _async_show_setup_form( self, errors: dict[str, str] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Show the setup form to the user.""" return self.async_show_form( step_id="user", @@ -82,7 +83,7 @@ def _async_show_setup_form( ) @callback - def _async_create_entry(self) -> dict[str, Any]: + def _async_create_entry(self) -> FlowResultDict: return self.async_create_entry( title=self.serial_number, data={ diff --git a/homeassistant/components/enphase_envoy/config_flow.py b/homeassistant/components/enphase_envoy/config_flow.py index a47a095fde77e..5f8fdcfd0e7b7 100644 --- a/homeassistant/components/enphase_envoy/config_flow.py +++ b/homeassistant/components/enphase_envoy/config_flow.py @@ -17,6 +17,7 @@ CONF_USERNAME, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResultDict from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.httpx_client import get_async_client @@ -131,7 +132,7 @@ async def async_step_reauth(self, user_input): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle the initial step.""" errors = {} diff --git a/homeassistant/components/homematicip_cloud/config_flow.py b/homeassistant/components/homematicip_cloud/config_flow.py index d90d8d7023b8a..4e18e4fdb6b57 100644 --- a/homeassistant/components/homematicip_cloud/config_flow.py +++ b/homeassistant/components/homematicip_cloud/config_flow.py @@ -1,11 +1,10 @@ """Config flow to configure the HomematicIP Cloud component.""" from __future__ import annotations -from typing import Any - import voluptuous as vol from homeassistant import config_entries +from homeassistant.data_entry_flow import FlowResultDict from .const import ( _LOGGER, @@ -29,11 +28,11 @@ def __init__(self) -> None: """Initialize HomematicIP Cloud config flow.""" self.auth = None - async def async_step_user(self, user_input=None) -> dict[str, Any]: + async def async_step_user(self, user_input=None) -> FlowResultDict: """Handle a flow initialized by the user.""" return await self.async_step_init(user_input) - async def async_step_init(self, user_input=None) -> dict[str, Any]: + async def async_step_init(self, user_input=None) -> FlowResultDict: """Handle a flow start.""" errors = {} @@ -64,7 +63,7 @@ async def async_step_init(self, user_input=None) -> dict[str, Any]: errors=errors, ) - async def async_step_link(self, user_input=None) -> dict[str, Any]: + async def async_step_link(self, user_input=None) -> FlowResultDict: """Attempt to link with the HomematicIP Cloud access point.""" errors = {} @@ -86,7 +85,7 @@ async def async_step_link(self, user_input=None) -> dict[str, Any]: return self.async_show_form(step_id="link", errors=errors) - async def async_step_import(self, import_info) -> dict[str, Any]: + async def async_step_import(self, import_info) -> FlowResultDict: """Import a new access point as a config entry.""" hapid = import_info[HMIPC_HAPID].replace("-", "").upper() authtoken = import_info[HMIPC_AUTHTOKEN] diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index 9fd025d7b6ad7..1864d724b8cc1 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -14,6 +14,7 @@ from homeassistant.components import ssdp from homeassistant.const import CONF_HOST, CONF_USERNAME from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResultDict from homeassistant.helpers import aiohttp_client from .bridge import authenticate_bridge @@ -117,7 +118,7 @@ async def async_step_init(self, user_input=None): async def async_step_manual( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle manual bridge setup.""" if user_input is None: return self.async_show_form( @@ -252,7 +253,7 @@ def __init__(self, config_entry): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Manage Hue options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/ipp/config_flow.py b/homeassistant/components/ipp/config_flow.py index d2624931ea0d6..7c8a394731d57 100644 --- a/homeassistant/components/ipp/config_flow.py +++ b/homeassistant/components/ipp/config_flow.py @@ -23,6 +23,7 @@ CONF_SSL, CONF_VERIFY_SSL, ) +from homeassistant.data_entry_flow import FlowResultDict from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import ConfigType, HomeAssistantType @@ -63,7 +64,7 @@ def __init__(self): async def async_step_user( self, user_input: ConfigType | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle a flow initiated by the user.""" if user_input is None: return self._show_setup_form() @@ -99,7 +100,7 @@ async def async_step_user( return self.async_create_entry(title=user_input[CONF_HOST], data=user_input) - async def async_step_zeroconf(self, discovery_info: ConfigType) -> dict[str, Any]: + async def async_step_zeroconf(self, discovery_info: ConfigType) -> FlowResultDict: """Handle zeroconf discovery.""" port = discovery_info[CONF_PORT] zctype = discovery_info["type"] @@ -167,7 +168,7 @@ async def async_step_zeroconf(self, discovery_info: ConfigType) -> dict[str, Any async def async_step_zeroconf_confirm( self, user_input: ConfigType = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle a confirmation flow initiated by zeroconf.""" if user_input is None: return self.async_show_form( @@ -181,7 +182,7 @@ async def async_step_zeroconf_confirm( data=self.discovery_info, ) - def _show_setup_form(self, errors: dict | None = None) -> dict[str, Any]: + def _show_setup_form(self, errors: dict | None = None) -> FlowResultDict: """Show the setup form to the user.""" return self.async_show_form( step_id="user", diff --git a/homeassistant/components/litejet/config_flow.py b/homeassistant/components/litejet/config_flow.py index 124b229c78600..d453d6a90aa89 100644 --- a/homeassistant/components/litejet/config_flow.py +++ b/homeassistant/components/litejet/config_flow.py @@ -10,6 +10,7 @@ from homeassistant import config_entries from homeassistant.const import CONF_PORT +from homeassistant.data_entry_flow import FlowResultDict from .const import DOMAIN @@ -21,7 +22,7 @@ class LiteJetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Create a LiteJet config entry based upon user input.""" if self.hass.config_entries.async_entries(DOMAIN): return self.async_abort(reason="single_instance_allowed") diff --git a/homeassistant/components/met/config_flow.py b/homeassistant/components/met/config_flow.py index 5cfd71ea80121..895a6e33d2dda 100644 --- a/homeassistant/components/met/config_flow.py +++ b/homeassistant/components/met/config_flow.py @@ -1,13 +1,12 @@ """Config flow to configure Met component.""" from __future__ import annotations -from typing import Any - import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResultDict import homeassistant.helpers.config_validation as cv from .const import ( @@ -81,7 +80,7 @@ async def _show_config_form( errors=self._errors, ) - async def async_step_import(self, user_input: dict | None = None) -> dict[str, Any]: + async def async_step_import(self, user_input: dict | None = None) -> FlowResultDict: """Handle configuration by yaml file.""" return await self.async_step_user(user_input) diff --git a/homeassistant/components/mysensors/config_flow.py b/homeassistant/components/mysensors/config_flow.py index 4fd52f29bf368..5299b76d2f53f 100644 --- a/homeassistant/components/mysensors/config_flow.py +++ b/homeassistant/components/mysensors/config_flow.py @@ -27,6 +27,7 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResultDict import homeassistant.helpers.config_validation as cv from . import CONF_RETAIN, CONF_VERSION, DEFAULT_VERSION @@ -281,7 +282,7 @@ async def async_step_gw_mqtt(self, user_input: dict[str, str] | None = None): @callback def _async_create_entry( self, user_input: dict[str, str] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Create the config entry.""" return self.async_create_entry( title=f"{user_input[CONF_DEVICE]}", diff --git a/homeassistant/components/nzbget/config_flow.py b/homeassistant/components/nzbget/config_flow.py index 980fbc1b2f9f8..c0feffd9cef9c 100644 --- a/homeassistant/components/nzbget/config_flow.py +++ b/homeassistant/components/nzbget/config_flow.py @@ -18,6 +18,7 @@ CONF_VERIFY_SSL, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResultDict from homeassistant.helpers.typing import ConfigType from .const import ( @@ -66,7 +67,7 @@ def async_get_options_flow(config_entry): async def async_step_import( self, user_input: ConfigType | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle a flow initiated by configuration file.""" if CONF_SCAN_INTERVAL in user_input: user_input[CONF_SCAN_INTERVAL] = user_input[ @@ -77,7 +78,7 @@ async def async_step_import( async def async_step_user( self, user_input: ConfigType | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle a flow initiated by the user.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") diff --git a/homeassistant/components/plum_lightpad/config_flow.py b/homeassistant/components/plum_lightpad/config_flow.py index 40432810cc5fa..f5ff0f2e06d72 100644 --- a/homeassistant/components/plum_lightpad/config_flow.py +++ b/homeassistant/components/plum_lightpad/config_flow.py @@ -2,7 +2,6 @@ from __future__ import annotations import logging -from typing import Any from aiohttp import ContentTypeError from requests.exceptions import ConnectTimeout, HTTPError @@ -10,6 +9,7 @@ from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.data_entry_flow import FlowResultDict from homeassistant.helpers.typing import ConfigType from .const import DOMAIN @@ -37,7 +37,7 @@ def _show_form(self, errors=None): async def async_step_user( self, user_input: ConfigType | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle a flow initialized by the user or redirected to by import.""" if not user_input: return self._show_form() @@ -61,6 +61,6 @@ async def async_step_user( async def async_step_import( self, import_config: ConfigType | None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Import a config entry from configuration.yaml.""" return await self.async_step_user(import_config) diff --git a/homeassistant/components/roku/config_flow.py b/homeassistant/components/roku/config_flow.py index d10e22cd1bc1a..ae79c214d3fe5 100644 --- a/homeassistant/components/roku/config_flow.py +++ b/homeassistant/components/roku/config_flow.py @@ -2,7 +2,6 @@ from __future__ import annotations import logging -from typing import Any from urllib.parse import urlparse from rokuecp import Roku, RokuError @@ -16,6 +15,7 @@ from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL, ConfigFlow from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResultDict from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN @@ -54,7 +54,7 @@ def __init__(self): self.discovery_info = {} @callback - def _show_form(self, errors: dict | None = None) -> dict[str, Any]: + def _show_form(self, errors: dict | None = None) -> FlowResultDict: """Show the form to the user.""" return self.async_show_form( step_id="user", @@ -62,7 +62,7 @@ def _show_form(self, errors: dict | None = None) -> dict[str, Any]: errors=errors or {}, ) - async def async_step_user(self, user_input: dict | None = None) -> dict[str, Any]: + async def async_step_user(self, user_input: dict | None = None) -> FlowResultDict: """Handle a flow initialized by the user.""" if not user_input: return self._show_form() @@ -115,7 +115,7 @@ async def async_step_homekit(self, discovery_info): async def async_step_ssdp( self, discovery_info: dict | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle a flow initialized by discovery.""" host = urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname name = discovery_info[ATTR_UPNP_FRIENDLY_NAME] @@ -141,7 +141,7 @@ async def async_step_ssdp( async def async_step_discovery_confirm( self, user_input: dict | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle user-confirmation of discovered device.""" if user_input is None: return self.async_show_form( diff --git a/homeassistant/components/rpi_power/config_flow.py b/homeassistant/components/rpi_power/config_flow.py index b635972f43fe5..01e789f23b79a 100644 --- a/homeassistant/components/rpi_power/config_flow.py +++ b/homeassistant/components/rpi_power/config_flow.py @@ -7,6 +7,7 @@ from homeassistant import config_entries from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultDict from homeassistant.helpers.config_entry_flow import DiscoveryFlowHandler from .const import DOMAIN @@ -34,7 +35,7 @@ def __init__(self) -> None: async def async_step_onboarding( self, data: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle a flow initialized by onboarding.""" has_devices = await self._discovery_function(self.hass) diff --git a/homeassistant/components/sentry/config_flow.py b/homeassistant/components/sentry/config_flow.py index b294fa4623666..115a4747a7aab 100644 --- a/homeassistant/components/sentry/config_flow.py +++ b/homeassistant/components/sentry/config_flow.py @@ -9,6 +9,7 @@ from homeassistant import config_entries from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResultDict from .const import ( CONF_DSN, @@ -48,7 +49,7 @@ def async_get_options_flow( async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle a user config flow.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -79,7 +80,7 @@ def __init__(self, config_entry: config_entries.ConfigEntry): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Manage Sentry options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/sma/config_flow.py b/homeassistant/components/sma/config_flow.py index e4186ec987e4f..95be954dba572 100644 --- a/homeassistant/components/sma/config_flow.py +++ b/homeassistant/components/sma/config_flow.py @@ -16,6 +16,7 @@ CONF_SSL, CONF_VERIFY_SSL, ) +from homeassistant.data_entry_flow import FlowResultDict from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -68,7 +69,7 @@ def __init__(self) -> None: async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """First step in config flow.""" errors = {} if user_input is not None: @@ -117,7 +118,7 @@ async def async_step_user( async def async_step_import( self, import_config: dict[str, Any] | None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Import a config flow from configuration.""" device_info = await validate_input(self.hass, import_config) import_config[DEVICE_INFO] = device_info diff --git a/homeassistant/components/solaredge/config_flow.py b/homeassistant/components/solaredge/config_flow.py index eecd11d7b128f..07f987fb00941 100644 --- a/homeassistant/components/solaredge/config_flow.py +++ b/homeassistant/components/solaredge/config_flow.py @@ -10,6 +10,7 @@ from homeassistant import config_entries from homeassistant.const import CONF_API_KEY, CONF_NAME from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResultDict from homeassistant.util import slugify from .const import CONF_SITE_ID, DEFAULT_NAME, DOMAIN @@ -56,7 +57,7 @@ def _check_site(self, site_id: str, api_key: str) -> bool: async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Step when user initializes a integration.""" self._errors = {} if user_input is not None: @@ -92,7 +93,7 @@ async def async_step_user( async def async_step_import( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Import a config entry.""" if self._site_in_configuration_exists(user_input[CONF_SITE_ID]): return self.async_abort(reason="already_configured") diff --git a/homeassistant/components/sonarr/config_flow.py b/homeassistant/components/sonarr/config_flow.py index acee381591c7a..7b1d991c871ca 100644 --- a/homeassistant/components/sonarr/config_flow.py +++ b/homeassistant/components/sonarr/config_flow.py @@ -16,6 +16,7 @@ CONF_VERIFY_SSL, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResultDict from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import ConfigType @@ -75,7 +76,7 @@ def async_get_options_flow(config_entry): """Get the options flow for this handler.""" return SonarrOptionsFlowHandler(config_entry) - async def async_step_reauth(self, data: ConfigType | None = None) -> dict[str, Any]: + async def async_step_reauth(self, data: ConfigType | None = None) -> FlowResultDict: """Handle configuration by re-auth.""" self._reauth = True self._entry_data = dict(data) @@ -86,7 +87,7 @@ async def async_step_reauth(self, data: ConfigType | None = None) -> dict[str, A async def async_step_reauth_confirm( self, user_input: ConfigType | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Confirm reauth dialog.""" if user_input is None: return self.async_show_form( @@ -100,7 +101,7 @@ async def async_step_reauth_confirm( async def async_step_user( self, user_input: ConfigType | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle a flow initiated by the user.""" errors = {} @@ -139,7 +140,7 @@ async def async_step_user( async def _async_reauth_update_entry( self, entry_id: str, data: dict - ) -> dict[str, Any]: + ) -> FlowResultDict: """Update existing config entry.""" entry = self.hass.config_entries.async_get_entry(entry_id) self.hass.config_entries.async_update_entry(entry, data=data) diff --git a/homeassistant/components/spotify/config_flow.py b/homeassistant/components/spotify/config_flow.py index d0fb73e18bdb3..0fb23f7c56ff0 100644 --- a/homeassistant/components/spotify/config_flow.py +++ b/homeassistant/components/spotify/config_flow.py @@ -9,6 +9,7 @@ from homeassistant import config_entries from homeassistant.components import persistent_notification +from homeassistant.data_entry_flow import FlowResultDict from homeassistant.helpers import config_entry_oauth2_flow from .const import DOMAIN, SPOTIFY_SCOPES @@ -38,7 +39,7 @@ def extra_authorize_data(self) -> dict[str, Any]: """Extra data that needs to be appended to the authorize url.""" return {"scope": ",".join(SPOTIFY_SCOPES)} - async def async_oauth_create_entry(self, data: dict[str, Any]) -> dict[str, Any]: + async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResultDict: """Create an entry for Spotify.""" spotify = Spotify(auth=data["token"]["access_token"]) @@ -60,7 +61,7 @@ async def async_oauth_create_entry(self, data: dict[str, Any]) -> dict[str, Any] return self.async_create_entry(title=name, data=data) - async def async_step_reauth(self, entry: dict[str, Any]) -> dict[str, Any]: + async def async_step_reauth(self, entry: dict[str, Any]) -> FlowResultDict: """Perform reauth upon migration of old entries.""" if entry: self.entry = entry @@ -76,7 +77,7 @@ async def async_step_reauth(self, entry: dict[str, Any]) -> dict[str, Any]: async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Confirm reauth dialog.""" if user_input is None: return self.async_show_form( diff --git a/homeassistant/components/toon/config_flow.py b/homeassistant/components/toon/config_flow.py index bc673d2d181b5..ee76f6472cebc 100644 --- a/homeassistant/components/toon/config_flow.py +++ b/homeassistant/components/toon/config_flow.py @@ -8,6 +8,7 @@ import voluptuous as vol from homeassistant import config_entries +from homeassistant.data_entry_flow import FlowResultDict from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.config_entry_oauth2_flow import AbstractOAuth2FlowHandler @@ -29,7 +30,7 @@ def logger(self) -> logging.Logger: """Return logger.""" return logging.getLogger(__name__) - async def async_oauth_create_entry(self, data: dict[str, Any]) -> dict[str, Any]: + async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResultDict: """Test connection and load up agreements.""" self.data = data @@ -49,7 +50,7 @@ async def async_oauth_create_entry(self, data: dict[str, Any]) -> dict[str, Any] async def async_step_import( self, config: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Start a configuration flow based on imported data. This step is merely here to trigger "discovery" when the `toon` @@ -66,7 +67,7 @@ async def async_step_import( async def async_step_agreement( self, user_input: dict[str, Any] = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Select Toon agreement to add.""" if len(self.agreements) == 1: return await self._create_entry(self.agreements[0]) @@ -87,7 +88,7 @@ async def async_step_agreement( agreement_index = agreements_list.index(user_input[CONF_AGREEMENT]) return await self._create_entry(self.agreements[agreement_index]) - async def _create_entry(self, agreement: Agreement) -> dict[str, Any]: + async def _create_entry(self, agreement: Agreement) -> FlowResultDict: if CONF_MIGRATE in self.context: await self.hass.config_entries.async_remove(self.context[CONF_MIGRATE]) diff --git a/homeassistant/components/twentemilieu/config_flow.py b/homeassistant/components/twentemilieu/config_flow.py index 25cdd57b26d8e..7dedf705f915a 100644 --- a/homeassistant/components/twentemilieu/config_flow.py +++ b/homeassistant/components/twentemilieu/config_flow.py @@ -13,6 +13,7 @@ from homeassistant import config_entries from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_ID +from homeassistant.data_entry_flow import FlowResultDict from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import CONF_HOUSE_LETTER, CONF_HOUSE_NUMBER, CONF_POST_CODE, DOMAIN @@ -26,7 +27,7 @@ class TwenteMilieuFlowHandler(ConfigFlow, domain=DOMAIN): async def _show_setup_form( self, errors: dict[str, str] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Show the setup form to the user.""" return self.async_show_form( step_id="user", @@ -42,7 +43,7 @@ async def _show_setup_form( async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle a flow initiated by the user.""" if user_input is None: return await self._show_setup_form(user_input) diff --git a/homeassistant/components/verisure/config_flow.py b/homeassistant/components/verisure/config_flow.py index 3a434cd8b4829..6e984d72e2d59 100644 --- a/homeassistant/components/verisure/config_flow.py +++ b/homeassistant/components/verisure/config_flow.py @@ -19,6 +19,7 @@ ) from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResultDict from .const import ( CONF_GIID, @@ -57,7 +58,7 @@ def async_get_options_flow(config_entry: ConfigEntry) -> VerisureOptionsFlowHand async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle the initial step.""" errors: dict[str, str] = {} @@ -96,7 +97,7 @@ async def async_step_user( async def async_step_installation( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Select Verisure installation to add.""" if len(self.installations) == 1: user_input = {CONF_GIID: list(self.installations)[0]} @@ -124,14 +125,14 @@ async def async_step_installation( }, ) - async def async_step_reauth(self, data: dict[str, Any]) -> dict[str, Any]: + async def async_step_reauth(self, data: dict[str, Any]) -> FlowResultDict: """Handle initiation of re-authentication with Verisure.""" self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle re-authentication with Verisure.""" errors: dict[str, str] = {} @@ -173,7 +174,7 @@ async def async_step_reauth_confirm( errors=errors, ) - async def async_step_import(self, user_input: dict[str, Any]) -> dict[str, Any]: + async def async_step_import(self, user_input: dict[str, Any]) -> FlowResultDict: """Import Verisure YAML configuration.""" if user_input[CONF_GIID]: self.giid = user_input[CONF_GIID] @@ -203,7 +204,7 @@ def __init__(self, entry: ConfigEntry) -> None: async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Manage Verisure options.""" errors = {} diff --git a/homeassistant/components/vizio/config_flow.py b/homeassistant/components/vizio/config_flow.py index 2c3c365b15a30..da76f8081b4f4 100644 --- a/homeassistant/components/vizio/config_flow.py +++ b/homeassistant/components/vizio/config_flow.py @@ -30,6 +30,7 @@ CONF_TYPE, ) from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResultDict from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import DiscoveryInfoType @@ -111,7 +112,7 @@ def __init__(self, config_entry: ConfigEntry) -> None: async def async_step_init( self, user_input: dict[str, Any] = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Manage the vizio options.""" if user_input is not None: if user_input.get(CONF_APPS_TO_INCLUDE_OR_EXCLUDE): @@ -193,7 +194,7 @@ def __init__(self) -> None: self._data = None self._apps = {} - async def _create_entry(self, input_dict: dict[str, Any]) -> dict[str, Any]: + async def _create_entry(self, input_dict: dict[str, Any]) -> FlowResultDict: """Create vizio config entry.""" # Remove extra keys that will not be used by entry setup input_dict.pop(CONF_APPS_TO_INCLUDE_OR_EXCLUDE, None) @@ -206,7 +207,7 @@ async def _create_entry(self, input_dict: dict[str, Any]) -> dict[str, Any]: async def async_step_user( self, user_input: dict[str, Any] = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle a flow initialized by the user.""" errors = {} @@ -279,7 +280,7 @@ async def async_step_user( return self.async_show_form(step_id="user", data_schema=schema, errors=errors) - async def async_step_import(self, import_config: dict[str, Any]) -> dict[str, Any]: + async def async_step_import(self, import_config: dict[str, Any]) -> FlowResultDict: """Import a config entry from configuration.yaml.""" # Check if new config entry matches any existing config entries for entry in self.hass.config_entries.async_entries(DOMAIN): @@ -343,7 +344,7 @@ async def async_step_import(self, import_config: dict[str, Any]) -> dict[str, An async def async_step_zeroconf( self, discovery_info: DiscoveryInfoType | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle zeroconf discovery.""" # If host already has port, no need to add it again if ":" not in discovery_info[CONF_HOST]: @@ -380,7 +381,7 @@ async def async_step_zeroconf( async def async_step_pair_tv( self, user_input: dict[str, Any] = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """ Start pairing process for TV. @@ -445,7 +446,7 @@ async def async_step_pair_tv( errors=errors, ) - async def _pairing_complete(self, step_id: str) -> dict[str, Any]: + async def _pairing_complete(self, step_id: str) -> FlowResultDict: """Handle config flow completion.""" if not self._must_show_form: return await self._create_entry(self._data) @@ -459,7 +460,7 @@ async def _pairing_complete(self, step_id: str) -> dict[str, Any]: async def async_step_pairing_complete( self, user_input: dict[str, Any] = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """ Complete non-import sourced config flow. @@ -469,7 +470,7 @@ async def async_step_pairing_complete( async def async_step_pairing_complete_import( self, user_input: dict[str, Any] = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """ Complete import sourced config flow. diff --git a/homeassistant/components/wled/config_flow.py b/homeassistant/components/wled/config_flow.py index 9b57109d18fa9..052fc858a1af8 100644 --- a/homeassistant/components/wled/config_flow.py +++ b/homeassistant/components/wled/config_flow.py @@ -1,8 +1,6 @@ """Config flow to configure the WLED integration.""" from __future__ import annotations -from typing import Any - import voluptuous as vol from wled import WLED, WLEDConnectionError @@ -12,6 +10,7 @@ ConfigFlow, ) from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME +from homeassistant.data_entry_flow import FlowResultDict from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import ConfigType @@ -26,13 +25,13 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: ConfigType | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle a flow initiated by the user.""" return await self._handle_config_flow(user_input) async def async_step_zeroconf( self, discovery_info: ConfigType | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle zeroconf discovery.""" if discovery_info is None: return self.async_abort(reason="cannot_connect") @@ -55,13 +54,13 @@ async def async_step_zeroconf( async def async_step_zeroconf_confirm( self, user_input: ConfigType = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle a flow initiated by zeroconf.""" return await self._handle_config_flow(user_input) async def _handle_config_flow( self, user_input: ConfigType | None = None, prepare: bool = False - ) -> dict[str, Any]: + ) -> FlowResultDict: """Config flow handler for WLED.""" source = self.context.get("source") @@ -102,7 +101,7 @@ async def _handle_config_flow( data={CONF_HOST: user_input[CONF_HOST], CONF_MAC: user_input[CONF_MAC]}, ) - def _show_setup_form(self, errors: dict | None = None) -> dict[str, Any]: + def _show_setup_form(self, errors: dict | None = None) -> FlowResultDict: """Show the setup form to the user.""" return self.async_show_form( step_id="user", @@ -110,7 +109,7 @@ def _show_setup_form(self, errors: dict | None = None) -> dict[str, Any]: errors=errors or {}, ) - def _show_confirm_dialog(self, errors: dict | None = None) -> dict[str, Any]: + def _show_confirm_dialog(self, errors: dict | None = None) -> FlowResultDict: """Show the confirm dialog to the user.""" name = self.context.get(CONF_NAME) return self.async_show_form( diff --git a/script/scaffold/templates/config_flow/integration/config_flow.py b/script/scaffold/templates/config_flow/integration/config_flow.py index eea7d73b54c17..02c00be8e2a66 100644 --- a/script/scaffold/templates/config_flow/integration/config_flow.py +++ b/script/scaffold/templates/config_flow/integration/config_flow.py @@ -8,6 +8,7 @@ from homeassistant import config_entries from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultDict from homeassistant.exceptions import HomeAssistantError from .const import DOMAIN @@ -69,7 +70,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle the initial step.""" if user_input is None: return self.async_show_form( From fec6ea3f764b0431f0cedf9335f401b3c12b62cb Mon Sep 17 00:00:00 2001 From: Matt Zimmerman Date: Thu, 22 Apr 2021 21:54:55 -0700 Subject: [PATCH 0465/1317] SmartTub cleanup (#49579) --- homeassistant/components/smarttub/binary_sensor.py | 10 +++++++++- homeassistant/components/smarttub/const.py | 2 -- homeassistant/components/smarttub/controller.py | 4 +--- homeassistant/components/smarttub/entity.py | 8 ++++---- homeassistant/components/smarttub/light.py | 2 +- homeassistant/components/smarttub/switch.py | 2 +- tests/components/smarttub/test_binary_sensor.py | 11 +++-------- 7 files changed, 19 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/smarttub/binary_sensor.py b/homeassistant/components/smarttub/binary_sensor.py index bbeece366551e..31c6f6d0bc0ce 100644 --- a/homeassistant/components/smarttub/binary_sensor.py +++ b/homeassistant/components/smarttub/binary_sensor.py @@ -41,6 +41,14 @@ def __init__(self, coordinator, spa): """Initialize the entity.""" super().__init__(coordinator, spa, "Online", "online") + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry. + + This seems to be very noisy and not generally useful, so disable by default. + """ + return False + @property def is_on(self) -> bool: """Return true if the binary sensor is on.""" @@ -72,7 +80,7 @@ def unique_id(self): @property def reminder(self) -> SpaReminder: """Return the underlying SpaReminder object for this entity.""" - return self.coordinator.data[self.spa.id]["reminders"][self.reminder_id] + return self.coordinator.data[self.spa.id][ATTR_REMINDERS][self.reminder_id] @property def is_on(self) -> bool: diff --git a/homeassistant/components/smarttub/const.py b/homeassistant/components/smarttub/const.py index ad737bcd63a20..23bd8bd8ec0da 100644 --- a/homeassistant/components/smarttub/const.py +++ b/homeassistant/components/smarttub/const.py @@ -25,5 +25,3 @@ ATTR_PUMPS = "pumps" ATTR_REMINDERS = "reminders" ATTR_STATUS = "status" - -CONF_CONFIG_ENTRY = "config_entry" diff --git a/homeassistant/components/smarttub/controller.py b/homeassistant/components/smarttub/controller.py index 0b395a10fe58d..b1f9a3d494824 100644 --- a/homeassistant/components/smarttub/controller.py +++ b/homeassistant/components/smarttub/controller.py @@ -37,7 +37,6 @@ def __init__(self, hass): self._hass = hass self._account = None self.spas = set() - self._spa_devices = {} self.coordinator = None @@ -110,14 +109,13 @@ async def async_register_devices(self, entry): """Register devices with the device registry for all spas.""" device_registry = await dr.async_get_registry(self._hass) for spa in self.spas: - device = device_registry.async_get_or_create( + device_registry.async_get_or_create( config_entry_id=entry.entry_id, identifiers={(DOMAIN, spa.id)}, manufacturer=spa.brand, name=get_spa_name(spa), model=spa.model, ) - self._spa_devices[spa.id] = device async def login(self, email, password) -> Account: """Retrieve the account corresponding to the specified email and password. diff --git a/homeassistant/components/smarttub/entity.py b/homeassistant/components/smarttub/entity.py index 8be956a2b70e9..7cdd04ac17306 100644 --- a/homeassistant/components/smarttub/entity.py +++ b/homeassistant/components/smarttub/entity.py @@ -18,7 +18,7 @@ class SmartTubEntity(CoordinatorEntity): """Base class for SmartTub entities.""" def __init__( - self, coordinator: DataUpdateCoordinator, spa: smarttub.Spa, entity_type + self, coordinator: DataUpdateCoordinator, spa: smarttub.Spa, entity_name ): """Initialize the entity. @@ -28,12 +28,12 @@ def __init__( super().__init__(coordinator) self.spa = spa - self._entity_type = entity_type + self._entity_name = entity_name @property def unique_id(self) -> str: """Return a unique id for the entity.""" - return f"{self.spa.id}-{self._entity_type}" + return f"{self.spa.id}-{self._entity_name}" @property def device_info(self) -> str: @@ -48,7 +48,7 @@ def device_info(self) -> str: def name(self) -> str: """Return the name of the entity.""" spa_name = get_spa_name(self.spa) - return f"{spa_name} {self._entity_type}" + return f"{spa_name} {self._entity_name}" @property def spa_status(self) -> smarttub.SpaState: diff --git a/homeassistant/components/smarttub/light.py b/homeassistant/components/smarttub/light.py index 57acf58341514..1e4229ee4e695 100644 --- a/homeassistant/components/smarttub/light.py +++ b/homeassistant/components/smarttub/light.py @@ -50,7 +50,7 @@ def __init__(self, coordinator, light): @property def light(self) -> SpaLight: """Return the underlying SpaLight object for this entity.""" - return self.coordinator.data[self.spa.id]["lights"][self.light_zone] + return self.coordinator.data[self.spa.id][ATTR_LIGHTS][self.light_zone] @property def unique_id(self) -> str: diff --git a/homeassistant/components/smarttub/switch.py b/homeassistant/components/smarttub/switch.py index 26239df9dff79..7cab25e6cf7cc 100644 --- a/homeassistant/components/smarttub/switch.py +++ b/homeassistant/components/smarttub/switch.py @@ -39,7 +39,7 @@ def __init__(self, coordinator, pump: SpaPump): @property def pump(self) -> SpaPump: """Return the underlying SpaPump object for this entity.""" - return self.coordinator.data[self.spa.id]["pumps"][self.pump_id] + return self.coordinator.data[self.spa.id][ATTR_PUMPS][self.pump_id] @property def unique_id(self) -> str: diff --git a/tests/components/smarttub/test_binary_sensor.py b/tests/components/smarttub/test_binary_sensor.py index 5db97310c561c..b5a7c516a0e4a 100644 --- a/tests/components/smarttub/test_binary_sensor.py +++ b/tests/components/smarttub/test_binary_sensor.py @@ -1,9 +1,5 @@ """Test the SmartTub binary sensor platform.""" -from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_CONNECTIVITY, - STATE_OFF, - STATE_ON, -) +from homeassistant.components.binary_sensor import STATE_OFF async def test_binary_sensors(spa, setup_entry, hass): @@ -11,9 +7,8 @@ async def test_binary_sensors(spa, setup_entry, hass): entity_id = f"binary_sensor.{spa.brand}_{spa.model}_online" state = hass.states.get(entity_id) - assert state is not None - assert state.state == STATE_ON - assert state.attributes.get("device_class") == DEVICE_CLASS_CONNECTIVITY + # disabled by default + assert state is None async def test_reminders(spa, setup_entry, hass): From e6d94845dd17330f06df6faeab82a0ee9e568658 Mon Sep 17 00:00:00 2001 From: Matt Zimmerman Date: Thu, 22 Apr 2021 21:55:58 -0700 Subject: [PATCH 0466/1317] SmartTub: use get_full_status() (#49580) --- homeassistant/components/smarttub/controller.py | 12 +++++------- tests/components/smarttub/conftest.py | 12 ++++++++---- tests/components/smarttub/test_climate.py | 6 +++--- tests/components/smarttub/test_light.py | 5 ++--- tests/components/smarttub/test_switch.py | 3 ++- 5 files changed, 20 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/smarttub/controller.py b/homeassistant/components/smarttub/controller.py index b1f9a3d494824..06b0989233c1b 100644 --- a/homeassistant/components/smarttub/controller.py +++ b/homeassistant/components/smarttub/controller.py @@ -92,16 +92,14 @@ async def async_update_data(self): return data async def _get_spa_data(self, spa): - status, pumps, lights, reminders = await asyncio.gather( - spa.get_status(), - spa.get_pumps(), - spa.get_lights(), + full_status, reminders = await asyncio.gather( + spa.get_status_full(), spa.get_reminders(), ) return { - ATTR_STATUS: status, - ATTR_PUMPS: {pump.id: pump for pump in pumps}, - ATTR_LIGHTS: {light.zone: light for light in lights}, + ATTR_STATUS: full_status, + ATTR_PUMPS: {pump.id: pump for pump in full_status.pumps}, + ATTR_LIGHTS: {light.zone: light for light in full_status.lights}, ATTR_REMINDERS: {reminder.id: reminder for reminder in reminders}, } diff --git a/tests/components/smarttub/conftest.py b/tests/components/smarttub/conftest.py index 7f23c2355bc7b..84566fcccc577 100644 --- a/tests/components/smarttub/conftest.py +++ b/tests/components/smarttub/conftest.py @@ -42,9 +42,9 @@ def mock_spa(): mock_spa.id = "mockspa1" mock_spa.brand = "mockbrand1" mock_spa.model = "mockmodel1" - mock_spa.get_status.return_value = smarttub.SpaState( + full_status = smarttub.SpaStateFull( mock_spa, - **{ + { "setTemperature": 39, "water": {"temperature": 38}, "heater": "ON", @@ -69,8 +69,12 @@ def mock_spa(): "uv": "OFF", "blowoutCycle": "INACTIVE", "cleanupCycle": "INACTIVE", + "lights": [], + "pumps": [], }, ) + mock_spa.get_status_full.return_value = full_status + mock_circulation_pump = create_autospec(smarttub.SpaPump, instance=True) mock_circulation_pump.id = "CP" mock_circulation_pump.spa = mock_spa @@ -89,7 +93,7 @@ def mock_spa(): mock_jet_on.state = smarttub.SpaPump.PumpState.HIGH mock_jet_on.type = smarttub.SpaPump.PumpType.JET - mock_spa.get_pumps.return_value = [mock_circulation_pump, mock_jet_off, mock_jet_on] + full_status.pumps = [mock_circulation_pump, mock_jet_off, mock_jet_on] mock_light_off = create_autospec(smarttub.SpaLight, instance=True) mock_light_off.spa = mock_spa @@ -103,7 +107,7 @@ def mock_spa(): mock_light_on.intensity = 50 mock_light_on.mode = smarttub.SpaLight.LightMode.PURPLE - mock_spa.get_lights.return_value = [mock_light_off, mock_light_on] + full_status.lights = [mock_light_off, mock_light_on] mock_filter_reminder = create_autospec(smarttub.SpaReminder, instance=True) mock_filter_reminder.id = "FILTER01" diff --git a/tests/components/smarttub/test_climate.py b/tests/components/smarttub/test_climate.py index a034a4ce17ede..a9c2de4e6e21c 100644 --- a/tests/components/smarttub/test_climate.py +++ b/tests/components/smarttub/test_climate.py @@ -42,7 +42,7 @@ async def test_thermostat_update(spa, setup_entry, hass): assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_HEAT - spa.get_status.return_value.heater = "OFF" + spa.get_status_full.return_value.heater = "OFF" await trigger_update(hass) state = hass.states.get(entity_id) @@ -85,11 +85,11 @@ async def test_thermostat_update(spa, setup_entry, hass): ) spa.set_heat_mode.assert_called_with(smarttub.Spa.HeatMode.ECONOMY) - spa.get_status.return_value.heat_mode = smarttub.Spa.HeatMode.ECONOMY + spa.get_status_full.return_value.heat_mode = smarttub.Spa.HeatMode.ECONOMY await trigger_update(hass) state = hass.states.get(entity_id) assert state.attributes.get(ATTR_PRESET_MODE) == PRESET_ECO - spa.get_status.side_effect = smarttub.APIError + spa.get_status_full.side_effect = smarttub.APIError await trigger_update(hass) # should not fail diff --git a/tests/components/smarttub/test_light.py b/tests/components/smarttub/test_light.py index fe178278beef9..9f128cc279c2e 100644 --- a/tests/components/smarttub/test_light.py +++ b/tests/components/smarttub/test_light.py @@ -32,9 +32,8 @@ async def test_light( assert state is not None assert state.state == light_state - light: SpaLight = next( - light for light in await spa.get_lights() if light.zone == light_zone - ) + status = await spa.get_status_full() + light: SpaLight = next(light for light in status.lights if light.zone == light_zone) await hass.services.async_call( "light", diff --git a/tests/components/smarttub/test_switch.py b/tests/components/smarttub/test_switch.py index 81b5360406553..f97877f6cde87 100644 --- a/tests/components/smarttub/test_switch.py +++ b/tests/components/smarttub/test_switch.py @@ -16,7 +16,8 @@ async def test_pumps(spa, setup_entry, hass, pump_id, pump_state, entity_suffix): """Test pump entities.""" - pump = next(pump for pump in await spa.get_pumps() if pump.id == pump_id) + status = await spa.get_status_full() + pump = next(pump for pump in status.pumps if pump.id == pump_id) entity_id = f"switch.{spa.brand}_{spa.model}_{entity_suffix}" state = hass.states.get(entity_id) From 66dbb17a4a4a19cfd16fa9aae88dfdbbbb027fb9 Mon Sep 17 00:00:00 2001 From: Thomas Hollstegge Date: Fri, 23 Apr 2021 07:12:52 +0200 Subject: [PATCH 0467/1317] Fix opening cover via emulated_hue without specifying a position (#49570) --- .../components/emulated_hue/hue_api.py | 3 +- tests/components/emulated_hue/test_hue_api.py | 33 +++++++++++++++---- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index 0bb6b82f81393..be30de012860c 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -525,9 +525,10 @@ async def put(self, request, username, entity_number): # If the requested entity is a cover, convert to open_cover/close_cover elif entity.domain == cover.DOMAIN: domain = entity.domain - service = SERVICE_CLOSE_COVER if service == SERVICE_TURN_ON: service = SERVICE_OPEN_COVER + else: + service = SERVICE_CLOSE_COVER if ( entity_features & SUPPORT_SET_POSITION diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 38288270a1b13..c0adad38c9d7d 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -885,10 +885,10 @@ async def test_put_light_state_media_player(hass_hue, hue_client): assert walkman.attributes[media_player.ATTR_MEDIA_VOLUME_LEVEL] == level -async def test_close_cover(hass_hue, hue_client): +async def test_open_cover_without_position(hass_hue, hue_client): """Test opening cover .""" cover_id = "cover.living_room_window" - # Turn the office light off first + # Close cover first await hass_hue.services.async_call( cover.DOMAIN, const.SERVICE_CLOSE_COVER, @@ -908,25 +908,44 @@ async def test_close_cover(hass_hue, hue_client): assert cover_test.state == "closed" # Go through the API to turn it on - cover_result = await perform_put_light_state( - hass_hue, hue_client, cover_id, True, 100 - ) + cover_result = await perform_put_light_state(hass_hue, hue_client, cover_id, True) assert cover_result.status == HTTP_OK assert CONTENT_TYPE_JSON in cover_result.headers["content-type"] - for _ in range(7): + for _ in range(11): future = dt_util.utcnow() + timedelta(seconds=1) async_fire_time_changed(hass_hue, future) await hass_hue.async_block_till_done() cover_result_json = await cover_result.json() - assert len(cover_result_json) == 2 + assert len(cover_result_json) == 1 # Check to make sure the state changed cover_test_2 = hass_hue.states.get(cover_id) assert cover_test_2.state == "open" + assert cover_test_2.attributes.get("current_position") == 100 + + # Go through the API to turn it off + cover_result = await perform_put_light_state(hass_hue, hue_client, cover_id, False) + + assert cover_result.status == HTTP_OK + assert CONTENT_TYPE_JSON in cover_result.headers["content-type"] + + for _ in range(11): + future = dt_util.utcnow() + timedelta(seconds=1) + async_fire_time_changed(hass_hue, future) + await hass_hue.async_block_till_done() + + cover_result_json = await cover_result.json() + + assert len(cover_result_json) == 1 + + # Check to make sure the state changed + cover_test_2 = hass_hue.states.get(cover_id) + assert cover_test_2.state == "closed" + assert cover_test_2.attributes.get("current_position") == 0 async def test_set_position_cover(hass_hue, hue_client): From c753606a744ea56eae9b6219a4b13a153d59c409 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 22 Apr 2021 19:39:49 -1000 Subject: [PATCH 0468/1317] Bump async-upnp-client to 0.16.1 (#49577) --- homeassistant/components/dlna_dmr/manifest.json | 2 +- homeassistant/components/ssdp/manifest.json | 2 +- homeassistant/components/upnp/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index 928df4b1ecc23..ee4b5b26ab6b4 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -2,7 +2,7 @@ "domain": "dlna_dmr", "name": "DLNA Digital Media Renderer", "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", - "requirements": ["async-upnp-client==0.16.0"], + "requirements": ["async-upnp-client==0.16.1"], "codeowners": [], "iot_class": "local_push" } diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index c2ad7921ac24f..6188f4aa24701 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -5,7 +5,7 @@ "requirements": [ "defusedxml==0.6.0", "netdisco==2.8.2", - "async-upnp-client==0.16.0" + "async-upnp-client==0.16.1" ], "after_dependencies": ["zeroconf"], "codeowners": [], diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index 50046802e4712..5c4e7a0c357a9 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -3,7 +3,7 @@ "name": "UPnP", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/upnp", - "requirements": ["async-upnp-client==0.16.0"], + "requirements": ["async-upnp-client==0.16.1"], "codeowners": ["@StevenLooman"], "ssdp": [ { diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index dfcf3e81c9a98..bfe401f6d62f7 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodiscover==1.4.0 aiohttp==3.7.4.post0 aiohttp_cors==0.7.0 astral==2.2 -async-upnp-client==0.16.0 +async-upnp-client==0.16.1 async_timeout==3.0.1 attrs==20.3.0 awesomeversion==21.2.3 diff --git a/requirements_all.txt b/requirements_all.txt index c3f0f0e42dea1..e9b4e2cba88bc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -292,7 +292,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.dlna_dmr # homeassistant.components.ssdp # homeassistant.components.upnp -async-upnp-client==0.16.0 +async-upnp-client==0.16.1 # homeassistant.components.supla asyncpysupla==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a7260aadb2537..a5f2463d33af2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -184,7 +184,7 @@ arcam-fmj==0.5.3 # homeassistant.components.dlna_dmr # homeassistant.components.ssdp # homeassistant.components.upnp -async-upnp-client==0.16.0 +async-upnp-client==0.16.1 # homeassistant.components.aurora auroranoaa==0.0.2 From 265fdea83b4cf459a1341a6c6e54d32c65661f91 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 23 Apr 2021 00:23:43 -0700 Subject: [PATCH 0469/1317] Allow config entries to store a reason (#49581) --- .../components/config/config_entries.py | 1 + homeassistant/config_entries.py | 19 +++++++++++++++++++ tests/common.py | 3 +++ .../components/config/test_config_entries.py | 10 ++++++++-- tests/test_config_entries.py | 4 ++++ 5 files changed, 35 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index edf9426874135..6f9b96b9dfab9 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -390,4 +390,5 @@ def entry_json(entry: config_entries.ConfigEntry) -> dict: "supports_options": supports_options, "supports_unload": entry.supports_unload, "disabled_by": entry.disabled_by, + "reason": entry.reason, } diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index bf9a45d06f0cb..5b35b7ef65c07 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -138,6 +138,7 @@ class ConfigEntry: "disabled_by", "_setup_lock", "update_listeners", + "reason", "_async_cancel_retry_setup", "_on_unload", ) @@ -202,6 +203,9 @@ def __init__( weakref.ReferenceType[UpdateListenerType] | weakref.WeakMethod ] = [] + # Reason why config entry is in a failed state + self.reason: str | None = None + # Function to cancel a scheduled retry self._async_cancel_retry_setup: Callable[[], Any] | None = None @@ -236,6 +240,7 @@ async def async_setup( ) if self.domain == integration.domain: self.state = ENTRY_STATE_SETUP_ERROR + self.reason = "Import error" return if self.domain == integration.domain: @@ -249,13 +254,17 @@ async def async_setup( err, ) self.state = ENTRY_STATE_SETUP_ERROR + self.reason = "Import error" return # Perform migration if not await self.async_migrate(hass): self.state = ENTRY_STATE_MIGRATION_ERROR + self.reason = None return + error_reason = None + try: result = await component.async_setup_entry(hass, self) # type: ignore @@ -267,6 +276,7 @@ async def async_setup( except ConfigEntryAuthFailed as ex: message = str(ex) auth_base_message = "could not authenticate" + error_reason = message or auth_base_message auth_message = ( f"{auth_base_message}: {message}" if message else auth_base_message ) @@ -281,6 +291,7 @@ async def async_setup( result = False except ConfigEntryNotReady as ex: self.state = ENTRY_STATE_SETUP_RETRY + self.reason = str(ex) or None wait_time = 2 ** min(tries, 4) * 5 tries += 1 message = str(ex) @@ -329,8 +340,10 @@ async def setup_again(*_: Any) -> None: if result: self.state = ENTRY_STATE_LOADED + self.reason = None else: self.state = ENTRY_STATE_SETUP_ERROR + self.reason = error_reason async def async_shutdown(self) -> None: """Call when Home Assistant is stopping.""" @@ -352,6 +365,7 @@ async def async_unload( """ if self.source == SOURCE_IGNORE: self.state = ENTRY_STATE_NOT_LOADED + self.reason = None return True if integration is None: @@ -363,6 +377,7 @@ async def async_unload( # that has been renamed without removing the config # entry. self.state = ENTRY_STATE_NOT_LOADED + self.reason = None return True component = integration.get_component() @@ -375,6 +390,7 @@ async def async_unload( self.async_cancel_retry_setup() self.state = ENTRY_STATE_NOT_LOADED + self.reason = None return True supports_unload = hasattr(component, "async_unload_entry") @@ -382,6 +398,7 @@ async def async_unload( if not supports_unload: if integration.domain == self.domain: self.state = ENTRY_STATE_FAILED_UNLOAD + self.reason = "Unload not supported" return False try: @@ -392,6 +409,7 @@ async def async_unload( # Only adjust state if we unloaded the component if result and integration.domain == self.domain: self.state = ENTRY_STATE_NOT_LOADED + self.reason = None self._async_process_on_unload() @@ -402,6 +420,7 @@ async def async_unload( ) if integration.domain == self.domain: self.state = ENTRY_STATE_FAILED_UNLOAD + self.reason = "Unknown error" return False async def async_remove(self, hass: HomeAssistant) -> None: diff --git a/tests/common.py b/tests/common.py index cc971ca4f13ff..d63e385910836 100644 --- a/tests/common.py +++ b/tests/common.py @@ -734,6 +734,7 @@ def __init__( connection_class=config_entries.CONN_CLASS_UNKNOWN, unique_id=None, disabled_by=None, + reason=None, ): """Initialize a mock config entry.""" kwargs = { @@ -753,6 +754,8 @@ def __init__( if state is not None: kwargs["state"] = state super().__init__(**kwargs) + if reason is not None: + self.reason = reason def add_to_hass(self, hass): """Test helper to add entry to hass.""" diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 128d0798b660f..271333b092a9e 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -65,7 +65,8 @@ def async_get_options_flow(config, options): domain="comp2", title="Test 2", source="bla2", - state=core_ce.ENTRY_STATE_LOADED, + state=core_ce.ENTRY_STATE_SETUP_ERROR, + reason="Unsupported API", connection_class=core_ce.CONN_CLASS_ASSUMED, ).add_to_hass(hass) MockConfigEntry( @@ -90,16 +91,18 @@ def async_get_options_flow(config, options): "supports_options": True, "supports_unload": True, "disabled_by": None, + "reason": None, }, { "domain": "comp2", "title": "Test 2", "source": "bla2", - "state": "loaded", + "state": "setup_error", "connection_class": "assumed", "supports_options": False, "supports_unload": False, "disabled_by": None, + "reason": "Unsupported API", }, { "domain": "comp3", @@ -110,6 +113,7 @@ def async_get_options_flow(config, options): "supports_options": False, "supports_unload": False, "disabled_by": "user", + "reason": None, }, ] @@ -330,6 +334,7 @@ async def async_step_user(self, user_input=None): "supports_options": False, "supports_unload": False, "title": "Test Entry", + "reason": None, }, "description": None, "description_placeholders": None, @@ -399,6 +404,7 @@ async def async_step_account(self, user_input=None): "supports_options": False, "supports_unload": False, "title": "user-title", + "reason": None, }, "description": None, "description_placeholders": None, diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 20ab5e67fef6c..326c7ba19cadd 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -865,12 +865,14 @@ async def test_setup_raise_not_ready(hass, caplog): assert p_hass is hass assert p_wait_time == 5 assert entry.state == config_entries.ENTRY_STATE_SETUP_RETRY + assert entry.reason == "The internet connection is offline" mock_setup_entry.side_effect = None mock_setup_entry.return_value = True await p_setup(None) assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.reason is None async def test_setup_raise_not_ready_from_exception(hass, caplog): @@ -2555,6 +2557,7 @@ async def test_setup_raise_auth_failed(hass, caplog): assert "could not authenticate: The password is no longer valid" in caplog.text assert entry.state == config_entries.ENTRY_STATE_SETUP_ERROR + assert entry.reason == "The password is no longer valid" flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 assert flows[0]["context"]["entry_id"] == entry.entry_id @@ -2562,6 +2565,7 @@ async def test_setup_raise_auth_failed(hass, caplog): caplog.clear() entry.state = config_entries.ENTRY_STATE_NOT_LOADED + entry.reason = None await entry.async_setup(hass) await hass.async_block_till_done() From a5a3c98aff98deefaf9c2c766479387e12cd382b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 23 Apr 2021 09:25:37 +0200 Subject: [PATCH 0470/1317] Make lights supporting rgbw and rgbww accept colors (#49565) * Allow lights supporting rgbw and rgbww accepting colors * Tweak, update tests --- homeassistant/components/light/__init__.py | 22 ++++++++++++++- tests/components/light/test_init.py | 32 ++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 835c4fd2fa93b..0b78ed3672ebf 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -317,11 +317,23 @@ async def async_handle_light_on_service(light, call): hs_color = params.pop(ATTR_HS_COLOR) if COLOR_MODE_RGB in supported_color_modes: params[ATTR_RGB_COLOR] = color_util.color_hs_to_RGB(*hs_color) + elif COLOR_MODE_RGBW in supported_color_modes: + params[ATTR_RGBW_COLOR] = (*color_util.color_hs_to_RGB(*hs_color), 0) + elif COLOR_MODE_RGBWW in supported_color_modes: + params[ATTR_RGBWW_COLOR] = ( + *color_util.color_hs_to_RGB(*hs_color), + 0, + 0, + ) elif COLOR_MODE_XY in supported_color_modes: params[ATTR_XY_COLOR] = color_util.color_hs_to_xy(*hs_color) elif ATTR_RGB_COLOR in params and COLOR_MODE_RGB not in supported_color_modes: rgb_color = params.pop(ATTR_RGB_COLOR) - if COLOR_MODE_HS in supported_color_modes: + if COLOR_MODE_RGBW in supported_color_modes: + params[ATTR_RGBW_COLOR] = (*rgb_color, 0) + if COLOR_MODE_RGBWW in supported_color_modes: + params[ATTR_RGBWW_COLOR] = (*rgb_color, 0, 0) + elif COLOR_MODE_HS in supported_color_modes: params[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color) elif COLOR_MODE_XY in supported_color_modes: params[ATTR_XY_COLOR] = color_util.color_RGB_to_xy(*rgb_color) @@ -331,6 +343,14 @@ async def async_handle_light_on_service(light, call): params[ATTR_HS_COLOR] = color_util.color_xy_to_hs(*xy_color) elif COLOR_MODE_RGB in supported_color_modes: params[ATTR_RGB_COLOR] = color_util.color_xy_to_RGB(*xy_color) + elif COLOR_MODE_RGBW in supported_color_modes: + params[ATTR_RGBW_COLOR] = (*color_util.color_xy_to_RGB(*xy_color), 0) + elif COLOR_MODE_RGBWW in supported_color_modes: + params[ATTR_RGBWW_COLOR] = ( + *color_util.color_xy_to_RGB(*xy_color), + 0, + 0, + ) # Remove deprecated white value if the light supports color mode if supported_color_modes: diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index 3adb146a22515..f0cca89892c58 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -1209,6 +1209,8 @@ async def test_light_service_call_color_conversion(hass): platform.ENTITIES.append(platform.MockLight("Test_xy", STATE_ON)) platform.ENTITIES.append(platform.MockLight("Test_all", STATE_ON)) platform.ENTITIES.append(platform.MockLight("Test_legacy", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("Test_rgbw", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("Test_rgbww", STATE_ON)) entity0 = platform.ENTITIES[0] entity0.supported_color_modes = {light.COLOR_MODE_HS} @@ -1229,6 +1231,12 @@ async def test_light_service_call_color_conversion(hass): entity4 = platform.ENTITIES[4] entity4.supported_features = light.SUPPORT_COLOR + entity5 = platform.ENTITIES[5] + entity5.supported_color_modes = {light.COLOR_MODE_RGBW} + + entity6 = platform.ENTITIES[6] + entity6.supported_color_modes = {light.COLOR_MODE_RGBWW} + assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) await hass.async_block_till_done() @@ -1251,6 +1259,12 @@ async def test_light_service_call_color_conversion(hass): state = hass.states.get(entity4.entity_id) assert state.attributes["supported_color_modes"] == [light.COLOR_MODE_HS] + state = hass.states.get(entity5.entity_id) + assert state.attributes["supported_color_modes"] == [light.COLOR_MODE_RGBW] + + state = hass.states.get(entity6.entity_id) + assert state.attributes["supported_color_modes"] == [light.COLOR_MODE_RGBWW] + await hass.services.async_call( "light", "turn_on", @@ -1261,6 +1275,8 @@ async def test_light_service_call_color_conversion(hass): entity2.entity_id, entity3.entity_id, entity4.entity_id, + entity5.entity_id, + entity6.entity_id, ], "brightness_pct": 100, "hs_color": (240, 100), @@ -1277,6 +1293,10 @@ async def test_light_service_call_color_conversion(hass): assert data == {"brightness": 255, "hs_color": (240.0, 100.0)} _, data = entity4.last_call("turn_on") assert data == {"brightness": 255, "hs_color": (240.0, 100.0)} + _, data = entity5.last_call("turn_on") + assert data == {"brightness": 255, "rgbw_color": (0, 0, 255, 0)} + _, data = entity6.last_call("turn_on") + assert data == {"brightness": 255, "rgbww_color": (0, 0, 255, 0, 0)} await hass.services.async_call( "light", @@ -1288,6 +1308,8 @@ async def test_light_service_call_color_conversion(hass): entity2.entity_id, entity3.entity_id, entity4.entity_id, + entity5.entity_id, + entity6.entity_id, ], "brightness_pct": 50, "rgb_color": (128, 0, 0), @@ -1304,6 +1326,10 @@ async def test_light_service_call_color_conversion(hass): assert data == {"brightness": 128, "rgb_color": (128, 0, 0)} _, data = entity4.last_call("turn_on") assert data == {"brightness": 128, "hs_color": (0.0, 100.0)} + _, data = entity5.last_call("turn_on") + assert data == {"brightness": 128, "rgbw_color": (128, 0, 0, 0)} + _, data = entity6.last_call("turn_on") + assert data == {"brightness": 128, "rgbww_color": (128, 0, 0, 0, 0)} await hass.services.async_call( "light", @@ -1315,6 +1341,8 @@ async def test_light_service_call_color_conversion(hass): entity2.entity_id, entity3.entity_id, entity4.entity_id, + entity5.entity_id, + entity6.entity_id, ], "brightness_pct": 50, "xy_color": (0.1, 0.8), @@ -1331,6 +1359,10 @@ async def test_light_service_call_color_conversion(hass): assert data == {"brightness": 128, "xy_color": (0.1, 0.8)} _, data = entity4.last_call("turn_on") assert data == {"brightness": 128, "hs_color": (125.176, 100.0)} + _, data = entity5.last_call("turn_on") + assert data == {"brightness": 128, "rgbw_color": (0, 255, 22, 0)} + _, data = entity6.last_call("turn_on") + assert data == {"brightness": 128, "rgbww_color": (0, 255, 22, 0, 0)} async def test_light_state_color_conversion(hass): From 017e32a0cbbd868f9ca4c7b3809af2c0ff03c6fc Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 23 Apr 2021 09:49:02 +0200 Subject: [PATCH 0471/1317] Integrations h*: Rename HomeAssistantType to HomeAssistant. (#49590) --- homeassistant/components/heos/__init__.py | 9 +++--- homeassistant/components/heos/media_player.py | 4 +-- homeassistant/components/heos/services.py | 6 ++-- .../components/homematicip_cloud/__init__.py | 11 +++---- .../homematicip_cloud/alarm_control_panel.py | 5 ++-- .../homematicip_cloud/binary_sensor.py | 4 +-- .../components/homematicip_cloud/climate.py | 4 +-- .../components/homematicip_cloud/cover.py | 4 +-- .../components/homematicip_cloud/hap.py | 9 +++--- .../components/homematicip_cloud/light.py | 4 +-- .../components/homematicip_cloud/sensor.py | 4 +-- .../components/homematicip_cloud/services.py | 29 +++++++++---------- .../components/homematicip_cloud/switch.py | 4 +-- .../components/homematicip_cloud/weather.py | 4 +-- .../components/huawei_lte/__init__.py | 16 ++++------ .../components/huawei_lte/binary_sensor.py | 4 +-- .../components/huawei_lte/device_tracker.py | 7 ++--- homeassistant/components/huawei_lte/notify.py | 4 +-- homeassistant/components/huawei_lte/sensor.py | 5 ++-- homeassistant/components/huawei_lte/switch.py | 4 +-- .../components/homematicip_cloud/conftest.py | 7 +++-- tests/components/homematicip_cloud/helper.py | 4 +-- 22 files changed, 73 insertions(+), 79 deletions(-) diff --git a/homeassistant/components/heos/__init__.py b/homeassistant/components/heos/__init__.py index a4db978a39de7..fb51c1d158cab 100644 --- a/homeassistant/components/heos/__init__.py +++ b/homeassistant/components/heos/__init__.py @@ -11,9 +11,10 @@ from homeassistant.components.media_player.const import DOMAIN as MEDIA_PLAYER_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STOP +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from homeassistant.util import Throttle from . import services @@ -36,7 +37,7 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistantType, config: ConfigType): +async def async_setup(hass: HomeAssistant, config: ConfigType): """Set up the HEOS component.""" if DOMAIN not in config: return True @@ -60,7 +61,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): return True -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Initialize config entry which represents the HEOS controller.""" # For backwards compat if entry.unique_id is None: @@ -124,7 +125,7 @@ async def disconnect_controller(event): return True -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" controller_manager = hass.data[DOMAIN][DATA_CONTROLLER_MANAGER] await controller_manager.disconnect() diff --git a/homeassistant/components/heos/media_player.py b/homeassistant/components/heos/media_player.py index b919db5834514..565c1ac7aa480 100644 --- a/homeassistant/components/heos/media_player.py +++ b/homeassistant/components/heos/media_player.py @@ -30,7 +30,7 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_IDLE, STATE_PAUSED, STATE_PLAYING -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from homeassistant.util.dt import utcnow from .const import DATA_SOURCE_MANAGER, DOMAIN as HEOS_DOMAIN, SIGNAL_HEOS_UPDATED @@ -63,7 +63,7 @@ async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ): """Add media players for a config entry.""" players = hass.data[HEOS_DOMAIN][DOMAIN] diff --git a/homeassistant/components/heos/services.py b/homeassistant/components/heos/services.py index ee5df1b483b55..68328f3e1a268 100644 --- a/homeassistant/components/heos/services.py +++ b/homeassistant/components/heos/services.py @@ -5,8 +5,8 @@ from pyheos import CommandFailedError, Heos, HeosError, const import voluptuous as vol +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.typing import HomeAssistantType from .const import ( ATTR_PASSWORD, @@ -25,7 +25,7 @@ HEOS_SIGN_OUT_SCHEMA = vol.Schema({}) -def register(hass: HomeAssistantType, controller: Heos): +def register(hass: HomeAssistant, controller: Heos): """Register HEOS services.""" hass.services.async_register( DOMAIN, @@ -41,7 +41,7 @@ def register(hass: HomeAssistantType, controller: Heos): ) -def remove(hass: HomeAssistantType): +def remove(hass: HomeAssistant): """Unregister HEOS services.""" hass.services.async_remove(DOMAIN, SERVICE_SIGN_IN) hass.services.async_remove(DOMAIN, SERVICE_SIGN_OUT) diff --git a/homeassistant/components/homematicip_cloud/__init__.py b/homeassistant/components/homematicip_cloud/__init__.py index ca1af8266c629..00604bbc8a6c1 100644 --- a/homeassistant/components/homematicip_cloud/__init__.py +++ b/homeassistant/components/homematicip_cloud/__init__.py @@ -4,10 +4,11 @@ from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, EVENT_HOMEASSISTANT_STOP +from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_registry import async_entries_for_config_entry -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from .const import ( CONF_ACCESSPOINT, @@ -40,7 +41,7 @@ ) -async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the HomematicIP Cloud component.""" hass.data[DOMAIN] = {} @@ -66,7 +67,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: return True -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up an access point from a config entry.""" # 0.104 introduced config entry unique id, this makes upgrading possible @@ -107,7 +108,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool return True -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" hap = hass.data[DOMAIN].pop(entry.unique_id) hap.reset_connection_listener() @@ -118,7 +119,7 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> boo async def async_remove_obsolete_entities( - hass: HomeAssistantType, entry: ConfigEntry, hap: HomematicipHAP + hass: HomeAssistant, entry: ConfigEntry, hap: HomematicipHAP ): """Remove obsolete entities from entity registry.""" diff --git a/homeassistant/components/homematicip_cloud/alarm_control_panel.py b/homeassistant/components/homematicip_cloud/alarm_control_panel.py index 7fa5e197aa824..f4776d52743cb 100644 --- a/homeassistant/components/homematicip_cloud/alarm_control_panel.py +++ b/homeassistant/components/homematicip_cloud/alarm_control_panel.py @@ -18,8 +18,7 @@ STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, ) -from homeassistant.core import callback -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant, callback from . import DOMAIN as HMIPC_DOMAIN from .hap import HomematicipHAP @@ -30,7 +29,7 @@ async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP alrm control panel from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.unique_id] diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index 4fcf1f67dd4f1..4f15a8c72006a 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -44,7 +44,7 @@ BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericEntity from .hap import HomematicipHAP @@ -80,7 +80,7 @@ async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP Cloud binary sensor from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.unique_id] diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py index 5cdadf4d5f1e0..05234cd43a612 100644 --- a/homeassistant/components/homematicip_cloud/climate.py +++ b/homeassistant/components/homematicip_cloud/climate.py @@ -26,7 +26,7 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericEntity from .hap import HomematicipHAP @@ -43,7 +43,7 @@ async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP climate from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.unique_id] diff --git a/homeassistant/components/homematicip_cloud/cover.py b/homeassistant/components/homematicip_cloud/cover.py index aa1be11758eeb..2d3e1ea518ccc 100644 --- a/homeassistant/components/homematicip_cloud/cover.py +++ b/homeassistant/components/homematicip_cloud/cover.py @@ -18,7 +18,7 @@ CoverEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericEntity from .hap import HomematicipHAP @@ -30,7 +30,7 @@ async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP cover from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.unique_id] diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py index 5ad4efed1f674..e731da2262e29 100644 --- a/homeassistant/components/homematicip_cloud/hap.py +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -8,10 +8,9 @@ from homematicip.base.enums import EventType from homeassistant.config_entries import ConfigEntry -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.typing import HomeAssistantType from .const import HMIPC_AUTHTOKEN, HMIPC_HAPID, HMIPC_NAME, HMIPC_PIN, PLATFORMS from .errors import HmipcConnectionError @@ -54,7 +53,7 @@ async def async_register(self): except HmipConnectionError: return False - async def get_auth(self, hass: HomeAssistantType, hapid, pin): + async def get_auth(self, hass: HomeAssistant, hapid, pin): """Create a HomematicIP access point object.""" auth = AsyncAuth(hass.loop, async_get_clientsession(hass)) try: @@ -70,7 +69,7 @@ async def get_auth(self, hass: HomeAssistantType, hapid, pin): class HomematicipHAP: """Manages HomematicIP HTTP and WebSocket connection.""" - def __init__(self, hass: HomeAssistantType, config_entry: ConfigEntry) -> None: + def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Initialize HomematicIP Cloud connection.""" self.hass = hass self.config_entry = config_entry @@ -234,7 +233,7 @@ def shutdown(self, event) -> None: ) async def get_hap( - self, hass: HomeAssistantType, hapid: str, authtoken: str, name: str + self, hass: HomeAssistant, hapid: str, authtoken: str, name: str ) -> AsyncHome: """Create a HomematicIP access point object.""" home = AsyncHome(hass.loop, async_get_clientsession(hass)) diff --git a/homeassistant/components/homematicip_cloud/light.py b/homeassistant/components/homematicip_cloud/light.py index 5732ea1bf9670..a2f2a6aea53ac 100644 --- a/homeassistant/components/homematicip_cloud/light.py +++ b/homeassistant/components/homematicip_cloud/light.py @@ -26,7 +26,7 @@ LightEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericEntity from .hap import HomematicipHAP @@ -36,7 +36,7 @@ async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP Cloud lights from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.unique_id] diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index 9e6e96232b46c..475df8ec2afa3 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -40,7 +40,7 @@ SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS, ) -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericEntity from .hap import HomematicipHAP @@ -62,7 +62,7 @@ async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP Cloud sensors from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.unique_id] diff --git a/homeassistant/components/homematicip_cloud/services.py b/homeassistant/components/homematicip_cloud/services.py index aa82e72e284c9..34e564cff6924 100644 --- a/homeassistant/components/homematicip_cloud/services.py +++ b/homeassistant/components/homematicip_cloud/services.py @@ -11,13 +11,14 @@ import voluptuous as vol from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import comp_entity_ids from homeassistant.helpers.service import ( async_register_admin_service, verify_domain_control, ) -from homeassistant.helpers.typing import HomeAssistantType, ServiceCallType +from homeassistant.helpers.typing import ServiceCallType from .const import DOMAIN as HMIPC_DOMAIN @@ -107,7 +108,7 @@ ) -async def async_setup_services(hass: HomeAssistantType) -> None: +async def async_setup_services(hass: HomeAssistant) -> None: """Set up the HomematicIP Cloud services.""" if hass.services.async_services().get(HMIPC_DOMAIN): @@ -194,7 +195,7 @@ async def async_call_hmipc_service(service: ServiceCallType): ) -async def async_unload_services(hass: HomeAssistantType): +async def async_unload_services(hass: HomeAssistant): """Unload HomematicIP Cloud services.""" if hass.data[HMIPC_DOMAIN]: return @@ -204,7 +205,7 @@ async def async_unload_services(hass: HomeAssistantType): async def _async_activate_eco_mode_with_duration( - hass: HomeAssistantType, service: ServiceCallType + hass: HomeAssistant, service: ServiceCallType ) -> None: """Service to activate eco mode with duration.""" duration = service.data[ATTR_DURATION] @@ -220,7 +221,7 @@ async def _async_activate_eco_mode_with_duration( async def _async_activate_eco_mode_with_period( - hass: HomeAssistantType, service: ServiceCallType + hass: HomeAssistant, service: ServiceCallType ) -> None: """Service to activate eco mode with period.""" endtime = service.data[ATTR_ENDTIME] @@ -236,7 +237,7 @@ async def _async_activate_eco_mode_with_period( async def _async_activate_vacation( - hass: HomeAssistantType, service: ServiceCallType + hass: HomeAssistant, service: ServiceCallType ) -> None: """Service to activate vacation.""" endtime = service.data[ATTR_ENDTIME] @@ -253,7 +254,7 @@ async def _async_activate_vacation( async def _async_deactivate_eco_mode( - hass: HomeAssistantType, service: ServiceCallType + hass: HomeAssistant, service: ServiceCallType ) -> None: """Service to deactivate eco mode.""" hapid = service.data.get(ATTR_ACCESSPOINT_ID) @@ -268,7 +269,7 @@ async def _async_deactivate_eco_mode( async def _async_deactivate_vacation( - hass: HomeAssistantType, service: ServiceCallType + hass: HomeAssistant, service: ServiceCallType ) -> None: """Service to deactivate vacation.""" hapid = service.data.get(ATTR_ACCESSPOINT_ID) @@ -283,7 +284,7 @@ async def _async_deactivate_vacation( async def _set_active_climate_profile( - hass: HomeAssistantType, service: ServiceCallType + hass: HomeAssistant, service: ServiceCallType ) -> None: """Service to set the active climate profile.""" entity_id_list = service.data[ATTR_ENTITY_ID] @@ -301,9 +302,7 @@ async def _set_active_climate_profile( await group.set_active_profile(climate_profile_index) -async def _async_dump_hap_config( - hass: HomeAssistantType, service: ServiceCallType -) -> None: +async def _async_dump_hap_config(hass: HomeAssistant, service: ServiceCallType) -> None: """Service to dump the configuration of a Homematic IP Access Point.""" config_path = service.data.get(ATTR_CONFIG_OUTPUT_PATH) or hass.config.config_dir config_file_prefix = service.data[ATTR_CONFIG_OUTPUT_FILE_PREFIX] @@ -325,9 +324,7 @@ async def _async_dump_hap_config( config_file.write_text(json_state, encoding="utf8") -async def _async_reset_energy_counter( - hass: HomeAssistantType, service: ServiceCallType -): +async def _async_reset_energy_counter(hass: HomeAssistant, service: ServiceCallType): """Service to reset the energy counter.""" entity_id_list = service.data[ATTR_ENTITY_ID] @@ -343,7 +340,7 @@ async def _async_reset_energy_counter( await device.reset_energy_counter() -def _get_home(hass: HomeAssistantType, hapid: str) -> AsyncHome | None: +def _get_home(hass: HomeAssistant, hapid: str) -> AsyncHome | None: """Return a HmIP home.""" hap = hass.data[HMIPC_DOMAIN].get(hapid) if hap: diff --git a/homeassistant/components/homematicip_cloud/switch.py b/homeassistant/components/homematicip_cloud/switch.py index 8172d64d35734..3ea52c9fb8990 100644 --- a/homeassistant/components/homematicip_cloud/switch.py +++ b/homeassistant/components/homematicip_cloud/switch.py @@ -22,7 +22,7 @@ from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericEntity from .generic_entity import ATTR_GROUP_MEMBER_UNREACHABLE @@ -30,7 +30,7 @@ async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP switch from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.unique_id] diff --git a/homeassistant/components/homematicip_cloud/weather.py b/homeassistant/components/homematicip_cloud/weather.py index bdfd505a3174e..dcd8ff4dff741 100644 --- a/homeassistant/components/homematicip_cloud/weather.py +++ b/homeassistant/components/homematicip_cloud/weather.py @@ -21,7 +21,7 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import TEMP_CELSIUS -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericEntity from .hap import HomematicipHAP @@ -46,7 +46,7 @@ async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP weather sensor from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.unique_id] diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index ea3a909206d0b..ece967aa72b66 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -41,7 +41,7 @@ CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.core import CALLBACK_TYPE, ServiceCall +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, ServiceCall from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import ( config_validation as cv, @@ -51,7 +51,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from .const import ( ADMIN_SERVICES, @@ -309,7 +309,7 @@ class HuaweiLteData: routers: dict[str, Router] = attr.ib(init=False, factory=dict) -async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up Huawei LTE component from config entry.""" url = config_entry.data[CONF_URL] @@ -458,9 +458,7 @@ def _update_router(*_: Any) -> None: return True -async def async_unload_entry( - hass: HomeAssistantType, config_entry: ConfigEntry -) -> bool: +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload config entry.""" # Forward config entry unload to platforms @@ -474,7 +472,7 @@ async def async_unload_entry( return True -async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up Huawei LTE component.""" # dicttoxml (used by huawei-lte-api) has uselessly verbose INFO level. @@ -556,9 +554,7 @@ def service_handler(service: ServiceCall) -> None: return True -async def async_migrate_entry( - hass: HomeAssistantType, config_entry: ConfigEntry -) -> bool: +async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Migrate config entry to new version.""" if config_entry.version == 1: options = dict(config_entry.options) diff --git a/homeassistant/components/huawei_lte/binary_sensor.py b/homeassistant/components/huawei_lte/binary_sensor.py index 833a632b0dba2..6cb7c8d2ed7d2 100644 --- a/homeassistant/components/huawei_lte/binary_sensor.py +++ b/homeassistant/components/huawei_lte/binary_sensor.py @@ -13,8 +13,8 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_URL +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import HomeAssistantType from . import HuaweiLteBaseEntity from .const import ( @@ -28,7 +28,7 @@ async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable[[list[Entity], bool], None], ) -> None: diff --git a/homeassistant/components/huawei_lte/device_tracker.py b/homeassistant/components/huawei_lte/device_tracker.py index 7e83369688abe..25b1094c638b2 100644 --- a/homeassistant/components/huawei_lte/device_tracker.py +++ b/homeassistant/components/huawei_lte/device_tracker.py @@ -15,11 +15,10 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_URL -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import HomeAssistantType from . import HuaweiLteBaseEntity, Router from .const import ( @@ -52,7 +51,7 @@ def _get_hosts( async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable[[list[Entity], bool], None], ) -> None: @@ -129,7 +128,7 @@ def _is_us(host: _HostType) -> bool: @callback def async_add_new_entities( - hass: HomeAssistantType, + hass: HomeAssistant, router_url: str, async_add_entities: Callable[[list[Entity], bool], None], tracked: set[str], diff --git a/homeassistant/components/huawei_lte/notify.py b/homeassistant/components/huawei_lte/notify.py index ea7b5d9f6ab8b..1b3b85b67112d 100644 --- a/homeassistant/components/huawei_lte/notify.py +++ b/homeassistant/components/huawei_lte/notify.py @@ -10,7 +10,7 @@ from homeassistant.components.notify import ATTR_TARGET, BaseNotificationService from homeassistant.const import CONF_RECIPIENT, CONF_URL -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from . import Router from .const import DOMAIN @@ -19,7 +19,7 @@ async def async_get_service( - hass: HomeAssistantType, + hass: HomeAssistant, config: dict[str, Any], discovery_info: dict[str, Any] | None = None, ) -> HuaweiLteSmsNotificationService | None: diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index 0384c872d4c24..54573c01dfa5b 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -23,8 +23,9 @@ STATE_UNKNOWN, TIME_SECONDS, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import HomeAssistantType, StateType +from homeassistant.helpers.typing import StateType from . import HuaweiLteBaseEntity from .const import ( @@ -329,7 +330,7 @@ class SensorMeta(NamedTuple): async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable[[list[Entity], bool], None], ) -> None: diff --git a/homeassistant/components/huawei_lte/switch.py b/homeassistant/components/huawei_lte/switch.py index 9279226e8eccb..d5da6accdb39c 100644 --- a/homeassistant/components/huawei_lte/switch.py +++ b/homeassistant/components/huawei_lte/switch.py @@ -13,8 +13,8 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_URL +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import HomeAssistantType from . import HuaweiLteBaseEntity from .const import DOMAIN, KEY_DIALUP_MOBILE_DATASWITCH @@ -23,7 +23,7 @@ async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable[[list[Entity], bool], None], ) -> None: diff --git a/tests/components/homematicip_cloud/conftest.py b/tests/components/homematicip_cloud/conftest.py index aac7f60558ca0..2d21f9cf86139 100644 --- a/tests/components/homematicip_cloud/conftest.py +++ b/tests/components/homematicip_cloud/conftest.py @@ -20,7 +20,8 @@ ) from homeassistant.components.homematicip_cloud.hap import HomematicipHAP from homeassistant.config_entries import SOURCE_IMPORT -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType from .helper import AUTH_TOKEN, HAPID, HAPPIN, HomeFactory @@ -70,7 +71,7 @@ def hmip_config_entry_fixture() -> config_entries.ConfigEntry: @pytest.fixture(name="default_mock_hap_factory") async def default_mock_hap_factory_fixture( - hass: HomeAssistantType, mock_connection, hmip_config_entry + hass: HomeAssistant, mock_connection, hmip_config_entry ) -> HomematicipHAP: """Create a mocked homematic access point.""" return HomeFactory(hass, mock_connection, hmip_config_entry) @@ -98,7 +99,7 @@ def dummy_config_fixture() -> ConfigType: @pytest.fixture(name="mock_hap_with_service") async def mock_hap_with_service_fixture( - hass: HomeAssistantType, default_mock_hap_factory, dummy_config + hass: HomeAssistant, default_mock_hap_factory, dummy_config ) -> HomematicipHAP: """Create a fake homematic access point with hass services.""" mock_hap = await default_mock_hap_factory.async_get_mock_hap() diff --git a/tests/components/homematicip_cloud/helper.py b/tests/components/homematicip_cloud/helper.py index 8da5e4861c050..22d950d0817c4 100644 --- a/tests/components/homematicip_cloud/helper.py +++ b/tests/components/homematicip_cloud/helper.py @@ -19,7 +19,7 @@ ATTR_MODEL_TYPE, ) from homeassistant.components.homematicip_cloud.hap import HomematicipHAP -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from tests.common import load_fixture @@ -76,7 +76,7 @@ class HomeFactory: def __init__( self, - hass: HomeAssistantType, + hass: HomeAssistant, mock_connection, hmip_config_entry: config_entries.ConfigEntry, ): From d52bc2373f8488ec1c39389578ae4ee6531cc5eb Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 23 Apr 2021 09:55:20 +0200 Subject: [PATCH 0472/1317] Integrations i* - m*: Rename HomeAssistantType to HomeAssistant. (#49586) --- homeassistant/components/ihc/__init__.py | 8 ++-- homeassistant/components/ipp/config_flow.py | 5 ++- homeassistant/components/ipp/sensor.py | 4 +- homeassistant/components/isy994/services.py | 9 ++-- homeassistant/components/isy994/switch.py | 4 +- homeassistant/components/izone/__init__.py | 5 ++- homeassistant/components/izone/climate.py | 6 +-- homeassistant/components/izone/discovery.py | 6 +-- homeassistant/components/kulersky/light.py | 4 +- homeassistant/components/lock/group.py | 5 +-- .../components/lock/reproduce_state.py | 7 ++- homeassistant/components/lovelace/__init__.py | 6 +-- homeassistant/components/lyric/climate.py | 4 +- homeassistant/components/lyric/sensor.py | 4 +- .../components/media_player/group.py | 5 +-- .../media_player/reproduce_state.py | 7 ++- .../components/meteo_france/__init__.py | 11 ++--- .../components/meteo_france/sensor.py | 4 +- tests/components/insteon/test_config_flow.py | 44 +++++++++---------- tests/components/insteon/test_init.py | 16 +++---- tests/components/isy994/test_config_flow.py | 22 +++++----- .../keenetic_ndms2/test_config_flow.py | 6 +-- 22 files changed, 94 insertions(+), 98 deletions(-) diff --git a/homeassistant/components/ihc/__init__.py b/homeassistant/components/ihc/__init__.py index 959d86a7cc178..c0fe8944c6680 100644 --- a/homeassistant/components/ihc/__init__.py +++ b/homeassistant/components/ihc/__init__.py @@ -18,9 +18,9 @@ CONF_USERNAME, TEMP_CELSIUS, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import HomeAssistantType from .const import ( ATTR_CONTROLLER_ID, @@ -284,9 +284,7 @@ def get_manual_configuration(hass, config, conf, ihc_controller, controller_id): discovery.load_platform(hass, platform, DOMAIN, discovery_info, config) -def autosetup_ihc_products( - hass: HomeAssistantType, config, ihc_controller, controller_id -): +def autosetup_ihc_products(hass: HomeAssistant, config, ihc_controller, controller_id): """Auto setup of IHC products from the IHC project file.""" project_xml = ihc_controller.get_project() if not project_xml: @@ -343,7 +341,7 @@ def get_discovery_info(platform_setup, groups, controller_id): return discovery_data -def setup_service_functions(hass: HomeAssistantType): +def setup_service_functions(hass: HomeAssistant): """Set up the IHC service functions.""" def _get_controller(call): diff --git a/homeassistant/components/ipp/config_flow.py b/homeassistant/components/ipp/config_flow.py index 7c8a394731d57..6f2d036600fb1 100644 --- a/homeassistant/components/ipp/config_flow.py +++ b/homeassistant/components/ipp/config_flow.py @@ -23,16 +23,17 @@ CONF_SSL, CONF_VERIFY_SSL, ) +from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultDict from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from .const import CONF_BASE_PATH, CONF_SERIAL, CONF_UUID, DOMAIN _LOGGER = logging.getLogger(__name__) -async def validate_input(hass: HomeAssistantType, data: dict) -> dict[str, Any]: +async def validate_input(hass: HomeAssistant, data: dict) -> dict[str, Any]: """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. diff --git a/homeassistant/components/ipp/sensor.py b/homeassistant/components/ipp/sensor.py index 83826409ed8d4..bce0fb2bbb8c2 100644 --- a/homeassistant/components/ipp/sensor.py +++ b/homeassistant/components/ipp/sensor.py @@ -7,8 +7,8 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_LOCATION, DEVICE_CLASS_TIMESTAMP, PERCENTAGE +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util.dt import utcnow from . import IPPDataUpdateCoordinator, IPPEntity @@ -27,7 +27,7 @@ async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable[[list[Entity], bool], None], ) -> None: diff --git a/homeassistant/components/isy994/services.py b/homeassistant/components/isy994/services.py index 39966a9d9947d..6f0484e3ff44c 100644 --- a/homeassistant/components/isy994/services.py +++ b/homeassistant/components/isy994/services.py @@ -13,12 +13,11 @@ CONF_UNIT_OF_MEASUREMENT, SERVICE_RELOAD, ) -from homeassistant.core import ServiceCall, callback +from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import async_get_platforms import homeassistant.helpers.entity_registry as er -from homeassistant.helpers.typing import HomeAssistantType from .const import ( _LOGGER, @@ -158,7 +157,7 @@ def valid_isy_commands(value: Any) -> str: @callback -def async_setup_services(hass: HomeAssistantType): +def async_setup_services(hass: HomeAssistant): """Create and register services for the ISY integration.""" existing_services = hass.services.async_services().get(DOMAIN) if existing_services and any( @@ -380,7 +379,7 @@ async def _async_send_node_command(call: ServiceCall): @callback -def async_unload_services(hass: HomeAssistantType): +def async_unload_services(hass: HomeAssistant): """Unload services for the ISY integration.""" if hass.data[DOMAIN]: # There is still another config entry for this domain, don't remove services. @@ -404,7 +403,7 @@ def async_unload_services(hass: HomeAssistantType): @callback -def async_setup_light_services(hass: HomeAssistantType): +def async_setup_light_services(hass: HomeAssistant): """Create device-specific services for the ISY Integration.""" platform = entity_platform.current_platform.get() diff --git a/homeassistant/components/isy994/switch.py b/homeassistant/components/isy994/switch.py index 28d2264f28350..0f274e579f6c2 100644 --- a/homeassistant/components/isy994/switch.py +++ b/homeassistant/components/isy994/switch.py @@ -5,7 +5,7 @@ from homeassistant.components.switch import DOMAIN as SWITCH, SwitchEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .const import _LOGGER, DOMAIN as ISY994_DOMAIN, ISY994_NODES, ISY994_PROGRAMS from .entity import ISYNodeEntity, ISYProgramEntity @@ -13,7 +13,7 @@ async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable[[list], None], ) -> bool: diff --git a/homeassistant/components/izone/__init__.py b/homeassistant/components/izone/__init__.py index 95aad18989940..3d708ceea17c7 100644 --- a/homeassistant/components/izone/__init__.py +++ b/homeassistant/components/izone/__init__.py @@ -3,8 +3,9 @@ from homeassistant import config_entries from homeassistant.const import CONF_EXCLUDE +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from .const import DATA_CONFIG, IZONE from .discovery import async_start_discovery_service, async_stop_discovery_service @@ -23,7 +24,7 @@ ) -async def async_setup(hass: HomeAssistantType, config: ConfigType): +async def async_setup(hass: HomeAssistant, config: ConfigType): """Register the iZone component config.""" conf = config.get(IZONE) if not conf: diff --git a/homeassistant/components/izone/climate.py b/homeassistant/components/izone/climate.py index d509896e841b4..6d4630d4c4674 100644 --- a/homeassistant/components/izone/climate.py +++ b/homeassistant/components/izone/climate.py @@ -31,11 +31,11 @@ PRECISION_TENTHS, TEMP_CELSIUS, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.temperature import display_temp as show_temp -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from .const import ( DATA_CONFIG, @@ -70,7 +70,7 @@ async def async_setup_entry( - hass: HomeAssistantType, config: ConfigType, async_add_entities + hass: HomeAssistant, config: ConfigType, async_add_entities ): """Initialize an IZone Controller.""" disco = hass.data[DATA_DISCOVERY_SERVICE] diff --git a/homeassistant/components/izone/discovery.py b/homeassistant/components/izone/discovery.py index 2a4ad516af146..715c87bc7a8c0 100644 --- a/homeassistant/components/izone/discovery.py +++ b/homeassistant/components/izone/discovery.py @@ -2,9 +2,9 @@ import pizone from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.typing import HomeAssistantType from .const import ( DATA_DISCOVERY_SERVICE, @@ -47,7 +47,7 @@ def zone_update(self, ctrl: pizone.Controller, zone: pizone.Zone) -> None: async_dispatcher_send(self.hass, DISPATCH_ZONE_UPDATE, ctrl, zone) -async def async_start_discovery_service(hass: HomeAssistantType): +async def async_start_discovery_service(hass: HomeAssistant): """Set up the pizone internal discovery.""" disco = hass.data.get(DATA_DISCOVERY_SERVICE) if disco: @@ -73,7 +73,7 @@ async def shutdown_event(event): return disco -async def async_stop_discovery_service(hass: HomeAssistantType): +async def async_stop_discovery_service(hass: HomeAssistant): """Stop the discovery service.""" disco = hass.data.get(DATA_DISCOVERY_SERVICE) if not disco: diff --git a/homeassistant/components/kulersky/light.py b/homeassistant/components/kulersky/light.py index 980d4612ce9aa..29c163474a935 100644 --- a/homeassistant/components/kulersky/light.py +++ b/homeassistant/components/kulersky/light.py @@ -18,9 +18,9 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.typing import HomeAssistantType import homeassistant.util.color as color_util from .const import DATA_ADDRESSES, DATA_DISCOVERY_SUBSCRIPTION, DOMAIN @@ -33,7 +33,7 @@ async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable[[list[Entity], bool], None], ) -> None: diff --git a/homeassistant/components/lock/group.py b/homeassistant/components/lock/group.py index d64b217275063..d463f72242ba9 100644 --- a/homeassistant/components/lock/group.py +++ b/homeassistant/components/lock/group.py @@ -3,13 +3,12 @@ from homeassistant.components.group import GroupIntegrationRegistry from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED -from homeassistant.core import callback -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant, callback @callback def async_describe_on_off_states( - hass: HomeAssistantType, registry: GroupIntegrationRegistry + hass: HomeAssistant, registry: GroupIntegrationRegistry ) -> None: """Describe group on off states.""" registry.on_off_states({STATE_LOCKED}, STATE_UNLOCKED) diff --git a/homeassistant/components/lock/reproduce_state.py b/homeassistant/components/lock/reproduce_state.py index e7e79f49be98f..ea5cf370af6c5 100644 --- a/homeassistant/components/lock/reproduce_state.py +++ b/homeassistant/components/lock/reproduce_state.py @@ -13,8 +13,7 @@ STATE_LOCKED, STATE_UNLOCKED, ) -from homeassistant.core import Context, State -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import Context, HomeAssistant, State from . import DOMAIN @@ -24,7 +23,7 @@ async def _async_reproduce_state( - hass: HomeAssistantType, + hass: HomeAssistant, state: State, *, context: Context | None = None, @@ -60,7 +59,7 @@ async def _async_reproduce_state( async def async_reproduce_states( - hass: HomeAssistantType, + hass: HomeAssistant, states: Iterable[State], *, context: Context | None = None, diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index 45011239f167e..a5f0e04313925 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -6,11 +6,11 @@ from homeassistant.components import frontend from homeassistant.config import async_hass_config_yaml, async_process_component_config from homeassistant.const import CONF_FILENAME, CONF_MODE, CONF_RESOURCES -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import collection, config_validation as cv from homeassistant.helpers.service import async_register_admin_service -from homeassistant.helpers.typing import ConfigType, HomeAssistantType, ServiceCallType +from homeassistant.helpers.typing import ConfigType, ServiceCallType from homeassistant.loader import async_get_integration from . import dashboard, resources, websocket @@ -67,7 +67,7 @@ ) -async def async_setup(hass: HomeAssistantType, config: ConfigType): +async def async_setup(hass: HomeAssistant, config: ConfigType): """Set up the Lovelace commands.""" mode = config[DOMAIN][CONF_MODE] yaml_resources = config[DOMAIN].get(CONF_RESOURCES) diff --git a/homeassistant/components/lyric/climate.py b/homeassistant/components/lyric/climate.py index 649706f9d8ef2..d61d638b99118 100644 --- a/homeassistant/components/lyric/climate.py +++ b/homeassistant/components/lyric/climate.py @@ -25,10 +25,10 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE +from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import LyricDeviceEntity @@ -88,7 +88,7 @@ async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up the Honeywell Lyric climate platform based on a config entry.""" coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] diff --git a/homeassistant/components/lyric/sensor.py b/homeassistant/components/lyric/sensor.py index db90f474124dc..f4d4d4b999ad3 100644 --- a/homeassistant/components/lyric/sensor.py +++ b/homeassistant/components/lyric/sensor.py @@ -11,7 +11,7 @@ DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, ) -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.util import dt as dt_util @@ -34,7 +34,7 @@ async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up the Honeywell Lyric sensor platform based on a config entry.""" coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] diff --git a/homeassistant/components/media_player/group.py b/homeassistant/components/media_player/group.py index 6ecf90fc0a186..b7b2efb55c870 100644 --- a/homeassistant/components/media_player/group.py +++ b/homeassistant/components/media_player/group.py @@ -9,13 +9,12 @@ STATE_PAUSED, STATE_PLAYING, ) -from homeassistant.core import callback -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant, callback @callback def async_describe_on_off_states( - hass: HomeAssistantType, registry: GroupIntegrationRegistry + hass: HomeAssistant, registry: GroupIntegrationRegistry ) -> None: """Describe group on off states.""" registry.on_off_states( diff --git a/homeassistant/components/media_player/reproduce_state.py b/homeassistant/components/media_player/reproduce_state.py index 5d491a83ce187..115d6da447d06 100644 --- a/homeassistant/components/media_player/reproduce_state.py +++ b/homeassistant/components/media_player/reproduce_state.py @@ -19,8 +19,7 @@ STATE_PAUSED, STATE_PLAYING, ) -from homeassistant.core import Context, State -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import Context, HomeAssistant, State from .const import ( ATTR_INPUT_SOURCE, @@ -40,7 +39,7 @@ async def _async_reproduce_states( - hass: HomeAssistantType, + hass: HomeAssistant, state: State, *, context: Context | None = None, @@ -104,7 +103,7 @@ async def call_service(service: str, keys: Iterable) -> None: async def async_reproduce_states( - hass: HomeAssistantType, + hass: HomeAssistant, states: Iterable[State], *, context: Context | None = None, diff --git a/homeassistant/components/meteo_france/__init__.py b/homeassistant/components/meteo_france/__init__.py index 1229a4e43affa..cdd55c06db75c 100644 --- a/homeassistant/components/meteo_france/__init__.py +++ b/homeassistant/components/meteo_france/__init__.py @@ -9,9 +9,10 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import ( @@ -38,7 +39,7 @@ ) -async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up Meteo-France from legacy config file.""" conf = config.get(DOMAIN) if not conf: @@ -54,7 +55,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: return True -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up an Meteo-France account from a config entry.""" hass.data.setdefault(DOMAIN, {}) @@ -180,7 +181,7 @@ async def _async_update_data_alert(): return True -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" if hass.data[DOMAIN][entry.entry_id][COORDINATOR_ALERT]: @@ -210,6 +211,6 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): return unload_ok -async def _async_update_listener(hass: HomeAssistantType, entry: ConfigEntry): +async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry): """Handle options update.""" await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/meteo_france/sensor.py b/homeassistant/components/meteo_france/sensor.py index b6ec221a97e92..802305667fc02 100644 --- a/homeassistant/components/meteo_france/sensor.py +++ b/homeassistant/components/meteo_france/sensor.py @@ -9,7 +9,7 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ATTRIBUTION -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -39,7 +39,7 @@ async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up the Meteo-France sensor platform.""" coordinator_forecast = hass.data[DOMAIN][entry.entry_id][COORDINATOR_FORECAST] diff --git a/tests/components/insteon/test_config_flow.py b/tests/components/insteon/test_config_flow.py index 1b08317ca303c..796c9b69d59ee 100644 --- a/tests/components/insteon/test_config_flow.py +++ b/tests/components/insteon/test_config_flow.py @@ -35,7 +35,7 @@ CONF_PORT, CONF_USERNAME, ) -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .const import ( MOCK_HOSTNAME, @@ -93,7 +93,7 @@ async def _device_form(hass, flow_id, connection, user_input): return result, mock_setup, mock_setup_entry -async def test_form_select_modem(hass: HomeAssistantType): +async def test_form_select_modem(hass: HomeAssistant): """Test we get a modem form.""" await setup.async_setup_component(hass, "persistent_notification", {}) result = await _init_form(hass, HUB2) @@ -101,7 +101,7 @@ async def test_form_select_modem(hass: HomeAssistantType): assert result["type"] == "form" -async def test_fail_on_existing(hass: HomeAssistantType): +async def test_fail_on_existing(hass: HomeAssistant): """Test we fail if the integration is already configured.""" config_entry = MockConfigEntry( domain=DOMAIN, @@ -121,7 +121,7 @@ async def test_fail_on_existing(hass: HomeAssistantType): assert result["reason"] == "single_instance_allowed" -async def test_form_select_plm(hass: HomeAssistantType): +async def test_form_select_plm(hass: HomeAssistant): """Test we set up the PLM correctly.""" await setup.async_setup_component(hass, "persistent_notification", {}) result = await _init_form(hass, PLM) @@ -136,7 +136,7 @@ async def test_form_select_plm(hass: HomeAssistantType): assert len(mock_setup_entry.mock_calls) == 1 -async def test_form_select_hub_v1(hass: HomeAssistantType): +async def test_form_select_hub_v1(hass: HomeAssistant): """Test we set up the Hub v1 correctly.""" await setup.async_setup_component(hass, "persistent_notification", {}) result = await _init_form(hass, HUB1) @@ -154,7 +154,7 @@ async def test_form_select_hub_v1(hass: HomeAssistantType): assert len(mock_setup_entry.mock_calls) == 1 -async def test_form_select_hub_v2(hass: HomeAssistantType): +async def test_form_select_hub_v2(hass: HomeAssistant): """Test we set up the Hub v2 correctly.""" await setup.async_setup_component(hass, "persistent_notification", {}) result = await _init_form(hass, HUB2) @@ -172,7 +172,7 @@ async def test_form_select_hub_v2(hass: HomeAssistantType): assert len(mock_setup_entry.mock_calls) == 1 -async def test_failed_connection_plm(hass: HomeAssistantType): +async def test_failed_connection_plm(hass: HomeAssistant): """Test a failed connection with the PLM.""" await setup.async_setup_component(hass, "persistent_notification", {}) result = await _init_form(hass, PLM) @@ -184,7 +184,7 @@ async def test_failed_connection_plm(hass: HomeAssistantType): assert result2["errors"] == {"base": "cannot_connect"} -async def test_failed_connection_hub(hass: HomeAssistantType): +async def test_failed_connection_hub(hass: HomeAssistant): """Test a failed connection with a Hub.""" await setup.async_setup_component(hass, "persistent_notification", {}) result = await _init_form(hass, HUB2) @@ -206,7 +206,7 @@ async def _import_config(hass, config): ) -async def test_import_plm(hass: HomeAssistantType): +async def test_import_plm(hass: HomeAssistant): """Test importing a minimum PLM config from yaml.""" await setup.async_setup_component(hass, "persistent_notification", {}) @@ -233,7 +233,7 @@ async def _options_init_form(hass, entry_id, step): return result2 -async def test_import_min_hub_v2(hass: HomeAssistantType): +async def test_import_min_hub_v2(hass: HomeAssistant): """Test importing a minimum Hub v2 config from yaml.""" await setup.async_setup_component(hass, "persistent_notification", {}) @@ -251,7 +251,7 @@ async def test_import_min_hub_v2(hass: HomeAssistantType): assert entry.data[CONF_HUB_VERSION] == 2 -async def test_import_min_hub_v1(hass: HomeAssistantType): +async def test_import_min_hub_v1(hass: HomeAssistant): """Test importing a minimum Hub v1 config from yaml.""" await setup.async_setup_component(hass, "persistent_notification", {}) @@ -267,7 +267,7 @@ async def test_import_min_hub_v1(hass: HomeAssistantType): assert entry.data[CONF_HUB_VERSION] == 1 -async def test_import_existing(hass: HomeAssistantType): +async def test_import_existing(hass: HomeAssistant): """Test we fail on an existing config imported.""" config_entry = MockConfigEntry( domain=DOMAIN, @@ -285,7 +285,7 @@ async def test_import_existing(hass: HomeAssistantType): assert result["reason"] == "single_instance_allowed" -async def test_import_failed_connection(hass: HomeAssistantType): +async def test_import_failed_connection(hass: HomeAssistant): """Test a failed connection on import.""" await setup.async_setup_component(hass, "persistent_notification", {}) @@ -310,7 +310,7 @@ async def _options_form(hass, flow_id, user_input): return result, mock_setup_entry -async def test_options_change_hub_config(hass: HomeAssistantType): +async def test_options_change_hub_config(hass: HomeAssistant): """Test changing Hub v2 config.""" config_entry = MockConfigEntry( domain=DOMAIN, @@ -337,7 +337,7 @@ async def test_options_change_hub_config(hass: HomeAssistantType): assert config_entry.data == {**user_input, CONF_HUB_VERSION: 2} -async def test_options_add_device_override(hass: HomeAssistantType): +async def test_options_add_device_override(hass: HomeAssistant): """Test adding a device override.""" config_entry = MockConfigEntry( domain=DOMAIN, @@ -380,7 +380,7 @@ async def test_options_add_device_override(hass: HomeAssistantType): assert result["data"] != result3["data"] -async def test_options_remove_device_override(hass: HomeAssistantType): +async def test_options_remove_device_override(hass: HomeAssistant): """Test removing a device override.""" config_entry = MockConfigEntry( domain=DOMAIN, @@ -404,7 +404,7 @@ async def test_options_remove_device_override(hass: HomeAssistantType): assert len(config_entry.options[CONF_OVERRIDE]) == 1 -async def test_options_remove_device_override_with_x10(hass: HomeAssistantType): +async def test_options_remove_device_override_with_x10(hass: HomeAssistant): """Test removing a device override when an X10 device is configured.""" config_entry = MockConfigEntry( domain=DOMAIN, @@ -437,7 +437,7 @@ async def test_options_remove_device_override_with_x10(hass: HomeAssistantType): assert len(config_entry.options[CONF_X10]) == 1 -async def test_options_add_x10_device(hass: HomeAssistantType): +async def test_options_add_x10_device(hass: HomeAssistant): """Test adding an X10 device.""" config_entry = MockConfigEntry( domain=DOMAIN, @@ -484,7 +484,7 @@ async def test_options_add_x10_device(hass: HomeAssistantType): assert result2["data"] != result3["data"] -async def test_options_remove_x10_device(hass: HomeAssistantType): +async def test_options_remove_x10_device(hass: HomeAssistant): """Test removing an X10 device.""" config_entry = MockConfigEntry( domain=DOMAIN, @@ -523,7 +523,7 @@ async def test_options_remove_x10_device(hass: HomeAssistantType): assert len(config_entry.options[CONF_X10]) == 1 -async def test_options_remove_x10_device_with_override(hass: HomeAssistantType): +async def test_options_remove_x10_device_with_override(hass: HomeAssistant): """Test removing an X10 device when a device override is configured.""" config_entry = MockConfigEntry( domain=DOMAIN, @@ -564,7 +564,7 @@ async def test_options_remove_x10_device_with_override(hass: HomeAssistantType): assert len(config_entry.options[CONF_OVERRIDE]) == 1 -async def test_options_dup_selection(hass: HomeAssistantType): +async def test_options_dup_selection(hass: HomeAssistant): """Test if a duplicate selection was made in options.""" config_entry = MockConfigEntry( domain=DOMAIN, @@ -586,7 +586,7 @@ async def test_options_dup_selection(hass: HomeAssistantType): assert result2["errors"] == {"base": "select_single"} -async def test_options_override_bad_data(hass: HomeAssistantType): +async def test_options_override_bad_data(hass: HomeAssistant): """Test for bad data in a device override.""" config_entry = MockConfigEntry( diff --git a/tests/components/insteon/test_init.py b/tests/components/insteon/test_init.py index 01546453868e8..ecd3dfc5620fb 100644 --- a/tests/components/insteon/test_init.py +++ b/tests/components/insteon/test_init.py @@ -23,7 +23,7 @@ CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from .const import ( @@ -54,7 +54,7 @@ async def mock_failed_connection(*args, **kwargs): raise ConnectionError("Connection failed") -async def test_setup_entry(hass: HomeAssistantType): +async def test_setup_entry(hass: HomeAssistant): """Test setting up the entry.""" config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_INPUT_PLM) config_entry.add_to_hass(hass) @@ -77,7 +77,7 @@ async def test_setup_entry(hass: HomeAssistantType): assert mock_close.called -async def test_import_plm(hass: HomeAssistantType): +async def test_import_plm(hass: HomeAssistant): """Test setting up the entry from YAML to a PLM.""" config = {} config[DOMAIN] = MOCK_IMPORT_CONFIG_PLM @@ -102,7 +102,7 @@ async def test_import_plm(hass: HomeAssistantType): assert CONF_PORT not in data -async def test_import_hub1(hass: HomeAssistantType): +async def test_import_hub1(hass: HomeAssistant): """Test setting up the entry from YAML to a hub v1.""" config = {} config[DOMAIN] = MOCK_IMPORT_MINIMUM_HUB_V1 @@ -129,7 +129,7 @@ async def test_import_hub1(hass: HomeAssistantType): assert CONF_PASSWORD not in data -async def test_import_hub2(hass: HomeAssistantType): +async def test_import_hub2(hass: HomeAssistant): """Test setting up the entry from YAML to a hub v2.""" config = {} config[DOMAIN] = MOCK_IMPORT_MINIMUM_HUB_V2 @@ -156,7 +156,7 @@ async def test_import_hub2(hass: HomeAssistantType): assert data[CONF_PASSWORD] == MOCK_IMPORT_MINIMUM_HUB_V2[CONF_PASSWORD] -async def test_import_options(hass: HomeAssistantType): +async def test_import_options(hass: HomeAssistant): """Test setting up the entry from YAML including options.""" config = {} config[DOMAIN] = MOCK_IMPORT_FULL_CONFIG_PLM @@ -189,7 +189,7 @@ async def test_import_options(hass: HomeAssistantType): assert options[CONF_X10][1] == MOCK_IMPORT_FULL_CONFIG_PLM[CONF_X10][1] -async def test_import_failed_connection(hass: HomeAssistantType): +async def test_import_failed_connection(hass: HomeAssistant): """Test a failed connection in import does not create a config entry.""" config = {} config[DOMAIN] = MOCK_IMPORT_CONFIG_PLM @@ -208,7 +208,7 @@ async def test_import_failed_connection(hass: HomeAssistantType): assert not hass.config_entries.async_entries(DOMAIN) -async def test_setup_entry_failed_connection(hass: HomeAssistantType, caplog): +async def test_setup_entry_failed_connection(hass: HomeAssistant, caplog): """Test setting up the entry with a failed connection.""" config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_INPUT_PLM) config_entry.add_to_hass(hass) diff --git a/tests/components/isy994/test_config_flow.py b/tests/components/isy994/test_config_flow.py index c2236006b3908..1107e184e9b6b 100644 --- a/tests/components/isy994/test_config_flow.py +++ b/tests/components/isy994/test_config_flow.py @@ -17,7 +17,7 @@ ) from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_SSDP from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -70,7 +70,7 @@ PATCH_ASYNC_SETUP_ENTRY = "homeassistant.components.isy994.async_setup_entry" -async def test_form(hass: HomeAssistantType): +async def test_form(hass: HomeAssistant): """Test we get the form.""" await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( @@ -103,7 +103,7 @@ async def test_form(hass: HomeAssistantType): assert len(mock_setup_entry.mock_calls) == 1 -async def test_form_invalid_host(hass: HomeAssistantType): +async def test_form_invalid_host(hass: HomeAssistant): """Test we handle invalid host.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -123,7 +123,7 @@ async def test_form_invalid_host(hass: HomeAssistantType): assert result2["errors"] == {"base": "invalid_host"} -async def test_form_invalid_auth(hass: HomeAssistantType): +async def test_form_invalid_auth(hass: HomeAssistant): """Test we handle invalid auth.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -141,7 +141,7 @@ async def test_form_invalid_auth(hass: HomeAssistantType): assert result2["errors"] == {"base": "invalid_auth"} -async def test_form_cannot_connect(hass: HomeAssistantType): +async def test_form_cannot_connect(hass: HomeAssistant): """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -159,7 +159,7 @@ async def test_form_cannot_connect(hass: HomeAssistantType): assert result2["errors"] == {"base": "cannot_connect"} -async def test_form_existing_config_entry(hass: HomeAssistantType): +async def test_form_existing_config_entry(hass: HomeAssistant): """Test if config entry already exists.""" MockConfigEntry(domain=DOMAIN, unique_id=MOCK_UUID).add_to_hass(hass) await setup.async_setup_component(hass, "persistent_notification", {}) @@ -182,7 +182,7 @@ async def test_form_existing_config_entry(hass: HomeAssistantType): assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT -async def test_import_flow_some_fields(hass: HomeAssistantType) -> None: +async def test_import_flow_some_fields(hass: HomeAssistant) -> None: """Test import config flow with just the basic fields.""" with patch(PATCH_CONFIGURATION) as mock_config_class, patch( PATCH_CONNECTION @@ -205,7 +205,7 @@ async def test_import_flow_some_fields(hass: HomeAssistantType) -> None: assert result["data"][CONF_PASSWORD] == MOCK_PASSWORD -async def test_import_flow_with_https(hass: HomeAssistantType) -> None: +async def test_import_flow_with_https(hass: HomeAssistant) -> None: """Test import config with https.""" with patch(PATCH_CONFIGURATION) as mock_config_class, patch( @@ -229,7 +229,7 @@ async def test_import_flow_with_https(hass: HomeAssistantType) -> None: assert result["data"][CONF_PASSWORD] == MOCK_PASSWORD -async def test_import_flow_all_fields(hass: HomeAssistantType) -> None: +async def test_import_flow_all_fields(hass: HomeAssistant) -> None: """Test import config flow with all fields.""" with patch(PATCH_CONFIGURATION) as mock_config_class, patch( PATCH_CONNECTION @@ -257,7 +257,7 @@ async def test_import_flow_all_fields(hass: HomeAssistantType) -> None: assert result["data"][CONF_TLS_VER] == MOCK_TLS_VERSION -async def test_form_ssdp_already_configured(hass: HomeAssistantType) -> None: +async def test_form_ssdp_already_configured(hass: HomeAssistant) -> None: """Test ssdp abort when the serial number is already configured.""" await setup.async_setup_component(hass, "persistent_notification", {}) @@ -279,7 +279,7 @@ async def test_form_ssdp_already_configured(hass: HomeAssistantType) -> None: assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT -async def test_form_ssdp(hass: HomeAssistantType): +async def test_form_ssdp(hass: HomeAssistant): """Test we can setup from ssdp.""" await setup.async_setup_component(hass, "persistent_notification", {}) diff --git a/tests/components/keenetic_ndms2/test_config_flow.py b/tests/components/keenetic_ndms2/test_config_flow.py index ae22ea31ffb37..b96448101cf89 100644 --- a/tests/components/keenetic_ndms2/test_config_flow.py +++ b/tests/components/keenetic_ndms2/test_config_flow.py @@ -9,7 +9,7 @@ from homeassistant import config_entries, data_entry_flow from homeassistant.components import keenetic_ndms2 as keenetic from homeassistant.components.keenetic_ndms2 import const -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from . import MOCK_DATA, MOCK_NAME, MOCK_OPTIONS @@ -43,7 +43,7 @@ def mock_keenetic_connect_failed(): yield -async def test_flow_works(hass: HomeAssistantType, connect): +async def test_flow_works(hass: HomeAssistant, connect): """Test config flow.""" result = await hass.config_entries.flow.async_init( @@ -67,7 +67,7 @@ async def test_flow_works(hass: HomeAssistantType, connect): assert len(mock_setup_entry.mock_calls) == 1 -async def test_import_works(hass: HomeAssistantType, connect): +async def test_import_works(hass: HomeAssistant, connect): """Test config flow.""" with patch( From a3966192515a54ab8eae70f9418da679211ac85f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Fri, 23 Apr 2021 10:56:42 +0300 Subject: [PATCH 0473/1317] Use disabled_by constants (#49584) Co-authored-by: J. Nick Koston --- .../components/config/config_entries.py | 2 +- .../components/config/device_registry.py | 4 +- .../components/config/entity_registry.py | 4 +- tests/components/accuweather/test_sensor.py | 2 +- tests/components/brother/test_sensor.py | 2 +- .../components/config/test_config_entries.py | 10 +-- .../components/config/test_device_registry.py | 5 +- .../components/config/test_entity_registry.py | 8 +-- tests/components/hyperion/test_light.py | 2 +- tests/components/hyperion/test_switch.py | 2 +- tests/components/ipp/test_sensor.py | 2 +- tests/components/litejet/test_scene.py | 2 +- tests/components/met/test_weather.py | 2 +- .../components/monoprice/test_media_player.py | 2 +- tests/components/ozw/test_binary_sensor.py | 2 +- tests/components/ozw/test_sensor.py | 2 +- tests/components/sonarr/test_sensor.py | 2 +- tests/components/tasmota/test_sensor.py | 2 +- tests/components/wled/test_sensor.py | 2 +- .../components/zwave_js/test_binary_sensor.py | 2 +- tests/helpers/test_device_registry.py | 25 +++---- tests/helpers/test_entity.py | 6 +- tests/helpers/test_entity_platform.py | 2 +- tests/helpers/test_entity_registry.py | 65 +++++++++++-------- tests/test_config_entries.py | 8 ++- 25 files changed, 92 insertions(+), 75 deletions(-) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 6f9b96b9dfab9..264510627e090 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -311,7 +311,7 @@ async def config_entry_update(hass, connection, msg): "type": "config_entries/disable", "entry_id": str, # We only allow setting disabled_by user via API. - "disabled_by": vol.Any("user", None), + "disabled_by": vol.Any(config_entries.DISABLED_USER, None), } ) async def config_entry_disable(hass, connection, msg): diff --git a/homeassistant/components/config/device_registry.py b/homeassistant/components/config/device_registry.py index a43a863444aa3..4363fbbbe4da3 100644 --- a/homeassistant/components/config/device_registry.py +++ b/homeassistant/components/config/device_registry.py @@ -7,7 +7,7 @@ require_admin, ) from homeassistant.core import callback -from homeassistant.helpers.device_registry import async_get_registry +from homeassistant.helpers.device_registry import DISABLED_USER, async_get_registry WS_TYPE_LIST = "config/device_registry/list" SCHEMA_WS_LIST = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( @@ -22,7 +22,7 @@ vol.Optional("area_id"): vol.Any(str, None), vol.Optional("name_by_user"): vol.Any(str, None), # We only allow setting disabled_by user via API. - vol.Optional("disabled_by"): vol.Any("user", None), + vol.Optional("disabled_by"): vol.Any(DISABLED_USER, None), } ) diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index f0ee30ca120d6..43196acf3195e 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -10,7 +10,7 @@ ) from homeassistant.core import callback from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.entity_registry import async_get_registry +from homeassistant.helpers.entity_registry import DISABLED_USER, async_get_registry async def async_setup(hass): @@ -75,7 +75,7 @@ async def websocket_get_entity(hass, connection, msg): vol.Optional("area_id"): vol.Any(str, None), vol.Optional("new_entity_id"): str, # We only allow setting disabled_by user via API. - vol.Optional("disabled_by"): vol.Any("user", None), + vol.Optional("disabled_by"): vol.Any(DISABLED_USER, None), } ) async def websocket_update_entity(hass, connection, msg): diff --git a/tests/components/accuweather/test_sensor.py b/tests/components/accuweather/test_sensor.py index bafad72ec0bd8..a4436445340c2 100644 --- a/tests/components/accuweather/test_sensor.py +++ b/tests/components/accuweather/test_sensor.py @@ -173,7 +173,7 @@ async def test_sensor_disabled(hass): assert entry assert entry.unique_id == "0123456-apparenttemperature" assert entry.disabled - assert entry.disabled_by == "integration" + assert entry.disabled_by == er.DISABLED_INTEGRATION # Test enabling entity updated_entry = registry.async_update_entity( diff --git a/tests/components/brother/test_sensor.py b/tests/components/brother/test_sensor.py index 49f7340a37ac9..225cf5ce87aa3 100644 --- a/tests/components/brother/test_sensor.py +++ b/tests/components/brother/test_sensor.py @@ -250,7 +250,7 @@ async def test_disabled_by_default_sensors(hass): assert entry assert entry.unique_id == "0123456789_uptime" assert entry.disabled - assert entry.disabled_by == "integration" + assert entry.disabled_by == er.DISABLED_INTEGRATION async def test_availability(hass): diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 271333b092a9e..4b8155b65135c 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -73,7 +73,7 @@ def async_get_options_flow(config, options): domain="comp3", title="Test 3", source="bla3", - disabled_by="user", + disabled_by=core_ce.DISABLED_USER, ).add_to_hass(hass) resp = await client.get("/api/config/config_entries/entry") @@ -112,7 +112,7 @@ def async_get_options_flow(config, options): "connection_class": "unknown", "supports_options": False, "supports_unload": False, - "disabled_by": "user", + "disabled_by": core_ce.DISABLED_USER, "reason": None, }, ] @@ -800,14 +800,14 @@ async def test_disable_entry(hass, hass_ws_client): "id": 5, "type": "config_entries/disable", "entry_id": entry.entry_id, - "disabled_by": "user", + "disabled_by": core_ce.DISABLED_USER, } ) response = await ws_client.receive_json() assert response["success"] assert response["result"] == {"require_restart": True} - assert entry.disabled_by == "user" + assert entry.disabled_by == core_ce.DISABLED_USER assert entry.state == "failed_unload" # Enable @@ -853,7 +853,7 @@ async def test_disable_entry_nonexisting(hass, hass_ws_client): "id": 5, "type": "config_entries/disable", "entry_id": "non_existing", - "disabled_by": "user", + "disabled_by": core_ce.DISABLED_USER, } ) response = await ws_client.receive_json() diff --git a/tests/components/config/test_device_registry.py b/tests/components/config/test_device_registry.py index a123a2edb35cf..04a353cb2008b 100644 --- a/tests/components/config/test_device_registry.py +++ b/tests/components/config/test_device_registry.py @@ -2,6 +2,7 @@ import pytest from homeassistant.components.config import device_registry +from homeassistant.helpers import device_registry as helpers_dr from tests.common import mock_device_registry from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401 @@ -94,7 +95,7 @@ async def test_update_device(hass, client, registry): "device_id": device.id, "area_id": "12345A", "name_by_user": "Test Friendly Name", - "disabled_by": "user", + "disabled_by": helpers_dr.DISABLED_USER, "type": "config/device_registry/update", } ) @@ -104,5 +105,5 @@ async def test_update_device(hass, client, registry): assert msg["result"]["id"] == device.id assert msg["result"]["area_id"] == "12345A" assert msg["result"]["name_by_user"] == "Test Friendly Name" - assert msg["result"]["disabled_by"] == "user" + assert msg["result"]["disabled_by"] == helpers_dr.DISABLED_USER assert len(registry.devices) == 1 diff --git a/tests/components/config/test_entity_registry.py b/tests/components/config/test_entity_registry.py index 93d33bc956270..3d5861c2db323 100644 --- a/tests/components/config/test_entity_registry.py +++ b/tests/components/config/test_entity_registry.py @@ -5,7 +5,7 @@ from homeassistant.components.config import entity_registry from homeassistant.const import ATTR_ICON -from homeassistant.helpers.entity_registry import RegistryEntry +from homeassistant.helpers.entity_registry import DISABLED_USER, RegistryEntry from tests.common import ( MockConfigEntry, @@ -200,14 +200,14 @@ async def test_update_entity(hass, client): "id": 7, "type": "config/entity_registry/update", "entity_id": "test_domain.world", - "disabled_by": "user", + "disabled_by": DISABLED_USER, } ) msg = await client.receive_json() assert hass.states.get("test_domain.world") is None - assert registry.entities["test_domain.world"].disabled_by == "user" + assert registry.entities["test_domain.world"].disabled_by == DISABLED_USER # UPDATE DISABLED_BY TO NONE await client.send_json( @@ -305,7 +305,7 @@ async def test_enable_entity_disabled_device(hass, client, device_registry): identifiers={("bridgeid", "0123")}, manufacturer="manufacturer", model="model", - disabled_by="user", + disabled_by=DISABLED_USER, ) mock_registry( diff --git a/tests/components/hyperion/test_light.py b/tests/components/hyperion/test_light.py index bb20e6445650e..e0ab681b7a3a4 100644 --- a/tests/components/hyperion/test_light.py +++ b/tests/components/hyperion/test_light.py @@ -1345,7 +1345,7 @@ async def test_lights_can_be_enabled(hass: HomeAssistantType) -> None: entry = entity_registry.async_get(TEST_PRIORITY_LIGHT_ENTITY_ID_1) assert entry assert entry.disabled - assert entry.disabled_by == "integration" + assert entry.disabled_by == er.DISABLED_INTEGRATION entity_state = hass.states.get(TEST_PRIORITY_LIGHT_ENTITY_ID_1) assert not entity_state diff --git a/tests/components/hyperion/test_switch.py b/tests/components/hyperion/test_switch.py index 5105d80f40d5d..764f234eb0ec1 100644 --- a/tests/components/hyperion/test_switch.py +++ b/tests/components/hyperion/test_switch.py @@ -199,7 +199,7 @@ async def test_switches_can_be_enabled(hass: HomeAssistantType) -> None: entry = entity_registry.async_get(entity_id) assert entry assert entry.disabled - assert entry.disabled_by == "integration" + assert entry.disabled_by == er.DISABLED_INTEGRATION entity_state = hass.states.get(entity_id) assert not entity_state diff --git a/tests/components/ipp/test_sensor.py b/tests/components/ipp/test_sensor.py index 9366b290feffd..405d7309b2378 100644 --- a/tests/components/ipp/test_sensor.py +++ b/tests/components/ipp/test_sensor.py @@ -95,7 +95,7 @@ async def test_disabled_by_default_sensors( entry = registry.async_get("sensor.epson_xp_6000_series_uptime") assert entry assert entry.disabled - assert entry.disabled_by == "integration" + assert entry.disabled_by == er.DISABLED_INTEGRATION async def test_missing_entry_unique_id( diff --git a/tests/components/litejet/test_scene.py b/tests/components/litejet/test_scene.py index c76c8738f86d8..077793279d8cb 100644 --- a/tests/components/litejet/test_scene.py +++ b/tests/components/litejet/test_scene.py @@ -23,7 +23,7 @@ async def test_disabled_by_default(hass, mock_litejet): entry = registry.async_get(ENTITY_SCENE) assert entry assert entry.disabled - assert entry.disabled_by == "integration" + assert entry.disabled_by == er.DISABLED_INTEGRATION async def test_activate(hass, mock_litejet): diff --git a/tests/components/met/test_weather.py b/tests/components/met/test_weather.py index 92e9b67466814..32f36d096307c 100644 --- a/tests/components/met/test_weather.py +++ b/tests/components/met/test_weather.py @@ -21,7 +21,7 @@ async def test_tracking_home(hass, mock_weather): entry = registry.async_get("weather.test_home_hourly") assert entry assert entry.disabled - assert entry.disabled_by == "integration" + assert entry.disabled_by == er.DISABLED_INTEGRATION # Test we track config await hass.config.async_update(latitude=10, longitude=20) diff --git a/tests/components/monoprice/test_media_player.py b/tests/components/monoprice/test_media_player.py index 9d3bbd40a4640..977d57cb07ca0 100644 --- a/tests/components/monoprice/test_media_player.py +++ b/tests/components/monoprice/test_media_player.py @@ -518,7 +518,7 @@ async def test_first_run_with_failing_zones(hass): entry = registry.async_get(ZONE_7_ID) assert entry.disabled - assert entry.disabled_by == "integration" + assert entry.disabled_by == er.DISABLED_INTEGRATION async def test_not_first_run_with_failing_zone(hass): diff --git a/tests/components/ozw/test_binary_sensor.py b/tests/components/ozw/test_binary_sensor.py index 95b150b579141..e6af71d41b409 100644 --- a/tests/components/ozw/test_binary_sensor.py +++ b/tests/components/ozw/test_binary_sensor.py @@ -22,7 +22,7 @@ async def test_binary_sensor(hass, generic_data, binary_sensor_msg): entry = registry.async_get(entity_id) assert entry assert entry.disabled - assert entry.disabled_by == "integration" + assert entry.disabled_by == er.DISABLED_INTEGRATION # Test enabling legacy entity updated_entry = registry.async_update_entity( diff --git a/tests/components/ozw/test_sensor.py b/tests/components/ozw/test_sensor.py index 500bd81aa0be2..e043d5eb58d8d 100644 --- a/tests/components/ozw/test_sensor.py +++ b/tests/components/ozw/test_sensor.py @@ -43,7 +43,7 @@ async def test_sensor(hass, generic_data): entry = registry.async_get(entity_id) assert entry assert entry.disabled - assert entry.disabled_by == "integration" + assert entry.disabled_by == er.DISABLED_INTEGRATION # Test enabling entity updated_entry = registry.async_update_entity( diff --git a/tests/components/sonarr/test_sensor.py b/tests/components/sonarr/test_sensor.py index 3f99325c3ef7f..9824319f3ffb9 100644 --- a/tests/components/sonarr/test_sensor.py +++ b/tests/components/sonarr/test_sensor.py @@ -117,7 +117,7 @@ async def test_disabled_by_default_sensors( entry = registry.async_get(entity_id) assert entry assert entry.disabled - assert entry.disabled_by == "integration" + assert entry.disabled_by == er.DISABLED_INTEGRATION async def test_availability( diff --git a/tests/components/tasmota/test_sensor.py b/tests/components/tasmota/test_sensor.py index 2b7c388ca2f19..0d6820f2d3440 100644 --- a/tests/components/tasmota/test_sensor.py +++ b/tests/components/tasmota/test_sensor.py @@ -542,7 +542,7 @@ async def test_enable_status_sensor(hass, mqtt_mock, setup_tasmota): assert state is None entry = entity_reg.async_get("sensor.tasmota_signal") assert entry.disabled - assert entry.disabled_by == "integration" + assert entry.disabled_by == er.DISABLED_INTEGRATION # Enable the status sensor updated_entry = entity_reg.async_update_entity( diff --git a/tests/components/wled/test_sensor.py b/tests/components/wled/test_sensor.py index 9cebf2cda3220..f20e2f0419ad8 100644 --- a/tests/components/wled/test_sensor.py +++ b/tests/components/wled/test_sensor.py @@ -194,4 +194,4 @@ async def test_disabled_by_default_sensors( entry = registry.async_get(entity_id) assert entry assert entry.disabled - assert entry.disabled_by == "integration" + assert entry.disabled_by == er.DISABLED_INTEGRATION diff --git a/tests/components/zwave_js/test_binary_sensor.py b/tests/components/zwave_js/test_binary_sensor.py index ddfed9727e65a..421c808bc0bfd 100644 --- a/tests/components/zwave_js/test_binary_sensor.py +++ b/tests/components/zwave_js/test_binary_sensor.py @@ -69,7 +69,7 @@ async def test_disabled_legacy_sensor(hass, multisensor_6, integration): entry = registry.async_get(entity_id) assert entry assert entry.disabled - assert entry.disabled_by == "integration" + assert entry.disabled_by == er.DISABLED_INTEGRATION # Test enabling legacy entity updated_entry = registry.async_update_entity( diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 1a768662fc772..518434cf79b52 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -4,6 +4,7 @@ import pytest +from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_STARTED from homeassistant.core import CoreState, callback from homeassistant.exceptions import RequiredParameterMissing @@ -181,7 +182,7 @@ async def test_loading_from_storage(hass, hass_storage): "entry_type": "service", "area_id": "12345A", "name_by_user": "Test Friendly Name", - "disabled_by": "user", + "disabled_by": device_registry.DISABLED_USER, "suggested_area": "Kitchen", } ], @@ -212,7 +213,7 @@ async def test_loading_from_storage(hass, hass_storage): assert entry.area_id == "12345A" assert entry.name_by_user == "Test Friendly Name" assert entry.entry_type == "service" - assert entry.disabled_by == "user" + assert entry.disabled_by == device_registry.DISABLED_USER assert isinstance(entry.config_entries, set) assert isinstance(entry.connections, set) assert isinstance(entry.identifiers, set) @@ -493,7 +494,7 @@ async def test_loading_saving_data(hass, registry, area_registry): manufacturer="manufacturer", model="light", via_device=("hue", "0123"), - disabled_by="user", + disabled_by=device_registry.DISABLED_USER, ) orig_light2 = registry.async_get_or_create( @@ -542,7 +543,7 @@ async def test_loading_saving_data(hass, registry, area_registry): manufacturer="manufacturer", model="light", via_device=("hue", "0123"), - disabled_by="user", + disabled_by=device_registry.DISABLED_USER, suggested_area="Kitchen", ) @@ -651,7 +652,7 @@ async def test_update(registry): name_by_user="Test Friendly Name", new_identifiers=new_identifiers, via_device_id="98765B", - disabled_by="user", + disabled_by=device_registry.DISABLED_USER, ) assert mock_save.call_count == 1 @@ -662,7 +663,7 @@ async def test_update(registry): assert updated_entry.name_by_user == "Test Friendly Name" assert updated_entry.identifiers == new_identifiers assert updated_entry.via_device_id == "98765B" - assert updated_entry.disabled_by == "user" + assert updated_entry.disabled_by == device_registry.DISABLED_USER assert registry.async_get_device({("hue", "456")}) is None assert registry.async_get_device({("bla", "123")}) is None @@ -1226,21 +1227,23 @@ async def test_disable_config_entry_disables_devices(hass, registry): entry2 = registry.async_get_or_create( config_entry_id=config_entry.entry_id, connections={("mac", "34:56:AB:CD:EF:12")}, - disabled_by="user", + disabled_by=device_registry.DISABLED_USER, ) assert not entry1.disabled assert entry2.disabled - await hass.config_entries.async_set_disabled_by(config_entry.entry_id, "user") + await hass.config_entries.async_set_disabled_by( + config_entry.entry_id, config_entries.DISABLED_USER + ) await hass.async_block_till_done() entry1 = registry.async_get(entry1.id) assert entry1.disabled - assert entry1.disabled_by == "config_entry" + assert entry1.disabled_by == device_registry.DISABLED_CONFIG_ENTRY entry2 = registry.async_get(entry2.id) assert entry2.disabled - assert entry2.disabled_by == "user" + assert entry2.disabled_by == device_registry.DISABLED_USER await hass.config_entries.async_set_disabled_by(config_entry.entry_id, None) await hass.async_block_till_done() @@ -1249,4 +1252,4 @@ async def test_disable_config_entry_disables_devices(hass, registry): assert not entry1.disabled entry2 = registry.async_get(entry2.id) assert entry2.disabled - assert entry2.disabled_by == "user" + assert entry2.disabled_by == device_registry.DISABLED_USER diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 6eeabb59eba0c..8d587301fb849 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -575,7 +575,7 @@ async def test_warn_disabled(hass, caplog): entity_id="hello.world", unique_id="test-unique-id", platform="test-platform", - disabled_by="user", + disabled_by=entity_registry.DISABLED_USER, ) mock_registry(hass, {"hello.world": entry}) @@ -616,7 +616,9 @@ async def test_disabled_in_entity_registry(hass): await ent.add_to_platform_finish() assert hass.states.get("hello.world") is not None - entry2 = registry.async_update_entity("hello.world", disabled_by="user") + entry2 = registry.async_update_entity( + "hello.world", disabled_by=entity_registry.DISABLED_USER + ) await hass.async_block_till_done() assert entry2 != entry assert ent.registry_entry == entry2 diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index d24084ff51714..9ab269811f912 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -926,7 +926,7 @@ async def test_entity_disabled_by_integration(hass): entry_default = registry.async_get_or_create(DOMAIN, DOMAIN, "default") assert entry_default.disabled_by is None entry_disabled = registry.async_get_or_create(DOMAIN, DOMAIN, "disabled") - assert entry_disabled.disabled_by == "integration" + assert entry_disabled.disabled_by == er.DISABLED_INTEGRATION async def test_entity_info_added_to_entity_registry(hass): diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index d671aacebb3d9..a1050e5fc67a5 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -3,6 +3,7 @@ import pytest +from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_START, STATE_UNAVAILABLE from homeassistant.core import CoreState, callback, valid_entity_id from homeassistant.helpers import entity_registry as er @@ -239,19 +240,19 @@ async def test_loading_extra_values(hass, hass_storage): "entity_id": "test.disabled_user", "platform": "super_platform", "unique_id": "disabled-user", - "disabled_by": "user", + "disabled_by": er.DISABLED_USER, }, { "entity_id": "test.disabled_hass", "platform": "super_platform", "unique_id": "disabled-hass", - "disabled_by": "hass", + "disabled_by": er.DISABLED_HASS, }, { "entity_id": "test.invalid__entity", "platform": "super_platform", "unique_id": "invalid-hass", - "disabled_by": "hass", + "disabled_by": er.DISABLED_HASS, }, ] }, @@ -361,7 +362,7 @@ async def test_migration(hass): "unique_id": "test-unique", "platform": "test-platform", "name": "Test Name", - "disabled_by": "hass", + "disabled_by": er.DISABLED_HASS, } } with patch("os.path.isfile", return_value=True), patch("os.remove"), patch( @@ -378,7 +379,7 @@ async def test_migration(hass): config_entry=mock_config, ) assert entry.name == "Test Name" - assert entry.disabled_by == "hass" + assert entry.disabled_by == er.DISABLED_HASS assert entry.config_entry_id == "test-config-id" @@ -497,13 +498,15 @@ async def test_update_entity(registry): async def test_disabled_by(registry): """Test that we can disable an entry when we create it.""" - entry = registry.async_get_or_create("light", "hue", "5678", disabled_by="hass") - assert entry.disabled_by == "hass" + entry = registry.async_get_or_create( + "light", "hue", "5678", disabled_by=er.DISABLED_HASS + ) + assert entry.disabled_by == er.DISABLED_HASS entry = registry.async_get_or_create( - "light", "hue", "5678", disabled_by="integration" + "light", "hue", "5678", disabled_by=er.DISABLED_INTEGRATION ) - assert entry.disabled_by == "hass" + assert entry.disabled_by == er.DISABLED_HASS entry2 = registry.async_get_or_create("light", "hue", "1234") assert entry2.disabled_by is None @@ -519,12 +522,16 @@ async def test_disabled_by_system_options(registry): entry = registry.async_get_or_create( "light", "hue", "AAAA", config_entry=mock_config ) - assert entry.disabled_by == "integration" + assert entry.disabled_by == er.DISABLED_INTEGRATION entry2 = registry.async_get_or_create( - "light", "hue", "BBBB", config_entry=mock_config, disabled_by="user" + "light", + "hue", + "BBBB", + config_entry=mock_config, + disabled_by=er.DISABLED_USER, ) - assert entry2.disabled_by == "user" + assert entry2.disabled_by == er.DISABLED_USER async def test_restore_states(hass): @@ -755,7 +762,7 @@ async def test_disable_device_disables_entities(hass, registry): "ABCD", config_entry=config_entry, device_id=device_entry.id, - disabled_by="user", + disabled_by=er.DISABLED_USER, ) entry3 = registry.async_get_or_create( "light", @@ -763,25 +770,25 @@ async def test_disable_device_disables_entities(hass, registry): "EFGH", config_entry=config_entry, device_id=device_entry.id, - disabled_by="config_entry", + disabled_by=er.DISABLED_CONFIG_ENTRY, ) assert not entry1.disabled assert entry2.disabled assert entry3.disabled - device_registry.async_update_device(device_entry.id, disabled_by="user") + device_registry.async_update_device(device_entry.id, disabled_by=er.DISABLED_USER) await hass.async_block_till_done() entry1 = registry.async_get(entry1.entity_id) assert entry1.disabled - assert entry1.disabled_by == "device" + assert entry1.disabled_by == er.DISABLED_DEVICE entry2 = registry.async_get(entry2.entity_id) assert entry2.disabled - assert entry2.disabled_by == "user" + assert entry2.disabled_by == er.DISABLED_USER entry3 = registry.async_get(entry3.entity_id) assert entry3.disabled - assert entry3.disabled_by == "config_entry" + assert entry3.disabled_by == er.DISABLED_CONFIG_ENTRY device_registry.async_update_device(device_entry.id, disabled_by=None) await hass.async_block_till_done() @@ -790,10 +797,10 @@ async def test_disable_device_disables_entities(hass, registry): assert not entry1.disabled entry2 = registry.async_get(entry2.entity_id) assert entry2.disabled - assert entry2.disabled_by == "user" + assert entry2.disabled_by == er.DISABLED_USER entry3 = registry.async_get(entry3.entity_id) assert entry3.disabled - assert entry3.disabled_by == "config_entry" + assert entry3.disabled_by == er.DISABLED_CONFIG_ENTRY async def test_disable_config_entry_disables_entities(hass, registry): @@ -820,7 +827,7 @@ async def test_disable_config_entry_disables_entities(hass, registry): "ABCD", config_entry=config_entry, device_id=device_entry.id, - disabled_by="user", + disabled_by=er.DISABLED_USER, ) entry3 = registry.async_get_or_create( "light", @@ -828,25 +835,27 @@ async def test_disable_config_entry_disables_entities(hass, registry): "EFGH", config_entry=config_entry, device_id=device_entry.id, - disabled_by="device", + disabled_by=er.DISABLED_DEVICE, ) assert not entry1.disabled assert entry2.disabled assert entry3.disabled - await hass.config_entries.async_set_disabled_by(config_entry.entry_id, "user") + await hass.config_entries.async_set_disabled_by( + config_entry.entry_id, config_entries.DISABLED_USER + ) await hass.async_block_till_done() entry1 = registry.async_get(entry1.entity_id) assert entry1.disabled - assert entry1.disabled_by == "config_entry" + assert entry1.disabled_by == er.DISABLED_CONFIG_ENTRY entry2 = registry.async_get(entry2.entity_id) assert entry2.disabled - assert entry2.disabled_by == "user" + assert entry2.disabled_by == er.DISABLED_USER entry3 = registry.async_get(entry3.entity_id) assert entry3.disabled - assert entry3.disabled_by == "device" + assert entry3.disabled_by == er.DISABLED_DEVICE await hass.config_entries.async_set_disabled_by(config_entry.entry_id, None) await hass.async_block_till_done() @@ -855,7 +864,7 @@ async def test_disable_config_entry_disables_entities(hass, registry): assert not entry1.disabled entry2 = registry.async_get(entry2.entity_id) assert entry2.disabled - assert entry2.disabled_by == "user" + assert entry2.disabled_by == er.DISABLED_USER # The device was re-enabled, so entity disabled by the device will be re-enabled too entry3 = registry.async_get(entry3.entity_id) assert not entry3.disabled_by @@ -885,7 +894,7 @@ async def test_disabled_entities_excluded_from_entity_list(hass, registry): "ABCD", config_entry=config_entry, device_id=device_entry.id, - disabled_by="user", + disabled_by=er.DISABLED_USER, ) entries = er.async_entries_for_device(registry, device_entry.id) diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 326c7ba19cadd..4de62cc0cfc0b 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -504,7 +504,9 @@ async def test_domains_gets_domains_excludes_ignore_and_disabled(manager): domain="ignored", source=config_entries.SOURCE_IGNORE ).add_to_manager(manager) MockConfigEntry(domain="test3").add_to_manager(manager) - MockConfigEntry(domain="disabled", disabled_by="user").add_to_manager(manager) + MockConfigEntry( + domain="disabled", disabled_by=config_entries.DISABLED_USER + ).add_to_manager(manager) assert manager.async_domains() == ["test", "test2", "test3"] assert manager.async_domains(include_ignore=False) == ["test", "test2", "test3"] assert manager.async_domains(include_disabled=False) == ["test", "test2", "test3"] @@ -1348,7 +1350,7 @@ async def test_reload_entry_entity_registry_ignores_no_entry(hass): # Test we ignore entities without config entry entry = registry.async_get_or_create("light", "hue", "123") - registry.async_update_entity(entry.entity_id, disabled_by="user") + registry.async_update_entity(entry.entity_id, disabled_by=er.DISABLED_USER) await hass.async_block_till_done() assert not handler.changed assert handler._remove_call_later is None @@ -1387,7 +1389,7 @@ async def test_reload_entry_entity_registry_works(hass): assert handler._remove_call_later is None # Disable entity, we should not do anything, only act when enabled. - registry.async_update_entity(entity_entry.entity_id, disabled_by="user") + registry.async_update_entity(entity_entry.entity_id, disabled_by=er.DISABLED_USER) await hass.async_block_till_done() assert not handler.changed assert handler._remove_call_later is None From 9685cefba44a2f53fb4d1fb51f17ac087e65ce3e Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 23 Apr 2021 10:11:58 +0200 Subject: [PATCH 0474/1317] Integrations h* - i*: Rename HomeAssistantType to HomeAssistant. (#49587) --- homeassistant/components/hyperion/__init__.py | 10 +-- homeassistant/components/hyperion/light.py | 5 +- homeassistant/components/hyperion/switch.py | 5 +- .../components/iaqualink/__init__.py | 9 +-- .../components/iaqualink/binary_sensor.py | 4 +- homeassistant/components/iaqualink/climate.py | 4 +- homeassistant/components/iaqualink/light.py | 4 +- homeassistant/components/iaqualink/sensor.py | 4 +- homeassistant/components/iaqualink/switch.py | 4 +- homeassistant/components/icloud/__init__.py | 9 +-- homeassistant/components/icloud/account.py | 4 +- .../components/icloud/device_tracker.py | 9 +-- homeassistant/components/icloud/sensor.py | 5 +- .../components/isy994/binary_sensor.py | 5 +- homeassistant/components/isy994/climate.py | 4 +- homeassistant/components/isy994/cover.py | 4 +- homeassistant/components/isy994/fan.py | 4 +- homeassistant/components/isy994/helpers.py | 4 +- homeassistant/components/isy994/light.py | 4 +- homeassistant/components/isy994/lock.py | 4 +- homeassistant/components/isy994/sensor.py | 4 +- tests/components/hyperion/__init__.py | 8 +-- tests/components/hyperion/test_config_flow.py | 62 ++++++++--------- tests/components/hyperion/test_light.py | 68 +++++++++---------- tests/components/hyperion/test_switch.py | 10 +-- tests/components/icloud/test_config_flow.py | 42 ++++++------ 26 files changed, 143 insertions(+), 156 deletions(-) diff --git a/homeassistant/components/hyperion/__init__.py b/homeassistant/components/hyperion/__init__.py index 0aa94e13cac48..74c6998dc0196 100644 --- a/homeassistant/components/hyperion/__init__.py +++ b/homeassistant/components/hyperion/__init__.py @@ -20,7 +20,7 @@ async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from .const import ( CONF_INSTANCE_CLIENTS, @@ -290,16 +290,12 @@ async def setup_then_listen() -> None: return True -async def _async_entry_updated( - hass: HomeAssistantType, config_entry: ConfigEntry -) -> None: +async def _async_entry_updated(hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Handle entry updates.""" await hass.config_entries.async_reload(config_entry.entry_id) -async def async_unload_entry( - hass: HomeAssistantType, config_entry: ConfigEntry -) -> bool: +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = all( await asyncio.gather( diff --git a/homeassistant/components/hyperion/light.py b/homeassistant/components/hyperion/light.py index f8d760c0a9fdf..4449b9baf7120 100644 --- a/homeassistant/components/hyperion/light.py +++ b/homeassistant/components/hyperion/light.py @@ -19,12 +19,11 @@ LightEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.typing import HomeAssistantType import homeassistant.util.color as color_util from . import ( @@ -81,7 +80,7 @@ async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable ) -> bool: """Set up a Hyperion platform from config entry.""" diff --git a/homeassistant/components/hyperion/switch.py b/homeassistant/components/hyperion/switch.py index 5a7dd0c2cf552..b7e7847e44759 100644 --- a/homeassistant/components/hyperion/switch.py +++ b/homeassistant/components/hyperion/switch.py @@ -26,12 +26,11 @@ from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import slugify from . import ( @@ -82,7 +81,7 @@ def _component_to_switch_name(component: str, instance_name: str) -> str: async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable ) -> bool: """Set up a Hyperion platform from config entry.""" entry_data = hass.data[DOMAIN][config_entry.entry_id] diff --git a/homeassistant/components/iaqualink/__init__.py b/homeassistant/components/iaqualink/__init__.py index 0435645d87cd1..86dd6cb29329e 100644 --- a/homeassistant/components/iaqualink/__init__.py +++ b/homeassistant/components/iaqualink/__init__.py @@ -27,6 +27,7 @@ from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -36,7 +37,7 @@ ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from .const import DOMAIN, UPDATE_INTERVAL @@ -58,7 +59,7 @@ ) -async def async_setup(hass: HomeAssistantType, config: ConfigType) -> None: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> None: """Set up the Aqualink component.""" conf = config.get(DOMAIN) @@ -74,7 +75,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> None: return True -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> None: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: """Set up Aqualink from a config entry.""" username = entry.data[CONF_USERNAME] password = entry.data[CONF_PASSWORD] @@ -157,7 +158,7 @@ async def _async_systems_update(now): return True -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" forward_unload = hass.config_entries.async_forward_entry_unload diff --git a/homeassistant/components/iaqualink/binary_sensor.py b/homeassistant/components/iaqualink/binary_sensor.py index 07edc2dd2eac8..26d446541e6ff 100644 --- a/homeassistant/components/iaqualink/binary_sensor.py +++ b/homeassistant/components/iaqualink/binary_sensor.py @@ -5,7 +5,7 @@ BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from . import AqualinkEntity from .const import DOMAIN as AQUALINK_DOMAIN @@ -14,7 +14,7 @@ async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up discovered binary sensors.""" devs = [] diff --git a/homeassistant/components/iaqualink/climate.py b/homeassistant/components/iaqualink/climate.py index 73988c4e52389..13245429c0a92 100644 --- a/homeassistant/components/iaqualink/climate.py +++ b/homeassistant/components/iaqualink/climate.py @@ -20,7 +20,7 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from . import AqualinkEntity, refresh_system from .const import CLIMATE_SUPPORTED_MODES, DOMAIN as AQUALINK_DOMAIN @@ -31,7 +31,7 @@ async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up discovered switches.""" devs = [] diff --git a/homeassistant/components/iaqualink/light.py b/homeassistant/components/iaqualink/light.py index b86b2c00f5764..79030e1e3ca6b 100644 --- a/homeassistant/components/iaqualink/light.py +++ b/homeassistant/components/iaqualink/light.py @@ -10,7 +10,7 @@ LightEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from . import AqualinkEntity, refresh_system from .const import DOMAIN as AQUALINK_DOMAIN @@ -19,7 +19,7 @@ async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up discovered lights.""" devs = [] diff --git a/homeassistant/components/iaqualink/sensor.py b/homeassistant/components/iaqualink/sensor.py index eac6e2b785190..ae32db9eb9ed6 100644 --- a/homeassistant/components/iaqualink/sensor.py +++ b/homeassistant/components/iaqualink/sensor.py @@ -4,7 +4,7 @@ from homeassistant.components.sensor import DOMAIN, SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from . import AqualinkEntity from .const import DOMAIN as AQUALINK_DOMAIN @@ -13,7 +13,7 @@ async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up discovered sensors.""" devs = [] diff --git a/homeassistant/components/iaqualink/switch.py b/homeassistant/components/iaqualink/switch.py index d19c334b46128..a9fde150af398 100644 --- a/homeassistant/components/iaqualink/switch.py +++ b/homeassistant/components/iaqualink/switch.py @@ -1,7 +1,7 @@ """Support for Aqualink pool feature switches.""" from homeassistant.components.switch import DOMAIN, SwitchEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from . import AqualinkEntity, refresh_system from .const import DOMAIN as AQUALINK_DOMAIN @@ -10,7 +10,7 @@ async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up discovered switches.""" devs = [] diff --git a/homeassistant/components/icloud/__init__.py b/homeassistant/components/icloud/__init__.py index 6a3897a54c048..4bedb89ee0b3b 100644 --- a/homeassistant/components/icloud/__init__.py +++ b/homeassistant/components/icloud/__init__.py @@ -5,8 +5,9 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import ConfigType, HomeAssistantType, ServiceDataType +from homeassistant.helpers.typing import ConfigType, ServiceDataType from homeassistant.util import slugify from .account import IcloudAccount @@ -86,7 +87,7 @@ ) -async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up iCloud from legacy config file.""" conf = config.get(DOMAIN) @@ -103,7 +104,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: return True -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up an iCloud account from a config entry.""" hass.data.setdefault(DOMAIN, {}) @@ -221,7 +222,7 @@ def _get_account(account_identifier: str) -> any: return True -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" unload_ok = all( await asyncio.gather( diff --git a/homeassistant/components/icloud/account.py b/homeassistant/components/icloud/account.py index 5c3bd2bf51968..55fd661768d62 100644 --- a/homeassistant/components/icloud/account.py +++ b/homeassistant/components/icloud/account.py @@ -16,11 +16,11 @@ from homeassistant.components.zone import async_active_zone from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry from homeassistant.const import ATTR_ATTRIBUTION, CONF_USERNAME +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.event import track_point_in_utc_time from homeassistant.helpers.storage import Store -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import slugify from homeassistant.util.async_ import run_callback_threadsafe from homeassistant.util.dt import utcnow @@ -76,7 +76,7 @@ class IcloudAccount: def __init__( self, - hass: HomeAssistantType, + hass: HomeAssistant, username: str, password: str, icloud_dir: Store, diff --git a/homeassistant/components/icloud/device_tracker.py b/homeassistant/components/icloud/device_tracker.py index 502c2b00f8bb0..131f9335b4301 100644 --- a/homeassistant/components/icloud/device_tracker.py +++ b/homeassistant/components/icloud/device_tracker.py @@ -4,9 +4,8 @@ from homeassistant.components.device_tracker import SOURCE_TYPE_GPS from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.typing import HomeAssistantType from .account import IcloudAccount, IcloudDevice from .const import ( @@ -17,14 +16,12 @@ ) -async def async_setup_scanner( - hass: HomeAssistantType, config, see, discovery_info=None -): +async def async_setup_scanner(hass: HomeAssistant, config, see, discovery_info=None): """Old way of setting up the iCloud tracker.""" async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up device tracker for iCloud component.""" account = hass.data[DOMAIN][entry.unique_id] diff --git a/homeassistant/components/icloud/sensor.py b/homeassistant/components/icloud/sensor.py index f889495af25e5..3a875db81ed1a 100644 --- a/homeassistant/components/icloud/sensor.py +++ b/homeassistant/components/icloud/sensor.py @@ -4,17 +4,16 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.icon import icon_for_battery_level -from homeassistant.helpers.typing import HomeAssistantType from .account import IcloudAccount, IcloudDevice from .const import DOMAIN async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up device tracker for iCloud component.""" account = hass.data[DOMAIN][entry.unique_id] diff --git a/homeassistant/components/isy994/binary_sensor.py b/homeassistant/components/isy994/binary_sensor.py index 57b134e090060..6fe00c693bc73 100644 --- a/homeassistant/components/isy994/binary_sensor.py +++ b/homeassistant/components/isy994/binary_sensor.py @@ -26,9 +26,8 @@ BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.event import async_track_point_in_utc_time -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import dt as dt_util from .const import ( @@ -60,7 +59,7 @@ async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable[[list], None], ) -> bool: diff --git a/homeassistant/components/isy994/climate.py b/homeassistant/components/isy994/climate.py index 2c9aa52b3a740..efa0918745349 100644 --- a/homeassistant/components/isy994/climate.py +++ b/homeassistant/components/isy994/climate.py @@ -34,7 +34,7 @@ TEMP_CELSIUS, TEMP_FAHRENHEIT, ) -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .const import ( _LOGGER, @@ -61,7 +61,7 @@ async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable[[list], None], ) -> bool: diff --git a/homeassistant/components/isy994/cover.py b/homeassistant/components/isy994/cover.py index bdc2bc7f6d48c..65d91d24d2407 100644 --- a/homeassistant/components/isy994/cover.py +++ b/homeassistant/components/isy994/cover.py @@ -12,7 +12,7 @@ CoverEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .const import ( _LOGGER, @@ -27,7 +27,7 @@ async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable[[list], None], ) -> bool: diff --git a/homeassistant/components/isy994/fan.py b/homeassistant/components/isy994/fan.py index 183d4b31d3b55..e70201982b849 100644 --- a/homeassistant/components/isy994/fan.py +++ b/homeassistant/components/isy994/fan.py @@ -8,7 +8,7 @@ from homeassistant.components.fan import DOMAIN as FAN, SUPPORT_SET_SPEED, FanEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from homeassistant.util.percentage import ( int_states_in_range, percentage_to_ranged_value, @@ -23,7 +23,7 @@ async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable[[list], None], ) -> bool: diff --git a/homeassistant/components/isy994/helpers.py b/homeassistant/components/isy994/helpers.py index 81a74430d3ad8..5322c8e0abf2e 100644 --- a/homeassistant/components/isy994/helpers.py +++ b/homeassistant/components/isy994/helpers.py @@ -21,8 +21,8 @@ from homeassistant.components.light import DOMAIN as LIGHT from homeassistant.components.sensor import DOMAIN as SENSOR from homeassistant.components.switch import DOMAIN as SWITCH +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_registry import async_get_registry -from homeassistant.helpers.typing import HomeAssistantType from .const import ( _LOGGER, @@ -366,7 +366,7 @@ def _categorize_variables( async def migrate_old_unique_ids( - hass: HomeAssistantType, platform: str, devices: list[Any] | None + hass: HomeAssistant, platform: str, devices: list[Any] | None ) -> None: """Migrate to new controller-specific unique ids.""" registry = await async_get_registry(hass) diff --git a/homeassistant/components/isy994/light.py b/homeassistant/components/isy994/light.py index 7f35e96acaf57..4cb42492daf7e 100644 --- a/homeassistant/components/isy994/light.py +++ b/homeassistant/components/isy994/light.py @@ -11,8 +11,8 @@ LightEntity, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.typing import HomeAssistantType from .const import ( _LOGGER, @@ -29,7 +29,7 @@ async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable[[list], None], ) -> bool: diff --git a/homeassistant/components/isy994/lock.py b/homeassistant/components/isy994/lock.py index ceb26f3044c86..e8db796805b1d 100644 --- a/homeassistant/components/isy994/lock.py +++ b/homeassistant/components/isy994/lock.py @@ -5,7 +5,7 @@ from homeassistant.components.lock import DOMAIN as LOCK, LockEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .const import _LOGGER, DOMAIN as ISY994_DOMAIN, ISY994_NODES, ISY994_PROGRAMS from .entity import ISYNodeEntity, ISYProgramEntity @@ -15,7 +15,7 @@ async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable[[list], None], ) -> bool: diff --git a/homeassistant/components/isy994/sensor.py b/homeassistant/components/isy994/sensor.py index 2927fbb62b159..1c560c924cafa 100644 --- a/homeassistant/components/isy994/sensor.py +++ b/homeassistant/components/isy994/sensor.py @@ -8,7 +8,7 @@ from homeassistant.components.sensor import DOMAIN as SENSOR, SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .const import ( _LOGGER, @@ -26,7 +26,7 @@ async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable[[list], None], ) -> bool: diff --git a/tests/components/hyperion/__init__.py b/tests/components/hyperion/__init__.py index 7938527a12d0c..ac77a5a040764 100644 --- a/tests/components/hyperion/__init__.py +++ b/tests/components/hyperion/__init__.py @@ -11,8 +11,8 @@ from homeassistant.components.hyperion.const import CONF_PRIORITY, DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.typing import HomeAssistantType from tests.common import MockConfigEntry @@ -120,7 +120,7 @@ def create_mock_client() -> Mock: def add_test_config_entry( - hass: HomeAssistantType, + hass: HomeAssistant, data: dict[str, Any] | None = None, options: dict[str, Any] | None = None, ) -> ConfigEntry: @@ -142,7 +142,7 @@ def add_test_config_entry( async def setup_test_config_entry( - hass: HomeAssistantType, + hass: HomeAssistant, config_entry: ConfigEntry | None = None, hyperion_client: Mock | None = None, options: dict[str, Any] | None = None, @@ -173,7 +173,7 @@ def call_registered_callback( def register_test_entity( - hass: HomeAssistantType, domain: str, type_name: str, entity_id: str + hass: HomeAssistant, domain: str, type_name: str, entity_id: str ) -> None: """Register a test entity.""" unique_id = get_hyperion_unique_id(TEST_SYSINFO_ID, TEST_INSTANCE, type_name) diff --git a/tests/components/hyperion/test_config_flow.py b/tests/components/hyperion/test_config_flow.py index 381dc018407ec..d8b12e3c72b73 100644 --- a/tests/components/hyperion/test_config_flow.py +++ b/tests/components/hyperion/test_config_flow.py @@ -25,7 +25,7 @@ CONF_TOKEN, SERVICE_TURN_ON, ) -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from . import ( TEST_AUTH_REQUIRED_RESP, @@ -98,7 +98,7 @@ } -async def _create_mock_entry(hass: HomeAssistantType) -> MockConfigEntry: +async def _create_mock_entry(hass: HomeAssistant) -> MockConfigEntry: """Add a test Hyperion entity to hass.""" entry: MockConfigEntry = MockConfigEntry( # type: ignore[no-untyped-call] entry_id=TEST_CONFIG_ENTRY_ID, @@ -125,7 +125,7 @@ async def _create_mock_entry(hass: HomeAssistantType) -> MockConfigEntry: async def _init_flow( - hass: HomeAssistantType, + hass: HomeAssistant, source: str = SOURCE_USER, data: dict[str, Any] | None = None, ) -> Any: @@ -138,7 +138,7 @@ async def _init_flow( async def _configure_flow( - hass: HomeAssistantType, result: dict, user_input: dict[str, Any] | None = None + hass: HomeAssistant, result: dict, user_input: dict[str, Any] | None = None ) -> Any: """Provide input to a flow.""" user_input = user_input or {} @@ -156,7 +156,7 @@ async def _configure_flow( return result -async def test_user_if_no_configuration(hass: HomeAssistantType) -> None: +async def test_user_if_no_configuration(hass: HomeAssistant) -> None: """Check flow behavior when no configuration is present.""" result = await _init_flow(hass) @@ -165,7 +165,7 @@ async def test_user_if_no_configuration(hass: HomeAssistantType) -> None: assert result["handler"] == DOMAIN -async def test_user_existing_id_abort(hass: HomeAssistantType) -> None: +async def test_user_existing_id_abort(hass: HomeAssistant) -> None: """Verify a duplicate ID results in an abort.""" result = await _init_flow(hass) @@ -179,7 +179,7 @@ async def test_user_existing_id_abort(hass: HomeAssistantType) -> None: assert result["reason"] == "already_configured" -async def test_user_client_errors(hass: HomeAssistantType) -> None: +async def test_user_client_errors(hass: HomeAssistant) -> None: """Verify correct behaviour with client errors.""" result = await _init_flow(hass) @@ -205,7 +205,7 @@ async def test_user_client_errors(hass: HomeAssistantType) -> None: assert result["reason"] == "auth_required_error" -async def test_user_confirm_cannot_connect(hass: HomeAssistantType) -> None: +async def test_user_confirm_cannot_connect(hass: HomeAssistant) -> None: """Test a failure to connect during confirmation.""" result = await _init_flow(hass) @@ -224,7 +224,7 @@ async def test_user_confirm_cannot_connect(hass: HomeAssistantType) -> None: assert result["reason"] == "cannot_connect" -async def test_user_confirm_id_error(hass: HomeAssistantType) -> None: +async def test_user_confirm_id_error(hass: HomeAssistant) -> None: """Test a failure fetching the server id during confirmation.""" result = await _init_flow(hass) @@ -240,7 +240,7 @@ async def test_user_confirm_id_error(hass: HomeAssistantType) -> None: assert result["reason"] == "no_id" -async def test_user_noauth_flow_success(hass: HomeAssistantType) -> None: +async def test_user_noauth_flow_success(hass: HomeAssistant) -> None: """Check a full flow without auth.""" result = await _init_flow(hass) @@ -258,7 +258,7 @@ async def test_user_noauth_flow_success(hass: HomeAssistantType) -> None: } -async def test_user_auth_required(hass: HomeAssistantType) -> None: +async def test_user_auth_required(hass: HomeAssistant) -> None: """Verify correct behaviour when auth is required.""" result = await _init_flow(hass) @@ -273,7 +273,7 @@ async def test_user_auth_required(hass: HomeAssistantType) -> None: assert result["step_id"] == "auth" -async def test_auth_static_token_auth_required_fail(hass: HomeAssistantType) -> None: +async def test_auth_static_token_auth_required_fail(hass: HomeAssistant) -> None: """Verify correct behaviour with a failed auth required call.""" result = await _init_flow(hass) @@ -287,7 +287,7 @@ async def test_auth_static_token_auth_required_fail(hass: HomeAssistantType) -> assert result["reason"] == "auth_required_error" -async def test_auth_static_token_success(hass: HomeAssistantType) -> None: +async def test_auth_static_token_success(hass: HomeAssistant) -> None: """Test a successful flow with a static token.""" result = await _init_flow(hass) assert result["step_id"] == "user" @@ -312,7 +312,7 @@ async def test_auth_static_token_success(hass: HomeAssistantType) -> None: } -async def test_auth_static_token_login_connect_fail(hass: HomeAssistantType) -> None: +async def test_auth_static_token_login_connect_fail(hass: HomeAssistant) -> None: """Test correct behavior with a static token that cannot connect.""" result = await _init_flow(hass) assert result["step_id"] == "user" @@ -333,7 +333,7 @@ async def test_auth_static_token_login_connect_fail(hass: HomeAssistantType) -> assert result["reason"] == "cannot_connect" -async def test_auth_static_token_login_fail(hass: HomeAssistantType) -> None: +async def test_auth_static_token_login_fail(hass: HomeAssistant) -> None: """Test correct behavior with a static token that cannot login.""" result = await _init_flow(hass) assert result["step_id"] == "user" @@ -356,7 +356,7 @@ async def test_auth_static_token_login_fail(hass: HomeAssistantType) -> None: assert result["errors"]["base"] == "invalid_access_token" -async def test_auth_create_token_approval_declined(hass: HomeAssistantType) -> None: +async def test_auth_create_token_approval_declined(hass: HomeAssistant) -> None: """Verify correct behaviour when a token request is declined.""" result = await _init_flow(hass) @@ -400,7 +400,7 @@ async def test_auth_create_token_approval_declined(hass: HomeAssistantType) -> N async def test_auth_create_token_approval_declined_task_canceled( - hass: HomeAssistantType, + hass: HomeAssistant, ) -> None: """Verify correct behaviour when a token request is declined.""" result = await _init_flow(hass) @@ -461,7 +461,7 @@ def create_task(arg: Any) -> CanceledAwaitableMock: async def test_auth_create_token_when_issued_token_fails( - hass: HomeAssistantType, + hass: HomeAssistant, ) -> None: """Verify correct behaviour when a token is granted by fails to authenticate.""" result = await _init_flow(hass) @@ -506,7 +506,7 @@ async def test_auth_create_token_when_issued_token_fails( assert result["reason"] == "cannot_connect" -async def test_auth_create_token_success(hass: HomeAssistantType) -> None: +async def test_auth_create_token_success(hass: HomeAssistant) -> None: """Verify correct behaviour when a token is successfully created.""" result = await _init_flow(hass) @@ -552,7 +552,7 @@ async def test_auth_create_token_success(hass: HomeAssistantType) -> None: async def test_auth_create_token_success_but_login_fail( - hass: HomeAssistantType, + hass: HomeAssistant, ) -> None: """Verify correct behaviour when a token is successfully created but the login fails.""" result = await _init_flow(hass) @@ -592,7 +592,7 @@ async def test_auth_create_token_success_but_login_fail( assert result["reason"] == "auth_new_token_not_work_error" -async def test_ssdp_success(hass: HomeAssistantType) -> None: +async def test_ssdp_success(hass: HomeAssistant) -> None: """Check an SSDP flow.""" client = create_mock_client() @@ -617,7 +617,7 @@ async def test_ssdp_success(hass: HomeAssistantType) -> None: } -async def test_ssdp_cannot_connect(hass: HomeAssistantType) -> None: +async def test_ssdp_cannot_connect(hass: HomeAssistant) -> None: """Check an SSDP flow that cannot connect.""" client = create_mock_client() @@ -633,7 +633,7 @@ async def test_ssdp_cannot_connect(hass: HomeAssistantType) -> None: assert result["reason"] == "cannot_connect" -async def test_ssdp_missing_serial(hass: HomeAssistantType) -> None: +async def test_ssdp_missing_serial(hass: HomeAssistant) -> None: """Check an SSDP flow where no id is provided.""" client = create_mock_client() @@ -650,7 +650,7 @@ async def test_ssdp_missing_serial(hass: HomeAssistantType) -> None: assert result["reason"] == "no_id" -async def test_ssdp_failure_bad_port_json(hass: HomeAssistantType) -> None: +async def test_ssdp_failure_bad_port_json(hass: HomeAssistant) -> None: """Check an SSDP flow with bad json port.""" client = create_mock_client() @@ -668,7 +668,7 @@ async def test_ssdp_failure_bad_port_json(hass: HomeAssistantType) -> None: assert result["data"][CONF_PORT] == const.DEFAULT_PORT_JSON -async def test_ssdp_failure_bad_port_ui(hass: HomeAssistantType) -> None: +async def test_ssdp_failure_bad_port_ui(hass: HomeAssistant) -> None: """Check an SSDP flow with bad ui port.""" client = create_mock_client() @@ -703,7 +703,7 @@ async def test_ssdp_failure_bad_port_ui(hass: HomeAssistantType) -> None: } -async def test_ssdp_abort_duplicates(hass: HomeAssistantType) -> None: +async def test_ssdp_abort_duplicates(hass: HomeAssistant) -> None: """Check an SSDP flow where no id is provided.""" client = create_mock_client() @@ -723,7 +723,7 @@ async def test_ssdp_abort_duplicates(hass: HomeAssistantType) -> None: assert result_2["reason"] == "already_in_progress" -async def test_options_priority(hass: HomeAssistantType) -> None: +async def test_options_priority(hass: HomeAssistant) -> None: """Check an options flow priority option.""" config_entry = add_test_config_entry(hass) @@ -761,7 +761,7 @@ async def test_options_priority(hass: HomeAssistantType) -> None: assert client.async_send_set_color.call_args[1][CONF_PRIORITY] == new_priority -async def test_options_effect_show_list(hass: HomeAssistantType) -> None: +async def test_options_effect_show_list(hass: HomeAssistant) -> None: """Check an options flow effect show list.""" config_entry = add_test_config_entry(hass) @@ -795,7 +795,7 @@ async def test_options_effect_show_list(hass: HomeAssistantType) -> None: ) -async def test_options_effect_hide_list_cannot_connect(hass: HomeAssistantType) -> None: +async def test_options_effect_hide_list_cannot_connect(hass: HomeAssistant) -> None: """Check an options flow effect hide list with a failed connection.""" config_entry = add_test_config_entry(hass) @@ -814,7 +814,7 @@ async def test_options_effect_hide_list_cannot_connect(hass: HomeAssistantType) assert result["reason"] == "cannot_connect" -async def test_reauth_success(hass: HomeAssistantType) -> None: +async def test_reauth_success(hass: HomeAssistant) -> None: """Check a reauth flow that succeeds.""" config_data = { @@ -848,7 +848,7 @@ async def test_reauth_success(hass: HomeAssistantType) -> None: assert CONF_TOKEN in config_entry.data -async def test_reauth_cannot_connect(hass: HomeAssistantType) -> None: +async def test_reauth_cannot_connect(hass: HomeAssistant) -> None: """Check a reauth flow that fails to connect.""" config_data = { diff --git a/tests/components/hyperion/test_light.py b/tests/components/hyperion/test_light.py index e0ab681b7a3a4..de0110cb19fb7 100644 --- a/tests/components/hyperion/test_light.py +++ b/tests/components/hyperion/test_light.py @@ -39,8 +39,8 @@ SERVICE_TURN_OFF, SERVICE_TURN_ON, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import dt import homeassistant.util.color as color_util @@ -74,7 +74,7 @@ def _get_config_entry_from_unique_id( - hass: HomeAssistantType, unique_id: str + hass: HomeAssistant, unique_id: str ) -> ConfigEntry | None: for entry in hass.config_entries.async_entries(domain=DOMAIN): if TEST_SYSINFO_ID == entry.unique_id: @@ -82,14 +82,14 @@ def _get_config_entry_from_unique_id( return None -async def test_setup_config_entry(hass: HomeAssistantType) -> None: +async def test_setup_config_entry(hass: HomeAssistant) -> None: """Test setting up the component via config entries.""" await setup_test_config_entry(hass, hyperion_client=create_mock_client()) assert hass.states.get(TEST_ENTITY_ID_1) is not None async def test_setup_config_entry_not_ready_connect_fail( - hass: HomeAssistantType, + hass: HomeAssistant, ) -> None: """Test the component not being ready.""" client = create_mock_client() @@ -99,7 +99,7 @@ async def test_setup_config_entry_not_ready_connect_fail( async def test_setup_config_entry_not_ready_switch_instance_fail( - hass: HomeAssistantType, + hass: HomeAssistant, ) -> None: """Test the component not being ready.""" client = create_mock_client() @@ -110,7 +110,7 @@ async def test_setup_config_entry_not_ready_switch_instance_fail( async def test_setup_config_entry_not_ready_load_state_fail( - hass: HomeAssistantType, + hass: HomeAssistant, ) -> None: """Test the component not being ready.""" client = create_mock_client() @@ -126,7 +126,7 @@ async def test_setup_config_entry_not_ready_load_state_fail( assert hass.states.get(TEST_ENTITY_ID_1) is None -async def test_setup_config_entry_dynamic_instances(hass: HomeAssistantType) -> None: +async def test_setup_config_entry_dynamic_instances(hass: HomeAssistant) -> None: """Test dynamic changes in the instance configuration.""" registry = er.async_get(hass) @@ -241,7 +241,7 @@ async def test_setup_config_entry_dynamic_instances(hass: HomeAssistantType) -> assert hass.states.get(TEST_ENTITY_ID_3) is not None -async def test_light_basic_properies(hass: HomeAssistantType) -> None: +async def test_light_basic_properies(hass: HomeAssistant) -> None: """Test the basic properties.""" client = create_mock_client() await setup_test_config_entry(hass, hyperion_client=client) @@ -262,7 +262,7 @@ async def test_light_basic_properies(hass: HomeAssistantType) -> None: ) -async def test_light_async_turn_on(hass: HomeAssistantType) -> None: +async def test_light_async_turn_on(hass: HomeAssistant) -> None: """Test turning the light on.""" client = create_mock_client() await setup_test_config_entry(hass, hyperion_client=client) @@ -507,7 +507,7 @@ async def test_light_async_turn_on(hass: HomeAssistantType) -> None: async def test_light_async_turn_on_fail_async_send_set_component( - hass: HomeAssistantType, + hass: HomeAssistant, ) -> None: """Test set_component failure when turning the light on.""" client = create_mock_client() @@ -523,7 +523,7 @@ async def test_light_async_turn_on_fail_async_send_set_component( async def test_light_async_turn_on_fail_async_send_set_component_source( - hass: HomeAssistantType, + hass: HomeAssistant, ) -> None: """Test async_send_set_component failure when selecting the source.""" client = create_mock_client() @@ -546,7 +546,7 @@ async def test_light_async_turn_on_fail_async_send_set_component_source( async def test_light_async_turn_on_fail_async_send_clear_source( - hass: HomeAssistantType, + hass: HomeAssistant, ) -> None: """Test async_send_clear failure when turning the light on.""" client = create_mock_client() @@ -566,7 +566,7 @@ async def test_light_async_turn_on_fail_async_send_clear_source( async def test_light_async_turn_on_fail_async_send_clear_effect( - hass: HomeAssistantType, + hass: HomeAssistant, ) -> None: """Test async_send_clear failure when turning on an effect.""" client = create_mock_client() @@ -583,7 +583,7 @@ async def test_light_async_turn_on_fail_async_send_clear_effect( async def test_light_async_turn_on_fail_async_send_set_effect( - hass: HomeAssistantType, + hass: HomeAssistant, ) -> None: """Test async_send_set_effect failure when turning on the light.""" client = create_mock_client() @@ -603,7 +603,7 @@ async def test_light_async_turn_on_fail_async_send_set_effect( async def test_light_async_turn_on_fail_async_send_set_color( - hass: HomeAssistantType, + hass: HomeAssistant, ) -> None: """Test async_send_set_color failure when turning on the light.""" client = create_mock_client() @@ -623,7 +623,7 @@ async def test_light_async_turn_on_fail_async_send_set_color( async def test_light_async_turn_off_fail_async_send_set_component( - hass: HomeAssistantType, + hass: HomeAssistant, ) -> None: """Test async_send_set_component failure when turning off the light.""" client = create_mock_client() @@ -642,7 +642,7 @@ async def test_light_async_turn_off_fail_async_send_set_component( async def test_priority_light_async_turn_off_fail_async_send_clear( - hass: HomeAssistantType, + hass: HomeAssistant, ) -> None: """Test async_send_clear failure when turning off a priority light.""" client = create_mock_client() @@ -662,7 +662,7 @@ async def test_priority_light_async_turn_off_fail_async_send_clear( assert client.method_calls[-1] == call.async_send_clear(priority=180) -async def test_light_async_turn_off(hass: HomeAssistantType) -> None: +async def test_light_async_turn_off(hass: HomeAssistant) -> None: """Test turning the light off.""" client = create_mock_client() await setup_test_config_entry(hass, hyperion_client=client) @@ -705,7 +705,7 @@ async def test_light_async_turn_off(hass: HomeAssistantType) -> None: async def test_light_async_updates_from_hyperion_client( - hass: HomeAssistantType, + hass: HomeAssistant, ) -> None: """Test receiving a variety of Hyperion client callbacks.""" client = create_mock_client() @@ -825,7 +825,7 @@ async def test_light_async_updates_from_hyperion_client( assert entity_state.state == "on" -async def test_full_state_loaded_on_start(hass: HomeAssistantType) -> None: +async def test_full_state_loaded_on_start(hass: HomeAssistant) -> None: """Test receiving a variety of Hyperion client callbacks.""" client = create_mock_client() @@ -848,7 +848,7 @@ async def test_full_state_loaded_on_start(hass: HomeAssistantType) -> None: assert entity_state.attributes["hs_color"] == (180.0, 100.0) -async def test_unload_entry(hass: HomeAssistantType) -> None: +async def test_unload_entry(hass: HomeAssistant) -> None: """Test unload.""" client = create_mock_client() await setup_test_config_entry(hass, hyperion_client=client) @@ -862,7 +862,7 @@ async def test_unload_entry(hass: HomeAssistantType) -> None: assert client.async_client_disconnect.call_count == 2 -async def test_version_log_warning(caplog, hass: HomeAssistantType) -> None: # type: ignore[no-untyped-def] +async def test_version_log_warning(caplog, hass: HomeAssistant) -> None: # type: ignore[no-untyped-def] """Test warning on old version.""" client = create_mock_client() client.async_sysinfo_version = AsyncMock(return_value="2.0.0-alpha.7") @@ -871,7 +871,7 @@ async def test_version_log_warning(caplog, hass: HomeAssistantType) -> None: # assert "Please consider upgrading" in caplog.text -async def test_version_no_log_warning(caplog, hass: HomeAssistantType) -> None: # type: ignore[no-untyped-def] +async def test_version_no_log_warning(caplog, hass: HomeAssistant) -> None: # type: ignore[no-untyped-def] """Test no warning on acceptable version.""" client = create_mock_client() client.async_sysinfo_version = AsyncMock(return_value="2.0.0-alpha.9") @@ -880,7 +880,7 @@ async def test_version_no_log_warning(caplog, hass: HomeAssistantType) -> None: assert "Please consider upgrading" not in caplog.text -async def test_setup_entry_no_token_reauth(hass: HomeAssistantType) -> None: +async def test_setup_entry_no_token_reauth(hass: HomeAssistant) -> None: """Verify a reauth flow when auth is required but no token provided.""" client = create_mock_client() config_entry = add_test_config_entry(hass) @@ -903,7 +903,7 @@ async def test_setup_entry_no_token_reauth(hass: HomeAssistantType) -> None: assert config_entry.state == ENTRY_STATE_SETUP_ERROR -async def test_setup_entry_bad_token_reauth(hass: HomeAssistantType) -> None: +async def test_setup_entry_bad_token_reauth(hass: HomeAssistant) -> None: """Verify a reauth flow when a bad token is provided.""" client = create_mock_client() config_entry = add_test_config_entry( @@ -932,7 +932,7 @@ async def test_setup_entry_bad_token_reauth(hass: HomeAssistantType) -> None: async def test_priority_light_async_updates( - hass: HomeAssistantType, + hass: HomeAssistant, ) -> None: """Test receiving a variety of Hyperion client callbacks to a HyperionPriorityLight.""" priority_template = { @@ -1094,7 +1094,7 @@ async def test_priority_light_async_updates( async def test_priority_light_async_updates_off_sets_black( - hass: HomeAssistantType, + hass: HomeAssistant, ) -> None: """Test turning the HyperionPriorityLight off.""" client = create_mock_client() @@ -1142,7 +1142,7 @@ async def test_priority_light_async_updates_off_sets_black( async def test_priority_light_prior_color_preserved_after_black( - hass: HomeAssistantType, + hass: HomeAssistant, ) -> None: """Test that color is preserved in an on->off->on cycle for a HyperionPriorityLight. @@ -1265,7 +1265,7 @@ async def test_priority_light_prior_color_preserved_after_black( assert entity_state.attributes["hs_color"] == hs_color -async def test_priority_light_has_no_external_sources(hass: HomeAssistantType) -> None: +async def test_priority_light_has_no_external_sources(hass: HomeAssistant) -> None: """Ensure a HyperionPriorityLight does not list external sources.""" client = create_mock_client() client.priorities = [] @@ -1283,7 +1283,7 @@ async def test_priority_light_has_no_external_sources(hass: HomeAssistantType) - assert entity_state.attributes["effect_list"] == [hyperion_light.KEY_EFFECT_SOLID] -async def test_light_option_effect_hide_list(hass: HomeAssistantType) -> None: +async def test_light_option_effect_hide_list(hass: HomeAssistant) -> None: """Test the effect_hide_list option.""" client = create_mock_client() client.effects = [{const.KEY_NAME: "One"}, {const.KEY_NAME: "Two"}] @@ -1304,7 +1304,7 @@ async def test_light_option_effect_hide_list(hass: HomeAssistantType) -> None: ] -async def test_device_info(hass: HomeAssistantType) -> None: +async def test_device_info(hass: HomeAssistant) -> None: """Verify device information includes expected details.""" client = create_mock_client() @@ -1336,7 +1336,7 @@ async def test_device_info(hass: HomeAssistantType) -> None: assert TEST_ENTITY_ID_1 in entities_from_device -async def test_lights_can_be_enabled(hass: HomeAssistantType) -> None: +async def test_lights_can_be_enabled(hass: HomeAssistant) -> None: """Verify lights can be enabled.""" client = create_mock_client() await setup_test_config_entry(hass, hyperion_client=client) @@ -1369,7 +1369,7 @@ async def test_lights_can_be_enabled(hass: HomeAssistantType) -> None: assert entity_state -async def test_deprecated_effect_names(caplog, hass: HomeAssistantType) -> None: # type: ignore[no-untyped-def] +async def test_deprecated_effect_names(caplog, hass: HomeAssistant) -> None: # type: ignore[no-untyped-def] """Test deprecated effects function and issue a warning.""" client = create_mock_client() client.async_send_clear = AsyncMock(return_value=True) @@ -1401,7 +1401,7 @@ async def test_deprecated_effect_names(caplog, hass: HomeAssistantType) -> None: async def test_deprecated_effect_names_not_in_effect_list( - hass: HomeAssistantType, + hass: HomeAssistant, ) -> None: """Test deprecated effects are not in shown effect list.""" await setup_test_config_entry(hass) diff --git a/tests/components/hyperion/test_switch.py b/tests/components/hyperion/test_switch.py index 764f234eb0ec1..2367ad9613385 100644 --- a/tests/components/hyperion/test_switch.py +++ b/tests/components/hyperion/test_switch.py @@ -20,8 +20,8 @@ from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON +from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import dt, slugify from . import ( @@ -52,7 +52,7 @@ TEST_SWITCH_COMPONENT_ALL_ENTITY_ID = f"{TEST_SWITCH_COMPONENT_BASE_ENTITY_ID}_all" -async def test_switch_turn_on_off(hass: HomeAssistantType) -> None: +async def test_switch_turn_on_off(hass: HomeAssistant) -> None: """Test turning the light on.""" client = create_mock_client() client.async_send_set_component = AsyncMock(return_value=True) @@ -121,7 +121,7 @@ async def test_switch_turn_on_off(hass: HomeAssistantType) -> None: assert entity_state.state == "on" -async def test_switch_has_correct_entities(hass: HomeAssistantType) -> None: +async def test_switch_has_correct_entities(hass: HomeAssistant) -> None: """Test that the correct switch entities are created.""" client = create_mock_client() client.components = TEST_COMPONENTS @@ -144,7 +144,7 @@ async def test_switch_has_correct_entities(hass: HomeAssistantType) -> None: assert entity_state, f"Couldn't find entity: {entity_id}" -async def test_device_info(hass: HomeAssistantType) -> None: +async def test_device_info(hass: HomeAssistant) -> None: """Verify device information includes expected details.""" client = create_mock_client() client.components = TEST_COMPONENTS @@ -184,7 +184,7 @@ async def test_device_info(hass: HomeAssistantType) -> None: assert entity_id in entities_from_device -async def test_switches_can_be_enabled(hass: HomeAssistantType) -> None: +async def test_switches_can_be_enabled(hass: HomeAssistant) -> None: """Verify switches can be enabled.""" client = create_mock_client() client.components = TEST_COMPONENTS diff --git a/tests/components/icloud/test_config_flow.py b/tests/components/icloud/test_config_flow.py index 998a69c575a29..59c5ebf24a941 100644 --- a/tests/components/icloud/test_config_flow.py +++ b/tests/components/icloud/test_config_flow.py @@ -20,7 +20,7 @@ ) from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -157,7 +157,7 @@ def mock_controller_service_validate_verification_code_failed(): yield service_mock -async def test_user(hass: HomeAssistantType, service: MagicMock): +async def test_user(hass: HomeAssistant, service: MagicMock): """Test user config.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=None @@ -175,9 +175,7 @@ async def test_user(hass: HomeAssistantType, service: MagicMock): assert result["step_id"] == CONF_TRUSTED_DEVICE -async def test_user_with_cookie( - hass: HomeAssistantType, service_authenticated: MagicMock -): +async def test_user_with_cookie(hass: HomeAssistant, service_authenticated: MagicMock): """Test user config with presence of a cookie.""" # test with all provided result = await hass.config_entries.flow.async_init( @@ -199,7 +197,7 @@ async def test_user_with_cookie( assert result["data"][CONF_GPS_ACCURACY_THRESHOLD] == DEFAULT_GPS_ACCURACY_THRESHOLD -async def test_import(hass: HomeAssistantType, service: MagicMock): +async def test_import(hass: HomeAssistant, service: MagicMock): """Test import step.""" # import with required result = await hass.config_entries.flow.async_init( @@ -227,7 +225,7 @@ async def test_import(hass: HomeAssistantType, service: MagicMock): async def test_import_with_cookie( - hass: HomeAssistantType, service_authenticated: MagicMock + hass: HomeAssistant, service_authenticated: MagicMock ): """Test import step with presence of a cookie.""" # import with required @@ -268,7 +266,7 @@ async def test_import_with_cookie( async def test_two_accounts_setup( - hass: HomeAssistantType, service_authenticated: MagicMock + hass: HomeAssistant, service_authenticated: MagicMock ): """Test to setup two accounts.""" MockConfigEntry( @@ -293,7 +291,7 @@ async def test_two_accounts_setup( assert result["data"][CONF_GPS_ACCURACY_THRESHOLD] == DEFAULT_GPS_ACCURACY_THRESHOLD -async def test_already_setup(hass: HomeAssistantType): +async def test_already_setup(hass: HomeAssistant): """Test we abort if the account is already setup.""" MockConfigEntry( domain=DOMAIN, @@ -320,7 +318,7 @@ async def test_already_setup(hass: HomeAssistantType): assert result["reason"] == "already_configured" -async def test_login_failed(hass: HomeAssistantType): +async def test_login_failed(hass: HomeAssistant): """Test when we have errors during login.""" with patch( "homeassistant.components.icloud.config_flow.PyiCloudService.authenticate", @@ -336,7 +334,7 @@ async def test_login_failed(hass: HomeAssistantType): async def test_no_device( - hass: HomeAssistantType, service_authenticated_no_device: MagicMock + hass: HomeAssistant, service_authenticated_no_device: MagicMock ): """Test when we have no devices.""" result = await hass.config_entries.flow.async_init( @@ -348,7 +346,7 @@ async def test_no_device( assert result["reason"] == "no_device" -async def test_trusted_device(hass: HomeAssistantType, service: MagicMock): +async def test_trusted_device(hass: HomeAssistant, service: MagicMock): """Test trusted_device step.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -361,7 +359,7 @@ async def test_trusted_device(hass: HomeAssistantType, service: MagicMock): assert result["step_id"] == CONF_TRUSTED_DEVICE -async def test_trusted_device_success(hass: HomeAssistantType, service: MagicMock): +async def test_trusted_device_success(hass: HomeAssistant, service: MagicMock): """Test trusted_device step success.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -377,7 +375,7 @@ async def test_trusted_device_success(hass: HomeAssistantType, service: MagicMoc async def test_send_verification_code_failed( - hass: HomeAssistantType, service_send_verification_code_failed: MagicMock + hass: HomeAssistant, service_send_verification_code_failed: MagicMock ): """Test when we have errors during send_verification_code.""" result = await hass.config_entries.flow.async_init( @@ -394,7 +392,7 @@ async def test_send_verification_code_failed( assert result["errors"] == {CONF_TRUSTED_DEVICE: "send_verification_code"} -async def test_verification_code(hass: HomeAssistantType, service: MagicMock): +async def test_verification_code(hass: HomeAssistant, service: MagicMock): """Test verification_code step.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -410,7 +408,7 @@ async def test_verification_code(hass: HomeAssistantType, service: MagicMock): assert result["step_id"] == CONF_VERIFICATION_CODE -async def test_verification_code_success(hass: HomeAssistantType, service: MagicMock): +async def test_verification_code_success(hass: HomeAssistant, service: MagicMock): """Test verification_code step success.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -436,7 +434,7 @@ async def test_verification_code_success(hass: HomeAssistantType, service: Magic async def test_validate_verification_code_failed( - hass: HomeAssistantType, service_validate_verification_code_failed: MagicMock + hass: HomeAssistant, service_validate_verification_code_failed: MagicMock ): """Test when we have errors during validate_verification_code.""" result = await hass.config_entries.flow.async_init( @@ -456,7 +454,7 @@ async def test_validate_verification_code_failed( assert result["errors"] == {"base": "validate_verification_code"} -async def test_2fa_code_success(hass: HomeAssistantType, service_2fa: MagicMock): +async def test_2fa_code_success(hass: HomeAssistant, service_2fa: MagicMock): """Test 2fa step success.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -481,7 +479,7 @@ async def test_2fa_code_success(hass: HomeAssistantType, service_2fa: MagicMock) async def test_validate_2fa_code_failed( - hass: HomeAssistantType, service_validate_2fa_code_failed: MagicMock + hass: HomeAssistant, service_validate_2fa_code_failed: MagicMock ): """Test when we have errors during validate_verification_code.""" result = await hass.config_entries.flow.async_init( @@ -499,9 +497,7 @@ async def test_validate_2fa_code_failed( assert result["errors"] == {"base": "validate_verification_code"} -async def test_password_update( - hass: HomeAssistantType, service_authenticated: MagicMock -): +async def test_password_update(hass: HomeAssistant, service_authenticated: MagicMock): """Test that password reauthentication works successfully.""" config_entry = MockConfigEntry( domain=DOMAIN, data=MOCK_CONFIG, entry_id="test", unique_id=USERNAME @@ -525,7 +521,7 @@ async def test_password_update( assert config_entry.data[CONF_PASSWORD] == PASSWORD_2 -async def test_password_update_wrong_password(hass: HomeAssistantType): +async def test_password_update_wrong_password(hass: HomeAssistant): """Test that during password reauthentication wrong password returns correct error.""" config_entry = MockConfigEntry( domain=DOMAIN, data=MOCK_CONFIG, entry_id="test", unique_id=USERNAME From 968460099a3d7654512bd3f67cee1b16e0b36536 Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Fri, 23 Apr 2021 11:19:43 +0300 Subject: [PATCH 0475/1317] Change Jewish calendar IOT class to calculated (#49571) This integration doesn't poll at all, rather all values are calculated based on location and date, so I think this is the more correct value here --- homeassistant/components/jewish_calendar/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/jewish_calendar/manifest.json b/homeassistant/components/jewish_calendar/manifest.json index 9bec8fce5b061..ec29a3e5d9986 100644 --- a/homeassistant/components/jewish_calendar/manifest.json +++ b/homeassistant/components/jewish_calendar/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/jewish_calendar", "requirements": ["hdate==0.10.2"], "codeowners": ["@tsvi"], - "iot_class": "local_polling" + "iot_class": "calculated" } From d168749a51540b6225569ebeb603ba286bdf7559 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 23 Apr 2021 10:34:02 +0200 Subject: [PATCH 0476/1317] Integrations: HomeAssistantType --> HomeAssistant. Last batch. (#49591) --- .../components/garmin_connect/sensor.py | 4 +-- .../components/geniushub/__init__.py | 8 +++--- .../components/geniushub/binary_sensor.py | 5 ++-- homeassistant/components/geniushub/climate.py | 5 ++-- homeassistant/components/geniushub/sensor.py | 5 ++-- homeassistant/components/geniushub/switch.py | 5 ++-- .../components/geniushub/water_heater.py | 5 ++-- .../components/gpslogger/device_tracker.py | 5 ++-- homeassistant/components/gtfs/sensor.py | 9 +++---- homeassistant/components/hassio/__init__.py | 27 ++++++++----------- .../components/hassio/addon_panel.py | 4 +-- homeassistant/components/hassio/auth.py | 7 +++-- homeassistant/components/hassio/ingress.py | 5 ++-- 13 files changed, 44 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/garmin_connect/sensor.py b/homeassistant/components/garmin_connect/sensor.py index 46db6e615f1c4..5cabb96c8e9b2 100644 --- a/homeassistant/components/garmin_connect/sensor.py +++ b/homeassistant/components/garmin_connect/sensor.py @@ -13,7 +13,7 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ATTRIBUTION, CONF_ID -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .alarm_util import calculate_next_active_alarms from .const import ATTRIBUTION, DOMAIN, GARMIN_ENTITY_LIST @@ -22,7 +22,7 @@ async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up Garmin Connect sensor based on a config entry.""" garmin_data = hass.data[DOMAIN][entry.entry_id] diff --git a/homeassistant/components/geniushub/__init__.py b/homeassistant/components/geniushub/__init__.py index f1d2a1d47c1bd..bf5fc03ded5fc 100644 --- a/homeassistant/components/geniushub/__init__.py +++ b/homeassistant/components/geniushub/__init__.py @@ -19,7 +19,7 @@ CONF_USERNAME, TEMP_CELSIUS, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.discovery import async_load_platform @@ -30,7 +30,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.service import verify_domain_control -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -96,7 +96,7 @@ ) -async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Create a Genius Hub system.""" hass.data[DOMAIN] = {} @@ -129,7 +129,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: @callback -def setup_service_functions(hass: HomeAssistantType, broker): +def setup_service_functions(hass: HomeAssistant, broker): """Set up the service functions.""" @verify_domain_control(hass, DOMAIN) diff --git a/homeassistant/components/geniushub/binary_sensor.py b/homeassistant/components/geniushub/binary_sensor.py index d935192f97d4a..dd39189bd389c 100644 --- a/homeassistant/components/geniushub/binary_sensor.py +++ b/homeassistant/components/geniushub/binary_sensor.py @@ -1,6 +1,7 @@ """Support for Genius Hub binary_sensor devices.""" from homeassistant.components.binary_sensor import BinarySensorEntity -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType from . import DOMAIN, GeniusDevice @@ -8,7 +9,7 @@ async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None ) -> None: """Set up the Genius Hub sensor entities.""" if discovery_info is None: diff --git a/homeassistant/components/geniushub/climate.py b/homeassistant/components/geniushub/climate.py index 089fd96483555..b60132b9e4c64 100644 --- a/homeassistant/components/geniushub/climate.py +++ b/homeassistant/components/geniushub/climate.py @@ -13,7 +13,8 @@ SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, ) -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType from . import DOMAIN, GeniusHeatingZone @@ -28,7 +29,7 @@ async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None ) -> None: """Set up the Genius Hub climate entities.""" if discovery_info is None: diff --git a/homeassistant/components/geniushub/sensor.py b/homeassistant/components/geniushub/sensor.py index 3234ccd577ff8..0c96ec595b607 100644 --- a/homeassistant/components/geniushub/sensor.py +++ b/homeassistant/components/geniushub/sensor.py @@ -6,7 +6,8 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util from . import DOMAIN, GeniusDevice, GeniusEntity @@ -21,7 +22,7 @@ async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None ) -> None: """Set up the Genius Hub sensor entities.""" if discovery_info is None: diff --git a/homeassistant/components/geniushub/switch.py b/homeassistant/components/geniushub/switch.py index cb45911d25019..faff6b8e2f9e0 100644 --- a/homeassistant/components/geniushub/switch.py +++ b/homeassistant/components/geniushub/switch.py @@ -5,8 +5,9 @@ from homeassistant.components.switch import DEVICE_CLASS_OUTLET, SwitchEntity from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from . import ATTR_DURATION, DOMAIN, GeniusZone @@ -26,7 +27,7 @@ async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None ) -> None: """Set up the Genius Hub switch entities.""" if discovery_info is None: diff --git a/homeassistant/components/geniushub/water_heater.py b/homeassistant/components/geniushub/water_heater.py index bb775432d8e15..8dcbce7c1bdaf 100644 --- a/homeassistant/components/geniushub/water_heater.py +++ b/homeassistant/components/geniushub/water_heater.py @@ -7,7 +7,8 @@ WaterHeaterEntity, ) from homeassistant.const import STATE_OFF -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType from . import DOMAIN, GeniusHeatingZone @@ -32,7 +33,7 @@ async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None ) -> None: """Set up the Genius Hub water_heater entities.""" if discovery_info is None: diff --git a/homeassistant/components/gpslogger/device_tracker.py b/homeassistant/components/gpslogger/device_tracker.py index 25701e8c2e79a..5bce10ab088f6 100644 --- a/homeassistant/components/gpslogger/device_tracker.py +++ b/homeassistant/components/gpslogger/device_tracker.py @@ -7,11 +7,10 @@ ATTR_LATITUDE, ATTR_LONGITUDE, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.typing import HomeAssistantType from . import DOMAIN as GPL_DOMAIN, TRACKER_UPDATE from .const import ( @@ -23,7 +22,7 @@ ) -async def async_setup_entry(hass: HomeAssistantType, entry, async_add_entities): +async def async_setup_entry(hass: HomeAssistant, entry, async_add_entities): """Configure a dispatcher connection based on a config entry.""" @callback diff --git a/homeassistant/components/gtfs/sensor.py b/homeassistant/components/gtfs/sensor.py index 61f0bb7d9c11c..d71a2fab67d15 100644 --- a/homeassistant/components/gtfs/sensor.py +++ b/homeassistant/components/gtfs/sensor.py @@ -19,12 +19,9 @@ DEVICE_CLASS_TIMESTAMP, STATE_UNKNOWN, ) +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import ( - ConfigType, - DiscoveryInfoType, - HomeAssistantType, -) +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import slugify import homeassistant.util.dt as dt_util @@ -482,7 +479,7 @@ def get_next_departure( def setup_platform( - hass: HomeAssistantType, + hass: HomeAssistant, config: ConfigType, add_entities: Callable[[list], None], discovery_info: DiscoveryInfoType | None = None, diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 6dd2a067c8906..4889c7c137aea 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -24,7 +24,6 @@ from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import DeviceRegistry, async_get_registry -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.loader import bind_hass from homeassistant.util.dt import utcnow @@ -152,7 +151,7 @@ @bind_hass -async def async_get_addon_info(hass: HomeAssistantType, slug: str) -> dict: +async def async_get_addon_info(hass: HomeAssistant, slug: str) -> dict: """Return add-on info. The caller of the function should handle HassioAPIError. @@ -162,7 +161,7 @@ async def async_get_addon_info(hass: HomeAssistantType, slug: str) -> dict: @bind_hass -async def async_update_diagnostics(hass: HomeAssistantType, diagnostics: bool) -> dict: +async def async_update_diagnostics(hass: HomeAssistant, diagnostics: bool) -> dict: """Update Supervisor diagnostics toggle. The caller of the function should handle HassioAPIError. @@ -173,7 +172,7 @@ async def async_update_diagnostics(hass: HomeAssistantType, diagnostics: bool) - @bind_hass @api_data -async def async_install_addon(hass: HomeAssistantType, slug: str) -> dict: +async def async_install_addon(hass: HomeAssistant, slug: str) -> dict: """Install add-on. The caller of the function should handle HassioAPIError. @@ -185,7 +184,7 @@ async def async_install_addon(hass: HomeAssistantType, slug: str) -> dict: @bind_hass @api_data -async def async_uninstall_addon(hass: HomeAssistantType, slug: str) -> dict: +async def async_uninstall_addon(hass: HomeAssistant, slug: str) -> dict: """Uninstall add-on. The caller of the function should handle HassioAPIError. @@ -197,7 +196,7 @@ async def async_uninstall_addon(hass: HomeAssistantType, slug: str) -> dict: @bind_hass @api_data -async def async_update_addon(hass: HomeAssistantType, slug: str) -> dict: +async def async_update_addon(hass: HomeAssistant, slug: str) -> dict: """Update add-on. The caller of the function should handle HassioAPIError. @@ -209,7 +208,7 @@ async def async_update_addon(hass: HomeAssistantType, slug: str) -> dict: @bind_hass @api_data -async def async_start_addon(hass: HomeAssistantType, slug: str) -> dict: +async def async_start_addon(hass: HomeAssistant, slug: str) -> dict: """Start add-on. The caller of the function should handle HassioAPIError. @@ -221,7 +220,7 @@ async def async_start_addon(hass: HomeAssistantType, slug: str) -> dict: @bind_hass @api_data -async def async_stop_addon(hass: HomeAssistantType, slug: str) -> dict: +async def async_stop_addon(hass: HomeAssistant, slug: str) -> dict: """Stop add-on. The caller of the function should handle HassioAPIError. @@ -234,7 +233,7 @@ async def async_stop_addon(hass: HomeAssistantType, slug: str) -> dict: @bind_hass @api_data async def async_set_addon_options( - hass: HomeAssistantType, slug: str, options: dict + hass: HomeAssistant, slug: str, options: dict ) -> dict: """Set add-on options. @@ -246,9 +245,7 @@ async def async_set_addon_options( @bind_hass -async def async_get_addon_discovery_info( - hass: HomeAssistantType, slug: str -) -> dict | None: +async def async_get_addon_discovery_info(hass: HomeAssistant, slug: str) -> dict | None: """Return discovery data for an add-on.""" hassio = hass.data[DOMAIN] data = await hassio.retrieve_discovery_messages() @@ -259,7 +256,7 @@ async def async_get_addon_discovery_info( @bind_hass @api_data async def async_create_snapshot( - hass: HomeAssistantType, payload: dict, partial: bool = False + hass: HomeAssistant, payload: dict, partial: bool = False ) -> dict: """Create a full or partial snapshot. @@ -536,9 +533,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b return True -async def async_unload_entry( - hass: HomeAssistantType, config_entry: ConfigEntry -) -> bool: +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = all( await asyncio.gather( diff --git a/homeassistant/components/hassio/addon_panel.py b/homeassistant/components/hassio/addon_panel.py index a48c8b4d05b14..d540479d7792a 100644 --- a/homeassistant/components/hassio/addon_panel.py +++ b/homeassistant/components/hassio/addon_panel.py @@ -6,7 +6,7 @@ from homeassistant.components.http import HomeAssistantView from homeassistant.const import ATTR_ICON, HTTP_BAD_REQUEST -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .const import ATTR_ADMIN, ATTR_ENABLE, ATTR_PANELS, ATTR_TITLE from .handler import HassioAPIError @@ -14,7 +14,7 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup_addon_panel(hass: HomeAssistantType, hassio): +async def async_setup_addon_panel(hass: HomeAssistant, hassio): """Add-on Ingress Panel setup.""" hassio_addon_panel = HassIOAddonPanel(hass, hassio) hass.http.register_view(hassio_addon_panel) diff --git a/homeassistant/components/hassio/auth.py b/homeassistant/components/hassio/auth.py index a1c032fe0fe91..6c9b36fb3a071 100644 --- a/homeassistant/components/hassio/auth.py +++ b/homeassistant/components/hassio/auth.py @@ -13,9 +13,8 @@ from homeassistant.components.http.const import KEY_HASS_USER from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.const import HTTP_OK -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import HomeAssistantType from .const import ATTR_ADDON, ATTR_PASSWORD, ATTR_USERNAME @@ -23,7 +22,7 @@ @callback -def async_setup_auth_view(hass: HomeAssistantType, user: User): +def async_setup_auth_view(hass: HomeAssistant, user: User): """Auth setup.""" hassio_auth = HassIOAuth(hass, user) hassio_password_reset = HassIOPasswordReset(hass, user) @@ -35,7 +34,7 @@ def async_setup_auth_view(hass: HomeAssistantType, user: User): class HassIOBaseAuth(HomeAssistantView): """Hass.io view to handle auth requests.""" - def __init__(self, hass: HomeAssistantType, user: User): + def __init__(self, hass: HomeAssistant, user: User): """Initialize WebView.""" self.hass = hass self.user = user diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index 1f0a49ae497ee..7519c8603988d 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -12,8 +12,7 @@ from multidict import CIMultiDict from homeassistant.components.http import HomeAssistantView -from homeassistant.core import callback -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant, callback from .const import X_HASSIO, X_INGRESS_PATH @@ -21,7 +20,7 @@ @callback -def async_setup_ingress_view(hass: HomeAssistantType, host: str): +def async_setup_ingress_view(hass: HomeAssistant, host: str): """Auth setup.""" websession = hass.helpers.aiohttp_client.async_get_clientsession() From 7579a321df3a0154cfb45cdef06440fbaf2d7c57 Mon Sep 17 00:00:00 2001 From: Xuefer Date: Fri, 23 Apr 2021 16:43:02 +0800 Subject: [PATCH 0477/1317] Encode ONVIF username password in URL (#49512) * onvif: encode username password in url Signed-off-by: Xuefer * onvif: use yarl to set username password for steam url Signed-off-by: Xuefer --- homeassistant/components/onvif/camera.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index 50390464df885..91f4c76abac94 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -5,6 +5,7 @@ from haffmpeg.tools import IMAGE_JPEG, ImageFrame from onvif.exceptions import ONVIFError import voluptuous as vol +from yarl import URL from homeassistant.components.camera import SUPPORT_STREAM, Camera from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS, DATA_FFMPEG @@ -175,9 +176,10 @@ async def handle_async_mjpeg_stream(self, request): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" uri_no_auth = await self.device.async_get_stream_uri(self.profile) - self._stream_uri = uri_no_auth.replace( - "rtsp://", f"rtsp://{self.device.username}:{self.device.password}@", 1 - ) + url = URL(uri_no_auth) + url = url.with_user(self.device.username) + url = url.with_password(self.device.password) + self._stream_uri = str(url) async def async_perform_ptz( self, From 39cb22374d20ec16e163bab07ce194b6a36c34bd Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 23 Apr 2021 11:08:58 +0200 Subject: [PATCH 0478/1317] Remove HomeAssistantType from typing.py as it is no longer used. (#49593) --- homeassistant/helpers/typing.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/helpers/typing.py b/homeassistant/helpers/typing.py index 279bc0f686f82..54e63ab49efc3 100644 --- a/homeassistant/helpers/typing.py +++ b/homeassistant/helpers/typing.py @@ -9,7 +9,6 @@ ContextType = homeassistant.core.Context DiscoveryInfoType = Dict[str, Any] EventType = homeassistant.core.Event -HomeAssistantType = homeassistant.core.HomeAssistant ServiceCallType = homeassistant.core.ServiceCall ServiceDataType = Dict[str, Any] StateType = Union[None, str, int, float] From 28a909c46339dfc3f7b4c4a21ce343aa582a81b4 Mon Sep 17 00:00:00 2001 From: mariwing Date: Fri, 23 Apr 2021 11:16:24 +0200 Subject: [PATCH 0479/1317] Requesting data from last seven days (#49485) --- homeassistant/components/withings/common.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/withings/common.py b/homeassistant/components/withings/common.py index c0d9bcb2599f0..c2a91275d7218 100644 --- a/homeassistant/components/withings/common.py +++ b/homeassistant/components/withings/common.py @@ -774,8 +774,12 @@ async def _async_get_all_data(self) -> dict[MeasureType, Any] | None: async def async_get_measures(self) -> dict[MeasureType, Any]: """Get the measures data.""" _LOGGER.debug("Updating withings measures") + now = dt.utcnow() + startdate = now - datetime.timedelta(days=7) - response = await self._hass.async_add_executor_job(self._api.measure_get_meas) + response = await self._hass.async_add_executor_job( + self._api.measure_get_meas, None, None, startdate, now, None, startdate + ) # Sort from oldest to newest. groups = sorted( From 50d2c3bfe34760db7aaf4f64c57fb254e6299539 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Fri, 23 Apr 2021 05:25:53 -0400 Subject: [PATCH 0480/1317] Add target and selectors to remote services (#49384) --- homeassistant/components/remote/services.yaml | 93 ++++++++++++++----- 1 file changed, 70 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/remote/services.yaml b/homeassistant/components/remote/services.yaml index 3868479efc65b..13459d452bf6b 100644 --- a/homeassistant/components/remote/services.yaml +++ b/homeassistant/components/remote/services.yaml @@ -1,82 +1,129 @@ # Describes the format for available remote services turn_on: + name: Turn On description: Sends the Power On Command. + target: fields: - entity_id: - description: Name(s) of entities to turn on. - example: "remote.family_room" activity: description: Activity ID or Activity Name to start. example: "BedroomTV" + selector: + text: toggle: + name: Toggle description: Toggles a device. - fields: - entity_id: - description: Name(s) of entities to toggle. - example: "remote.family_room" + target: turn_off: + name: Turn Off description: Sends the Power Off Command. - fields: - entity_id: - description: Name(s) of entities to turn off. - example: "remote.family_room" + target: send_command: + name: Send Command description: Sends a command or a list of commands to a device. + target: fields: - entity_id: - description: Name(s) of entities to send command from. - example: "remote.family_room" device: + name: Device description: Device ID to send command to. example: "32756745" command: + name: Command description: A single command or a list of commands to send. + required: true example: "Play" + selector: + text: num_repeats: - description: An optional value that specifies the number of times you want to repeat the command(s). If not specified, the command(s) will not be repeated. + name: Repeats + description: An optional value that specifies the number of times you want to repeat the command(s). example: "5" + default: 1 + selector: + number: + min: 0 + max: 255 + step: 1 + mode: slider delay_secs: - description: An optional value that specifies that number of seconds you want to wait in between repeated commands. If not specified, the default of 0.4 seconds will be used. + name: Delay Seconds + description: Specify the number of seconds you want to wait in between repeated commands. example: "0.75" + default: 0.4 + selector: + number: + min: 0 + max: 60 + step: 0.1 + mode: slider hold_secs: - description: An optional value that specifies that number of seconds you want to have it held before the release is send. If not specified, the release will be send immediately after the press. + name: Hold Seconds + description: An optional value that specifies the number of seconds you want to have it held before the release is send. example: "2.5" + default: 0 + selector: + number: + min: 0 + max: 60 + step: 0.1 + mode: slider learn_command: + name: Learn Command description: Learns a command or a list of commands from a device. + target: fields: - entity_id: - description: Name(s) of entities to learn command from. - example: "remote.bedroom" device: description: Device ID to learn command from. example: "television" command: + name: Command description: A single command or a list of commands to learn. example: "Turn on" + selector: + object: command_type: + name: Command Type description: The type of command to be learned. example: "rf" + default: "ir" + selector: + select: + options: + - "ir" + - "rf" alternative: + name: Alternative description: If code must be stored as alternative (useful for discrete remotes). example: "True" + selector: + boolean: timeout: + name: Timeout description: Timeout, in seconds, for the command to be learned. example: "30" + selector: + number: + min: 0 + max: 60 + step: 5 + mode: slider delete_command: + name: Delete Command description: Deletes a command or a list of commands from the database. + target: fields: - entity_id: - description: Name(s) of the remote entities holding the database. - example: "remote.bedroom" device: description: Name of the device from which commands will be deleted. example: "television" command: + name: Command description: A single command or a list of commands to delete. + required: true example: "Mute" + selector: + object: From c6edc7ae4f8bdcd6fbb13dcd9d73433705e087a0 Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Fri, 23 Apr 2021 13:48:24 +0200 Subject: [PATCH 0481/1317] Clean up devolo Home Control config flow (#49585) --- .../devolo_home_control/config_flow.py | 24 ++++++++------ .../devolo_home_control/exceptions.py | 6 ++++ .../devolo_home_control/test_config_flow.py | 31 ++++++++++++++++--- 3 files changed, 47 insertions(+), 14 deletions(-) create mode 100644 homeassistant/components/devolo_home_control/exceptions.py diff --git a/homeassistant/components/devolo_home_control/config_flow.py b/homeassistant/components/devolo_home_control/config_flow.py index 43bacfed6390a..49abba7723d36 100644 --- a/homeassistant/components/devolo_home_control/config_flow.py +++ b/homeassistant/components/devolo_home_control/config_flow.py @@ -1,6 +1,4 @@ """Config flow to configure the devolo home control integration.""" -import logging - import voluptuous as vol from homeassistant import config_entries @@ -15,8 +13,7 @@ DOMAIN, SUPPORTED_MODEL_TYPES, ) - -_LOGGER = logging.getLogger(__name__) +from .exceptions import CredentialsInvalid class DevoloHomeControlFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @@ -39,8 +36,11 @@ async def async_step_user(self, user_input=None): vol.Required(CONF_MYDEVOLO, default=DEFAULT_MYDEVOLO) ] = str if user_input is None: - return self._show_form(user_input) - return await self._connect_mydevolo(user_input) + return self._show_form(step_id="user") + try: + return await self._connect_mydevolo(user_input) + except CredentialsInvalid: + return self._show_form(step_id="user", errors={"base": "invalid_auth"}) async def async_step_zeroconf(self, discovery_info: DiscoveryInfoType): """Handle zeroconf discovery.""" @@ -54,7 +54,12 @@ async def async_step_zeroconf_confirm(self, user_input=None): """Handle a flow initiated by zeroconf.""" if user_input is None: return self._show_form(step_id="zeroconf_confirm") - return await self._connect_mydevolo(user_input) + try: + return await self._connect_mydevolo(user_input) + except CredentialsInvalid: + return self._show_form( + step_id="zeroconf_confirm", errors={"base": "invalid_auth"} + ) async def _connect_mydevolo(self, user_input): """Connect to mydevolo.""" @@ -63,8 +68,7 @@ async def _connect_mydevolo(self, user_input): mydevolo.credentials_valid ) if not credentials_valid: - return self._show_form({"base": "invalid_auth"}) - _LOGGER.debug("Credentials valid") + raise CredentialsInvalid uuid = await self.hass.async_add_executor_job(mydevolo.uuid) await self.async_set_unique_id(uuid) self._abort_if_unique_id_configured() @@ -79,7 +83,7 @@ async def _connect_mydevolo(self, user_input): ) @callback - def _show_form(self, errors=None, step_id="user"): + def _show_form(self, step_id, errors=None): """Show the form to the user.""" return self.async_show_form( step_id=step_id, diff --git a/homeassistant/components/devolo_home_control/exceptions.py b/homeassistant/components/devolo_home_control/exceptions.py new file mode 100644 index 0000000000000..378efa41cc515 --- /dev/null +++ b/homeassistant/components/devolo_home_control/exceptions.py @@ -0,0 +1,6 @@ +"""Custom exceptions for the devolo_home_control integration.""" +from homeassistant.exceptions import HomeAssistantError + + +class CredentialsInvalid(HomeAssistantError): + """Given credentials are invalid.""" diff --git a/tests/components/devolo_home_control/test_config_flow.py b/tests/components/devolo_home_control/test_config_flow.py index 0b02cb9f4a17c..7765e7335e4f0 100644 --- a/tests/components/devolo_home_control/test_config_flow.py +++ b/tests/components/devolo_home_control/test_config_flow.py @@ -22,20 +22,22 @@ async def test_form(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["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {} await _setup(hass, result) @pytest.mark.credentials_invalid -async def test_form_invalid_credentials(hass): +async def test_form_invalid_credentials_user(hass): """Test if we get the error message on invalid credentials.""" await setup.async_setup_component(hass, "persistent_notification", {}) 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["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( @@ -98,7 +100,7 @@ async def test_form_advanced_options(hass): assert len(mock_setup_entry.mock_calls) == 1 -async def test_show_zeroconf_form(hass): +async def test_form_zeroconf(hass): """Test that the zeroconf confirmation form is served.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -112,6 +114,27 @@ async def test_show_zeroconf_form(hass): await _setup(hass, result) +@pytest.mark.credentials_invalid +async def test_form_invalid_credentials_zeroconf(hass): + """Test if we get the error message on invalid credentials.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=DISCOVERY_INFO, + ) + + assert result["step_id"] == "zeroconf_confirm" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"username": "test-username", "password": "test-password"}, + ) + + assert result["errors"] == {"base": "invalid_auth"} + + async def test_zeroconf_wrong_device(hass): """Test that the zeroconf ignores wrong devices.""" result = await hass.config_entries.flow.async_init( From a6d87b7fae4ecebdb56ac4dcb0e9d96cc86a740a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 23 Apr 2021 10:56:23 -0700 Subject: [PATCH 0482/1317] Batch Google Report State (#49511) * Batch Google Report State * Fix batching --- .../google_assistant/report_state.py | 59 ++++++++++++++++--- .../google_assistant/test_report_state.py | 38 ++++++++++-- 2 files changed, 85 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/google_assistant/report_state.py b/homeassistant/components/google_assistant/report_state.py index cdfb06c5c3963..f7c57732876b4 100644 --- a/homeassistant/components/google_assistant/report_state.py +++ b/homeassistant/components/google_assistant/report_state.py @@ -1,8 +1,11 @@ """Google Report State implementation.""" +from __future__ import annotations + +from collections import deque import logging from homeassistant.const import MATCH_ALL -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback from homeassistant.helpers.event import async_call_later from homeassistant.helpers.significant_change import create_checker @@ -14,6 +17,8 @@ # https://github.com/actions-on-google/smart-home-nodejs/issues/196#issuecomment-439156639 INITIAL_REPORT_DELAY = 60 +# Seconds to wait to group states +REPORT_STATE_WINDOW = 1 _LOGGER = logging.getLogger(__name__) @@ -22,8 +27,35 @@ def async_enable_report_state(hass: HomeAssistant, google_config: AbstractConfig): """Enable state reporting.""" checker = None + unsub_pending: CALLBACK_TYPE | None = None + pending = deque([{}]) + + async def report_states(now=None): + """Report the states.""" + nonlocal pending + nonlocal unsub_pending + + pending.append({}) + + # We will report all batches except last one because those are finalized. + while len(pending) > 1: + await google_config.async_report_state_all( + {"devices": {"states": pending.popleft()}} + ) + + # If things got queued up in last batch while we were reporting, schedule ourselves again + if pending[0]: + unsub_pending = async_call_later( + hass, REPORT_STATE_WINDOW, report_states_job + ) + else: + unsub_pending = None + + report_states_job = HassJob(report_states) async def async_entity_state_listener(changed_entity, old_state, new_state): + nonlocal unsub_pending + if not hass.is_running: return @@ -47,11 +79,19 @@ async def async_entity_state_listener(changed_entity, old_state, new_state): if not checker.async_is_significant_change(new_state, extra_arg=entity_data): return - _LOGGER.debug("Reporting state for %s: %s", changed_entity, entity_data) + _LOGGER.debug("Scheduling report state for %s: %s", changed_entity, entity_data) - await google_config.async_report_state_all( - {"devices": {"states": {changed_entity: entity_data}}} - ) + # If a significant change is already scheduled and we have another significant one, + # let's create a new batch of changes + if changed_entity in pending[-1]: + pending.append({}) + + pending[-1][changed_entity] = entity_data + + if unsub_pending is None: + unsub_pending = async_call_later( + hass, REPORT_STATE_WINDOW, report_states_job + ) @callback def extra_significant_check( @@ -102,5 +142,10 @@ async def inital_report(_now): unsub = async_call_later(hass, INITIAL_REPORT_DELAY, inital_report) - # pylint: disable=unnecessary-lambda - return lambda: unsub() + @callback + def unsub_all(): + unsub() + if unsub_pending: + unsub_pending() # pylint: disable=not-callable + + return unsub_all diff --git a/tests/components/google_assistant/test_report_state.py b/tests/components/google_assistant/test_report_state.py index f464be60bb93c..542a971c5a728 100644 --- a/tests/components/google_assistant/test_report_state.py +++ b/tests/components/google_assistant/test_report_state.py @@ -1,4 +1,5 @@ """Test Google report state.""" +from datetime import timedelta from unittest.mock import AsyncMock, patch from homeassistant.components.google_assistant import error, report_state @@ -41,10 +42,25 @@ async def test_report_state(hass, caplog, legacy_patchable_time): hass.states.async_set("light.kitchen", "on") await hass.async_block_till_done() - assert len(mock_report.mock_calls) == 1 - assert mock_report.mock_calls[0][1][0] == { - "devices": {"states": {"light.kitchen": {"on": True, "online": True}}} - } + hass.states.async_set("light.kitchen_2", "on") + await hass.async_block_till_done() + + assert len(mock_report.mock_calls) == 0 + + async_fire_time_changed( + hass, utcnow() + timedelta(seconds=report_state.REPORT_STATE_WINDOW) + ) + await hass.async_block_till_done() + + assert len(mock_report.mock_calls) == 1 + assert mock_report.mock_calls[0][1][0] == { + "devices": { + "states": { + "light.kitchen": {"on": True, "online": True}, + "light.kitchen_2": {"on": True, "online": True}, + }, + } + } # Test that if serialize returns same value, we don't send with patch( @@ -57,6 +73,9 @@ async def test_report_state(hass, caplog, legacy_patchable_time): # Changed, but serialize is same, so filtered out by extra check hass.states.async_set("light.double_report", "off") + async_fire_time_changed( + hass, utcnow() + timedelta(seconds=report_state.REPORT_STATE_WINDOW) + ) await hass.async_block_till_done() assert len(mock_report.mock_calls) == 1 @@ -69,6 +88,9 @@ async def test_report_state(hass, caplog, legacy_patchable_time): BASIC_CONFIG, "async_report_state_all", AsyncMock() ) as mock_report: hass.states.async_set("switch.ac", "on", {"something": "else"}) + async_fire_time_changed( + hass, utcnow() + timedelta(seconds=report_state.REPORT_STATE_WINDOW) + ) await hass.async_block_till_done() assert len(mock_report.mock_calls) == 0 @@ -81,9 +103,12 @@ async def test_report_state(hass, caplog, legacy_patchable_time): side_effect=error.SmartHomeError("mock-error", "mock-msg"), ): hass.states.async_set("light.kitchen", "off") + async_fire_time_changed( + hass, utcnow() + timedelta(seconds=report_state.REPORT_STATE_WINDOW) + ) await hass.async_block_till_done() - assert "Not reporting state for light.kitchen: mock-error" + assert "Not reporting state for light.kitchen: mock-error" in caplog.text assert len(mock_report.mock_calls) == 0 unsub() @@ -92,6 +117,9 @@ async def test_report_state(hass, caplog, legacy_patchable_time): BASIC_CONFIG, "async_report_state_all", AsyncMock() ) as mock_report: hass.states.async_set("light.kitchen", "on") + async_fire_time_changed( + hass, utcnow() + timedelta(seconds=report_state.REPORT_STATE_WINDOW) + ) await hass.async_block_till_done() assert len(mock_report.mock_calls) == 0 From 8013eb0e082bbb10433820d0355ffb3e75e825f2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 23 Apr 2021 20:02:12 +0200 Subject: [PATCH 0483/1317] Allow data entry flows to hint for additional steps (#49202) --- homeassistant/components/mqtt/config_flow.py | 4 +++- homeassistant/data_entry_flow.py | 3 +++ tests/components/config/test_config_entries.py | 5 +++++ tests/components/subaru/test_config_flow.py | 1 + 4 files changed, 12 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 5e5b8c54cf249..11f9a39782363 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -158,7 +158,7 @@ async def async_step_init(self, user_input=None): return await self.async_step_broker() async def async_step_broker(self, user_input=None): - """Manage the MQTT options.""" + """Manage the MQTT broker configuration.""" errors = {} current_config = self.config_entry.data yaml_config = self.hass.data.get(DATA_MQTT_CONFIG, {}) @@ -201,6 +201,7 @@ async def async_step_broker(self, user_input=None): step_id="broker", data_schema=vol.Schema(fields), errors=errors, + last_step=False, ) async def async_step_options(self, user_input=None): @@ -321,6 +322,7 @@ async def async_step_options(self, user_input=None): step_id="options", data_schema=vol.Schema(fields), errors=errors, + last_step=True, ) diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index b75d956c5273f..a43f30354260f 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -72,6 +72,7 @@ class FlowResultDict(TypedDict, total=False): reason: str context: dict[str, Any] result: Any + last_step: bool | None class FlowManager(abc.ABC): @@ -345,6 +346,7 @@ def async_show_form( data_schema: vol.Schema = None, errors: dict[str, str] | None = None, description_placeholders: dict[str, Any] | None = None, + last_step: bool | None = None, ) -> FlowResultDict: """Return the definition of a form to gather user input.""" return { @@ -355,6 +357,7 @@ def async_show_form( "data_schema": data_schema, "errors": errors, "description_placeholders": description_placeholders, + "last_step": last_step, # Display next or submit button in frontend } @callback diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 4b8155b65135c..abad057b64cc0 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -239,6 +239,7 @@ async def async_step_user(self, user_input=None): "show_advanced_options": True, }, "errors": {"username": "Should be unique."}, + "last_step": None, } @@ -375,6 +376,7 @@ async def async_step_account(self, user_input=None): "data_schema": [{"name": "user_title", "type": "string"}], "description_placeholders": None, "errors": None, + "last_step": None, } with patch.dict(HANDLERS, {"test": TestFlow}): @@ -445,6 +447,7 @@ async def async_step_account(self, user_input=None): "data_schema": [{"name": "user_title", "type": "string"}], "description_placeholders": None, "errors": None, + "last_step": None, } hass_admin_user.groups = [] @@ -612,6 +615,7 @@ async def async_step_init(self, user_input=None): "data_schema": [{"name": "enabled", "required": True, "type": "boolean"}], "description_placeholders": {"enabled": "Set to true to be true"}, "errors": None, + "last_step": None, } @@ -660,6 +664,7 @@ async def async_step_finish(self, user_input=None): "data_schema": [{"name": "enabled", "type": "boolean"}], "description_placeholders": None, "errors": None, + "last_step": None, } with patch.dict(HANDLERS, {"test": TestFlow}): diff --git a/tests/components/subaru/test_config_flow.py b/tests/components/subaru/test_config_flow.py index 35e254fe3026e..031b9c29d09de 100644 --- a/tests/components/subaru/test_config_flow.py +++ b/tests/components/subaru/test_config_flow.py @@ -131,6 +131,7 @@ async def test_pin_form_init(pin_form): "handler": DOMAIN, "step_id": "pin", "type": "form", + "last_step": None, } assert pin_form == expected From 019484f148ba685b3ac55157764dfc2a8c4785f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Fri, 23 Apr 2021 20:57:10 +0200 Subject: [PATCH 0484/1317] Use dev endpoint for dev installations (#49597) --- .../components/analytics/analytics.py | 15 ++- homeassistant/components/analytics/const.py | 1 + tests/components/analytics/test_analytics.py | 104 +++++++++++++++--- tests/components/analytics/test_init.py | 13 ++- 4 files changed, 112 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/analytics/analytics.py b/homeassistant/components/analytics/analytics.py index e6e8678cc1094..daadab65228f5 100644 --- a/homeassistant/components/analytics/analytics.py +++ b/homeassistant/components/analytics/analytics.py @@ -18,6 +18,7 @@ from .const import ( ANALYTICS_ENDPOINT_URL, + ANALYTICS_ENDPOINT_URL_DEV, ATTR_ADDON_COUNT, ATTR_ADDONS, ATTR_AUTO_UPDATE, @@ -78,6 +79,14 @@ def uuid(self) -> bool: """Return the uuid for the analytics integration.""" return self._data[ATTR_UUID] + @property + def endpoint(self) -> str: + """Return the endpoint that will receive the payload.""" + if HA_VERSION.endswith("0.dev0"): + # dev installations will contact the dev analytics environment + return ANALYTICS_ENDPOINT_URL_DEV + return ANALYTICS_ENDPOINT_URL + @property def supervisor(self) -> bool: """Return bool if a supervisor is present.""" @@ -219,7 +228,7 @@ async def send_analytics(self, _=None) -> None: try: with async_timeout.timeout(30): - response = await self.session.post(ANALYTICS_ENDPOINT_URL, json=payload) + response = await self.session.post(self.endpoint, json=payload) if response.status == 200: LOGGER.info( ( @@ -230,7 +239,9 @@ async def send_analytics(self, _=None) -> None: ) else: LOGGER.warning( - "Sending analytics failed with statuscode %s", response.status + "Sending analytics failed with statuscode %s from %s", + response.status, + self.endpoint, ) except asyncio.TimeoutError: LOGGER.error("Timeout sending analytics to %s", ANALYTICS_ENDPOINT_URL) diff --git a/homeassistant/components/analytics/const.py b/homeassistant/components/analytics/const.py index a6fe91b5a44b1..e7046898e9b1a 100644 --- a/homeassistant/components/analytics/const.py +++ b/homeassistant/components/analytics/const.py @@ -5,6 +5,7 @@ import voluptuous as vol ANALYTICS_ENDPOINT_URL = "https://analytics-api.home-assistant.io/v1" +ANALYTICS_ENDPOINT_URL_DEV = "https://analytics-api-dev.home-assistant.io/v1" DOMAIN = "analytics" INTERVAL = timedelta(days=1) STORAGE_KEY = "core.analytics" diff --git a/tests/components/analytics/test_analytics.py b/tests/components/analytics/test_analytics.py index e1716df9cdb9d..1ac8c0fa8f07d 100644 --- a/tests/components/analytics/test_analytics.py +++ b/tests/components/analytics/test_analytics.py @@ -7,6 +7,7 @@ from homeassistant.components.analytics.analytics import Analytics from homeassistant.components.analytics.const import ( ANALYTICS_ENDPOINT_URL, + ANALYTICS_ENDPOINT_URL_DEV, ATTR_BASE, ATTR_DIAGNOSTICS, ATTR_PREFERENCES, @@ -14,16 +15,18 @@ ATTR_USAGE, ) from homeassistant.components.api import ATTR_UUID -from homeassistant.const import ATTR_DOMAIN, __version__ as HA_VERSION +from homeassistant.const import ATTR_DOMAIN from homeassistant.loader import IntegrationNotFound from homeassistant.setup import async_setup_component MOCK_UUID = "abcdefg" +MOCK_VERSION = "1970.1.0" +MOCK_VERSION_DEV = "1970.1.0.dev0" +MOCK_VERSION_NIGHTLY = "1970.1.0.dev19700101" async def test_no_send(hass, caplog, aioclient_mock): """Test send when no prefrences are defined.""" - aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) analytics = Analytics(hass) with patch( "homeassistant.components.hassio.is_hassio", @@ -77,8 +80,13 @@ async def test_failed_to_send(hass, caplog, aioclient_mock): analytics = Analytics(hass) await analytics.save_preferences({ATTR_BASE: True}) assert analytics.preferences[ATTR_BASE] - await analytics.send_analytics() - assert "Sending analytics failed with statuscode 400" in caplog.text + + with patch("homeassistant.components.analytics.analytics.HA_VERSION", MOCK_VERSION): + await analytics.send_analytics() + assert ( + f"Sending analytics failed with statuscode 400 from {ANALYTICS_ENDPOINT_URL}" + in caplog.text + ) async def test_failed_to_send_raises(hass, caplog, aioclient_mock): @@ -87,7 +95,9 @@ async def test_failed_to_send_raises(hass, caplog, aioclient_mock): analytics = Analytics(hass) await analytics.save_preferences({ATTR_BASE: True}) assert analytics.preferences[ATTR_BASE] - await analytics.send_analytics() + + with patch("homeassistant.components.analytics.analytics.HA_VERSION", MOCK_VERSION): + await analytics.send_analytics() assert "Error sending analytics" in caplog.text @@ -99,12 +109,14 @@ async def test_send_base(hass, caplog, aioclient_mock): await analytics.save_preferences({ATTR_BASE: True}) assert analytics.preferences[ATTR_BASE] - with patch("uuid.UUID.hex", new_callable=PropertyMock) as hex: + with patch("uuid.UUID.hex", new_callable=PropertyMock) as hex, patch( + "homeassistant.components.analytics.analytics.HA_VERSION", MOCK_VERSION + ): hex.return_value = MOCK_UUID await analytics.send_analytics() assert f"'uuid': '{MOCK_UUID}'" in caplog.text - assert f"'version': '{HA_VERSION}'" in caplog.text + assert f"'version': '{MOCK_VERSION}'" in caplog.text assert "'installation_type':" in caplog.text assert "'integration_count':" not in caplog.text assert "'integrations':" not in caplog.text @@ -132,14 +144,16 @@ async def test_send_base_with_supervisor(hass, caplog, aioclient_mock): side_effect=Mock(return_value=True), ), patch( "uuid.UUID.hex", new_callable=PropertyMock - ) as hex: + ) as hex, patch( + "homeassistant.components.analytics.analytics.HA_VERSION", MOCK_VERSION + ): hex.return_value = MOCK_UUID await analytics.load() await analytics.send_analytics() assert f"'uuid': '{MOCK_UUID}'" in caplog.text - assert f"'version': '{HA_VERSION}'" in caplog.text + assert f"'version': '{MOCK_VERSION}'" in caplog.text assert "'supervisor': {'healthy': True, 'supported': True}}" in caplog.text assert "'installation_type':" in caplog.text assert "'integration_count':" not in caplog.text @@ -156,7 +170,8 @@ async def test_send_usage(hass, caplog, aioclient_mock): assert analytics.preferences[ATTR_USAGE] hass.config.components = ["default_config"] - await analytics.send_analytics() + with patch("homeassistant.components.analytics.analytics.HA_VERSION", MOCK_VERSION): + await analytics.send_analytics() assert "'integrations': ['default_config']" in caplog.text assert "'integration_count':" not in caplog.text @@ -200,6 +215,8 @@ async def test_send_usage_with_supervisor(hass, caplog, aioclient_mock): ), patch( "homeassistant.components.hassio.is_hassio", side_effect=Mock(return_value=True), + ), patch( + "homeassistant.components.analytics.analytics.HA_VERSION", MOCK_VERSION ): await analytics.send_analytics() assert ( @@ -218,7 +235,8 @@ async def test_send_statistics(hass, caplog, aioclient_mock): assert analytics.preferences[ATTR_STATISTICS] hass.config.components = ["default_config"] - await analytics.send_analytics() + with patch("homeassistant.components.analytics.analytics.HA_VERSION", MOCK_VERSION): + await analytics.send_analytics() assert ( "'state_count': 0, 'automation_count': 0, 'integration_count': 1, 'user_count': 0" in caplog.text @@ -238,7 +256,7 @@ async def test_send_statistics_one_integration_fails(hass, caplog, aioclient_moc with patch( "homeassistant.components.analytics.analytics.async_get_integration", side_effect=IntegrationNotFound("any"), - ): + ), patch("homeassistant.components.analytics.analytics.HA_VERSION", MOCK_VERSION): await analytics.send_analytics() post_call = aioclient_mock.mock_calls[0] @@ -260,7 +278,7 @@ async def test_send_statistics_async_get_integration_unknown_exception( with pytest.raises(ValueError), patch( "homeassistant.components.analytics.analytics.async_get_integration", side_effect=ValueError, - ): + ), patch("homeassistant.components.analytics.analytics.HA_VERSION", MOCK_VERSION): await analytics.send_analytics() @@ -300,6 +318,8 @@ async def test_send_statistics_with_supervisor(hass, caplog, aioclient_mock): ), patch( "homeassistant.components.hassio.is_hassio", side_effect=Mock(return_value=True), + ), patch( + "homeassistant.components.analytics.analytics.HA_VERSION", MOCK_VERSION ): await analytics.send_analytics() assert "'addon_count': 1" in caplog.text @@ -314,7 +334,9 @@ async def test_reusing_uuid(hass, aioclient_mock): await analytics.save_preferences({ATTR_BASE: True}) - with patch("uuid.UUID.hex", new_callable=PropertyMock) as hex: + with patch("uuid.UUID.hex", new_callable=PropertyMock) as hex, patch( + "homeassistant.components.analytics.analytics.HA_VERSION", MOCK_VERSION + ): # This is not actually called but that in itself prove the test hex.return_value = MOCK_UUID await analytics.send_analytics() @@ -329,7 +351,59 @@ async def test_custom_integrations(hass, aioclient_mock): assert await async_setup_component(hass, "test_package", {"test_package": {}}) await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True}) - await analytics.send_analytics() + with patch("homeassistant.components.analytics.analytics.HA_VERSION", MOCK_VERSION): + await analytics.send_analytics() payload = aioclient_mock.mock_calls[0][2] assert payload["custom_integrations"][0][ATTR_DOMAIN] == "test_package" + + +async def test_dev_url(hass, aioclient_mock): + """Test sending payload to dev url.""" + aioclient_mock.post(ANALYTICS_ENDPOINT_URL_DEV, status=200) + analytics = Analytics(hass) + await analytics.save_preferences({ATTR_BASE: True}) + + with patch( + "homeassistant.components.analytics.analytics.HA_VERSION", MOCK_VERSION_DEV + ): + await analytics.send_analytics() + + payload = aioclient_mock.mock_calls[0] + assert str(payload[1]) == ANALYTICS_ENDPOINT_URL_DEV + + +async def test_dev_url_error(hass, aioclient_mock, caplog): + """Test sending payload to dev url that returns error.""" + aioclient_mock.post(ANALYTICS_ENDPOINT_URL_DEV, status=400) + analytics = Analytics(hass) + await analytics.save_preferences({ATTR_BASE: True}) + + with patch( + "homeassistant.components.analytics.analytics.HA_VERSION", MOCK_VERSION_DEV + ): + + await analytics.send_analytics() + + payload = aioclient_mock.mock_calls[0] + assert str(payload[1]) == ANALYTICS_ENDPOINT_URL_DEV + assert ( + f"Sending analytics failed with statuscode 400 from {ANALYTICS_ENDPOINT_URL_DEV}" + in caplog.text + ) + + +async def test_nightly_endpoint(hass, aioclient_mock): + """Test sending payload to production url when running nightly.""" + aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) + analytics = Analytics(hass) + await analytics.save_preferences({ATTR_BASE: True}) + + with patch( + "homeassistant.components.analytics.analytics.HA_VERSION", MOCK_VERSION_NIGHTLY + ): + + await analytics.send_analytics() + + payload = aioclient_mock.mock_calls[0] + assert str(payload[1]) == ANALYTICS_ENDPOINT_URL diff --git a/tests/components/analytics/test_init.py b/tests/components/analytics/test_init.py index af10592692644..e48d662594d35 100644 --- a/tests/components/analytics/test_init.py +++ b/tests/components/analytics/test_init.py @@ -1,7 +1,11 @@ """The tests for the analytics .""" +from unittest.mock import patch + from homeassistant.components.analytics.const import ANALYTICS_ENDPOINT_URL, DOMAIN from homeassistant.setup import async_setup_component +MOCK_VERSION = "1970.1.0" + async def test_setup(hass): """Test setup of the integration.""" @@ -24,10 +28,11 @@ async def test_websocket(hass, hass_ws_client, aioclient_mock): assert response["success"] - await ws_client.send_json( - {"id": 2, "type": "analytics/preferences", "preferences": {"base": True}} - ) - response = await ws_client.receive_json() + with patch("homeassistant.components.analytics.analytics.HA_VERSION", MOCK_VERSION): + await ws_client.send_json( + {"id": 2, "type": "analytics/preferences", "preferences": {"base": True}} + ) + response = await ws_client.receive_json() assert len(aioclient_mock.mock_calls) == 1 assert response["result"]["preferences"]["base"] From 694a1631248589ca7827b2367e3645ea78131a01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Fri, 23 Apr 2021 23:29:20 +0200 Subject: [PATCH 0485/1317] Update met.no library (#49607) --- homeassistant/components/met/manifest.json | 2 +- homeassistant/components/norway_air/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/met/manifest.json b/homeassistant/components/met/manifest.json index d38c44c58809b..97edf8eb67ff4 100644 --- a/homeassistant/components/met/manifest.json +++ b/homeassistant/components/met/manifest.json @@ -3,7 +3,7 @@ "name": "Meteorologisk institutt (Met.no)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/met", - "requirements": ["pyMetno==0.8.2"], + "requirements": ["pyMetno==0.8.3"], "codeowners": ["@danielhiversen", "@thimic"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/norway_air/manifest.json b/homeassistant/components/norway_air/manifest.json index ae213e4f53941..69b2e85808bc3 100644 --- a/homeassistant/components/norway_air/manifest.json +++ b/homeassistant/components/norway_air/manifest.json @@ -2,7 +2,7 @@ "domain": "norway_air", "name": "Om Luftkvalitet i Norge (Norway Air)", "documentation": "https://www.home-assistant.io/integrations/norway_air", - "requirements": ["pyMetno==0.8.2"], + "requirements": ["pyMetno==0.8.3"], "codeowners": [], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index e9b4e2cba88bc..6fceb82faceff 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1235,7 +1235,7 @@ pyMetEireann==0.2 # homeassistant.components.met # homeassistant.components.norway_air -pyMetno==0.8.2 +pyMetno==0.8.3 # homeassistant.components.rfxtrx pyRFXtrx==0.26.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a5f2463d33af2..59d8489698689 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -666,7 +666,7 @@ pyMetEireann==0.2 # homeassistant.components.met # homeassistant.components.norway_air -pyMetno==0.8.2 +pyMetno==0.8.3 # homeassistant.components.rfxtrx pyRFXtrx==0.26.1 From 32dfaccf1fab5ce6d7ab73a3dee7c9aa4c93351d Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sat, 24 Apr 2021 00:03:34 +0000 Subject: [PATCH 0486/1317] [ci skip] Translation update --- .../components/airvisual/translations/lb.json | 3 ++- .../components/asuswrt/translations/lb.json | 12 ++++++++++ .../components/august/translations/lb.json | 8 +++++++ .../components/cast/translations/lb.json | 3 +++ .../components/climacell/translations/lb.json | 11 ++++++++++ .../components/deconz/translations/lb.json | 2 ++ .../components/denonavr/translations/pl.json | 1 + .../devolo_home_control/translations/ca.json | 7 ++++++ .../devolo_home_control/translations/et.json | 7 ++++++ .../devolo_home_control/translations/pl.json | 7 ++++++ .../devolo_home_control/translations/ru.json | 7 ++++++ .../translations/zh-Hant.json | 7 ++++++ .../huawei_lte/translations/pl.json | 3 ++- .../components/mysensors/translations/pl.json | 1 + .../components/picnic/translations/pl.json | 22 +++++++++++++++++++ .../components/smarttub/translations/pl.json | 6 ++++- .../smarttub/translations/zh-Hant.json | 6 ++++- 17 files changed, 109 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/asuswrt/translations/lb.json create mode 100644 homeassistant/components/climacell/translations/lb.json create mode 100644 homeassistant/components/picnic/translations/pl.json diff --git a/homeassistant/components/airvisual/translations/lb.json b/homeassistant/components/airvisual/translations/lb.json index d6799ba6e37b9..5a4fb2c07f222 100644 --- a/homeassistant/components/airvisual/translations/lb.json +++ b/homeassistant/components/airvisual/translations/lb.json @@ -23,7 +23,8 @@ "geography_by_name": { "data": { "city": "Stad", - "country": "Land" + "country": "Land", + "state": "Kanton" } }, "node_pro": { diff --git a/homeassistant/components/asuswrt/translations/lb.json b/homeassistant/components/asuswrt/translations/lb.json new file mode 100644 index 0000000000000..0c1512ac67acc --- /dev/null +++ b/homeassistant/components/asuswrt/translations/lb.json @@ -0,0 +1,12 @@ +{ + "config": { + "error": { + "ssh_not_file": "SSH Schl\u00ebssel Datei net fonnt" + }, + "step": { + "user": { + "title": "AsusWRT" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/august/translations/lb.json b/homeassistant/components/august/translations/lb.json index 87fef5f521b5b..569771dc393f9 100644 --- a/homeassistant/components/august/translations/lb.json +++ b/homeassistant/components/august/translations/lb.json @@ -10,6 +10,9 @@ "unknown": "Onerwaarte Feeler" }, "step": { + "reauth_validate": { + "description": "G\u00ebff Passwuert an fir {username}." + }, "user": { "data": { "login_method": "Login Method", @@ -20,6 +23,11 @@ "description": "Wann d'Login Method 'E-Mail' ass, dannn ass de Benotzernumm d'E-Mail Adress. Wann d'Login-Method 'Telefon' ass, ass den Benotzernumm d'Telefonsnummer am Format '+ NNNNNNNNN'.", "title": "August Kont ariichten" }, + "user_validate": { + "data": { + "login_method": "Login Method" + } + }, "validation": { "data": { "code": "Verifikatiouns Code" diff --git a/homeassistant/components/cast/translations/lb.json b/homeassistant/components/cast/translations/lb.json index bf4bc68b5ad6b..8f572aa48cea6 100644 --- a/homeassistant/components/cast/translations/lb.json +++ b/homeassistant/components/cast/translations/lb.json @@ -5,6 +5,9 @@ "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun m\u00e9iglech." }, "step": { + "config": { + "title": "Google Cast" + }, "confirm": { "description": "Soll den Ariichtungs Prozess gestart ginn?" } diff --git a/homeassistant/components/climacell/translations/lb.json b/homeassistant/components/climacell/translations/lb.json new file mode 100644 index 0000000000000..e075d198b7fe8 --- /dev/null +++ b/homeassistant/components/climacell/translations/lb.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_version": "API Versioun" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/translations/lb.json b/homeassistant/components/deconz/translations/lb.json index 06b8dbacdc53b..84a535c8ebc8d 100644 --- a/homeassistant/components/deconz/translations/lb.json +++ b/homeassistant/components/deconz/translations/lb.json @@ -42,6 +42,8 @@ "button_2": "Zweete Kn\u00e4ppchen", "button_3": "Dr\u00ebtte Kn\u00e4ppchen", "button_4": "V\u00e9ierte Kn\u00e4ppchen", + "button_5": "F\u00ebnnefte Kn\u00e4ppchen", + "button_6": "Sechste Kn\u00e4ppchen", "close": "Zoumaachen", "dim_down": "Verd\u00e4ischteren", "dim_up": "Erhellen", diff --git a/homeassistant/components/denonavr/translations/pl.json b/homeassistant/components/denonavr/translations/pl.json index 19061bf52525e..c874cc6fb7ee5 100644 --- a/homeassistant/components/denonavr/translations/pl.json +++ b/homeassistant/components/denonavr/translations/pl.json @@ -37,6 +37,7 @@ "init": { "data": { "show_all_sources": "Poka\u017c wszystkie \u017ar\u00f3d\u0142a", + "update_audyssey": "Uaktualnij ustawienia Audyssey", "zone2": "Konfiguracja Strefy 2", "zone3": "Konfiguracja Strefy 3" }, diff --git a/homeassistant/components/devolo_home_control/translations/ca.json b/homeassistant/components/devolo_home_control/translations/ca.json index 317e918c48a24..57ca0b7c2090e 100644 --- a/homeassistant/components/devolo_home_control/translations/ca.json +++ b/homeassistant/components/devolo_home_control/translations/ca.json @@ -14,6 +14,13 @@ "password": "Contrasenya", "username": "Correu electr\u00f2nic / ID de devolo" } + }, + "zeroconf_confirm": { + "data": { + "mydevolo_url": "mydevolo URL", + "password": "Contrasenya", + "username": "Correu electr\u00f2nic / ID de devolo" + } } } } diff --git a/homeassistant/components/devolo_home_control/translations/et.json b/homeassistant/components/devolo_home_control/translations/et.json index 9299c87170a82..f781e4b404225 100644 --- a/homeassistant/components/devolo_home_control/translations/et.json +++ b/homeassistant/components/devolo_home_control/translations/et.json @@ -14,6 +14,13 @@ "password": "Salas\u00f5na", "username": "E-post / devolo ID" } + }, + "zeroconf_confirm": { + "data": { + "mydevolo_url": "mydevolo URL", + "password": "Salas\u00f5na", + "username": "E-post / devolo ID" + } } } } diff --git a/homeassistant/components/devolo_home_control/translations/pl.json b/homeassistant/components/devolo_home_control/translations/pl.json index 699ad56c85bbc..e07f41deb6d70 100644 --- a/homeassistant/components/devolo_home_control/translations/pl.json +++ b/homeassistant/components/devolo_home_control/translations/pl.json @@ -14,6 +14,13 @@ "password": "Has\u0142o", "username": "Nazwa u\u017cytkownika/identyfikator devolo" } + }, + "zeroconf_confirm": { + "data": { + "mydevolo_url": "URL mydevolo", + "password": "Has\u0142o", + "username": "Adres e-mail/identyfikator devolo" + } } } } diff --git a/homeassistant/components/devolo_home_control/translations/ru.json b/homeassistant/components/devolo_home_control/translations/ru.json index b2e82f1355b75..66293556e7c5e 100644 --- a/homeassistant/components/devolo_home_control/translations/ru.json +++ b/homeassistant/components/devolo_home_control/translations/ru.json @@ -14,6 +14,13 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b / devolo ID" } + }, + "zeroconf_confirm": { + "data": { + "mydevolo_url": "URL-\u0430\u0434\u0440\u0435\u0441 mydevolo", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b / devolo ID" + } } } } diff --git a/homeassistant/components/devolo_home_control/translations/zh-Hant.json b/homeassistant/components/devolo_home_control/translations/zh-Hant.json index e408e9794cab0..b855480da9eec 100644 --- a/homeassistant/components/devolo_home_control/translations/zh-Hant.json +++ b/homeassistant/components/devolo_home_control/translations/zh-Hant.json @@ -14,6 +14,13 @@ "password": "\u5bc6\u78bc", "username": "\u96fb\u5b50\u90f5\u4ef6 / devolo ID" } + }, + "zeroconf_confirm": { + "data": { + "mydevolo_url": "mydevolo \u7db2\u5740", + "password": "\u5bc6\u78bc", + "username": "\u96fb\u5b50\u90f5\u4ef6 / devolo ID" + } } } } diff --git a/homeassistant/components/huawei_lte/translations/pl.json b/homeassistant/components/huawei_lte/translations/pl.json index 0720182b6974f..2d71c097b3b29 100644 --- a/homeassistant/components/huawei_lte/translations/pl.json +++ b/homeassistant/components/huawei_lte/translations/pl.json @@ -34,7 +34,8 @@ "data": { "name": "Nazwa us\u0142ugi powiadomie\u0144 (zmiana wymaga ponownego uruchomienia)", "recipient": "Odbiorcy powiadomie\u0144 SMS", - "track_new_devices": "\u015aled\u017a nowe urz\u0105dzenia" + "track_new_devices": "\u015aled\u017a nowe urz\u0105dzenia", + "track_wired_clients": "\u015aled\u017a klient\u00f3w sieci przewodowej" } } } diff --git a/homeassistant/components/mysensors/translations/pl.json b/homeassistant/components/mysensors/translations/pl.json index fa67ffe403042..f3233a01d5023 100644 --- a/homeassistant/components/mysensors/translations/pl.json +++ b/homeassistant/components/mysensors/translations/pl.json @@ -33,6 +33,7 @@ "invalid_serial": "Nieprawid\u0142owy port szeregowy", "invalid_subscribe_topic": "Nieprawid\u0142owy temat \"subscribe\"", "invalid_version": "Nieprawid\u0142owa wersja MySensors", + "mqtt_required": "Integracja MQTT nie jest skonfigurowana", "not_a_number": "Prosz\u0119 wpisa\u0107 numer", "port_out_of_range": "Numer portu musi by\u0107 pomi\u0119dzy 1 a 65535", "same_topic": "Tematy \"subscribe\" i \"publish\" s\u0105 takie same", diff --git a/homeassistant/components/picnic/translations/pl.json b/homeassistant/components/picnic/translations/pl.json new file mode 100644 index 0000000000000..c278f29d13cf5 --- /dev/null +++ b/homeassistant/components/picnic/translations/pl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "country_code": "Kod kraju", + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + } + } + } + }, + "title": "Picnic" +} \ No newline at end of file diff --git a/homeassistant/components/smarttub/translations/pl.json b/homeassistant/components/smarttub/translations/pl.json index 2c3f097d6d003..f17feed06b5fb 100644 --- a/homeassistant/components/smarttub/translations/pl.json +++ b/homeassistant/components/smarttub/translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_configured": "Konto jest ju\u017c skonfigurowane", "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, "error": { @@ -9,6 +9,10 @@ "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { + "reauth_confirm": { + "description": "Integracja SmartTub wymaga ponownego uwierzytelnienia konta", + "title": "Ponownie uwierzytelnij integracj\u0119" + }, "user": { "data": { "email": "Adres e-mail", diff --git a/homeassistant/components/smarttub/translations/zh-Hant.json b/homeassistant/components/smarttub/translations/zh-Hant.json index 880b809db0cd9..ab0b75bf1c8b1 100644 --- a/homeassistant/components/smarttub/translations/zh-Hant.json +++ b/homeassistant/components/smarttub/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { @@ -9,6 +9,10 @@ "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { + "reauth_confirm": { + "description": "SmartTub \u6574\u5408\u9700\u8981\u91cd\u65b0\u8a8d\u8b49\u60a8\u7684\u5e33\u865f", + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" + }, "user": { "data": { "email": "\u96fb\u5b50\u90f5\u4ef6", From 0072923fbe203008ee3bea2d674b80ea6b7fd67b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 23 Apr 2021 20:10:58 -0700 Subject: [PATCH 0487/1317] Bump frontend to 20210423.0 --- homeassistant/components/frontend/manifest.json | 10 +++++++--- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 4e8cf0295d908..c5f065b49bfe3 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,9 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20210416.0"], + "requirements": [ + "home-assistant-frontend==20210423.0" + ], "dependencies": [ "api", "auth", @@ -15,6 +17,8 @@ "system_log", "websocket_api" ], - "codeowners": ["@home-assistant/frontend"], + "codeowners": [ + "@home-assistant/frontend" + ], "quality_scale": "internal" -} +} \ No newline at end of file diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index bfe401f6d62f7..b8a31c5fcc6df 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ defusedxml==0.6.0 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.43.0 -home-assistant-frontend==20210416.0 +home-assistant-frontend==20210423.0 httpx==0.17.1 jinja2>=2.11.3 netdisco==2.8.2 diff --git a/requirements_all.txt b/requirements_all.txt index 6fceb82faceff..7b7f0df47a4ae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -762,7 +762,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210416.0 +home-assistant-frontend==20210423.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 59d8489698689..b2bf0f938fcd0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -423,7 +423,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210416.0 +home-assistant-frontend==20210423.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 33d4d545a741b8477ce64fdb4f58db7a593cb8c4 Mon Sep 17 00:00:00 2001 From: Jakub Bartkowiak Date: Sat, 24 Apr 2021 05:22:56 +0200 Subject: [PATCH 0488/1317] Fix charging error in Roomba integration (#49416) --- homeassistant/components/roomba/__init__.py | 4 ++-- .../components/roomba/config_flow.py | 4 ++-- homeassistant/components/roomba/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/roomba/test_config_flow.py | 23 +++++++++---------- 6 files changed, 18 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/roomba/__init__.py b/homeassistant/components/roomba/__init__.py index ae1fc05ad5365..aa7c06d23a088 100644 --- a/homeassistant/components/roomba/__init__.py +++ b/homeassistant/components/roomba/__init__.py @@ -3,7 +3,7 @@ import logging import async_timeout -from roombapy import Roomba, RoombaConnectionError +from roombapy import RoombaConnectionError, RoombaFactory from homeassistant import exceptions from homeassistant.const import ( @@ -40,7 +40,7 @@ async def async_setup_entry(hass, config_entry): }, ) - roomba = Roomba( + roomba = RoombaFactory.create_roomba( address=config_entry.data[CONF_HOST], blid=config_entry.data[CONF_BLID], password=config_entry.data[CONF_PASSWORD], diff --git a/homeassistant/components/roomba/config_flow.py b/homeassistant/components/roomba/config_flow.py index 11bd7fe275826..376447157c83a 100644 --- a/homeassistant/components/roomba/config_flow.py +++ b/homeassistant/components/roomba/config_flow.py @@ -2,7 +2,7 @@ import asyncio -from roombapy import Roomba +from roombapy import RoombaFactory from roombapy.discovery import RoombaDiscovery from roombapy.getpassword import RoombaPassword import voluptuous as vol @@ -40,7 +40,7 @@ async def validate_input(hass: core.HomeAssistant, data): Data has the keys from DATA_SCHEMA with values provided by the user. """ - roomba = Roomba( + roomba = RoombaFactory.create_roomba( address=data[CONF_HOST], blid=data[CONF_BLID], password=data[CONF_PASSWORD], diff --git a/homeassistant/components/roomba/manifest.json b/homeassistant/components/roomba/manifest.json index ce17cf8c2c2cb..2aaa1f6762e5b 100644 --- a/homeassistant/components/roomba/manifest.json +++ b/homeassistant/components/roomba/manifest.json @@ -3,7 +3,7 @@ "name": "iRobot Roomba and Braava", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/roomba", - "requirements": ["roombapy==1.6.2"], + "requirements": ["roombapy==1.6.3"], "codeowners": ["@pschmitt", "@cyr-ius", "@shenxn"], "dhcp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 7b7f0df47a4ae..79d08c9c640eb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1991,7 +1991,7 @@ rocketchat-API==0.6.1 rokuecp==0.8.1 # homeassistant.components.roomba -roombapy==1.6.2 +roombapy==1.6.3 # homeassistant.components.roon roonapi==0.0.32 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b2bf0f938fcd0..eed3d3e666daa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1058,7 +1058,7 @@ ring_doorbell==0.6.2 rokuecp==0.8.1 # homeassistant.components.roomba -roombapy==1.6.2 +roombapy==1.6.3 # homeassistant.components.roon roonapi==0.0.32 diff --git a/tests/components/roomba/test_config_flow.py b/tests/components/roomba/test_config_flow.py index ffea0c3140c07..32b3c1d95b35b 100644 --- a/tests/components/roomba/test_config_flow.py +++ b/tests/components/roomba/test_config_flow.py @@ -2,8 +2,7 @@ from unittest.mock import MagicMock, PropertyMock, patch import pytest -from roombapy import RoombaConnectionError -from roombapy.roomba import RoombaInfo +from roombapy import RoombaConnectionError, RoombaInfo from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS @@ -144,7 +143,7 @@ async def test_form_user_discovery_and_password_fetch(hass): assert result2["step_id"] == "link" with patch( - "homeassistant.components.roomba.config_flow.Roomba", + "homeassistant.components.roomba.config_flow.RoombaFactory.create_roomba", return_value=mocked_roomba, ), patch( "homeassistant.components.roomba.config_flow.RoombaPassword", @@ -260,7 +259,7 @@ async def test_form_user_discovery_manual_and_auto_password_fetch(hass): assert result3["errors"] is None with patch( - "homeassistant.components.roomba.config_flow.Roomba", + "homeassistant.components.roomba.config_flow.RoombaFactory.create_roomba", return_value=mocked_roomba, ), patch( "homeassistant.components.roomba.config_flow.RoombaPassword", @@ -359,7 +358,7 @@ async def test_form_user_discovery_manual_and_auto_password_fetch_but_cannot_con assert result3["errors"] is None with patch( - "homeassistant.components.roomba.config_flow.Roomba", + "homeassistant.components.roomba.config_flow.RoombaFactory.create_roomba", return_value=mocked_roomba, ), patch( "homeassistant.components.roomba.config_flow.RoombaPassword", @@ -410,7 +409,7 @@ async def test_form_user_discovery_no_devices_found_and_auto_password_fetch(hass assert result2["errors"] is None with patch( - "homeassistant.components.roomba.config_flow.Roomba", + "homeassistant.components.roomba.config_flow.RoombaFactory.create_roomba", return_value=mocked_roomba, ), patch( "homeassistant.components.roomba.config_flow.RoombaPassword", @@ -479,7 +478,7 @@ async def test_form_user_discovery_no_devices_found_and_password_fetch_fails(has await hass.async_block_till_done() with patch( - "homeassistant.components.roomba.config_flow.Roomba", + "homeassistant.components.roomba.config_flow.RoombaFactory.create_roomba", return_value=mocked_roomba, ), patch( "homeassistant.components.roomba.async_setup_entry", @@ -548,7 +547,7 @@ async def test_form_user_discovery_not_devices_found_and_password_fetch_fails_an await hass.async_block_till_done() with patch( - "homeassistant.components.roomba.config_flow.Roomba", + "homeassistant.components.roomba.config_flow.RoombaFactory.create_roomba", return_value=mocked_roomba, ), patch( "homeassistant.components.roomba.async_setup_entry", @@ -606,7 +605,7 @@ async def test_form_user_discovery_and_password_fetch_gets_connection_refused(ha await hass.async_block_till_done() with patch( - "homeassistant.components.roomba.config_flow.Roomba", + "homeassistant.components.roomba.config_flow.RoombaFactory.create_roomba", return_value=mocked_roomba, ), patch( "homeassistant.components.roomba.async_setup_entry", @@ -657,7 +656,7 @@ async def test_dhcp_discovery_and_roomba_discovery_finds(hass, discovery_data): assert result["description_placeholders"] == {"name": "robot_name"} with patch( - "homeassistant.components.roomba.config_flow.Roomba", + "homeassistant.components.roomba.config_flow.RoombaFactory.create_roomba", return_value=mocked_roomba, ), patch( "homeassistant.components.roomba.config_flow.RoombaPassword", @@ -727,7 +726,7 @@ async def test_dhcp_discovery_falls_back_to_manual(hass, discovery_data): assert result3["errors"] is None with patch( - "homeassistant.components.roomba.config_flow.Roomba", + "homeassistant.components.roomba.config_flow.RoombaFactory.create_roomba", return_value=mocked_roomba, ), patch( "homeassistant.components.roomba.config_flow.RoombaPassword", @@ -789,7 +788,7 @@ async def test_dhcp_discovery_no_devices_falls_back_to_manual(hass, discovery_da assert result2["errors"] is None with patch( - "homeassistant.components.roomba.config_flow.Roomba", + "homeassistant.components.roomba.config_flow.RoombaFactory.create_roomba", return_value=mocked_roomba, ), patch( "homeassistant.components.roomba.config_flow.RoombaPassword", From a380632384f2544d8af4955a65f3530138af7076 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 24 Apr 2021 06:12:08 +0200 Subject: [PATCH 0489/1317] Upgrade watchdog to 2.0.3 (#49594) --- homeassistant/components/folder_watcher/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/folder_watcher/manifest.json b/homeassistant/components/folder_watcher/manifest.json index 6263a0495b749..01482a2c5fe60 100644 --- a/homeassistant/components/folder_watcher/manifest.json +++ b/homeassistant/components/folder_watcher/manifest.json @@ -2,7 +2,7 @@ "domain": "folder_watcher", "name": "Folder Watcher", "documentation": "https://www.home-assistant.io/integrations/folder_watcher", - "requirements": ["watchdog==2.0.2"], + "requirements": ["watchdog==2.0.3"], "codeowners": [], "quality_scale": "internal", "iot_class": "local_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 79d08c9c640eb..9d139759fcc6c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2314,7 +2314,7 @@ wakeonlan==2.0.1 waqiasync==1.0.0 # homeassistant.components.folder_watcher -watchdog==2.0.2 +watchdog==2.0.3 # homeassistant.components.waterfurnace waterfurnace==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index eed3d3e666daa..075d3be686fbd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1223,7 +1223,7 @@ vultr==0.1.2 wakeonlan==2.0.1 # homeassistant.components.folder_watcher -watchdog==2.0.2 +watchdog==2.0.3 # homeassistant.components.wiffi wiffi==1.0.1 From bbe58091a8614f0688688c11bbf2cf3d80f670e9 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Fri, 23 Apr 2021 23:00:28 -0700 Subject: [PATCH 0490/1317] Create a motionEye integration (#48239) --- CODEOWNERS | 1 + .../components/motioneye/__init__.py | 258 ++++++++++++++ homeassistant/components/motioneye/camera.py | 208 ++++++++++++ .../components/motioneye/config_flow.py | 127 +++++++ homeassistant/components/motioneye/const.py | 20 ++ .../components/motioneye/manifest.json | 13 + .../components/motioneye/strings.json | 25 ++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/motioneye/__init__.py | 180 ++++++++++ tests/components/motioneye/test_camera.py | 315 ++++++++++++++++++ .../components/motioneye/test_config_flow.py | 233 +++++++++++++ 13 files changed, 1387 insertions(+) create mode 100644 homeassistant/components/motioneye/__init__.py create mode 100644 homeassistant/components/motioneye/camera.py create mode 100644 homeassistant/components/motioneye/config_flow.py create mode 100644 homeassistant/components/motioneye/const.py create mode 100644 homeassistant/components/motioneye/manifest.json create mode 100644 homeassistant/components/motioneye/strings.json create mode 100644 tests/components/motioneye/__init__.py create mode 100644 tests/components/motioneye/test_camera.py create mode 100644 tests/components/motioneye/test_config_flow.py diff --git a/CODEOWNERS b/CODEOWNERS index 6d044f4d06b6a..d6226c08a5d32 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -294,6 +294,7 @@ homeassistant/components/modbus/* @adamchengtkc @janiversen @vzahradnik homeassistant/components/monoprice/* @etsinko @OnFreund homeassistant/components/moon/* @fabaff homeassistant/components/motion_blinds/* @starkillerOG +homeassistant/components/motioneye/* @dermotduffy homeassistant/components/mpd/* @fabaff homeassistant/components/mqtt/* @emontnemery homeassistant/components/msteams/* @peroyvind diff --git a/homeassistant/components/motioneye/__init__.py b/homeassistant/components/motioneye/__init__.py new file mode 100644 index 0000000000000..61e7a7d12f365 --- /dev/null +++ b/homeassistant/components/motioneye/__init__.py @@ -0,0 +1,258 @@ +"""The motionEye integration.""" +from __future__ import annotations + +import asyncio +import logging +from typing import Any, Callable + +from motioneye_client.client import ( + MotionEyeClient, + MotionEyeClientError, + MotionEyeClientInvalidAuthError, +) +from motioneye_client.const import KEY_CAMERAS, KEY_ID, KEY_NAME + +from homeassistant.components.camera.const import DOMAIN as CAMERA_DOMAIN +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry +from homeassistant.const import CONF_SOURCE, CONF_URL +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import ( + CONF_ADMIN_PASSWORD, + CONF_ADMIN_USERNAME, + CONF_CLIENT, + CONF_CONFIG_ENTRY, + CONF_COORDINATOR, + CONF_SURVEILLANCE_PASSWORD, + CONF_SURVEILLANCE_USERNAME, + DEFAULT_SCAN_INTERVAL, + DOMAIN, + MOTIONEYE_MANUFACTURER, + SIGNAL_CAMERA_ADD, +) + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = [CAMERA_DOMAIN] + + +def create_motioneye_client( + *args: Any, + **kwargs: Any, +) -> MotionEyeClient: + """Create a MotionEyeClient.""" + return MotionEyeClient(*args, **kwargs) + + +def get_motioneye_device_identifier( + config_entry_id: str, camera_id: int +) -> tuple[str, str, int]: + """Get the identifiers for a motionEye device.""" + return (DOMAIN, config_entry_id, camera_id) + + +def get_motioneye_entity_unique_id( + config_entry_id: str, camera_id: int, entity_type: str +) -> str: + """Get the unique_id for a motionEye entity.""" + return f"{config_entry_id}_{camera_id}_{entity_type}" + + +def get_camera_from_cameras( + camera_id: int, data: dict[str, Any] +) -> dict[str, Any] | None: + """Get an individual camera dict from a multiple cameras data response.""" + for camera in data.get(KEY_CAMERAS) or []: + if camera.get(KEY_ID) == camera_id: + val: dict[str, Any] = camera + return val + return None + + +def is_acceptable_camera(camera: dict[str, Any] | None) -> bool: + """Determine if a camera dict is acceptable.""" + return bool(camera and KEY_ID in camera and KEY_NAME in camera) + + +@callback +def listen_for_new_cameras( + hass: HomeAssistant, + entry: ConfigEntry, + add_func: Callable, +) -> None: + """Listen for new cameras.""" + + entry.async_on_unload( + async_dispatcher_connect( + hass, + SIGNAL_CAMERA_ADD.format(entry.entry_id), + add_func, + ) + ) + + +async def _create_reauth_flow( + hass: HomeAssistant, + config_entry: ConfigEntry, +) -> None: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={ + CONF_SOURCE: SOURCE_REAUTH, + CONF_CONFIG_ENTRY: config_entry, + }, + data=config_entry.data, + ) + ) + + +@callback +def _add_camera( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + client: MotionEyeClient, + entry: ConfigEntry, + camera_id: int, + camera: dict[str, Any], + device_identifier: tuple[str, str, int], +) -> None: + """Add a motionEye camera to hass.""" + + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={device_identifier}, + manufacturer=MOTIONEYE_MANUFACTURER, + model=MOTIONEYE_MANUFACTURER, + name=camera[KEY_NAME], + ) + + async_dispatcher_send( + hass, + SIGNAL_CAMERA_ADD.format(entry.entry_id), + camera, + ) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up motionEye from a config entry.""" + hass.data.setdefault(DOMAIN, {}) + + client = create_motioneye_client( + entry.data[CONF_URL], + admin_username=entry.data.get(CONF_ADMIN_USERNAME), + admin_password=entry.data.get(CONF_ADMIN_PASSWORD), + surveillance_username=entry.data.get(CONF_SURVEILLANCE_USERNAME), + surveillance_password=entry.data.get(CONF_SURVEILLANCE_PASSWORD), + ) + + try: + await client.async_client_login() + except MotionEyeClientInvalidAuthError: + await client.async_client_close() + await _create_reauth_flow(hass, entry) + return False + except MotionEyeClientError as exc: + await client.async_client_close() + raise ConfigEntryNotReady from exc + + @callback + async def async_update_data() -> dict[str, Any] | None: + try: + return await client.async_get_cameras() + except MotionEyeClientError as exc: + raise UpdateFailed("Error communicating with API") from exc + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name=DOMAIN, + update_method=async_update_data, + update_interval=DEFAULT_SCAN_INTERVAL, + ) + hass.data[DOMAIN][entry.entry_id] = { + CONF_CLIENT: client, + CONF_COORDINATOR: coordinator, + } + + current_cameras: set[tuple[str, str, int]] = set() + device_registry = await dr.async_get_registry(hass) + + @callback + def _async_process_motioneye_cameras() -> None: + """Process motionEye camera additions and removals.""" + inbound_camera: set[tuple[str, str, int]] = set() + if KEY_CAMERAS not in coordinator.data: + return + + for camera in coordinator.data[KEY_CAMERAS]: + if not is_acceptable_camera(camera): + return + camera_id = camera[KEY_ID] + device_identifier = get_motioneye_device_identifier( + entry.entry_id, camera_id + ) + inbound_camera.add(device_identifier) + + if device_identifier in current_cameras: + continue + current_cameras.add(device_identifier) + _add_camera( + hass, + device_registry, + client, + entry, + camera_id, + camera, + device_identifier, + ) + + # Ensure every device associated with this config entry is still in the list of + # motionEye cameras, otherwise remove the device (and thus entities). + for device_entry in dr.async_entries_for_config_entry( + device_registry, entry.entry_id + ): + for identifier in device_entry.identifiers: + if identifier in inbound_camera: + break + else: + device_registry.async_remove_device(device_entry.id) + + async def setup_then_listen() -> None: + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_setup(entry, platform) + for platform in PLATFORMS + ] + ) + entry.async_on_unload( + coordinator.async_add_listener(_async_process_motioneye_cameras) + ) + await coordinator.async_refresh() + + hass.async_create_task(setup_then_listen()) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS + ] + ) + ) + if unload_ok: + config_data = hass.data[DOMAIN].pop(entry.entry_id) + await config_data[CONF_CLIENT].async_client_close() + + return unload_ok diff --git a/homeassistant/components/motioneye/camera.py b/homeassistant/components/motioneye/camera.py new file mode 100644 index 0000000000000..58df22198bfa9 --- /dev/null +++ b/homeassistant/components/motioneye/camera.py @@ -0,0 +1,208 @@ +"""The motionEye integration.""" +from __future__ import annotations + +import logging +from typing import Any, Callable + +import aiohttp +from motioneye_client.client import MotionEyeClient +from motioneye_client.const import ( + DEFAULT_SURVEILLANCE_USERNAME, + KEY_ID, + KEY_MOTION_DETECTION, + KEY_NAME, + KEY_STREAMING_AUTH_MODE, +) + +from homeassistant.components.mjpeg.camera import ( + CONF_MJPEG_URL, + CONF_STILL_IMAGE_URL, + CONF_VERIFY_SSL, + MjpegCamera, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_AUTHENTICATION, + CONF_NAME, + CONF_PASSWORD, + CONF_USERNAME, + HTTP_BASIC_AUTHENTICATION, + HTTP_DIGEST_AUTHENTICATION, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from . import ( + get_camera_from_cameras, + get_motioneye_device_identifier, + get_motioneye_entity_unique_id, + is_acceptable_camera, + listen_for_new_cameras, +) +from .const import ( + CONF_CLIENT, + CONF_COORDINATOR, + CONF_SURVEILLANCE_PASSWORD, + CONF_SURVEILLANCE_USERNAME, + DOMAIN, + MOTIONEYE_MANUFACTURER, + TYPE_MOTIONEYE_MJPEG_CAMERA, +) + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = ["camera"] + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable +) -> bool: + """Set up motionEye from a config entry.""" + entry_data = hass.data[DOMAIN][entry.entry_id] + + @callback + def camera_add(camera: dict[str, Any]) -> None: + """Add a new motionEye camera.""" + async_add_entities( + [ + MotionEyeMjpegCamera( + entry.entry_id, + entry.data.get( + CONF_SURVEILLANCE_USERNAME, DEFAULT_SURVEILLANCE_USERNAME + ), + entry.data.get(CONF_SURVEILLANCE_PASSWORD, ""), + camera, + entry_data[CONF_CLIENT], + entry_data[CONF_COORDINATOR], + ) + ] + ) + + listen_for_new_cameras(hass, entry, camera_add) + return True + + +class MotionEyeMjpegCamera(MjpegCamera, CoordinatorEntity): + """motionEye mjpeg camera.""" + + def __init__( + self, + config_entry_id: str, + username: str, + password: str, + camera: dict[str, Any], + client: MotionEyeClient, + coordinator: DataUpdateCoordinator, + ): + """Initialize a MJPEG camera.""" + self._surveillance_username = username + self._surveillance_password = password + self._client = client + self._camera_id = camera[KEY_ID] + self._device_identifier = get_motioneye_device_identifier( + config_entry_id, self._camera_id + ) + self._unique_id = get_motioneye_entity_unique_id( + config_entry_id, self._camera_id, TYPE_MOTIONEYE_MJPEG_CAMERA + ) + self._motion_detection_enabled: bool = camera.get(KEY_MOTION_DETECTION, False) + self._available = MotionEyeMjpegCamera._is_acceptable_streaming_camera(camera) + + # motionEye cameras are always streaming or unavailable. + self.is_streaming = True + + MjpegCamera.__init__( + self, + { + CONF_VERIFY_SSL: False, + **self._get_mjpeg_camera_properties_for_camera(camera), + }, + ) + CoordinatorEntity.__init__(self, coordinator) + + @callback + def _get_mjpeg_camera_properties_for_camera( + self, camera: dict[str, Any] + ) -> dict[str, Any]: + """Convert a motionEye camera to MjpegCamera internal properties.""" + auth = None + if camera.get(KEY_STREAMING_AUTH_MODE) in [ + HTTP_BASIC_AUTHENTICATION, + HTTP_DIGEST_AUTHENTICATION, + ]: + auth = camera[KEY_STREAMING_AUTH_MODE] + + return { + CONF_NAME: camera[KEY_NAME], + CONF_USERNAME: self._surveillance_username if auth is not None else None, + CONF_PASSWORD: self._surveillance_password if auth is not None else None, + CONF_MJPEG_URL: self._client.get_camera_stream_url(camera) or "", + CONF_STILL_IMAGE_URL: self._client.get_camera_snapshot_url(camera), + CONF_AUTHENTICATION: auth, + } + + @callback + def _set_mjpeg_camera_state_for_camera(self, camera: dict[str, Any]) -> None: + """Set the internal state to match the given camera.""" + + # Sets the state of the underlying (inherited) MjpegCamera based on the updated + # MotionEye camera dictionary. + properties = self._get_mjpeg_camera_properties_for_camera(camera) + self._name = properties[CONF_NAME] + self._username = properties[CONF_USERNAME] + self._password = properties[CONF_PASSWORD] + self._mjpeg_url = properties[CONF_MJPEG_URL] + self._still_image_url = properties[CONF_STILL_IMAGE_URL] + self._authentication = properties[CONF_AUTHENTICATION] + + if self._authentication == HTTP_BASIC_AUTHENTICATION: + self._auth = aiohttp.BasicAuth(self._username, password=self._password) + + @property + def unique_id(self) -> str: + """Return a unique id for this instance.""" + return self._unique_id + + @classmethod + def _is_acceptable_streaming_camera(cls, camera: dict[str, Any] | None) -> bool: + """Determine if a camera is streaming/usable.""" + return is_acceptable_camera(camera) and MotionEyeClient.is_camera_streaming( + camera + ) + + @property + def available(self) -> bool: + """Return if entity is available.""" + return self._available + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + available = False + if self.coordinator.last_update_success: + camera = get_camera_from_cameras(self._camera_id, self.coordinator.data) + if MotionEyeMjpegCamera._is_acceptable_streaming_camera(camera): + assert camera + self._set_mjpeg_camera_state_for_camera(camera) + self._motion_detection_enabled = camera.get(KEY_MOTION_DETECTION, False) + available = True + self._available = available + CoordinatorEntity._handle_coordinator_update(self) + + @property + def brand(self) -> str: + """Return the camera brand.""" + return MOTIONEYE_MANUFACTURER + + @property + def motion_detection_enabled(self) -> bool: + """Return the camera motion detection status.""" + return self._motion_detection_enabled + + @property + def device_info(self) -> dict[str, Any]: + """Return the device information.""" + return {"identifiers": {self._device_identifier}} diff --git a/homeassistant/components/motioneye/config_flow.py b/homeassistant/components/motioneye/config_flow.py new file mode 100644 index 0000000000000..45da759e91baa --- /dev/null +++ b/homeassistant/components/motioneye/config_flow.py @@ -0,0 +1,127 @@ +"""Config flow for motionEye integration.""" +from __future__ import annotations + +import logging +from typing import Any + +from motioneye_client.client import ( + MotionEyeClientConnectionError, + MotionEyeClientInvalidAuthError, + MotionEyeClientRequestError, +) +import voluptuous as vol + +from homeassistant.config_entries import ( + CONN_CLASS_LOCAL_POLL, + SOURCE_REAUTH, + ConfigFlow, +) +from homeassistant.const import CONF_SOURCE, CONF_URL +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.typing import ConfigType + +from . import create_motioneye_client +from .const import ( # pylint:disable=unused-import + CONF_ADMIN_PASSWORD, + CONF_ADMIN_USERNAME, + CONF_CONFIG_ENTRY, + CONF_SURVEILLANCE_PASSWORD, + CONF_SURVEILLANCE_USERNAME, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + + +class MotionEyeConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for motionEye.""" + + VERSION = 1 + CONNECTION_CLASS = CONN_CLASS_LOCAL_POLL + + async def async_step_user( + self, user_input: ConfigType | None = None + ) -> dict[str, Any]: + """Handle the initial step.""" + out: dict[str, Any] = {} + errors = {} + if user_input is None: + entry = self.context.get(CONF_CONFIG_ENTRY) + user_input = entry.data if entry else {} + else: + try: + # Cannot use cv.url validation in the schema itself, so + # apply extra validation here. + cv.url(user_input[CONF_URL]) + except vol.Invalid: + errors["base"] = "invalid_url" + else: + client = create_motioneye_client( + user_input[CONF_URL], + admin_username=user_input.get(CONF_ADMIN_USERNAME), + admin_password=user_input.get(CONF_ADMIN_PASSWORD), + surveillance_username=user_input.get(CONF_SURVEILLANCE_USERNAME), + surveillance_password=user_input.get(CONF_SURVEILLANCE_PASSWORD), + ) + + try: + await client.async_client_login() + except MotionEyeClientConnectionError: + errors["base"] = "cannot_connect" + except MotionEyeClientInvalidAuthError: + errors["base"] = "invalid_auth" + except MotionEyeClientRequestError: + errors["base"] = "unknown" + else: + entry = self.context.get(CONF_CONFIG_ENTRY) + if ( + self.context.get(CONF_SOURCE) == SOURCE_REAUTH + and entry is not None + ): + self.hass.config_entries.async_update_entry( + entry, data=user_input + ) + # Need to manually reload, as the listener won't have been + # installed because the initial load did not succeed (the reauth + # flow will not be initiated if the load succeeds). + await self.hass.config_entries.async_reload(entry.entry_id) + out = self.async_abort(reason="reauth_successful") + return out + + out = self.async_create_entry( + title=f"{user_input[CONF_URL]}", + data=user_input, + ) + return out + + out = self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_URL, default=user_input.get(CONF_URL, "")): str, + vol.Optional( + CONF_ADMIN_USERNAME, default=user_input.get(CONF_ADMIN_USERNAME) + ): str, + vol.Optional( + CONF_ADMIN_PASSWORD, default=user_input.get(CONF_ADMIN_PASSWORD) + ): str, + vol.Optional( + CONF_SURVEILLANCE_USERNAME, + default=user_input.get(CONF_SURVEILLANCE_USERNAME), + ): str, + vol.Optional( + CONF_SURVEILLANCE_PASSWORD, + default=user_input.get(CONF_SURVEILLANCE_PASSWORD), + ): str, + } + ), + errors=errors, + ) + return out + + async def async_step_reauth( + self, + config_data: ConfigType | None = None, + ) -> dict[str, Any]: + """Handle a reauthentication flow.""" + return await self.async_step_user(config_data) diff --git a/homeassistant/components/motioneye/const.py b/homeassistant/components/motioneye/const.py new file mode 100644 index 0000000000000..a76053b28541d --- /dev/null +++ b/homeassistant/components/motioneye/const.py @@ -0,0 +1,20 @@ +"""Constants for the motionEye integration.""" +from datetime import timedelta + +DOMAIN = "motioneye" + +CONF_CONFIG_ENTRY = "config_entry" +CONF_CLIENT = "client" +CONF_COORDINATOR = "coordinator" +CONF_ADMIN_PASSWORD = "admin_password" +CONF_ADMIN_USERNAME = "admin_username" +CONF_SURVEILLANCE_USERNAME = "surveillance_username" +CONF_SURVEILLANCE_PASSWORD = "surveillance_password" +DEFAULT_SCAN_INTERVAL = timedelta(seconds=30) + +MOTIONEYE_MANUFACTURER = "motionEye" + +SIGNAL_CAMERA_ADD = f"{DOMAIN}_camera_add_signal." "{}" +SIGNAL_CAMERA_REMOVE = f"{DOMAIN}_camera_remove_signal." "{}" + +TYPE_MOTIONEYE_MJPEG_CAMERA = "motioneye_mjpeg_camera" diff --git a/homeassistant/components/motioneye/manifest.json b/homeassistant/components/motioneye/manifest.json new file mode 100644 index 0000000000000..a4a1e028d5347 --- /dev/null +++ b/homeassistant/components/motioneye/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "motioneye", + "name": "motionEye", + "documentation": "https://www.home-assistant.io/integrations/motioneye", + "config_flow": true, + "requirements": [ + "motioneye-client==0.3.2" + ], + "codeowners": [ + "@dermotduffy" + ], + "iot_class": "local_polling" +} diff --git a/homeassistant/components/motioneye/strings.json b/homeassistant/components/motioneye/strings.json new file mode 100644 index 0000000000000..d365ba272ea43 --- /dev/null +++ b/homeassistant/components/motioneye/strings.json @@ -0,0 +1,25 @@ +{ + "config": { + "step": { + "user": { + "data": { + "url": "[%key:common::config_flow::data::url%]", + "admin_username": "Admin [%key:common::config_flow::data::username%]", + "admin_password": "Admin [%key:common::config_flow::data::password%]", + "surveillance_username": "Surveillance [%key:common::config_flow::data::username%]", + "surveillance_password": "Surveillance [%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]", + "invalid_url": "Invalid URL" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index f4bb23d698c2b..764ce9e594b8f 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -151,6 +151,7 @@ "mobile_app", "monoprice", "motion_blinds", + "motioneye", "mqtt", "mullvad", "myq", diff --git a/requirements_all.txt b/requirements_all.txt index 9d139759fcc6c..bfb926195acf1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -953,6 +953,9 @@ mitemp_bt==0.0.3 # homeassistant.components.motion_blinds motionblinds==0.4.10 +# homeassistant.components.motioneye +motioneye-client==0.3.2 + # homeassistant.components.mullvad mullvad-api==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 075d3be686fbd..50f0d3b70ca84 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -513,6 +513,9 @@ minio==4.0.9 # homeassistant.components.motion_blinds motionblinds==0.4.10 +# homeassistant.components.motioneye +motioneye-client==0.3.2 + # homeassistant.components.mullvad mullvad-api==1.0.0 diff --git a/tests/components/motioneye/__init__.py b/tests/components/motioneye/__init__.py new file mode 100644 index 0000000000000..a462d08303847 --- /dev/null +++ b/tests/components/motioneye/__init__.py @@ -0,0 +1,180 @@ +"""Tests for the motionEye integration.""" +from __future__ import annotations + +from typing import Any +from unittest.mock import AsyncMock, Mock, patch + +from motioneye_client.const import DEFAULT_PORT + +from homeassistant.components.motioneye.const import DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_URL +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +TEST_CONFIG_ENTRY_ID = "74565ad414754616000674c87bdc876c" +TEST_URL = f"http://test:{DEFAULT_PORT+1}" +TEST_CAMERA_ID = 100 +TEST_CAMERA_NAME = "Test Camera" +TEST_CAMERA_ENTITY_ID = "camera.test_camera" +TEST_CAMERA_DEVICE_IDENTIFIER = (DOMAIN, TEST_CONFIG_ENTRY_ID, TEST_CAMERA_ID) +TEST_CAMERA = { + "show_frame_changes": False, + "framerate": 25, + "actions": [], + "preserve_movies": 0, + "auto_threshold_tuning": True, + "recording_mode": "motion-triggered", + "monday_to": "", + "streaming_resolution": 100, + "light_switch_detect": 0, + "command_end_notifications_enabled": False, + "smb_shares": False, + "upload_server": "", + "monday_from": "", + "movie_passthrough": False, + "auto_brightness": False, + "frame_change_threshold": 3.0, + "name": TEST_CAMERA_NAME, + "movie_format": "mp4:h264_omx", + "network_username": "", + "preserve_pictures": 0, + "event_gap": 30, + "enabled": True, + "upload_movie": True, + "video_streaming": True, + "upload_location": "", + "max_movie_length": 0, + "movie_file_name": "%Y-%m-%d/%H-%M-%S", + "upload_authorization_key": "", + "still_images": False, + "upload_method": "post", + "max_frame_change_threshold": 0, + "device_url": "rtsp://localhost/live", + "text_overlay": False, + "right_text": "timestamp", + "upload_picture": True, + "email_notifications_enabled": False, + "working_schedule_type": "during", + "movie_quality": 75, + "disk_total": 44527655808, + "upload_service": "ftp", + "upload_password": "", + "wednesday_to": "", + "mask_type": "smart", + "command_storage_enabled": False, + "disk_used": 11419704992, + "streaming_motion": 0, + "manual_snapshots": True, + "noise_level": 12, + "mask_lines": [], + "upload_enabled": False, + "root_directory": f"/var/lib/motioneye/{TEST_CAMERA_NAME}", + "clean_cloud_enabled": False, + "working_schedule": False, + "pre_capture": 1, + "command_notifications_enabled": False, + "streaming_framerate": 25, + "email_notifications_picture_time_span": 0, + "thursday_to": "", + "streaming_server_resize": False, + "upload_subfolders": True, + "sunday_to": "", + "left_text": "", + "image_file_name": "%Y-%m-%d/%H-%M-%S", + "rotation": 0, + "capture_mode": "manual", + "movies": False, + "motion_detection": True, + "text_scale": 1, + "upload_username": "", + "upload_port": "", + "available_disks": [], + "network_smb_ver": "1.0", + "streaming_auth_mode": "basic", + "despeckle_filter": "", + "snapshot_interval": 0, + "minimum_motion_frames": 20, + "auto_noise_detect": True, + "network_share_name": "", + "sunday_from": "", + "friday_from": "", + "web_hook_storage_enabled": False, + "custom_left_text": "", + "streaming_port": 8081, + "id": TEST_CAMERA_ID, + "post_capture": 1, + "streaming_quality": 75, + "wednesday_from": "", + "proto": "netcam", + "extra_options": [], + "image_quality": 85, + "create_debug_media": False, + "friday_to": "", + "custom_right_text": "", + "web_hook_notifications_enabled": False, + "saturday_from": "", + "available_resolutions": [ + "1600x1200", + "1920x1080", + ], + "tuesday_from": "", + "network_password": "", + "saturday_to": "", + "network_server": "", + "smart_mask_sluggishness": 5, + "mask": False, + "tuesday_to": "", + "thursday_from": "", + "storage_device": "custom-path", + "resolution": "1920x1080", +} +TEST_CAMERAS = {"cameras": [TEST_CAMERA]} +TEST_SURVEILLANCE_USERNAME = "surveillance_username" + + +def create_mock_motioneye_client() -> AsyncMock: + """Create mock motionEye client.""" + mock_client = AsyncMock() + mock_client.async_client_login = AsyncMock(return_value={}) + mock_client.async_get_cameras = AsyncMock(return_value=TEST_CAMERAS) + mock_client.async_client_close = AsyncMock(return_value=True) + mock_client.get_camera_snapshot_url = Mock(return_value="") + mock_client.get_camera_stream_url = Mock(return_value="") + return mock_client + + +def create_mock_motioneye_config_entry( + hass: HomeAssistant, + data: dict[str, Any] | None = None, + options: dict[str, Any] | None = None, +) -> ConfigEntry: + """Add a test config entry.""" + config_entry: MockConfigEntry = MockConfigEntry( # type: ignore[no-untyped-call] + entry_id=TEST_CONFIG_ENTRY_ID, + domain=DOMAIN, + data=data or {CONF_URL: TEST_URL}, + title=f"{TEST_URL}", + options=options or {}, + ) + config_entry.add_to_hass(hass) # type: ignore[no-untyped-call] + return config_entry + + +async def setup_mock_motioneye_config_entry( + hass: HomeAssistant, + config_entry: ConfigEntry | None = None, + client: Mock | None = None, +) -> ConfigEntry: + """Add a mock MotionEye config entry to hass.""" + config_entry = config_entry or create_mock_motioneye_config_entry(hass) + client = client or create_mock_motioneye_client() + + with patch( + "homeassistant.components.motioneye.MotionEyeClient", + return_value=client, + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + return config_entry diff --git a/tests/components/motioneye/test_camera.py b/tests/components/motioneye/test_camera.py new file mode 100644 index 0000000000000..921dc9df9203d --- /dev/null +++ b/tests/components/motioneye/test_camera.py @@ -0,0 +1,315 @@ +"""Test the motionEye camera.""" +import copy +import logging +from typing import Any +from unittest.mock import AsyncMock, Mock + +from aiohttp import web # type: ignore +from aiohttp.web_exceptions import HTTPBadGateway +from motioneye_client.client import ( + MotionEyeClientError, + MotionEyeClientInvalidAuthError, +) +from motioneye_client.const import ( + KEY_CAMERAS, + KEY_MOTION_DETECTION, + KEY_NAME, + KEY_VIDEO_STREAMING, +) +import pytest + +from homeassistant.components.camera import async_get_image, async_get_mjpeg_stream +from homeassistant.components.motioneye import get_motioneye_device_identifier +from homeassistant.components.motioneye.const import ( + CONF_SURVEILLANCE_USERNAME, + DEFAULT_SCAN_INTERVAL, + DOMAIN, + MOTIONEYE_MANUFACTURER, +) +from homeassistant.const import CONF_URL +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.device_registry import async_get_registry +import homeassistant.util.dt as dt_util + +from . import ( + TEST_CAMERA_DEVICE_IDENTIFIER, + TEST_CAMERA_ENTITY_ID, + TEST_CAMERA_ID, + TEST_CAMERA_NAME, + TEST_CAMERAS, + TEST_CONFIG_ENTRY_ID, + TEST_SURVEILLANCE_USERNAME, + create_mock_motioneye_client, + create_mock_motioneye_config_entry, + setup_mock_motioneye_config_entry, +) + +from tests.common import async_fire_time_changed + +_LOGGER = logging.getLogger(__name__) + + +async def test_setup_camera(hass: HomeAssistant) -> None: + """Test a basic camera.""" + client = create_mock_motioneye_client() + await setup_mock_motioneye_config_entry(hass, client=client) + + entity_state = hass.states.get(TEST_CAMERA_ENTITY_ID) + assert entity_state + assert entity_state.state == "idle" + assert entity_state.attributes.get("friendly_name") == TEST_CAMERA_NAME + + +async def test_setup_camera_auth_fail(hass: HomeAssistant) -> None: + """Test a successful camera.""" + client = create_mock_motioneye_client() + client.async_client_login = AsyncMock(side_effect=MotionEyeClientInvalidAuthError) + await setup_mock_motioneye_config_entry(hass, client=client) + assert not hass.states.get(TEST_CAMERA_ENTITY_ID) + + +async def test_setup_camera_client_error(hass: HomeAssistant) -> None: + """Test a successful camera.""" + client = create_mock_motioneye_client() + client.async_client_login = AsyncMock(side_effect=MotionEyeClientError) + await setup_mock_motioneye_config_entry(hass, client=client) + assert not hass.states.get(TEST_CAMERA_ENTITY_ID) + + +async def test_setup_camera_empty_data(hass: HomeAssistant) -> None: + """Test a successful camera.""" + client = create_mock_motioneye_client() + client.async_get_cameras = AsyncMock(return_value={}) + await setup_mock_motioneye_config_entry(hass, client=client) + assert not hass.states.get(TEST_CAMERA_ENTITY_ID) + + +async def test_setup_camera_bad_data(hass: HomeAssistant) -> None: + """Test bad camera data.""" + client = create_mock_motioneye_client() + cameras = copy.deepcopy(TEST_CAMERAS) + del cameras[KEY_CAMERAS][0][KEY_NAME] + + client.async_get_cameras = AsyncMock(return_value=cameras) + await setup_mock_motioneye_config_entry(hass, client=client) + assert not hass.states.get(TEST_CAMERA_ENTITY_ID) + + +async def test_setup_camera_without_streaming(hass: HomeAssistant) -> None: + """Test a camera without streaming enabled.""" + client = create_mock_motioneye_client() + cameras = copy.deepcopy(TEST_CAMERAS) + cameras[KEY_CAMERAS][0][KEY_VIDEO_STREAMING] = False + + client.async_get_cameras = AsyncMock(return_value=cameras) + await setup_mock_motioneye_config_entry(hass, client=client) + entity_state = hass.states.get(TEST_CAMERA_ENTITY_ID) + assert entity_state + assert entity_state.state == "unavailable" + + +async def test_setup_camera_new_data_same(hass: HomeAssistant) -> None: + """Test a data refresh with the same data.""" + client = create_mock_motioneye_client() + await setup_mock_motioneye_config_entry(hass, client=client) + async_fire_time_changed(hass, dt_util.utcnow() + DEFAULT_SCAN_INTERVAL) + await hass.async_block_till_done() + assert hass.states.get(TEST_CAMERA_ENTITY_ID) + + +async def test_setup_camera_new_data_camera_removed(hass: HomeAssistant) -> None: + """Test a data refresh with a removed camera.""" + device_registry = await async_get_registry(hass) + entity_registry = await er.async_get_registry(hass) + + client = create_mock_motioneye_client() + config_entry = await setup_mock_motioneye_config_entry(hass, client=client) + + # Create some random old devices/entity_ids and ensure they get cleaned up. + old_device_id = "old-device-id" + old_entity_unique_id = "old-entity-unique_id" + old_device = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, identifiers={(DOMAIN, old_device_id)} + ) + entity_registry.async_get_or_create( + domain=DOMAIN, + platform="camera", + unique_id=old_entity_unique_id, + config_entry=config_entry, + device_id=old_device.id, + ) + + await hass.async_block_till_done() + assert hass.states.get(TEST_CAMERA_ENTITY_ID) + assert device_registry.async_get_device({TEST_CAMERA_DEVICE_IDENTIFIER}) + + client.async_get_cameras = AsyncMock(return_value={KEY_CAMERAS: []}) + async_fire_time_changed(hass, dt_util.utcnow() + DEFAULT_SCAN_INTERVAL) + await hass.async_block_till_done() + assert not hass.states.get(TEST_CAMERA_ENTITY_ID) + assert not device_registry.async_get_device({TEST_CAMERA_DEVICE_IDENTIFIER}) + assert not device_registry.async_get_device({(DOMAIN, old_device_id)}) + assert not entity_registry.async_get_entity_id( + DOMAIN, "camera", old_entity_unique_id + ) + + +async def test_setup_camera_new_data_error(hass: HomeAssistant) -> None: + """Test a data refresh that fails.""" + client = create_mock_motioneye_client() + await setup_mock_motioneye_config_entry(hass, client=client) + assert hass.states.get(TEST_CAMERA_ENTITY_ID) + client.async_get_cameras = AsyncMock(side_effect=MotionEyeClientError) + async_fire_time_changed(hass, dt_util.utcnow() + DEFAULT_SCAN_INTERVAL) + await hass.async_block_till_done() + entity_state = hass.states.get(TEST_CAMERA_ENTITY_ID) + assert entity_state.state == "unavailable" + + +async def test_setup_camera_new_data_without_streaming(hass: HomeAssistant) -> None: + """Test a data refresh without streaming.""" + client = create_mock_motioneye_client() + await setup_mock_motioneye_config_entry(hass, client=client) + entity_state = hass.states.get(TEST_CAMERA_ENTITY_ID) + assert entity_state.state == "idle" + + cameras = copy.deepcopy(TEST_CAMERAS) + cameras[KEY_CAMERAS][0][KEY_VIDEO_STREAMING] = False + client.async_get_cameras = AsyncMock(return_value=cameras) + async_fire_time_changed(hass, dt_util.utcnow() + DEFAULT_SCAN_INTERVAL) + await hass.async_block_till_done() + entity_state = hass.states.get(TEST_CAMERA_ENTITY_ID) + assert entity_state.state == "unavailable" + + +async def test_unload_camera(hass: HomeAssistant) -> None: + """Test unloading camera.""" + client = create_mock_motioneye_client() + entry = await setup_mock_motioneye_config_entry(hass, client=client) + assert hass.states.get(TEST_CAMERA_ENTITY_ID) + assert not client.async_client_close.called + await hass.config_entries.async_unload(entry.entry_id) + assert client.async_client_close.called + + +async def test_get_still_image_from_camera( + aiohttp_server: Any, hass: HomeAssistant +) -> None: + """Test getting a still image.""" + + image_handler = Mock(return_value="") + + app = web.Application() + app.add_routes( + [ + web.get( + "/foo", + image_handler, + ) + ] + ) + + server = await aiohttp_server(app) + client = create_mock_motioneye_client() + client.get_camera_snapshot_url = Mock( + return_value=f"http://localhost:{server.port}/foo" + ) + config_entry = create_mock_motioneye_config_entry( + hass, + data={ + CONF_URL: f"http://localhost:{server.port}", + CONF_SURVEILLANCE_USERNAME: TEST_SURVEILLANCE_USERNAME, + }, + ) + + await setup_mock_motioneye_config_entry( + hass, config_entry=config_entry, client=client + ) + await hass.async_block_till_done() + + # It won't actually get a stream from the dummy handler, so just catch + # the expected exception, then verify the right handler was called. + with pytest.raises(HomeAssistantError): + await async_get_image(hass, TEST_CAMERA_ENTITY_ID, timeout=None) # type: ignore[no-untyped-call] + assert image_handler.called + + +async def test_get_stream_from_camera(aiohttp_server: Any, hass: HomeAssistant) -> None: + """Test getting a stream.""" + + stream_handler = Mock(return_value="") + app = web.Application() + app.add_routes([web.get("/", stream_handler)]) + stream_server = await aiohttp_server(app) + + client = create_mock_motioneye_client() + client.get_camera_stream_url = Mock( + return_value=f"http://localhost:{stream_server.port}/" + ) + config_entry = create_mock_motioneye_config_entry( + hass, + data={ + CONF_URL: f"http://localhost:{stream_server.port}", + # The port won't be used as the client is a mock. + CONF_SURVEILLANCE_USERNAME: TEST_SURVEILLANCE_USERNAME, + }, + ) + cameras = copy.deepcopy(TEST_CAMERAS) + client.async_get_cameras = AsyncMock(return_value=cameras) + await setup_mock_motioneye_config_entry( + hass, config_entry=config_entry, client=client + ) + await hass.async_block_till_done() + + # It won't actually get a stream from the dummy handler, so just catch + # the expected exception, then verify the right handler was called. + with pytest.raises(HTTPBadGateway): + await async_get_mjpeg_stream(hass, None, TEST_CAMERA_ENTITY_ID) # type: ignore[no-untyped-call] + assert stream_handler.called + + +async def test_state_attributes(hass: HomeAssistant) -> None: + """Test state attributes are set correctly.""" + client = create_mock_motioneye_client() + await setup_mock_motioneye_config_entry(hass, client=client) + + entity_state = hass.states.get(TEST_CAMERA_ENTITY_ID) + assert entity_state + assert entity_state.attributes.get("brand") == MOTIONEYE_MANUFACTURER + assert entity_state.attributes.get("motion_detection") + + cameras = copy.deepcopy(TEST_CAMERAS) + cameras[KEY_CAMERAS][0][KEY_MOTION_DETECTION] = False + client.async_get_cameras = AsyncMock(return_value=cameras) + async_fire_time_changed(hass, dt_util.utcnow() + DEFAULT_SCAN_INTERVAL) + await hass.async_block_till_done() + + entity_state = hass.states.get(TEST_CAMERA_ENTITY_ID) + assert entity_state + assert not entity_state.attributes.get("motion_detection") + + +async def test_device_info(hass: HomeAssistant) -> None: + """Verify device information includes expected details.""" + client = create_mock_motioneye_client() + entry = await setup_mock_motioneye_config_entry(hass, client=client) + + device_identifier = get_motioneye_device_identifier(entry.entry_id, TEST_CAMERA_ID) + device_registry = dr.async_get(hass) + + device = device_registry.async_get_device({device_identifier}) + assert device + assert device.config_entries == {TEST_CONFIG_ENTRY_ID} + assert device.identifiers == {device_identifier} + assert device.manufacturer == MOTIONEYE_MANUFACTURER + assert device.model == MOTIONEYE_MANUFACTURER + assert device.name == TEST_CAMERA_NAME + + entity_registry = await er.async_get_registry(hass) + entities_from_device = [ + entry.entity_id + for entry in er.async_entries_for_device(entity_registry, device.id) + ] + assert TEST_CAMERA_ENTITY_ID in entities_from_device diff --git a/tests/components/motioneye/test_config_flow.py b/tests/components/motioneye/test_config_flow.py new file mode 100644 index 0000000000000..2c16aea14be7c --- /dev/null +++ b/tests/components/motioneye/test_config_flow.py @@ -0,0 +1,233 @@ +"""Test the motionEye config flow.""" +import logging +from unittest.mock import AsyncMock, patch + +from motioneye_client.client import ( + MotionEyeClientConnectionError, + MotionEyeClientInvalidAuthError, + MotionEyeClientRequestError, +) + +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.motioneye.const import ( + CONF_ADMIN_PASSWORD, + CONF_ADMIN_USERNAME, + CONF_CONFIG_ENTRY, + CONF_SURVEILLANCE_PASSWORD, + CONF_SURVEILLANCE_USERNAME, + DOMAIN, +) +from homeassistant.const import CONF_URL +from homeassistant.core import HomeAssistant + +from . import TEST_URL, create_mock_motioneye_client, create_mock_motioneye_config_entry + +_LOGGER = logging.getLogger(__name__) + + +async def test_user_success(hass: HomeAssistant) -> None: + """Test successful user flow.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + mock_client = create_mock_motioneye_client() + + with patch( + "homeassistant.components.motioneye.MotionEyeClient", + return_value=mock_client, + ), patch( + "homeassistant.components.motioneye.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_URL: TEST_URL, + CONF_ADMIN_USERNAME: "admin-username", + CONF_ADMIN_PASSWORD: "admin-password", + CONF_SURVEILLANCE_USERNAME: "surveillance-username", + CONF_SURVEILLANCE_PASSWORD: "surveillance-password", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == "create_entry" + assert result["title"] == f"{TEST_URL}" + assert result["data"] == { + CONF_URL: TEST_URL, + CONF_ADMIN_USERNAME: "admin-username", + CONF_ADMIN_PASSWORD: "admin-password", + CONF_SURVEILLANCE_USERNAME: "surveillance-username", + CONF_SURVEILLANCE_PASSWORD: "surveillance-password", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_user_invalid_auth(hass: HomeAssistant) -> None: + """Test invalid auth is handled correctly.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + mock_client = create_mock_motioneye_client() + mock_client.async_client_login = AsyncMock( + side_effect=MotionEyeClientInvalidAuthError + ) + + with patch( + "homeassistant.components.motioneye.MotionEyeClient", + return_value=mock_client, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_URL: TEST_URL, + CONF_ADMIN_USERNAME: "admin-username", + CONF_ADMIN_PASSWORD: "admin-password", + CONF_SURVEILLANCE_USERNAME: "surveillance-username", + CONF_SURVEILLANCE_PASSWORD: "surveillance-password", + }, + ) + await mock_client.async_client_close() + + assert result["type"] == "form" + assert result["errors"] == {"base": "invalid_auth"} + + +async def test_user_invalid_url(hass: HomeAssistant) -> None: + """Test invalid url is handled correctly.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.motioneye.MotionEyeClient", + return_value=create_mock_motioneye_client(), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_URL: "not a url", + CONF_ADMIN_USERNAME: "admin-username", + CONF_ADMIN_PASSWORD: "admin-password", + CONF_SURVEILLANCE_USERNAME: "surveillance-username", + CONF_SURVEILLANCE_PASSWORD: "surveillance-password", + }, + ) + + assert result["type"] == "form" + assert result["errors"] == {"base": "invalid_url"} + + +async def test_user_cannot_connect(hass: HomeAssistant) -> None: + """Test connection failure is handled correctly.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + mock_client = create_mock_motioneye_client() + mock_client.async_client_login = AsyncMock( + side_effect=MotionEyeClientConnectionError, + ) + + with patch( + "homeassistant.components.motioneye.MotionEyeClient", + return_value=mock_client, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_URL: TEST_URL, + CONF_ADMIN_USERNAME: "admin-username", + CONF_ADMIN_PASSWORD: "admin-password", + CONF_SURVEILLANCE_USERNAME: "surveillance-username", + CONF_SURVEILLANCE_PASSWORD: "surveillance-password", + }, + ) + await mock_client.async_client_close() + + assert result["type"] == "form" + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_user_request_error(hass: HomeAssistant) -> None: + """Test a request error is handled correctly.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + mock_client = create_mock_motioneye_client() + mock_client.async_client_login = AsyncMock(side_effect=MotionEyeClientRequestError) + + with patch( + "homeassistant.components.motioneye.MotionEyeClient", + return_value=mock_client, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_URL: TEST_URL, + CONF_ADMIN_USERNAME: "admin-username", + CONF_ADMIN_PASSWORD: "admin-password", + CONF_SURVEILLANCE_USERNAME: "surveillance-username", + CONF_SURVEILLANCE_PASSWORD: "surveillance-password", + }, + ) + await mock_client.async_client_close() + + assert result["type"] == "form" + assert result["errors"] == {"base": "unknown"} + + +async def test_reauth(hass: HomeAssistant) -> None: + """Test a reauth.""" + config_data = { + CONF_URL: TEST_URL, + } + + config_entry = create_mock_motioneye_config_entry(hass, data=config_data) + + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + CONF_CONFIG_ENTRY: config_entry, + }, + ) + assert result["type"] == "form" + assert result["errors"] == {} + + mock_client = create_mock_motioneye_client() + + new_data = { + CONF_URL: TEST_URL, + CONF_ADMIN_USERNAME: "admin-username", + CONF_ADMIN_PASSWORD: "admin-password", + CONF_SURVEILLANCE_USERNAME: "surveillance-username", + CONF_SURVEILLANCE_PASSWORD: "surveillance-password", + } + + with patch( + "homeassistant.components.motioneye.MotionEyeClient", + return_value=mock_client, + ), patch( + "homeassistant.components.motioneye.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + new_data, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "reauth_successful" + assert config_entry.data == new_data + + assert len(mock_setup_entry.mock_calls) == 1 From 9a7d500b80bb397fcbbbef9f62bffe176ad3062b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 24 Apr 2021 02:13:25 -1000 Subject: [PATCH 0491/1317] Cancel august interval track at stop event (#49198) --- homeassistant/components/august/activity.py | 1 - homeassistant/components/august/subscriber.py | 35 +++++++++++++++---- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/august/activity.py b/homeassistant/components/august/activity.py index 402852013f8aa..402a2ccd610b6 100644 --- a/homeassistant/components/august/activity.py +++ b/homeassistant/components/august/activity.py @@ -31,7 +31,6 @@ def __init__(self, hass, api, august_gateway, house_ids, pubnub): self._house_ids = house_ids self._latest_activities = {} self._last_update_time = None - self._abort_async_track_time_interval = None self.pubnub = pubnub self._update_debounce = {} diff --git a/homeassistant/components/august/subscriber.py b/homeassistant/components/august/subscriber.py index 3a7edd8a3429f..5223b8b4a388f 100644 --- a/homeassistant/components/august/subscriber.py +++ b/homeassistant/components/august/subscriber.py @@ -1,6 +1,7 @@ """Base class for August entity.""" +from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback from homeassistant.helpers.event import async_track_time_interval @@ -15,6 +16,7 @@ def __init__(self, hass, update_interval): self._update_interval = update_interval self._subscriptions = {} self._unsub_interval = None + self._stop_interval = None @callback def async_subscribe_device_id(self, device_id, update_callback): @@ -23,9 +25,8 @@ def async_subscribe_device_id(self, device_id, update_callback): Returns a callable that can be used to unsubscribe. """ if not self._subscriptions: - self._unsub_interval = async_track_time_interval( - self._hass, self._async_refresh, self._update_interval - ) + self._async_setup_listeners() + self._subscriptions.setdefault(device_id, []).append(update_callback) def _unsubscribe(): @@ -33,15 +34,37 @@ def _unsubscribe(): return _unsubscribe + @callback + def _async_setup_listeners(self): + """Create interval and stop listeners.""" + self._unsub_interval = async_track_time_interval( + self._hass, self._async_refresh, self._update_interval + ) + + @callback + def _async_cancel_update_interval(_): + self._stop_interval = None + self._unsub_interval() + + self._stop_interval = self._hass.bus.async_listen( + EVENT_HOMEASSISTANT_STOP, _async_cancel_update_interval + ) + @callback def async_unsubscribe_device_id(self, device_id, update_callback): """Remove a callback subscriber.""" self._subscriptions[device_id].remove(update_callback) if not self._subscriptions[device_id]: del self._subscriptions[device_id] - if not self._subscriptions: - self._unsub_interval() - self._unsub_interval = None + + if self._subscriptions: + return + + self._unsub_interval() + self._unsub_interval = None + if self._stop_interval: + self._stop_interval() + self._stop_interval = None @callback def async_signal_device_id_update(self, device_id): From 671148b6ca699bc1bc5f4f94f8bae1508aaf0b4b Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sat, 24 Apr 2021 14:18:14 +0200 Subject: [PATCH 0492/1317] Update xknx to version 0.18.1 (#49609) --- homeassistant/components/knx/__init__.py | 9 ++++---- homeassistant/components/knx/manifest.json | 2 +- homeassistant/components/knx/schema.py | 26 +++++++++++++++++----- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 28 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 5caa284cc48f7..a8a923dc00aed 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -15,7 +15,8 @@ ConnectionConfig, ConnectionType, ) -from xknx.telegram import AddressFilter, GroupAddress, Telegram +from xknx.telegram import AddressFilter, Telegram +from xknx.telegram.address import parse_device_group_address from xknx.telegram.apci import GroupValueRead, GroupValueResponse, GroupValueWrite from homeassistant.const import ( @@ -412,7 +413,7 @@ def register_callback(self) -> TelegramQueue.Callback: async def service_event_register_modify(self, call: ServiceCall) -> None: """Service for adding or removing a GroupAddress to the knx_event filter.""" attr_address = call.data[KNX_ADDRESS] - group_addresses = map(GroupAddress, attr_address) + group_addresses = map(parse_device_group_address, attr_address) if call.data.get(SERVICE_KNX_ATTR_REMOVE): for group_address in group_addresses: @@ -483,7 +484,7 @@ async def service_send_to_knx_bus(self, call: ServiceCall) -> None: for address in attr_address: telegram = Telegram( - destination_address=GroupAddress(address), + destination_address=parse_device_group_address(address), payload=GroupValueWrite(payload), ) await self.xknx.telegrams.put(telegram) @@ -492,7 +493,7 @@ async def service_read_to_knx_bus(self, call: ServiceCall) -> None: """Service for sending a GroupValueRead telegram to the KNX bus.""" for address in call.data[KNX_ADDRESS]: telegram = Telegram( - destination_address=GroupAddress(address), + destination_address=parse_device_group_address(address), payload=GroupValueRead(), ) await self.xknx.telegrams.put(telegram) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 5f8711141e38b..bcca5855bf106 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -2,7 +2,7 @@ "domain": "knx", "name": "KNX", "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==0.18.0"], + "requirements": ["xknx==0.18.1"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "silver", "iot_class": "local_push" diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index fb4b29fbd7053..dc5a09534ecdd 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -1,8 +1,13 @@ """Voluptuous schemas for the KNX integration.""" +from __future__ import annotations + +from typing import Any + import voluptuous as vol from xknx.devices.climate import SetpointShiftMode +from xknx.exceptions import CouldNotParseAddress from xknx.io import DEFAULT_MCAST_PORT -from xknx.telegram.address import GroupAddress, IndividualAddress +from xknx.telegram.address import IndividualAddress, parse_device_group_address from homeassistant.const import ( CONF_DEVICE_CLASS, @@ -29,11 +34,20 @@ # KNX VALIDATORS ################## -ga_validator = vol.Any( - cv.matches_regex(GroupAddress.ADDRESS_RE.pattern), - vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)), - msg="value does not match pattern for KNX group address '
//', '
/' or '' (eg.'1/2/3', '9/234', '123')", -) + +def ga_validator(value: Any) -> str | int: + """Validate that value is parsable as GroupAddress or InternalGroupAddress.""" + if isinstance(value, (str, int)): + try: + parse_device_group_address(value) + return value + except CouldNotParseAddress: + pass + raise vol.Invalid( + f"value '{value}' is not a valid KNX group address '
//', '
/' or '' (eg.'1/2/3', '9/234', '123'), nor xknx internal address 'i-'." + ) + + ga_list_validator = vol.All(cv.ensure_list, [ga_validator]) ia_validator = vol.Any( diff --git a/requirements_all.txt b/requirements_all.txt index bfb926195acf1..81af5e09aa333 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2353,7 +2353,7 @@ xbox-webapi==2.0.8 xboxapi==2.0.1 # homeassistant.components.knx -xknx==0.18.0 +xknx==0.18.1 # homeassistant.components.bluesound # homeassistant.components.rest diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 50f0d3b70ca84..e74d0517ccb78 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1244,7 +1244,7 @@ wolf_smartset==0.1.8 xbox-webapi==2.0.8 # homeassistant.components.knx -xknx==0.18.0 +xknx==0.18.1 # homeassistant.components.bluesound # homeassistant.components.rest From b0fecdcc3d8a5b72aab2c8d83c4124fcb3192a3f Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 24 Apr 2021 15:46:16 +0200 Subject: [PATCH 0493/1317] Add entity service for deCONZ alarm control panel to control states used to help guide user transition between primary states (#49606) --- .../components/deconz/alarm_control_panel.py | 44 +++++++++ homeassistant/components/deconz/services.yaml | 26 +++++ .../deconz/test_alarm_control_panel.py | 99 +++++++++++++++++++ 3 files changed, 169 insertions(+) diff --git a/homeassistant/components/deconz/alarm_control_panel.py b/homeassistant/components/deconz/alarm_control_panel.py index 7a6ed19bcd676..ce8467c736aeb 100644 --- a/homeassistant/components/deconz/alarm_control_panel.py +++ b/homeassistant/components/deconz/alarm_control_panel.py @@ -8,6 +8,7 @@ ANCILLARY_CONTROL_DISARMED, AncillaryControl, ) +import voluptuous as vol from homeassistant.components.alarm_control_panel import ( DOMAIN, @@ -24,12 +25,35 @@ STATE_UNKNOWN, ) from homeassistant.core import callback +from homeassistant.helpers import entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import NEW_SENSOR from .deconz_device import DeconzDevice from .gateway import get_gateway_from_config_entry +PANEL_ARMING_AWAY = "arming_away" +PANEL_ARMING_HOME = "arming_home" +PANEL_ARMING_NIGHT = "arming_night" +PANEL_ENTRY_DELAY = "entry_delay" +PANEL_EXIT_DELAY = "exit_delay" +PANEL_NOT_READY_TO_ARM = "not_ready_to_arm" + +SERVICE_ALARM_PANEL_STATE = "alarm_panel_state" +CONF_ALARM_PANEL_STATE = "panel_state" +SERVICE_ALARM_PANEL_STATE_SCHEMA = { + vol.Required(CONF_ALARM_PANEL_STATE): vol.In( + [ + PANEL_ARMING_AWAY, + PANEL_ARMING_HOME, + PANEL_ARMING_NIGHT, + PANEL_ENTRY_DELAY, + PANEL_EXIT_DELAY, + PANEL_NOT_READY_TO_ARM, + ] + ) +} + DECONZ_TO_ALARM_STATE = { ANCILLARY_CONTROL_ARMED_AWAY: STATE_ALARM_ARMED_AWAY, ANCILLARY_CONTROL_ARMED_NIGHT: STATE_ALARM_ARMED_NIGHT, @@ -46,6 +70,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities) -> None: gateway = get_gateway_from_config_entry(hass, config_entry) gateway.entities[DOMAIN] = set() + platform = entity_platform.current_platform.get() + @callback def async_add_alarm_control_panel(sensors=gateway.api.sensors.values()) -> None: """Add alarm control panel devices from deCONZ.""" @@ -60,6 +86,11 @@ def async_add_alarm_control_panel(sensors=gateway.api.sensors.values()) -> None: entities.append(DeconzAlarmControlPanel(sensor, gateway)) if entities: + platform.async_register_entity_service( + SERVICE_ALARM_PANEL_STATE, + SERVICE_ALARM_PANEL_STATE_SCHEMA, + "async_set_panel_state", + ) async_add_entities(entities) config_entry.async_on_unload( @@ -86,6 +117,15 @@ def __init__(self, device, gateway) -> None: self._features |= SUPPORT_ALARM_ARM_HOME self._features |= SUPPORT_ALARM_ARM_NIGHT + self._service_to_device_panel_command = { + PANEL_ARMING_AWAY: self._device.arming_away, + PANEL_ARMING_HOME: self._device.arming_stay, + PANEL_ARMING_NIGHT: self._device.arming_night, + PANEL_ENTRY_DELAY: self._device.entry_delay, + PANEL_EXIT_DELAY: self._device.exit_delay, + PANEL_NOT_READY_TO_ARM: self._device.not_ready_to_arm, + } + @property def supported_features(self) -> int: """Return the list of supported features.""" @@ -131,3 +171,7 @@ async def async_alarm_arm_night(self, code: None = None) -> None: async def async_alarm_disarm(self, code: None = None) -> None: """Send disarm command.""" await self._device.disarm() + + async def async_set_panel_state(self, panel_state: str) -> None: + """Send panel_state command.""" + await self._service_to_device_panel_command[panel_state]() diff --git a/homeassistant/components/deconz/services.yaml b/homeassistant/components/deconz/services.yaml index 3bce097f7d391..684e86e223c34 100644 --- a/homeassistant/components/deconz/services.yaml +++ b/homeassistant/components/deconz/services.yaml @@ -64,3 +64,29 @@ remove_orphaned_entries: It can be found as part of the integration name. Useful if you run multiple deCONZ integrations. example: "00212EFFFF012345" + +alarm_panel_state: + name: Alarm panel state + description: Put keypad panel in an intermediate state, to help with visual and audible cues to the user. + target: + entity: + integration: deconz + domain: alarm_control_panel + fields: + panel_state: + name: Panel state + description: >- + - "arming_away/home/night": set panel in right visual arming state. + - "entry_delay": make panel beep until panel is disarmed. Beep interval is short. + - "exit_delay": make panel beep until panel is set to armed state. Beep interval is long. + - "not_ready_to_arm": turn on yellow status led on the panel. Indicate not all conditions for arming are met. + required: true + selector: + select: + options: + - "arming_away" + - "arming_home" + - "arming_night" + - "entry_delay" + - "exit_delay" + - "not_ready_to_arm" diff --git a/tests/components/deconz/test_alarm_control_panel.py b/tests/components/deconz/test_alarm_control_panel.py index b0425d5701aa3..951a15b6580f3 100644 --- a/tests/components/deconz/test_alarm_control_panel.py +++ b/tests/components/deconz/test_alarm_control_panel.py @@ -6,12 +6,29 @@ ANCILLARY_CONTROL_ARMED_AWAY, ANCILLARY_CONTROL_ARMED_NIGHT, ANCILLARY_CONTROL_ARMED_STAY, + ANCILLARY_CONTROL_ARMING_AWAY, + ANCILLARY_CONTROL_ARMING_NIGHT, + ANCILLARY_CONTROL_ARMING_STAY, ANCILLARY_CONTROL_DISARMED, + ANCILLARY_CONTROL_ENTRY_DELAY, + ANCILLARY_CONTROL_EXIT_DELAY, + ANCILLARY_CONTROL_NOT_READY_TO_ARM, ) from homeassistant.components.alarm_control_panel import ( DOMAIN as ALARM_CONTROL_PANEL_DOMAIN, ) +from homeassistant.components.deconz.alarm_control_panel import ( + CONF_ALARM_PANEL_STATE, + PANEL_ARMING_AWAY, + PANEL_ARMING_HOME, + PANEL_ARMING_NIGHT, + PANEL_ENTRY_DELAY, + PANEL_EXIT_DELAY, + PANEL_NOT_READY_TO_ARM, + SERVICE_ALARM_PANEL_STATE, +) +from homeassistant.components.deconz.const import DOMAIN as DECONZ_DOMAIN from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_ALARM_ARM_AWAY, @@ -204,6 +221,88 @@ async def test_alarm_control_panel(hass, aioclient_mock, mock_deconz_websocket): "panel": ANCILLARY_CONTROL_DISARMED, } + # Verify entity service calls + + # Service set panel to arming away + + await hass.services.async_call( + DECONZ_DOMAIN, + SERVICE_ALARM_PANEL_STATE, + { + ATTR_ENTITY_ID: "alarm_control_panel.keypad", + CONF_ALARM_PANEL_STATE: PANEL_ARMING_AWAY, + }, + blocking=True, + ) + assert aioclient_mock.mock_calls[5][2] == {"panel": ANCILLARY_CONTROL_ARMING_AWAY} + + # Service set panel to arming home + + await hass.services.async_call( + DECONZ_DOMAIN, + SERVICE_ALARM_PANEL_STATE, + { + ATTR_ENTITY_ID: "alarm_control_panel.keypad", + CONF_ALARM_PANEL_STATE: PANEL_ARMING_HOME, + }, + blocking=True, + ) + assert aioclient_mock.mock_calls[6][2] == {"panel": ANCILLARY_CONTROL_ARMING_STAY} + + # Service set panel to arming night + + await hass.services.async_call( + DECONZ_DOMAIN, + SERVICE_ALARM_PANEL_STATE, + { + ATTR_ENTITY_ID: "alarm_control_panel.keypad", + CONF_ALARM_PANEL_STATE: PANEL_ARMING_NIGHT, + }, + blocking=True, + ) + assert aioclient_mock.mock_calls[7][2] == {"panel": ANCILLARY_CONTROL_ARMING_NIGHT} + + # Service set panel to entry delay + + await hass.services.async_call( + DECONZ_DOMAIN, + SERVICE_ALARM_PANEL_STATE, + { + ATTR_ENTITY_ID: "alarm_control_panel.keypad", + CONF_ALARM_PANEL_STATE: PANEL_ENTRY_DELAY, + }, + blocking=True, + ) + assert aioclient_mock.mock_calls[8][2] == {"panel": ANCILLARY_CONTROL_ENTRY_DELAY} + + # Service set panel to exit delay + + await hass.services.async_call( + DECONZ_DOMAIN, + SERVICE_ALARM_PANEL_STATE, + { + ATTR_ENTITY_ID: "alarm_control_panel.keypad", + CONF_ALARM_PANEL_STATE: PANEL_EXIT_DELAY, + }, + blocking=True, + ) + assert aioclient_mock.mock_calls[9][2] == {"panel": ANCILLARY_CONTROL_EXIT_DELAY} + + # Service set panel to not ready to arm + + await hass.services.async_call( + DECONZ_DOMAIN, + SERVICE_ALARM_PANEL_STATE, + { + ATTR_ENTITY_ID: "alarm_control_panel.keypad", + CONF_ALARM_PANEL_STATE: PANEL_NOT_READY_TO_ARM, + }, + blocking=True, + ) + assert aioclient_mock.mock_calls[10][2] == { + "panel": ANCILLARY_CONTROL_NOT_READY_TO_ARM + } + await hass.config_entries.async_unload(config_entry.entry_id) states = hass.states.async_all() From dcee78b7473002a3ec2eb667f3996745455a560e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 24 Apr 2021 07:14:31 -0700 Subject: [PATCH 0494/1317] Template sensor/binary sensors without trigger now respect section unique id (#49613) --- homeassistant/components/template/__init__.py | 11 ++++++-- .../components/template/binary_sensor.py | 13 ++++++++-- homeassistant/components/template/sensor.py | 13 ++++++++-- .../components/template/test_binary_sensor.py | 26 +++++++++++++++++-- tests/components/template/test_sensor.py | 22 +++++++++++++--- 5 files changed, 74 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/template/__init__.py b/homeassistant/components/template/__init__.py index dfacac17a9b8f..3e34b92797159 100644 --- a/homeassistant/components/template/__init__.py +++ b/homeassistant/components/template/__init__.py @@ -6,7 +6,11 @@ from typing import Callable from homeassistant import config as conf_util -from homeassistant.const import EVENT_HOMEASSISTANT_START, SERVICE_RELOAD +from homeassistant.const import ( + CONF_UNIQUE_ID, + EVENT_HOMEASSISTANT_START, + SERVICE_RELOAD, +) from homeassistant.core import CoreState, Event, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import ( @@ -84,7 +88,10 @@ async def init_coordinator(hass, conf_section): hass, platform_domain, DOMAIN, - {"entities": conf_section[platform_domain]}, + { + "unique_id": conf_section.get(CONF_UNIQUE_ID), + "entities": conf_section[platform_domain], + }, hass_config, ) ) diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index 83c31406c4a4d..2e1d2f71590a4 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -133,7 +133,9 @@ def rewrite_legacy_to_modern_conf(cfg: dict[str, dict]) -> list[dict]: @callback -def _async_create_template_tracking_entities(async_add_entities, hass, definitions): +def _async_create_template_tracking_entities( + async_add_entities, hass, definitions: list[dict], unique_id_prefix: str | None +): """Create the template binary sensors.""" sensors = [] @@ -152,6 +154,9 @@ def _async_create_template_tracking_entities(async_add_entities, hass, definitio delay_off_raw = entity_conf.get(CONF_DELAY_OFF) unique_id = entity_conf.get(CONF_UNIQUE_ID) + if unique_id and unique_id_prefix: + unique_id = f"{unique_id_prefix}-{unique_id}" + sensors.append( BinarySensorTemplate( hass, @@ -179,6 +184,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities, hass, rewrite_legacy_to_modern_conf(config[CONF_SENSORS]), + None, ) return @@ -190,7 +196,10 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= return _async_create_template_tracking_entities( - async_add_entities, hass, discovery_info["entities"] + async_add_entities, + hass, + discovery_info["entities"], + discovery_info["unique_id"], ) diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index 224756c201285..d470964465a3b 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -140,7 +140,9 @@ def rewrite_legacy_to_modern_conf(cfg: dict[str, dict]) -> list[dict]: @callback -def _async_create_template_tracking_entities(async_add_entities, hass, definitions): +def _async_create_template_tracking_entities( + async_add_entities, hass, definitions: list[dict], unique_id_prefix: str | None +): """Create the template sensors.""" sensors = [] @@ -158,6 +160,9 @@ def _async_create_template_tracking_entities(async_add_entities, hass, definitio attribute_templates = entity_conf.get(CONF_ATTRIBUTES, {}) unique_id = entity_conf.get(CONF_UNIQUE_ID) + if unique_id and unique_id_prefix: + unique_id = f"{unique_id_prefix}-{unique_id}" + sensors.append( SensorTemplate( hass, @@ -184,6 +189,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities, hass, rewrite_legacy_to_modern_conf(config[CONF_SENSORS]), + None, ) return @@ -195,7 +201,10 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= return _async_create_template_tracking_entities( - async_add_entities, hass, discovery_info["entities"] + async_add_entities, + hass, + discovery_info["entities"], + discovery_info["unique_id"], ) diff --git a/tests/components/template/test_binary_sensor.py b/tests/components/template/test_binary_sensor.py index 3c38b184418e9..703564058670c 100644 --- a/tests/components/template/test_binary_sensor.py +++ b/tests/components/template/test_binary_sensor.py @@ -842,8 +842,16 @@ async def test_unique_id(hass): """Test unique_id option only creates one binary sensor per id.""" await setup.async_setup_component( hass, - binary_sensor.DOMAIN, + "template", { + "template": { + "unique_id": "group-id", + "binary_sensor": { + "name": "top-level", + "unique_id": "sensor-id", + "state": "on", + }, + }, "binary_sensor": { "platform": "template", "sensors": { @@ -864,7 +872,21 @@ async def test_unique_id(hass): await hass.async_start() await hass.async_block_till_done() - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all()) == 2 + + ent_reg = entity_registry.async_get(hass) + + assert len(ent_reg.entities) == 2 + assert ( + ent_reg.async_get_entity_id("binary_sensor", "template", "group-id-sensor-id") + is not None + ) + assert ( + ent_reg.async_get_entity_id( + "binary_sensor", "template", "not-so-unique-anymore" + ) + is not None + ) async def test_template_validation_error(hass, caplog): diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py index b510a0c75f87d..4047a8224328b 100644 --- a/tests/components/template/test_sensor.py +++ b/tests/components/template/test_sensor.py @@ -636,8 +636,12 @@ async def test_unique_id(hass): """Test unique_id option only creates one sensor per id.""" await async_setup_component( hass, - sensor.DOMAIN, + "template", { + "template": { + "unique_id": "group-id", + "sensor": {"name": "top-level", "unique_id": "sensor-id", "state": "5"}, + }, "sensor": { "platform": "template", "sensors": { @@ -650,7 +654,7 @@ async def test_unique_id(hass): "value_template": "{{ false }}", }, }, - } + }, }, ) @@ -658,7 +662,19 @@ async def test_unique_id(hass): await hass.async_start() await hass.async_block_till_done() - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all()) == 2 + + ent_reg = entity_registry.async_get(hass) + + assert len(ent_reg.entities) == 2 + assert ( + ent_reg.async_get_entity_id("sensor", "template", "group-id-sensor-id") + is not None + ) + assert ( + ent_reg.async_get_entity_id("sensor", "template", "not-so-unique-anymore") + is not None + ) async def test_sun_renders_once_per_sensor(hass): From 46ef85f4715b796b0bbe1b343e211ffd5b7f4f74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliv=C3=A9r=20Falvai?= Date: Sat, 24 Apr 2021 17:41:15 +0200 Subject: [PATCH 0495/1317] Add new Huawei LTE sensor metadata, improve icons (#49436) --- homeassistant/components/huawei_lte/sensor.py | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index 54573c01dfa5b..5f322e924eca7 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -69,7 +69,9 @@ class SensorMeta(NamedTuple): name="WAN IPv6 address", icon="mdi:ip" ), (KEY_DEVICE_SIGNAL, "band"): SensorMeta(name="Band"), - (KEY_DEVICE_SIGNAL, "cell_id"): SensorMeta(name="Cell ID"), + (KEY_DEVICE_SIGNAL, "cell_id"): SensorMeta( + name="Cell ID", icon="mdi:transmission-tower" + ), (KEY_DEVICE_SIGNAL, "dl_mcs"): SensorMeta(name="Downlink MCS"), (KEY_DEVICE_SIGNAL, "dlbandwidth"): SensorMeta( name="Downlink bandwidth", @@ -102,8 +104,13 @@ class SensorMeta(NamedTuple): (KEY_DEVICE_SIGNAL, "mode"): SensorMeta( name="Mode", formatter=lambda x: ({"0": "2G", "2": "3G", "7": "4G"}.get(x, "Unknown"), None), + icon=lambda x: ( + {"2G": "mdi:signal-2g", "3G": "mdi:signal-3g", "4G": "mdi:signal-4g"}.get( + str(x), "mdi:signal" + ) + ), ), - (KEY_DEVICE_SIGNAL, "pci"): SensorMeta(name="PCI"), + (KEY_DEVICE_SIGNAL, "pci"): SensorMeta(name="PCI", icon="mdi:transmission-tower"), (KEY_DEVICE_SIGNAL, "rsrq"): SensorMeta( name="RSRQ", device_class=DEVICE_CLASS_SIGNAL_STRENGTH, @@ -174,6 +181,23 @@ class SensorMeta(NamedTuple): "mdi:signal-cellular-3", )[bisect((-20, -10, -6), x if x is not None else -1000)], ), + (KEY_DEVICE_SIGNAL, "transmode"): SensorMeta(name="Transmission mode"), + (KEY_DEVICE_SIGNAL, "cqi0"): SensorMeta( + name="CQI 0", + icon="mdi:speedometer", + ), + (KEY_DEVICE_SIGNAL, "cqi1"): SensorMeta( + name="CQI 1", + icon="mdi:speedometer", + ), + (KEY_DEVICE_SIGNAL, "ltedlfreq"): SensorMeta( + name="Downlink frequency", + formatter=lambda x: (round(int(x) / 10), "MHz"), + ), + (KEY_DEVICE_SIGNAL, "lteulfreq"): SensorMeta( + name="Uplink frequency", + formatter=lambda x: (round(int(x) / 10), "MHz"), + ), KEY_MONITORING_CHECK_NOTIFICATIONS: SensorMeta( exclude=re.compile( r"^(onlineupdatestatus|smsstoragefull)$", From 49c23bad29785fd541f9c2e0c09ae3dad795f22a Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sat, 24 Apr 2021 21:10:07 +0200 Subject: [PATCH 0496/1317] Revert "Remove HomeAssistantType from typing.py as it is no...2 (#49617) This reverts commit 39cb22374d20ec16e163bab07ce194b6a36c34bd. Added comment that HomeAssistantType is not to be used, but only kept in order not to break custom components. --- homeassistant/helpers/typing.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/helpers/typing.py b/homeassistant/helpers/typing.py index 54e63ab49efc3..58f999c5adc61 100644 --- a/homeassistant/helpers/typing.py +++ b/homeassistant/helpers/typing.py @@ -14,6 +14,12 @@ StateType = Union[None, str, int, float] TemplateVarsType = Optional[Mapping[str, Any]] +# HomeAssistantType is not to be used, +# It is not present in the core code base. +# It is kept in order not to break custom components +# In due time it will be removed. +HomeAssistantType = homeassistant.core.HomeAssistant + # Custom type for recorder Queries QueryType = Any From 28eaa67986eceb9c580f328f27a839961aa7b67a Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sun, 25 Apr 2021 00:04:46 +0000 Subject: [PATCH 0497/1317] [ci skip] Translation update --- .../components/motioneye/translations/ca.json | 25 +++++++++++++++++++ .../components/motioneye/translations/en.json | 25 +++++++++++++++++++ .../components/motioneye/translations/et.json | 25 +++++++++++++++++++ .../components/motioneye/translations/ru.json | 25 +++++++++++++++++++ .../motioneye/translations/zh-Hant.json | 25 +++++++++++++++++++ 5 files changed, 125 insertions(+) create mode 100644 homeassistant/components/motioneye/translations/ca.json create mode 100644 homeassistant/components/motioneye/translations/en.json create mode 100644 homeassistant/components/motioneye/translations/et.json create mode 100644 homeassistant/components/motioneye/translations/ru.json create mode 100644 homeassistant/components/motioneye/translations/zh-Hant.json diff --git a/homeassistant/components/motioneye/translations/ca.json b/homeassistant/components/motioneye/translations/ca.json new file mode 100644 index 0000000000000..65ce7e48781c2 --- /dev/null +++ b/homeassistant/components/motioneye/translations/ca.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "El servei ja est\u00e0 configurat", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "invalid_url": "URL inv\u00e0lid", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "admin_password": "Contrasenya d'administrador", + "admin_username": "Nom d'usuari d'usuari", + "surveillance_password": "Contrasenya de vigilant", + "surveillance_username": "Nom d'usuari de vigilant", + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/en.json b/homeassistant/components/motioneye/translations/en.json new file mode 100644 index 0000000000000..dd4f337e9f974 --- /dev/null +++ b/homeassistant/components/motioneye/translations/en.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Service is already configured", + "reauth_successful": "Re-authentication was successful" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "invalid_url": "Invalid URL", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "admin_password": "Admin Password", + "admin_username": "Admin Username", + "surveillance_password": "Surveillance Password", + "surveillance_username": "Surveillance Username", + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/et.json b/homeassistant/components/motioneye/translations/et.json new file mode 100644 index 0000000000000..c3e44c5297490 --- /dev/null +++ b/homeassistant/components/motioneye/translations/et.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Teenus on juba seadistatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" + }, + "error": { + "cannot_connect": "\u00dchendumine nurjus", + "invalid_auth": "Tuvastamine nurjus", + "invalid_url": "Sobimatu URL", + "unknown": "Tundmatu viga" + }, + "step": { + "user": { + "data": { + "admin_password": "Haldaja salas\u00f5na", + "admin_username": "Haldaja kasutajanimi", + "surveillance_password": "J\u00e4relvalve salas\u00f5na", + "surveillance_username": "J\u00e4relvalve kasutajanimi", + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/ru.json b/homeassistant/components/motioneye/translations/ru.json new file mode 100644 index 0000000000000..a983ddcae0f6b --- /dev/null +++ b/homeassistant/components/motioneye/translations/ru.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "invalid_url": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 URL-\u0430\u0434\u0440\u0435\u0441.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "admin_password": "\u041f\u0430\u0440\u043e\u043b\u044c \u0410\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u0430", + "admin_username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0410\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u0430", + "surveillance_password": "\u041f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u043d\u0430\u0431\u043b\u044e\u0434\u0435\u043d\u0438\u044f", + "surveillance_username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0434\u043b\u044f \u043d\u0430\u0431\u043b\u044e\u0434\u0435\u043d\u0438\u044f", + "url": "URL-\u0430\u0434\u0440\u0435\u0441" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/zh-Hant.json b/homeassistant/components/motioneye/translations/zh-Hant.json new file mode 100644 index 0000000000000..aa05784e53da0 --- /dev/null +++ b/homeassistant/components/motioneye/translations/zh-Hant.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "invalid_url": "\u7db2\u5740\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "admin_password": "Admin \u5bc6\u78bc", + "admin_username": "Admin \u4f7f\u7528\u8005\u540d\u7a31", + "surveillance_password": "Surveillance \u5bc6\u78bc", + "surveillance_username": "Surveillance \u4f7f\u7528\u8005\u540d\u7a31", + "url": "\u7db2\u5740" + } + } + } + } +} \ No newline at end of file From f1d48ddfe31d3de4bcba16057e809efe90ef7e0a Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 25 Apr 2021 02:39:24 +0200 Subject: [PATCH 0498/1317] Update pylint to 2.8.0 (#49637) --- homeassistant/__main__.py | 1 + homeassistant/components/alexa/handlers.py | 5 +--- .../components/command_line/notify.py | 30 ++++++++++--------- .../components/denonavr/media_player.py | 6 ++-- .../devolo_home_control/config_flow.py | 7 +---- .../components/hangouts/hangouts_bot.py | 1 + .../components/met_eireann/config_flow.py | 1 - .../components/motioneye/config_flow.py | 2 +- .../components/nfandroidtv/notify.py | 2 +- .../openalpr_local/image_processing.py | 1 - .../components/picnic/config_flow.py | 11 +++---- homeassistant/components/picnic/sensor.py | 13 ++++---- .../components/ping/device_tracker.py | 22 +++++++------- homeassistant/components/pushover/notify.py | 1 + homeassistant/components/rpi_camera/camera.py | 5 ++-- .../seven_segments/image_processing.py | 20 ++++++------- .../components/telegram_bot/__init__.py | 2 +- homeassistant/components/tradfri/light.py | 3 +- homeassistant/components/zeroconf/__init__.py | 3 +- homeassistant/components/zone/__init__.py | 6 ++-- homeassistant/core.py | 4 +-- homeassistant/helpers/config_validation.py | 2 +- homeassistant/helpers/selector.py | 4 +-- homeassistant/helpers/template.py | 1 - homeassistant/requirements.py | 2 +- homeassistant/util/package.py | 18 +++++------ homeassistant/util/ruamel_yaml.py | 4 +-- homeassistant/util/yaml/loader.py | 6 ++-- pyproject.toml | 5 ++++ requirements_test.txt | 4 +-- tests/components/command_line/test_notify.py | 15 ++++++---- tests/util/test_package.py | 21 ++++++------- 32 files changed, 114 insertions(+), 114 deletions(-) diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index d8256e2ef92aa..b01284d997446 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -145,6 +145,7 @@ def daemonize() -> None: sys.exit(0) # redirect standard file descriptors to devnull + # pylint: disable=consider-using-with infd = open(os.devnull) outfd = open(os.devnull, "a+") sys.stdout.flush() diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index cee4cda562d8c..da0011f817adf 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -1374,10 +1374,7 @@ async def async_api_seek(hass, config, directive, context): msg = f"{entity} did not return the current media position." raise AlexaVideoActionNotPermittedForContentError(msg) - seek_position = int(current_position) + int(position_delta / 1000) - - if seek_position < 0: - seek_position = 0 + seek_position = max(int(current_position) + int(position_delta / 1000), 0) media_duration = entity.attributes.get(media_player.ATTR_MEDIA_DURATION) if media_duration and 0 < int(media_duration) < seek_position: diff --git a/homeassistant/components/command_line/notify.py b/homeassistant/components/command_line/notify.py index 948bda7e45a94..1086c6300c2e7 100644 --- a/homeassistant/components/command_line/notify.py +++ b/homeassistant/components/command_line/notify.py @@ -7,6 +7,7 @@ from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService from homeassistant.const import CONF_COMMAND, CONF_NAME import homeassistant.helpers.config_validation as cv +from homeassistant.util.process import kill_subprocess from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT @@ -39,17 +40,18 @@ def __init__(self, command, timeout): def send_message(self, message="", **kwargs): """Send a message to a command line.""" - try: - proc = subprocess.Popen( - self.command, - universal_newlines=True, - stdin=subprocess.PIPE, - shell=True, # nosec # shell by design - ) - proc.communicate(input=message, timeout=self._timeout) - if proc.returncode != 0: - _LOGGER.error("Command failed: %s", self.command) - except subprocess.TimeoutExpired: - _LOGGER.error("Timeout for command: %s", self.command) - except subprocess.SubprocessError: - _LOGGER.error("Error trying to exec command: %s", self.command) + with subprocess.Popen( + self.command, + universal_newlines=True, + stdin=subprocess.PIPE, + shell=True, # nosec # shell by design + ) as proc: + try: + proc.communicate(input=message, timeout=self._timeout) + if proc.returncode != 0: + _LOGGER.error("Command failed: %s", self.command) + except subprocess.TimeoutExpired: + _LOGGER.error("Timeout for command: %s", self.command) + kill_subprocess(proc) + except subprocess.SubprocessError: + _LOGGER.error("Error trying to exec command: %s", self.command) diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index 254b7ffb02cd3..a3e35d42242b9 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -153,7 +153,7 @@ def __init__( ) self._available = True - def async_log_errors( # pylint: disable=no-self-argument + def async_log_errors( func: Coroutine, ) -> Coroutine: """ @@ -168,7 +168,7 @@ async def wrapper(self, *args, **kwargs): # pylint: disable=protected-access available = True try: - return await func(self, *args, **kwargs) # pylint: disable=not-callable + return await func(self, *args, **kwargs) except AvrTimoutError: available = False if self._available is True: @@ -203,7 +203,7 @@ async def wrapper(self, *args, **kwargs): _LOGGER.error( "Error %s occurred in method %s for Denon AVR receiver", err, - func.__name__, # pylint: disable=no-member + func.__name__, exc_info=True, ) finally: diff --git a/homeassistant/components/devolo_home_control/config_flow.py b/homeassistant/components/devolo_home_control/config_flow.py index 49abba7723d36..012fdbf3491b2 100644 --- a/homeassistant/components/devolo_home_control/config_flow.py +++ b/homeassistant/components/devolo_home_control/config_flow.py @@ -7,12 +7,7 @@ from homeassistant.helpers.typing import DiscoveryInfoType from . import configure_mydevolo -from .const import ( # pylint:disable=unused-import - CONF_MYDEVOLO, - DEFAULT_MYDEVOLO, - DOMAIN, - SUPPORTED_MODEL_TYPES, -) +from .const import CONF_MYDEVOLO, DEFAULT_MYDEVOLO, DOMAIN, SUPPORTED_MODEL_TYPES from .exceptions import CredentialsInvalid diff --git a/homeassistant/components/hangouts/hangouts_bot.py b/homeassistant/components/hangouts/hangouts_bot.py index 65e3c3923adf2..24be9fff77963 100644 --- a/homeassistant/components/hangouts/hangouts_bot.py +++ b/homeassistant/components/hangouts/hangouts_bot.py @@ -289,6 +289,7 @@ async def _async_send_message(self, message, targets, data): uri = data.get("image_file") if self.hass.config.is_allowed_path(uri): try: + # pylint: disable=consider-using-with image_file = open(uri, "rb") except OSError as error: _LOGGER.error( diff --git a/homeassistant/components/met_eireann/config_flow.py b/homeassistant/components/met_eireann/config_flow.py index 6d736b9061a84..051f94793feb8 100644 --- a/homeassistant/components/met_eireann/config_flow.py +++ b/homeassistant/components/met_eireann/config_flow.py @@ -6,7 +6,6 @@ from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME import homeassistant.helpers.config_validation as cv -# pylint:disable=unused-import from .const import DOMAIN, HOME_LOCATION_NAME diff --git a/homeassistant/components/motioneye/config_flow.py b/homeassistant/components/motioneye/config_flow.py index 45da759e91baa..f0ff0a3883669 100644 --- a/homeassistant/components/motioneye/config_flow.py +++ b/homeassistant/components/motioneye/config_flow.py @@ -21,7 +21,7 @@ from homeassistant.helpers.typing import ConfigType from . import create_motioneye_client -from .const import ( # pylint:disable=unused-import +from .const import ( CONF_ADMIN_PASSWORD, CONF_ADMIN_USERNAME, CONF_CONFIG_ENTRY, diff --git a/homeassistant/components/nfandroidtv/notify.py b/homeassistant/components/nfandroidtv/notify.py index e71d81f2b7904..ad2f3fb3706e9 100644 --- a/homeassistant/components/nfandroidtv/notify.py +++ b/homeassistant/components/nfandroidtv/notify.py @@ -266,7 +266,7 @@ def load_file( if local_path is not None: # Check whether path is whitelisted in configuration.yaml if self.is_allowed_path(local_path): - return open(local_path, "rb") + return open(local_path, "rb") # pylint: disable=consider-using-with _LOGGER.warning("'%s' is not secure to load data from!", local_path) else: _LOGGER.warning("Neither URL nor local path found in params!") diff --git a/homeassistant/components/openalpr_local/image_processing.py b/homeassistant/components/openalpr_local/image_processing.py index d098edba5b2f8..5e4b5298d1307 100644 --- a/homeassistant/components/openalpr_local/image_processing.py +++ b/homeassistant/components/openalpr_local/image_processing.py @@ -183,7 +183,6 @@ async def async_process_image(self, image): alpr = await asyncio.create_subprocess_exec( *self._cmd, - loop=self.hass.loop, stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.DEVNULL, diff --git a/homeassistant/components/picnic/config_flow.py b/homeassistant/components/picnic/config_flow.py index 0252e7caca533..108325df45ad8 100644 --- a/homeassistant/components/picnic/config_flow.py +++ b/homeassistant/components/picnic/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Picnic integration.""" +from __future__ import annotations + import logging -from typing import Tuple from python_picnic_api import PicnicAPI from python_picnic_api.session import PicnicAuthError @@ -10,11 +11,7 @@ from homeassistant import config_entries, core, exceptions from homeassistant.const import CONF_ACCESS_TOKEN, CONF_PASSWORD, CONF_USERNAME -from .const import ( # pylint: disable=unused-import - CONF_COUNTRY_CODE, - COUNTRY_CODES, - DOMAIN, -) +from .const import CONF_COUNTRY_CODE, COUNTRY_CODES, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -33,7 +30,7 @@ class PicnicHub: """Hub class to test user authentication.""" @staticmethod - def authenticate(username, password, country_code) -> Tuple[str, dict]: + def authenticate(username, password, country_code) -> tuple[str, dict]: """Test if we can authenticate with the Picnic API.""" picnic = PicnicAPI(username, password, country_code) return picnic.session.auth_token, picnic.get_user() diff --git a/homeassistant/components/picnic/sensor.py b/homeassistant/components/picnic/sensor.py index d3778003646fd..3e30582b5c24a 100644 --- a/homeassistant/components/picnic/sensor.py +++ b/homeassistant/components/picnic/sensor.py @@ -1,6 +1,7 @@ """Definition of Picnic sensors.""" +from __future__ import annotations -from typing import Any, Optional +from typing import Any from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ATTRIBUTION @@ -48,17 +49,17 @@ def __init__( self._service_unique_id = config_entry.unique_id @property - def unit_of_measurement(self) -> Optional[str]: + def unit_of_measurement(self) -> str | None: """Return the unit this state is expressed in.""" return self.properties.get("unit") @property - def unique_id(self) -> Optional[str]: + def unique_id(self) -> str | None: """Return a unique ID.""" return f"{self._service_unique_id}.{self.sensor_type}" @property - def name(self) -> Optional[str]: + def name(self) -> str | None: """Return the name of the entity.""" return self._to_capitalized_name(self.sensor_type) @@ -69,12 +70,12 @@ def state(self) -> StateType: return self.properties["state"](data_set) @property - def device_class(self) -> Optional[str]: + def device_class(self) -> str | None: """Return the class of this device, from component DEVICE_CLASSES.""" return self.properties.get("class") @property - def icon(self) -> Optional[str]: + def icon(self) -> str | None: """Return the icon to use in the frontend, if any.""" return self.properties["icon"] diff --git a/homeassistant/components/ping/device_tracker.py b/homeassistant/components/ping/device_tracker.py index e40b8168938b0..d7d812d371ddd 100644 --- a/homeassistant/components/ping/device_tracker.py +++ b/homeassistant/components/ping/device_tracker.py @@ -54,18 +54,18 @@ def __init__(self, ip_address, dev_id, hass, config, privileged): def ping(self): """Send an ICMP echo request and return True if success.""" - pinger = subprocess.Popen( + with subprocess.Popen( self._ping_cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL - ) - try: - pinger.communicate(timeout=1 + PING_TIMEOUT) - return pinger.returncode == 0 - except subprocess.TimeoutExpired: - kill_subprocess(pinger) - return False - - except subprocess.CalledProcessError: - return False + ) as pinger: + try: + pinger.communicate(timeout=1 + PING_TIMEOUT) + return pinger.returncode == 0 + except subprocess.TimeoutExpired: + kill_subprocess(pinger) + return False + + except subprocess.CalledProcessError: + return False def update(self) -> bool: """Update device state by sending one or more ping messages.""" diff --git a/homeassistant/components/pushover/notify.py b/homeassistant/components/pushover/notify.py index 952a399157ca4..3f599ac2d8a89 100644 --- a/homeassistant/components/pushover/notify.py +++ b/homeassistant/components/pushover/notify.py @@ -75,6 +75,7 @@ def send_message(self, message="", **kwargs): if self._hass.config.is_allowed_path(data[ATTR_ATTACHMENT]): # try to open it as a normal file. try: + # pylint: disable=consider-using-with file_handle = open(data[ATTR_ATTACHMENT], "rb") # Replace the attachment identifier with file object. image = file_handle diff --git a/homeassistant/components/rpi_camera/camera.py b/homeassistant/components/rpi_camera/camera.py index 47ce87c4a8d6a..2d7edd83fed62 100644 --- a/homeassistant/components/rpi_camera/camera.py +++ b/homeassistant/components/rpi_camera/camera.py @@ -56,9 +56,8 @@ def delete_temp_file(*args): # If no file path is defined, use a temporary file if file_path is None: - temp_file = NamedTemporaryFile(suffix=".jpg", delete=False) - temp_file.close() - file_path = temp_file.name + with NamedTemporaryFile(suffix=".jpg", delete=False) as temp_file: + file_path = temp_file.name setup_config[CONF_FILE_PATH] = file_path hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, delete_temp_file) diff --git a/homeassistant/components/seven_segments/image_processing.py b/homeassistant/components/seven_segments/image_processing.py index 6ff6b63746a22..c71be3c578a6d 100644 --- a/homeassistant/components/seven_segments/image_processing.py +++ b/homeassistant/components/seven_segments/image_processing.py @@ -124,14 +124,14 @@ def process_image(self, image): img = Image.open(stream) img.save(self.filepath, "png") - ocr = subprocess.Popen( + with subprocess.Popen( self._command, stdout=subprocess.PIPE, stderr=subprocess.PIPE - ) - out = ocr.communicate() - if out[0] != b"": - self._state = out[0].strip().decode("utf-8") - else: - self._state = None - _LOGGER.warning( - "Unable to detect value: %s", out[1].strip().decode("utf-8") - ) + ) as ocr: + out = ocr.communicate() + if out[0] != b"": + self._state = out[0].strip().decode("utf-8") + else: + self._state = None + _LOGGER.warning( + "Unable to detect value: %s", out[1].strip().decode("utf-8") + ) diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index 589d85bd20ec6..fe3728ba91b1c 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -282,7 +282,7 @@ def load_data( _LOGGER.warning("Can't load data in %s after %s retries", url, retry_num) elif filepath is not None: if hass.config.is_allowed_path(filepath): - return open(filepath, "rb") + return open(filepath, "rb") # pylint: disable=consider-using-with _LOGGER.warning("'%s' are not secure to load data from!", filepath) else: diff --git a/homeassistant/components/tradfri/light.py b/homeassistant/components/tradfri/light.py index bad4ce282b937..5da2c2b9b1fed 100644 --- a/homeassistant/components/tradfri/light.py +++ b/homeassistant/components/tradfri/light.py @@ -185,8 +185,7 @@ async def async_turn_on(self, **kwargs): dimmer_command = None if ATTR_BRIGHTNESS in kwargs: brightness = kwargs[ATTR_BRIGHTNESS] - if brightness > 254: - brightness = 254 + brightness = min(brightness, 254) dimmer_data = { ATTR_DIMMER: brightness, ATTR_TRANSITION_TIME: transition_time, diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 7b13c7fd75385..7d4205279ed54 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -1,6 +1,7 @@ """Support for exposing Home Assistant via Zeroconf.""" from __future__ import annotations +from collections.abc import Iterable from contextlib import suppress import fnmatch from functools import partial @@ -8,7 +9,7 @@ from ipaddress import ip_address import logging import socket -from typing import Any, Iterable, TypedDict, cast +from typing import Any, TypedDict, cast from pyroute2 import IPRoute import voluptuous as vol diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index 4866c27807459..b224c2a47d7d8 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import Any, Dict, cast +from typing import Any, cast import voluptuous as vol @@ -163,7 +163,7 @@ class ZoneStorageCollection(collection.StorageCollection): async def _process_create_data(self, data: dict) -> dict: """Validate the config is valid.""" - return cast(Dict, self.CREATE_SCHEMA(data)) + return cast(dict, self.CREATE_SCHEMA(data)) @callback def _get_suggested_id(self, info: dict) -> str: @@ -291,7 +291,7 @@ def from_yaml(cls, config: dict) -> Zone: """Return entity instance initialized from yaml storage.""" zone = cls(config) zone.editable = False - zone._generate_attrs() # pylint:disable=protected-access + zone._generate_attrs() return zone @property diff --git a/homeassistant/core.py b/homeassistant/core.py index 3313da887c267..c22526474a42e 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -7,7 +7,7 @@ from __future__ import annotations import asyncio -from collections.abc import Awaitable, Collection, Iterable, Mapping +from collections.abc import Awaitable, Collection, Coroutine, Iterable, Mapping import datetime import enum import functools @@ -18,7 +18,7 @@ import threading from time import monotonic from types import MappingProxyType -from typing import TYPE_CHECKING, Any, Callable, Coroutine, Optional, TypeVar, cast +from typing import TYPE_CHECKING, Any, Callable, Optional, TypeVar, cast import attr import voluptuous as vol diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index e0afbc49af294..8ad8d4a45a22e 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -483,7 +483,7 @@ def verify(value: dict) -> dict: for key in value.keys(): slug_validator(key) - return cast(Dict, schema(value)) + return cast(dict, schema(value)) return verify diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index 99d871fc25b67..5bde59c06dc2b 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -1,7 +1,7 @@ """Selectors for Home Assistant.""" from __future__ import annotations -from typing import Any, Callable, Dict, cast +from typing import Any, Callable, cast import voluptuous as vol @@ -31,7 +31,7 @@ def validate_selector(config: Any) -> dict: return {selector_type: {}} return { - selector_type: cast(Dict, selector_class.CONFIG_SCHEMA(config[selector_type])) + selector_type: cast(dict, selector_class.CONFIG_SCHEMA(config[selector_type])) } diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index b8721ef91d3fd..6ac220788e033 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -216,7 +216,6 @@ def __init__(self, template: Template) -> None: self.exception: TemplateError | None = None self.all_states = False self.all_states_lifecycle = False - # pylint: disable=unsubscriptable-object # for abc.Set, https://github.com/PyCQA/pylint/pull/4275 self.domains: collections.abc.Set[str] = set() self.domains_lifecycle: collections.abc.Set[str] = set() self.entities: collections.abc.Set[str] = set() diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py index 59321a1032e2a..02187fe8f0e3a 100644 --- a/homeassistant/requirements.py +++ b/homeassistant/requirements.py @@ -81,7 +81,7 @@ async def async_get_integration_with_requirements( try: await _async_process_integration(hass, integration, done) - except Exception: # pylint: disable=broad-except + except Exception: del cache[domain] event.set() raise diff --git a/homeassistant/util/package.py b/homeassistant/util/package.py index 99afcd0fcf871..50d46b6c46974 100644 --- a/homeassistant/util/package.py +++ b/homeassistant/util/package.py @@ -90,15 +90,15 @@ def install_package( # Workaround for incompatible prefix setting # See http://stackoverflow.com/a/4495175 args += ["--prefix="] - process = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE, env=env) - _, stderr = process.communicate() - if process.returncode != 0: - _LOGGER.error( - "Unable to install package %s: %s", - package, - stderr.decode("utf-8").lstrip().strip(), - ) - return False + with Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE, env=env) as process: + _, stderr = process.communicate() + if process.returncode != 0: + _LOGGER.error( + "Unable to install package %s: %s", + package, + stderr.decode("utf-8").lstrip().strip(), + ) + return False return True diff --git a/homeassistant/util/ruamel_yaml.py b/homeassistant/util/ruamel_yaml.py index 7bb49b0545beb..74d71678a6f2a 100644 --- a/homeassistant/util/ruamel_yaml.py +++ b/homeassistant/util/ruamel_yaml.py @@ -6,7 +6,7 @@ import logging import os from os import O_CREAT, O_TRUNC, O_WRONLY, stat_result -from typing import Dict, List, Union +from typing import Union import ruamel.yaml from ruamel.yaml import YAML # type: ignore @@ -19,7 +19,7 @@ _LOGGER = logging.getLogger(__name__) -JSON_TYPE = Union[List, Dict, str] # pylint: disable=invalid-name +JSON_TYPE = Union[list, dict, str] # pylint: disable=invalid-name class ExtSafeConstructor(SafeConstructor): diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index d63ddd6afa3e5..dbff753aa686f 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -7,7 +7,7 @@ import logging import os from pathlib import Path -from typing import Any, Dict, List, TextIO, TypeVar, Union, overload +from typing import Any, TextIO, TypeVar, Union, overload import yaml @@ -18,8 +18,8 @@ # mypy: allow-untyped-calls, no-warn-return-any -JSON_TYPE = Union[List, Dict, str] # pylint: disable=invalid-name -DICT_T = TypeVar("DICT_T", bound=Dict) # pylint: disable=invalid-name +JSON_TYPE = Union[list, dict, str] # pylint: disable=invalid-name +DICT_T = TypeVar("DICT_T", bound=dict) # pylint: disable=invalid-name _LOGGER = logging.getLogger(__name__) diff --git a/pyproject.toml b/pyproject.toml index 217cbebe3b0a6..0e38a19731971 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ ignore = [ jobs = 2 init-hook='from pylint.config.find_default_config_files import find_default_config_files; from pathlib import Path; import sys; sys.path.append(str(Path(Path(list(find_default_config_files())[0]).parent, "pylint/plugins")))' load-plugins = [ + "pylint.extensions.typing", "pylint_strict_informational", "hass_logger" ] @@ -109,6 +110,10 @@ overgeneral-exceptions = [ "HomeAssistantError", ] +[tool.pylint.TYPING] +py-version = "3.8" +runtime-typing = false + [tool.pytest.ini_options] testpaths = [ "tests", diff --git a/requirements_test.txt b/requirements_test.txt index d3c858f6f32c6..4403aedb7cc82 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -10,8 +10,8 @@ jsonpickle==1.4.1 mock-open==1.4.0 mypy==0.812 pre-commit==2.12.1 -pylint==2.7.4 -astroid==2.5.2 +pylint==2.8.0 +astroid==2.5.5 pipdeptree==1.0.0 pylint-strict-informational==0.1 pytest-aiohttp==0.3.0 diff --git a/tests/components/command_line/test_notify.py b/tests/components/command_line/test_notify.py index 5fef385bf81dc..561ac07df20ce 100644 --- a/tests/components/command_line/test_notify.py +++ b/tests/components/command_line/test_notify.py @@ -94,21 +94,24 @@ async def test_subprocess_exceptions(caplog: Any, hass: HomeAssistant) -> None: """Test that notify subprocess exceptions are handled correctly.""" with patch( - "homeassistant.components.command_line.notify.subprocess.Popen", - side_effect=[ + "homeassistant.components.command_line.notify.subprocess.Popen" + ) as check_output: + check_output.return_value.__enter__ = check_output + check_output.return_value.communicate.side_effect = [ subprocess.TimeoutExpired("cmd", 10), + None, subprocess.SubprocessError(), - ], - ) as check_output: + ] + await setup_test_service(hass, {"command": "exit 0"}) assert await hass.services.async_call( DOMAIN, "test", {"message": "error"}, blocking=True ) - assert check_output.call_count == 1 + assert check_output.call_count == 2 assert "Timeout for command" in caplog.text assert await hass.services.async_call( DOMAIN, "test", {"message": "error"}, blocking=True ) - assert check_output.call_count == 2 + assert check_output.call_count == 4 assert "Error trying to exec command" in caplog.text diff --git a/tests/util/test_package.py b/tests/util/test_package.py index 494fe5fa11fbe..3006cb17c3720 100644 --- a/tests/util/test_package.py +++ b/tests/util/test_package.py @@ -46,6 +46,7 @@ def lib_dir(deps_dir): def mock_popen(lib_dir): """Return a Popen mock.""" with patch("homeassistant.util.package.Popen") as popen_mock: + popen_mock.return_value.__enter__ = popen_mock popen_mock.return_value.communicate.return_value = ( bytes(lib_dir, "utf-8"), b"error", @@ -87,8 +88,8 @@ def test_install(mock_sys, mock_popen, mock_env_copy, mock_venv): """Test an install attempt on a package that doesn't exist.""" env = mock_env_copy() assert package.install_package(TEST_NEW_REQ, False) - assert mock_popen.call_count == 1 - assert mock_popen.call_args == call( + assert mock_popen.call_count == 2 + assert mock_popen.mock_calls[0] == call( [mock_sys.executable, "-m", "pip", "install", "--quiet", TEST_NEW_REQ], stdin=PIPE, stdout=PIPE, @@ -102,8 +103,8 @@ def test_install_upgrade(mock_sys, mock_popen, mock_env_copy, mock_venv): """Test an upgrade attempt on a package.""" env = mock_env_copy() assert package.install_package(TEST_NEW_REQ) - assert mock_popen.call_count == 1 - assert mock_popen.call_args == call( + assert mock_popen.call_count == 2 + assert mock_popen.mock_calls[0] == call( [ mock_sys.executable, "-m", @@ -140,8 +141,8 @@ def test_install_target(mock_sys, mock_popen, mock_env_copy, mock_venv): ] assert package.install_package(TEST_NEW_REQ, False, target=target) - assert mock_popen.call_count == 1 - assert mock_popen.call_args == call( + assert mock_popen.call_count == 2 + assert mock_popen.mock_calls[0] == call( args, stdin=PIPE, stdout=PIPE, stderr=PIPE, env=env ) assert mock_popen.return_value.communicate.call_count == 1 @@ -169,8 +170,8 @@ def test_install_constraint(mock_sys, mock_popen, mock_env_copy, mock_venv): env = mock_env_copy() constraints = "constraints_file.txt" assert package.install_package(TEST_NEW_REQ, False, constraints=constraints) - assert mock_popen.call_count == 1 - assert mock_popen.call_args == call( + assert mock_popen.call_count == 2 + assert mock_popen.mock_calls[0] == call( [ mock_sys.executable, "-m", @@ -194,8 +195,8 @@ def test_install_find_links(mock_sys, mock_popen, mock_env_copy, mock_venv): env = mock_env_copy() link = "https://wheels-repository" assert package.install_package(TEST_NEW_REQ, False, find_links=link) - assert mock_popen.call_count == 1 - assert mock_popen.call_args == call( + assert mock_popen.call_count == 2 + assert mock_popen.mock_calls[0] == call( [ mock_sys.executable, "-m", From a3525169441fa69fd8e194ac9e567449cca59300 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 25 Apr 2021 02:40:12 +0200 Subject: [PATCH 0499/1317] Implement DataUpdateCoordinator to fritzbox integration (#49611) --- homeassistant/components/fritzbox/__init__.py | 132 +++++++++++++-- .../components/fritzbox/binary_sensor.py | 90 +++++------ homeassistant/components/fritzbox/climate.py | 151 ++++++++---------- homeassistant/components/fritzbox/const.py | 3 +- homeassistant/components/fritzbox/sensor.py | 142 ++++++---------- homeassistant/components/fritzbox/switch.py | 108 ++++++------- .../components/fritzbox/test_binary_sensor.py | 4 +- tests/components/fritzbox/test_climate.py | 12 +- tests/components/fritzbox/test_init.py | 37 ++++- tests/components/fritzbox/test_sensor.py | 10 +- tests/components/fritzbox/test_switch.py | 10 +- 11 files changed, 374 insertions(+), 325 deletions(-) diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index 2d9812b9ff9c5..7201c171c6a21 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -1,22 +1,43 @@ """Support for AVM Fritz!Box smarthome devices.""" +from __future__ import annotations + import asyncio +from datetime import timedelta import socket -from pyfritzhome import Fritzhome, LoginError +from pyfritzhome import Fritzhome, FritzhomeDevice, LoginError +import requests import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, + ATTR_NAME, + ATTR_UNIT_OF_MEASUREMENT, CONF_DEVICES, CONF_HOST, CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, ) +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) -from .const import CONF_CONNECTIONS, DEFAULT_HOST, DEFAULT_USERNAME, DOMAIN, PLATFORMS +from .const import ( + CONF_CONNECTIONS, + CONF_COORDINATOR, + DEFAULT_HOST, + DEFAULT_USERNAME, + DOMAIN, + LOGGER, + PLATFORMS, +) def ensure_unique_hosts(value): @@ -58,7 +79,7 @@ def ensure_unique_hosts(value): ) -async def async_setup(hass, config): +async def async_setup(hass: HomeAssistant, config: dict[str, str]) -> bool: """Set up the AVM Fritz!Box integration.""" if DOMAIN in config: for entry_config in config[DOMAIN][CONF_DEVICES]: @@ -71,7 +92,7 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass, entry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up the AVM Fritz!Box platforms.""" fritz = Fritzhome( host=entry.data[CONF_HOST], @@ -84,8 +105,44 @@ async def async_setup_entry(hass, entry): except LoginError as err: raise ConfigEntryAuthFailed from err - hass.data.setdefault(DOMAIN, {CONF_CONNECTIONS: {}, CONF_DEVICES: set()}) - hass.data[DOMAIN][CONF_CONNECTIONS][entry.entry_id] = fritz + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = { + CONF_CONNECTIONS: fritz, + } + + def _update_fritz_devices() -> dict[str, FritzhomeDevice]: + """Update all fritzbox device data.""" + try: + devices = fritz.get_devices() + except requests.exceptions.HTTPError: + # If the device rebooted, login again + try: + fritz.login() + except requests.exceptions.HTTPError as ex: + raise ConfigEntryAuthFailed from ex + devices = fritz.get_devices() + + data = {} + for device in devices: + device.update() + data[device.ain] = device + return data + + async def async_update_coordinator(): + """Fetch all device data.""" + return await hass.async_add_executor_job(_update_fritz_devices) + + hass.data[DOMAIN][entry.entry_id][ + CONF_COORDINATOR + ] = coordinator = DataUpdateCoordinator( + hass, + LOGGER, + name=f"{entry.entry_id}", + update_method=async_update_coordinator, + update_interval=timedelta(seconds=30), + ) + + await coordinator.async_config_entry_first_refresh() for platform in PLATFORMS: hass.async_create_task( @@ -103,9 +160,9 @@ def logout_fritzbox(event): return True -async def async_unload_entry(hass, entry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unloading the AVM Fritz!Box platforms.""" - fritz = hass.data[DOMAIN][CONF_CONNECTIONS][entry.entry_id] + fritz = hass.data[DOMAIN][entry.entry_id][CONF_CONNECTIONS] await hass.async_add_executor_job(fritz.logout) unload_ok = all( @@ -117,6 +174,61 @@ async def async_unload_entry(hass, entry): ) ) if unload_ok: - hass.data[DOMAIN][CONF_CONNECTIONS].pop(entry.entry_id) + hass.data[DOMAIN].pop(entry.entry_id) return unload_ok + + +class FritzBoxEntity(CoordinatorEntity): + """Basis FritzBox entity.""" + + def __init__( + self, + entity_info: dict[str, str], + coordinator: DataUpdateCoordinator, + ain: str, + ): + """Initialize the FritzBox entity.""" + super().__init__(coordinator) + + self.ain = ain + self._name = entity_info[ATTR_NAME] + self._unique_id = entity_info[ATTR_ENTITY_ID] + self._unit_of_measurement = entity_info[ATTR_UNIT_OF_MEASUREMENT] + self._device_class = entity_info[ATTR_DEVICE_CLASS] + + @property + def device(self) -> FritzhomeDevice: + """Return device object from coordinator.""" + return self.coordinator.data[self.ain] + + @property + def device_info(self): + """Return device specific attributes.""" + return { + "name": self.device.name, + "identifiers": {(DOMAIN, self.ain)}, + "manufacturer": self.device.manufacturer, + "model": self.device.productname, + "sw_version": self.device.fw_version, + } + + @property + def unique_id(self): + """Return the unique ID of the device.""" + return self._unique_id + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit_of_measurement + + @property + def device_class(self): + """Return the device class.""" + return self._device_class diff --git a/homeassistant/components/fritzbox/binary_sensor.py b/homeassistant/components/fritzbox/binary_sensor.py index 1246eb4afaf2f..e118414bb2577 100644 --- a/homeassistant/components/fritzbox/binary_sensor.py +++ b/homeassistant/components/fritzbox/binary_sensor.py @@ -1,74 +1,56 @@ """Support for Fritzbox binary sensors.""" -import requests +from typing import Callable from homeassistant.components.binary_sensor import ( DEVICE_CLASS_WINDOW, BinarySensorEntity, ) -from homeassistant.const import CONF_DEVICES +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, + ATTR_NAME, + ATTR_UNIT_OF_MEASUREMENT, +) +from homeassistant.core import HomeAssistant -from .const import CONF_CONNECTIONS, DOMAIN as FRITZBOX_DOMAIN, LOGGER +from . import FritzBoxEntity +from .const import CONF_COORDINATOR, DOMAIN as FRITZBOX_DOMAIN -async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up the Fritzbox binary sensor from config_entry.""" +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable +) -> None: + """Set up the Fritzbox binary sensor from ConfigEntry.""" entities = [] - devices = hass.data[FRITZBOX_DOMAIN][CONF_DEVICES] - fritz = hass.data[FRITZBOX_DOMAIN][CONF_CONNECTIONS][config_entry.entry_id] - - for device in await hass.async_add_executor_job(fritz.get_devices): - if device.has_alarm and device.ain not in devices: - entities.append(FritzboxBinarySensor(device, fritz)) - devices.add(device.ain) - - async_add_entities(entities, True) - - -class FritzboxBinarySensor(BinarySensorEntity): - """Representation of a binary Fritzbox device.""" + coordinator = hass.data[FRITZBOX_DOMAIN][entry.entry_id][CONF_COORDINATOR] - def __init__(self, device, fritz): - """Initialize the Fritzbox binary sensor.""" - self._device = device - self._fritz = fritz + for ain, device in coordinator.data.items(): + if not device.has_alarm: + continue - @property - def device_info(self): - """Return device specific attributes.""" - return { - "name": self.name, - "identifiers": {(FRITZBOX_DOMAIN, self._device.ain)}, - "manufacturer": self._device.manufacturer, - "model": self._device.productname, - "sw_version": self._device.fw_version, - } + entities.append( + FritzboxBinarySensor( + { + ATTR_NAME: f"{device.name}", + ATTR_ENTITY_ID: f"{device.ain}", + ATTR_UNIT_OF_MEASUREMENT: None, + ATTR_DEVICE_CLASS: DEVICE_CLASS_WINDOW, + }, + coordinator, + ain, + ) + ) - @property - def unique_id(self): - """Return the unique ID of the device.""" - return self._device.ain + async_add_entities(entities) - @property - def name(self): - """Return the name of the entity.""" - return self._device.name - @property - def device_class(self): - """Return the class of this sensor.""" - return DEVICE_CLASS_WINDOW +class FritzboxBinarySensor(FritzBoxEntity, BinarySensorEntity): + """Representation of a binary Fritzbox device.""" @property def is_on(self): """Return true if sensor is on.""" - if not self._device.present: + if not self.device.present: return False - return self._device.alert_state - - def update(self): - """Get latest data from the Fritzbox.""" - try: - self._device.update() - except requests.exceptions.HTTPError as ex: - LOGGER.warning("Connection error: %s", ex) - self._fritz.login() + return self.device.alert_state diff --git a/homeassistant/components/fritzbox/climate.py b/homeassistant/components/fritzbox/climate.py index 50f56f3d510f9..121c379dc5c59 100644 --- a/homeassistant/components/fritzbox/climate.py +++ b/homeassistant/components/fritzbox/climate.py @@ -1,5 +1,5 @@ """Support for AVM Fritz!Box smarthome thermostate devices.""" -import requests +from typing import Callable from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( @@ -11,14 +11,20 @@ SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_BATTERY_LEVEL, + ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, + ATTR_NAME, ATTR_TEMPERATURE, - CONF_DEVICES, + ATTR_UNIT_OF_MEASUREMENT, PRECISION_HALVES, TEMP_CELSIUS, ) +from homeassistant.core import HomeAssistant +from . import FritzBoxEntity from .const import ( ATTR_STATE_BATTERY_LOW, ATTR_STATE_DEVICE_LOCKED, @@ -26,9 +32,8 @@ ATTR_STATE_LOCKED, ATTR_STATE_SUMMER_MODE, ATTR_STATE_WINDOW_OPEN, - CONF_CONNECTIONS, + CONF_COORDINATOR, DOMAIN as FRITZBOX_DOMAIN, - LOGGER, ) SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE @@ -47,48 +52,36 @@ OFF_REPORT_SET_TEMPERATURE = 0.0 -async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up the Fritzbox smarthome thermostat from config_entry.""" +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable +) -> None: + """Set up the Fritzbox smarthome thermostat from ConfigEntry.""" entities = [] - devices = hass.data[FRITZBOX_DOMAIN][CONF_DEVICES] - fritz = hass.data[FRITZBOX_DOMAIN][CONF_CONNECTIONS][config_entry.entry_id] - - for device in await hass.async_add_executor_job(fritz.get_devices): - if device.has_thermostat and device.ain not in devices: - entities.append(FritzboxThermostat(device, fritz)) - devices.add(device.ain) + coordinator = hass.data[FRITZBOX_DOMAIN][entry.entry_id][CONF_COORDINATOR] + + for ain, device in coordinator.data.items(): + if not device.has_thermostat: + continue + + entities.append( + FritzboxThermostat( + { + ATTR_NAME: f"{device.name}", + ATTR_ENTITY_ID: f"{device.ain}", + ATTR_UNIT_OF_MEASUREMENT: None, + ATTR_DEVICE_CLASS: None, + }, + coordinator, + ain, + ) + ) async_add_entities(entities) -class FritzboxThermostat(ClimateEntity): +class FritzboxThermostat(FritzBoxEntity, ClimateEntity): """The thermostat class for Fritzbox smarthome thermostates.""" - def __init__(self, device, fritz): - """Initialize the thermostat.""" - self._device = device - self._fritz = fritz - self._current_temperature = self._device.actual_temperature - self._target_temperature = self._device.target_temperature - self._comfort_temperature = self._device.comfort_temperature - self._eco_temperature = self._device.eco_temperature - - @property - def device_info(self): - """Return device specific attributes.""" - return { - "name": self.name, - "identifiers": {(FRITZBOX_DOMAIN, self._device.ain)}, - "manufacturer": self._device.manufacturer, - "model": self._device.productname, - "sw_version": self._device.fw_version, - } - - @property - def unique_id(self): - """Return the unique ID of the device.""" - return self._device.ain - @property def supported_features(self): """Return the list of supported features.""" @@ -97,12 +90,7 @@ def supported_features(self): @property def available(self): """Return if thermostat is available.""" - return self._device.present - - @property - def name(self): - """Return the name of the device.""" - return self._device.name + return self.device.present @property def temperature_unit(self): @@ -117,32 +105,35 @@ def precision(self): @property def current_temperature(self): """Return the current temperature.""" - return self._current_temperature + return self.device.actual_temperature @property def target_temperature(self): """Return the temperature we try to reach.""" - if self._target_temperature == ON_API_TEMPERATURE: + if self.device.target_temperature == ON_API_TEMPERATURE: return ON_REPORT_SET_TEMPERATURE - if self._target_temperature == OFF_API_TEMPERATURE: + if self.device.target_temperature == OFF_API_TEMPERATURE: return OFF_REPORT_SET_TEMPERATURE - return self._target_temperature + return self.device.target_temperature - def set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs): """Set new target temperature.""" if ATTR_HVAC_MODE in kwargs: hvac_mode = kwargs.get(ATTR_HVAC_MODE) - self.set_hvac_mode(hvac_mode) + await self.async_set_hvac_mode(hvac_mode) elif ATTR_TEMPERATURE in kwargs: temperature = kwargs.get(ATTR_TEMPERATURE) - self._device.set_target_temperature(temperature) + await self.hass.async_add_executor_job( + self.device.set_target_temperature, temperature + ) + await self.coordinator.async_refresh() @property def hvac_mode(self): """Return the current operation mode.""" if ( - self._target_temperature == OFF_REPORT_SET_TEMPERATURE - or self._target_temperature == OFF_API_TEMPERATURE + self.device.target_temperature == OFF_REPORT_SET_TEMPERATURE + or self.device.target_temperature == OFF_API_TEMPERATURE ): return HVAC_MODE_OFF @@ -153,19 +144,21 @@ def hvac_modes(self): """Return the list of available operation modes.""" return OPERATION_LIST - def set_hvac_mode(self, hvac_mode): + async def async_set_hvac_mode(self, hvac_mode): """Set new operation mode.""" if hvac_mode == HVAC_MODE_OFF: - self.set_temperature(temperature=OFF_REPORT_SET_TEMPERATURE) + await self.async_set_temperature(temperature=OFF_REPORT_SET_TEMPERATURE) else: - self.set_temperature(temperature=self._comfort_temperature) + await self.async_set_temperature( + temperature=self.device.comfort_temperature + ) @property def preset_mode(self): """Return current preset mode.""" - if self._target_temperature == self._comfort_temperature: + if self.device.target_temperature == self.device.comfort_temperature: return PRESET_COMFORT - if self._target_temperature == self._eco_temperature: + if self.device.target_temperature == self.device.eco_temperature: return PRESET_ECO @property @@ -173,12 +166,14 @@ def preset_modes(self): """Return supported preset modes.""" return [PRESET_ECO, PRESET_COMFORT] - def set_preset_mode(self, preset_mode): + async def async_set_preset_mode(self, preset_mode): """Set preset mode.""" if preset_mode == PRESET_COMFORT: - self.set_temperature(temperature=self._comfort_temperature) + await self.async_set_temperature( + temperature=self.device.comfort_temperature + ) elif preset_mode == PRESET_ECO: - self.set_temperature(temperature=self._eco_temperature) + await self.async_set_temperature(temperature=self.device.eco_temperature) @property def min_temp(self): @@ -194,31 +189,19 @@ def max_temp(self): def extra_state_attributes(self): """Return the device specific state attributes.""" attrs = { - ATTR_STATE_BATTERY_LOW: self._device.battery_low, - ATTR_STATE_DEVICE_LOCKED: self._device.device_lock, - ATTR_STATE_LOCKED: self._device.lock, + ATTR_STATE_BATTERY_LOW: self.device.battery_low, + ATTR_STATE_DEVICE_LOCKED: self.device.device_lock, + ATTR_STATE_LOCKED: self.device.lock, } # the following attributes are available since fritzos 7 - if self._device.battery_level is not None: - attrs[ATTR_BATTERY_LEVEL] = self._device.battery_level - if self._device.holiday_active is not None: - attrs[ATTR_STATE_HOLIDAY_MODE] = self._device.holiday_active - if self._device.summer_active is not None: - attrs[ATTR_STATE_SUMMER_MODE] = self._device.summer_active + if self.device.battery_level is not None: + attrs[ATTR_BATTERY_LEVEL] = self.device.battery_level + if self.device.holiday_active is not None: + attrs[ATTR_STATE_HOLIDAY_MODE] = self.device.holiday_active + if self.device.summer_active is not None: + attrs[ATTR_STATE_SUMMER_MODE] = self.device.summer_active if ATTR_STATE_WINDOW_OPEN is not None: - attrs[ATTR_STATE_WINDOW_OPEN] = self._device.window_open + attrs[ATTR_STATE_WINDOW_OPEN] = self.device.window_open return attrs - - def update(self): - """Update the data from the thermostat.""" - try: - self._device.update() - self._current_temperature = self._device.actual_temperature - self._target_temperature = self._device.target_temperature - self._comfort_temperature = self._device.comfort_temperature - self._eco_temperature = self._device.eco_temperature - except requests.exceptions.HTTPError as ex: - LOGGER.warning("Fritzbox connection error: %s", ex) - self._fritz.login() diff --git a/homeassistant/components/fritzbox/const.py b/homeassistant/components/fritzbox/const.py index 32a72e8e7a6a3..9189fbd81c64f 100644 --- a/homeassistant/components/fritzbox/const.py +++ b/homeassistant/components/fritzbox/const.py @@ -14,12 +14,13 @@ ATTR_TOTAL_CONSUMPTION_UNIT = "total_consumption_unit" CONF_CONNECTIONS = "connections" +CONF_COORDINATOR = "coordinator" DEFAULT_HOST = "fritz.box" DEFAULT_USERNAME = "admin" DOMAIN = "fritzbox" -LOGGER = logging.getLogger(__package__) +LOGGER: logging.Logger = logging.getLogger(__package__) PLATFORMS = ["binary_sensor", "climate", "switch", "sensor"] diff --git a/homeassistant/components/fritzbox/sensor.py b/homeassistant/components/fritzbox/sensor.py index 52d2617b223e3..39e7f6db091ac 100644 --- a/homeassistant/components/fritzbox/sensor.py +++ b/homeassistant/components/fritzbox/sensor.py @@ -1,143 +1,93 @@ """Support for AVM Fritz!Box smarthome temperature sensor only devices.""" -import requests +from typing import Callable from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - CONF_DEVICES, + ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, + ATTR_NAME, + ATTR_UNIT_OF_MEASUREMENT, DEVICE_CLASS_BATTERY, PERCENTAGE, TEMP_CELSIUS, ) +from homeassistant.core import HomeAssistant +from . import FritzBoxEntity from .const import ( ATTR_STATE_DEVICE_LOCKED, ATTR_STATE_LOCKED, - CONF_CONNECTIONS, + CONF_COORDINATOR, DOMAIN as FRITZBOX_DOMAIN, - LOGGER, ) -async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up the Fritzbox smarthome sensor from config_entry.""" +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable +) -> None: + """Set up the Fritzbox smarthome sensor from ConfigEntry.""" entities = [] - devices = hass.data[FRITZBOX_DOMAIN][CONF_DEVICES] - fritz = hass.data[FRITZBOX_DOMAIN][CONF_CONNECTIONS][config_entry.entry_id] + coordinator = hass.data[FRITZBOX_DOMAIN][entry.entry_id][CONF_COORDINATOR] - for device in await hass.async_add_executor_job(fritz.get_devices): + for ain, device in coordinator.data.items(): if ( device.has_temperature_sensor and not device.has_switch and not device.has_thermostat - and device.ain not in devices ): - entities.append(FritzBoxTempSensor(device, fritz)) - devices.add(device.ain) + entities.append( + FritzBoxTempSensor( + { + ATTR_NAME: f"{device.name}", + ATTR_ENTITY_ID: f"{device.ain}", + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_DEVICE_CLASS: None, + }, + coordinator, + ain, + ) + ) if device.battery_level is not None: - entities.append(FritzBoxBatterySensor(device, fritz)) - devices.add(f"{device.ain}_battery") + entities.append( + FritzBoxBatterySensor( + { + ATTR_NAME: f"{device.name} Battery", + ATTR_ENTITY_ID: f"{device.ain}_battery", + ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, + ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY, + }, + coordinator, + ain, + ) + ) async_add_entities(entities) -class FritzBoxBatterySensor(SensorEntity): - """The entity class for Fritzbox battery sensors.""" - - def __init__(self, device, fritz): - """Initialize the sensor.""" - self._device = device - self._fritz = fritz - - @property - def device_info(self): - """Return device specific attributes.""" - return { - "name": self.name, - "identifiers": {(FRITZBOX_DOMAIN, self._device.ain)}, - "manufacturer": self._device.manufacturer, - "model": self._device.productname, - "sw_version": self._device.fw_version, - } - - @property - def unique_id(self): - """Return the unique ID of the device.""" - return f"{self._device.ain}_battery" - - @property - def name(self): - """Return the name of the device.""" - return f"{self._device.name} Battery" +class FritzBoxBatterySensor(FritzBoxEntity, SensorEntity): + """The entity class for Fritzbox sensors.""" @property def state(self): """Return the state of the sensor.""" - return self._device.battery_level - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return PERCENTAGE - - @property - def device_class(self): - """Return the device class.""" - return DEVICE_CLASS_BATTERY + return self.device.battery_level -class FritzBoxTempSensor(SensorEntity): +class FritzBoxTempSensor(FritzBoxEntity, SensorEntity): """The entity class for Fritzbox temperature sensors.""" - def __init__(self, device, fritz): - """Initialize the switch.""" - self._device = device - self._fritz = fritz - - @property - def device_info(self): - """Return device specific attributes.""" - return { - "name": self.name, - "identifiers": {(FRITZBOX_DOMAIN, self._device.ain)}, - "manufacturer": self._device.manufacturer, - "model": self._device.productname, - "sw_version": self._device.fw_version, - } - - @property - def unique_id(self): - """Return the unique ID of the device.""" - return self._device.ain - - @property - def name(self): - """Return the name of the device.""" - return self._device.name - @property def state(self): """Return the state of the sensor.""" - return self._device.temperature - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return TEMP_CELSIUS - - def update(self): - """Get latest data and states from the device.""" - try: - self._device.update() - except requests.exceptions.HTTPError as ex: - LOGGER.warning("Fritzhome connection error: %s", ex) - self._fritz.login() + return self.device.temperature @property def extra_state_attributes(self): """Return the state attributes of the device.""" attrs = { - ATTR_STATE_DEVICE_LOCKED: self._device.device_lock, - ATTR_STATE_LOCKED: self._device.lock, + ATTR_STATE_DEVICE_LOCKED: self.device.device_lock, + ATTR_STATE_LOCKED: self.device.lock, } return attrs diff --git a/homeassistant/components/fritzbox/switch.py b/homeassistant/components/fritzbox/switch.py index 50c60f7bb3926..a7c1c8cf0fd78 100644 --- a/homeassistant/components/fritzbox/switch.py +++ b/homeassistant/components/fritzbox/switch.py @@ -1,113 +1,99 @@ """Support for AVM Fritz!Box smarthome switch devices.""" -import requests +from typing import Callable from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, + ATTR_NAME, ATTR_TEMPERATURE, - CONF_DEVICES, + ATTR_UNIT_OF_MEASUREMENT, ENERGY_KILO_WATT_HOUR, TEMP_CELSIUS, ) +from homeassistant.core import HomeAssistant +from . import FritzBoxEntity from .const import ( ATTR_STATE_DEVICE_LOCKED, ATTR_STATE_LOCKED, ATTR_TEMPERATURE_UNIT, ATTR_TOTAL_CONSUMPTION, ATTR_TOTAL_CONSUMPTION_UNIT, - CONF_CONNECTIONS, + CONF_COORDINATOR, DOMAIN as FRITZBOX_DOMAIN, - LOGGER, ) ATTR_TOTAL_CONSUMPTION_UNIT_VALUE = ENERGY_KILO_WATT_HOUR -async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up the Fritzbox smarthome switch from config_entry.""" +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable +) -> None: + """Set up the Fritzbox smarthome switch from ConfigEntry.""" entities = [] - devices = hass.data[FRITZBOX_DOMAIN][CONF_DEVICES] - fritz = hass.data[FRITZBOX_DOMAIN][CONF_CONNECTIONS][config_entry.entry_id] - - for device in await hass.async_add_executor_job(fritz.get_devices): - if device.has_switch and device.ain not in devices: - entities.append(FritzboxSwitch(device, fritz)) - devices.add(device.ain) + coordinator = hass.data[FRITZBOX_DOMAIN][entry.entry_id][CONF_COORDINATOR] + + for ain, device in coordinator.data.items(): + if not device.has_switch: + continue + + entities.append( + FritzboxSwitch( + { + ATTR_NAME: f"{device.name}", + ATTR_ENTITY_ID: f"{device.ain}", + ATTR_UNIT_OF_MEASUREMENT: None, + ATTR_DEVICE_CLASS: None, + }, + coordinator, + ain, + ) + ) async_add_entities(entities) -class FritzboxSwitch(SwitchEntity): +class FritzboxSwitch(FritzBoxEntity, SwitchEntity): """The switch class for Fritzbox switches.""" - def __init__(self, device, fritz): - """Initialize the switch.""" - self._device = device - self._fritz = fritz - - @property - def device_info(self): - """Return device specific attributes.""" - return { - "name": self.name, - "identifiers": {(FRITZBOX_DOMAIN, self._device.ain)}, - "manufacturer": self._device.manufacturer, - "model": self._device.productname, - "sw_version": self._device.fw_version, - } - - @property - def unique_id(self): - """Return the unique ID of the device.""" - return self._device.ain - @property def available(self): """Return if switch is available.""" - return self._device.present - - @property - def name(self): - """Return the name of the device.""" - return self._device.name + return self.device.present @property def is_on(self): """Return true if the switch is on.""" - return self._device.switch_state + return self.device.switch_state - def turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the switch on.""" - self._device.set_switch_state_on() + await self.hass.async_add_executor_job(self.device.set_switch_state_on) + await self.coordinator.async_refresh() - def turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the switch off.""" - self._device.set_switch_state_off() - - def update(self): - """Get latest data and states from the device.""" - try: - self._device.update() - except requests.exceptions.HTTPError as ex: - LOGGER.warning("Fritzhome connection error: %s", ex) - self._fritz.login() + await self.hass.async_add_executor_job(self.device.set_switch_state_off) + await self.coordinator.async_refresh() @property def extra_state_attributes(self): """Return the state attributes of the device.""" attrs = {} - attrs[ATTR_STATE_DEVICE_LOCKED] = self._device.device_lock - attrs[ATTR_STATE_LOCKED] = self._device.lock + attrs[ATTR_STATE_DEVICE_LOCKED] = self.device.device_lock + attrs[ATTR_STATE_LOCKED] = self.device.lock - if self._device.has_powermeter: + if self.device.has_powermeter: attrs[ ATTR_TOTAL_CONSUMPTION - ] = f"{((self._device.energy or 0.0) / 1000):.3f}" + ] = f"{((self.device.energy or 0.0) / 1000):.3f}" attrs[ATTR_TOTAL_CONSUMPTION_UNIT] = ATTR_TOTAL_CONSUMPTION_UNIT_VALUE - if self._device.has_temperature_sensor: + if self.device.has_temperature_sensor: attrs[ATTR_TEMPERATURE] = str( self.hass.config.units.temperature( - self._device.temperature, TEMP_CELSIUS + self.device.temperature, TEMP_CELSIUS ) ) attrs[ATTR_TEMPERATURE_UNIT] = self.hass.config.units.temperature_unit @@ -116,4 +102,4 @@ def extra_state_attributes(self): @property def current_power_w(self): """Return the current power usage in W.""" - return self._device.power / 1000 + return self.device.power / 1000 diff --git a/tests/components/fritzbox/test_binary_sensor.py b/tests/components/fritzbox/test_binary_sensor.py index 0d29db2f7b19b..f3334086d7930 100644 --- a/tests/components/fritzbox/test_binary_sensor.py +++ b/tests/components/fritzbox/test_binary_sensor.py @@ -58,7 +58,7 @@ async def test_is_off(hass: HomeAssistant, fritz: Mock): async def test_update(hass: HomeAssistant, fritz: Mock): - """Test update with error.""" + """Test update without error.""" device = FritzDeviceBinarySensorMock() fritz().get_devices.return_value = [device] @@ -91,4 +91,4 @@ async def test_update_error(hass: HomeAssistant, fritz: Mock): await hass.async_block_till_done() assert device.update.call_count == 2 - assert fritz().login.call_count == 2 + assert fritz().login.call_count == 1 diff --git a/tests/components/fritzbox/test_climate.py b/tests/components/fritzbox/test_climate.py index 5453f93609e69..f6fa802a22ee4 100644 --- a/tests/components/fritzbox/test_climate.py +++ b/tests/components/fritzbox/test_climate.py @@ -105,7 +105,7 @@ async def test_target_temperature_off(hass: HomeAssistant, fritz: Mock): async def test_update(hass: HomeAssistant, fritz: Mock): - """Test update with error.""" + """Test update without error.""" device = FritzDeviceClimateMock() fritz().get_devices.return_value = [device] @@ -126,7 +126,7 @@ async def test_update(hass: HomeAssistant, fritz: Mock): await hass.async_block_till_done() state = hass.states.get(ENTITY_ID) - assert device.update.call_count == 1 + assert device.update.call_count == 2 assert state assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 19 assert state.attributes[ATTR_TEMPERATURE] == 20 @@ -139,14 +139,14 @@ async def test_update_error(hass: HomeAssistant, fritz: Mock): fritz().get_devices.return_value = [device] await setup_fritzbox(hass, MOCK_CONFIG) - assert device.update.call_count == 0 + assert device.update.call_count == 1 assert fritz().login.call_count == 1 next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) await hass.async_block_till_done() - assert device.update.call_count == 1 + assert device.update.call_count == 2 assert fritz().login.call_count == 2 @@ -290,7 +290,7 @@ async def test_preset_mode_update(hass: HomeAssistant, fritz: Mock): await hass.async_block_till_done() state = hass.states.get(ENTITY_ID) - assert device.update.call_count == 1 + assert device.update.call_count == 2 assert state assert state.attributes[ATTR_PRESET_MODE] == PRESET_COMFORT @@ -301,6 +301,6 @@ async def test_preset_mode_update(hass: HomeAssistant, fritz: Mock): await hass.async_block_till_done() state = hass.states.get(ENTITY_ID) - assert device.update.call_count == 2 + assert device.update.call_count == 3 assert state assert state.attributes[ATTR_PRESET_MODE] == PRESET_ECO diff --git a/tests/components/fritzbox/test_init.py b/tests/components/fritzbox/test_init.py index bb5faa2c4d9d1..75d544ec21c21 100644 --- a/tests/components/fritzbox/test_init.py +++ b/tests/components/fritzbox/test_init.py @@ -2,6 +2,7 @@ from unittest.mock import Mock, call, patch from pyfritzhome import LoginError +from requests.exceptions import HTTPError from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN @@ -57,6 +58,39 @@ async def test_setup_duplicate_config(hass: HomeAssistant, fritz: Mock, caplog): assert "duplicate host entries found" in caplog.text +async def test_coordinator_update_after_reboot(hass: HomeAssistant, fritz: Mock): + """Test coordinator after reboot.""" + entry = MockConfigEntry( + domain=FB_DOMAIN, + data=MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], + unique_id="any", + ) + entry.add_to_hass(hass) + fritz().get_devices.side_effect = [HTTPError(), ""] + + assert await hass.config_entries.async_setup(entry.entry_id) + assert fritz().get_devices.call_count == 2 + assert fritz().login.call_count == 2 + + +async def test_coordinator_update_after_password_change( + hass: HomeAssistant, fritz: Mock +): + """Test coordinator after password change.""" + entry = MockConfigEntry( + domain=FB_DOMAIN, + data=MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], + unique_id="any", + ) + entry.add_to_hass(hass) + fritz().get_devices.side_effect = HTTPError() + fritz().login.side_effect = ["", HTTPError()] + + assert not await hass.config_entries.async_setup(entry.entry_id) + assert fritz().get_devices.call_count == 1 + assert fritz().login.call_count == 2 + + async def test_unload_remove(hass: HomeAssistant, fritz: Mock): """Test unload and remove of integration.""" fritz().get_devices.return_value = [FritzDeviceSwitchMock()] @@ -107,9 +141,10 @@ async def test_raise_config_entry_not_ready_when_offline(hass): with patch( "homeassistant.components.fritzbox.Fritzhome.login", side_effect=LoginError("user"), - ): + ) as mock_login: await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() + mock_login.assert_called_once() entries = hass.config_entries.async_entries() config_entry = entries[0] diff --git a/tests/components/fritzbox/test_sensor.py b/tests/components/fritzbox/test_sensor.py index d26f2b935e980..331babe8af7ee 100644 --- a/tests/components/fritzbox/test_sensor.py +++ b/tests/components/fritzbox/test_sensor.py @@ -57,19 +57,19 @@ async def test_setup(hass: HomeAssistant, fritz: Mock): async def test_update(hass: HomeAssistant, fritz: Mock): - """Test update with error.""" + """Test update without error.""" device = FritzDeviceSensorMock() fritz().get_devices.return_value = [device] await setup_fritzbox(hass, MOCK_CONFIG) - assert device.update.call_count == 0 + assert device.update.call_count == 1 assert fritz().login.call_count == 1 next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) await hass.async_block_till_done() - assert device.update.call_count == 1 + assert device.update.call_count == 2 assert fritz().login.call_count == 1 @@ -80,12 +80,12 @@ async def test_update_error(hass: HomeAssistant, fritz: Mock): fritz().get_devices.return_value = [device] await setup_fritzbox(hass, MOCK_CONFIG) - assert device.update.call_count == 0 + assert device.update.call_count == 1 assert fritz().login.call_count == 1 next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) await hass.async_block_till_done() - assert device.update.call_count == 1 + assert device.update.call_count == 2 assert fritz().login.call_count == 2 diff --git a/tests/components/fritzbox/test_switch.py b/tests/components/fritzbox/test_switch.py index 31198aa950db3..8546b6bf10a55 100644 --- a/tests/components/fritzbox/test_switch.py +++ b/tests/components/fritzbox/test_switch.py @@ -87,19 +87,19 @@ async def test_turn_off(hass: HomeAssistant, fritz: Mock): async def test_update(hass: HomeAssistant, fritz: Mock): - """Test update with error.""" + """Test update without error.""" device = FritzDeviceSwitchMock() fritz().get_devices.return_value = [device] await setup_fritzbox(hass, MOCK_CONFIG) - assert device.update.call_count == 0 + assert device.update.call_count == 1 assert fritz().login.call_count == 1 next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) await hass.async_block_till_done() - assert device.update.call_count == 1 + assert device.update.call_count == 2 assert fritz().login.call_count == 1 @@ -110,12 +110,12 @@ async def test_update_error(hass: HomeAssistant, fritz: Mock): fritz().get_devices.return_value = [device] await setup_fritzbox(hass, MOCK_CONFIG) - assert device.update.call_count == 0 + assert device.update.call_count == 1 assert fritz().login.call_count == 1 next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) await hass.async_block_till_done() - assert device.update.call_count == 1 + assert device.update.call_count == 2 assert fritz().login.call_count == 2 From f11834d85c457151b3683e080cb5dfc8ad1ab301 Mon Sep 17 00:00:00 2001 From: Daniel Pervan Date: Sun, 25 Apr 2021 02:40:39 +0200 Subject: [PATCH 0500/1317] Fix August Type error (#49636) --- homeassistant/components/august/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index 810e4d056388b..e966338f287cd 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -2,7 +2,7 @@ "domain": "august", "name": "August", "documentation": "https://www.home-assistant.io/integrations/august", - "requirements": ["yalexs==1.1.10"], + "requirements": ["yalexs==1.1.11"], "codeowners": ["@bdraco"], "dhcp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 81af5e09aa333..ac7d2501418a8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2369,7 +2369,7 @@ xs1-api-client==3.0.0 yalesmartalarmclient==0.1.6 # homeassistant.components.august -yalexs==1.1.10 +yalexs==1.1.11 # homeassistant.components.yeelight yeelight==0.6.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e74d0517ccb78..b6e322f78b73b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1254,7 +1254,7 @@ xknx==0.18.1 xmltodict==0.12.0 # homeassistant.components.august -yalexs==1.1.10 +yalexs==1.1.11 # homeassistant.components.yeelight yeelight==0.6.1 From aaba9766ffbf07ce80b63b6b70bfc23ebcac8a71 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 24 Apr 2021 15:16:52 -1000 Subject: [PATCH 0501/1317] Bump scapy to 2.4.5 for dhcp (#49437) --- homeassistant/components/dhcp/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/dhcp/manifest.json b/homeassistant/components/dhcp/manifest.json index 47cdc464fadb6..5808226500679 100644 --- a/homeassistant/components/dhcp/manifest.json +++ b/homeassistant/components/dhcp/manifest.json @@ -2,7 +2,7 @@ "domain": "dhcp", "name": "DHCP Discovery", "documentation": "https://www.home-assistant.io/integrations/dhcp", - "requirements": ["scapy==2.4.4", "aiodiscover==1.4.0"], + "requirements": ["scapy==2.4.5", "aiodiscover==1.4.0"], "codeowners": ["@bdraco"], "quality_scale": "internal", "iot_class": "local_push" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b8a31c5fcc6df..6b8449f012099 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -29,7 +29,7 @@ pytz>=2021.1 pyyaml==5.4.1 requests==2.25.1 ruamel.yaml==0.15.100 -scapy==2.4.4 +scapy==2.4.5 sqlalchemy==1.4.11 voluptuous-serialize==2.4.0 voluptuous==0.12.1 diff --git a/requirements_all.txt b/requirements_all.txt index ac7d2501418a8..a6bc723d27525 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2027,7 +2027,7 @@ samsungtvws==1.6.0 satel_integra==0.3.4 # homeassistant.components.dhcp -scapy==2.4.4 +scapy==2.4.5 # homeassistant.components.deutsche_bahn schiene==0.23 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b6e322f78b73b..460aa48391883 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1079,7 +1079,7 @@ samsungctl[websocket]==0.7.1 samsungtvws==1.6.0 # homeassistant.components.dhcp -scapy==2.4.4 +scapy==2.4.5 # homeassistant.components.screenlogic screenlogicpy==0.3.0 From 34a588d1ba2de83ba9163a54a86ac14e7a2de652 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sun, 25 Apr 2021 07:47:18 +0300 Subject: [PATCH 0502/1317] Fix Shelly button first trigger (#49635) --- homeassistant/components/shelly/__init__.py | 12 ++++++++++++ homeassistant/components/shelly/const.py | 4 +++- homeassistant/components/shelly/device_trigger.py | 7 ++++--- homeassistant/components/shelly/utils.py | 9 +++++---- 4 files changed, 24 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index be87e2556eb80..1e68ca784097e 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -33,6 +33,7 @@ POLLING_TIMEOUT_SEC, REST, REST_SENSORS_UPDATE_INTERVAL, + SHBTN_MODELS, SLEEP_PERIOD_MULTIPLIER, UPDATE_PERIOD_MULTIPLIER, ) @@ -181,6 +182,17 @@ def _async_device_updates_handler(self): if not self.device.initialized: return + # For buttons which are battery powered - set initial value for last_event_count + if self.model in SHBTN_MODELS and self._last_input_events_count.get(1) is None: + for block in self.device.blocks: + if block.type != "device": + continue + + if block.wakeupEvent[0] == "button": + self._last_input_events_count[1] = -1 + + break + # Check for input events for block in self.device.blocks: if ( diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index 4fda656e7b446..2609b7cd57f23 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -49,7 +49,7 @@ "long", } -SHBTN_1_INPUTS_EVENTS_TYPES = { +SHBTN_INPUTS_EVENTS_TYPES = { "single", "double", "triple", @@ -72,6 +72,8 @@ "button3": 3, } +SHBTN_MODELS = ["SHBTN-1", "SHBTN-2"] + # Kelvin value for colorTemp KELVIN_MAX_VALUE = 6500 KELVIN_MIN_VALUE_WHITE = 2700 diff --git a/homeassistant/components/shelly/device_trigger.py b/homeassistant/components/shelly/device_trigger.py index 9793804054370..05f806dd8e888 100644 --- a/homeassistant/components/shelly/device_trigger.py +++ b/homeassistant/components/shelly/device_trigger.py @@ -27,7 +27,8 @@ DOMAIN, EVENT_SHELLY_CLICK, INPUTS_EVENTS_SUBTYPES, - SHBTN_1_INPUTS_EVENTS_TYPES, + SHBTN_INPUTS_EVENTS_TYPES, + SHBTN_MODELS, SUPPORTED_INPUTS_EVENTS_TYPES, ) from .utils import get_device_wrapper, get_input_triggers @@ -69,8 +70,8 @@ async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]: if not wrapper: raise InvalidDeviceAutomationConfig(f"Device not found: {device_id}") - if wrapper.model in ("SHBTN-1", "SHBTN-2"): - for trigger in SHBTN_1_INPUTS_EVENTS_TYPES: + if wrapper.model in SHBTN_MODELS: + for trigger in SHBTN_INPUTS_EVENTS_TYPES: triggers.append( { CONF_PLATFORM: "device", diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index 126491f65c130..39134957fb99b 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -16,7 +16,8 @@ COAP, DATA_CONFIG_ENTRY, DOMAIN, - SHBTN_1_INPUTS_EVENTS_TYPES, + SHBTN_INPUTS_EVENTS_TYPES, + SHBTN_MODELS, SHIX3_1_INPUTS_EVENTS_TYPES, ) @@ -111,7 +112,7 @@ def get_device_channel_name( def is_momentary_input(settings: dict, block: aioshelly.Block) -> bool: """Return true if input button settings is set to a momentary type.""" # Shelly Button type is fixed to momentary and no btn_type - if settings["device"]["type"] in ("SHBTN-1", "SHBTN-2"): + if settings["device"]["type"] in SHBTN_MODELS: return True button = settings.get("relays") or settings.get("lights") or settings.get("inputs") @@ -158,8 +159,8 @@ def get_input_triggers( else: subtype = f"button{int(block.channel)+1}" - if device.settings["device"]["type"] in ("SHBTN-1", "SHBTN-2"): - trigger_types = SHBTN_1_INPUTS_EVENTS_TYPES + if device.settings["device"]["type"] in SHBTN_MODELS: + trigger_types = SHBTN_INPUTS_EVENTS_TYPES elif device.settings["device"]["type"] == "SHIX3-1": trigger_types = SHIX3_1_INPUTS_EVENTS_TYPES else: From 153d6e891efbc22297ecf6c9ce6786ac34c0356d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sun, 25 Apr 2021 12:27:40 +0300 Subject: [PATCH 0503/1317] Use config_entries.SOURCE_* constants (#49631) --- .../components/deconz/config_flow.py | 2 +- homeassistant/components/hassio/discovery.py | 5 +- homeassistant/components/heos/__init__.py | 4 +- .../components/samsungtv/__init__.py | 5 +- .../components/smartthings/__init__.py | 8 +- homeassistant/components/zeroconf/__init__.py | 6 +- tests/components/abode/test_config_flow.py | 4 +- tests/components/adguard/test_config_flow.py | 10 +- .../components/airvisual/test_config_flow.py | 4 +- tests/components/almond/test_config_flow.py | 2 +- tests/components/apple_tv/test_config_flow.py | 6 +- tests/components/august/test_config_flow.py | 4 +- tests/components/awair/test_config_flow.py | 8 +- .../azure_devops/test_config_flow.py | 16 +++- tests/components/blink/test_config_flow.py | 2 +- .../bmw_connected_drive/test_config_flow.py | 2 +- .../components/broadlink/test_config_flow.py | 20 ++-- tests/components/cast/test_config_flow.py | 19 ++-- .../cert_expiry/test_config_flow.py | 32 ++++--- .../components/config/test_config_entries.py | 10 +- tests/components/deconz/test_gateway.py | 4 +- .../devolo_home_control/test_config_flow.py | 3 +- tests/components/dhcp/test_init.py | 41 ++++++-- tests/components/dialogflow/test_init.py | 4 +- tests/components/eafm/test_config_flow.py | 7 +- tests/components/emonitor/test_config_flow.py | 6 +- tests/components/enocean/test_config_flow.py | 16 ++-- .../enphase_envoy/test_config_flow.py | 8 +- tests/components/esphome/test_config_flow.py | 27 +++--- .../fireservicerota/test_config_flow.py | 15 +-- tests/components/fritzbox/test_config_flow.py | 9 +- .../garmin_connect/test_config_flow.py | 16 ++-- tests/components/gdacs/test_config_flow.py | 10 +- tests/components/geofency/test_init.py | 4 +- .../geonetnz_quakes/test_config_flow.py | 10 +- tests/components/glances/test_config_flow.py | 10 +- tests/components/gpslogger/test_init.py | 4 +- tests/components/heos/test_config_flow.py | 12 +-- .../homekit_controller/test_config_flow.py | 93 ++++++++++++------- .../homematicip_cloud/test_config_flow.py | 31 +++++-- tests/components/hue/test_config_flow.py | 52 ++++++----- .../test_config_flow.py | 8 +- .../hvv_departures/test_config_flow.py | 6 +- tests/components/ifttt/test_init.py | 4 +- .../islamic_prayer_times/test_config_flow.py | 8 +- .../keenetic_ndms2/test_config_flow.py | 4 +- tests/components/kodi/test_config_flow.py | 32 +++++-- .../components/konnected/test_config_flow.py | 23 ++--- tests/components/litejet/test_config_flow.py | 11 ++- tests/components/locative/test_init.py | 4 +- .../lutron_caseta/test_config_flow.py | 4 +- tests/components/lyric/test_config_flow.py | 2 +- tests/components/mailgun/test_init.py | 6 +- tests/components/mazda/test_config_flow.py | 30 ++++-- tests/components/met/test_config_flow.py | 11 ++- tests/components/met/test_weather.py | 3 +- tests/components/mikrotik/test_config_flow.py | 16 ++-- tests/components/mill/test_config_flow.py | 9 +- tests/components/mqtt/test_config_flow.py | 14 +-- tests/components/myq/test_config_flow.py | 4 +- tests/components/netatmo/test_config_flow.py | 2 +- tests/components/onewire/__init__.py | 8 +- tests/components/onewire/test_init.py | 5 +- tests/components/onvif/test_config_flow.py | 10 +- .../components/ovo_energy/test_config_flow.py | 12 ++- .../components/owntracks/test_config_flow.py | 6 +- tests/components/plex/test_config_flow.py | 32 ++++--- .../components/powerwall/test_config_flow.py | 6 +- tests/components/ps4/test_config_flow.py | 30 +++--- .../pvpc_hourly_pricing/test_config_flow.py | 8 +- tests/components/rachio/test_config_flow.py | 4 +- tests/components/roomba/test_config_flow.py | 4 +- .../components/samsungtv/test_config_flow.py | 55 +++++------ .../screenlogic/test_config_flow.py | 2 +- tests/components/sharkiq/test_config_flow.py | 6 +- .../components/simplisafe/test_config_flow.py | 4 +- tests/components/sma/conftest.py | 3 +- .../smartthings/test_config_flow.py | 32 +++---- tests/components/smartthings/test_init.py | 5 +- .../somfy_mylink/test_config_flow.py | 4 +- .../speedtestdotnet/test_config_flow.py | 10 +- tests/components/spotify/test_config_flow.py | 6 +- tests/components/srp_energy/__init__.py | 2 +- .../components/srp_energy/test_config_flow.py | 10 +- tests/components/ssdp/test_init.py | 13 ++- tests/components/starline/test_config_flow.py | 7 +- tests/components/tado/test_config_flow.py | 4 +- tests/components/tasmota/test_config_flow.py | 20 ++-- tests/components/tibber/test_config_flow.py | 7 +- .../totalconnect/test_config_flow.py | 4 +- tests/components/traccar/test_init.py | 4 +- tests/components/tradfri/test_config_flow.py | 30 +++--- tests/components/tradfri/test_init.py | 5 +- .../transmission/test_config_flow.py | 26 ++++-- tests/components/twilio/test_init.py | 4 +- tests/components/unifi/test_config_flow.py | 14 +-- tests/components/volumio/test_config_flow.py | 12 ++- tests/components/withings/test_config_flow.py | 4 +- tests/components/zha/test_config_flow.py | 6 +- tests/components/zwave/test_websocket_api.py | 3 +- tests/helpers/test_config_entry_flow.py | 22 ++++- tests/test_config_entries.py | 2 +- tests/test_data_entry_flow.py | 8 +- 103 files changed, 723 insertions(+), 488 deletions(-) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index d1ea3826e2fa6..2029903bedfb0 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -199,7 +199,7 @@ async def async_step_ssdp(self, discovery_info): parsed_url = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]) entry = await self.async_set_unique_id(self.bridge_id) - if entry and entry.source == "hassio": + if entry and entry.source == config_entries.SOURCE_HASSIO: return self.async_abort(reason="already_configured") self._abort_if_unique_id_configured( diff --git a/homeassistant/components/hassio/discovery.py b/homeassistant/components/hassio/discovery.py index c682e34c30106..e7f8df3b61df6 100644 --- a/homeassistant/components/hassio/discovery.py +++ b/homeassistant/components/hassio/discovery.py @@ -5,6 +5,7 @@ from aiohttp import web from aiohttp.web_exceptions import HTTPServiceUnavailable +from homeassistant import config_entries from homeassistant.components.http import HomeAssistantView from homeassistant.const import ATTR_NAME, ATTR_SERVICE, EVENT_HOMEASSISTANT_START from homeassistant.core import callback @@ -87,7 +88,7 @@ async def async_process_new(self, data): # Use config flow await self.hass.config_entries.flow.async_init( - service, context={"source": "hassio"}, data=config_data + service, context={"source": config_entries.SOURCE_HASSIO}, data=config_data ) async def async_process_del(self, data): @@ -106,6 +107,6 @@ async def async_process_del(self, data): # Use config flow for entry in self.hass.config_entries.async_entries(service): - if entry.source != "hassio": + if entry.source != config_entries.SOURCE_HASSIO: continue await self.hass.config_entries.async_remove(entry) diff --git a/homeassistant/components/heos/__init__.py b/homeassistant/components/heos/__init__.py index fb51c1d158cab..652aa84483265 100644 --- a/homeassistant/components/heos/__init__.py +++ b/homeassistant/components/heos/__init__.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant.components.media_player.const import DOMAIN as MEDIA_PLAYER_DOMAIN -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady @@ -47,7 +47,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType): # Create new entry based on config hass.async_create_task( hass.config_entries.flow.async_init( - DOMAIN, context={"source": "import"}, data={CONF_HOST: host} + DOMAIN, context={"source": SOURCE_IMPORT}, data={CONF_HOST: host} ) ) else: diff --git a/homeassistant/components/samsungtv/__init__.py b/homeassistant/components/samsungtv/__init__.py index 8c17ff4794c83..64646533b2d94 100644 --- a/homeassistant/components/samsungtv/__init__.py +++ b/homeassistant/components/samsungtv/__init__.py @@ -3,6 +3,7 @@ import voluptuous as vol +from homeassistant import config_entries from homeassistant.components.media_player.const import DOMAIN as MP_DOMAIN from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT import homeassistant.helpers.config_validation as cv @@ -53,7 +54,9 @@ async def async_setup(hass, config): } hass.async_create_task( hass.config_entries.flow.async_init( - DOMAIN, context={"source": "import"}, data=entry_config + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=entry_config, ) ) diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index 456857efc9b4c..d9a96301e6678 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -10,7 +10,7 @@ from pysmartapp.event import EVENT_TYPE_DEVICE from pysmartthings import Attribute, Capability, SmartThings -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_ACCESS_TOKEN, CONF_CLIENT_ID, @@ -75,7 +75,9 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry): flows = hass.config_entries.flow.async_progress() if not [flow for flow in flows if flow["handler"] == DOMAIN]: hass.async_create_task( - hass.config_entries.flow.async_init(DOMAIN, context={"source": "import"}) + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT} + ) ) # Return False because it could not be migrated. @@ -182,7 +184,7 @@ async def retrieve_device_status(device): if not [flow for flow in flows if flow["handler"] == DOMAIN]: hass.async_create_task( hass.config_entries.flow.async_init( - DOMAIN, context={"source": "import"} + DOMAIN, context={"source": SOURCE_IMPORT} ) ) return False diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 7d4205279ed54..58d8ad21094a2 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -23,7 +23,7 @@ Zeroconf, ) -from homeassistant import util +from homeassistant import config_entries, util from homeassistant.const import ( EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STARTED, @@ -401,7 +401,9 @@ def handle_homekit( hass.add_job( hass.config_entries.flow.async_init( - homekit_models[test_model], context={"source": "homekit"}, data=info + homekit_models[test_model], + context={"source": config_entries.SOURCE_HOMEKIT}, + data=info, ) # type: ignore ) return True diff --git a/tests/components/abode/test_config_flow.py b/tests/components/abode/test_config_flow.py index 026735ed536cf..806038194bbdb 100644 --- a/tests/components/abode/test_config_flow.py +++ b/tests/components/abode/test_config_flow.py @@ -7,7 +7,7 @@ from homeassistant import data_entry_flow from homeassistant.components.abode import config_flow from homeassistant.components.abode.const import DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH, SOURCE_USER from homeassistant.const import ( CONF_PASSWORD, CONF_USERNAME, @@ -190,7 +190,7 @@ async def test_step_reauth(hass): ): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": "reauth"}, + context={"source": SOURCE_REAUTH}, data=conf, ) diff --git a/tests/components/adguard/test_config_flow.py b/tests/components/adguard/test_config_flow.py index 17fcbda666da2..872f9e5807ee7 100644 --- a/tests/components/adguard/test_config_flow.py +++ b/tests/components/adguard/test_config_flow.py @@ -96,7 +96,7 @@ async def test_integration_already_exists(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, data={"host": "mock-adguard", "port": "3000"}, - context={"source": "user"}, + context={"source": config_entries.SOURCE_USER}, ) assert result["type"] == "abort" assert result["reason"] == "already_configured" @@ -111,7 +111,7 @@ async def test_hassio_already_configured(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, data={"addon": "AdGuard Home Addon", "host": "mock-adguard", "port": "3000"}, - context={"source": "hassio"}, + context={"source": config_entries.SOURCE_HASSIO}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" @@ -126,7 +126,7 @@ async def test_hassio_ignored(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, data={"addon": "AdGuard Home Addon", "host": "mock-adguard", "port": "3000"}, - context={"source": "hassio"}, + context={"source": config_entries.SOURCE_HASSIO}, ) assert "type" in result @@ -148,7 +148,7 @@ async def test_hassio_confirm( result = await hass.config_entries.flow.async_init( DOMAIN, data={"addon": "AdGuard Home Addon", "host": "mock-adguard", "port": 3000}, - context={"source": "hassio"}, + context={"source": config_entries.SOURCE_HASSIO}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "hassio_confirm" @@ -176,7 +176,7 @@ async def test_hassio_connection_error( result = await hass.config_entries.flow.async_init( DOMAIN, data={"addon": "AdGuard Home Addon", "host": "mock-adguard", "port": 3000}, - context={"source": "hassio"}, + context={"source": config_entries.SOURCE_HASSIO}, ) result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) diff --git a/tests/components/airvisual/test_config_flow.py b/tests/components/airvisual/test_config_flow.py index 248abaf6b5f69..d7c5a08b62a22 100644 --- a/tests/components/airvisual/test_config_flow.py +++ b/tests/components/airvisual/test_config_flow.py @@ -19,7 +19,7 @@ INTEGRATION_TYPE_GEOGRAPHY_NAME, INTEGRATION_TYPE_NODE_PRO, ) -from homeassistant.config_entries import SOURCE_USER +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import ( CONF_API_KEY, CONF_IP_ADDRESS, @@ -349,7 +349,7 @@ async def test_step_reauth(hass): ).add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "reauth"}, data=entry_data + DOMAIN, context={"source": SOURCE_REAUTH}, data=entry_data ) assert result["step_id"] == "reauth_confirm" diff --git a/tests/components/almond/test_config_flow.py b/tests/components/almond/test_config_flow.py index 8f6b68e47ee78..892abaa9650a0 100644 --- a/tests/components/almond/test_config_flow.py +++ b/tests/components/almond/test_config_flow.py @@ -49,7 +49,7 @@ async def test_hassio(hass): """Test that Hass.io can discover this integration.""" result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": "hassio"}, + context={"source": config_entries.SOURCE_HASSIO}, data={"addon": "Almond add-on", "host": "almond-addon", "port": "1234"}, ) diff --git a/tests/components/apple_tv/test_config_flow.py b/tests/components/apple_tv/test_config_flow.py index c55bef8edfd89..615a1f404f57f 100644 --- a/tests/components/apple_tv/test_config_flow.py +++ b/tests/components/apple_tv/test_config_flow.py @@ -519,7 +519,7 @@ async def test_reconfigure_update_credentials(hass, mrp_device, pairing): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": "reauth"}, + context={"source": config_entries.SOURCE_REAUTH}, data={"identifier": "mrpid", "name": "apple tv"}, ) @@ -552,11 +552,11 @@ async def test_reconfigure_ongoing_aborts(hass, mrp_device): } await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "reauth"}, data=data + DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=data ) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "reauth"}, data=data + DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=data ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_in_progress" diff --git a/tests/components/august/test_config_flow.py b/tests/components/august/test_config_flow.py index c87291e0f79d2..53dac38fb1e3b 100644 --- a/tests/components/august/test_config_flow.py +++ b/tests/components/august/test_config_flow.py @@ -238,7 +238,7 @@ async def test_form_reauth(hass): entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "reauth"}, data=entry.data + DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=entry.data ) assert result["type"] == "form" assert result["errors"] == {} @@ -284,7 +284,7 @@ async def test_form_reauth_with_2fa(hass): entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "reauth"}, data=entry.data + DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=entry.data ) assert result["type"] == "form" assert result["errors"] == {} diff --git a/tests/components/awair/test_config_flow.py b/tests/components/awair/test_config_flow.py index 92d92dd5a63c9..84b922291610b 100644 --- a/tests/components/awair/test_config_flow.py +++ b/tests/components/awair/test_config_flow.py @@ -6,7 +6,7 @@ from homeassistant import data_entry_flow from homeassistant.components.awair.const import DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_ACCESS_TOKEN from .const import CONFIG, DEVICES_FIXTURE, NO_DEVICES_FIXTURE, UNIQUE_ID, USER_FIXTURE @@ -156,7 +156,7 @@ async def test_reauth(hass): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": "reauth", "unique_id": UNIQUE_ID}, + context={"source": SOURCE_REAUTH, "unique_id": UNIQUE_ID}, data=CONFIG, ) @@ -166,7 +166,7 @@ async def test_reauth(hass): with patch("python_awair.AwairClient.query", side_effect=AuthError()): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": "reauth", "unique_id": UNIQUE_ID}, + context={"source": SOURCE_REAUTH, "unique_id": UNIQUE_ID}, data=CONFIG, ) @@ -175,7 +175,7 @@ async def test_reauth(hass): with patch("python_awair.AwairClient.query", side_effect=AwairError()): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": "reauth", "unique_id": UNIQUE_ID}, + context={"source": SOURCE_REAUTH, "unique_id": UNIQUE_ID}, data=CONFIG, ) diff --git a/tests/components/azure_devops/test_config_flow.py b/tests/components/azure_devops/test_config_flow.py index 744d04042e028..7817f6fc5707e 100644 --- a/tests/components/azure_devops/test_config_flow.py +++ b/tests/components/azure_devops/test_config_flow.py @@ -62,7 +62,9 @@ async def test_reauth_authorization_error(hass: HomeAssistant) -> None: return_value=False, ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT + DOMAIN, + context={"source": config_entries.SOURCE_REAUTH}, + data=FIXTURE_USER_INPUT, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -110,7 +112,9 @@ async def test_reauth_connection_error(hass: HomeAssistant) -> None: side_effect=aiohttp.ClientError, ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT + DOMAIN, + context={"source": config_entries.SOURCE_REAUTH}, + data=FIXTURE_USER_INPUT, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -168,7 +172,9 @@ async def test_reauth_project_error(hass: HomeAssistant) -> None: return_value=None, ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT + DOMAIN, + context={"source": config_entries.SOURCE_REAUTH}, + data=FIXTURE_USER_INPUT, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -197,7 +203,9 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: mock_config.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT + DOMAIN, + context={"source": config_entries.SOURCE_REAUTH}, + data=FIXTURE_USER_INPUT, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM diff --git a/tests/components/blink/test_config_flow.py b/tests/components/blink/test_config_flow.py index 2d1746b87ba3d..7da395a6f1f02 100644 --- a/tests/components/blink/test_config_flow.py +++ b/tests/components/blink/test_config_flow.py @@ -246,7 +246,7 @@ async def test_form_unknown_error(hass): async def test_reauth_shows_user_step(hass): """Test reauth shows the user form.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "reauth"} + DOMAIN, context={"source": config_entries.SOURCE_REAUTH} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" diff --git a/tests/components/bmw_connected_drive/test_config_flow.py b/tests/components/bmw_connected_drive/test_config_flow.py index d56978deb2707..ba3c69fe9c568 100644 --- a/tests/components/bmw_connected_drive/test_config_flow.py +++ b/tests/components/bmw_connected_drive/test_config_flow.py @@ -30,7 +30,7 @@ }, "options": {CONF_READ_ONLY: False, CONF_USE_LOCATION: False}, "system_options": {"disable_new_entities": False}, - "source": "user", + "source": config_entries.SOURCE_USER, "connection_class": config_entries.CONN_CLASS_CLOUD_POLL, "unique_id": f"{FIXTURE_USER_INPUT[CONF_REGION]}-{FIXTURE_USER_INPUT[CONF_REGION]}", } diff --git a/tests/components/broadlink/test_config_flow.py b/tests/components/broadlink/test_config_flow.py index 135362d62d9df..68f1c54f6973a 100644 --- a/tests/components/broadlink/test_config_flow.py +++ b/tests/components/broadlink/test_config_flow.py @@ -733,7 +733,7 @@ async def test_flow_reauth_works(hass): with patch(DEVICE_FACTORY, return_value=mock_api): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "reauth"}, data=data + DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=data ) assert result["type"] == "form" @@ -769,7 +769,7 @@ async def test_flow_reauth_invalid_host(hass): with patch(DEVICE_FACTORY, return_value=mock_api): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "reauth"}, data=data + DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=data ) device.mac = get_device("Office").mac @@ -803,7 +803,7 @@ async def test_flow_reauth_valid_host(hass): with patch(DEVICE_FACTORY, return_value=mock_api): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "reauth"}, data=data + DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=data ) device.host = "192.168.1.128" @@ -834,7 +834,7 @@ async def test_dhcp_can_finish(hass): with patch(DEVICE_HELLO, return_value=mock_api): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": "dhcp"}, + context={"source": config_entries.SOURCE_DHCP}, data={ HOSTNAME: "broadlink", IP_ADDRESS: "1.2.3.4", @@ -868,7 +868,7 @@ async def test_dhcp_fails_to_connect(hass): with patch(DEVICE_HELLO, side_effect=blke.NetworkTimeoutError()): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": "dhcp"}, + context={"source": config_entries.SOURCE_DHCP}, data={ HOSTNAME: "broadlink", IP_ADDRESS: "1.2.3.4", @@ -887,7 +887,7 @@ async def test_dhcp_unreachable(hass): with patch(DEVICE_HELLO, side_effect=OSError(errno.ENETUNREACH, None)): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": "dhcp"}, + context={"source": config_entries.SOURCE_DHCP}, data={ HOSTNAME: "broadlink", IP_ADDRESS: "1.2.3.4", @@ -906,7 +906,7 @@ async def test_dhcp_connect_unknown_error(hass): with patch(DEVICE_HELLO, side_effect=OSError()): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": "dhcp"}, + context={"source": config_entries.SOURCE_DHCP}, data={ HOSTNAME: "broadlink", IP_ADDRESS: "1.2.3.4", @@ -928,7 +928,7 @@ async def test_dhcp_device_not_supported(hass): with patch(DEVICE_HELLO, return_value=mock_api): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": "dhcp"}, + context={"source": config_entries.SOURCE_DHCP}, data={ HOSTNAME: "broadlink", IP_ADDRESS: device.host, @@ -952,7 +952,7 @@ async def test_dhcp_already_exists(hass): with patch(DEVICE_HELLO, return_value=mock_api): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": "dhcp"}, + context={"source": config_entries.SOURCE_DHCP}, data={ HOSTNAME: "broadlink", IP_ADDRESS: "1.2.3.4", @@ -978,7 +978,7 @@ async def test_dhcp_updates_host(hass): with patch(DEVICE_HELLO, return_value=mock_api): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": "dhcp"}, + context={"source": config_entries.SOURCE_DHCP}, data={ HOSTNAME: "broadlink", IP_ADDRESS: "4.5.6.7", diff --git a/tests/components/cast/test_config_flow.py b/tests/components/cast/test_config_flow.py index 1febd9d880324..cc67d58502230 100644 --- a/tests/components/cast/test_config_flow.py +++ b/tests/components/cast/test_config_flow.py @@ -34,7 +34,14 @@ async def test_creating_entry_sets_up_media_player(hass): assert len(mock_setup.mock_calls) == 1 -@pytest.mark.parametrize("source", ["import", "user", "zeroconf"]) +@pytest.mark.parametrize( + "source", + [ + config_entries.SOURCE_IMPORT, + config_entries.SOURCE_USER, + config_entries.SOURCE_ZEROCONF, + ], +) async def test_single_instance(hass, source): """Test we only allow a single config flow.""" MockConfigEntry(domain="cast").add_to_hass(hass) @@ -50,7 +57,7 @@ async def test_single_instance(hass, source): async def test_user_setup(hass): """Test we can finish a config flow.""" result = await hass.config_entries.flow.async_init( - "cast", context={"source": "user"} + "cast", context={"source": config_entries.SOURCE_USER} ) assert result["type"] == "form" @@ -70,7 +77,7 @@ async def test_user_setup(hass): async def test_user_setup_options(hass): """Test we can finish a config flow.""" result = await hass.config_entries.flow.async_init( - "cast", context={"source": "user"} + "cast", context={"source": config_entries.SOURCE_USER} ) assert result["type"] == "form" @@ -92,7 +99,7 @@ async def test_user_setup_options(hass): async def test_zeroconf_setup(hass): """Test we can finish a config flow through zeroconf.""" result = await hass.config_entries.flow.async_init( - "cast", context={"source": "zeroconf"} + "cast", context={"source": config_entries.SOURCE_ZEROCONF} ) assert result["type"] == "form" @@ -169,7 +176,7 @@ async def test_option_flow(hass, parameter_data): orig_data = dict(config_entry.data) # Reconfigure ignore_cec, known_hosts, uuid - context = {"source": "user", "show_advanced_options": True} + context = {"source": config_entries.SOURCE_USER, "show_advanced_options": True} result = await hass.config_entries.options.async_init( config_entry.entry_id, context=context ) @@ -213,7 +220,7 @@ async def test_option_flow(hass, parameter_data): async def test_known_hosts(hass, castbrowser_mock, castbrowser_constructor_mock): """Test known hosts is passed to pychromecasts.""" result = await hass.config_entries.flow.async_init( - "cast", context={"source": "user"} + "cast", context={"source": config_entries.SOURCE_USER} ) result = await hass.config_entries.flow.async_configure( result["flow_id"], {"known_hosts": "192.168.0.1, 192.168.0.2"} diff --git a/tests/components/cert_expiry/test_config_flow.py b/tests/components/cert_expiry/test_config_flow.py index ed51ebf70a4ad..a1cd1367e5f35 100644 --- a/tests/components/cert_expiry/test_config_flow.py +++ b/tests/components/cert_expiry/test_config_flow.py @@ -3,7 +3,7 @@ import ssl from unittest.mock import patch -from homeassistant import data_entry_flow +from homeassistant import config_entries, data_entry_flow from homeassistant.components.cert_expiry.const import DEFAULT_PORT, DOMAIN from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT @@ -16,7 +16,7 @@ async def test_user(hass): """Test user config.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" @@ -40,7 +40,7 @@ async def test_user(hass): async def test_user_with_bad_cert(hass): """Test user config with bad certificate.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" @@ -72,7 +72,9 @@ async def test_import_host_only(hass): return_value=future_timestamp(1), ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "import"}, data={CONF_HOST: HOST} + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={CONF_HOST: HOST}, ) await hass.async_block_till_done() @@ -93,7 +95,7 @@ async def test_import_host_and_port(hass): ): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": "import"}, + context={"source": config_entries.SOURCE_IMPORT}, data={CONF_HOST: HOST, CONF_PORT: PORT}, ) await hass.async_block_till_done() @@ -114,7 +116,9 @@ async def test_import_non_default_port(hass): return_value=future_timestamp(1), ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "import"}, data={CONF_HOST: HOST, CONF_PORT: 888} + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={CONF_HOST: HOST, CONF_PORT: 888}, ) await hass.async_block_till_done() @@ -135,7 +139,7 @@ async def test_import_with_name(hass): ): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": "import"}, + context={"source": config_entries.SOURCE_IMPORT}, data={CONF_NAME: "legacy", CONF_HOST: HOST, CONF_PORT: PORT}, ) await hass.async_block_till_done() @@ -154,7 +158,9 @@ async def test_bad_import(hass): side_effect=ConnectionRefusedError(), ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "import"}, data={CONF_HOST: HOST} + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={CONF_HOST: HOST}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT @@ -170,13 +176,17 @@ async def test_abort_if_already_setup(hass): ).add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "import"}, data={CONF_HOST: HOST, CONF_PORT: PORT} + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={CONF_HOST: HOST, CONF_PORT: PORT}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"}, data={CONF_HOST: HOST, CONF_PORT: PORT} + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={CONF_HOST: HOST, CONF_PORT: PORT}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" @@ -185,7 +195,7 @@ async def test_abort_if_already_setup(hass): async def test_abort_on_socket_failed(hass): """Test we abort of we have errors during socket creation.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) with patch( diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index abad057b64cc0..2f7815c99bf05 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -330,7 +330,7 @@ async def async_step_user(self, user_input=None): "disabled_by": None, "domain": "test", "entry_id": entries[0].entry_id, - "source": "user", + "source": core_ce.SOURCE_USER, "state": "loaded", "supports_options": False, "supports_unload": False, @@ -401,7 +401,7 @@ async def async_step_account(self, user_input=None): "disabled_by": None, "domain": "test", "entry_id": entries[0].entry_id, - "source": "user", + "source": core_ce.SOURCE_USER, "state": "loaded", "supports_options": False, "supports_unload": False, @@ -476,7 +476,7 @@ async def async_step_account(self, user_input=None): with patch.dict(HANDLERS, {"test": TestFlow}): form = await hass.config_entries.flow.async_init( - "test", context={"source": "hassio"} + "test", context={"source": core_ce.SOURCE_HASSIO} ) await ws_client.send_json({"id": 5, "type": "config_entries/flow/progress"}) @@ -488,7 +488,7 @@ async def async_step_account(self, user_input=None): "flow_id": form["flow_id"], "handler": "test", "step_id": "account", - "context": {"source": "hassio"}, + "context": {"source": core_ce.SOURCE_HASSIO}, } ] @@ -886,7 +886,7 @@ async def async_step_user(self, user_input=None): with patch.dict(HANDLERS, {"test": TestFlow}): result = await hass.config_entries.flow.async_init( - "test", context={"source": "user"} + "test", context={"source": core_ce.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_gateway.py index afd4e55499db5..1712faaf08070 100644 --- a/tests/components/deconz/test_gateway.py +++ b/tests/components/deconz/test_gateway.py @@ -32,7 +32,7 @@ ATTR_UPNP_UDN, ) from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN -from homeassistant.config_entries import CONN_CLASS_LOCAL_PUSH, SOURCE_SSDP +from homeassistant.config_entries import CONN_CLASS_LOCAL_PUSH, SOURCE_SSDP, SOURCE_USER from homeassistant.const import ( CONF_API_KEY, CONF_HOST, @@ -109,7 +109,7 @@ async def setup_deconz_integration( get_state_response=DECONZ_WEB_REQUEST, entry_id="1", unique_id=BRIDGEID, - source="user", + source=SOURCE_USER, ): """Create the deCONZ gateway.""" config_entry = MockConfigEntry( diff --git a/tests/components/devolo_home_control/test_config_flow.py b/tests/components/devolo_home_control/test_config_flow.py index 7765e7335e4f0..94435545cc62b 100644 --- a/tests/components/devolo_home_control/test_config_flow.py +++ b/tests/components/devolo_home_control/test_config_flow.py @@ -67,7 +67,8 @@ async def test_form_already_configured(hass): async def test_form_advanced_options(hass): """Test if we get the advanced options if user has enabled it.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user", "show_advanced_options": True} + DOMAIN, + context={"source": config_entries.SOURCE_USER, "show_advanced_options": True}, ) assert result["type"] == "form" assert result["errors"] == {} diff --git a/tests/components/dhcp/test_init.py b/tests/components/dhcp/test_init.py index 25fbbea459a42..122e81786c2b4 100644 --- a/tests/components/dhcp/test_init.py +++ b/tests/components/dhcp/test_init.py @@ -7,6 +7,7 @@ from scapy.layers.dhcp import DHCP from scapy.layers.l2 import Ether +from homeassistant import config_entries from homeassistant.components import dhcp from homeassistant.components.device_tracker.const import ( ATTR_HOST_NAME, @@ -98,7 +99,9 @@ async def test_dhcp_match_hostname_and_macaddress(hass): assert len(mock_init.mock_calls) == 1 assert mock_init.mock_calls[0][1][0] == "mock-domain" - assert mock_init.mock_calls[0][2]["context"] == {"source": "dhcp"} + assert mock_init.mock_calls[0][2]["context"] == { + "source": config_entries.SOURCE_DHCP + } assert mock_init.mock_calls[0][2]["data"] == { dhcp.IP_ADDRESS: "192.168.210.56", dhcp.HOSTNAME: "connect", @@ -123,7 +126,9 @@ async def test_dhcp_renewal_match_hostname_and_macaddress(hass): assert len(mock_init.mock_calls) == 1 assert mock_init.mock_calls[0][1][0] == "mock-domain" - assert mock_init.mock_calls[0][2]["context"] == {"source": "dhcp"} + assert mock_init.mock_calls[0][2]["context"] == { + "source": config_entries.SOURCE_DHCP + } assert mock_init.mock_calls[0][2]["data"] == { dhcp.IP_ADDRESS: "192.168.1.120", dhcp.HOSTNAME: "irobot-ae9ec12dd3b04885bcbfa36afb01e1cc", @@ -144,7 +149,9 @@ async def test_dhcp_match_hostname(hass): assert len(mock_init.mock_calls) == 1 assert mock_init.mock_calls[0][1][0] == "mock-domain" - assert mock_init.mock_calls[0][2]["context"] == {"source": "dhcp"} + assert mock_init.mock_calls[0][2]["context"] == { + "source": config_entries.SOURCE_DHCP + } assert mock_init.mock_calls[0][2]["data"] == { dhcp.IP_ADDRESS: "192.168.210.56", dhcp.HOSTNAME: "connect", @@ -165,7 +172,9 @@ async def test_dhcp_match_macaddress(hass): assert len(mock_init.mock_calls) == 1 assert mock_init.mock_calls[0][1][0] == "mock-domain" - assert mock_init.mock_calls[0][2]["context"] == {"source": "dhcp"} + assert mock_init.mock_calls[0][2]["context"] == { + "source": config_entries.SOURCE_DHCP + } assert mock_init.mock_calls[0][2]["data"] == { dhcp.IP_ADDRESS: "192.168.210.56", dhcp.HOSTNAME: "connect", @@ -435,7 +444,9 @@ async def test_device_tracker_hostname_and_macaddress_exists_before_start(hass): assert len(mock_init.mock_calls) == 1 assert mock_init.mock_calls[0][1][0] == "mock-domain" - assert mock_init.mock_calls[0][2]["context"] == {"source": "dhcp"} + assert mock_init.mock_calls[0][2]["context"] == { + "source": config_entries.SOURCE_DHCP + } assert mock_init.mock_calls[0][2]["data"] == { dhcp.IP_ADDRESS: "192.168.210.56", dhcp.HOSTNAME: "connect", @@ -470,7 +481,9 @@ async def test_device_tracker_hostname_and_macaddress_after_start(hass): assert len(mock_init.mock_calls) == 1 assert mock_init.mock_calls[0][1][0] == "mock-domain" - assert mock_init.mock_calls[0][2]["context"] == {"source": "dhcp"} + assert mock_init.mock_calls[0][2]["context"] == { + "source": config_entries.SOURCE_DHCP + } assert mock_init.mock_calls[0][2]["data"] == { dhcp.IP_ADDRESS: "192.168.210.56", dhcp.HOSTNAME: "connect", @@ -614,7 +627,9 @@ async def test_aiodiscover_finds_new_hosts(hass): assert len(mock_init.mock_calls) == 1 assert mock_init.mock_calls[0][1][0] == "mock-domain" - assert mock_init.mock_calls[0][2]["context"] == {"source": "dhcp"} + assert mock_init.mock_calls[0][2]["context"] == { + "source": config_entries.SOURCE_DHCP + } assert mock_init.mock_calls[0][2]["data"] == { dhcp.IP_ADDRESS: "192.168.210.56", dhcp.HOSTNAME: "connect", @@ -667,14 +682,18 @@ async def test_aiodiscover_does_not_call_again_on_shorter_hostname(hass): assert len(mock_init.mock_calls) == 2 assert mock_init.mock_calls[0][1][0] == "mock-domain" - assert mock_init.mock_calls[0][2]["context"] == {"source": "dhcp"} + assert mock_init.mock_calls[0][2]["context"] == { + "source": config_entries.SOURCE_DHCP + } assert mock_init.mock_calls[0][2]["data"] == { dhcp.IP_ADDRESS: "192.168.210.56", dhcp.HOSTNAME: "irobot-abc", dhcp.MAC_ADDRESS: "b8b7f16db533", } assert mock_init.mock_calls[1][1][0] == "mock-domain" - assert mock_init.mock_calls[1][2]["context"] == {"source": "dhcp"} + assert mock_init.mock_calls[1][2]["context"] == { + "source": config_entries.SOURCE_DHCP + } assert mock_init.mock_calls[1][2]["data"] == { dhcp.IP_ADDRESS: "192.168.210.56", dhcp.HOSTNAME: "irobot-abcdef", @@ -715,7 +734,9 @@ async def test_aiodiscover_finds_new_hosts_after_interval(hass): assert len(mock_init.mock_calls) == 1 assert mock_init.mock_calls[0][1][0] == "mock-domain" - assert mock_init.mock_calls[0][2]["context"] == {"source": "dhcp"} + assert mock_init.mock_calls[0][2]["context"] == { + "source": config_entries.SOURCE_DHCP + } assert mock_init.mock_calls[0][2]["data"] == { dhcp.IP_ADDRESS: "192.168.210.56", dhcp.HOSTNAME: "connect", diff --git a/tests/components/dialogflow/test_init.py b/tests/components/dialogflow/test_init.py index 2213e52bef75f..c2d0316245a40 100644 --- a/tests/components/dialogflow/test_init.py +++ b/tests/components/dialogflow/test_init.py @@ -4,7 +4,7 @@ import pytest -from homeassistant import data_entry_flow +from homeassistant import config_entries, data_entry_flow from homeassistant.components import dialogflow, intent_script from homeassistant.config import async_process_ha_core_config from homeassistant.core import callback @@ -84,7 +84,7 @@ async def fixture(hass, aiohttp_client): ) result = await hass.config_entries.flow.async_init( - "dialogflow", context={"source": "user"} + "dialogflow", context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM, result diff --git a/tests/components/eafm/test_config_flow.py b/tests/components/eafm/test_config_flow.py index 8a1e2bd89fc14..403ea4f028a94 100644 --- a/tests/components/eafm/test_config_flow.py +++ b/tests/components/eafm/test_config_flow.py @@ -4,6 +4,7 @@ import pytest from voluptuous.error import MultipleInvalid +from homeassistant import config_entries from homeassistant.components.eafm import const @@ -11,7 +12,7 @@ async def test_flow_no_discovered_stations(hass, mock_get_stations): """Test config flow discovers no station.""" mock_get_stations.return_value = [] result = await hass.config_entries.flow.async_init( - const.DOMAIN, context={"source": "user"} + const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == "abort" assert result["reason"] == "no_stations" @@ -24,7 +25,7 @@ async def test_flow_invalid_station(hass, mock_get_stations): ] result = await hass.config_entries.flow.async_init( - const.DOMAIN, context={"source": "user"} + const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == "form" @@ -44,7 +45,7 @@ async def test_flow_works(hass, mock_get_stations, mock_get_station): ] result = await hass.config_entries.flow.async_init( - const.DOMAIN, context={"source": "user"} + const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == "form" diff --git a/tests/components/emonitor/test_config_flow.py b/tests/components/emonitor/test_config_flow.py index 1d71275409a64..ab2f62578b447 100644 --- a/tests/components/emonitor/test_config_flow.py +++ b/tests/components/emonitor/test_config_flow.py @@ -102,7 +102,7 @@ async def test_dhcp_can_confirm(hass): ): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": "dhcp"}, + context={"source": config_entries.SOURCE_DHCP}, data={ HOSTNAME: "emonitor", IP_ADDRESS: "1.2.3.4", @@ -146,7 +146,7 @@ async def test_dhcp_fails_to_connect(hass): ): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": "dhcp"}, + context={"source": config_entries.SOURCE_DHCP}, data={ HOSTNAME: "emonitor", IP_ADDRESS: "1.2.3.4", @@ -175,7 +175,7 @@ async def test_dhcp_already_exists(hass): ): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": "dhcp"}, + context={"source": config_entries.SOURCE_DHCP}, data={ HOSTNAME: "emonitor", IP_ADDRESS: "1.2.3.4", diff --git a/tests/components/enocean/test_config_flow.py b/tests/components/enocean/test_config_flow.py index 60a20af5eae93..d12b9a580c767 100644 --- a/tests/components/enocean/test_config_flow.py +++ b/tests/components/enocean/test_config_flow.py @@ -1,7 +1,7 @@ """Tests for EnOcean config flow.""" from unittest.mock import Mock, patch -from homeassistant import data_entry_flow +from homeassistant import config_entries, data_entry_flow from homeassistant.components.enocean.config_flow import EnOceanFlowHandler from homeassistant.components.enocean.const import DOMAIN from homeassistant.const import CONF_DEVICE @@ -21,7 +21,7 @@ async def test_user_flow_cannot_create_multiple_instances(hass): with patch(DONGLE_VALIDATE_PATH_METHOD, Mock(return_value=True)): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT @@ -34,7 +34,7 @@ async def test_user_flow_with_detected_dongle(hass): with patch(DONGLE_DETECT_METHOD, Mock(return_value=[FAKE_DONGLE_PATH])): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -48,7 +48,7 @@ async def test_user_flow_with_no_detected_dongle(hass): """Test the user flow with a detected ENOcean dongle.""" with patch(DONGLE_DETECT_METHOD, Mock(return_value=[])): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -141,7 +141,9 @@ async def test_import_flow_with_valid_path(hass): with patch(DONGLE_VALIDATE_PATH_METHOD, Mock(return_value=True)): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "import"}, data=DATA_TO_IMPORT + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=DATA_TO_IMPORT, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -157,7 +159,9 @@ async def test_import_flow_with_invalid_path(hass): Mock(return_value=False), ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "import"}, data=DATA_TO_IMPORT + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=DATA_TO_IMPORT, ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT diff --git a/tests/components/enphase_envoy/test_config_flow.py b/tests/components/enphase_envoy/test_config_flow.py index 0f48067ec6d35..45aeecf912ab7 100644 --- a/tests/components/enphase_envoy/test_config_flow.py +++ b/tests/components/enphase_envoy/test_config_flow.py @@ -130,7 +130,7 @@ async def test_import(hass: HomeAssistant) -> None: ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": "import"}, + context={"source": config_entries.SOURCE_IMPORT}, data={ "ip_address": "1.1.1.1", "name": "Pool Envoy", @@ -156,7 +156,7 @@ async def test_zeroconf(hass: HomeAssistant) -> None: await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": "zeroconf"}, + context={"source": config_entries.SOURCE_ZEROCONF}, data={ "properties": {"serialnum": "1234"}, "host": "1.1.1.1", @@ -253,7 +253,7 @@ async def test_zeroconf_serial_already_exists(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": "zeroconf"}, + context={"source": config_entries.SOURCE_ZEROCONF}, data={ "properties": {"serialnum": "1234"}, "host": "1.1.1.1", @@ -288,7 +288,7 @@ async def test_zeroconf_host_already_exists(hass: HomeAssistant) -> None: ) as mock_setup_entry: result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": "zeroconf"}, + context={"source": config_entries.SOURCE_ZEROCONF}, data={ "properties": {"serialnum": "1234"}, "host": "1.1.1.1", diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index 233255c1a890d..d5968e7f73183 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -4,6 +4,7 @@ import pytest +from homeassistant import config_entries from homeassistant.components.esphome import DOMAIN from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.data_entry_flow import ( @@ -51,7 +52,7 @@ async def test_user_connection_works(hass, mock_client): """Test we can finish a config flow.""" result = await hass.config_entries.flow.async_init( "esphome", - context={"source": "user"}, + context={"source": config_entries.SOURCE_USER}, data=None, ) @@ -62,7 +63,7 @@ async def test_user_connection_works(hass, mock_client): result = await hass.config_entries.flow.async_init( "esphome", - context={"source": "user"}, + context={"source": config_entries.SOURCE_USER}, data={CONF_HOST: "127.0.0.1", CONF_PORT: 80}, ) @@ -95,7 +96,7 @@ def __init__(self): mock_client.device_info.side_effect = exc result = await hass.config_entries.flow.async_init( "esphome", - context={"source": "user"}, + context={"source": config_entries.SOURCE_USER}, data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) @@ -114,7 +115,7 @@ async def test_user_connection_error(hass, mock_api_connection_error, mock_clien result = await hass.config_entries.flow.async_init( "esphome", - context={"source": "user"}, + context={"source": config_entries.SOURCE_USER}, data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) @@ -133,7 +134,7 @@ async def test_user_with_password(hass, mock_client): result = await hass.config_entries.flow.async_init( "esphome", - context={"source": "user"}, + context={"source": config_entries.SOURCE_USER}, data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) @@ -159,7 +160,7 @@ async def test_user_invalid_password(hass, mock_api_connection_error, mock_clien result = await hass.config_entries.flow.async_init( "esphome", - context={"source": "user"}, + context={"source": config_entries.SOURCE_USER}, data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) @@ -188,7 +189,7 @@ async def test_discovery_initiation(hass, mock_client): "properties": {}, } flow = await hass.config_entries.flow.async_init( - "esphome", context={"source": "zeroconf"}, data=service_info + "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info ) result = await hass.config_entries.flow.async_configure( @@ -220,7 +221,7 @@ async def test_discovery_already_configured_hostname(hass, mock_client): "properties": {}, } result = await hass.config_entries.flow.async_init( - "esphome", context={"source": "zeroconf"}, data=service_info + "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info ) assert result["type"] == RESULT_TYPE_ABORT @@ -245,7 +246,7 @@ async def test_discovery_already_configured_ip(hass, mock_client): "properties": {"address": "192.168.43.183"}, } result = await hass.config_entries.flow.async_init( - "esphome", context={"source": "zeroconf"}, data=service_info + "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info ) assert result["type"] == RESULT_TYPE_ABORT @@ -273,7 +274,7 @@ async def test_discovery_already_configured_name(hass, mock_client): "properties": {"address": "test8266.local"}, } result = await hass.config_entries.flow.async_init( - "esphome", context={"source": "zeroconf"}, data=service_info + "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info ) assert result["type"] == RESULT_TYPE_ABORT @@ -295,13 +296,13 @@ async def test_discovery_duplicate_data(hass, mock_client): mock_client.device_info = AsyncMock(return_value=MockDeviceInfo(False, "test8266")) result = await hass.config_entries.flow.async_init( - "esphome", data=service_info, context={"source": "zeroconf"} + "esphome", data=service_info, context={"source": config_entries.SOURCE_ZEROCONF} ) assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "discovery_confirm" result = await hass.config_entries.flow.async_init( - "esphome", data=service_info, context={"source": "zeroconf"} + "esphome", data=service_info, context={"source": config_entries.SOURCE_ZEROCONF} ) assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "already_in_progress" @@ -323,7 +324,7 @@ async def test_discovery_updates_unique_id(hass, mock_client): "properties": {"address": "test8266.local"}, } result = await hass.config_entries.flow.async_init( - "esphome", context={"source": "zeroconf"}, data=service_info + "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info ) assert result["type"] == RESULT_TYPE_ABORT diff --git a/tests/components/fireservicerota/test_config_flow.py b/tests/components/fireservicerota/test_config_flow.py index 6f4fd21a534d1..0553574ae776a 100644 --- a/tests/components/fireservicerota/test_config_flow.py +++ b/tests/components/fireservicerota/test_config_flow.py @@ -3,7 +3,7 @@ from pyfireservicerota import InvalidAuthError -from homeassistant import data_entry_flow +from homeassistant import config_entries, data_entry_flow from homeassistant.components.fireservicerota.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME @@ -40,7 +40,7 @@ async def test_show_form(hass): """Test that the form is served with no input.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" @@ -53,7 +53,7 @@ async def test_abort_if_already_setup(hass): ) entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"}, data=MOCK_CONF + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_CONF ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" @@ -67,7 +67,7 @@ async def test_invalid_credentials(hass): side_effect=InvalidAuthError, ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"}, data=MOCK_CONF + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_CONF ) assert result["errors"] == {"base": "invalid_auth"} @@ -86,7 +86,7 @@ async def test_step_user(hass): mock_fireservicerota.request_tokens.return_value = MOCK_TOKEN_INFO result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"}, data=MOCK_CONF + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_CONF ) await hass.async_block_till_done() @@ -123,7 +123,10 @@ async def test_reauth(hass): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": "reauth", "unique_id": entry.unique_id}, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": entry.unique_id, + }, data=MOCK_CONF, ) diff --git a/tests/components/fritzbox/test_config_flow.py b/tests/components/fritzbox/test_config_flow.py index 2ffa14003f01c..64e8c691638b0 100644 --- a/tests/components/fritzbox/test_config_flow.py +++ b/tests/components/fritzbox/test_config_flow.py @@ -12,7 +12,12 @@ ATTR_UPNP_FRIENDLY_NAME, ATTR_UPNP_UDN, ) -from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_SSDP, SOURCE_USER +from homeassistant.config_entries import ( + SOURCE_IMPORT, + SOURCE_REAUTH, + SOURCE_SSDP, + SOURCE_USER, +) from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import ( @@ -182,7 +187,7 @@ async def test_reauth_not_successful(hass: HomeAssistant, fritz: Mock): async def test_import(hass: HomeAssistant, fritz: Mock): """Test starting a flow by import.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "import"}, data=MOCK_USER_DATA + DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_USER_DATA ) assert result["type"] == RESULT_TYPE_CREATE_ENTRY assert result["title"] == "fake_host" diff --git a/tests/components/garmin_connect/test_config_flow.py b/tests/components/garmin_connect/test_config_flow.py index eed9d8dceae7c..f3784d5e2e210 100644 --- a/tests/components/garmin_connect/test_config_flow.py +++ b/tests/components/garmin_connect/test_config_flow.py @@ -8,7 +8,7 @@ ) import pytest -from homeassistant import data_entry_flow +from homeassistant import config_entries, data_entry_flow from homeassistant.components.garmin_connect.const import DOMAIN from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME @@ -34,7 +34,7 @@ def mock_garmin(): async def test_show_form(hass): """Test that the form is served with no input.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" @@ -47,7 +47,7 @@ async def test_step_user(hass, mock_garmin_connect): "homeassistant.components.garmin_connect.async_setup_entry", return_value=True ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"}, data=MOCK_CONF + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_CONF ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["data"] == MOCK_CONF @@ -57,7 +57,7 @@ async def test_connection_error(hass, mock_garmin_connect): """Test for connection error.""" mock_garmin_connect.login.side_effect = GarminConnectConnectionError("errormsg") result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"}, data=MOCK_CONF + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_CONF ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {"base": "cannot_connect"} @@ -67,7 +67,7 @@ async def test_authentication_error(hass, mock_garmin_connect): """Test for authentication error.""" mock_garmin_connect.login.side_effect = GarminConnectAuthenticationError("errormsg") result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"}, data=MOCK_CONF + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_CONF ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {"base": "invalid_auth"} @@ -79,7 +79,7 @@ async def test_toomanyrequest_error(hass, mock_garmin_connect): "errormsg" ) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"}, data=MOCK_CONF + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_CONF ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {"base": "too_many_requests"} @@ -89,7 +89,7 @@ async def test_unknown_error(hass, mock_garmin_connect): """Test for unknown error.""" mock_garmin_connect.login.side_effect = Exception result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"}, data=MOCK_CONF + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_CONF ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {"base": "unknown"} @@ -100,7 +100,7 @@ async def test_abort_if_already_setup(hass, mock_garmin_connect): entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONF, unique_id=MOCK_CONF[CONF_ID]) entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"}, data=MOCK_CONF + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_CONF ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/gdacs/test_config_flow.py b/tests/components/gdacs/test_config_flow.py index e2ecd3902d533..8496f0ca5a289 100644 --- a/tests/components/gdacs/test_config_flow.py +++ b/tests/components/gdacs/test_config_flow.py @@ -4,7 +4,7 @@ import pytest -from homeassistant import data_entry_flow +from homeassistant import config_entries, data_entry_flow from homeassistant.components.gdacs import CONF_CATEGORIES, DOMAIN from homeassistant.const import ( CONF_LATITUDE, @@ -27,7 +27,7 @@ async def test_duplicate_error(hass, config_entry): config_entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"}, data=conf + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=conf ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" @@ -36,7 +36,7 @@ async def test_duplicate_error(hass, config_entry): async def test_show_form(hass): """Test that the form is served with no input.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" @@ -53,7 +53,7 @@ async def test_step_import(hass): } result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "import"}, data=conf + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=conf ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == "-41.2, 174.7" @@ -73,7 +73,7 @@ async def test_step_user(hass): conf = {CONF_RADIUS: 25} result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"}, data=conf + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=conf ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == "-41.2, 174.7" diff --git a/tests/components/geofency/test_init.py b/tests/components/geofency/test_init.py index b84b6b681ae22..169cfebae17c8 100644 --- a/tests/components/geofency/test_init.py +++ b/tests/components/geofency/test_init.py @@ -4,7 +4,7 @@ import pytest -from homeassistant import data_entry_flow +from homeassistant import config_entries, data_entry_flow from homeassistant.components import zone from homeassistant.components.geofency import CONF_MOBILE_BEACONS, DOMAIN from homeassistant.config import async_process_ha_core_config @@ -157,7 +157,7 @@ async def webhook_id(hass, geofency_client): {"internal_url": "http://example.local:8123"}, ) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM, result diff --git a/tests/components/geonetnz_quakes/test_config_flow.py b/tests/components/geonetnz_quakes/test_config_flow.py index d362e9cdf0f66..9b471051656b1 100644 --- a/tests/components/geonetnz_quakes/test_config_flow.py +++ b/tests/components/geonetnz_quakes/test_config_flow.py @@ -2,7 +2,7 @@ from datetime import timedelta from unittest.mock import patch -from homeassistant import data_entry_flow +from homeassistant import config_entries, data_entry_flow from homeassistant.components.geonetnz_quakes import ( CONF_MINIMUM_MAGNITUDE, CONF_MMI, @@ -23,7 +23,7 @@ async def test_duplicate_error(hass, config_entry): config_entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"}, data=conf + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=conf ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" @@ -32,7 +32,7 @@ async def test_duplicate_error(hass, config_entry): async def test_show_form(hass): """Test that the form is served with no input.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" @@ -54,7 +54,7 @@ async def test_step_import(hass): "homeassistant.components.geonetnz_quakes.async_setup_entry", return_value=True ), patch("homeassistant.components.geonetnz_quakes.async_setup", return_value=True): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "import"}, data=conf + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=conf ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == "-41.2, 174.7" @@ -79,7 +79,7 @@ async def test_step_user(hass): "homeassistant.components.geonetnz_quakes.async_setup_entry", return_value=True ), patch("homeassistant.components.geonetnz_quakes.async_setup", return_value=True): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"}, data=conf + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=conf ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == "-41.2, 174.7" diff --git a/tests/components/glances/test_config_flow.py b/tests/components/glances/test_config_flow.py index 8734ca0e60d12..c9a2c333b8b48 100644 --- a/tests/components/glances/test_config_flow.py +++ b/tests/components/glances/test_config_flow.py @@ -3,7 +3,7 @@ from glances_api import Glances -from homeassistant import data_entry_flow +from homeassistant import config_entries, data_entry_flow from homeassistant.components import glances from homeassistant.const import CONF_SCAN_INTERVAL @@ -33,7 +33,7 @@ async def test_form(hass): """Test config entry configured successfully.""" result = await hass.config_entries.flow.async_init( - glances.DOMAIN, context={"source": "user"} + glances.DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" @@ -56,7 +56,7 @@ async def test_form_cannot_connect(hass): with patch("glances_api.Glances"): result = await hass.config_entries.flow.async_init( - glances.DOMAIN, context={"source": "user"} + glances.DOMAIN, context={"source": config_entries.SOURCE_USER} ) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=DEMO_USER_INPUT @@ -72,7 +72,7 @@ async def test_form_wrong_version(hass): user_input = DEMO_USER_INPUT.copy() user_input.update(version=1) result = await hass.config_entries.flow.async_init( - glances.DOMAIN, context={"source": "user"} + glances.DOMAIN, context={"source": config_entries.SOURCE_USER} ) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=user_input @@ -90,7 +90,7 @@ async def test_form_already_configured(hass): entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - glances.DOMAIN, context={"source": "user"} + glances.DOMAIN, context={"source": config_entries.SOURCE_USER} ) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=DEMO_USER_INPUT diff --git a/tests/components/gpslogger/test_init.py b/tests/components/gpslogger/test_init.py index 1dad262a28567..61e5862d3b107 100644 --- a/tests/components/gpslogger/test_init.py +++ b/tests/components/gpslogger/test_init.py @@ -3,7 +3,7 @@ import pytest -from homeassistant import data_entry_flow +from homeassistant import config_entries, data_entry_flow from homeassistant.components import gpslogger, zone from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN from homeassistant.components.gpslogger import DOMAIN, TRACKER_UPDATE @@ -69,7 +69,7 @@ async def webhook_id(hass, gpslogger_client): {"internal_url": "http://example.local:8123"}, ) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM, result diff --git a/tests/components/heos/test_config_flow.py b/tests/components/heos/test_config_flow.py index e62578e510800..76ff06e2a9678 100644 --- a/tests/components/heos/test_config_flow.py +++ b/tests/components/heos/test_config_flow.py @@ -8,7 +8,7 @@ from homeassistant.components import heos, ssdp from homeassistant.components.heos.config_flow import HeosFlowHandler from homeassistant.components.heos.const import DATA_DISCOVERED_HOSTS, DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_SSDP +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_SSDP, SOURCE_USER from homeassistant.const import CONF_HOST @@ -36,7 +36,7 @@ async def test_cannot_connect_shows_error_form(hass, controller): """Test form is shown with error when cannot connect.""" controller.connect.side_effect = HeosError() result = await hass.config_entries.flow.async_init( - heos.DOMAIN, context={"source": "user"}, data={CONF_HOST: "127.0.0.1"} + heos.DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "127.0.0.1"} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" @@ -52,7 +52,7 @@ async def test_create_entry_when_host_valid(hass, controller): data = {CONF_HOST: "127.0.0.1"} with patch("homeassistant.components.heos.async_setup_entry", return_value=True): result = await hass.config_entries.flow.async_init( - heos.DOMAIN, context={"source": "user"}, data=data + heos.DOMAIN, context={"source": SOURCE_USER}, data=data ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["result"].unique_id == DOMAIN @@ -68,7 +68,7 @@ async def test_create_entry_when_friendly_name_valid(hass, controller): data = {CONF_HOST: "Office (127.0.0.1)"} with patch("homeassistant.components.heos.async_setup_entry", return_value=True): result = await hass.config_entries.flow.async_init( - heos.DOMAIN, context={"source": "user"}, data=data + heos.DOMAIN, context={"source": SOURCE_USER}, data=data ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["result"].unique_id == DOMAIN @@ -83,7 +83,7 @@ async def test_discovery_shows_create_form(hass, controller, discovery_data): """Test discovery shows form to confirm setup and subsequent abort.""" await hass.config_entries.flow.async_init( - heos.DOMAIN, context={"source": "ssdp"}, data=discovery_data + heos.DOMAIN, context={"source": SOURCE_SSDP}, data=discovery_data ) await hass.async_block_till_done() flows_in_progress = hass.config_entries.flow.async_progress() @@ -96,7 +96,7 @@ async def test_discovery_shows_create_form(hass, controller, discovery_data): discovery_data[ssdp.ATTR_UPNP_FRIENDLY_NAME] = "Bedroom" await hass.config_entries.flow.async_init( - heos.DOMAIN, context={"source": "ssdp"}, data=discovery_data + heos.DOMAIN, context={"source": SOURCE_SSDP}, data=discovery_data ) await hass.async_block_till_done() flows_in_progress = hass.config_entries.flow.async_progress() diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index 42903a530629c..12381614a83ba 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -9,6 +9,7 @@ from aiohomekit.model.services import ServicesTypes import pytest +from homeassistant import config_entries from homeassistant.components.homekit_controller import config_flow from homeassistant.helpers import device_registry @@ -177,13 +178,15 @@ async def test_discovery_works(hass, controller, upper_case_props, missing_cshar # Device is discovered result = await hass.config_entries.flow.async_init( - "homekit_controller", context={"source": "zeroconf"}, data=discovery_info + "homekit_controller", + context={"source": config_entries.SOURCE_ZEROCONF}, + data=discovery_info, ) assert result["type"] == "form" assert result["step_id"] == "pair" assert get_flow_context(hass, result) == { "hkid": "00:00:00:00:00:00", - "source": "zeroconf", + "source": config_entries.SOURCE_ZEROCONF, "title_placeholders": {"name": "TestDevice"}, "unique_id": "00:00:00:00:00:00", } @@ -209,13 +212,17 @@ async def test_abort_duplicate_flow(hass, controller): # Device is discovered result = await hass.config_entries.flow.async_init( - "homekit_controller", context={"source": "zeroconf"}, data=discovery_info + "homekit_controller", + context={"source": config_entries.SOURCE_ZEROCONF}, + data=discovery_info, ) assert result["type"] == "form" assert result["step_id"] == "pair" result = await hass.config_entries.flow.async_init( - "homekit_controller", context={"source": "zeroconf"}, data=discovery_info + "homekit_controller", + context={"source": config_entries.SOURCE_ZEROCONF}, + data=discovery_info, ) assert result["type"] == "abort" assert result["reason"] == "already_in_progress" @@ -231,7 +238,9 @@ async def test_pair_already_paired_1(hass, controller): # Device is discovered result = await hass.config_entries.flow.async_init( - "homekit_controller", context={"source": "zeroconf"}, data=discovery_info + "homekit_controller", + context={"source": config_entries.SOURCE_ZEROCONF}, + data=discovery_info, ) assert result["type"] == "abort" assert result["reason"] == "already_paired" @@ -247,7 +256,9 @@ async def test_id_missing(hass, controller): # Device is discovered result = await hass.config_entries.flow.async_init( - "homekit_controller", context={"source": "zeroconf"}, data=discovery_info + "homekit_controller", + context={"source": config_entries.SOURCE_ZEROCONF}, + data=discovery_info, ) assert result["type"] == "abort" assert result["reason"] == "invalid_properties" @@ -262,7 +273,9 @@ async def test_discovery_ignored_model(hass, controller): # Device is discovered result = await hass.config_entries.flow.async_init( - "homekit_controller", context={"source": "zeroconf"}, data=discovery_info + "homekit_controller", + context={"source": config_entries.SOURCE_ZEROCONF}, + data=discovery_info, ) assert result["type"] == "abort" assert result["reason"] == "ignored_model" @@ -287,7 +300,9 @@ async def test_discovery_ignored_hk_bridge(hass, controller): # Device is discovered result = await hass.config_entries.flow.async_init( - "homekit_controller", context={"source": "zeroconf"}, data=discovery_info + "homekit_controller", + context={"source": config_entries.SOURCE_ZEROCONF}, + data=discovery_info, ) assert result["type"] == "abort" assert result["reason"] == "ignored_model" @@ -312,7 +327,9 @@ async def test_discovery_does_not_ignore_non_homekit(hass, controller): # Device is discovered result = await hass.config_entries.flow.async_init( - "homekit_controller", context={"source": "zeroconf"}, data=discovery_info + "homekit_controller", + context={"source": config_entries.SOURCE_ZEROCONF}, + data=discovery_info, ) assert result["type"] == "form" @@ -333,7 +350,9 @@ async def test_discovery_invalid_config_entry(hass, controller): # Device is discovered result = await hass.config_entries.flow.async_init( - "homekit_controller", context={"source": "zeroconf"}, data=discovery_info + "homekit_controller", + context={"source": config_entries.SOURCE_ZEROCONF}, + data=discovery_info, ) # Discovery of a HKID that is in a pairable state but for which there is @@ -362,7 +381,9 @@ async def test_discovery_already_configured(hass, controller): # Device is discovered result = await hass.config_entries.flow.async_init( - "homekit_controller", context={"source": "zeroconf"}, data=discovery_info + "homekit_controller", + context={"source": config_entries.SOURCE_ZEROCONF}, + data=discovery_info, ) assert result["type"] == "abort" assert result["reason"] == "already_configured" @@ -377,7 +398,9 @@ async def test_pair_abort_errors_on_start(hass, controller, exception, expected) # Device is discovered result = await hass.config_entries.flow.async_init( - "homekit_controller", context={"source": "zeroconf"}, data=discovery_info + "homekit_controller", + context={"source": config_entries.SOURCE_ZEROCONF}, + data=discovery_info, ) # User initiates pairing - device refuses to enter pairing mode @@ -397,7 +420,9 @@ async def test_pair_try_later_errors_on_start(hass, controller, exception, expec # Device is discovered result = await hass.config_entries.flow.async_init( - "homekit_controller", context={"source": "zeroconf"}, data=discovery_info + "homekit_controller", + context={"source": config_entries.SOURCE_ZEROCONF}, + data=discovery_info, ) # User initiates pairing - device refuses to enter pairing mode but may be successful after entering pairing mode or rebooting @@ -432,14 +457,16 @@ async def test_pair_form_errors_on_start(hass, controller, exception, expected): # Device is discovered result = await hass.config_entries.flow.async_init( - "homekit_controller", context={"source": "zeroconf"}, data=discovery_info + "homekit_controller", + context={"source": config_entries.SOURCE_ZEROCONF}, + data=discovery_info, ) assert get_flow_context(hass, result) == { "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, "unique_id": "00:00:00:00:00:00", - "source": "zeroconf", + "source": config_entries.SOURCE_ZEROCONF, } # User initiates pairing - device refuses to enter pairing mode @@ -455,7 +482,7 @@ async def test_pair_form_errors_on_start(hass, controller, exception, expected): "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, "unique_id": "00:00:00:00:00:00", - "source": "zeroconf", + "source": config_entries.SOURCE_ZEROCONF, } # User gets back the form @@ -480,14 +507,16 @@ async def test_pair_abort_errors_on_finish(hass, controller, exception, expected # Device is discovered result = await hass.config_entries.flow.async_init( - "homekit_controller", context={"source": "zeroconf"}, data=discovery_info + "homekit_controller", + context={"source": config_entries.SOURCE_ZEROCONF}, + data=discovery_info, ) assert get_flow_context(hass, result) == { "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, "unique_id": "00:00:00:00:00:00", - "source": "zeroconf", + "source": config_entries.SOURCE_ZEROCONF, } # User initiates pairing - this triggers the device to show a pairing code @@ -501,7 +530,7 @@ async def test_pair_abort_errors_on_finish(hass, controller, exception, expected "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, "unique_id": "00:00:00:00:00:00", - "source": "zeroconf", + "source": config_entries.SOURCE_ZEROCONF, } # User enters pairing code @@ -520,14 +549,16 @@ async def test_pair_form_errors_on_finish(hass, controller, exception, expected) # Device is discovered result = await hass.config_entries.flow.async_init( - "homekit_controller", context={"source": "zeroconf"}, data=discovery_info + "homekit_controller", + context={"source": config_entries.SOURCE_ZEROCONF}, + data=discovery_info, ) assert get_flow_context(hass, result) == { "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, "unique_id": "00:00:00:00:00:00", - "source": "zeroconf", + "source": config_entries.SOURCE_ZEROCONF, } # User initiates pairing - this triggers the device to show a pairing code @@ -541,7 +572,7 @@ async def test_pair_form_errors_on_finish(hass, controller, exception, expected) "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, "unique_id": "00:00:00:00:00:00", - "source": "zeroconf", + "source": config_entries.SOURCE_ZEROCONF, } # User enters pairing code @@ -555,7 +586,7 @@ async def test_pair_form_errors_on_finish(hass, controller, exception, expected) "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, "unique_id": "00:00:00:00:00:00", - "source": "zeroconf", + "source": config_entries.SOURCE_ZEROCONF, } @@ -565,13 +596,13 @@ async def test_user_works(hass, controller): # Device is discovered result = await hass.config_entries.flow.async_init( - "homekit_controller", context={"source": "user"} + "homekit_controller", context={"source": config_entries.SOURCE_USER} ) assert result["type"] == "form" assert result["step_id"] == "user" assert get_flow_context(hass, result) == { - "source": "user", + "source": config_entries.SOURCE_USER, } result = await hass.config_entries.flow.async_configure( @@ -581,7 +612,7 @@ async def test_user_works(hass, controller): assert result["step_id"] == "pair" assert get_flow_context(hass, result) == { - "source": "user", + "source": config_entries.SOURCE_USER, "unique_id": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, } @@ -596,7 +627,7 @@ async def test_user_works(hass, controller): async def test_user_no_devices(hass, controller): """Test user initiated pairing where no devices discovered.""" result = await hass.config_entries.flow.async_init( - "homekit_controller", context={"source": "user"} + "homekit_controller", context={"source": config_entries.SOURCE_USER} ) assert result["type"] == "abort" assert result["reason"] == "no_devices" @@ -612,7 +643,7 @@ async def test_user_no_unpaired_devices(hass, controller): # Device discovery is requested result = await hass.config_entries.flow.async_init( - "homekit_controller", context={"source": "user"} + "homekit_controller", context={"source": config_entries.SOURCE_USER} ) assert result["type"] == "abort" @@ -626,7 +657,7 @@ async def test_unignore_works(hass, controller): # Device is unignored result = await hass.config_entries.flow.async_init( "homekit_controller", - context={"source": "unignore"}, + context={"source": config_entries.SOURCE_UNIGNORE}, data={"unique_id": device.device_id}, ) assert result["type"] == "form" @@ -635,7 +666,7 @@ async def test_unignore_works(hass, controller): "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, "unique_id": "00:00:00:00:00:00", - "source": "unignore", + "source": config_entries.SOURCE_UNIGNORE, } # User initiates pairing by clicking on 'configure' - device enters pairing mode and displays code @@ -658,7 +689,7 @@ async def test_unignore_ignores_missing_devices(hass, controller): # Device is unignored result = await hass.config_entries.flow.async_init( "homekit_controller", - context={"source": "unignore"}, + context={"source": config_entries.SOURCE_UNIGNORE}, data={"unique_id": "00:00:00:00:00:01"}, ) diff --git a/tests/components/homematicip_cloud/test_config_flow.py b/tests/components/homematicip_cloud/test_config_flow.py index 0b573e66b1da0..002a5540fe5ec 100644 --- a/tests/components/homematicip_cloud/test_config_flow.py +++ b/tests/components/homematicip_cloud/test_config_flow.py @@ -1,6 +1,7 @@ """Tests for HomematicIP Cloud config flow.""" from unittest.mock import patch +from homeassistant import config_entries from homeassistant.components.homematicip_cloud.const import ( DOMAIN as HMIPC_DOMAIN, HMIPC_AUTHTOKEN, @@ -27,7 +28,9 @@ async def test_flow_works(hass, simple_mock_home): return_value=True, ): result = await hass.config_entries.flow.async_init( - HMIPC_DOMAIN, context={"source": "user"}, data=DEFAULT_CONFIG + HMIPC_DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=DEFAULT_CONFIG, ) assert result["type"] == "form" @@ -70,7 +73,9 @@ async def test_flow_init_connection_error(hass): return_value=False, ): result = await hass.config_entries.flow.async_init( - HMIPC_DOMAIN, context={"source": "user"}, data=DEFAULT_CONFIG + HMIPC_DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=DEFAULT_CONFIG, ) assert result["type"] == "form" @@ -90,7 +95,9 @@ async def test_flow_link_connection_error(hass): return_value=False, ): result = await hass.config_entries.flow.async_init( - HMIPC_DOMAIN, context={"source": "user"}, data=DEFAULT_CONFIG + HMIPC_DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=DEFAULT_CONFIG, ) assert result["type"] == "abort" @@ -107,7 +114,9 @@ async def test_flow_link_press_button(hass): return_value=True, ): result = await hass.config_entries.flow.async_init( - HMIPC_DOMAIN, context={"source": "user"}, data=DEFAULT_CONFIG + HMIPC_DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=DEFAULT_CONFIG, ) assert result["type"] == "form" @@ -119,7 +128,7 @@ async def test_init_flow_show_form(hass): """Test config flow shows up with a form.""" result = await hass.config_entries.flow.async_init( - HMIPC_DOMAIN, context={"source": "user"} + HMIPC_DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == "form" assert result["step_id"] == "init" @@ -133,7 +142,9 @@ async def test_init_already_configured(hass): return_value=True, ): result = await hass.config_entries.flow.async_init( - HMIPC_DOMAIN, context={"source": "user"}, data=DEFAULT_CONFIG + HMIPC_DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=DEFAULT_CONFIG, ) assert result["type"] == "abort" @@ -155,7 +166,9 @@ async def test_import_config(hass, simple_mock_home): "homeassistant.components.homematicip_cloud.hap.HomematicipHAP.async_connect", ): result = await hass.config_entries.flow.async_init( - HMIPC_DOMAIN, context={"source": "import"}, data=IMPORT_CONFIG + HMIPC_DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=IMPORT_CONFIG, ) assert result["type"] == "create_entry" @@ -178,7 +191,9 @@ async def test_import_existing_config(hass): return_value=True, ): result = await hass.config_entries.flow.async_init( - HMIPC_DOMAIN, context={"source": "import"}, data=IMPORT_CONFIG + HMIPC_DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=IMPORT_CONFIG, ) assert result["type"] == "abort" diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index 12a360cdf9428..3deec0988fa4d 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -54,7 +54,7 @@ async def test_flow_works(hass): return_value=[mock_bridge], ): result = await hass.config_entries.flow.async_init( - const.DOMAIN, context={"source": "user"} + const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == "form" @@ -101,7 +101,7 @@ async def test_manual_flow_works(hass, aioclient_mock): return_value=[mock_bridge], ): result = await hass.config_entries.flow.async_init( - const.DOMAIN, context={"source": "user"} + const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == "form" @@ -157,7 +157,7 @@ async def test_manual_flow_bridge_exist(hass, aioclient_mock): return_value=[], ): result = await hass.config_entries.flow.async_init( - const.DOMAIN, context={"source": "user"} + const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == "form" @@ -184,7 +184,7 @@ async def test_manual_flow_no_discovered_bridges(hass, aioclient_mock): aioclient_mock.get(URL_NUPNP, json=[]) result = await hass.config_entries.flow.async_init( - const.DOMAIN, context={"source": "user"} + const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == "form" assert result["step_id"] == "manual" @@ -198,7 +198,7 @@ async def test_flow_all_discovered_bridges_exist(hass, aioclient_mock): ).add_to_hass(hass) result = await hass.config_entries.flow.async_init( - const.DOMAIN, context={"source": "user"} + const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == "form" @@ -221,7 +221,7 @@ async def test_flow_bridges_discovered(hass, aioclient_mock): ) result = await hass.config_entries.flow.async_init( - const.DOMAIN, context={"source": "user"} + const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == "form" assert result["step_id"] == "init" @@ -248,7 +248,7 @@ async def test_flow_two_bridges_discovered_one_new(hass, aioclient_mock): ).add_to_hass(hass) result = await hass.config_entries.flow.async_init( - const.DOMAIN, context={"source": "user"} + const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == "form" @@ -266,7 +266,7 @@ async def test_flow_timeout_discovery(hass): side_effect=asyncio.TimeoutError, ): result = await hass.config_entries.flow.async_init( - const.DOMAIN, context={"source": "user"} + const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == "abort" @@ -283,7 +283,7 @@ async def test_flow_link_timeout(hass): return_value=[mock_bridge], ): result = await hass.config_entries.flow.async_init( - const.DOMAIN, context={"source": "user"} + const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) result = await hass.config_entries.flow.async_configure( @@ -308,7 +308,7 @@ async def test_flow_link_unknown_error(hass): return_value=[mock_bridge], ): result = await hass.config_entries.flow.async_init( - const.DOMAIN, context={"source": "user"} + const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) result = await hass.config_entries.flow.async_configure( @@ -334,7 +334,7 @@ async def test_flow_link_button_not_pressed(hass): return_value=[mock_bridge], ): result = await hass.config_entries.flow.async_init( - const.DOMAIN, context={"source": "user"} + const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) result = await hass.config_entries.flow.async_configure( @@ -360,7 +360,7 @@ async def test_flow_link_unknown_host(hass): return_value=[mock_bridge], ): result = await hass.config_entries.flow.async_init( - const.DOMAIN, context={"source": "user"} + const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) result = await hass.config_entries.flow.async_configure( @@ -380,7 +380,7 @@ async def test_bridge_ssdp(hass, mf_url): """Test a bridge being discovered.""" result = await hass.config_entries.flow.async_init( const.DOMAIN, - context={"source": "ssdp"}, + context={"source": config_entries.SOURCE_SSDP}, data={ ssdp.ATTR_SSDP_LOCATION: "http://0.0.0.0/", ssdp.ATTR_UPNP_MANUFACTURER_URL: mf_url, @@ -396,7 +396,7 @@ async def test_bridge_ssdp_discover_other_bridge(hass): """Test that discovery ignores other bridges.""" result = await hass.config_entries.flow.async_init( const.DOMAIN, - context={"source": "ssdp"}, + context={"source": config_entries.SOURCE_SSDP}, data={ssdp.ATTR_UPNP_MANUFACTURER_URL: "http://www.notphilips.com"}, ) @@ -408,7 +408,7 @@ async def test_bridge_ssdp_emulated_hue(hass): """Test if discovery info is from an emulated hue instance.""" result = await hass.config_entries.flow.async_init( const.DOMAIN, - context={"source": "ssdp"}, + context={"source": config_entries.SOURCE_SSDP}, data={ ssdp.ATTR_SSDP_LOCATION: "http://0.0.0.0/", ssdp.ATTR_UPNP_FRIENDLY_NAME: "Home Assistant Bridge", @@ -425,7 +425,7 @@ async def test_bridge_ssdp_missing_location(hass): """Test if discovery info is missing a location attribute.""" result = await hass.config_entries.flow.async_init( const.DOMAIN, - context={"source": "ssdp"}, + context={"source": config_entries.SOURCE_SSDP}, data={ ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL[0], ssdp.ATTR_UPNP_SERIAL: "1234", @@ -440,7 +440,7 @@ async def test_bridge_ssdp_missing_serial(hass): """Test if discovery info is a serial attribute.""" result = await hass.config_entries.flow.async_init( const.DOMAIN, - context={"source": "ssdp"}, + context={"source": config_entries.SOURCE_SSDP}, data={ ssdp.ATTR_SSDP_LOCATION: "http://0.0.0.0/", ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL[0], @@ -455,7 +455,7 @@ async def test_bridge_ssdp_espalexa(hass): """Test if discovery info is from an Espalexa based device.""" result = await hass.config_entries.flow.async_init( const.DOMAIN, - context={"source": "ssdp"}, + context={"source": config_entries.SOURCE_SSDP}, data={ ssdp.ATTR_SSDP_LOCATION: "http://0.0.0.0/", ssdp.ATTR_UPNP_FRIENDLY_NAME: "Espalexa (0.0.0.0)", @@ -476,7 +476,7 @@ async def test_bridge_ssdp_already_configured(hass): result = await hass.config_entries.flow.async_init( const.DOMAIN, - context={"source": "ssdp"}, + context={"source": config_entries.SOURCE_SSDP}, data={ ssdp.ATTR_SSDP_LOCATION: "http://0.0.0.0/", ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL[0], @@ -492,7 +492,7 @@ async def test_import_with_no_config(hass): """Test importing a host without an existing config file.""" result = await hass.config_entries.flow.async_init( const.DOMAIN, - context={"source": "import"}, + context={"source": config_entries.SOURCE_IMPORT}, data={"host": "0.0.0.0"}, ) @@ -531,7 +531,9 @@ async def test_creating_entry_removes_entries_for_same_host_or_bridge(hass): return_value=bridge, ): result = await hass.config_entries.flow.async_init( - "hue", data={"host": "2.2.2.2"}, context={"source": "import"} + "hue", + data={"host": "2.2.2.2"}, + context={"source": config_entries.SOURCE_IMPORT}, ) assert result["type"] == "form" @@ -561,7 +563,7 @@ async def test_bridge_homekit(hass, aioclient_mock): result = await hass.config_entries.flow.async_init( const.DOMAIN, - context={"source": "homekit"}, + context={"source": config_entries.SOURCE_HOMEKIT}, data={ "host": "0.0.0.0", "serial": "1234", @@ -589,7 +591,7 @@ async def test_bridge_import_already_configured(hass): result = await hass.config_entries.flow.async_init( const.DOMAIN, - context={"source": "import"}, + context={"source": config_entries.SOURCE_IMPORT}, data={"host": "0.0.0.0", "properties": {"id": "aa:bb:cc:dd:ee:ff"}}, ) @@ -605,7 +607,7 @@ async def test_bridge_homekit_already_configured(hass): result = await hass.config_entries.flow.async_init( const.DOMAIN, - context={"source": "homekit"}, + context={"source": config_entries.SOURCE_HOMEKIT}, data={"host": "0.0.0.0", "properties": {"id": "aa:bb:cc:dd:ee:ff"}}, ) @@ -622,7 +624,7 @@ async def test_ssdp_discovery_update_configuration(hass): result = await hass.config_entries.flow.async_init( const.DOMAIN, - context={"source": "ssdp"}, + context={"source": config_entries.SOURCE_SSDP}, data={ ssdp.ATTR_SSDP_LOCATION: "http://1.1.1.1/", ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL[0], diff --git a/tests/components/hunterdouglas_powerview/test_config_flow.py b/tests/components/hunterdouglas_powerview/test_config_flow.py index 442cd42f0bcd3..f88e65ff854d9 100644 --- a/tests/components/hunterdouglas_powerview/test_config_flow.py +++ b/tests/components/hunterdouglas_powerview/test_config_flow.py @@ -69,7 +69,9 @@ async def test_form_homekit(hass): """Test we get the form with homekit source.""" await setup.async_setup_component(hass, "persistent_notification", {}) - ignored_config_entry = MockConfigEntry(domain=DOMAIN, data={}, source="ignore") + ignored_config_entry = MockConfigEntry( + domain=DOMAIN, data={}, source=config_entries.SOURCE_IGNORE + ) ignored_config_entry.add_to_hass(hass) mock_powerview_userdata = _get_mock_powerview_userdata() @@ -79,7 +81,7 @@ async def test_form_homekit(hass): ): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": "homekit"}, + context={"source": config_entries.SOURCE_HOMEKIT}, data={ "host": "1.2.3.4", "properties": {"id": "AA::BB::CC::DD::EE::FF"}, @@ -114,7 +116,7 @@ async def test_form_homekit(hass): result3 = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": "homekit"}, + context={"source": config_entries.SOURCE_HOMEKIT}, data={ "host": "1.2.3.4", "properties": {"id": "AA::BB::CC::DD::EE::FF"}, diff --git a/tests/components/hvv_departures/test_config_flow.py b/tests/components/hvv_departures/test_config_flow.py index 3773dbb5967b3..2042c6b95d153 100644 --- a/tests/components/hvv_departures/test_config_flow.py +++ b/tests/components/hvv_departures/test_config_flow.py @@ -255,7 +255,7 @@ async def test_options_flow(hass): domain=DOMAIN, title="Wartenau", data=FIXTURE_CONFIG_ENTRY, - source="user", + source=SOURCE_USER, connection_class=CONN_CLASS_CLOUD_POLL, system_options={"disable_new_entities": False}, options=FIXTURE_OPTIONS, @@ -306,7 +306,7 @@ async def test_options_flow_invalid_auth(hass): domain=DOMAIN, title="Wartenau", data=FIXTURE_CONFIG_ENTRY, - source="user", + source=SOURCE_USER, connection_class=CONN_CLASS_CLOUD_POLL, system_options={"disable_new_entities": False}, options=FIXTURE_OPTIONS, @@ -347,7 +347,7 @@ async def test_options_flow_cannot_connect(hass): domain=DOMAIN, title="Wartenau", data=FIXTURE_CONFIG_ENTRY, - source="user", + source=SOURCE_USER, connection_class=CONN_CLASS_CLOUD_POLL, system_options={"disable_new_entities": False}, options=FIXTURE_OPTIONS, diff --git a/tests/components/ifttt/test_init.py b/tests/components/ifttt/test_init.py index 41885f0cd26b8..077fb6d74700b 100644 --- a/tests/components/ifttt/test_init.py +++ b/tests/components/ifttt/test_init.py @@ -1,5 +1,5 @@ """Test the init file of IFTTT.""" -from homeassistant import data_entry_flow +from homeassistant import config_entries, data_entry_flow from homeassistant.components import ifttt from homeassistant.config import async_process_ha_core_config from homeassistant.core import callback @@ -13,7 +13,7 @@ async def test_config_flow_registers_webhook(hass, aiohttp_client): ) result = await hass.config_entries.flow.async_init( - "ifttt", context={"source": "user"} + "ifttt", context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM, result diff --git a/tests/components/islamic_prayer_times/test_config_flow.py b/tests/components/islamic_prayer_times/test_config_flow.py index b7a942e4f1482..842a877e29295 100644 --- a/tests/components/islamic_prayer_times/test_config_flow.py +++ b/tests/components/islamic_prayer_times/test_config_flow.py @@ -3,7 +3,7 @@ import pytest -from homeassistant import data_entry_flow +from homeassistant import config_entries, data_entry_flow from homeassistant.components import islamic_prayer_times from homeassistant.components.islamic_prayer_times.const import CONF_CALC_METHOD, DOMAIN @@ -23,7 +23,7 @@ def mock_setup(): async def test_flow_works(hass): """Test user config.""" result = await hass.config_entries.flow.async_init( - islamic_prayer_times.DOMAIN, context={"source": "user"} + islamic_prayer_times.DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" @@ -62,7 +62,7 @@ async def test_import(hass): """Test import step.""" result = await hass.config_entries.flow.async_init( islamic_prayer_times.DOMAIN, - context={"source": "import"}, + context={"source": config_entries.SOURCE_IMPORT}, data={CONF_CALC_METHOD: "makkah"}, ) @@ -80,7 +80,7 @@ async def test_integration_already_configured(hass): ) entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - islamic_prayer_times.DOMAIN, context={"source": "user"} + islamic_prayer_times.DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT diff --git a/tests/components/keenetic_ndms2/test_config_flow.py b/tests/components/keenetic_ndms2/test_config_flow.py index b96448101cf89..7561fb0383942 100644 --- a/tests/components/keenetic_ndms2/test_config_flow.py +++ b/tests/components/keenetic_ndms2/test_config_flow.py @@ -136,7 +136,7 @@ async def test_host_already_configured(hass, connect): entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - keenetic.DOMAIN, context={"source": "user"} + keenetic.DOMAIN, context={"source": config_entries.SOURCE_USER} ) result2 = await hass.config_entries.flow.async_configure( @@ -151,7 +151,7 @@ async def test_connection_error(hass, connect_error): """Test error when connection is unsuccessful.""" result = await hass.config_entries.flow.async_init( - keenetic.DOMAIN, context={"source": "user"} + keenetic.DOMAIN, context={"source": config_entries.SOURCE_USER} ) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_DATA diff --git a/tests/components/kodi/test_config_flow.py b/tests/components/kodi/test_config_flow.py index 8b8bcf7e88acc..5ffa231239b67 100644 --- a/tests/components/kodi/test_config_flow.py +++ b/tests/components/kodi/test_config_flow.py @@ -411,7 +411,9 @@ async def test_discovery(hass): return_value=MockConnection(), ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "zeroconf"}, data=TEST_DISCOVERY + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=TEST_DISCOVERY, ) assert result["type"] == "form" @@ -450,7 +452,9 @@ async def test_discovery_cannot_connect_http(hass): return_value=MockConnection(), ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "zeroconf"}, data=TEST_DISCOVERY + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=TEST_DISCOVERY, ) assert result["type"] == "abort" @@ -471,7 +475,9 @@ async def test_discovery_cannot_connect_ws(hass): new=get_kodi_connection, ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "zeroconf"}, data=TEST_DISCOVERY + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=TEST_DISCOVERY, ) assert result["type"] == "form" @@ -489,7 +495,9 @@ async def test_discovery_exception_http(hass, user_flow): return_value=MockConnection(), ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "zeroconf"}, data=TEST_DISCOVERY + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=TEST_DISCOVERY, ) assert result["type"] == "abort" @@ -506,7 +514,9 @@ async def test_discovery_invalid_auth(hass): return_value=MockConnection(), ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "zeroconf"}, data=TEST_DISCOVERY + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=TEST_DISCOVERY, ) assert result["type"] == "form" @@ -524,14 +534,16 @@ async def test_discovery_duplicate_data(hass): return_value=MockConnection(), ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "zeroconf"}, data=TEST_DISCOVERY + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=TEST_DISCOVERY, ) assert result["type"] == "form" assert result["step_id"] == "discovery_confirm" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "zeroconf"}, data=TEST_DISCOVERY + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=TEST_DISCOVERY ) assert result["type"] == "abort" @@ -549,7 +561,7 @@ async def test_discovery_updates_unique_id(hass): entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "zeroconf"}, data=TEST_DISCOVERY + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=TEST_DISCOVERY ) assert result["type"] == "abort" @@ -563,7 +575,9 @@ async def test_discovery_updates_unique_id(hass): async def test_discovery_without_unique_id(hass): """Test a discovery flow with no unique id aborts.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "zeroconf"}, data=TEST_DISCOVERY_WO_UUID + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=TEST_DISCOVERY_WO_UUID, ) assert result["type"] == "abort" diff --git a/tests/components/konnected/test_config_flow.py b/tests/components/konnected/test_config_flow.py index 4b5cc602f9948..36f582fcb57b5 100644 --- a/tests/components/konnected/test_config_flow.py +++ b/tests/components/konnected/test_config_flow.py @@ -3,6 +3,7 @@ import pytest +from homeassistant import config_entries from homeassistant.components import konnected from homeassistant.components.konnected import config_flow @@ -28,7 +29,7 @@ def mock_constructor(host, port, websession): async def test_flow_works(hass, mock_panel): """Test config flow .""" result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": "user"} + config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == "form" assert result["step_id"] == "user" @@ -65,7 +66,7 @@ async def test_flow_works(hass, mock_panel): async def test_pro_flow_works(hass, mock_panel): """Test config flow .""" result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": "user"} + config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == "form" assert result["step_id"] == "user" @@ -110,7 +111,7 @@ async def test_ssdp(hass, mock_panel): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, - context={"source": "ssdp"}, + context={"source": config_entries.SOURCE_SSDP}, data={ "ssdp_location": "http://1.2.3.4:1234/Device.xml", "manufacturer": config_flow.KONN_MANUFACTURER, @@ -137,7 +138,7 @@ async def test_import_no_host_user_finish(hass, mock_panel): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, - context={"source": "import"}, + context={"source": config_entries.SOURCE_IMPORT}, data={ "default_options": { "blink": True, @@ -204,7 +205,7 @@ async def test_import_ssdp_host_user_finish(hass, mock_panel): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, - context={"source": "import"}, + context={"source": config_entries.SOURCE_IMPORT}, data={ "default_options": { "blink": True, @@ -238,7 +239,7 @@ async def test_import_ssdp_host_user_finish(hass, mock_panel): # discover the panel via ssdp ssdp_result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, - context={"source": "ssdp"}, + context={"source": config_entries.SOURCE_SSDP}, data={ "ssdp_location": "http://0.0.0.0:1234/Device.xml", "manufacturer": config_flow.KONN_MANUFACTURER, @@ -281,7 +282,7 @@ async def test_ssdp_already_configured(hass, mock_panel): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, - context={"source": "ssdp"}, + context={"source": config_entries.SOURCE_SSDP}, data={ "ssdp_location": "http://0.0.0.0:1234/Device.xml", "manufacturer": config_flow.KONN_MANUFACTURER, @@ -357,7 +358,7 @@ async def test_ssdp_host_update(hass, mock_panel): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, - context={"source": "ssdp"}, + context={"source": config_entries.SOURCE_SSDP}, data={ "ssdp_location": "http://1.1.1.1:1234/Device.xml", "manufacturer": config_flow.KONN_MANUFACTURER, @@ -382,7 +383,7 @@ async def test_import_existing_config(hass, mock_panel): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, - context={"source": "import"}, + context={"source": config_entries.SOURCE_IMPORT}, data=konnected.DEVICE_SCHEMA_YAML( { "host": "1.2.3.4", @@ -515,7 +516,7 @@ async def test_import_existing_config_entry(hass, mock_panel): hass.data[config_flow.DOMAIN] = {"access_token": "SUPERSECRETTOKEN"} result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, - context={"source": "import"}, + context={"source": config_entries.SOURCE_IMPORT}, data={ "host": "1.2.3.4", "port": 1234, @@ -573,7 +574,7 @@ async def test_import_pin_config(hass, mock_panel): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, - context={"source": "import"}, + context={"source": config_entries.SOURCE_IMPORT}, data=konnected.DEVICE_SCHEMA_YAML( { "host": "1.2.3.4", diff --git a/tests/components/litejet/test_config_flow.py b/tests/components/litejet/test_config_flow.py index 015ba1c64946a..1d72324f48489 100644 --- a/tests/components/litejet/test_config_flow.py +++ b/tests/components/litejet/test_config_flow.py @@ -3,6 +3,7 @@ from serial import SerialException +from homeassistant import config_entries from homeassistant.components.litejet.const import DOMAIN from homeassistant.const import CONF_PORT @@ -12,7 +13,7 @@ async def test_show_config_form(hass): """Test show configuration form.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == "form" @@ -24,7 +25,7 @@ async def test_create_entry(hass, mock_litejet): test_data = {CONF_PORT: "/dev/test"} result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"}, data=test_data + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=test_data ) assert result["type"] == "create_entry" @@ -43,7 +44,7 @@ async def test_flow_entry_already_exists(hass): test_data = {CONF_PORT: "/dev/test"} result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"}, data=test_data + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=test_data ) assert result["type"] == "abort" @@ -58,7 +59,7 @@ async def test_flow_open_failed(hass): mock_pylitejet.side_effect = SerialException result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"}, data=test_data + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=test_data ) assert result["type"] == "form" @@ -69,7 +70,7 @@ async def test_import_step(hass): """Test initializing via import step.""" test_data = {CONF_PORT: "/dev/imported"} result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "import"}, data=test_data + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=test_data ) assert result["type"] == "create_entry" diff --git a/tests/components/locative/test_init.py b/tests/components/locative/test_init.py index d183cb9814a19..bd39ec42978c6 100644 --- a/tests/components/locative/test_init.py +++ b/tests/components/locative/test_init.py @@ -3,7 +3,7 @@ import pytest -from homeassistant import data_entry_flow +from homeassistant import config_entries, data_entry_flow from homeassistant.components import locative from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN from homeassistant.components.locative import DOMAIN, TRACKER_UPDATE @@ -39,7 +39,7 @@ async def webhook_id(hass, locative_client): {"internal_url": "http://example.local:8123"}, ) result = await hass.config_entries.flow.async_init( - "locative", context={"source": "user"} + "locative", context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM, result diff --git a/tests/components/lutron_caseta/test_config_flow.py b/tests/components/lutron_caseta/test_config_flow.py index e65eb9d5f476c..14adefc37dbb9 100644 --- a/tests/components/lutron_caseta/test_config_flow.py +++ b/tests/components/lutron_caseta/test_config_flow.py @@ -197,7 +197,9 @@ async def test_already_configured_with_ignored(hass): """Test ignored entries do not break checking for existing entries.""" await setup.async_setup_component(hass, "persistent_notification", {}) - config_entry = MockConfigEntry(domain=DOMAIN, data={}, source="ignore") + config_entry = MockConfigEntry( + domain=DOMAIN, data={}, source=config_entries.SOURCE_IGNORE + ) config_entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( diff --git a/tests/components/lyric/test_config_flow.py b/tests/components/lyric/test_config_flow.py index 71fb473127d42..f5f41da08fd67 100644 --- a/tests/components/lyric/test_config_flow.py +++ b/tests/components/lyric/test_config_flow.py @@ -160,7 +160,7 @@ async def test_reauthentication_flow( old_entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "reauth"}, data=old_entry.data + DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=old_entry.data ) flows = hass.config_entries.flow.async_progress() diff --git a/tests/components/mailgun/test_init.py b/tests/components/mailgun/test_init.py index fd244d87a8f08..bf0df205c9331 100644 --- a/tests/components/mailgun/test_init.py +++ b/tests/components/mailgun/test_init.py @@ -4,7 +4,7 @@ import pytest -from homeassistant import data_entry_flow +from homeassistant import config_entries, data_entry_flow from homeassistant.components import mailgun, webhook from homeassistant.config import async_process_ha_core_config from homeassistant.const import CONF_API_KEY, CONF_DOMAIN @@ -35,7 +35,7 @@ async def webhook_id_with_api_key(hass): {"internal_url": "http://example.local:8123"}, ) result = await hass.config_entries.flow.async_init( - "mailgun", context={"source": "user"} + "mailgun", context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM, result @@ -55,7 +55,7 @@ async def webhook_id_without_api_key(hass): {"internal_url": "http://example.local:8123"}, ) result = await hass.config_entries.flow.async_init( - "mailgun", context={"source": "user"} + "mailgun", context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM, result diff --git a/tests/components/mazda/test_config_flow.py b/tests/components/mazda/test_config_flow.py index 06cb0e15d09b8..f6ea8c73a9bff 100644 --- a/tests/components/mazda/test_config_flow.py +++ b/tests/components/mazda/test_config_flow.py @@ -199,7 +199,10 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": "reauth", "entry_id": mock_config.entry_id}, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": mock_config.entry_id, + }, data=FIXTURE_USER_INPUT, ) @@ -239,7 +242,10 @@ async def test_reauth_authorization_error(hass: HomeAssistant) -> None: ): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": "reauth", "entry_id": mock_config.entry_id}, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": mock_config.entry_id, + }, data=FIXTURE_USER_INPUT, ) @@ -275,7 +281,10 @@ async def test_reauth_account_locked(hass: HomeAssistant) -> None: ): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": "reauth", "entry_id": mock_config.entry_id}, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": mock_config.entry_id, + }, data=FIXTURE_USER_INPUT, ) @@ -311,7 +320,10 @@ async def test_reauth_connection_error(hass: HomeAssistant) -> None: ): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": "reauth", "entry_id": mock_config.entry_id}, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": mock_config.entry_id, + }, data=FIXTURE_USER_INPUT, ) @@ -347,7 +359,10 @@ async def test_reauth_unknown_error(hass: HomeAssistant) -> None: ): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": "reauth", "entry_id": mock_config.entry_id}, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": mock_config.entry_id, + }, data=FIXTURE_USER_INPUT, ) @@ -383,7 +398,10 @@ async def test_reauth_user_has_new_email_address(hass: HomeAssistant) -> None: ): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": "reauth", "entry_id": mock_config.entry_id}, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": mock_config.entry_id, + }, data=FIXTURE_USER_INPUT, ) diff --git a/tests/components/met/test_config_flow.py b/tests/components/met/test_config_flow.py index 25e123f67e828..ff5deb18194f2 100644 --- a/tests/components/met/test_config_flow.py +++ b/tests/components/met/test_config_flow.py @@ -3,6 +3,7 @@ import pytest +from homeassistant import config_entries from homeassistant.components.met.const import DOMAIN, HOME_LOCATION_NAME from homeassistant.config import async_process_ha_core_config from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE @@ -20,7 +21,7 @@ def met_setup_fixture(): async def test_show_config_form(hass): """Test show configuration form.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == "form" @@ -38,7 +39,7 @@ async def test_flow_with_home_location(hass): hass.config.elevation = 3 result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == "form" @@ -61,7 +62,7 @@ async def test_create_entry(hass): } result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"}, data=test_data + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=test_data ) assert result["type"] == "create_entry" @@ -89,7 +90,7 @@ async def test_flow_entry_already_exists(hass): } result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"}, data=test_data + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=test_data ) assert result["type"] == "form" @@ -136,7 +137,7 @@ async def test_import_step(hass): "track_home": True, } result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "import"}, data=test_data + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=test_data ) assert result["type"] == "create_entry" diff --git a/tests/components/met/test_weather.py b/tests/components/met/test_weather.py index 32f36d096307c..8ffa4076b8a51 100644 --- a/tests/components/met/test_weather.py +++ b/tests/components/met/test_weather.py @@ -1,5 +1,6 @@ """Test Met weather entity.""" +from homeassistant import config_entries from homeassistant.components.met import DOMAIN from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN from homeassistant.helpers import entity_registry as er @@ -55,7 +56,7 @@ async def test_not_tracking_home(hass, mock_weather): await hass.config_entries.flow.async_init( "met", - context={"source": "user"}, + context={"source": config_entries.SOURCE_USER}, data={"name": "Somewhere", "latitude": 10, "longitude": 20, "elevation": 0}, ) await hass.async_block_till_done() diff --git a/tests/components/mikrotik/test_config_flow.py b/tests/components/mikrotik/test_config_flow.py index c884315f4004c..411408e8c9853 100644 --- a/tests/components/mikrotik/test_config_flow.py +++ b/tests/components/mikrotik/test_config_flow.py @@ -5,7 +5,7 @@ import librouteros import pytest -from homeassistant import data_entry_flow +from homeassistant import config_entries, data_entry_flow from homeassistant.components import mikrotik from homeassistant.const import ( CONF_HOST, @@ -81,7 +81,9 @@ def mock_api_connection_error(): async def test_import(hass, api): """Test import step.""" result = await hass.config_entries.flow.async_init( - mikrotik.DOMAIN, context={"source": "import"}, data=DEMO_CONFIG + mikrotik.DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=DEMO_CONFIG, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -98,7 +100,7 @@ async def test_flow_works(hass, api): """Test config flow.""" result = await hass.config_entries.flow.async_init( - mikrotik.DOMAIN, context={"source": "user"} + mikrotik.DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" @@ -150,7 +152,7 @@ async def test_host_already_configured(hass, auth_error): entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - mikrotik.DOMAIN, context={"source": "user"} + mikrotik.DOMAIN, context={"source": config_entries.SOURCE_USER} ) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=DEMO_USER_INPUT @@ -168,7 +170,7 @@ async def test_name_exists(hass, api): user_input[CONF_HOST] = "0.0.0.1" result = await hass.config_entries.flow.async_init( - mikrotik.DOMAIN, context={"source": "user"} + mikrotik.DOMAIN, context={"source": config_entries.SOURCE_USER} ) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=user_input @@ -182,7 +184,7 @@ async def test_connection_error(hass, conn_error): """Test error when connection is unsuccessful.""" result = await hass.config_entries.flow.async_init( - mikrotik.DOMAIN, context={"source": "user"} + mikrotik.DOMAIN, context={"source": config_entries.SOURCE_USER} ) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=DEMO_USER_INPUT @@ -195,7 +197,7 @@ async def test_wrong_credentials(hass, auth_error): """Test error when credentials are wrong.""" result = await hass.config_entries.flow.async_init( - mikrotik.DOMAIN, context={"source": "user"} + mikrotik.DOMAIN, context={"source": config_entries.SOURCE_USER} ) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=DEMO_USER_INPUT diff --git a/tests/components/mill/test_config_flow.py b/tests/components/mill/test_config_flow.py index ee565d3221199..ce35b3d9708b6 100644 --- a/tests/components/mill/test_config_flow.py +++ b/tests/components/mill/test_config_flow.py @@ -3,6 +3,7 @@ import pytest +from homeassistant import config_entries from homeassistant.components.mill.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME @@ -19,7 +20,7 @@ def mill_setup_fixture(): async def test_show_config_form(hass): """Test show configuration form.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == "form" @@ -35,7 +36,7 @@ async def test_create_entry(hass): with patch("mill.Mill.connect", return_value=True): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"}, data=test_data + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=test_data ) assert result["type"] == "create_entry" @@ -60,7 +61,7 @@ async def test_flow_entry_already_exists(hass): with patch("mill.Mill.connect", return_value=True): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"}, data=test_data + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=test_data ) assert result["type"] == "abort" @@ -84,7 +85,7 @@ async def test_connection_error(hass): with patch("mill.Mill.connect", return_value=False): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"}, data=test_data + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=test_data ) assert result["type"] == "form" diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index b41a446a8c061..55bacb0ef91f3 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -5,7 +5,7 @@ import pytest import voluptuous as vol -from homeassistant import data_entry_flow +from homeassistant import config_entries, data_entry_flow from homeassistant.components import mqtt from homeassistant.setup import async_setup_component @@ -33,7 +33,7 @@ async def test_user_connection_works(hass, mock_try_connection, mock_finish_setu mock_try_connection.return_value = True result = await hass.config_entries.flow.async_init( - "mqtt", context={"source": "user"} + "mqtt", context={"source": config_entries.SOURCE_USER} ) assert result["type"] == "form" @@ -58,7 +58,7 @@ async def test_user_connection_fails(hass, mock_try_connection, mock_finish_setu mock_try_connection.return_value = False result = await hass.config_entries.flow.async_init( - "mqtt", context={"source": "user"} + "mqtt", context={"source": config_entries.SOURCE_USER} ) assert result["type"] == "form" @@ -84,7 +84,7 @@ async def test_manual_config_set(hass, mock_try_connection, mock_finish_setup): mock_try_connection.return_value = True result = await hass.config_entries.flow.async_init( - "mqtt", context={"source": "user"} + "mqtt", context={"source": config_entries.SOURCE_USER} ) assert result["type"] == "abort" @@ -94,7 +94,7 @@ async def test_user_single_instance(hass): MockConfigEntry(domain="mqtt").add_to_hass(hass) result = await hass.config_entries.flow.async_init( - "mqtt", context={"source": "user"} + "mqtt", context={"source": config_entries.SOURCE_USER} ) assert result["type"] == "abort" assert result["reason"] == "single_instance_allowed" @@ -105,7 +105,7 @@ async def test_hassio_single_instance(hass): MockConfigEntry(domain="mqtt").add_to_hass(hass) result = await hass.config_entries.flow.async_init( - "mqtt", context={"source": "hassio"} + "mqtt", context={"source": config_entries.SOURCE_HASSIO} ) assert result["type"] == "abort" assert result["reason"] == "single_instance_allowed" @@ -125,7 +125,7 @@ async def test_hassio_confirm(hass, mock_try_connection, mock_finish_setup): "password": "mock-pass", "protocol": "3.1.1", }, - context={"source": "hassio"}, + context={"source": config_entries.SOURCE_HASSIO}, ) assert result["type"] == "form" assert result["step_id"] == "hassio_confirm" diff --git a/tests/components/myq/test_config_flow.py b/tests/components/myq/test_config_flow.py index bbfa090b01c9e..683b6beab8aa8 100644 --- a/tests/components/myq/test_config_flow.py +++ b/tests/components/myq/test_config_flow.py @@ -88,7 +88,7 @@ async def test_form_homekit(hass): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": "homekit"}, + context={"source": config_entries.SOURCE_HOMEKIT}, data={"properties": {"id": "AA:BB:CC:DD:EE:FF"}}, ) assert result["type"] == "form" @@ -107,7 +107,7 @@ async def test_form_homekit(hass): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": "homekit"}, + context={"source": config_entries.SOURCE_HOMEKIT}, data={"properties": {"id": "AA:BB:CC:DD:EE:FF"}}, ) assert result["type"] == "abort" diff --git a/tests/components/netatmo/test_config_flow.py b/tests/components/netatmo/test_config_flow.py index 03c751aae9667..6bd8086c8201f 100644 --- a/tests/components/netatmo/test_config_flow.py +++ b/tests/components/netatmo/test_config_flow.py @@ -36,7 +36,7 @@ async def test_abort_if_existing_entry(hass): result = await hass.config_entries.flow.async_init( "netatmo", - context={"source": "homekit"}, + context={"source": config_entries.SOURCE_HOMEKIT}, data={"host": "0.0.0.0", "properties": {"id": "aa:bb:cc:dd:ee:ff"}}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT diff --git a/tests/components/onewire/__init__.py b/tests/components/onewire/__init__.py index fdc0c7fe12cca..088c6a3ad11bf 100644 --- a/tests/components/onewire/__init__.py +++ b/tests/components/onewire/__init__.py @@ -14,7 +14,7 @@ DEFAULT_SYSBUS_MOUNT_DIR, DOMAIN, ) -from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL +from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL, SOURCE_USER from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE from .const import MOCK_OWPROXY_DEVICES, MOCK_SYSBUS_DEVICES @@ -26,7 +26,7 @@ async def setup_onewire_sysbus_integration(hass): """Create the 1-Wire integration.""" config_entry = MockConfigEntry( domain=DOMAIN, - source="user", + source=SOURCE_USER, data={ CONF_TYPE: CONF_TYPE_SYSBUS, CONF_MOUNT_DIR: DEFAULT_SYSBUS_MOUNT_DIR, @@ -51,7 +51,7 @@ async def setup_onewire_owserver_integration(hass): """Create the 1-Wire integration.""" config_entry = MockConfigEntry( domain=DOMAIN, - source="user", + source=SOURCE_USER, data={ CONF_TYPE: CONF_TYPE_OWSERVER, CONF_HOST: "1.2.3.4", @@ -76,7 +76,7 @@ async def setup_onewire_patched_owserver_integration(hass): """Create the 1-Wire integration.""" config_entry = MockConfigEntry( domain=DOMAIN, - source="user", + source=SOURCE_USER, data={ CONF_TYPE: CONF_TYPE_OWSERVER, CONF_HOST: "1.2.3.4", diff --git a/tests/components/onewire/test_init.py b/tests/components/onewire/test_init.py index 5783b241a2f63..21e512a2229ce 100644 --- a/tests/components/onewire/test_init.py +++ b/tests/components/onewire/test_init.py @@ -10,6 +10,7 @@ ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED, ENTRY_STATE_SETUP_RETRY, + SOURCE_USER, ) from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -29,7 +30,7 @@ async def test_owserver_connect_failure(hass): """Test connection failure raises ConfigEntryNotReady.""" config_entry_owserver = MockConfigEntry( domain=DOMAIN, - source="user", + source=SOURCE_USER, data={ CONF_TYPE: CONF_TYPE_OWSERVER, CONF_HOST: "1.2.3.4", @@ -58,7 +59,7 @@ async def test_failed_owserver_listing(hass): """Create the 1-Wire integration.""" config_entry_owserver = MockConfigEntry( domain=DOMAIN, - source="user", + source=SOURCE_USER, data={ CONF_TYPE: CONF_TYPE_OWSERVER, CONF_HOST: "1.2.3.4", diff --git a/tests/components/onvif/test_config_flow.py b/tests/components/onvif/test_config_flow.py index 1802d21134802..a8827247689a0 100644 --- a/tests/components/onvif/test_config_flow.py +++ b/tests/components/onvif/test_config_flow.py @@ -137,7 +137,7 @@ async def setup_onvif_integration( options=None, unique_id=MAC, entry_id="1", - source="user", + source=config_entries.SOURCE_USER, ): """Create an ONVIF config entry.""" if not config: @@ -180,7 +180,7 @@ async def test_flow_discovered_devices(hass): """Test that config flow works for discovered devices.""" result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": "user"} + config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -245,7 +245,7 @@ async def test_flow_discovered_devices_ignore_configured_manual_input(hass): await setup_onvif_integration(hass) result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": "user"} + config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -296,7 +296,7 @@ async def test_flow_discovery_ignore_existing_and_abort(hass): ) result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": "user"} + config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -348,7 +348,7 @@ async def test_flow_discovery_ignore_existing_and_abort(hass): async def test_flow_manual_entry(hass): """Test that config flow works for discovered devices.""" result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": "user"} + config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM diff --git a/tests/components/ovo_energy/test_config_flow.py b/tests/components/ovo_energy/test_config_flow.py index d5ee8c6d3d973..a8f0c098aba9b 100644 --- a/tests/components/ovo_energy/test_config_flow.py +++ b/tests/components/ovo_energy/test_config_flow.py @@ -105,7 +105,9 @@ async def test_reauth_authorization_error(hass: HomeAssistant) -> None: return_value=False, ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT + DOMAIN, + context={"source": config_entries.SOURCE_REAUTH}, + data=FIXTURE_USER_INPUT, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -129,7 +131,9 @@ async def test_reauth_connection_error(hass: HomeAssistant) -> None: side_effect=aiohttp.ClientError, ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT + DOMAIN, + context={"source": config_entries.SOURCE_REAUTH}, + data=FIXTURE_USER_INPUT, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -158,7 +162,9 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: mock_config.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT + DOMAIN, + context={"source": config_entries.SOURCE_REAUTH}, + data=FIXTURE_USER_INPUT, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM diff --git a/tests/components/owntracks/test_config_flow.py b/tests/components/owntracks/test_config_flow.py index d6ac059ce2687..93d4bf5385b46 100644 --- a/tests/components/owntracks/test_config_flow.py +++ b/tests/components/owntracks/test_config_flow.py @@ -3,7 +3,7 @@ import pytest -from homeassistant import data_entry_flow +from homeassistant import config_entries, data_entry_flow from homeassistant.components.owntracks import config_flow from homeassistant.components.owntracks.config_flow import CONF_CLOUDHOOK, CONF_SECRET from homeassistant.components.owntracks.const import DOMAIN @@ -143,7 +143,7 @@ async def test_unload(hass): "homeassistant.config_entries.ConfigEntries.async_forward_entry_setup" ) as mock_forward: result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "import"}, data={} + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data={} ) assert len(mock_forward.mock_calls) == 1 @@ -175,7 +175,7 @@ async def test_with_cloud_sub(hass): return_value="https://hooks.nabu.casa/ABCD", ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"}, data={} + DOMAIN, context={"source": config_entries.SOURCE_USER}, data={} ) entry = result["result"] diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index e0555bab0e84d..d5209201d940f 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -26,6 +26,7 @@ ENTRY_STATE_LOADED, SOURCE_INTEGRATION_DISCOVERY, SOURCE_REAUTH, + SOURCE_USER, ) from homeassistant.const import ( CONF_HOST, @@ -52,7 +53,7 @@ async def test_bad_credentials(hass): ) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == "form" assert result["step_id"] == "user" @@ -85,7 +86,7 @@ async def test_bad_hostname(hass, mock_plex_calls): ) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == "form" assert result["step_id"] == "user" @@ -119,7 +120,7 @@ async def test_unknown_exception(hass): ) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == "form" assert result["step_id"] == "user" @@ -150,7 +151,7 @@ async def test_no_servers_found(hass, mock_plex_calls, requests_mock, empty_payl ) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == "form" assert result["step_id"] == "user" @@ -181,7 +182,7 @@ async def test_single_available_server(hass, mock_plex_calls): ) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == "form" assert result["step_id"] == "user" @@ -226,7 +227,7 @@ async def test_multiple_servers_with_selection( ) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == "form" assert result["step_id"] == "user" @@ -290,7 +291,7 @@ async def test_adding_last_unconfigured_server( ).add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == "form" assert result["step_id"] == "user" @@ -350,7 +351,7 @@ async def test_all_available_servers_configured( ).add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == "form" assert result["step_id"] == "user" @@ -478,7 +479,7 @@ async def test_external_timed_out(hass): ) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == "form" assert result["step_id"] == "user" @@ -508,7 +509,7 @@ async def test_callback_view(hass, aiohttp_client): ) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == "form" assert result["step_id"] == "user" @@ -545,7 +546,7 @@ def __init__(self): # Basic mode result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": "user"} + config_flow.DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == "form" @@ -555,7 +556,8 @@ def __init__(self): # Advanced automatic result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": "user", "show_advanced_options": True} + config_flow.DOMAIN, + context={"source": SOURCE_USER, "show_advanced_options": True}, ) assert result["data_schema"] is not None @@ -572,7 +574,8 @@ def __init__(self): # Advanced manual result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": "user", "show_advanced_options": True} + config_flow.DOMAIN, + context={"source": SOURCE_USER, "show_advanced_options": True}, ) assert result["data_schema"] is not None @@ -668,7 +671,8 @@ async def test_manual_config_with_token(hass, mock_plex_calls): """Test creating via manual configuration with only token.""" result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": "user", "show_advanced_options": True} + config_flow.DOMAIN, + context={"source": SOURCE_USER, "show_advanced_options": True}, ) assert result["type"] == "form" diff --git a/tests/components/powerwall/test_config_flow.py b/tests/components/powerwall/test_config_flow.py index 407a63bac23fb..61230f59e52f7 100644 --- a/tests/components/powerwall/test_config_flow.py +++ b/tests/components/powerwall/test_config_flow.py @@ -159,7 +159,9 @@ async def test_already_configured_with_ignored(hass): """Test ignored entries do not break checking for existing entries.""" await setup.async_setup_component(hass, "persistent_notification", {}) - config_entry = MockConfigEntry(domain=DOMAIN, data={}, source="ignore") + config_entry = MockConfigEntry( + domain=DOMAIN, data={}, source=config_entries.SOURCE_IGNORE + ) config_entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( @@ -220,7 +222,7 @@ async def test_form_reauth(hass): entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "reauth"}, data=entry.data + DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=entry.data ) assert result["type"] == "form" assert result["errors"] == {} diff --git a/tests/components/ps4/test_config_flow.py b/tests/components/ps4/test_config_flow.py index bcae74c19fbf5..f8c28d236beb8 100644 --- a/tests/components/ps4/test_config_flow.py +++ b/tests/components/ps4/test_config_flow.py @@ -4,7 +4,7 @@ from pyps4_2ndscreen.errors import CredentialTimeout import pytest -from homeassistant import data_entry_flow +from homeassistant import config_entries, data_entry_flow from homeassistant.components import ps4 from homeassistant.components.ps4.config_flow import LOCAL_UDP_PORT from homeassistant.components.ps4.const import ( @@ -101,7 +101,7 @@ async def test_full_flow_implementation(hass): # User Step Started, results in Step Creds with patch("pyps4_2ndscreen.Helper.port_bind", return_value=None): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "creds" @@ -142,7 +142,7 @@ async def test_multiple_flow_implementation(hass): # User Step Started, results in Step Creds with patch("pyps4_2ndscreen.Helper.port_bind", return_value=None): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "creds" @@ -194,7 +194,7 @@ async def test_multiple_flow_implementation(hass): return_value=[{"host-ip": MOCK_HOST}, {"host-ip": MOCK_HOST_ADDITIONAL}], ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "creds" @@ -247,7 +247,7 @@ async def test_port_bind_abort(hass): with patch("pyps4_2ndscreen.Helper.port_bind", return_value=MOCK_UDP_PORT): reason = "port_987_bind_error" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == reason @@ -255,7 +255,7 @@ async def test_port_bind_abort(hass): with patch("pyps4_2ndscreen.Helper.port_bind", return_value=MOCK_TCP_PORT): reason = "port_997_bind_error" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == reason @@ -267,7 +267,7 @@ async def test_duplicate_abort(hass): with patch("pyps4_2ndscreen.Helper.port_bind", return_value=None): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "creds" @@ -297,7 +297,7 @@ async def test_additional_device(hass): with patch("pyps4_2ndscreen.Helper.port_bind", return_value=None): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "creds" @@ -370,7 +370,7 @@ async def test_no_devices_found_abort(hass): """Test that failure to find devices aborts flow.""" with patch("pyps4_2ndscreen.Helper.port_bind", return_value=None): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "creds" @@ -395,7 +395,7 @@ async def test_manual_mode(hass): """Test host specified in manual mode is passed to Step Link.""" with patch("pyps4_2ndscreen.Helper.port_bind", return_value=None): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "creds" @@ -423,7 +423,7 @@ async def test_credential_abort(hass): """Test that failure to get credentials aborts flow.""" with patch("pyps4_2ndscreen.Helper.port_bind", return_value=None): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "creds" @@ -441,7 +441,7 @@ async def test_credential_timeout(hass): """Test that Credential Timeout shows error.""" with patch("pyps4_2ndscreen.Helper.port_bind", return_value=None): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "creds" @@ -460,7 +460,7 @@ async def test_wrong_pin_error(hass): """Test that incorrect pin throws an error.""" with patch("pyps4_2ndscreen.Helper.port_bind", return_value=None): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "creds" @@ -492,7 +492,7 @@ async def test_device_connection_error(hass): """Test that device not connected or on throws an error.""" with patch("pyps4_2ndscreen.Helper.port_bind", return_value=None): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "creds" @@ -524,7 +524,7 @@ async def test_manual_mode_no_ip_error(hass): """Test no IP specified in manual mode throws an error.""" with patch("pyps4_2ndscreen.Helper.port_bind", return_value=None): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "creds" diff --git a/tests/components/pvpc_hourly_pricing/test_config_flow.py b/tests/components/pvpc_hourly_pricing/test_config_flow.py index 038b106f6c81a..31a7005c4cc12 100644 --- a/tests/components/pvpc_hourly_pricing/test_config_flow.py +++ b/tests/components/pvpc_hourly_pricing/test_config_flow.py @@ -4,7 +4,7 @@ from pytz import timezone -from homeassistant import data_entry_flow +from homeassistant import config_entries, data_entry_flow from homeassistant.components.pvpc_hourly_pricing import ATTR_TARIFF, DOMAIN from homeassistant.const import CONF_NAME from homeassistant.helpers import entity_registry as er @@ -34,7 +34,7 @@ def mock_now(): with patch("homeassistant.util.dt.utcnow", new=mock_now): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -50,7 +50,7 @@ def mock_now(): # Check abort when configuring another with same tariff result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM result = await hass.config_entries.flow.async_configure( @@ -66,7 +66,7 @@ def mock_now(): # and add it again with UI result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM diff --git a/tests/components/rachio/test_config_flow.py b/tests/components/rachio/test_config_flow.py index ddf403343cffe..324cfd0fbf11d 100644 --- a/tests/components/rachio/test_config_flow.py +++ b/tests/components/rachio/test_config_flow.py @@ -111,7 +111,7 @@ async def test_form_homekit(hass): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": "homekit"}, + context={"source": config_entries.SOURCE_HOMEKIT}, data={"properties": {"id": "AA:BB:CC:DD:EE:FF"}}, ) assert result["type"] == "form" @@ -128,7 +128,7 @@ async def test_form_homekit(hass): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": "homekit"}, + context={"source": config_entries.SOURCE_HOMEKIT}, data={"properties": {"id": "AA:BB:CC:DD:EE:FF"}}, ) assert result["type"] == "abort" diff --git a/tests/components/roomba/test_config_flow.py b/tests/components/roomba/test_config_flow.py index 32b3c1d95b35b..bebf17247616d 100644 --- a/tests/components/roomba/test_config_flow.py +++ b/tests/components/roomba/test_config_flow.py @@ -820,7 +820,9 @@ async def test_dhcp_discovery_with_ignored(hass): """Test ignored entries do not break checking for existing entries.""" await setup.async_setup_component(hass, "persistent_notification", {}) - config_entry = MockConfigEntry(domain=DOMAIN, data={}, source="ignore") + config_entry = MockConfigEntry( + domain=DOMAIN, data={}, source=config_entries.SOURCE_IGNORE + ) config_entry.add_to_hass(hass) with patch( diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index ea78ecacb3e67..fb1b2a2bc67e1 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -6,6 +6,7 @@ from samsungtvws.exceptions import ConnectionFailure from websocket import WebSocketProtocolException +from homeassistant import config_entries from homeassistant.components.samsungtv.const import ( CONF_MANUFACTURER, CONF_MODEL, @@ -102,7 +103,7 @@ async def test_user_legacy(hass, remote): """Test starting a flow by user.""" # show form result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == "form" assert result["step_id"] == "user" @@ -129,7 +130,7 @@ async def test_user_websocket(hass, remotews): ): # show form result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == "form" assert result["step_id"] == "user" @@ -157,7 +158,7 @@ async def test_user_legacy_missing_auth(hass): ), patch("homeassistant.components.samsungtv.config_flow.socket"): # legacy device missing authentication result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) assert result["type"] == "abort" assert result["reason"] == "auth_missing" @@ -171,7 +172,7 @@ async def test_user_legacy_not_supported(hass): ), patch("homeassistant.components.samsungtv.config_flow.socket"): # legacy device not supported result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) assert result["type"] == "abort" assert result["reason"] == "not_supported" @@ -190,7 +191,7 @@ async def test_user_websocket_not_supported(hass): ): # websocket device not supported result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) assert result["type"] == "abort" assert result["reason"] == "not_supported" @@ -208,7 +209,7 @@ async def test_user_not_successful(hass): "homeassistant.components.samsungtv.config_flow.socket" ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) assert result["type"] == "abort" assert result["reason"] == "cannot_connect" @@ -226,7 +227,7 @@ async def test_user_not_successful_2(hass): "homeassistant.components.samsungtv.config_flow.socket" ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) assert result["type"] == "abort" assert result["reason"] == "cannot_connect" @@ -237,13 +238,13 @@ async def test_user_already_configured(hass, remote): # entry was added result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) assert result["type"] == "create_entry" # failed as already configured result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) assert result["type"] == "abort" assert result["reason"] == "already_configured" @@ -254,7 +255,7 @@ async def test_ssdp(hass, remote): # confirm to add the entry result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "ssdp"}, data=MOCK_SSDP_DATA + DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA ) assert result["type"] == "form" assert result["step_id"] == "confirm" @@ -277,7 +278,9 @@ async def test_ssdp_noprefix(hass, remote): # confirm to add the entry result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "ssdp"}, data=MOCK_SSDP_DATA_NOPREFIX + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=MOCK_SSDP_DATA_NOPREFIX, ) assert result["type"] == "form" assert result["step_id"] == "confirm" @@ -304,7 +307,7 @@ async def test_ssdp_legacy_missing_auth(hass): # confirm to add the entry result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "ssdp"}, data=MOCK_SSDP_DATA + DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA ) assert result["type"] == "form" assert result["step_id"] == "confirm" @@ -326,7 +329,7 @@ async def test_ssdp_legacy_not_supported(hass): # confirm to add the entry result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "ssdp"}, data=MOCK_SSDP_DATA + DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA ) assert result["type"] == "form" assert result["step_id"] == "confirm" @@ -352,7 +355,7 @@ async def test_ssdp_websocket_not_supported(hass): ): # confirm to add the entry result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "ssdp"}, data=MOCK_SSDP_DATA + DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA ) assert result["type"] == "form" assert result["step_id"] == "confirm" @@ -379,7 +382,7 @@ async def test_ssdp_not_successful(hass): # confirm to add the entry result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "ssdp"}, data=MOCK_SSDP_DATA + DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA ) assert result["type"] == "form" assert result["step_id"] == "confirm" @@ -406,7 +409,7 @@ async def test_ssdp_not_successful_2(hass): # confirm to add the entry result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "ssdp"}, data=MOCK_SSDP_DATA + DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA ) assert result["type"] == "form" assert result["step_id"] == "confirm" @@ -424,14 +427,14 @@ async def test_ssdp_already_in_progress(hass, remote): # confirm to add the entry result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "ssdp"}, data=MOCK_SSDP_DATA + DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA ) assert result["type"] == "form" assert result["step_id"] == "confirm" # failed as already in progress result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "ssdp"}, data=MOCK_SSDP_DATA + DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA ) assert result["type"] == "abort" assert result["reason"] == "already_in_progress" @@ -442,7 +445,7 @@ async def test_ssdp_already_configured(hass, remote): # entry was added result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) assert result["type"] == "create_entry" entry = result["result"] @@ -452,7 +455,7 @@ async def test_ssdp_already_configured(hass, remote): # failed as already configured result2 = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "ssdp"}, data=MOCK_SSDP_DATA + DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA ) assert result2["type"] == "abort" assert result2["reason"] == "already_configured" @@ -477,7 +480,7 @@ async def test_autodetect_websocket(hass, remote, remotews): remotews.return_value = remote result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) assert result["type"] == "create_entry" assert result["data"][CONF_METHOD] == "websocket" @@ -503,7 +506,7 @@ async def test_autodetect_websocket_ssl(hass, remote, remotews): remotews.return_value = remote result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) assert result["type"] == "create_entry" assert result["data"][CONF_METHOD] == "websocket" @@ -522,7 +525,7 @@ async def test_autodetect_auth_missing(hass, remote): side_effect=[AccessDenied("Boom")], ) as remote: result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) assert result["type"] == "abort" assert result["reason"] == "auth_missing" @@ -537,7 +540,7 @@ async def test_autodetect_not_supported(hass, remote): side_effect=[UnhandledResponse("Boom")], ) as remote: result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) assert result["type"] == "abort" assert result["reason"] == "not_supported" @@ -549,7 +552,7 @@ async def test_autodetect_legacy(hass, remote): """Test for send key with autodetection of protocol.""" with patch("homeassistant.components.samsungtv.bridge.Remote") as remote: result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) assert result["type"] == "create_entry" assert result["data"][CONF_METHOD] == "legacy" @@ -567,7 +570,7 @@ async def test_autodetect_none(hass, remote, remotews): side_effect=OSError("Boom"), ) as remotews: result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) assert result["type"] == "abort" assert result["reason"] == "cannot_connect" diff --git a/tests/components/screenlogic/test_config_flow.py b/tests/components/screenlogic/test_config_flow.py index 71dc493500150..f64e35a28b6f4 100644 --- a/tests/components/screenlogic/test_config_flow.py +++ b/tests/components/screenlogic/test_config_flow.py @@ -137,7 +137,7 @@ async def test_dhcp(hass): await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": "dhcp"}, + context={"source": config_entries.SOURCE_DHCP}, data={ HOSTNAME: "Pentair: 01-01-01", IP_ADDRESS: "1.1.1.1", diff --git a/tests/components/sharkiq/test_config_flow.py b/tests/components/sharkiq/test_config_flow.py index d291d9f1bd11d..dc631f48a46ce 100644 --- a/tests/components/sharkiq/test_config_flow.py +++ b/tests/components/sharkiq/test_config_flow.py @@ -73,7 +73,9 @@ async def test_reauth_success(hass: HomeAssistant): mock_config.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "reauth", "unique_id": UNIQUE_ID}, data=CONFIG + DOMAIN, + context={"source": config_entries.SOURCE_REAUTH, "unique_id": UNIQUE_ID}, + data=CONFIG, ) assert result["type"] == "abort" @@ -99,7 +101,7 @@ async def test_reauth( with patch("sharkiqpy.AylaApi.async_sign_in", side_effect=side_effect): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": "reauth", "unique_id": UNIQUE_ID}, + context={"source": config_entries.SOURCE_REAUTH, "unique_id": UNIQUE_ID}, data=CONFIG, ) diff --git a/tests/components/simplisafe/test_config_flow.py b/tests/components/simplisafe/test_config_flow.py index 8f9d3a9897c33..a048e4b074581 100644 --- a/tests/components/simplisafe/test_config_flow.py +++ b/tests/components/simplisafe/test_config_flow.py @@ -9,7 +9,7 @@ from homeassistant import data_entry_flow from homeassistant.components.simplisafe import DOMAIN -from homeassistant.config_entries import SOURCE_USER +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME from tests.common import MockConfigEntry @@ -107,7 +107,7 @@ async def test_step_reauth(hass): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": "reauth"}, + context={"source": SOURCE_REAUTH}, data={CONF_CODE: "1234", CONF_USERNAME: "user@email.com"}, ) assert result["step_id"] == "reauth_confirm" diff --git a/tests/components/sma/conftest.py b/tests/components/sma/conftest.py index 7522aeedf1b0a..9ec9e1f5a11bf 100644 --- a/tests/components/sma/conftest.py +++ b/tests/components/sma/conftest.py @@ -3,6 +3,7 @@ import pytest +from homeassistant import config_entries from homeassistant.components.sma.const import DOMAIN from . import MOCK_CUSTOM_SETUP_DATA, MOCK_DEVICE @@ -18,7 +19,7 @@ def mock_config_entry(): title=MOCK_DEVICE["name"], unique_id=MOCK_DEVICE["serial"], data=MOCK_CUSTOM_SETUP_DATA, - source="import", + source=config_entries.SOURCE_IMPORT, ) diff --git a/tests/components/smartthings/test_config_flow.py b/tests/components/smartthings/test_config_flow.py index d8e0f6ed784ed..15e045338af4f 100644 --- a/tests/components/smartthings/test_config_flow.py +++ b/tests/components/smartthings/test_config_flow.py @@ -6,7 +6,7 @@ from pysmartthings import APIResponseError from pysmartthings.installedapp import format_install_url -from homeassistant import data_entry_flow +from homeassistant import config_entries, data_entry_flow from homeassistant.components.smartthings import smartapp from homeassistant.components.smartthings.const import ( CONF_APP_ID, @@ -31,7 +31,7 @@ async def test_import_shows_user_step(hass): """Test import source shows the user form.""" # Webhook confirmation shown result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "import"} + DOMAIN, context={"source": config_entries.SOURCE_IMPORT} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" @@ -56,7 +56,7 @@ async def test_entry_created(hass, app, app_oauth_client, location, smartthings_ # Webhook confirmation shown result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" @@ -127,7 +127,7 @@ async def test_entry_created_from_update_event( # Webhook confirmation shown result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" @@ -198,7 +198,7 @@ async def test_entry_created_existing_app_new_oauth_client( # Webhook confirmation shown result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" @@ -282,7 +282,7 @@ async def test_entry_created_existing_app_copies_oauth_client( # Webhook confirmation shown result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" @@ -372,7 +372,7 @@ async def test_entry_created_with_cloudhook( # Webhook confirmation shown result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" @@ -434,7 +434,7 @@ async def test_invalid_webhook_aborts(hass): {"external_url": "http://example.local:8123"}, ) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "invalid_webhook_url" @@ -450,7 +450,7 @@ async def test_invalid_token_shows_error(hass): # Webhook confirmation shown result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" @@ -487,7 +487,7 @@ async def test_unauthorized_token_shows_error(hass, smartthings_mock): # Webhook confirmation shown result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" @@ -524,7 +524,7 @@ async def test_forbidden_token_shows_error(hass, smartthings_mock): # Webhook confirmation shown result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" @@ -564,7 +564,7 @@ async def test_webhook_problem_shows_error(hass, smartthings_mock): # Webhook confirmation shown result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" @@ -603,7 +603,7 @@ async def test_api_error_shows_error(hass, smartthings_mock): # Webhook confirmation shown result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" @@ -641,7 +641,7 @@ async def test_unknown_response_error_shows_error(hass, smartthings_mock): # Webhook confirmation shown result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" @@ -675,7 +675,7 @@ async def test_unknown_error_shows_error(hass, smartthings_mock): # Webhook confirmation shown result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" @@ -717,7 +717,7 @@ async def test_no_available_locations_aborts( # Webhook confirmation shown result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" diff --git a/tests/components/smartthings/test_init.py b/tests/components/smartthings/test_init.py index eed1d5d26b1f9..e38c123829c45 100644 --- a/tests/components/smartthings/test_init.py +++ b/tests/components/smartthings/test_init.py @@ -6,6 +6,7 @@ from pysmartthings import InstalledAppStatus, OAuthToken import pytest +from homeassistant import config_entries from homeassistant.components import cloud, smartthings from homeassistant.components.smartthings.const import ( CONF_CLOUDHOOK_URL, @@ -41,7 +42,7 @@ async def test_migration_creates_new_flow(hass, smartthings_mock, config_entry): flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 assert flows[0]["handler"] == "smartthings" - assert flows[0]["context"] == {"source": "import"} + assert flows[0]["context"] == {"source": config_entries.SOURCE_IMPORT} async def test_unrecoverable_api_errors_create_new_flow( @@ -71,7 +72,7 @@ async def test_unrecoverable_api_errors_create_new_flow( flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 assert flows[0]["handler"] == "smartthings" - assert flows[0]["context"] == {"source": "import"} + assert flows[0]["context"] == {"source": config_entries.SOURCE_IMPORT} hass.config_entries.flow.async_abort(flows[0]["flow_id"]) diff --git a/tests/components/somfy_mylink/test_config_flow.py b/tests/components/somfy_mylink/test_config_flow.py index 980a01f318c09..59f6bd37407b7 100644 --- a/tests/components/somfy_mylink/test_config_flow.py +++ b/tests/components/somfy_mylink/test_config_flow.py @@ -465,7 +465,9 @@ async def test_already_configured_with_ignored(hass): """Test ignored entries do not break checking for existing entries.""" await setup.async_setup_component(hass, "persistent_notification", {}) - config_entry = MockConfigEntry(domain=DOMAIN, data={}, source="ignore") + config_entry = MockConfigEntry( + domain=DOMAIN, data={}, source=config_entries.SOURCE_IGNORE + ) config_entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( diff --git a/tests/components/speedtestdotnet/test_config_flow.py b/tests/components/speedtestdotnet/test_config_flow.py index dee271d94a3e8..a7a65511ee553 100644 --- a/tests/components/speedtestdotnet/test_config_flow.py +++ b/tests/components/speedtestdotnet/test_config_flow.py @@ -5,7 +5,7 @@ import pytest from speedtest import NoMatchedServers -from homeassistant import data_entry_flow +from homeassistant import config_entries, data_entry_flow from homeassistant.components import speedtestdotnet from homeassistant.components.speedtestdotnet.const import ( CONF_MANUAL, @@ -34,7 +34,7 @@ def mock_setup(): async def test_flow_works(hass, mock_setup): """Test user config.""" result = await hass.config_entries.flow.async_init( - speedtestdotnet.DOMAIN, context={"source": "user"} + speedtestdotnet.DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" @@ -53,7 +53,7 @@ async def test_import_fails(hass, mock_setup): mock_api.return_value.get_servers.side_effect = NoMatchedServers result = await hass.config_entries.flow.async_init( speedtestdotnet.DOMAIN, - context={"source": "import"}, + context={"source": config_entries.SOURCE_IMPORT}, data={ CONF_SERVER_ID: "223", CONF_MANUAL: True, @@ -71,7 +71,7 @@ async def test_import_success(hass, mock_setup): with patch("speedtest.Speedtest"): result = await hass.config_entries.flow.async_init( speedtestdotnet.DOMAIN, - context={"source": "import"}, + context={"source": config_entries.SOURCE_IMPORT}, data={ CONF_SERVER_ID: "1", CONF_MANUAL: True, @@ -132,7 +132,7 @@ async def test_integration_already_configured(hass): ) entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - speedtestdotnet.DOMAIN, context={"source": "user"} + speedtestdotnet.DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/spotify/test_config_flow.py b/tests/components/spotify/test_config_flow.py index 37a33ef66b2da..cd0be3f7cc8ac 100644 --- a/tests/components/spotify/test_config_flow.py +++ b/tests/components/spotify/test_config_flow.py @@ -5,7 +5,7 @@ from homeassistant import data_entry_flow, setup from homeassistant.components.spotify.const import DOMAIN -from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.helpers import config_entry_oauth2_flow @@ -181,7 +181,7 @@ async def test_reauthentication( old_entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "reauth"}, data=old_entry.data + DOMAIN, context={"source": SOURCE_REAUTH}, data=old_entry.data ) flows = hass.config_entries.flow.async_progress() @@ -246,7 +246,7 @@ async def test_reauth_account_mismatch( old_entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "reauth"}, data=old_entry.data + DOMAIN, context={"source": SOURCE_REAUTH}, data=old_entry.data ) flows = hass.config_entries.flow.async_progress() diff --git a/tests/components/srp_energy/__init__.py b/tests/components/srp_energy/__init__.py index 5e2a4695d0bab..e14f9186f44d5 100644 --- a/tests/components/srp_energy/__init__.py +++ b/tests/components/srp_energy/__init__.py @@ -23,7 +23,7 @@ async def init_integration( config=None, options=None, entry_id="1", - source="user", + source=config_entries.SOURCE_USER, side_effect=None, usage=None, ): diff --git a/tests/components/srp_energy/test_config_flow.py b/tests/components/srp_energy/test_config_flow.py index 5295d8cdb13b2..c8d458dfb82f8 100644 --- a/tests/components/srp_energy/test_config_flow.py +++ b/tests/components/srp_energy/test_config_flow.py @@ -11,7 +11,7 @@ async def test_form(hass): """Test user config.""" # First get the form result = await hass.config_entries.flow.async_init( - SRP_ENERGY_DOMAIN, context={"source": "user"} + SRP_ENERGY_DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" @@ -40,7 +40,7 @@ async def test_form(hass): async def test_form_invalid_auth(hass): """Test user config with invalid auth.""" result = await hass.config_entries.flow.async_init( - SRP_ENERGY_DOMAIN, context={"source": "user"} + SRP_ENERGY_DOMAIN, context={"source": config_entries.SOURCE_USER} ) with patch( @@ -58,7 +58,7 @@ async def test_form_invalid_auth(hass): async def test_form_value_error(hass): """Test user config that throws a value error.""" result = await hass.config_entries.flow.async_init( - SRP_ENERGY_DOMAIN, context={"source": "user"} + SRP_ENERGY_DOMAIN, context={"source": config_entries.SOURCE_USER} ) with patch( @@ -76,7 +76,7 @@ async def test_form_value_error(hass): async def test_form_unknown_exception(hass): """Test user config that throws an unknown exception.""" result = await hass.config_entries.flow.async_init( - SRP_ENERGY_DOMAIN, context={"source": "user"} + SRP_ENERGY_DOMAIN, context={"source": config_entries.SOURCE_USER} ) with patch( @@ -106,7 +106,7 @@ async def test_integration_already_configured(hass): """Test integration is already configured.""" await init_integration(hass) result = await hass.config_entries.flow.async_init( - SRP_ENERGY_DOMAIN, context={"source": "user"} + SRP_ENERGY_DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/ssdp/test_init.py b/tests/components/ssdp/test_init.py index f0f4a94e562bb..78b0f9e05b6c9 100644 --- a/tests/components/ssdp/test_init.py +++ b/tests/components/ssdp/test_init.py @@ -6,6 +6,7 @@ import aiohttp import pytest +from homeassistant import config_entries from homeassistant.components import ssdp from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP from homeassistant.setup import async_setup_component @@ -39,7 +40,9 @@ async def _mock_async_scan(*args, async_callback=None, **kwargs): assert len(mock_init.mock_calls) == 1 assert mock_init.mock_calls[0][1][0] == "mock-domain" - assert mock_init.mock_calls[0][2]["context"] == {"source": "ssdp"} + assert mock_init.mock_calls[0][2]["context"] == { + "source": config_entries.SOURCE_SSDP + } assert mock_init.mock_calls[0][2]["data"] == { ssdp.ATTR_SSDP_ST: "mock-st", ssdp.ATTR_SSDP_LOCATION: None, @@ -88,7 +91,9 @@ async def _mock_async_scan(*args, async_callback=None, **kwargs): assert len(aioclient_mock.mock_calls) == 1 assert len(mock_init.mock_calls) == 1 assert mock_init.mock_calls[0][1][0] == "mock-domain" - assert mock_init.mock_calls[0][2]["context"] == {"source": "ssdp"} + assert mock_init.mock_calls[0][2]["context"] == { + "source": config_entries.SOURCE_SSDP + } async def test_scan_not_all_present(hass, aioclient_mock): @@ -266,7 +271,9 @@ async def _mock_async_scan(*args, async_callback=None, **kwargs): assert len(mock_init.mock_calls) == 1 assert mock_init.mock_calls[0][1][0] == "mock-domain" - assert mock_init.mock_calls[0][2]["context"] == {"source": "ssdp"} + assert mock_init.mock_calls[0][2]["context"] == { + "source": config_entries.SOURCE_SSDP + } assert mock_init.mock_calls[0][2]["data"] == { "ssdp_location": "http://1.1.1.1", "ssdp_st": "mock-st", diff --git a/tests/components/starline/test_config_flow.py b/tests/components/starline/test_config_flow.py index 7050376dbfee6..4cd222dbc919f 100644 --- a/tests/components/starline/test_config_flow.py +++ b/tests/components/starline/test_config_flow.py @@ -1,6 +1,7 @@ """Tests for StarLine config flow.""" import requests_mock +from homeassistant import config_entries from homeassistant.components.starline import config_flow TEST_APP_ID = "666" @@ -42,7 +43,7 @@ async def test_flow_works(hass): ) result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": "user"} + config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == "form" assert result["step_id"] == "auth_app" @@ -76,7 +77,7 @@ async def test_step_auth_app_code_falls(hass): ) result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, - context={"source": "user"}, + context={"source": config_entries.SOURCE_USER}, data={ config_flow.CONF_APP_ID: TEST_APP_ID, config_flow.CONF_APP_SECRET: TEST_APP_SECRET, @@ -99,7 +100,7 @@ async def test_step_auth_app_token_falls(hass): ) result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, - context={"source": "user"}, + context={"source": config_entries.SOURCE_USER}, data={ config_flow.CONF_APP_ID: TEST_APP_ID, config_flow.CONF_APP_SECRET: TEST_APP_SECRET, diff --git a/tests/components/tado/test_config_flow.py b/tests/components/tado/test_config_flow.py index 90b7e87504c83..77656f1c81fd0 100644 --- a/tests/components/tado/test_config_flow.py +++ b/tests/components/tado/test_config_flow.py @@ -125,7 +125,7 @@ async def test_form_homekit(hass): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": "homekit"}, + context={"source": config_entries.SOURCE_HOMEKIT}, data={"properties": {"id": "AA:BB:CC:DD:EE:FF"}}, ) assert result["type"] == "form" @@ -144,7 +144,7 @@ async def test_form_homekit(hass): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": "homekit"}, + context={"source": config_entries.SOURCE_HOMEKIT}, data={"properties": {"id": "AA:BB:CC:DD:EE:FF"}}, ) assert result["type"] == "abort" diff --git a/tests/components/tasmota/test_config_flow.py b/tests/components/tasmota/test_config_flow.py index 469e5e298125a..9f199f0aa66a5 100644 --- a/tests/components/tasmota/test_config_flow.py +++ b/tests/components/tasmota/test_config_flow.py @@ -1,4 +1,5 @@ """Test config flow.""" +from homeassistant import config_entries from homeassistant.components.mqtt.models import Message from tests.common import MockConfigEntry @@ -9,7 +10,7 @@ async def test_mqtt_abort_if_existing_entry(hass, mqtt_mock): MockConfigEntry(domain="tasmota").add_to_hass(hass) result = await hass.config_entries.flow.async_init( - "tasmota", context={"source": "mqtt"} + "tasmota", context={"source": config_entries.SOURCE_MQTT} ) assert result["type"] == "abort" @@ -20,7 +21,7 @@ async def test_mqtt_abort_invalid_topic(hass, mqtt_mock): """Check MQTT flow aborts if discovery topic is invalid.""" discovery_info = Message("", "", 0, False, subscribed_topic="custom_prefix/##") result = await hass.config_entries.flow.async_init( - "tasmota", context={"source": "mqtt"}, data=discovery_info + "tasmota", context={"source": config_entries.SOURCE_MQTT}, data=discovery_info ) assert result["type"] == "abort" assert result["reason"] == "invalid_discovery_info" @@ -30,7 +31,7 @@ async def test_mqtt_setup(hass, mqtt_mock) -> None: """Test we can finish a config flow through MQTT with custom prefix.""" discovery_info = Message("", "", 0, False, subscribed_topic="custom_prefix/123/#") result = await hass.config_entries.flow.async_init( - "tasmota", context={"source": "mqtt"}, data=discovery_info + "tasmota", context={"source": config_entries.SOURCE_MQTT}, data=discovery_info ) assert result["type"] == "form" @@ -45,7 +46,7 @@ async def test_mqtt_setup(hass, mqtt_mock) -> None: async def test_user_setup(hass, mqtt_mock): """Test we can finish a config flow.""" result = await hass.config_entries.flow.async_init( - "tasmota", context={"source": "user"} + "tasmota", context={"source": config_entries.SOURCE_USER} ) assert result["type"] == "form" @@ -60,7 +61,8 @@ async def test_user_setup(hass, mqtt_mock): async def test_user_setup_advanced(hass, mqtt_mock): """Test we can finish a config flow.""" result = await hass.config_entries.flow.async_init( - "tasmota", context={"source": "user", "show_advanced_options": True} + "tasmota", + context={"source": config_entries.SOURCE_USER, "show_advanced_options": True}, ) assert result["type"] == "form" @@ -77,7 +79,8 @@ async def test_user_setup_advanced(hass, mqtt_mock): async def test_user_setup_advanced_strip_wildcard(hass, mqtt_mock): """Test we can finish a config flow.""" result = await hass.config_entries.flow.async_init( - "tasmota", context={"source": "user", "show_advanced_options": True} + "tasmota", + context={"source": config_entries.SOURCE_USER, "show_advanced_options": True}, ) assert result["type"] == "form" @@ -94,7 +97,8 @@ async def test_user_setup_advanced_strip_wildcard(hass, mqtt_mock): async def test_user_setup_invalid_topic_prefix(hass, mqtt_mock): """Test abort on invalid discovery topic.""" result = await hass.config_entries.flow.async_init( - "tasmota", context={"source": "user", "show_advanced_options": True} + "tasmota", + context={"source": config_entries.SOURCE_USER, "show_advanced_options": True}, ) assert result["type"] == "form" @@ -111,7 +115,7 @@ async def test_user_single_instance(hass, mqtt_mock): MockConfigEntry(domain="tasmota").add_to_hass(hass) result = await hass.config_entries.flow.async_init( - "tasmota", context={"source": "user"} + "tasmota", context={"source": config_entries.SOURCE_USER} ) assert result["type"] == "abort" assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/tibber/test_config_flow.py b/tests/components/tibber/test_config_flow.py index 479f314123aad..6eaa52ac10344 100644 --- a/tests/components/tibber/test_config_flow.py +++ b/tests/components/tibber/test_config_flow.py @@ -3,6 +3,7 @@ import pytest +from homeassistant import config_entries from homeassistant.components.tibber.const import DOMAIN from homeassistant.const import CONF_ACCESS_TOKEN @@ -19,7 +20,7 @@ def tibber_setup_fixture(): async def test_show_config_form(hass): """Test show configuration form.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == "form" @@ -42,7 +43,7 @@ async def test_create_entry(hass): with patch("tibber.Tibber", return_value=tibber_mock): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"}, data=test_data + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=test_data ) assert result["type"] == "create_entry" @@ -65,7 +66,7 @@ async def test_flow_entry_already_exists(hass): with patch("tibber.Tibber.update_info", return_value=None): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"}, data=test_data + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=test_data ) assert result["type"] == "abort" diff --git a/tests/components/totalconnect/test_config_flow.py b/tests/components/totalconnect/test_config_flow.py index 5d1723a835e3a..2f89beab0e084 100644 --- a/tests/components/totalconnect/test_config_flow.py +++ b/tests/components/totalconnect/test_config_flow.py @@ -3,7 +3,7 @@ from homeassistant import data_entry_flow from homeassistant.components.totalconnect.const import CONF_LOCATION, DOMAIN -from homeassistant.config_entries import SOURCE_USER +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_PASSWORD from .common import ( @@ -133,7 +133,7 @@ async def test_reauth(hass): entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "reauth"}, data=entry.data + DOMAIN, context={"source": SOURCE_REAUTH}, data=entry.data ) assert result["step_id"] == "reauth_confirm" diff --git a/tests/components/traccar/test_init.py b/tests/components/traccar/test_init.py index f372358ea1db9..5e995e10e92b3 100644 --- a/tests/components/traccar/test_init.py +++ b/tests/components/traccar/test_init.py @@ -3,7 +3,7 @@ import pytest -from homeassistant import data_entry_flow +from homeassistant import config_entries, data_entry_flow from homeassistant.components import traccar, zone from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN from homeassistant.components.traccar import DOMAIN, TRACKER_UPDATE @@ -66,7 +66,7 @@ async def webhook_id_fixture(hass, client): {"external_url": "http://example.com"}, ) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM, result diff --git a/tests/components/tradfri/test_config_flow.py b/tests/components/tradfri/test_config_flow.py index a155e8b383c40..ca6380a931048 100644 --- a/tests/components/tradfri/test_config_flow.py +++ b/tests/components/tradfri/test_config_flow.py @@ -3,7 +3,7 @@ import pytest -from homeassistant import data_entry_flow +from homeassistant import config_entries, data_entry_flow from homeassistant.components.tradfri import config_flow from tests.common import MockConfigEntry @@ -23,7 +23,7 @@ async def test_user_connection_successful(hass, mock_auth, mock_entry_setup): mock_auth.side_effect = lambda hass, host, code: {"host": host, "gateway_id": "bla"} flow = await hass.config_entries.flow.async_init( - "tradfri", context={"source": "user"} + "tradfri", context={"source": config_entries.SOURCE_USER} ) result = await hass.config_entries.flow.async_configure( @@ -45,7 +45,7 @@ async def test_user_connection_timeout(hass, mock_auth, mock_entry_setup): mock_auth.side_effect = config_flow.AuthError("timeout") flow = await hass.config_entries.flow.async_init( - "tradfri", context={"source": "user"} + "tradfri", context={"source": config_entries.SOURCE_USER} ) result = await hass.config_entries.flow.async_configure( @@ -63,7 +63,7 @@ async def test_user_connection_bad_key(hass, mock_auth, mock_entry_setup): mock_auth.side_effect = config_flow.AuthError("invalid_security_code") flow = await hass.config_entries.flow.async_init( - "tradfri", context={"source": "user"} + "tradfri", context={"source": config_entries.SOURCE_USER} ) result = await hass.config_entries.flow.async_configure( @@ -82,7 +82,7 @@ async def test_discovery_connection(hass, mock_auth, mock_entry_setup): flow = await hass.config_entries.flow.async_init( "tradfri", - context={"source": "homekit"}, + context={"source": config_entries.SOURCE_HOMEKIT}, data={"host": "123.123.123.123", "properties": {"id": "homekit-id"}}, ) @@ -112,7 +112,7 @@ async def test_import_connection(hass, mock_auth, mock_entry_setup): flow = await hass.config_entries.flow.async_init( "tradfri", - context={"source": "import"}, + context={"source": config_entries.SOURCE_IMPORT}, data={"host": "123.123.123.123", "import_groups": True}, ) @@ -143,7 +143,7 @@ async def test_import_connection_no_groups(hass, mock_auth, mock_entry_setup): flow = await hass.config_entries.flow.async_init( "tradfri", - context={"source": "import"}, + context={"source": config_entries.SOURCE_IMPORT}, data={"host": "123.123.123.123", "import_groups": False}, ) @@ -174,7 +174,7 @@ async def test_import_connection_legacy(hass, mock_gateway_info, mock_entry_setu result = await hass.config_entries.flow.async_init( "tradfri", - context={"source": "import"}, + context={"source": config_entries.SOURCE_IMPORT}, data={"host": "123.123.123.123", "key": "mock-key", "import_groups": True}, ) @@ -204,7 +204,7 @@ async def test_import_connection_legacy_no_groups( result = await hass.config_entries.flow.async_init( "tradfri", - context={"source": "import"}, + context={"source": config_entries.SOURCE_IMPORT}, data={"host": "123.123.123.123", "key": "mock-key", "import_groups": False}, ) @@ -230,7 +230,7 @@ async def test_discovery_duplicate_aborted(hass): flow = await hass.config_entries.flow.async_init( "tradfri", - context={"source": "homekit"}, + context={"source": config_entries.SOURCE_HOMEKIT}, data={"host": "new-host", "properties": {"id": "homekit-id"}}, ) @@ -245,7 +245,9 @@ async def test_import_duplicate_aborted(hass): MockConfigEntry(domain="tradfri", data={"host": "some-host"}).add_to_hass(hass) flow = await hass.config_entries.flow.async_init( - "tradfri", context={"source": "import"}, data={"host": "some-host"} + "tradfri", + context={"source": config_entries.SOURCE_IMPORT}, + data={"host": "some-host"}, ) assert flow["type"] == data_entry_flow.RESULT_TYPE_ABORT @@ -256,7 +258,7 @@ async def test_duplicate_discovery(hass, mock_auth, mock_entry_setup): """Test a duplicate discovery in progress is ignored.""" result = await hass.config_entries.flow.async_init( "tradfri", - context={"source": "homekit"}, + context={"source": config_entries.SOURCE_HOMEKIT}, data={"host": "123.123.123.123", "properties": {"id": "homekit-id"}}, ) @@ -264,7 +266,7 @@ async def test_duplicate_discovery(hass, mock_auth, mock_entry_setup): result2 = await hass.config_entries.flow.async_init( "tradfri", - context={"source": "homekit"}, + context={"source": config_entries.SOURCE_HOMEKIT}, data={"host": "123.123.123.123", "properties": {"id": "homekit-id"}}, ) @@ -281,7 +283,7 @@ async def test_discovery_updates_unique_id(hass): flow = await hass.config_entries.flow.async_init( "tradfri", - context={"source": "homekit"}, + context={"source": config_entries.SOURCE_HOMEKIT}, data={"host": "some-host", "properties": {"id": "homekit-id"}}, ) diff --git a/tests/components/tradfri/test_init.py b/tests/components/tradfri/test_init.py index da9ae9da146c2..8e11ab06f34da 100644 --- a/tests/components/tradfri/test_init.py +++ b/tests/components/tradfri/test_init.py @@ -1,6 +1,7 @@ """Tests for Tradfri setup.""" from unittest.mock import patch +from homeassistant import config_entries from homeassistant.components import tradfri from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component @@ -34,7 +35,7 @@ async def test_config_yaml_host_imported(hass): progress = hass.config_entries.flow.async_progress() assert len(progress) == 1 assert progress[0]["handler"] == "tradfri" - assert progress[0]["context"] == {"source": "import"} + assert progress[0]["context"] == {"source": config_entries.SOURCE_IMPORT} async def test_config_json_host_not_imported(hass): @@ -71,7 +72,7 @@ async def test_config_json_host_imported( config_entry = mock_entry_setup.mock_calls[0][1][1] assert config_entry.domain == "tradfri" - assert config_entry.source == "import" + assert config_entry.source == config_entries.SOURCE_IMPORT assert config_entry.title == "mock-host" diff --git a/tests/components/transmission/test_config_flow.py b/tests/components/transmission/test_config_flow.py index 2982d363da9e3..79b341e45049f 100644 --- a/tests/components/transmission/test_config_flow.py +++ b/tests/components/transmission/test_config_flow.py @@ -5,7 +5,7 @@ import pytest from transmissionrpc.error import TransmissionError -from homeassistant import data_entry_flow +from homeassistant import config_entries, data_entry_flow from homeassistant.components import transmission from homeassistant.components.transmission import config_flow from homeassistant.components.transmission.const import ( @@ -96,7 +96,7 @@ def init_config_flow(hass): async def test_flow_user_config(hass, api): """Test user config.""" result = await hass.config_entries.flow.async_init( - transmission.DOMAIN, context={"source": "user"} + transmission.DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" @@ -106,7 +106,7 @@ async def test_flow_required_fields(hass, api): """Test with required fields only.""" result = await hass.config_entries.flow.async_init( transmission.DOMAIN, - context={"source": "user"}, + context={"source": config_entries.SOURCE_USER}, data={CONF_NAME: NAME, CONF_HOST: HOST, CONF_PORT: PORT}, ) @@ -120,7 +120,9 @@ async def test_flow_required_fields(hass, api): async def test_flow_all_provided(hass, api): """Test with all provided.""" result = await hass.config_entries.flow.async_init( - transmission.DOMAIN, context={"source": "user"}, data=MOCK_ENTRY + transmission.DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=MOCK_ENTRY, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -208,7 +210,9 @@ async def test_host_already_configured(hass, api): mock_entry_unique_name = MOCK_ENTRY.copy() mock_entry_unique_name[CONF_NAME] = "Transmission 1" result = await hass.config_entries.flow.async_init( - transmission.DOMAIN, context={"source": "user"}, data=mock_entry_unique_name + transmission.DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=mock_entry_unique_name, ) assert result["type"] == "abort" assert result["reason"] == "already_configured" @@ -217,7 +221,9 @@ async def test_host_already_configured(hass, api): mock_entry_unique_port[CONF_PORT] = 9092 mock_entry_unique_port[CONF_NAME] = "Transmission 2" result = await hass.config_entries.flow.async_init( - transmission.DOMAIN, context={"source": "user"}, data=mock_entry_unique_port + transmission.DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=mock_entry_unique_port, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -225,7 +231,9 @@ async def test_host_already_configured(hass, api): mock_entry_unique_host[CONF_HOST] = "192.168.1.101" mock_entry_unique_host[CONF_NAME] = "Transmission 3" result = await hass.config_entries.flow.async_init( - transmission.DOMAIN, context={"source": "user"}, data=mock_entry_unique_host + transmission.DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=mock_entry_unique_host, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -242,7 +250,9 @@ async def test_name_already_configured(hass, api): mock_entry = MOCK_ENTRY.copy() mock_entry[CONF_HOST] = "0.0.0.0" result = await hass.config_entries.flow.async_init( - transmission.DOMAIN, context={"source": "user"}, data=mock_entry + transmission.DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=mock_entry, ) assert result["type"] == "form" diff --git a/tests/components/twilio/test_init.py b/tests/components/twilio/test_init.py index 580e5f83ebf85..3529159eae1db 100644 --- a/tests/components/twilio/test_init.py +++ b/tests/components/twilio/test_init.py @@ -1,5 +1,5 @@ """Test the init file of Twilio.""" -from homeassistant import data_entry_flow +from homeassistant import config_entries, data_entry_flow from homeassistant.components import twilio from homeassistant.config import async_process_ha_core_config from homeassistant.core import callback @@ -12,7 +12,7 @@ async def test_config_flow_registers_webhook(hass, aiohttp_client): {"internal_url": "http://example.local:8123"}, ) result = await hass.config_entries.flow.async_init( - "twilio", context={"source": "user"} + "twilio", context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM, result diff --git a/tests/components/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py index 43d14981bbb65..1967369e22b68 100644 --- a/tests/components/unifi/test_config_flow.py +++ b/tests/components/unifi/test_config_flow.py @@ -91,7 +91,7 @@ async def test_flow_works(hass, aioclient_mock, mock_discovery): """Test config flow.""" mock_discovery.return_value = "1" result = await hass.config_entries.flow.async_init( - UNIFI_DOMAIN, context={"source": "user"} + UNIFI_DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -157,7 +157,7 @@ async def test_flow_works(hass, aioclient_mock, mock_discovery): async def test_flow_works_negative_discovery(hass, aioclient_mock, mock_discovery): """Test config flow with a negative outcome of async_discovery_unifi.""" result = await hass.config_entries.flow.async_init( - UNIFI_DOMAIN, context={"source": "user"} + UNIFI_DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -174,7 +174,7 @@ async def test_flow_works_negative_discovery(hass, aioclient_mock, mock_discover async def test_flow_multiple_sites(hass, aioclient_mock): """Test config flow works when finding multiple sites.""" result = await hass.config_entries.flow.async_init( - UNIFI_DOMAIN, context={"source": "user"} + UNIFI_DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -222,7 +222,7 @@ async def test_flow_raise_already_configured(hass, aioclient_mock): await setup_unifi_integration(hass, aioclient_mock) result = await hass.config_entries.flow.async_init( - UNIFI_DOMAIN, context={"source": "user"} + UNIFI_DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -277,7 +277,7 @@ async def test_flow_aborts_configuration_updated(hass, aioclient_mock): entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - UNIFI_DOMAIN, context={"source": "user"} + UNIFI_DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -321,7 +321,7 @@ async def test_flow_aborts_configuration_updated(hass, aioclient_mock): async def test_flow_fails_user_credentials_faulty(hass, aioclient_mock): """Test config flow.""" result = await hass.config_entries.flow.async_init( - UNIFI_DOMAIN, context={"source": "user"} + UNIFI_DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -348,7 +348,7 @@ async def test_flow_fails_user_credentials_faulty(hass, aioclient_mock): async def test_flow_fails_controller_unavailable(hass, aioclient_mock): """Test config flow.""" result = await hass.config_entries.flow.async_init( - UNIFI_DOMAIN, context={"source": "user"} + UNIFI_DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM diff --git a/tests/components/volumio/test_config_flow.py b/tests/components/volumio/test_config_flow.py index a0d3fc85ee34b..fed967f9ffc7e 100644 --- a/tests/components/volumio/test_config_flow.py +++ b/tests/components/volumio/test_config_flow.py @@ -170,7 +170,7 @@ async def test_discovery(hass): """Test discovery flow works.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "zeroconf"}, data=TEST_DISCOVERY + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=TEST_DISCOVERY ) with patch( @@ -200,7 +200,7 @@ async def test_discovery_cannot_connect(hass): """Test discovery aborts if cannot connect.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "zeroconf"}, data=TEST_DISCOVERY + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=TEST_DISCOVERY ) with patch( @@ -219,13 +219,13 @@ async def test_discovery_cannot_connect(hass): async def test_discovery_duplicate_data(hass): """Test discovery aborts if same mDNS packet arrives.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "zeroconf"}, data=TEST_DISCOVERY + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=TEST_DISCOVERY ) assert result["type"] == "form" assert result["step_id"] == "discovery_confirm" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "zeroconf"}, data=TEST_DISCOVERY + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=TEST_DISCOVERY ) assert result["type"] == "abort" assert result["reason"] == "already_in_progress" @@ -252,7 +252,9 @@ async def test_discovery_updates_unique_id(hass): return_value=True, ) as mock_setup_entry: result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "zeroconf"}, data=TEST_DISCOVERY + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=TEST_DISCOVERY, ) await hass.async_block_till_done() diff --git a/tests/components/withings/test_config_flow.py b/tests/components/withings/test_config_flow.py index 4cbada948f417..618dd19f80b3c 100644 --- a/tests/components/withings/test_config_flow.py +++ b/tests/components/withings/test_config_flow.py @@ -1,6 +1,7 @@ """Tests for config flow.""" from aiohttp.test_utils import TestClient +from homeassistant import config_entries from homeassistant.components.withings import const from homeassistant.config import async_process_ha_core_config from homeassistant.const import ( @@ -58,7 +59,8 @@ async def test_config_reauth_profile( config_entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - const.DOMAIN, context={"source": "reauth", "profile": "person0"} + const.DOMAIN, + context={"source": config_entries.SOURCE_REAUTH, "profile": "person0"}, ) assert result assert result["type"] == "form" diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index 127c5518a41bb..16747980b15e7 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -10,7 +10,7 @@ from homeassistant import setup from homeassistant.components.zha import config_flow from homeassistant.components.zha.core.const import CONF_RADIO_TYPE, DOMAIN, RadioType -from homeassistant.config_entries import SOURCE_USER +from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_SOURCE from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM @@ -39,7 +39,7 @@ async def test_discovery(detect_mock, hass): "properties": {"name": "tube_123456"}, } flow = await hass.config_entries.flow.async_init( - "zha", context={"source": "zeroconf"}, data=service_info + "zha", context={"source": SOURCE_ZEROCONF}, data=service_info ) result = await hass.config_entries.flow.async_configure( flow["flow_id"], user_input={} @@ -71,7 +71,7 @@ async def test_discovery_already_setup(detect_mock, hass): MockConfigEntry(domain=DOMAIN, data={"usb_path": "/dev/ttyUSB1"}).add_to_hass(hass) result = await hass.config_entries.flow.async_init( - "zha", context={"source": "zeroconf"}, data=service_info + "zha", context={"source": SOURCE_ZEROCONF}, data=service_info ) await hass.async_block_till_done() diff --git a/tests/components/zwave/test_websocket_api.py b/tests/components/zwave/test_websocket_api.py index 9727906709ffe..2e37ed47fce42 100644 --- a/tests/components/zwave/test_websocket_api.py +++ b/tests/components/zwave/test_websocket_api.py @@ -1,6 +1,7 @@ """Test Z-Wave Websocket API.""" from unittest.mock import call, patch +from homeassistant import config_entries from homeassistant.bootstrap import async_setup_component from homeassistant.components.zwave.const import ( CONF_AUTOHEAL, @@ -83,6 +84,6 @@ async def test_zwave_ozw_migration_api(hass, mock_openzwave, hass_ws_client): assert result["flow_id"] == "mock_flow_id" assert async_init.call_args == call( "ozw", - context={"source": "import"}, + context={"source": config_entries.SOURCE_IMPORT}, data={"usb_path": "/dev/zwave", "network_key": NETWORK_KEY}, ) diff --git a/tests/helpers/test_config_entry_flow.py b/tests/helpers/test_config_entry_flow.py index b3233556957bf..edee75fe2407f 100644 --- a/tests/helpers/test_config_entry_flow.py +++ b/tests/helpers/test_config_entry_flow.py @@ -91,7 +91,16 @@ async def test_user_has_confirmation(hass, discovery_flow_conf): assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY -@pytest.mark.parametrize("source", ["discovery", "mqtt", "ssdp", "zeroconf", "dhcp"]) +@pytest.mark.parametrize( + "source", + [ + config_entries.SOURCE_DISCOVERY, + config_entries.SOURCE_MQTT, + config_entries.SOURCE_SSDP, + config_entries.SOURCE_ZEROCONF, + config_entries.SOURCE_DHCP, + ], +) async def test_discovery_single_instance(hass, discovery_flow_conf, source): """Test we not allow duplicates.""" flow = config_entries.HANDLERS["test"]() @@ -105,7 +114,16 @@ async def test_discovery_single_instance(hass, discovery_flow_conf, source): assert result["reason"] == "single_instance_allowed" -@pytest.mark.parametrize("source", ["discovery", "mqtt", "ssdp", "zeroconf", "dhcp"]) +@pytest.mark.parametrize( + "source", + [ + config_entries.SOURCE_DISCOVERY, + config_entries.SOURCE_MQTT, + config_entries.SOURCE_SSDP, + config_entries.SOURCE_ZEROCONF, + config_entries.SOURCE_DHCP, + ], +) async def test_discovery_confirmation(hass, discovery_flow_conf, source): """Test we ask for confirmation via discovery.""" flow = config_entries.HANDLERS["test"]() diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 4de62cc0cfc0b..741953f552be8 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -2068,7 +2068,7 @@ async def async_step_unignore(self, user_input): # But after a 'tick' the unignore step has run and we can see a config entry. await hass.async_block_till_done() entry = hass.config_entries.async_entries("comp")[0] - assert entry.source == "unignore" + assert entry.source == config_entries.SOURCE_UNIGNORE assert entry.unique_id == "mock-unique-id" assert entry.title == "yo" diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index 47b217936561b..34b07a2a871fd 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -5,7 +5,7 @@ import pytest import voluptuous as vol -from homeassistant import data_entry_flow +from homeassistant import config_entries, data_entry_flow from homeassistant.util.decorator import Registry from tests.common import async_capture_events @@ -182,7 +182,9 @@ async def async_step_init(self, info): data = {"id": "hello", "token": "secret"} - await manager.async_init("test", context={"source": "discovery"}, data=data) + await manager.async_init( + "test", context={"source": config_entries.SOURCE_DISCOVERY}, data=data + ) assert len(manager.async_progress()) == 0 assert len(manager.mock_created_entries) == 1 @@ -191,7 +193,7 @@ async def async_step_init(self, info): assert entry["handler"] == "test" assert entry["title"] == "hello" assert entry["data"] == data - assert entry["source"] == "discovery" + assert entry["source"] == config_entries.SOURCE_DISCOVERY async def test_finish_callback_change_result_type(hass): From e2837f08e82678eb4fe73d91eb76cebca4431bed Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 24 Apr 2021 23:32:34 -1000 Subject: [PATCH 0504/1317] Small cleanups for august (#49493) --- .../components/august/binary_sensor.py | 13 ++- homeassistant/components/august/camera.py | 18 ++-- homeassistant/components/august/lock.py | 13 +-- homeassistant/components/august/sensor.py | 10 +- tests/components/august/test_binary_sensor.py | 95 +++++++++++++++++++ 5 files changed, 123 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/august/binary_sensor.py b/homeassistant/components/august/binary_sensor.py index bb2bcda39e6c8..e72d4b186a5fe 100644 --- a/homeassistant/components/august/binary_sensor.py +++ b/homeassistant/components/august/binary_sensor.py @@ -2,7 +2,7 @@ from datetime import datetime, timedelta import logging -from yalexs.activity import ACTION_DOORBELL_CALL_MISSED, ActivityType +from yalexs.activity import ACTION_DOORBELL_CALL_MISSED, SOURCE_PUBNUB, ActivityType from yalexs.lock import LockDoorStatus from yalexs.util import update_lock_detail_from_activity @@ -97,7 +97,7 @@ def _native_datetime(): async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the August binary sensors.""" data = hass.data[DOMAIN][config_entry.entry_id][DATA_AUGUST] - devices = [] + entities = [] for door in data.locks: detail = data.get_device_detail(door.device_id) @@ -109,7 +109,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): continue _LOGGER.debug("Adding sensor class door for %s", door.device_name) - devices.append(AugustDoorBinarySensor(data, "door_open", door)) + entities.append(AugustDoorBinarySensor(data, "door_open", door)) for doorbell in data.doorbells: for sensor_type in SENSOR_TYPES_DOORBELL: @@ -118,9 +118,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): SENSOR_TYPES_DOORBELL[sensor_type][SENSOR_DEVICE_CLASS], doorbell.device_name, ) - devices.append(AugustDoorbellBinarySensor(data, sensor_type, doorbell)) + entities.append(AugustDoorbellBinarySensor(data, sensor_type, doorbell)) - async_add_entities(devices, True) + async_add_entities(entities) class AugustDoorBinarySensor(AugustEntityMixin, BinarySensorEntity): @@ -163,6 +163,9 @@ def _update_from_data(self): if door_activity is not None: update_lock_detail_from_activity(self._detail, door_activity) + # If the source is pubnub the lock must be online since its a live update + if door_activity.source == SOURCE_PUBNUB: + self._detail.set_online(True) bridge_activity = self._data.activity_stream.get_latest_device_activity( self._device_id, {ActivityType.BRIDGE_OPERATION} diff --git a/homeassistant/components/august/camera.py b/homeassistant/components/august/camera.py index e002e0b25176d..daaa7624aa3e2 100644 --- a/homeassistant/components/august/camera.py +++ b/homeassistant/components/august/camera.py @@ -14,23 +14,25 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up August cameras.""" data = hass.data[DOMAIN][config_entry.entry_id][DATA_AUGUST] - devices = [] - - for doorbell in data.doorbells: - devices.append(AugustCamera(data, doorbell, DEFAULT_TIMEOUT)) - - async_add_entities(devices, True) + session = aiohttp_client.async_get_clientsession(hass) + async_add_entities( + [ + AugustCamera(data, doorbell, session, DEFAULT_TIMEOUT) + for doorbell in data.doorbells + ] + ) class AugustCamera(AugustEntityMixin, Camera): """An implementation of a August security camera.""" - def __init__(self, data, device, timeout): + def __init__(self, data, device, session, timeout): """Initialize a August security camera.""" super().__init__(data, device) self._data = data self._device = device self._timeout = timeout + self._session = session self._image_url = None self._image_content = None @@ -76,7 +78,7 @@ async def async_camera_image(self): if self._image_url is not self._detail.image_url: self._image_url = self._detail.image_url self._image_content = await self._detail.async_get_doorbell_image( - aiohttp_client.async_get_clientsession(self.hass), timeout=self._timeout + self._session, timeout=self._timeout ) return self._image_content diff --git a/homeassistant/components/august/lock.py b/homeassistant/components/august/lock.py index 59c97190d7fe1..6e4ee7e6f5c92 100644 --- a/homeassistant/components/august/lock.py +++ b/homeassistant/components/august/lock.py @@ -1,7 +1,7 @@ """Support for August lock.""" import logging -from yalexs.activity import ActivityType +from yalexs.activity import SOURCE_PUBNUB, ActivityType from yalexs.lock import LockStatus from yalexs.util import update_lock_detail_from_activity @@ -19,13 +19,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up August locks.""" data = hass.data[DOMAIN][config_entry.entry_id][DATA_AUGUST] - devices = [] - - for lock in data.locks: - _LOGGER.debug("Adding lock for %s", lock.device_name) - devices.append(AugustLock(data, lock)) - - async_add_entities(devices, True) + async_add_entities([AugustLock(data, lock) for lock in data.locks]) class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity): @@ -80,6 +74,9 @@ def _update_from_data(self): if lock_activity is not None: self._changed_by = lock_activity.operated_by update_lock_detail_from_activity(self._detail, lock_activity) + # If the source is pubnub the lock must be online since its a live update + if lock_activity.source == SOURCE_PUBNUB: + self._detail.set_online(True) bridge_activity = self._data.activity_stream.get_latest_device_activity( self._device_id, {ActivityType.BRIDGE_OPERATION} diff --git a/homeassistant/components/august/sensor.py b/homeassistant/components/august/sensor.py index 44597a6485ead..1d973a83fc3a4 100644 --- a/homeassistant/components/august/sensor.py +++ b/homeassistant/components/august/sensor.py @@ -45,7 +45,7 @@ def _retrieve_linked_keypad_battery_state(detail): async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the August sensors.""" data = hass.data[DOMAIN][config_entry.entry_id][DATA_AUGUST] - devices = [] + entities = [] migrate_unique_id_devices = [] operation_sensors = [] batteries = { @@ -72,7 +72,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): "Adding battery sensor for %s", device.device_name, ) - devices.append(AugustBatterySensor(data, "device_battery", device, device)) + entities.append(AugustBatterySensor(data, "device_battery", device, device)) for device in batteries["linked_keypad_battery"]: detail = data.get_device_detail(device.device_id) @@ -90,15 +90,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities): keypad_battery_sensor = AugustBatterySensor( data, "linked_keypad_battery", detail.keypad, device ) - devices.append(keypad_battery_sensor) + entities.append(keypad_battery_sensor) migrate_unique_id_devices.append(keypad_battery_sensor) for device in operation_sensors: - devices.append(AugustOperatorSensor(data, device)) + entities.append(AugustOperatorSensor(data, device)) await _async_migrate_old_unique_ids(hass, migrate_unique_id_devices) - async_add_entities(devices, True) + async_add_entities(entities) async def _async_migrate_old_unique_ids(hass, devices): diff --git a/tests/components/august/test_binary_sensor.py b/tests/components/august/test_binary_sensor.py index 0912b05bec11f..26c824e584286 100644 --- a/tests/components/august/test_binary_sensor.py +++ b/tests/components/august/test_binary_sensor.py @@ -21,6 +21,7 @@ _create_august_with_devices, _mock_activities_from_fixture, _mock_doorbell_from_fixture, + _mock_doorsense_enabled_august_lock_detail, _mock_lock_from_fixture, ) @@ -251,3 +252,97 @@ async def test_doorbell_device_registry(hass): assert reg_device.name == "tmt100 Name" assert reg_device.manufacturer == "August Home Inc." assert reg_device.sw_version == "3.1.0-HYDRC75+201909251139" + + +async def test_door_sense_update_via_pubnub(hass): + """Test creation of a lock with doorsense and bridge.""" + lock_one = await _mock_doorsense_enabled_august_lock_detail(hass) + assert lock_one.pubsub_channel == "pubsub" + pubnub = AugustPubNub() + + activities = await _mock_activities_from_fixture(hass, "get_activity.lock.json") + config_entry = await _create_august_with_devices( + hass, [lock_one], activities=activities, pubnub=pubnub + ) + + binary_sensor_online_with_doorsense_name = hass.states.get( + "binary_sensor.online_with_doorsense_name_open" + ) + assert binary_sensor_online_with_doorsense_name.state == STATE_ON + + pubnub.message( + pubnub, + Mock( + channel=lock_one.pubsub_channel, + timetoken=dt_util.utcnow().timestamp() * 10000000, + message={"status": "kAugLockState_Unlocking", "doorState": "closed"}, + ), + ) + + await hass.async_block_till_done() + binary_sensor_online_with_doorsense_name = hass.states.get( + "binary_sensor.online_with_doorsense_name_open" + ) + assert binary_sensor_online_with_doorsense_name.state == STATE_OFF + + pubnub.message( + pubnub, + Mock( + channel=lock_one.pubsub_channel, + timetoken=dt_util.utcnow().timestamp() * 10000000, + message={"status": "kAugLockState_Locking", "doorState": "open"}, + ), + ) + + await hass.async_block_till_done() + binary_sensor_online_with_doorsense_name = hass.states.get( + "binary_sensor.online_with_doorsense_name_open" + ) + assert binary_sensor_online_with_doorsense_name.state == STATE_ON + + async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(seconds=30)) + await hass.async_block_till_done() + binary_sensor_online_with_doorsense_name = hass.states.get( + "binary_sensor.online_with_doorsense_name_open" + ) + assert binary_sensor_online_with_doorsense_name.state == STATE_ON + + pubnub.connected = True + async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(seconds=30)) + await hass.async_block_till_done() + binary_sensor_online_with_doorsense_name = hass.states.get( + "binary_sensor.online_with_doorsense_name_open" + ) + assert binary_sensor_online_with_doorsense_name.state == STATE_ON + + # Ensure pubnub status is always preserved + async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(hours=2)) + await hass.async_block_till_done() + binary_sensor_online_with_doorsense_name = hass.states.get( + "binary_sensor.online_with_doorsense_name_open" + ) + assert binary_sensor_online_with_doorsense_name.state == STATE_ON + + pubnub.message( + pubnub, + Mock( + channel=lock_one.pubsub_channel, + timetoken=dt_util.utcnow().timestamp() * 10000000, + message={"status": "kAugLockState_Unlocking", "doorState": "open"}, + ), + ) + await hass.async_block_till_done() + binary_sensor_online_with_doorsense_name = hass.states.get( + "binary_sensor.online_with_doorsense_name_open" + ) + assert binary_sensor_online_with_doorsense_name.state == STATE_ON + + async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(hours=4)) + await hass.async_block_till_done() + binary_sensor_online_with_doorsense_name = hass.states.get( + "binary_sensor.online_with_doorsense_name_open" + ) + assert binary_sensor_online_with_doorsense_name.state == STATE_ON + + await hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done() From 3e3cd0981db9c6b9709445a763df63db84bbef73 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sun, 25 Apr 2021 11:50:08 +0200 Subject: [PATCH 0505/1317] Reduce hue gamut warning to debug (#49624) --- homeassistant/components/hue/light.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/hue/light.py b/homeassistant/components/hue/light.py index 3d1937340059f..e139f5a0c950e 100644 --- a/homeassistant/components/hue/light.py +++ b/homeassistant/components/hue/light.py @@ -299,7 +299,7 @@ def __init__(self, coordinator, bridge, is_group, light, supported_features, roo _LOGGER.warning(err, self.name) if self.gamut and not color.check_valid_gamut(self.gamut): err = "Color gamut of %s: %s, not valid, setting gamut to None." - _LOGGER.warning(err, self.name, str(self.gamut)) + _LOGGER.debug(err, self.name, str(self.gamut)) self.gamut_typ = GAMUT_TYPE_UNAVAILABLE self.gamut = None From 376b787e4dfb14d8418b29ffe172a255b60a3d93 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 25 Apr 2021 00:05:49 -1000 Subject: [PATCH 0506/1317] Skip recorder commit if there is nothing to do (#49614) --- homeassistant/components/recorder/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index db20c72c81edc..9e9592f868705 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -666,6 +666,9 @@ def _handle_database_error(self, err): return False def _commit_event_session_or_retry(self): + """Commit the event session if there is work to do.""" + if not self.event_session.new and not self.event_session.dirty: + return tries = 1 while tries <= self.db_max_retries: try: From b92f29997e526197fc8f46cce8379ee0354d990e Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sun, 25 Apr 2021 12:10:33 +0200 Subject: [PATCH 0507/1317] Rework Fritz config_flow and device_tracker (#48287) Co-authored-by: J. Nick Koston --- .coveragerc | 3 + CODEOWNERS | 1 + homeassistant/components/fritz/__init__.py | 80 +++- homeassistant/components/fritz/common.py | 235 ++++++++++ homeassistant/components/fritz/config_flow.py | 248 +++++++++++ homeassistant/components/fritz/const.py | 18 + .../components/fritz/device_tracker.py | 256 +++++++---- homeassistant/components/fritz/manifest.json | 19 +- homeassistant/components/fritz/strings.json | 44 ++ .../components/fritz/translations/en.json | 44 ++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/ssdp.py | 5 + requirements_all.txt | 1 + requirements_test_all.txt | 1 + tests/components/fritz/__init__.py | 128 ++++++ tests/components/fritz/test_config_flow.py | 416 ++++++++++++++++++ 16 files changed, 1412 insertions(+), 88 deletions(-) create mode 100644 homeassistant/components/fritz/common.py create mode 100644 homeassistant/components/fritz/config_flow.py create mode 100644 homeassistant/components/fritz/const.py create mode 100644 homeassistant/components/fritz/strings.json create mode 100644 homeassistant/components/fritz/translations/en.json create mode 100644 tests/components/fritz/__init__.py create mode 100644 tests/components/fritz/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 26d49395164c7..a9342397123a1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -329,6 +329,9 @@ omit = homeassistant/components/freebox/router.py homeassistant/components/freebox/sensor.py homeassistant/components/freebox/switch.py + homeassistant/components/fritz/__init__.py + homeassistant/components/fritz/common.py + homeassistant/components/fritz/const.py homeassistant/components/fritz/device_tracker.py homeassistant/components/fritzbox_callmonitor/__init__.py homeassistant/components/fritzbox_callmonitor/const.py diff --git a/CODEOWNERS b/CODEOWNERS index d6226c08a5d32..976a5c7d6eff5 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -164,6 +164,7 @@ homeassistant/components/forked_daapd/* @uvjustin homeassistant/components/fortios/* @kimfrellsen homeassistant/components/foscam/* @skgsergio homeassistant/components/freebox/* @hacf-fr @Quentame +homeassistant/components/fritz/* @mammuth @AaronDavidSchneider @chemelli74 homeassistant/components/fronius/* @nielstron homeassistant/components/frontend/* @home-assistant/frontend homeassistant/components/garmin_connect/* @cyberjunky diff --git a/homeassistant/components/fritz/__init__.py b/homeassistant/components/fritz/__init__.py index 7069a29f163b2..6c8f54ea92899 100644 --- a/homeassistant/components/fritz/__init__.py +++ b/homeassistant/components/fritz/__init__.py @@ -1 +1,79 @@ -"""The fritz component.""" +"""Support for AVM Fritz!Box functions.""" +import asyncio +import logging + +from fritzconnection.core.exceptions import FritzConnectionException, FritzSecurityError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, + EVENT_HOMEASSISTANT_STOP, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers.typing import ConfigType + +from .common import FritzBoxTools +from .const import DOMAIN, PLATFORMS + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up fritzboxtools from config entry.""" + _LOGGER.debug("Setting up FRITZ!Box Tools component") + fritz_tools = FritzBoxTools( + hass=hass, + host=entry.data[CONF_HOST], + port=entry.data[CONF_PORT], + username=entry.data[CONF_USERNAME], + password=entry.data[CONF_PASSWORD], + ) + + try: + await fritz_tools.async_setup() + await fritz_tools.async_start() + except FritzSecurityError as ex: + raise ConfigEntryAuthFailed from ex + except FritzConnectionException as ex: + raise ConfigEntryNotReady from ex + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = fritz_tools + + @callback + def _async_unload(event): + fritz_tools.async_unload() + + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_unload) + ) + # Load the other platforms like switch + for domain in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, domain) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigType) -> bool: + """Unload FRITZ!Box Tools config entry.""" + fritzbox: FritzBoxTools = hass.data[DOMAIN][entry.entry_id] + fritzbox.async_unload() + + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py new file mode 100644 index 0000000000000..70783caef2572 --- /dev/null +++ b/homeassistant/components/fritz/common.py @@ -0,0 +1,235 @@ +"""Support for AVM FRITZ!Box classes.""" +from dataclasses import dataclass +from datetime import datetime, timedelta +import logging +from typing import Any, Dict, Optional + +# pylint: disable=import-error +from fritzconnection import FritzConnection +from fritzconnection.lib.fritzhosts import FritzHosts +from fritzconnection.lib.fritzstatus import FritzStatus + +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.util import dt as dt_util + +from .const import ( + DEFAULT_HOST, + DEFAULT_PORT, + DEFAULT_USERNAME, + DOMAIN, + TRACKER_SCAN_INTERVAL, +) + +_LOGGER = logging.getLogger(__name__) + + +@dataclass +class Device: + """FRITZ!Box device class.""" + + mac: str + ip_address: str + name: str + + +class FritzBoxTools: + """FrtizBoxTools class.""" + + def __init__( + self, + hass, + password, + username=DEFAULT_USERNAME, + host=DEFAULT_HOST, + port=DEFAULT_PORT, + ): + """Initialize FritzboxTools class.""" + self._cancel_scan = None + self._device_info = None + self._devices: Dict[str, Any] = {} + self._unique_id = None + self.connection = None + self.fritzhosts = None + self.fritzstatus = None + self.hass = hass + self.host = host + self.password = password + self.port = port + self.username = username + + async def async_setup(self): + """Wrap up FritzboxTools class setup.""" + return await self.hass.async_add_executor_job(self.setup) + + def setup(self): + """Set up FritzboxTools class.""" + + self.connection = FritzConnection( + address=self.host, + port=self.port, + user=self.username, + password=self.password, + timeout=60.0, + ) + + self.fritzstatus = FritzStatus(fc=self.connection) + if self._unique_id is None: + self._unique_id = self.connection.call_action("DeviceInfo:1", "GetInfo")[ + "NewSerialNumber" + ] + + self._device_info = self._fetch_device_info() + + async def async_start(self): + """Start FritzHosts connection.""" + self.fritzhosts = FritzHosts(fc=self.connection) + + await self.hass.async_add_executor_job(self.scan_devices) + + self._cancel_scan = async_track_time_interval( + self.hass, self.scan_devices, timedelta(seconds=TRACKER_SCAN_INTERVAL) + ) + + @callback + async def async_unload(self): + """Unload FritzboxTools class.""" + _LOGGER.debug("Unloading FRITZ!Box router integration") + if self._cancel_scan is not None: + self._cancel_scan() + self._cancel_scan = None + + @property + def unique_id(self): + """Return unique id.""" + return self._unique_id + + @property + def fritzbox_model(self): + """Return model.""" + return self._device_info["model"].replace("FRITZ!Box ", "") + + @property + def device_info(self): + """Return device info.""" + return self._device_info + + @property + def devices(self) -> Dict[str, Any]: + """Return devices.""" + return self._devices + + @property + def signal_device_new(self) -> str: + """Event specific per FRITZ!Box entry to signal new device.""" + return f"{DOMAIN}-device-new-{self._unique_id}" + + @property + def signal_device_update(self) -> str: + """Event specific per FRITZ!Box entry to signal updates in devices.""" + return f"{DOMAIN}-device-update-{self._unique_id}" + + def _update_info(self): + """Retrieve latest information from the FRITZ!Box.""" + return self.fritzhosts.get_hosts_info() + + def scan_devices(self, now: Optional[datetime] = None) -> None: + """Scan for new devices and return a list of found device ids.""" + + _LOGGER.debug("Checking devices for FRITZ!Box router %s", self.host) + + new_device = False + for known_host in self._update_info(): + if not known_host.get("mac"): + continue + + dev_mac = known_host["mac"] + dev_name = known_host["name"] + dev_ip = known_host["ip"] + dev_home = known_host["status"] + + dev_info = Device(dev_mac, dev_ip, dev_name) + + if dev_mac in self._devices: + self._devices[dev_mac].update(dev_info, dev_home) + else: + device = FritzDevice(dev_mac) + device.update(dev_info, dev_home) + self._devices[dev_mac] = device + new_device = True + + async_dispatcher_send(self.hass, self.signal_device_update) + if new_device: + async_dispatcher_send(self.hass, self.signal_device_new) + + def _fetch_device_info(self): + """Fetch device info.""" + info = self.connection.call_action("DeviceInfo:1", "GetInfo") + + dev_info = {} + dev_info["identifiers"] = { + # Serial numbers are unique identifiers within a specific domain + (DOMAIN, self.unique_id) + } + dev_info["manufacturer"] = "AVM" + + if dev_name := info.get("NewName"): + dev_info["name"] = dev_name + if dev_model := info.get("NewModelName"): + dev_info["model"] = dev_model + if dev_sw_ver := info.get("NewSoftwareVersion"): + dev_info["sw_version"] = dev_sw_ver + + return dev_info + + +class FritzDevice: + """FritzScanner device.""" + + def __init__(self, mac, name=None): + """Initialize device info.""" + self._mac = mac + self._name = name + self._ip_address = None + self._last_activity = None + self._connected = False + + def update(self, dev_info, dev_home): + """Update device info.""" + utc_point_in_time = dt_util.utcnow() + if not self._name: + self._name = dev_info.name or self._mac.replace(":", "_") + self._connected = dev_home + + if not self._connected: + self._ip_address = None + return + + self._last_activity = utc_point_in_time + self._ip_address = dev_info.ip_address + + @property + def is_connected(self): + """Return connected status.""" + return self._connected + + @property + def mac_address(self): + """Get MAC address.""" + return self._mac + + @property + def hostname(self): + """Get Name.""" + return self._name + + @property + def ip_address(self): + """Get IP address.""" + return self._ip_address + + @property + def last_activity(self): + """Return device last activity.""" + return self._last_activity diff --git a/homeassistant/components/fritz/config_flow.py b/homeassistant/components/fritz/config_flow.py new file mode 100644 index 0000000000000..8cebf6fd7dea1 --- /dev/null +++ b/homeassistant/components/fritz/config_flow.py @@ -0,0 +1,248 @@ +"""Config flow to configure the FRITZ!Box Tools integration.""" +import logging +from urllib.parse import urlparse + +from fritzconnection.core.exceptions import FritzConnectionException, FritzSecurityError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components.ssdp import ( + ATTR_SSDP_LOCATION, + ATTR_UPNP_FRIENDLY_NAME, + ATTR_UPNP_UDN, +) +from homeassistant.config_entries import ConfigEntry, ConfigFlow +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME +from homeassistant.core import callback + +from .common import FritzBoxTools +from .const import ( + DEFAULT_HOST, + DEFAULT_PORT, + DOMAIN, + ERROR_AUTH_INVALID, + ERROR_CONNECTION_ERROR, + ERROR_UNKNOWN, +) + +_LOGGER = logging.getLogger(__name__) + + +@config_entries.HANDLERS.register(DOMAIN) +class FritzBoxToolsFlowHandler(ConfigFlow): + """Handle a FRITZ!Box Tools config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + def __init__(self): + """Initialize FRITZ!Box Tools flow.""" + self._host = None + self._entry = None + self._name = None + self._password = None + self._port = None + self._username = None + self.import_schema = None + self.fritz_tools = None + + async def fritz_tools_init(self): + """Initialize FRITZ!Box Tools class.""" + self.fritz_tools = FritzBoxTools( + hass=self.hass, + host=self._host, + port=self._port, + username=self._username, + password=self._password, + ) + + try: + await self.fritz_tools.async_setup() + except FritzSecurityError: + return ERROR_AUTH_INVALID + except FritzConnectionException: + return ERROR_CONNECTION_ERROR + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + return ERROR_UNKNOWN + + return None + + async def async_check_configured_entry(self) -> ConfigEntry: + """Check if entry is configured.""" + for entry in self._async_current_entries(include_ignore=False): + if entry.data[CONF_HOST] == self._host: + return entry + return None + + @callback + def _async_create_entry(self): + """Async create flow handler entry.""" + return self.async_create_entry( + title=self._name, + data={ + CONF_HOST: self.fritz_tools.host, + CONF_PASSWORD: self.fritz_tools.password, + CONF_PORT: self.fritz_tools.port, + CONF_USERNAME: self.fritz_tools.username, + }, + ) + + async def async_step_ssdp(self, discovery_info): + """Handle a flow initialized by discovery.""" + ssdp_location = urlparse(discovery_info[ATTR_SSDP_LOCATION]) + self._host = ssdp_location.hostname + self._port = ssdp_location.port + self._name = discovery_info.get(ATTR_UPNP_FRIENDLY_NAME) + self.context[CONF_HOST] = self._host + + if uuid := discovery_info.get(ATTR_UPNP_UDN): + if uuid.startswith("uuid:"): + uuid = uuid[5:] + await self.async_set_unique_id(uuid) + self._abort_if_unique_id_configured({CONF_HOST: self._host}) + + for progress in self._async_in_progress(): + if progress.get("context", {}).get(CONF_HOST) == self._host: + return self.async_abort(reason="already_in_progress") + + if entry := await self.async_check_configured_entry(): + if uuid and not entry.unique_id: + self.hass.config_entries.async_update_entry(entry, unique_id=uuid) + return self.async_abort(reason="already_configured") + + self.context["title_placeholders"] = { + "name": self._name.replace("FRITZ!Box ", "") + } + return await self.async_step_confirm() + + async def async_step_confirm(self, user_input=None): + """Handle user-confirmation of discovered node.""" + + if user_input is None: + return self._show_setup_form_confirm() + + errors = {} + + self._username = user_input[CONF_USERNAME] + self._password = user_input[CONF_PASSWORD] + + error = await self.fritz_tools_init() + + if error: + errors["base"] = error + return self._show_setup_form_confirm(errors) + + return self._async_create_entry() + + def _show_setup_form_init(self, errors=None): + """Show the setup form to the user.""" + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Optional(CONF_HOST, default=DEFAULT_HOST): str, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): vol.Coerce(int), + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } + ), + errors=errors or {}, + ) + + def _show_setup_form_confirm(self, errors=None): + """Show the setup form to the user.""" + return self.async_show_form( + step_id="confirm", + data_schema=vol.Schema( + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } + ), + description_placeholders={"name": self._name}, + errors=errors or {}, + ) + + async def async_step_user(self, user_input=None): + """Handle a flow initiated by the user.""" + if user_input is None: + return self._show_setup_form_init() + self._host = user_input[CONF_HOST] + self._port = user_input[CONF_PORT] + self._username = user_input[CONF_USERNAME] + self._password = user_input[CONF_PASSWORD] + + if not (error := await self.fritz_tools_init()): + self._name = self.fritz_tools.device_info["model"] + + if await self.async_check_configured_entry(): + error = "already_configured" + + if error: + return self._show_setup_form_init({"base": error}) + + return self._async_create_entry() + + async def async_step_reauth(self, data): + """Handle flow upon an API authentication error.""" + self._entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + self._host = data[CONF_HOST] + self._port = data[CONF_PORT] + self._username = data[CONF_USERNAME] + self._password = data[CONF_PASSWORD] + return await self.async_step_reauth_confirm() + + def _show_setup_form_reauth_confirm(self, user_input, errors=None): + """Show the reauth form to the user.""" + return self.async_show_form( + step_id="reauth_confirm", + data_schema=vol.Schema( + { + vol.Required( + CONF_USERNAME, default=user_input.get(CONF_USERNAME) + ): str, + vol.Required(CONF_PASSWORD): str, + } + ), + description_placeholders={"host": self._host}, + errors=errors or {}, + ) + + async def async_step_reauth_confirm(self, user_input=None): + """Dialog that informs the user that reauth is required.""" + if user_input is None: + return self._show_setup_form_reauth_confirm( + user_input={CONF_USERNAME: self._username} + ) + + self._username = user_input[CONF_USERNAME] + self._password = user_input[CONF_PASSWORD] + + if error := await self.fritz_tools_init(): + return self._show_setup_form_reauth_confirm( + user_input=user_input, errors={"base": error} + ) + + self.hass.config_entries.async_update_entry( + self._entry, + data={ + CONF_HOST: self._host, + CONF_PASSWORD: self._password, + CONF_PORT: self._port, + CONF_USERNAME: self._username, + }, + ) + await self.hass.config_entries.async_reload(self._entry.entry_id) + return self.async_abort(reason="reauth_successful") + + async def async_step_import(self, import_config): + """Import a config entry from configuration.yaml.""" + return await self.async_step_user( + { + CONF_HOST: import_config[CONF_HOST], + CONF_USERNAME: import_config[CONF_USERNAME], + CONF_PASSWORD: import_config.get(CONF_PASSWORD), + CONF_PORT: import_config.get(CONF_PORT, DEFAULT_PORT), + } + ) diff --git a/homeassistant/components/fritz/const.py b/homeassistant/components/fritz/const.py new file mode 100644 index 0000000000000..90b7d1554e799 --- /dev/null +++ b/homeassistant/components/fritz/const.py @@ -0,0 +1,18 @@ +"""Constants for the FRITZ!Box Tools integration.""" + +DOMAIN = "fritz" + +PLATFORMS = ["device_tracker"] + + +DEFAULT_DEVICE_NAME = "Unknown device" +DEFAULT_HOST = "192.168.178.1" +DEFAULT_PORT = 49000 +DEFAULT_USERNAME = "" + + +ERROR_AUTH_INVALID = "invalid_auth" +ERROR_CONNECTION_ERROR = "connection_error" +ERROR_UNKNOWN = "unknown_error" + +TRACKER_SCAN_INTERVAL = 30 diff --git a/homeassistant/components/fritz/device_tracker.py b/homeassistant/components/fritz/device_tracker.py index 4da566376a6f8..03196c0cf94f8 100644 --- a/homeassistant/components/fritz/device_tracker.py +++ b/homeassistant/components/fritz/device_tracker.py @@ -1,107 +1,195 @@ """Support for FRITZ!Box routers.""" import logging +from typing import Dict -from fritzconnection.core import exceptions as fritzexceptions -from fritzconnection.lib.fritzhosts import FritzHosts import voluptuous as vol from homeassistant.components.device_tracker import ( - DOMAIN, + DOMAIN as DEVICE_TRACKER_DOMAIN, PLATFORM_SCHEMA, - DeviceScanner, + SOURCE_TYPE_ROUTER, ) +from homeassistant.components.device_tracker.config_entry import ScannerEntity +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.typing import ConfigType -_LOGGER = logging.getLogger(__name__) +from .common import FritzBoxTools +from .const import DEFAULT_DEVICE_NAME, DOMAIN -DEFAULT_HOST = "169.254.1.1" # This IP is valid for all FRITZ!Box routers. -DEFAULT_USERNAME = "admin" +_LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, - vol.Optional(CONF_PASSWORD): cv.string, - } +YAML_DEFAULT_HOST = "169.254.1.1" +YAML_DEFAULT_USERNAME = "admin" + +PLATFORM_SCHEMA = vol.All( + cv.deprecated(CONF_HOST), + cv.deprecated(CONF_USERNAME), + cv.deprecated(CONF_PASSWORD), + PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_HOST, default=YAML_DEFAULT_HOST): cv.string, + vol.Optional(CONF_USERNAME, default=YAML_DEFAULT_USERNAME): cv.string, + vol.Optional(CONF_PASSWORD): cv.string, + } + ), ) -def get_scanner(hass, config): - """Validate the configuration and return FritzBoxScanner.""" - scanner = FritzBoxScanner(config[DOMAIN]) - return scanner if scanner.success_init else None +async def async_get_scanner(hass: HomeAssistant, config: ConfigType): + """Import legacy FRITZ!Box configuration.""" + _LOGGER.debug("Import legacy FRITZ!Box configuration from YAML") -class FritzBoxScanner(DeviceScanner): - """This class queries a FRITZ!Box router.""" + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config[DEVICE_TRACKER_DOMAIN], + ) + ) - def __init__(self, config): - """Initialize the scanner.""" - self.last_results = [] - self.host = config[CONF_HOST] - self.username = config[CONF_USERNAME] - self.password = config.get(CONF_PASSWORD) - self.success_init = True - - # Establish a connection to the FRITZ!Box. - try: - self.fritz_box = FritzHosts( - address=self.host, user=self.username, password=self.password - ) - except (ValueError, TypeError): - self.fritz_box = None - - # At this point it is difficult to tell if a connection is established. - # So just check for null objects. - if self.fritz_box is None or not self.fritz_box.modelname: - self.success_init = False - - if self.success_init: - _LOGGER.info("Successfully connected to %s", self.fritz_box.modelname) - self._update_info() - else: - _LOGGER.error( - "Failed to establish connection to FRITZ!Box with IP: %s", self.host - ) + _LOGGER.warning( + "Your Fritz configuration has been imported into the UI, " + "please remove it from configuration.yaml. " + "Loading Fritz via scanner setup is now deprecated" + ) - def scan_devices(self): - """Scan for new devices and return a list of found device ids.""" - self._update_info() - active_hosts = [] - for known_host in self.last_results: - if known_host["status"] and known_host.get("mac"): - active_hosts.append(known_host["mac"]) - return active_hosts - - def get_device_name(self, device): - """Return the name of the given device or None if is not known.""" - ret = self.fritz_box.get_specific_host_entry(device).get("NewHostName") - if ret == {}: - return None - return ret - - def get_extra_attributes(self, device): - """Return the attributes (ip, mac) of the given device or None if is not known.""" - ip_device = None - try: - ip_device = self.fritz_box.get_specific_host_entry(device).get( - "NewIPAddress" - ) - except fritzexceptions.FritzLookUpError as fritz_lookup_error: - _LOGGER.warning( - "Host entry for %s not found: %s", device, fritz_lookup_error - ) + return None + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities +) -> None: + """Set up device tracker for FRITZ!Box component.""" + _LOGGER.debug("Starting FRITZ!Box device tracker") + router = hass.data[DOMAIN][entry.entry_id] + tracked = set() + + @callback + def update_router(): + """Update the values of the router.""" + _async_add_entities(router, async_add_entities, tracked) + + async_dispatcher_connect(hass, router.signal_device_new, update_router) - if not ip_device: - return {} - return {"ip": ip_device, "mac": device} + update_router() - def _update_info(self): - """Retrieve latest information from the FRITZ!Box.""" - if not self.success_init: - return False - _LOGGER.debug("Scanning") - self.last_results = self.fritz_box.get_hosts_info() - return True +@callback +def _async_add_entities(router, async_add_entities, tracked): + """Add new tracker entities from the router.""" + new_tracked = [] + + for mac, device in router.devices.items(): + if mac in tracked: + continue + + new_tracked.append(FritzBoxTracker(router, device)) + tracked.add(mac) + + if new_tracked: + async_add_entities(new_tracked) + + +class FritzBoxTracker(ScannerEntity): + """This class queries a FRITZ!Box router.""" + + def __init__(self, router: FritzBoxTools, device): + """Initialize a FRITZ!Box device.""" + self._router = router + self._mac = device.mac_address + self._name = device.hostname or DEFAULT_DEVICE_NAME + self._active = False + self._attrs = {} + + @property + def is_connected(self): + """Return device status.""" + return self._active + + @property + def name(self): + """Return device name.""" + return self._name + + @property + def unique_id(self): + """Return device unique id.""" + return self._mac + + @property + def ip_address(self) -> str: + """Return the primary ip address of the device.""" + return self._router.devices[self._mac].ip_address + + @property + def mac_address(self) -> str: + """Return the mac address of the device.""" + return self._mac + + @property + def hostname(self) -> str: + """Return hostname of the device.""" + return self._router.devices[self._mac].hostname + + @property + def source_type(self) -> str: + """Return tracker source type.""" + return SOURCE_TYPE_ROUTER + + @property + def device_info(self) -> Dict[str, any]: + """Return the device information.""" + return { + "connections": {(CONNECTION_NETWORK_MAC, self._mac)}, + "identifiers": {(DOMAIN, self.unique_id)}, + "name": self.name, + "manufacturer": "AVM", + "model": "FRITZ!Box Tracked device", + } + + @property + def should_poll(self) -> bool: + """No polling needed.""" + return False + + @property + def icon(self): + """Return device icon.""" + if self.is_connected: + return "mdi:lan-connect" + return "mdi:lan-disconnect" + + @callback + def async_process_update(self) -> None: + """Update device.""" + + device = self._router.devices[self._mac] + self._active = device.is_connected + + if device.last_activity: + self._attrs["last_time_reachable"] = device.last_activity.isoformat( + timespec="seconds" + ) + + @callback + def async_on_demand_update(self): + """Update state.""" + self.async_process_update() + self.async_write_ha_state() + + async def async_added_to_hass(self): + """Register state update callback.""" + self.async_process_update() + self.async_on_remove( + async_dispatcher_connect( + self.hass, + self._router.signal_device_update, + self.async_on_demand_update, + ) + ) diff --git a/homeassistant/components/fritz/manifest.json b/homeassistant/components/fritz/manifest.json index 522c7574b0625..68b1bde4f3818 100644 --- a/homeassistant/components/fritz/manifest.json +++ b/homeassistant/components/fritz/manifest.json @@ -1,8 +1,21 @@ { "domain": "fritz", - "name": "AVM FRITZ!Box", + "name": "AVM FRITZ!Box Tools", "documentation": "https://www.home-assistant.io/integrations/fritz", - "requirements": ["fritzconnection==1.4.2"], - "codeowners": [], + "requirements": [ + "fritzconnection==1.4.2", + "xmltodict==0.12.0" + ], + "codeowners": [ + "@mammuth", + "@AaronDavidSchneider", + "@chemelli74" + ], + "config_flow": true, + "ssdp": [ + { + "st": "urn:schemas-upnp-org:device:fritzbox:1" + } + ], "iot_class": "local_polling" } diff --git a/homeassistant/components/fritz/strings.json b/homeassistant/components/fritz/strings.json new file mode 100644 index 0000000000000..3a94d39a50c3d --- /dev/null +++ b/homeassistant/components/fritz/strings.json @@ -0,0 +1,44 @@ +{ + "config": { + "flow_title": "FRITZ!Box Tools: {name}", + "step": { + "confirm": { + "title": "Setup FRITZ!Box Tools", + "description": "Discovered FRITZ!Box: {name}\n\nSetup FRITZ!Box Tools to control your {name}", + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + }, + "start_config": { + "title": "Setup FRITZ!Box Tools - mandatory", + "description": "Setup FRITZ!Box Tools to control your FRITZ!Box.\nMinimum needed: username, password.", + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + }, + "reauth_confirm": { + "title": "Updating FRITZ!Box Tools - credentials", + "description": "Update FRITZ!Box Tools credentials for: {host}.\n\nFRITZ!Box Tools is unable to log in to your FRITZ!Box.", + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "abort": { + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + }, + "error": { + "connection_error": "[%key:common::config_flow::error::cannot_connect%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + } + } +} diff --git a/homeassistant/components/fritz/translations/en.json b/homeassistant/components/fritz/translations/en.json new file mode 100644 index 0000000000000..7497383dcfc44 --- /dev/null +++ b/homeassistant/components/fritz/translations/en.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress", + "reauth_successful": "Re-authentication was successful" + }, + "error": { + "already_configured": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress", + "connection_error": "Failed to connect", + "invalid_auth": "Invalid authentication" + }, + "flow_title": "FRITZ!Box Tools: {name}", + "step": { + "confirm": { + "data": { + "password": "Password", + "username": "Username" + }, + "description": "Discovered FRITZ!Box: {name}\n\nSetup FRITZ!Box Tools to control your {name}", + "title": "Setup FRITZ!Box Tools" + }, + "reauth_confirm": { + "data": { + "password": "Password", + "username": "Username" + }, + "description": "Update FRITZ!Box Tools credentials for: {host}.\n\nFRITZ!Box Tools is unable to log in to your FRITZ!Box.", + "title": "Updating FRITZ!Box Tools - credentials" + }, + "start_config": { + "data": { + "host": "Host", + "password": "Password", + "port": "Port", + "username": "Username" + }, + "description": "Setup FRITZ!Box Tools to control your FRITZ!Box.\nMinimum needed: username, password.", + "title": "Setup FRITZ!Box Tools - mandatory" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 764ce9e594b8f..bbf27893dc3bd 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -76,6 +76,7 @@ "forked_daapd", "foscam", "freebox", + "fritz", "fritzbox", "fritzbox_callmonitor", "garmin_connect", diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index 8d28a499aafc1..4141de31f7326 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -83,6 +83,11 @@ "manufacturer": "DIRECTV" } ], + "fritz": [ + { + "st": "urn:schemas-upnp-org:device:fritzbox:1" + } + ], "fritzbox": [ { "st": "urn:schemas-upnp-org:device:fritzbox:1" diff --git a/requirements_all.txt b/requirements_all.txt index a6bc723d27525..aa8d605f997ee 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2356,6 +2356,7 @@ xboxapi==2.0.1 xknx==0.18.1 # homeassistant.components.bluesound +# homeassistant.components.fritz # homeassistant.components.rest # homeassistant.components.startca # homeassistant.components.ted5000 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 460aa48391883..5fd213671d7f7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1247,6 +1247,7 @@ xbox-webapi==2.0.8 xknx==0.18.1 # homeassistant.components.bluesound +# homeassistant.components.fritz # homeassistant.components.rest # homeassistant.components.startca # homeassistant.components.ted5000 diff --git a/tests/components/fritz/__init__.py b/tests/components/fritz/__init__.py new file mode 100644 index 0000000000000..5a9b6cb1652d0 --- /dev/null +++ b/tests/components/fritz/__init__.py @@ -0,0 +1,128 @@ +"""Tests for the AVM Fritz!Box integration.""" +from unittest import mock + +from homeassistant.components.fritz.const import DOMAIN +from homeassistant.const import ( + CONF_DEVICES, + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, +) + +MOCK_CONFIG = { + DOMAIN: { + CONF_DEVICES: [ + { + CONF_HOST: "fake_host", + CONF_PORT: "1234", + CONF_PASSWORD: "fake_pass", + CONF_USERNAME: "fake_user", + } + ] + } +} + + +class FritzConnectionMock: # pylint: disable=too-few-public-methods + """FritzConnection mocking.""" + + FRITZBOX_DATA = { + ("WANIPConn:1", "GetStatusInfo"): { + "NewConnectionStatus": "Connected", + "NewUptime": 35307, + }, + ("WANIPConnection:1", "GetStatusInfo"): {}, + ("WANCommonIFC:1", "GetCommonLinkProperties"): { + "NewLayer1DownstreamMaxBitRate": 10087000, + "NewLayer1UpstreamMaxBitRate": 2105000, + "NewPhysicalLinkStatus": "Up", + }, + ("WANCommonIFC:1", "GetAddonInfos"): { + "NewByteSendRate": 3438, + "NewByteReceiveRate": 67649, + "NewTotalBytesSent": 1712232562, + "NewTotalBytesReceived": 5221019883, + }, + ("LANEthernetInterfaceConfig:1", "GetStatistics"): { + "NewBytesSent": 23004321, + "NewBytesReceived": 12045, + }, + ("DeviceInfo:1", "GetInfo"): { + "NewSerialNumber": 1234, + "NewName": "TheName", + "NewModelName": "FRITZ!Box 7490", + }, + } + + FRITZBOX_DATA_INDEXED = { + ("X_AVM-DE_Homeauto:1", "GetGenericDeviceInfos"): [ + { + "NewSwitchIsValid": "VALID", + "NewMultimeterIsValid": "VALID", + "NewTemperatureIsValid": "VALID", + "NewDeviceId": 16, + "NewAIN": "08761 0114116", + "NewDeviceName": "FRITZ!DECT 200 #1", + "NewTemperatureOffset": "0", + "NewSwitchLock": "0", + "NewProductName": "FRITZ!DECT 200", + "NewPresent": "CONNECTED", + "NewMultimeterPower": 1673, + "NewHkrComfortTemperature": "0", + "NewSwitchMode": "AUTO", + "NewManufacturer": "AVM", + "NewMultimeterIsEnabled": "ENABLED", + "NewHkrIsTemperature": "0", + "NewFunctionBitMask": 2944, + "NewTemperatureIsEnabled": "ENABLED", + "NewSwitchState": "ON", + "NewSwitchIsEnabled": "ENABLED", + "NewFirmwareVersion": "03.87", + "NewHkrSetVentilStatus": "CLOSED", + "NewMultimeterEnergy": 5182, + "NewHkrComfortVentilStatus": "CLOSED", + "NewHkrReduceTemperature": "0", + "NewHkrReduceVentilStatus": "CLOSED", + "NewHkrIsEnabled": "DISABLED", + "NewHkrSetTemperature": "0", + "NewTemperatureCelsius": "225", + "NewHkrIsValid": "INVALID", + }, + {}, + ], + ("Hosts1", "GetGenericHostEntry"): [ + { + "NewSerialNumber": 1234, + "NewName": "TheName", + "NewModelName": "FRITZ!Box 7490", + }, + {}, + ], + } + + MODELNAME = "FRITZ!Box 7490" + + def __init__(self): + """Inint Mocking class.""" + type(self).modelname = mock.PropertyMock(return_value=self.MODELNAME) + self.call_action = mock.Mock(side_effect=self._side_effect_callaction) + type(self).actionnames = mock.PropertyMock( + side_effect=self._side_effect_actionnames + ) + services = { + srv: None + for srv, _ in list(self.FRITZBOX_DATA.keys()) + + list(self.FRITZBOX_DATA_INDEXED.keys()) + } + type(self).services = mock.PropertyMock(side_effect=[services]) + + def _side_effect_callaction(self, service, action, **kwargs): + if kwargs: + index = next(iter(kwargs.values())) + return self.FRITZBOX_DATA_INDEXED[(service, action)][index] + + return self.FRITZBOX_DATA[(service, action)] + + def _side_effect_actionnames(self): + return list(self.FRITZBOX_DATA.keys()) + list(self.FRITZBOX_DATA_INDEXED.keys()) diff --git a/tests/components/fritz/test_config_flow.py b/tests/components/fritz/test_config_flow.py new file mode 100644 index 0000000000000..14830249da9da --- /dev/null +++ b/tests/components/fritz/test_config_flow.py @@ -0,0 +1,416 @@ +"""Tests for AVM Fritz!Box config flow.""" +from unittest.mock import patch + +from fritzconnection.core.exceptions import FritzConnectionException, FritzSecurityError +import pytest + +from homeassistant.components.fritz.const import ( + DOMAIN, + ERROR_AUTH_INVALID, + ERROR_CONNECTION_ERROR, + ERROR_UNKNOWN, +) +from homeassistant.components.ssdp import ( + ATTR_SSDP_LOCATION, + ATTR_UPNP_FRIENDLY_NAME, + ATTR_UPNP_UDN, +) +from homeassistant.config_entries import ( + SOURCE_IMPORT, + SOURCE_REAUTH, + SOURCE_SSDP, + SOURCE_USER, +) +from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + +from . import MOCK_CONFIG, FritzConnectionMock + +from tests.common import MockConfigEntry + +ATTR_HOST = "host" +ATTR_NEW_SERIAL_NUMBER = "NewSerialNumber" + +MOCK_HOST = "fake_host" +MOCK_SERIAL_NUMBER = "fake_serial_number" + + +MOCK_USER_DATA = MOCK_CONFIG[DOMAIN][CONF_DEVICES][0] +MOCK_DEVICE_INFO = { + ATTR_HOST: MOCK_HOST, + ATTR_NEW_SERIAL_NUMBER: MOCK_SERIAL_NUMBER, +} +MOCK_IMPORT_CONFIG = {CONF_HOST: MOCK_HOST, CONF_USERNAME: "username"} +MOCK_SSDP_DATA = { + ATTR_SSDP_LOCATION: "https://fake_host:12345/test", + ATTR_UPNP_FRIENDLY_NAME: "fake_name", + ATTR_UPNP_UDN: "uuid:only-a-test", +} + + +@pytest.fixture() +def fc_class_mock(mocker): + """Fixture that sets up a mocked FritzConnection class.""" + result = mocker.patch("fritzconnection.FritzConnection", autospec=True) + result.return_value = FritzConnectionMock() + yield result + + +async def test_user(hass: HomeAssistant, fc_class_mock): + """Test starting a flow by user.""" + with patch( + "homeassistant.components.fritz.common.FritzConnection", + side_effect=fc_class_mock, + ), patch("homeassistant.components.fritz.common.FritzStatus"), patch( + "homeassistant.components.fritz.async_setup_entry" + ) as mock_setup_entry: + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_USER_DATA + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["data"][CONF_HOST] == "fake_host" + assert result["data"][CONF_PASSWORD] == "fake_pass" + assert result["data"][CONF_USERNAME] == "fake_user" + assert not result["result"].unique_id + await hass.async_block_till_done() + + assert mock_setup_entry.called + + +async def test_user_already_configured(hass: HomeAssistant, fc_class_mock): + """Test starting a flow by user with an already configured device.""" + + mock_config = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + mock_config.add_to_hass(hass) + + with patch( + "homeassistant.components.fritz.common.FritzConnection", + side_effect=fc_class_mock, + ), patch("homeassistant.components.fritz.common.FritzStatus"): + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_USER_DATA + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + + +async def test_exception_security(hass: HomeAssistant): + """Test starting a flow by user with invalid credentials.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + + with patch( + "homeassistant.components.fritz.common.FritzConnection", + side_effect=FritzSecurityError, + ), patch("homeassistant.components.fritz.common.FritzStatus"): + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_USER_DATA + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"]["base"] == ERROR_AUTH_INVALID + + +async def test_exception_connection(hass: HomeAssistant): + """Test starting a flow by user with a connection error.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + + with patch( + "homeassistant.components.fritz.common.FritzConnection", + side_effect=FritzConnectionException, + ), patch("homeassistant.components.fritz.common.FritzStatus"): + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_USER_DATA + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"]["base"] == ERROR_CONNECTION_ERROR + + +async def test_exception_unknown(hass: HomeAssistant): + """Test starting a flow by user with an unknown exception.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + + with patch( + "homeassistant.components.fritz.common.FritzConnection", + side_effect=OSError, + ), patch("homeassistant.components.fritz.common.FritzStatus"): + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_USER_DATA + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"]["base"] == ERROR_UNKNOWN + + +async def test_reauth_successful(hass: HomeAssistant, fc_class_mock): + """Test starting a reauthentication flow.""" + + mock_config = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + mock_config.add_to_hass(hass) + + with patch( + "homeassistant.components.fritz.common.FritzConnection", + side_effect=fc_class_mock, + ), patch("homeassistant.components.fritz.common.FritzStatus"), patch( + "homeassistant.components.fritz.async_setup_entry" + ) as mock_setup_entry: + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_REAUTH, "entry_id": mock_config.entry_id}, + data=mock_config.data, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_USERNAME: "other_fake_user", + CONF_PASSWORD: "other_fake_password", + }, + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "reauth_successful" + + assert mock_setup_entry.called + + +async def test_reauth_not_successful(hass: HomeAssistant, fc_class_mock): + """Test starting a reauthentication flow but no connection found.""" + + mock_config = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + mock_config.add_to_hass(hass) + + with patch( + "homeassistant.components.fritz.common.FritzConnection", + side_effect=FritzConnectionException, + ), patch("homeassistant.components.fritz.common.FritzStatus"): + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_REAUTH, "entry_id": mock_config.entry_id}, + data=mock_config.data, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_USERNAME: "other_fake_user", + CONF_PASSWORD: "other_fake_password", + }, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "reauth_confirm" + + +async def test_ssdp_already_configured(hass: HomeAssistant, fc_class_mock): + """Test starting a flow from discovery with an already configured device.""" + + mock_config = MockConfigEntry( + domain=DOMAIN, + data=MOCK_USER_DATA, + unique_id="only-a-test", + ) + mock_config.add_to_hass(hass) + + with patch( + "homeassistant.components.fritz.common.FritzConnection", + side_effect=fc_class_mock, + ), patch("homeassistant.components.fritz.common.FritzStatus"): + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_ssdp_already_configured_host(hass: HomeAssistant, fc_class_mock): + """Test starting a flow from discovery with an already configured host.""" + + mock_config = MockConfigEntry( + domain=DOMAIN, + data=MOCK_USER_DATA, + unique_id="different-test", + ) + mock_config.add_to_hass(hass) + + with patch( + "homeassistant.components.fritz.common.FritzConnection", + side_effect=fc_class_mock, + ), patch("homeassistant.components.fritz.common.FritzStatus"): + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_ssdp_already_configured_host_uuid(hass: HomeAssistant, fc_class_mock): + """Test starting a flow from discovery with a laready configured uuid.""" + + mock_config = MockConfigEntry( + domain=DOMAIN, + data=MOCK_USER_DATA, + unique_id=None, + ) + mock_config.add_to_hass(hass) + + with patch( + "homeassistant.components.fritz.common.FritzConnection", + side_effect=fc_class_mock, + ), patch("homeassistant.components.fritz.common.FritzStatus"): + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_ssdp_already_in_progress_host(hass: HomeAssistant, fc_class_mock): + """Test starting a flow from discovery twice.""" + with patch( + "homeassistant.components.fritz.common.FritzConnection", + side_effect=fc_class_mock, + ), patch("homeassistant.components.fritz.common.FritzStatus"): + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "confirm" + + MOCK_NO_UNIQUE_ID = MOCK_SSDP_DATA.copy() + del MOCK_NO_UNIQUE_ID[ATTR_UPNP_UDN] + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_NO_UNIQUE_ID + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_in_progress" + + +async def test_ssdp(hass: HomeAssistant, fc_class_mock): + """Test starting a flow from discovery.""" + with patch( + "homeassistant.components.fritz.common.FritzConnection", + side_effect=fc_class_mock, + ), patch("homeassistant.components.fritz.common.FritzStatus"), patch( + "homeassistant.components.fritz.async_setup_entry" + ) as mock_setup_entry: + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_USERNAME: "fake_user", + CONF_PASSWORD: "fake_pass", + }, + ) + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["data"][CONF_HOST] == "fake_host" + assert result["data"][CONF_PASSWORD] == "fake_pass" + assert result["data"][CONF_USERNAME] == "fake_user" + + assert mock_setup_entry.called + + +async def test_ssdp_exception(hass: HomeAssistant): + """Test starting a flow from discovery but no device found.""" + with patch( + "homeassistant.components.fritz.common.FritzConnection", + side_effect=FritzConnectionException, + ), patch("homeassistant.components.fritz.common.FritzStatus"): + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_USERNAME: "fake_user", + CONF_PASSWORD: "fake_pass", + }, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "confirm" + + +async def test_import(hass: HomeAssistant, fc_class_mock): + """Test importing.""" + with patch( + "homeassistant.components.fritz.common.FritzConnection", + side_effect=fc_class_mock, + ), patch("homeassistant.components.fritz.common.FritzStatus"), patch( + "homeassistant.components.fritz.async_setup_entry" + ) as mock_setup_entry: + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_IMPORT_CONFIG + ) + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["data"][CONF_HOST] == "fake_host" + assert result["data"][CONF_PASSWORD] is None + assert result["data"][CONF_USERNAME] == "username" + await hass.async_block_till_done() + + assert mock_setup_entry.called From 9f8e683ae39432d9d17c0ed6e990fb3f995bf92e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 25 Apr 2021 03:13:22 -0700 Subject: [PATCH 0508/1317] Ask for IoT class during scaffold (#49647) Co-authored-by: Milan Meulemans Co-authored-by: Franck Nijhof --- script/scaffold/__main__.py | 2 +- script/scaffold/gather_info.py | 18 ++++++++++++++++++ script/scaffold/generate.py | 4 ++-- script/scaffold/model.py | 1 + .../integration/__init__.py | 2 +- 5 files changed, 23 insertions(+), 4 deletions(-) diff --git a/script/scaffold/__main__.py b/script/scaffold/__main__.py index 5a6645109fd6d..0504cdb8b37b4 100644 --- a/script/scaffold/__main__.py +++ b/script/scaffold/__main__.py @@ -99,7 +99,7 @@ def main(): if args.develop: print("Running tests") print(f"$ pytest -vvv tests/components/{info.domain}") - subprocess.run(["pytest", "-vvv", "tests/components/{info.domain}"]) + subprocess.run(["pytest", "-vvv", f"tests/components/{info.domain}"]) print() docs.print_relevant_docs(args.template, info) diff --git a/script/scaffold/gather_info.py b/script/scaffold/gather_info.py index fda5081e7c379..8442650dce470 100644 --- a/script/scaffold/gather_info.py +++ b/script/scaffold/gather_info.py @@ -2,6 +2,7 @@ import json from homeassistant.util import slugify +from script.hassfest.manifest import SUPPORTED_IOT_CLASSES from .const import COMPONENT_DIR from .error import ExitApp @@ -46,6 +47,7 @@ def gather_info(arguments) -> Info: "codeowner": "@developer", "requirement": "aiodevelop==1.2.3", "oauth2": True, + "iot_class": "local_polling", } ) else: @@ -86,6 +88,22 @@ def gather_new_integration(determine_auth: bool) -> Info: ] ], }, + "iot_class": { + "prompt": ( + f"""How will your integration gather data? + +Valid values are {', '.join(SUPPORTED_IOT_CLASSES)} + +More info @ https://developers.home-assistant.io/docs/creating_integration_manifest#iot-class +""" + ), + "validators": [ + [ + f"You need to pick one of {', '.join(SUPPORTED_IOT_CLASSES)}", + lambda value: value in SUPPORTED_IOT_CLASSES, + ] + ], + }, } if determine_auth: diff --git a/script/scaffold/generate.py b/script/scaffold/generate.py index 10de17e45ee13..7ebc364d7ee13 100644 --- a/script/scaffold/generate.py +++ b/script/scaffold/generate.py @@ -67,10 +67,10 @@ def _append(path: Path, text): path.write_text(path.read_text() + text) -def _custom_tasks(template, info) -> None: +def _custom_tasks(template, info: Info) -> None: """Handle custom tasks for templates.""" if template == "integration": - changes = {"codeowners": [info.codeowner]} + changes = {"codeowners": [info.codeowner], "iot_class": info.iot_class} if info.requirement: changes["requirements"] = [info.requirement] diff --git a/script/scaffold/model.py b/script/scaffold/model.py index f9c71072a1b40..93801f973ea92 100644 --- a/script/scaffold/model.py +++ b/script/scaffold/model.py @@ -18,6 +18,7 @@ class Info: is_new: bool = attr.ib() codeowner: str = attr.ib(default=None) requirement: str = attr.ib(default=None) + iot_class: str = attr.ib(default=None) authentication: str = attr.ib(default=None) discoverable: str = attr.ib(default=None) oauth2: str = attr.ib(default=None) diff --git a/script/scaffold/templates/config_flow_discovery/integration/__init__.py b/script/scaffold/templates/config_flow_discovery/integration/__init__.py index 6c187d1dafece..773bf594838db 100644 --- a/script/scaffold/templates/config_flow_discovery/integration/__init__.py +++ b/script/scaffold/templates/config_flow_discovery/integration/__init__.py @@ -10,7 +10,7 @@ # TODO List the platforms that you want to support. # For your initial PR, limit it to 1 platform. -PLATFORMS = ["light"] +PLATFORMS = ["binary_sensor"] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: From 3fa8ffa73137ae125f94d1f20341735ef2336e06 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 25 Apr 2021 00:38:40 -1000 Subject: [PATCH 0509/1317] Enable mccabe complexity checks in flake8 (#49616) Co-authored-by: Franck Nijhof --- .pre-commit-config.yaml | 1 + .../components/bluetooth_le_tracker/device_tracker.py | 2 +- homeassistant/components/buienradar/sensor.py | 2 +- homeassistant/components/device_sun_light_trigger/__init__.py | 2 +- homeassistant/components/emulated_hue/hue_api.py | 2 +- homeassistant/components/glances/sensor.py | 2 +- homeassistant/components/hassio/__init__.py | 2 +- homeassistant/components/hdmi_cec/__init__.py | 2 +- homeassistant/components/homeassistant/__init__.py | 2 +- homeassistant/components/homekit/accessories.py | 2 +- homeassistant/components/huawei_lte/config_flow.py | 2 +- homeassistant/components/influxdb/__init__.py | 2 +- homeassistant/components/isy994/services.py | 2 +- homeassistant/components/light/__init__.py | 2 +- homeassistant/components/mqtt/climate.py | 2 +- homeassistant/components/mqtt/discovery.py | 4 +++- homeassistant/components/mqtt/fan.py | 2 +- homeassistant/components/mqtt/light/schema_basic.py | 4 ++-- homeassistant/components/mqtt/light/schema_json.py | 2 +- homeassistant/components/mqtt/light/schema_template.py | 2 +- homeassistant/components/netatmo/sensor.py | 2 +- homeassistant/components/ozw/__init__.py | 2 +- homeassistant/components/plex/media_browser.py | 4 +++- homeassistant/components/plex/server.py | 2 +- homeassistant/components/simplisafe/__init__.py | 2 +- homeassistant/components/sonos/media_player.py | 2 +- homeassistant/components/spotify/media_player.py | 2 +- homeassistant/components/stream/worker.py | 2 +- homeassistant/components/synology_dsm/__init__.py | 2 +- homeassistant/components/systemmonitor/sensor.py | 2 +- homeassistant/components/wink/__init__.py | 2 +- homeassistant/components/xmpp/notify.py | 2 +- homeassistant/components/zwave/__init__.py | 2 +- homeassistant/components/zwave_js/__init__.py | 4 +++- homeassistant/config.py | 2 +- homeassistant/helpers/check_config.py | 4 +++- homeassistant/helpers/condition.py | 2 +- homeassistant/helpers/entity_platform.py | 2 +- homeassistant/helpers/update_coordinator.py | 2 +- requirements_test_pre_commit.txt | 1 + setup.cfg | 1 + tests/components/august/mocks.py | 2 +- 42 files changed, 51 insertions(+), 40 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a99f1d7de337b..29a46279f22c5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -33,6 +33,7 @@ repos: - pydocstyle==6.0.0 - flake8-comprehensions==3.4.0 - flake8-noqa==1.1.0 + - mccabe==0.6.1 files: ^(homeassistant|script|tests)/.+\.py$ - repo: https://github.com/PyCQA/bandit rev: 1.7.0 diff --git a/homeassistant/components/bluetooth_le_tracker/device_tracker.py b/homeassistant/components/bluetooth_le_tracker/device_tracker.py index 9ac79afde2cfb..6fb6f2109f13f 100644 --- a/homeassistant/components/bluetooth_le_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_le_tracker/device_tracker.py @@ -46,7 +46,7 @@ ) -def setup_scanner(hass, config, see, discovery_info=None): +def setup_scanner(hass, config, see, discovery_info=None): # noqa: C901 """Set up the Bluetooth LE Scanner.""" new_devices = {} diff --git a/homeassistant/components/buienradar/sensor.py b/homeassistant/components/buienradar/sensor.py index 170493969f8d7..5ff15a509785f 100644 --- a/homeassistant/components/buienradar/sensor.py +++ b/homeassistant/components/buienradar/sensor.py @@ -271,7 +271,7 @@ def data_updated(self, data): self.async_write_ha_state() @callback - def _load_data(self, data): + def _load_data(self, data): # noqa: C901 """Load the sensor with relevant data.""" # Find sensor diff --git a/homeassistant/components/device_sun_light_trigger/__init__.py b/homeassistant/components/device_sun_light_trigger/__init__.py index 5ae7e43b19a12..cb3c10dae7544 100644 --- a/homeassistant/components/device_sun_light_trigger/__init__.py +++ b/homeassistant/components/device_sun_light_trigger/__init__.py @@ -80,7 +80,7 @@ async def activate_on_start(_): return True -async def activate_automation( +async def activate_automation( # noqa: C901 hass, device_group, light_group, light_profile, disable_turn_off ): """Activate the automation.""" diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index be30de012860c..bbd899b559b10 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -322,7 +322,7 @@ def __init__(self, config): """Initialize the instance of the view.""" self.config = config - async def put(self, request, username, entity_number): + async def put(self, request, username, entity_number): # noqa: C901 """Process a request to set the state of an individual light.""" if not is_local(ip_address(request.remote)): return self.json_message("Only local IPs allowed", HTTP_UNAUTHORIZED) diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py index bbe045eb23220..7e599af414c3c 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -132,7 +132,7 @@ async def will_remove_from_hass(self): self.unsub_update() self.unsub_update = None - async def async_update(self): + async def async_update(self): # noqa: C901 """Get the latest data from REST API.""" value = self.glances_data.api.data if value is None: diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 4889c7c137aea..a2e7960972d97 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -336,7 +336,7 @@ def get_supervisor_ip(): return os.environ["SUPERVISOR"].partition(":")[0] -async def async_setup(hass: HomeAssistant, config: Config) -> bool: +async def async_setup(hass: HomeAssistant, config: Config) -> bool: # noqa: C901 """Set up the Hass.io component.""" # Check local setup for env in ("HASSIO", "HASSIO_TOKEN"): diff --git a/homeassistant/components/hdmi_cec/__init__.py b/homeassistant/components/hdmi_cec/__init__.py index c7dfd335c3212..7182642904045 100644 --- a/homeassistant/components/hdmi_cec/__init__.py +++ b/homeassistant/components/hdmi_cec/__init__.py @@ -187,7 +187,7 @@ def parse_mapping(mapping, parents=None): yield (val, pad_physical_address(cur)) -def setup(hass: HomeAssistant, base_config): +def setup(hass: HomeAssistant, base_config): # noqa: C901 """Set up the CEC capability.""" # Parse configuration into a dict of device name to physical address diff --git a/homeassistant/components/homeassistant/__init__.py b/homeassistant/components/homeassistant/__init__.py index f80d3a0efb4ce..fd7f2207bc775 100644 --- a/homeassistant/components/homeassistant/__init__.py +++ b/homeassistant/components/homeassistant/__init__.py @@ -50,7 +50,7 @@ SHUTDOWN_SERVICES = (SERVICE_HOMEASSISTANT_STOP, SERVICE_HOMEASSISTANT_RESTART) -async def async_setup(hass: ha.HomeAssistant, config: dict) -> bool: +async def async_setup(hass: ha.HomeAssistant, config: dict) -> bool: # noqa: C901 """Set up general services related to Home Assistant.""" async def async_handle_turn_service(service): diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 307dbf0e80636..3aeaa31faed05 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -92,7 +92,7 @@ TYPES = Registry() -def get_accessory(hass, driver, state, aid, config): +def get_accessory(hass, driver, state, aid, config): # noqa: C901 """Take state and return an accessory object if supported.""" if not aid: _LOGGER.warning( diff --git a/homeassistant/components/huawei_lte/config_flow.py b/homeassistant/components/huawei_lte/config_flow.py index c95131308d6ef..beeeab6e0bbb9 100644 --- a/homeassistant/components/huawei_lte/config_flow.py +++ b/homeassistant/components/huawei_lte/config_flow.py @@ -101,7 +101,7 @@ def _already_configured(self, user_input: dict[str, Any]) -> bool: } return user_input[CONF_URL] in existing_urls - async def async_step_user( + async def async_step_user( # noqa: C901 self, user_input: dict[str, Any] | None = None ) -> FlowResultDict: """Handle user initiated config flow.""" diff --git a/homeassistant/components/influxdb/__init__.py b/homeassistant/components/influxdb/__init__.py index dde10ffca76e4..bb5cf0173c108 100644 --- a/homeassistant/components/influxdb/__init__.py +++ b/homeassistant/components/influxdb/__init__.py @@ -326,7 +326,7 @@ class InfluxClient: close: Callable[[], None] -def get_influx_connection(conf, test_write=False, test_read=False): +def get_influx_connection(conf, test_write=False, test_read=False): # noqa: C901 """Create the correct influx connection for the API version.""" kwargs = { CONF_TIMEOUT: TIMEOUT, diff --git a/homeassistant/components/isy994/services.py b/homeassistant/components/isy994/services.py index 6f0484e3ff44c..023f1022661e2 100644 --- a/homeassistant/components/isy994/services.py +++ b/homeassistant/components/isy994/services.py @@ -157,7 +157,7 @@ def valid_isy_commands(value: Any) -> str: @callback -def async_setup_services(hass: HomeAssistant): +def async_setup_services(hass: HomeAssistant): # noqa: C901 """Create and register services for the ISY integration.""" existing_services = hass.services.async_services().get(DOMAIN) if existing_services and any( diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 0b78ed3672ebf..dba75b805ade6 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -242,7 +242,7 @@ def filter_turn_off_params(params): return {k: v for k, v in params.items() if k in (ATTR_TRANSITION, ATTR_FLASH)} -async def async_setup(hass, config): +async def async_setup(hass, config): # noqa: C901 """Expose light control via state machine and services.""" component = hass.data[DOMAIN] = EntityComponent( _LOGGER, DOMAIN, hass, SCAN_INTERVAL diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index dd766ef2035a4..da0ed485b7236 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -359,7 +359,7 @@ def _setup_from_config(self, config): tpl.hass = self.hass self._command_templates = command_templates - async def _subscribe_topics(self): + async def _subscribe_topics(self): # noqa: C901 """(Re)Subscribe to topics.""" topics = {} qos = self._config[CONF_QOS] diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 1d3f5034ff25a..3a5a3cb5f87fb 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -79,7 +79,9 @@ class MQTTConfig(dict): """Dummy class to allow adding attributes.""" -async def async_start(hass: HomeAssistant, discovery_topic, config_entry=None) -> bool: +async def async_start( # noqa: C901 + hass: HomeAssistant, discovery_topic, config_entry=None +) -> bool: """Start MQTT Discovery.""" mqtt_integrations = {} diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 35393ea819c0e..bdbe3412539d9 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -335,7 +335,7 @@ def _setup_from_config(self, config): tpl.hass = self.hass tpl_dict[key] = tpl.async_render_with_possible_json_value - async def _subscribe_topics(self): + async def _subscribe_topics(self): # noqa: C901 """(Re)Subscribe to topics.""" topics = {} diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 9c4b0f3a3e3a3..000ab956911ef 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -250,7 +250,7 @@ def _setup_from_config(self, config): ) self._optimistic_xy = optimistic or topic[CONF_XY_STATE_TOPIC] is None - async def _subscribe_topics(self): + async def _subscribe_topics(self): # noqa: C901 """(Re)Subscribe to topics.""" topics = {} @@ -579,7 +579,7 @@ def supported_features(self): return supported_features - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): # noqa: C901 """Turn the device on. This method is a coroutine. diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 9940d646a35bc..5143b92622aa4 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -487,7 +487,7 @@ def _scale_rgbxx(self, rgbxx, kwargs): def _supports_color_mode(self, color_mode): return self.supported_color_modes and color_mode in self.supported_color_modes - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): # noqa: C901 """Turn the device on. This method is a coroutine. diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index 7c0266265db5c..c5eee7006d6f6 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -142,7 +142,7 @@ def _setup_from_config(self, config): or self._templates[CONF_STATE_TEMPLATE] is None ) - async def _subscribe_topics(self): + async def _subscribe_topics(self): # noqa: C901 """(Re)Subscribe to topics.""" for tpl in self._templates.values(): if tpl is not None: diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 4c6facb3eca16..380ae1eff69c6 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -333,7 +333,7 @@ def entity_registry_enabled_default(self) -> bool: return self._enabled_default @callback - def async_update_callback(self): + def async_update_callback(self): # noqa: C901 """Update the entity's state.""" if self._data is None: if self._state is None: diff --git a/homeassistant/components/ozw/__init__.py b/homeassistant/components/ozw/__init__.py index c484eb4e0c0af..f3d827a57ff88 100644 --- a/homeassistant/components/ozw/__init__.py +++ b/homeassistant/components/ozw/__init__.py @@ -56,7 +56,7 @@ DATA_STOP_MQTT_CLIENT = "ozw_stop_mqtt_client" -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): # noqa: C901 """Set up ozw from a config entry.""" hass.data.setdefault(DOMAIN, {}) ozw_data = hass.data[DOMAIN][entry.entry_id] = {} diff --git a/homeassistant/components/plex/media_browser.py b/homeassistant/components/plex/media_browser.py index f3f92880c44c6..e19d86e89ec46 100644 --- a/homeassistant/components/plex/media_browser.py +++ b/homeassistant/components/plex/media_browser.py @@ -52,7 +52,9 @@ class UnknownMediaType(BrowseError): _LOGGER = logging.getLogger(__name__) -def browse_media(entity, is_internal, media_content_type=None, media_content_id=None): +def browse_media( # noqa: C901 + entity, is_internal, media_content_type=None, media_content_id=None +): """Implement the websocket media browsing helper.""" def item_payload(item): diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index d4bd4b09ef26b..4dcdda044ebe6 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -316,7 +316,7 @@ def _fetch_platform_data(self): self.plextv_clients(), ) - async def _async_update_platforms(self): + async def _async_update_platforms(self): # noqa: C901 """Update the platform entities.""" _LOGGER.debug("Updating devices") diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 6324df3311783..12c27c3c63cf9 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -181,7 +181,7 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass, config_entry): +async def async_setup_entry(hass, config_entry): # noqa: C901 """Set up SimpliSafe as config entry.""" hass.data[DOMAIN][DATA_LISTENER][config_entry.entry_id] = [] diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 8c7b61e96ecda..202a72d37ab20 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -158,7 +158,7 @@ def __init__(self) -> None: self.hosts_heartbeat = None -async def async_setup_entry( +async def async_setup_entry( # noqa: C901 hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable ) -> None: """Set up Sonos from a config entry.""" diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index 0a291582a30ae..8beb9733fa22b 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -533,7 +533,7 @@ async def async_browse_media(self, media_content_type=None, media_content_id=Non return response -def build_item_response(spotify, user, payload): +def build_item_response(spotify, user, payload): # noqa: C901 """Create response payload for the provided media query.""" media_content_type = payload["media_content_type"] media_content_id = payload["media_content_id"] diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index cd4528b3088e2..fb3562c1b5378 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -122,7 +122,7 @@ def close(self): self._stream_buffer.output.close() -def stream_worker(source, options, segment_buffer, quit_event): +def stream_worker(source, options, segment_buffer, quit_event): # noqa: C901 """Handle consuming streams.""" try: diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index 3c9461f6ca3ee..74cf8775b1c9c 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -118,7 +118,7 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): # noqa: C901 """Set up Synology DSM sensors.""" # Migrate old unique_id diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index 6d6e898908de2..70fd8275bd70c 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -365,7 +365,7 @@ async def async_added_to_hass(self) -> None: ) -def _update( +def _update( # noqa: C901 type_: str, data: SensorData ) -> tuple[str | None, str | None, datetime.datetime | None]: """Get the latest system information.""" diff --git a/homeassistant/components/wink/__init__.py b/homeassistant/components/wink/__init__.py index 198bddc937b56..f11e15670e9af 100644 --- a/homeassistant/components/wink/__init__.py +++ b/homeassistant/components/wink/__init__.py @@ -280,7 +280,7 @@ def wink_configuration_callback(callback_data): ) -def setup(hass, config): +def setup(hass, config): # noqa: C901 """Set up the Wink component.""" if hass.data.get(DOMAIN) is None: diff --git a/homeassistant/components/xmpp/notify.py b/homeassistant/components/xmpp/notify.py index 2abd3ffa245c4..bc5ebf12f75ad 100644 --- a/homeassistant/components/xmpp/notify.py +++ b/homeassistant/components/xmpp/notify.py @@ -113,7 +113,7 @@ async def async_send_message(self, message="", **kwargs): ) -async def async_send_message( +async def async_send_message( # noqa: C901 sender, password, recipients, diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index 12ea668dce867..6cf39709aaf6a 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -392,7 +392,7 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass, config_entry): +async def async_setup_entry(hass, config_entry): # noqa: C901 """Set up Z-Wave from a config entry. Will automatically load components to support devices found on the network. diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index b7d95ab7bc708..7c77105bbea13 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -108,7 +108,9 @@ def register_node_in_dev_reg( return device -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry( # noqa: C901 + hass: HomeAssistant, entry: ConfigEntry +) -> bool: """Set up Z-Wave JS from a config entry.""" use_addon = entry.data.get(CONF_USE_ADDON) if use_addon: diff --git a/homeassistant/config.py b/homeassistant/config.py index 958dcea555f4a..d22df2184f6dd 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -755,7 +755,7 @@ async def merge_packages_config( return config -async def async_process_component_config( +async def async_process_component_config( # noqa: C901 hass: HomeAssistant, config: ConfigType, integration: Integration ) -> ConfigType | None: """Check component configuration and return processed configuration. diff --git a/homeassistant/helpers/check_config.py b/homeassistant/helpers/check_config.py index a486c8bcc1479..26e063ae1f244 100644 --- a/homeassistant/helpers/check_config.py +++ b/homeassistant/helpers/check_config.py @@ -63,7 +63,9 @@ def error_str(self) -> str: return "\n".join([err.message for err in self.errors]) -async def async_check_ha_config_file(hass: HomeAssistant) -> HomeAssistantConfig: +async def async_check_ha_config_file( # noqa: C901 + hass: HomeAssistant, +) -> HomeAssistantConfig: """Load and check if Home Assistant configuration file is valid. This method is a coroutine. diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index b6030c61a1cdb..d1a7c95d16c4a 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -301,7 +301,7 @@ def numeric_state( ).result() -def async_numeric_state( +def async_numeric_state( # noqa: C901 hass: HomeAssistant, entity: None | str | State, below: float | str | None = None, diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index abeeb40ca76cd..e87960db7791e 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -388,7 +388,7 @@ async def async_add_entities( self.scan_interval, ) - async def _async_add_entity( # type: ignore[no-untyped-def] + async def _async_add_entity( # type: ignore[no-untyped-def] # noqa: C901 self, entity, update_before_add, entity_registry, device_registry ): """Add an entity to the platform.""" diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index 863fa71d43cdb..f9b97698220cd 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -157,7 +157,7 @@ async def async_refresh(self) -> None: """Refresh data and log errors.""" await self._async_refresh(log_failures=True) - async def _async_refresh( + async def _async_refresh( # noqa: C901 self, log_failures: bool = True, raise_on_auth_failed: bool = False, diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 92920c91549b7..3a146eb425e49 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -8,6 +8,7 @@ flake8-docstrings==1.6.0 flake8-noqa==1.1.0 flake8==3.9.1 isort==5.8.0 +mccabe==0.6.1 pycodestyle==2.7.0 pydocstyle==6.0.0 pyflakes==2.3.1 diff --git a/setup.cfg b/setup.cfg index d8569ad218857..3efd58e5ac9a0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -17,6 +17,7 @@ classifier = [flake8] exclude = .venv,.git,.tox,docs,venv,bin,lib,deps,build +max-complexity = 25 doctests = True # To work with Black # E501: line too long diff --git a/tests/components/august/mocks.py b/tests/components/august/mocks.py index 9a54a708a4f83..13d8f18d0d994 100644 --- a/tests/components/august/mocks.py +++ b/tests/components/august/mocks.py @@ -77,7 +77,7 @@ async def _mock_setup_august( return entry -async def _create_august_with_devices( +async def _create_august_with_devices( # noqa: C901 hass, devices, api_call_side_effects=None, activities=None, pubnub=None ): if api_call_side_effects is None: From 08622129427f1b037bf7d37b5926fbe2466b0bba Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 25 Apr 2021 00:41:40 -1000 Subject: [PATCH 0510/1317] Switch screenlogic discovery to use async version (#49650) --- .../components/screenlogic/climate.py | 2 +- .../components/screenlogic/config_flow.py | 4 ++-- .../components/screenlogic/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/screenlogic/test_config_flow.py | 18 +++++++++++------- 6 files changed, 17 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/screenlogic/climate.py b/homeassistant/components/screenlogic/climate.py index b50879bfd499a..fac03ea577a85 100644 --- a/homeassistant/components/screenlogic/climate.py +++ b/homeassistant/components/screenlogic/climate.py @@ -89,7 +89,7 @@ def target_temperature(self) -> float: @property def temperature_unit(self) -> str: """Return the unit of measurement.""" - if self.config_data["is_celcius"]["value"] == 1: + if self.config_data["is_celsius"]["value"] == 1: return TEMP_CELSIUS return TEMP_FAHRENHEIT diff --git a/homeassistant/components/screenlogic/config_flow.py b/homeassistant/components/screenlogic/config_flow.py index fb33bd7e2276a..05eaedf5ab77a 100644 --- a/homeassistant/components/screenlogic/config_flow.py +++ b/homeassistant/components/screenlogic/config_flow.py @@ -1,7 +1,7 @@ """Config flow for ScreenLogic.""" import logging -from screenlogicpy import ScreenLogicError, discover +from screenlogicpy import ScreenLogicError, discovery from screenlogicpy.const import SL_GATEWAY_IP, SL_GATEWAY_NAME, SL_GATEWAY_PORT from screenlogicpy.requests import login import voluptuous as vol @@ -27,7 +27,7 @@ async def async_discover_gateways_by_unique_id(hass): """Discover gateways and return a dict of them by unique id.""" discovered_gateways = {} try: - hosts = await hass.async_add_executor_job(discover) + hosts = await discovery.async_discover() _LOGGER.debug("Discovered hosts: %s", hosts) except ScreenLogicError as ex: _LOGGER.debug(ex) diff --git a/homeassistant/components/screenlogic/manifest.json b/homeassistant/components/screenlogic/manifest.json index e4d1be9bfb4fd..abef9ec99edef 100644 --- a/homeassistant/components/screenlogic/manifest.json +++ b/homeassistant/components/screenlogic/manifest.json @@ -3,7 +3,7 @@ "name": "Pentair ScreenLogic", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/screenlogic", - "requirements": ["screenlogicpy==0.3.0"], + "requirements": ["screenlogicpy==0.4.1"], "codeowners": ["@dieselrabbit"], "dhcp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index aa8d605f997ee..47bd4ab74991b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2033,7 +2033,7 @@ scapy==2.4.5 schiene==0.23 # homeassistant.components.screenlogic -screenlogicpy==0.3.0 +screenlogicpy==0.4.1 # homeassistant.components.scsgate scsgate==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5fd213671d7f7..7ca01cdfe682a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1082,7 +1082,7 @@ samsungtvws==1.6.0 scapy==2.4.5 # homeassistant.components.screenlogic -screenlogicpy==0.3.0 +screenlogicpy==0.4.1 # homeassistant.components.emulated_kasa # homeassistant.components.sense diff --git a/tests/components/screenlogic/test_config_flow.py b/tests/components/screenlogic/test_config_flow.py index f64e35a28b6f4..a24ce36e7a1ab 100644 --- a/tests/components/screenlogic/test_config_flow.py +++ b/tests/components/screenlogic/test_config_flow.py @@ -30,7 +30,7 @@ async def test_flow_discovery(hass): """Test the flow works with basic discovery.""" await setup.async_setup_component(hass, "persistent_notification", {}) with patch( - "homeassistant.components.screenlogic.config_flow.discover", + "homeassistant.components.screenlogic.config_flow.discovery.async_discover", return_value=[ { SL_GATEWAY_IP: "1.1.1.1", @@ -74,7 +74,7 @@ async def test_flow_discover_none(hass): """Test when nothing is discovered.""" await setup.async_setup_component(hass, "persistent_notification", {}) with patch( - "homeassistant.components.screenlogic.config_flow.discover", + "homeassistant.components.screenlogic.config_flow.discovery.async_discover", return_value=[], ): result = await hass.config_entries.flow.async_init( @@ -90,7 +90,7 @@ async def test_flow_discover_error(hass): """Test when discovery errors.""" await setup.async_setup_component(hass, "persistent_notification", {}) with patch( - "homeassistant.components.screenlogic.config_flow.discover", + "homeassistant.components.screenlogic.config_flow.discovery.async_discover", side_effect=ScreenLogicError("Fake error"), ): result = await hass.config_entries.flow.async_init( @@ -182,7 +182,7 @@ async def test_form_manual_entry(hass): """Test we get the form.""" await setup.async_setup_component(hass, "persistent_notification", {}) with patch( - "homeassistant.components.screenlogic.config_flow.discover", + "homeassistant.components.screenlogic.config_flow.discovery.async_discover", return_value=[ { SL_GATEWAY_IP: "1.1.1.1", @@ -241,9 +241,13 @@ async def test_form_manual_entry(hass): async def test_form_cannot_connect(hass): """Test we handle cannot connect error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) + with patch( + "homeassistant.components.screenlogic.config_flow.discovery.async_discover", + return_value=[], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) with patch( "homeassistant.components.screenlogic.config_flow.login.create_socket", From 914451d99cebb3dd2026648562db61258c3edabb Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sun, 25 Apr 2021 15:25:02 +0200 Subject: [PATCH 0511/1317] Remove dead code in modbus sensor and 100% test coverage (#49634) --- homeassistant/components/modbus/sensor.py | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index 254bfe6e0fbfe..c747d0a29d00f 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -306,22 +306,16 @@ def _update(self): v_result.append(f"{float(v_temp):.{self._precision}f}") self._value = ",".join(map(str, v_result)) else: - val = val[0] - # Apply scale and precision to floats and ints - if isinstance(val, (float, int)): - val = self._scale * val + self._offset + val = self._scale * val[0] + self._offset - # We could convert int to float, and the code would still work; however - # we lose some precision, and unit tests will fail. Therefore, we do - # the conversion only when it's absolutely necessary. - if isinstance(val, int) and self._precision == 0: - self._value = str(val) - else: - self._value = f"{float(val):.{self._precision}f}" - else: - # Don't process remaining datatypes (bytes and booleans) + # We could convert int to float, and the code would still work; however + # we lose some precision, and unit tests will fail. Therefore, we do + # the conversion only when it's absolutely necessary. + if isinstance(val, int) and self._precision == 0: self._value = str(val) + else: + self._value = f"{float(val):.{self._precision}f}" self._available = True self.schedule_update_ha_state() From 3077363f449744e690091ea7761d0018f1e4ee78 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Sun, 25 Apr 2021 06:27:46 -0700 Subject: [PATCH 0512/1317] Supplementary fixes to new motionEye integration (#49626) --- .../components/motioneye/__init__.py | 28 +-- homeassistant/components/motioneye/camera.py | 9 +- .../components/motioneye/config_flow.py | 162 ++++++++++-------- homeassistant/components/motioneye/const.py | 1 - .../components/motioneye/test_config_flow.py | 78 +++++++-- 5 files changed, 164 insertions(+), 114 deletions(-) diff --git a/homeassistant/components/motioneye/__init__.py b/homeassistant/components/motioneye/__init__.py index 61e7a7d12f365..5387de8225c1a 100644 --- a/homeassistant/components/motioneye/__init__.py +++ b/homeassistant/components/motioneye/__init__.py @@ -13,10 +13,10 @@ from motioneye_client.const import KEY_CAMERAS, KEY_ID, KEY_NAME from homeassistant.components.camera.const import DOMAIN as CAMERA_DOMAIN -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry -from homeassistant.const import CONF_SOURCE, CONF_URL +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import device_registry as dr from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, @@ -28,7 +28,6 @@ CONF_ADMIN_PASSWORD, CONF_ADMIN_USERNAME, CONF_CLIENT, - CONF_CONFIG_ENTRY, CONF_COORDINATOR, CONF_SURVEILLANCE_PASSWORD, CONF_SURVEILLANCE_USERNAME, @@ -98,22 +97,6 @@ def listen_for_new_cameras( ) -async def _create_reauth_flow( - hass: HomeAssistant, - config_entry: ConfigEntry, -) -> None: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={ - CONF_SOURCE: SOURCE_REAUTH, - CONF_CONFIG_ENTRY: config_entry, - }, - data=config_entry.data, - ) - ) - - @callback def _add_camera( hass: HomeAssistant, @@ -155,10 +138,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await client.async_client_login() - except MotionEyeClientInvalidAuthError: + except MotionEyeClientInvalidAuthError as exc: await client.async_client_close() - await _create_reauth_flow(hass, entry) - return False + raise ConfigEntryAuthFailed from exc except MotionEyeClientError as exc: await client.async_client_close() raise ConfigEntryNotReady from exc diff --git a/homeassistant/components/motioneye/camera.py b/homeassistant/components/motioneye/camera.py index 58df22198bfa9..5f64616e1a4bf 100644 --- a/homeassistant/components/motioneye/camera.py +++ b/homeassistant/components/motioneye/camera.py @@ -59,7 +59,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable -) -> bool: +) -> None: """Set up motionEye from a config entry.""" entry_data = hass.data[DOMAIN][entry.entry_id] @@ -82,7 +82,6 @@ def camera_add(camera: dict[str, Any]) -> None: ) listen_for_new_cameras(hass, entry, camera_add) - return True class MotionEyeMjpegCamera(MjpegCamera, CoordinatorEntity): @@ -96,7 +95,7 @@ def __init__( camera: dict[str, Any], client: MotionEyeClient, coordinator: DataUpdateCoordinator, - ): + ) -> None: """Initialize a MJPEG camera.""" self._surveillance_username = username self._surveillance_password = password @@ -109,7 +108,7 @@ def __init__( config_entry_id, self._camera_id, TYPE_MOTIONEYE_MJPEG_CAMERA ) self._motion_detection_enabled: bool = camera.get(KEY_MOTION_DETECTION, False) - self._available = MotionEyeMjpegCamera._is_acceptable_streaming_camera(camera) + self._available = self._is_acceptable_streaming_camera(camera) # motionEye cameras are always streaming or unavailable. self.is_streaming = True @@ -184,7 +183,7 @@ def _handle_coordinator_update(self) -> None: available = False if self.coordinator.last_update_success: camera = get_camera_from_cameras(self._camera_id, self.coordinator.data) - if MotionEyeMjpegCamera._is_acceptable_streaming_camera(camera): + if self._is_acceptable_streaming_camera(camera): assert camera self._set_mjpeg_camera_state_for_camera(camera) self._motion_detection_enabled = camera.get(KEY_MOTION_DETECTION, False) diff --git a/homeassistant/components/motioneye/config_flow.py b/homeassistant/components/motioneye/config_flow.py index f0ff0a3883669..5e37ae7bf6b09 100644 --- a/homeassistant/components/motioneye/config_flow.py +++ b/homeassistant/components/motioneye/config_flow.py @@ -24,7 +24,6 @@ from .const import ( CONF_ADMIN_PASSWORD, CONF_ADMIN_USERNAME, - CONF_CONFIG_ENTRY, CONF_SURVEILLANCE_PASSWORD, CONF_SURVEILLANCE_USERNAME, DOMAIN, @@ -43,81 +42,96 @@ async def async_step_user( self, user_input: ConfigType | None = None ) -> dict[str, Any]: """Handle the initial step.""" - out: dict[str, Any] = {} - errors = {} + + def _get_form( + user_input: ConfigType, errors: dict[str, str] | None = None + ) -> dict[str, Any]: + """Show the form to the user.""" + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required( + CONF_URL, default=user_input.get(CONF_URL, "") + ): str, + vol.Optional( + CONF_ADMIN_USERNAME, + default=user_input.get(CONF_ADMIN_USERNAME), + ): str, + vol.Optional( + CONF_ADMIN_PASSWORD, + default=user_input.get(CONF_ADMIN_PASSWORD), + ): str, + vol.Optional( + CONF_SURVEILLANCE_USERNAME, + default=user_input.get(CONF_SURVEILLANCE_USERNAME), + ): str, + vol.Optional( + CONF_SURVEILLANCE_PASSWORD, + default=user_input.get(CONF_SURVEILLANCE_PASSWORD), + ): str, + } + ), + errors=errors, + ) + + reauth_entry = None + if self.context.get("entry_id"): + reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + if user_input is None: - entry = self.context.get(CONF_CONFIG_ENTRY) - user_input = entry.data if entry else {} - else: - try: - # Cannot use cv.url validation in the schema itself, so - # apply extra validation here. - cv.url(user_input[CONF_URL]) - except vol.Invalid: - errors["base"] = "invalid_url" - else: - client = create_motioneye_client( - user_input[CONF_URL], - admin_username=user_input.get(CONF_ADMIN_USERNAME), - admin_password=user_input.get(CONF_ADMIN_PASSWORD), - surveillance_username=user_input.get(CONF_SURVEILLANCE_USERNAME), - surveillance_password=user_input.get(CONF_SURVEILLANCE_PASSWORD), - ) - - try: - await client.async_client_login() - except MotionEyeClientConnectionError: - errors["base"] = "cannot_connect" - except MotionEyeClientInvalidAuthError: - errors["base"] = "invalid_auth" - except MotionEyeClientRequestError: - errors["base"] = "unknown" - else: - entry = self.context.get(CONF_CONFIG_ENTRY) - if ( - self.context.get(CONF_SOURCE) == SOURCE_REAUTH - and entry is not None - ): - self.hass.config_entries.async_update_entry( - entry, data=user_input - ) - # Need to manually reload, as the listener won't have been - # installed because the initial load did not succeed (the reauth - # flow will not be initiated if the load succeeds). - await self.hass.config_entries.async_reload(entry.entry_id) - out = self.async_abort(reason="reauth_successful") - return out - - out = self.async_create_entry( - title=f"{user_input[CONF_URL]}", - data=user_input, - ) - return out - - out = self.async_show_form( - step_id="user", - data_schema=vol.Schema( - { - vol.Required(CONF_URL, default=user_input.get(CONF_URL, "")): str, - vol.Optional( - CONF_ADMIN_USERNAME, default=user_input.get(CONF_ADMIN_USERNAME) - ): str, - vol.Optional( - CONF_ADMIN_PASSWORD, default=user_input.get(CONF_ADMIN_PASSWORD) - ): str, - vol.Optional( - CONF_SURVEILLANCE_USERNAME, - default=user_input.get(CONF_SURVEILLANCE_USERNAME), - ): str, - vol.Optional( - CONF_SURVEILLANCE_PASSWORD, - default=user_input.get(CONF_SURVEILLANCE_PASSWORD), - ): str, - } - ), - errors=errors, + return _get_form(reauth_entry.data if reauth_entry else {}) + + try: + # Cannot use cv.url validation in the schema itself, so + # apply extra validation here. + cv.url(user_input[CONF_URL]) + except vol.Invalid: + return _get_form(user_input, {"base": "invalid_url"}) + + client = create_motioneye_client( + user_input[CONF_URL], + admin_username=user_input.get(CONF_ADMIN_USERNAME), + admin_password=user_input.get(CONF_ADMIN_PASSWORD), + surveillance_username=user_input.get(CONF_SURVEILLANCE_USERNAME), + surveillance_password=user_input.get(CONF_SURVEILLANCE_PASSWORD), + ) + + errors = {} + try: + await client.async_client_login() + except MotionEyeClientConnectionError: + errors["base"] = "cannot_connect" + except MotionEyeClientInvalidAuthError: + errors["base"] = "invalid_auth" + except MotionEyeClientRequestError: + errors["base"] = "unknown" + finally: + await client.async_client_close() + + if errors: + return _get_form(user_input, errors) + + if self.context.get(CONF_SOURCE) == SOURCE_REAUTH and reauth_entry is not None: + self.hass.config_entries.async_update_entry(reauth_entry, data=user_input) + # Need to manually reload, as the listener won't have been + # installed because the initial load did not succeed (the reauth + # flow will not be initiated if the load succeeds). + await self.hass.config_entries.async_reload(reauth_entry.entry_id) + return self.async_abort(reason="reauth_successful") + + # Search for duplicates: there isn't a useful unique_id, but + # at least prevent entries with the same motionEye URL. + for existing_entry in self._async_current_entries(include_ignore=False): + if existing_entry.data.get(CONF_URL) == user_input[CONF_URL]: + return self.async_abort(reason="already_configured") + + return self.async_create_entry( + title=f"{user_input[CONF_URL]}", + data=user_input, ) - return out async def async_step_reauth( self, diff --git a/homeassistant/components/motioneye/const.py b/homeassistant/components/motioneye/const.py index a76053b28541d..fbd0d9b4d2e9a 100644 --- a/homeassistant/components/motioneye/const.py +++ b/homeassistant/components/motioneye/const.py @@ -3,7 +3,6 @@ DOMAIN = "motioneye" -CONF_CONFIG_ENTRY = "config_entry" CONF_CLIENT = "client" CONF_COORDINATOR = "coordinator" CONF_ADMIN_PASSWORD = "admin_password" diff --git a/tests/components/motioneye/test_config_flow.py b/tests/components/motioneye/test_config_flow.py index 2c16aea14be7c..d8700e162c48f 100644 --- a/tests/components/motioneye/test_config_flow.py +++ b/tests/components/motioneye/test_config_flow.py @@ -12,7 +12,6 @@ from homeassistant.components.motioneye.const import ( CONF_ADMIN_PASSWORD, CONF_ADMIN_USERNAME, - CONF_CONFIG_ENTRY, CONF_SURVEILLANCE_PASSWORD, CONF_SURVEILLANCE_USERNAME, DOMAIN, @@ -22,6 +21,8 @@ from . import TEST_URL, create_mock_motioneye_client, create_mock_motioneye_config_entry +from tests.common import MockConfigEntry + _LOGGER = logging.getLogger(__name__) @@ -32,7 +33,7 @@ async def test_user_success(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == "form" - assert result["errors"] == {} + assert not result["errors"] mock_client = create_mock_motioneye_client() @@ -65,6 +66,7 @@ async def test_user_success(hass: HomeAssistant) -> None: CONF_SURVEILLANCE_PASSWORD: "surveillance-password", } assert len(mock_setup_entry.mock_calls) == 1 + assert mock_client.async_client_close.called async def test_user_invalid_auth(hass: HomeAssistant) -> None: @@ -92,10 +94,11 @@ async def test_user_invalid_auth(hass: HomeAssistant) -> None: CONF_SURVEILLANCE_PASSWORD: "surveillance-password", }, ) - await mock_client.async_client_close() + await hass.async_block_till_done() assert result["type"] == "form" assert result["errors"] == {"base": "invalid_auth"} + assert mock_client.async_client_close.called async def test_user_invalid_url(hass: HomeAssistant) -> None: @@ -105,9 +108,10 @@ async def test_user_invalid_url(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) + mock_client = create_mock_motioneye_client() with patch( "homeassistant.components.motioneye.MotionEyeClient", - return_value=create_mock_motioneye_client(), + return_value=mock_client, ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -119,6 +123,7 @@ async def test_user_invalid_url(hass: HomeAssistant) -> None: CONF_SURVEILLANCE_PASSWORD: "surveillance-password", }, ) + await hass.async_block_till_done() assert result["type"] == "form" assert result["errors"] == {"base": "invalid_url"} @@ -149,10 +154,11 @@ async def test_user_cannot_connect(hass: HomeAssistant) -> None: CONF_SURVEILLANCE_PASSWORD: "surveillance-password", }, ) - await mock_client.async_client_close() + await hass.async_block_till_done() assert result["type"] == "form" assert result["errors"] == {"base": "cannot_connect"} + assert mock_client.async_client_close.called async def test_user_request_error(hass: HomeAssistant) -> None: @@ -178,10 +184,11 @@ async def test_user_request_error(hass: HomeAssistant) -> None: CONF_SURVEILLANCE_PASSWORD: "surveillance-password", }, ) - await mock_client.async_client_close() + await hass.async_block_till_done() assert result["type"] == "form" assert result["errors"] == {"base": "unknown"} + assert mock_client.async_client_close.called async def test_reauth(hass: HomeAssistant) -> None: @@ -197,11 +204,11 @@ async def test_reauth(hass: HomeAssistant) -> None: DOMAIN, context={ "source": config_entries.SOURCE_REAUTH, - CONF_CONFIG_ENTRY: config_entry, + "entry_id": config_entry.entry_id, }, ) assert result["type"] == "form" - assert result["errors"] == {} + assert not result["errors"] mock_client = create_mock_motioneye_client() @@ -226,8 +233,57 @@ async def test_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "reauth_successful" - assert config_entry.data == new_data + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "reauth_successful" + assert config_entry.data == new_data assert len(mock_setup_entry.mock_calls) == 1 + assert mock_client.async_client_close.called + + +async def test_duplicate(hass: HomeAssistant) -> None: + """Test that a duplicate entry (same URL) is rejected.""" + config_data = { + CONF_URL: TEST_URL, + } + + # Add an existing entry with the same URL. + existing_entry: MockConfigEntry = MockConfigEntry( # type: ignore[no-untyped-call] + domain=DOMAIN, + data=config_data, + ) + existing_entry.add_to_hass(hass) # type: ignore[no-untyped-call] + + # Now do the usual config entry process, and verify it is rejected. + create_mock_motioneye_config_entry(hass, data=config_data) + + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert not result["errors"] + mock_client = create_mock_motioneye_client() + + new_data = { + CONF_URL: TEST_URL, + CONF_ADMIN_USERNAME: "admin-username", + CONF_ADMIN_PASSWORD: "admin-password", + CONF_SURVEILLANCE_USERNAME: "surveillance-username", + CONF_SURVEILLANCE_PASSWORD: "surveillance-password", + } + + with patch( + "homeassistant.components.motioneye.MotionEyeClient", + return_value=mock_client, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + new_data, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + assert mock_client.async_client_close.called From 7ecd4f5eede8810d224962c1975a584b26f6bd59 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Sun, 25 Apr 2021 09:48:03 -0400 Subject: [PATCH 0513/1317] Fix pylint failures caused by fritz (#49655) * Fix test failures caused by fritz * Fix typing.Any Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- homeassistant/components/fritz/common.py | 12 ++++++------ homeassistant/components/fritz/config_flow.py | 1 - homeassistant/components/fritz/device_tracker.py | 8 ++++---- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 70783caef2572..f05fea0dcd499 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -1,8 +1,10 @@ """Support for AVM FRITZ!Box classes.""" +from __future__ import annotations + from dataclasses import dataclass from datetime import datetime, timedelta import logging -from typing import Any, Dict, Optional +from typing import Any # pylint: disable=import-error from fritzconnection import FritzConnection @@ -48,7 +50,7 @@ def __init__( """Initialize FritzboxTools class.""" self._cancel_scan = None self._device_info = None - self._devices: Dict[str, Any] = {} + self._devices: dict[str, Any] = {} self._unique_id = None self.connection = None self.fritzhosts = None @@ -65,7 +67,6 @@ async def async_setup(self): def setup(self): """Set up FritzboxTools class.""" - self.connection = FritzConnection( address=self.host, port=self.port, @@ -116,7 +117,7 @@ def device_info(self): return self._device_info @property - def devices(self) -> Dict[str, Any]: + def devices(self) -> dict[str, Any]: """Return devices.""" return self._devices @@ -134,9 +135,8 @@ def _update_info(self): """Retrieve latest information from the FRITZ!Box.""" return self.fritzhosts.get_hosts_info() - def scan_devices(self, now: Optional[datetime] = None) -> None: + def scan_devices(self, now: datetime | None) -> None: """Scan for new devices and return a list of found device ids.""" - _LOGGER.debug("Checking devices for FRITZ!Box router %s", self.host) new_device = False diff --git a/homeassistant/components/fritz/config_flow.py b/homeassistant/components/fritz/config_flow.py index 8cebf6fd7dea1..ba048e9775908 100644 --- a/homeassistant/components/fritz/config_flow.py +++ b/homeassistant/components/fritz/config_flow.py @@ -118,7 +118,6 @@ async def async_step_ssdp(self, discovery_info): async def async_step_confirm(self, user_input=None): """Handle user-confirmation of discovered node.""" - if user_input is None: return self._show_setup_form_confirm() diff --git a/homeassistant/components/fritz/device_tracker.py b/homeassistant/components/fritz/device_tracker.py index 03196c0cf94f8..42da58d7336d9 100644 --- a/homeassistant/components/fritz/device_tracker.py +++ b/homeassistant/components/fritz/device_tracker.py @@ -1,6 +1,8 @@ """Support for FRITZ!Box routers.""" +from __future__ import annotations + import logging -from typing import Dict +from typing import Any import voluptuous as vol @@ -42,7 +44,6 @@ async def async_get_scanner(hass: HomeAssistant, config: ConfigType): """Import legacy FRITZ!Box configuration.""" - _LOGGER.debug("Import legacy FRITZ!Box configuration from YAML") hass.async_create_task( @@ -143,7 +144,7 @@ def source_type(self) -> str: return SOURCE_TYPE_ROUTER @property - def device_info(self) -> Dict[str, any]: + def device_info(self) -> dict[str, Any]: """Return the device information.""" return { "connections": {(CONNECTION_NETWORK_MAC, self._mac)}, @@ -168,7 +169,6 @@ def icon(self): @callback def async_process_update(self) -> None: """Update device.""" - device = self._router.devices[self._mac] self._active = device.is_connected From 3be8c9c1c07e2f23b6ab947d29885393af6bc30a Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sun, 25 Apr 2021 12:20:21 -0500 Subject: [PATCH 0514/1317] Add battery support for Sonos speakers (#49441) Co-authored-by: Walter Huf Co-authored-by: J. Nick Koston --- homeassistant/components/sonos/__init__.py | 149 ++++++- homeassistant/components/sonos/const.py | 22 + homeassistant/components/sonos/entity.py | 79 ++++ .../components/sonos/media_player.py | 422 ++++++------------ homeassistant/components/sonos/sensor.py | 204 +++++++++ homeassistant/components/sonos/speaker.py | 217 +++++++++ tests/components/sonos/conftest.py | 15 +- tests/components/sonos/test_media_player.py | 6 +- tests/components/sonos/test_sensor.py | 62 +++ 9 files changed, 885 insertions(+), 291 deletions(-) create mode 100644 homeassistant/components/sonos/entity.py create mode 100644 homeassistant/components/sonos/sensor.py create mode 100644 homeassistant/components/sonos/speaker.py create mode 100644 tests/components/sonos/test_sensor.py diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index c3a977e32e10e..7a9d994737d83 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -1,16 +1,46 @@ """Support to embed Sonos.""" +from __future__ import annotations + +import asyncio +import datetime +from functools import partial +import logging +import socket + +import pysonos +from pysonos import events_asyncio +from pysonos.core import SoCo +from pysonos.exceptions import SoCoException import voluptuous as vol from homeassistant import config_entries from homeassistant.components.media_player import DOMAIN as MP_DOMAIN -from homeassistant.const import CONF_HOSTS +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_HOSTS, + EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, +) +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_send, dispatcher_send + +from .const import ( + DATA_SONOS, + DISCOVERY_INTERVAL, + DOMAIN, + PLATFORMS, + SONOS_GROUP_UPDATE, + SONOS_SEEN, +) +from .speaker import SonosSpeaker -from .const import DOMAIN +_LOGGER = logging.getLogger(__name__) CONF_ADVERTISE_ADDR = "advertise_addr" CONF_INTERFACE_ADDR = "interface_addr" + CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( @@ -31,6 +61,19 @@ ) +class SonosData: + """Storage class for platform global data.""" + + def __init__(self): + """Initialize the data.""" + self.discovered = {} + self.media_player_entities = {} + self.topology_condition = asyncio.Condition() + self.discovery_thread = None + self.hosts_heartbeat = None + self.platforms_ready = set() + + async def async_setup(hass, config): """Set up the Sonos component.""" conf = config.get(DOMAIN) @@ -47,9 +90,103 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass, entry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: """Set up Sonos from a config entry.""" - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, MP_DOMAIN) - ) + pysonos.config.EVENTS_MODULE = events_asyncio + + if DATA_SONOS not in hass.data: + hass.data[DATA_SONOS] = SonosData() + + config = hass.data[DOMAIN].get("media_player", {}) + _LOGGER.debug("Reached async_setup_entry, config=%s", config) + + advertise_addr = config.get(CONF_ADVERTISE_ADDR) + if advertise_addr: + pysonos.config.EVENT_ADVERTISE_IP = advertise_addr + + def _stop_discovery(event: Event) -> None: + data = hass.data[DATA_SONOS] + if data.discovery_thread: + data.discovery_thread.stop() + data.discovery_thread = None + if data.hosts_heartbeat: + data.hosts_heartbeat() + data.hosts_heartbeat = None + + def _discovery(now: datetime.datetime | None = None) -> None: + """Discover players from network or configuration.""" + hosts = config.get(CONF_HOSTS) + + def _discovered_player(soco: SoCo) -> None: + """Handle a (re)discovered player.""" + try: + _LOGGER.debug("Reached _discovered_player, soco=%s", soco) + + data = hass.data[DATA_SONOS] + + if soco.uid not in data.discovered: + _LOGGER.debug("Adding new speaker") + speaker_info = soco.get_speaker_info(True) + speaker = SonosSpeaker(hass, soco, speaker_info) + data.discovered[soco.uid] = speaker + speaker.setup() + else: + dispatcher_send(hass, f"{SONOS_SEEN}-{soco.uid}", soco) + + except SoCoException as ex: + _LOGGER.debug("SoCoException, ex=%s", ex) + + if hosts: + for host in hosts: + try: + _LOGGER.debug("Testing %s", host) + player = pysonos.SoCo(socket.gethostbyname(host)) + if player.is_visible: + # Make sure that the player is available + _ = player.volume + + _discovered_player(player) + except (OSError, SoCoException) as ex: + _LOGGER.debug("Exception %s", ex) + if now is None: + _LOGGER.warning("Failed to initialize '%s'", host) + + _LOGGER.debug("Tested all hosts") + hass.data[DATA_SONOS].hosts_heartbeat = hass.helpers.event.call_later( + DISCOVERY_INTERVAL.total_seconds(), _discovery + ) + else: + _LOGGER.debug("Starting discovery thread") + hass.data[DATA_SONOS].discovery_thread = pysonos.discover_thread( + _discovered_player, + interval=DISCOVERY_INTERVAL.total_seconds(), + interface_addr=config.get(CONF_INTERFACE_ADDR), + ) + hass.data[DATA_SONOS].discovery_thread.name = "Sonos-Discovery" + + @callback + def _async_signal_update_groups(event): + async_dispatcher_send(hass, SONOS_GROUP_UPDATE) + + @callback + def start_discovery(): + _LOGGER.debug("Adding discovery job") + hass.async_add_executor_job(_discovery) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop_discovery) + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_START, _async_signal_update_groups + ) + + @callback + def platform_ready(platform, _): + hass.data[DATA_SONOS].platforms_ready.add(platform) + if hass.data[DATA_SONOS].platforms_ready == PLATFORMS: + start_discovery() + + for platform in PLATFORMS: + task = hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, platform) + ) + task.add_done_callback(partial(platform_ready, platform)) + return True diff --git a/homeassistant/components/sonos/const.py b/homeassistant/components/sonos/const.py index 63d5745da21a7..b841347ce27b9 100644 --- a/homeassistant/components/sonos/const.py +++ b/homeassistant/components/sonos/const.py @@ -1,4 +1,7 @@ """Const for Sonos.""" +import datetime + +from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.media_player.const import ( MEDIA_CLASS_ALBUM, MEDIA_CLASS_ARTIST, @@ -15,9 +18,11 @@ MEDIA_TYPE_PLAYLIST, MEDIA_TYPE_TRACK, ) +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN DOMAIN = "sonos" DATA_SONOS = "sonos_media_player" +PLATFORMS = {MP_DOMAIN, SENSOR_DOMAIN} SONOS_ARTIST = "artists" SONOS_ALBUM = "albums" @@ -121,3 +126,20 @@ MEDIA_TYPE_PLAYLIST, MEDIA_TYPE_TRACK, ] + +SONOS_CONTENT_UPDATE = "sonos_content_update" +SONOS_DISCOVERY_UPDATE = "sonos_discovery_update" +SONOS_ENTITY_CREATED = "sonos_entity_created" +SONOS_ENTITY_UPDATE = "sonos_entity_update" +SONOS_GROUP_UPDATE = "sonos_group_update" +SONOS_MEDIA_UPDATE = "sonos_media_update" +SONOS_PROPERTIES_UPDATE = "sonos_properties_update" +SONOS_PLAYER_RECONNECTED = "sonos_player_reconnected" +SONOS_STATE_UPDATED = "sonos_state_updated" +SONOS_VOLUME_UPDATE = "sonos_properties_update" +SONOS_SEEN = "sonos_seen" + +BATTERY_SCAN_INTERVAL = datetime.timedelta(minutes=15) +SCAN_INTERVAL = datetime.timedelta(seconds=10) +DISCOVERY_INTERVAL = datetime.timedelta(seconds=60) +SEEN_EXPIRE_TIME = 3.5 * DISCOVERY_INTERVAL diff --git a/homeassistant/components/sonos/entity.py b/homeassistant/components/sonos/entity.py new file mode 100644 index 0000000000000..69a88077e3189 --- /dev/null +++ b/homeassistant/components/sonos/entity.py @@ -0,0 +1,79 @@ +"""Entity representing a Sonos player.""" +from __future__ import annotations + +import logging +from typing import Any + +from pysonos.core import SoCo + +from homeassistant.core import callback +import homeassistant.helpers.device_registry as dr +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity + +from . import SonosData +from .const import DOMAIN, SONOS_ENTITY_UPDATE, SONOS_STATE_UPDATED +from .speaker import SonosSpeaker + +_LOGGER = logging.getLogger(__name__) + + +class SonosEntity(Entity): + """Representation of a Sonos entity.""" + + def __init__(self, speaker: SonosSpeaker, sonos_data: SonosData): + """Initialize a SonosEntity.""" + self.speaker = speaker + self.data = sonos_data + + async def async_added_to_hass(self) -> None: + """Handle common setup when added to hass.""" + await self.speaker.async_seen() + + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{SONOS_ENTITY_UPDATE}-{self.soco.uid}", + self.async_update, # pylint: disable=no-member + ) + ) + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{SONOS_STATE_UPDATED}-{self.soco.uid}", + self.async_write_state, + ) + ) + + @property + def soco(self) -> SoCo: + """Return the speaker SoCo instance.""" + return self.speaker.soco + + @property + def device_info(self) -> dict[str, Any]: + """Return information about the device.""" + return { + "identifiers": {(DOMAIN, self.soco.uid)}, + "name": self.speaker.zone_name, + "model": self.speaker.model_name.replace("Sonos ", ""), + "sw_version": self.speaker.version, + "connections": {(dr.CONNECTION_NETWORK_MAC, self.speaker.mac_address)}, + "manufacturer": "Sonos", + "suggested_area": self.speaker.zone_name, + } + + @property + def available(self) -> bool: + """Return whether this device is available.""" + return self.speaker.available + + @property + def should_poll(self) -> bool: + """Return that we should not be polled (we handle that internally).""" + return False + + @callback + def async_write_state(self) -> None: + """Flush the current entity state.""" + self.async_write_ha_state() diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 202a72d37ab20..57ce1f8a8ae03 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -7,13 +7,11 @@ import datetime import functools as ft import logging -import socket from typing import Any, Callable import urllib.parse import async_timeout -import pysonos -from pysonos import alarms, events_asyncio +from pysonos import alarms from pysonos.core import ( MUSIC_SRC_LINE_IN, MUSIC_SRC_RADIO, @@ -23,7 +21,7 @@ SoCo, ) from pysonos.data_structures import DidlFavorite -from pysonos.events_base import Event, SubscriptionBase +from pysonos.events_base import Event as SonosEvent from pysonos.exceptions import SoCoException, SoCoUPnPException import pysonos.music_library import pysonos.snapshot @@ -32,6 +30,7 @@ from homeassistant.components.media_player import MediaPlayerEntity from homeassistant.components.media_player.const import ( ATTR_MEDIA_ENQUEUE, + DOMAIN as MP_DOMAIN, MEDIA_TYPE_ALBUM, MEDIA_TYPE_ARTIST, MEDIA_TYPE_MUSIC, @@ -59,35 +58,36 @@ from homeassistant.components.plex.const import PLEX_URI_SCHEME from homeassistant.components.plex.services import play_on_sonos from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_TIME, - CONF_HOSTS, - EVENT_HOMEASSISTANT_STOP, - STATE_IDLE, - STATE_PAUSED, - STATE_PLAYING, -) +from homeassistant.const import ATTR_TIME, STATE_IDLE, STATE_PAUSED, STATE_PLAYING from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers import config_validation as cv, entity_platform, service -import homeassistant.helpers.device_registry as dr +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.network import is_internal_request from homeassistant.util.dt import utcnow -from . import CONF_ADVERTISE_ADDR, CONF_INTERFACE_ADDR +from . import SonosData from .const import ( DATA_SONOS, DOMAIN as SONOS_DOMAIN, MEDIA_TYPES_TO_SONOS, PLAYABLE_MEDIA_TYPES, + SONOS_CONTENT_UPDATE, + SONOS_DISCOVERY_UPDATE, + SONOS_ENTITY_CREATED, + SONOS_GROUP_UPDATE, + SONOS_MEDIA_UPDATE, + SONOS_PLAYER_RECONNECTED, + SONOS_VOLUME_UPDATE, ) +from .entity import SonosEntity from .media_browser import build_item_response, get_media, library_payload +from .speaker import SonosSpeaker _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = 10 -DISCOVERY_INTERVAL = 60 -SEEN_EXPIRE_TIME = 3.5 * DISCOVERY_INTERVAL - SUPPORT_SONOS = ( SUPPORT_BROWSE_MEDIA | SUPPORT_CLEAR_PLAYLIST @@ -146,98 +146,17 @@ UNAVAILABLE_VALUES = {"", "NOT_IMPLEMENTED", None} -class SonosData: - """Storage class for platform global data.""" - - def __init__(self) -> None: - """Initialize the data.""" - self.entities: list[SonosEntity] = [] - self.discovered: list[str] = [] - self.topology_condition = asyncio.Condition() - self.discovery_thread = None - self.hosts_heartbeat = None - - -async def async_setup_entry( # noqa: C901 +async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable ) -> None: """Set up Sonos from a config entry.""" - if DATA_SONOS not in hass.data: - hass.data[DATA_SONOS] = SonosData() - - config = hass.data[SONOS_DOMAIN].get("media_player", {}) - _LOGGER.debug("Reached async_setup_entry, config=%s", config) - pysonos.config.EVENTS_MODULE = events_asyncio - - advertise_addr = config.get(CONF_ADVERTISE_ADDR) - if advertise_addr: - pysonos.config.EVENT_ADVERTISE_IP = advertise_addr - - def _stop_discovery(event: Event) -> None: - data = hass.data[DATA_SONOS] - if data.discovery_thread: - data.discovery_thread.stop() - data.discovery_thread = None - if data.hosts_heartbeat: - data.hosts_heartbeat() - data.hosts_heartbeat = None - - def _discovery(now: datetime.datetime | None = None) -> None: - """Discover players from network or configuration.""" - hosts = config.get(CONF_HOSTS) - - def _discovered_player(soco: SoCo) -> None: - """Handle a (re)discovered player.""" - try: - _LOGGER.debug("Reached _discovered_player, soco=%s", soco) - - if soco.uid not in hass.data[DATA_SONOS].discovered: - _LOGGER.debug("Adding new entity") - hass.data[DATA_SONOS].discovered.append(soco.uid) - hass.add_job(async_add_entities, [SonosEntity(soco)]) - else: - entity = _get_entity_from_soco_uid(hass, soco.uid) - if entity and (entity.soco == soco or not entity.available): - _LOGGER.debug("Seen %s", entity) - hass.add_job(entity.async_seen(soco)) # type: ignore - - except SoCoException as ex: - _LOGGER.debug("SoCoException, ex=%s", ex) - - if hosts: - for host in hosts: - try: - _LOGGER.debug("Testing %s", host) - player = pysonos.SoCo(socket.gethostbyname(host)) - if player.is_visible: - # Make sure that the player is available - _ = player.volume - - _discovered_player(player) - except (OSError, SoCoException) as ex: - _LOGGER.debug("Exception %s", ex) - if now is None: - _LOGGER.warning("Failed to initialize '%s'", host) - - _LOGGER.debug("Tested all hosts") - hass.data[DATA_SONOS].hosts_heartbeat = hass.helpers.event.call_later( - DISCOVERY_INTERVAL, _discovery - ) - else: - _LOGGER.debug("Starting discovery thread") - hass.data[DATA_SONOS].discovery_thread = pysonos.discover_thread( - _discovered_player, - interval=DISCOVERY_INTERVAL, - interface_addr=config.get(CONF_INTERFACE_ADDR), - ) - hass.data[DATA_SONOS].discovery_thread.name = "Sonos-Discovery" - - _LOGGER.debug("Adding discovery job") - hass.async_add_executor_job(_discovery) - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop_discovery) - platform = entity_platform.current_platform.get() + @callback + def async_create_entities(speaker: SonosSpeaker) -> None: + """Handle device discovery and create entities.""" + async_add_entities([SonosMediaPlayerEntity(speaker, hass.data[DATA_SONOS])]) + @service.verify_domain_control(hass, SONOS_DOMAIN) async def async_service_handle(service_call: ServiceCall) -> None: """Handle dispatched services.""" @@ -248,28 +167,30 @@ async def async_service_handle(service_call: ServiceCall) -> None: return for entity in entities: - assert isinstance(entity, SonosEntity) + assert isinstance(entity, SonosMediaPlayerEntity) if service_call.service == SERVICE_JOIN: master = platform.entities.get(service_call.data[ATTR_MASTER]) if master: - await SonosEntity.join_multi(hass, master, entities) # type: ignore[arg-type] + await SonosMediaPlayerEntity.join_multi(hass, master, entities) # type: ignore[arg-type] else: _LOGGER.error( "Invalid master specified for join service: %s", service_call.data[ATTR_MASTER], ) elif service_call.service == SERVICE_UNJOIN: - await SonosEntity.unjoin_multi(hass, entities) # type: ignore[arg-type] + await SonosMediaPlayerEntity.unjoin_multi(hass, entities) # type: ignore[arg-type] elif service_call.service == SERVICE_SNAPSHOT: - await SonosEntity.snapshot_multi( + await SonosMediaPlayerEntity.snapshot_multi( hass, entities, service_call.data[ATTR_WITH_GROUP] # type: ignore[arg-type] ) elif service_call.service == SERVICE_RESTORE: - await SonosEntity.restore_multi( + await SonosMediaPlayerEntity.restore_multi( hass, entities, service_call.data[ATTR_WITH_GROUP] # type: ignore[arg-type] ) + async_dispatcher_connect(hass, SONOS_DISCOVERY_UPDATE, async_create_entities) + hass.services.async_register( SONOS_DOMAIN, SERVICE_JOIN, @@ -343,13 +264,11 @@ async def async_service_handle(service_call: ServiceCall) -> None: ) -def _get_entity_from_soco_uid(hass: HomeAssistant, uid: str) -> SonosEntity | None: - """Return SonosEntity from SoCo uid.""" - entities: list[SonosEntity] = hass.data[DATA_SONOS].entities - for entity in entities: - if uid == entity.unique_id: - return entity - return None +def _get_entity_from_soco_uid( + hass: HomeAssistant, uid: str +) -> SonosMediaPlayerEntity | None: + """Return SonosMediaPlayerEntity from SoCo uid.""" + return hass.data[DATA_SONOS].media_player_entities.get(uid) # type: ignore[no-any-return] def soco_error(errorcodes: list[str] | None = None) -> Callable: @@ -378,7 +297,7 @@ def soco_coordinator(funct: Callable) -> Callable: """Call function on coordinator.""" @ft.wraps(funct) - def wrapper(entity: SonosEntity, *args: Any, **kwargs: Any) -> Any: + def wrapper(entity: SonosMediaPlayerEntity, *args: Any, **kwargs: Any) -> Any: """Wrap for call to coordinator.""" if entity.is_coordinator: return funct(entity, *args, **kwargs) @@ -396,22 +315,18 @@ def _timespan_secs(timespan: str | None) -> None | float: return sum(60 ** x[0] * int(x[1]) for x in enumerate(reversed(timespan.split(":")))) -class SonosEntity(MediaPlayerEntity): +class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): """Representation of a Sonos entity.""" - def __init__(self, player: SoCo) -> None: + def __init__(self, speaker: SonosSpeaker, sonos_data: SonosData) -> None: """Initialize the Sonos entity.""" - self._subscriptions: list[SubscriptionBase] = [] - self._poll_timer: Callable | None = None - self._seen_timer: Callable | None = None + super().__init__(speaker, sonos_data) self._volume_increment = 2 - self._unique_id: str = player.uid - self._player: SoCo = player self._player_volume: int | None = None self._player_muted: bool | None = None self._play_mode: str | None = None - self._coordinator: SonosEntity | None = None - self._sonos_group: list[SonosEntity] = [self] + self._coordinator: SonosMediaPlayerEntity | None = None + self._sonos_group: list[SonosMediaPlayerEntity] = [self] self._status: str | None = None self._uri: str | None = None self._media_library = pysonos.music_library.MusicLibrary(self.soco) @@ -429,28 +344,59 @@ def __init__(self, player: SoCo) -> None: self._source_name: str | None = None self._favorites: list[DidlFavorite] = [] self._soco_snapshot: pysonos.snapshot.Snapshot | None = None - self._snapshot_group: list[SonosEntity] | None = None - - # Set these early since device_info() needs them - speaker_info: dict = self.soco.get_speaker_info(True) - self._name: str = speaker_info["zone_name"] - self._model: str = speaker_info["model_name"] - self._sw_version: str = speaker_info["software_version"] - self._mac_address: str = speaker_info["mac_address"] + self._snapshot_group: list[SonosMediaPlayerEntity] | None = None async def async_added_to_hass(self) -> None: """Subscribe sonos events.""" - await self.async_seen(self.soco) + self.data.media_player_entities[self.unique_id] = self + await self.async_reconnect_player() + await super().async_added_to_hass() + + self.async_on_remove( + async_dispatcher_connect( + self.hass, SONOS_GROUP_UPDATE, self.async_update_groups + ) + ) + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{SONOS_CONTENT_UPDATE}-{self.soco.uid}", + self.async_update_content, + ) + ) + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{SONOS_MEDIA_UPDATE}-{self.soco.uid}", + self.async_update_media, + ) + ) + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{SONOS_VOLUME_UPDATE}-{self.soco.uid}", + self.async_update_volume, + ) + ) + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{SONOS_PLAYER_RECONNECTED}-{self.soco.uid}", + self.async_reconnect_player, + ) + ) - self.hass.data[DATA_SONOS].entities.append(self) + if self.hass.is_running: + async_dispatcher_send(self.hass, SONOS_GROUP_UPDATE) - for entity in self.hass.data[DATA_SONOS].entities: - await entity.create_update_groups_coro() + async_dispatcher_send( + self.hass, f"{SONOS_ENTITY_CREATED}-{self.soco.uid}", MP_DOMAIN + ) @property def unique_id(self) -> str: """Return a unique ID.""" - return self._unique_id + return self.soco.uid # type: ignore[no-any-return] def __hash__(self) -> int: """Return a hash of self.""" @@ -459,20 +405,7 @@ def __hash__(self) -> int: @property def name(self) -> str: """Return the name of the entity.""" - return self._name - - @property - def device_info(self) -> dict: - """Return information about the device.""" - return { - "identifiers": {(SONOS_DOMAIN, self._unique_id)}, - "name": self._name, - "model": self._model.replace("Sonos ", ""), - "sw_version": self._sw_version, - "connections": {(dr.CONNECTION_NETWORK_MAC, self._mac_address)}, - "manufacturer": "Sonos", - "suggested_area": self._name, - } + return self.speaker.zone_name # type: ignore[no-any-return] @property # type: ignore[misc] @soco_coordinator @@ -496,65 +429,11 @@ def is_coordinator(self) -> bool: """Return true if player is a coordinator.""" return self._coordinator is None - @property - def soco(self) -> SoCo: - """Return soco object.""" - return self._player - @property def coordinator(self) -> SoCo: """Return coordinator of this player.""" return self._coordinator - async def async_seen(self, player: SoCo) -> None: - """Record that this player was seen right now.""" - was_available = self.available - _LOGGER.debug("Async seen: %s, was_available: %s", player, was_available) - - self._player = player - - if self._seen_timer: - self._seen_timer() - - self._seen_timer = self.hass.helpers.event.async_call_later( - SEEN_EXPIRE_TIME, self.async_unseen - ) - - if was_available: - return - - self._poll_timer = self.hass.helpers.event.async_track_time_interval( - self.update, datetime.timedelta(seconds=SCAN_INTERVAL) - ) - - done = await self._async_attach_player() - if not done: - assert self._seen_timer is not None - self._seen_timer() - await self.async_unseen() - - self.async_write_ha_state() - - async def async_unseen(self, now: datetime.datetime | None = None) -> None: - """Make this player unavailable when it was not seen recently.""" - self._seen_timer = None - - if self._poll_timer: - self._poll_timer() - self._poll_timer = None - - for subscription in self._subscriptions: - await subscription.unsubscribe() - - self._subscriptions = [] - - self.async_write_ha_state() - - @property - def available(self) -> bool: - """Return True if entity is available.""" - return self._seen_timer is not None - def _clear_media_position(self) -> None: """Clear the media_position.""" self._media_position = None @@ -572,49 +451,23 @@ def _set_favorites(self) -> None: # Skip unknown types _LOGGER.error("Unhandled favorite '%s': %s", fav.title, ex) - def _attach_player(self) -> None: - """Get basic information and add event subscriptions.""" + async def async_reconnect_player(self) -> None: + """Set basic information when player is reconnected.""" + await self.hass.async_add_executor_job(self._reconnect_player) + + def _reconnect_player(self) -> None: + """Set basic information when player is reconnected.""" self._play_mode = self.soco.play_mode self.update_volume() self._set_favorites() - async def _async_attach_player(self) -> bool: - """Get basic information and add event subscriptions.""" - try: - await self.hass.async_add_executor_job(self._attach_player) - - player = self.soco - - if self._subscriptions: - raise RuntimeError( - f"Attempted to attach subscriptions to player: {player} " - f"when existing subscriptions exist: {self._subscriptions}" - ) - - await self._subscribe(player.avTransport, self.async_update_media) - await self._subscribe(player.renderingControl, self.async_update_volume) - await self._subscribe(player.zoneGroupTopology, self.async_update_groups) - await self._subscribe(player.contentDirectory, self.async_update_content) - return True - except SoCoException as ex: - _LOGGER.warning("Could not connect %s: %s", self.entity_id, ex) - return False - - async def _subscribe( - self, target: SubscriptionBase, sub_callback: Callable - ) -> None: - """Create a sonos subscription.""" - subscription = await target.subscribe(auto_renew=True) - subscription.callback = sub_callback - self._subscriptions.append(subscription) - - @property - def should_poll(self) -> bool: - """Return that we should not be polled (we handle that internally).""" - return False + async def async_update(self, now: datetime.datetime | None = None) -> None: + """Retrieve latest state.""" + await self.hass.async_add_executor_job(self._update, now) - def update(self, now: datetime.datetime | None = None) -> None: + def _update(self, now: datetime.datetime | None = None) -> None: """Retrieve latest state.""" + _LOGGER.debug("Polling speaker %s", self.speaker.zone_name) try: self.update_groups() self.update_volume() @@ -624,11 +477,11 @@ def update(self, now: datetime.datetime | None = None) -> None: pass @callback - def async_update_media(self, event: Event | None = None) -> None: + def async_update_media(self, event: SonosEvent | None = None) -> None: """Update information about currently playing media.""" self.hass.async_add_executor_job(self.update_media, event) - def update_media(self, event: Event | None = None) -> None: + def update_media(self, event: SonosEvent | None = None) -> None: """Update information about currently playing media.""" variables = event and event.variables @@ -685,7 +538,8 @@ def update_media(self, event: Event | None = None) -> None: self.schedule_update_ha_state() # Also update slaves - for entity in self.hass.data[DATA_SONOS].entities: + entities = self.data.media_player_entities.values() + for entity in entities: coordinator = entity.coordinator if coordinator and coordinator.unique_id == self.unique_id: entity.schedule_update_ha_state() @@ -774,7 +628,7 @@ def update_media_music(self, update_media_position: bool, track_info: dict) -> N self._queue_position = playlist_position - 1 @callback - def async_update_volume(self, event: Event) -> None: + def async_update_volume(self, event: SonosEvent) -> None: """Update information about currently volume settings.""" variables = event.variables @@ -799,20 +653,22 @@ def update_volume(self) -> None: self._night_sound = self.soco.night_mode self._speech_enhance = self.soco.dialog_mode - def update_groups(self, event: Event | None = None) -> None: + def update_groups(self, event: SonosEvent | None = None) -> None: """Handle callback for topology change event.""" coro = self.create_update_groups_coro(event) if coro: self.hass.add_job(coro) # type: ignore @callback - def async_update_groups(self, event: Event | None = None) -> None: + def async_update_groups(self, event: SonosEvent | None = None) -> None: """Handle callback for topology change event.""" coro = self.create_update_groups_coro(event) if coro: self.hass.async_add_job(coro) # type: ignore - def create_update_groups_coro(self, event: Event | None = None) -> Coroutine | None: + def create_update_groups_coro( + self, event: SonosEvent | None = None + ) -> Coroutine | None: """Handle callback for topology change event.""" def _get_soco_group() -> list[str]: @@ -831,7 +687,7 @@ def _get_soco_group() -> list[str]: return [coordinator_uid] + slave_uids - async def _async_extract_group(event: Event) -> list[str]: + async def _async_extract_group(event: SonosEvent) -> list[str]: """Extract group layout from a topology event.""" group = event and event.zone_player_uui_ds_in_group if group: @@ -859,22 +715,18 @@ def _async_regroup(group: list[str]) -> None: # pylint: disable=protected-access slave._coordinator = self slave._sonos_group = sonos_group - slave.async_schedule_update_ha_state() + slave.async_write_ha_state() - async def _async_handle_group_event(event: Event) -> None: + async def _async_handle_group_event(event: SonosEvent) -> None: """Get async lock and handle event.""" - if event and self._poll_timer: - # Cancel poll timer since we do receive events - self._poll_timer() - self._poll_timer = None - async with self.hass.data[DATA_SONOS].topology_condition: + async with self.data.topology_condition: group = await _async_extract_group(event) if self.unique_id == group[0]: _async_regroup(group) - self.hass.data[DATA_SONOS].topology_condition.notify_all() + self.data.topology_condition.notify_all() if event and not hasattr(event, "zone_player_uui_ds_in_group"): return None @@ -882,7 +734,7 @@ async def _async_handle_group_event(event: Event) -> None: return _async_handle_group_event(event) @callback - def async_update_content(self, event: Event | None = None) -> None: + def async_update_content(self, event: SonosEvent | None = None) -> None: """Update information about available content.""" if event and "favorites_update_id" in event.variables: self.hass.async_add_job(self._set_favorites) @@ -992,12 +844,12 @@ def supported_features(self) -> int: @soco_error() def volume_up(self) -> None: """Volume up media player.""" - self._player.volume += self._volume_increment + self.soco.volume += self._volume_increment @soco_error() def volume_down(self) -> None: """Volume down media player.""" - self._player.volume -= self._volume_increment + self.soco.volume -= self._volume_increment @soco_error() def set_volume_level(self, volume: str) -> None: @@ -1054,7 +906,7 @@ def source_list(self) -> list[str]: """List of available input sources.""" sources = [fav.title for fav in self._favorites] - model = self._model.upper() + model = self.speaker.model_name.upper() if "PLAY:5" in model or "CONNECT" in model: sources += [SOURCE_LINEIN] elif "PLAYBAR" in model: @@ -1168,7 +1020,9 @@ def play_media(self, media_type: str, media_id: str, **kwargs: Any) -> None: _LOGGER.error('Sonos does not support a media type of "%s"', media_type) @soco_error() - def join(self, slaves: list[SonosEntity]) -> list[SonosEntity]: + def join( + self, slaves: list[SonosMediaPlayerEntity] + ) -> list[SonosMediaPlayerEntity]: """Form a group with other players.""" if self._coordinator: self.unjoin() @@ -1188,14 +1042,16 @@ def join(self, slaves: list[SonosEntity]) -> list[SonosEntity]: @staticmethod async def join_multi( - hass: HomeAssistant, master: SonosEntity, entities: list[SonosEntity] + hass: HomeAssistant, + master: SonosMediaPlayerEntity, + entities: list[SonosMediaPlayerEntity], ) -> None: """Form a group with other players.""" async with hass.data[DATA_SONOS].topology_condition: - group: list[SonosEntity] = await hass.async_add_executor_job( + group: list[SonosMediaPlayerEntity] = await hass.async_add_executor_job( master.join, entities ) - await SonosEntity.wait_for_groups(hass, [group]) + await SonosMediaPlayerEntity.wait_for_groups(hass, [group]) @soco_error() def unjoin(self) -> None: @@ -1204,10 +1060,12 @@ def unjoin(self) -> None: self._coordinator = None @staticmethod - async def unjoin_multi(hass: HomeAssistant, entities: list[SonosEntity]) -> None: + async def unjoin_multi( + hass: HomeAssistant, entities: list[SonosMediaPlayerEntity] + ) -> None: """Unjoin several players from their group.""" - def _unjoin_all(entities: list[SonosEntity]) -> None: + def _unjoin_all(entities: list[SonosMediaPlayerEntity]) -> None: """Sync helper.""" # Unjoin slaves first to prevent inheritance of queues coordinators = [e for e in entities if e.is_coordinator] @@ -1218,7 +1076,7 @@ def _unjoin_all(entities: list[SonosEntity]) -> None: async with hass.data[DATA_SONOS].topology_condition: await hass.async_add_executor_job(_unjoin_all, entities) - await SonosEntity.wait_for_groups(hass, [[e] for e in entities]) + await SonosMediaPlayerEntity.wait_for_groups(hass, [[e] for e in entities]) @soco_error() def snapshot(self, with_group: bool) -> None: @@ -1232,12 +1090,12 @@ def snapshot(self, with_group: bool) -> None: @staticmethod async def snapshot_multi( - hass: HomeAssistant, entities: list[SonosEntity], with_group: bool + hass: HomeAssistant, entities: list[SonosMediaPlayerEntity], with_group: bool ) -> None: """Snapshot all the entities and optionally their groups.""" # pylint: disable=protected-access - def _snapshot_all(entities: list[SonosEntity]) -> None: + def _snapshot_all(entities: list[SonosMediaPlayerEntity]) -> None: """Sync helper.""" for entity in entities: entity.snapshot(with_group) @@ -1266,14 +1124,14 @@ def restore(self) -> None: @staticmethod async def restore_multi( - hass: HomeAssistant, entities: list[SonosEntity], with_group: bool + hass: HomeAssistant, entities: list[SonosMediaPlayerEntity], with_group: bool ) -> None: """Restore snapshots for all the entities.""" # pylint: disable=protected-access def _restore_groups( - entities: list[SonosEntity], with_group: bool - ) -> list[list[SonosEntity]]: + entities: list[SonosMediaPlayerEntity], with_group: bool + ) -> list[list[SonosMediaPlayerEntity]]: """Pause all current coordinators and restore groups.""" for entity in (e for e in entities if e.is_coordinator): if entity.state == STATE_PLAYING: @@ -1296,7 +1154,7 @@ def _restore_groups( return groups - def _restore_players(entities: list[SonosEntity]) -> None: + def _restore_players(entities: list[SonosMediaPlayerEntity]) -> None: """Restore state of all players.""" for entity in (e for e in entities if not e.is_coordinator): entity.restore() @@ -1316,18 +1174,18 @@ def _restore_players(entities: list[SonosEntity]) -> None: _restore_groups, entities_set, with_group ) - await SonosEntity.wait_for_groups(hass, groups) + await SonosMediaPlayerEntity.wait_for_groups(hass, groups) await hass.async_add_executor_job(_restore_players, entities_set) @staticmethod async def wait_for_groups( - hass: HomeAssistant, groups: list[list[SonosEntity]] + hass: HomeAssistant, groups: list[list[SonosMediaPlayerEntity]] ) -> None: """Wait until all groups are present, or timeout.""" # pylint: disable=protected-access - def _test_groups(groups: list[list[SonosEntity]]) -> bool: + def _test_groups(groups: list[list[SonosMediaPlayerEntity]]) -> bool: """Return whether all groups exist now.""" for group in groups: coordinator = group[0] @@ -1350,7 +1208,7 @@ def _test_groups(groups: list[list[SonosEntity]]) -> bool: except asyncio.TimeoutError: _LOGGER.warning("Timeout waiting for target groups %s", groups) - for entity in hass.data[DATA_SONOS].entities: + for entity in hass.data[DATA_SONOS].media_player_entities.values(): entity.soco._zgs_cache.clear() @soco_error() diff --git a/homeassistant/components/sonos/sensor.py b/homeassistant/components/sonos/sensor.py new file mode 100644 index 0000000000000..67c5040a4a411 --- /dev/null +++ b/homeassistant/components/sonos/sensor.py @@ -0,0 +1,204 @@ +"""Entity representing a Sonos battery level.""" +from __future__ import annotations + +import contextlib +import datetime +import logging +from typing import Any + +from pysonos.core import SoCo +from pysonos.events_base import Event as SonosEvent +from pysonos.exceptions import SoCoException + +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE, STATE_UNKNOWN +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.icon import icon_for_battery_level +from homeassistant.util import dt as dt_util + +from . import SonosData +from .const import ( + BATTERY_SCAN_INTERVAL, + DATA_SONOS, + SONOS_DISCOVERY_UPDATE, + SONOS_ENTITY_CREATED, + SONOS_PROPERTIES_UPDATE, +) +from .entity import SonosEntity +from .speaker import SonosSpeaker + +_LOGGER = logging.getLogger(__name__) + +ATTR_BATTERY_LEVEL = "battery_level" +ATTR_BATTERY_CHARGING = "charging" +ATTR_BATTERY_POWERSOURCE = "power_source" + +EVENT_CHARGING = { + "CHARGING": True, + "NOT_CHARGING": False, +} + + +def fetch_battery_info_or_none(soco: SoCo) -> dict[str, Any] | None: + """Fetch battery_info from the given SoCo object. + + Returns None if the device doesn't support battery info + or if the device is offline. + """ + with contextlib.suppress(ConnectionError, TimeoutError, SoCoException): + return soco.get_battery_info() + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Sonos from a config entry.""" + + sonos_data = hass.data[DATA_SONOS] + + async def _async_create_entity(speaker: SonosSpeaker) -> SonosBatteryEntity | None: + if battery_info := await hass.async_add_executor_job( + fetch_battery_info_or_none, speaker.soco + ): + return SonosBatteryEntity(speaker, sonos_data, battery_info) + return None + + async def _async_create_entities(speaker: SonosSpeaker): + if entity := await _async_create_entity(speaker): + async_add_entities([entity]) + else: + async_dispatcher_send( + hass, f"{SONOS_ENTITY_CREATED}-{speaker.soco.uid}", SENSOR_DOMAIN + ) + + async_dispatcher_connect(hass, SONOS_DISCOVERY_UPDATE, _async_create_entities) + + +class SonosBatteryEntity(SonosEntity, Entity): + """Representation of a Sonos Battery entity.""" + + def __init__( + self, speaker: SonosSpeaker, sonos_data: SonosData, battery_info: dict[str, Any] + ): + """Initialize a SonosBatteryEntity.""" + super().__init__(speaker, sonos_data) + self._battery_info: dict[str, Any] = battery_info + self._last_event: datetime.datetime = None + + async def async_added_to_hass(self) -> None: + """Register polling callback when added to hass.""" + await super().async_added_to_hass() + + self.async_on_remove( + self.hass.helpers.event.async_track_time_interval( + self.async_update, BATTERY_SCAN_INTERVAL + ) + ) + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{SONOS_PROPERTIES_UPDATE}-{self.soco.uid}", + self.async_update_battery_info, + ) + ) + async_dispatcher_send( + self.hass, f"{SONOS_ENTITY_CREATED}-{self.soco.uid}", SENSOR_DOMAIN + ) + + async def async_update_battery_info(self, event: SonosEvent = None) -> None: + """Update battery info using the provided SonosEvent.""" + if event is None: + return + + if (more_info := event.variables.get("more_info")) is None: + return + + more_info_dict = dict(x.split(":") for x in more_info.split(",")) + self._last_event = dt_util.utcnow() + + is_charging = EVENT_CHARGING[more_info_dict["BattChg"]] + if is_charging == self.charging: + self._battery_info.update({"Level": int(more_info_dict["BattPct"])}) + else: + if battery_info := await self.hass.async_add_executor_job( + fetch_battery_info_or_none, self.soco + ): + self._battery_info = battery_info + + self.async_write_ha_state() + + @property + def unique_id(self) -> str: + """Return the unique ID of the sensor.""" + return f"{self.soco.uid}-battery" + + @property + def name(self) -> str: + """Return the name of the sensor.""" + return f"{self.speaker.zone_name} Battery" + + @property + def device_class(self) -> str: + """Return the entity's device class.""" + return DEVICE_CLASS_BATTERY + + @property + def unit_of_measurement(self) -> str: + """Get the unit of measurement.""" + return PERCENTAGE + + async def async_update(self, event=None) -> None: + """Poll the device for the current state.""" + if not self.available: + # wait for the Sonos device to come back online + return + + if ( + self._last_event + and dt_util.utcnow() - self._last_event < BATTERY_SCAN_INTERVAL + ): + return + + if battery_info := await self.hass.async_add_executor_job( + fetch_battery_info_or_none, self.soco + ): + self._battery_info = battery_info + self.async_write_ha_state() + + @property + def battery_level(self) -> int: + """Return the battery level.""" + return self._battery_info.get("Level", 0) + + @property + def power_source(self) -> str: + """Return the name of the power source. + + Observed to be either BATTERY or SONOS_CHARGING_RING or USB_POWER. + """ + return self._battery_info.get("PowerSource", STATE_UNKNOWN) + + @property + def charging(self) -> bool: + """Return the charging status of this battery.""" + return self.power_source not in ("BATTERY", STATE_UNKNOWN) + + @property + def icon(self) -> str: + """Return the icon of the sensor.""" + return icon_for_battery_level(self.battery_level, self.charging) + + @property + def state(self) -> int | None: + """Return the state of the sensor.""" + return self._battery_info.get("Level") + + @property + def device_state_attributes(self) -> dict[str, Any]: + """Return entity specific state attributes.""" + return { + ATTR_BATTERY_CHARGING: self.charging, + ATTR_BATTERY_POWERSOURCE: self.power_source, + } diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py new file mode 100644 index 0000000000000..b2e53755da5bd --- /dev/null +++ b/homeassistant/components/sonos/speaker.py @@ -0,0 +1,217 @@ +"""Base class for common speaker tasks.""" +from __future__ import annotations + +from asyncio import gather +import datetime +import logging +from typing import Any, Callable + +from pysonos.core import SoCo +from pysonos.events_base import Event as SonosEvent, SubscriptionBase +from pysonos.exceptions import SoCoException + +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import ( + async_dispatcher_send, + dispatcher_connect, + dispatcher_send, +) + +from .const import ( + PLATFORMS, + SCAN_INTERVAL, + SEEN_EXPIRE_TIME, + SONOS_CONTENT_UPDATE, + SONOS_DISCOVERY_UPDATE, + SONOS_ENTITY_CREATED, + SONOS_ENTITY_UPDATE, + SONOS_GROUP_UPDATE, + SONOS_MEDIA_UPDATE, + SONOS_PLAYER_RECONNECTED, + SONOS_PROPERTIES_UPDATE, + SONOS_SEEN, + SONOS_STATE_UPDATED, + SONOS_VOLUME_UPDATE, +) + +_LOGGER = logging.getLogger(__name__) + + +class SonosSpeaker: + """Representation of a Sonos speaker.""" + + def __init__(self, hass: HomeAssistant, soco: SoCo, speaker_info: dict[str, Any]): + """Initialize a SonosSpeaker.""" + self._is_ready: bool = False + self._subscriptions: list[SubscriptionBase] = [] + self._poll_timer: Callable | None = None + self._seen_timer: Callable | None = None + self._seen_dispatcher: Callable | None = None + self._entity_creation_dispatcher: Callable | None = None + self._platforms_ready: set[str] = set() + + self.hass: HomeAssistant = hass + self.soco: SoCo = soco + + self.mac_address = speaker_info["mac_address"] + self.model_name = speaker_info["model_name"] + self.version = speaker_info["software_version"] + self.zone_name = speaker_info["zone_name"] + + def setup(self) -> None: + """Run initial setup of the speaker.""" + self._entity_creation_dispatcher = dispatcher_connect( + self.hass, + f"{SONOS_ENTITY_CREATED}-{self.soco.uid}", + self.async_handle_new_entity, + ) + self._seen_dispatcher = dispatcher_connect( + self.hass, f"{SONOS_SEEN}-{self.soco.uid}", self.async_seen + ) + dispatcher_send(self.hass, SONOS_DISCOVERY_UPDATE, self) + + async def async_handle_new_entity(self, entity_type: str) -> None: + """Listen to new entities to trigger first subscription.""" + self._platforms_ready.add(entity_type) + if self._platforms_ready == PLATFORMS: + await self.async_subscribe() + self._is_ready = True + + @callback + def async_write_entity_states(self) -> bool: + """Write states for associated SonosEntity instances.""" + async_dispatcher_send(self.hass, f"{SONOS_STATE_UPDATED}-{self.soco.uid}") + + @property + def available(self) -> bool: + """Return whether this speaker is available.""" + return self._seen_timer is not None + + async def async_subscribe(self) -> bool: + """Initiate event subscriptions.""" + _LOGGER.debug("Creating subscriptions for %s", self.zone_name) + try: + self.async_dispatch_player_reconnected() + + if self._subscriptions: + raise RuntimeError( + f"Attempted to attach subscriptions to player: {self.soco} " + f"when existing subscriptions exist: {self._subscriptions}" + ) + + await gather( + self._subscribe(self.soco.avTransport, self.async_dispatch_media), + self._subscribe(self.soco.renderingControl, self.async_dispatch_volume), + self._subscribe( + self.soco.contentDirectory, self.async_dispatch_content + ), + self._subscribe( + self.soco.zoneGroupTopology, self.async_dispatch_groups + ), + self._subscribe( + self.soco.deviceProperties, self.async_dispatch_properties + ), + ) + return True + except SoCoException as ex: + _LOGGER.warning("Could not connect %s: %s", self.zone_name, ex) + return False + + async def _subscribe( + self, target: SubscriptionBase, sub_callback: Callable + ) -> None: + """Create a Sonos subscription.""" + subscription = await target.subscribe(auto_renew=True) + subscription.callback = sub_callback + self._subscriptions.append(subscription) + + @callback + def async_dispatch_media(self, event: SonosEvent | None = None) -> None: + """Update currently playing media from event.""" + async_dispatcher_send(self.hass, f"{SONOS_MEDIA_UPDATE}-{self.soco.uid}", event) + + @callback + def async_dispatch_content(self, event: SonosEvent | None = None) -> None: + """Update available content from event.""" + async_dispatcher_send( + self.hass, f"{SONOS_CONTENT_UPDATE}-{self.soco.uid}", event + ) + + @callback + def async_dispatch_volume(self, event: SonosEvent | None = None) -> None: + """Update volume from event.""" + async_dispatcher_send( + self.hass, f"{SONOS_VOLUME_UPDATE}-{self.soco.uid}", event + ) + + @callback + def async_dispatch_properties(self, event: SonosEvent | None = None) -> None: + """Update properties from event.""" + async_dispatcher_send( + self.hass, f"{SONOS_PROPERTIES_UPDATE}-{self.soco.uid}", event + ) + + @callback + def async_dispatch_groups(self, event: SonosEvent | None = None) -> None: + """Update groups from event.""" + if event and self._poll_timer: + _LOGGER.debug( + "Received event, cancelling poll timer for %s", self.zone_name + ) + self._poll_timer() + self._poll_timer = None + + async_dispatcher_send(self.hass, SONOS_GROUP_UPDATE, event) + + @callback + def async_dispatch_player_reconnected(self) -> None: + """Signal that player has been reconnected.""" + async_dispatcher_send(self.hass, f"{SONOS_PLAYER_RECONNECTED}-{self.soco.uid}") + + async def async_seen(self, soco: SoCo | None = None) -> None: + """Record that this speaker was seen right now.""" + if soco is not None: + self.soco = soco + + was_available = self.available + _LOGGER.debug("Async seen: %s, was_available: %s", self.soco, was_available) + + if self._seen_timer: + self._seen_timer() + + self._seen_timer = self.hass.helpers.event.async_call_later( + SEEN_EXPIRE_TIME.total_seconds(), self.async_unseen + ) + + if was_available: + self.async_write_entity_states() + return + + self._poll_timer = self.hass.helpers.event.async_track_time_interval( + async_dispatcher_send(self.hass, f"{SONOS_ENTITY_UPDATE}-{self.soco.uid}"), + SCAN_INTERVAL, + ) + + if self._is_ready: + done = await self.async_subscribe() + if not done: + assert self._seen_timer is not None + self._seen_timer() + await self.async_unseen() + + self.async_write_entity_states() + + async def async_unseen(self, now: datetime.datetime | None = None) -> None: + """Make this player unavailable when it was not seen recently.""" + self.async_write_entity_states() + + self._seen_timer = None + + if self._poll_timer: + self._poll_timer() + self._poll_timer = None + + for subscription in self._subscriptions: + await subscription.unsubscribe() + + self._subscriptions = [] diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index 3562d991e985b..f22c462f8818d 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -17,7 +17,7 @@ def config_entry_fixture(): @pytest.fixture(name="soco") -def soco_fixture(music_library, speaker_info, dummy_soco_service): +def soco_fixture(music_library, speaker_info, battery_info, dummy_soco_service): """Create a mock pysonos SoCo fixture.""" with patch("pysonos.SoCo", autospec=True) as mock, patch( "socket.gethostbyname", return_value="192.168.42.2" @@ -31,10 +31,12 @@ def soco_fixture(music_library, speaker_info, dummy_soco_service): mock_soco.renderingControl = dummy_soco_service mock_soco.zoneGroupTopology = dummy_soco_service mock_soco.contentDirectory = dummy_soco_service + mock_soco.deviceProperties = dummy_soco_service mock_soco.mute = False mock_soco.night_mode = True mock_soco.dialog_mode = True mock_soco.volume = 19 + mock_soco.get_battery_info.return_value = battery_info yield mock_soco @@ -82,3 +84,14 @@ def speaker_info_fixture(): "software_version": "49.2-64250", "mac_address": "00-11-22-33-44-55", } + + +@pytest.fixture(name="battery_info") +def battery_info_fixture(): + """Create battery_info fixture.""" + return { + "Health": "GREEN", + "Level": 100, + "Temperature": "NORMAL", + "PowerSource": "SONOS_CHARGING_RING", + } diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index d5b0158d6c446..af8437cd41779 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -20,7 +20,8 @@ async def test_async_setup_entry_hosts(hass, config_entry, config, soco): """Test static setup.""" await setup_platform(hass, config_entry, config) - entity = hass.data[media_player.DATA_SONOS].entities[0] + entities = list(hass.data[media_player.DATA_SONOS].media_player_entities.values()) + entity = entities[0] assert entity.soco == soco @@ -28,7 +29,8 @@ async def test_async_setup_entry_discover(hass, config_entry, discover): """Test discovery setup.""" await setup_platform(hass, config_entry, {}) - entity = hass.data[media_player.DATA_SONOS].entities[0] + entities = list(hass.data[media_player.DATA_SONOS].media_player_entities.values()) + entity = entities[0] assert entity.unique_id == "RINCON_test" diff --git a/tests/components/sonos/test_sensor.py b/tests/components/sonos/test_sensor.py new file mode 100644 index 0000000000000..3752af7f377d6 --- /dev/null +++ b/tests/components/sonos/test_sensor.py @@ -0,0 +1,62 @@ +"""Tests for the Sonos battery sensor platform.""" +from pysonos.exceptions import NotSupportedException + +from homeassistant.components.sonos import DOMAIN +from homeassistant.setup import async_setup_component + + +async def setup_platform(hass, config_entry, config): + """Set up the media player platform for testing.""" + config_entry.add_to_hass(hass) + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + + +async def test_entity_registry_unsupported(hass, config_entry, config, soco): + """Test sonos device without battery registered in the device registry.""" + soco.get_battery_info.side_effect = NotSupportedException + + await setup_platform(hass, config_entry, config) + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + assert "media_player.zone_a" in entity_registry.entities + assert "sensor.zone_a_battery" not in entity_registry.entities + + +async def test_entity_registry_supported(hass, config_entry, config, soco): + """Test sonos device with battery registered in the device registry.""" + await setup_platform(hass, config_entry, config) + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + assert "media_player.zone_a" in entity_registry.entities + assert "sensor.zone_a_battery" in entity_registry.entities + + +async def test_battery_missing_attributes(hass, config_entry, config, soco): + """Test sonos device with unknown battery state.""" + soco.get_battery_info.return_value = {} + + await setup_platform(hass, config_entry, config) + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + assert entity_registry.entities.get("sensor.zone_a_battery") is None + + +async def test_battery_attributes(hass, config_entry, config, soco): + """Test sonos device with battery state.""" + await setup_platform(hass, config_entry, config) + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + battery = entity_registry.entities["sensor.zone_a_battery"] + battery_state = hass.states.get(battery.entity_id) + + # confirm initial state from conftest + assert battery_state.state == "100" + assert battery_state.attributes.get("unit_of_measurement") == "%" + assert battery_state.attributes.get("icon") == "mdi:battery-charging-100" + assert battery_state.attributes.get("charging") + assert battery_state.attributes.get("power_source") == "SONOS_CHARGING_RING" From 510a3ae9154144a120c24d58ea0f7822f8927c0d Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 25 Apr 2021 20:16:38 +0200 Subject: [PATCH 0515/1317] Improve zeroconf test fixture (#49657) --- tests/components/conftest.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tests/components/conftest.py b/tests/components/conftest.py index 3b1781ba510de..9e029e159a12f 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -3,12 +3,15 @@ import pytest -from homeassistant.components import zeroconf -zeroconf.orig_install_multiple_zeroconf_catcher = ( - zeroconf.install_multiple_zeroconf_catcher -) -zeroconf.install_multiple_zeroconf_catcher = lambda zc: None +@pytest.fixture(scope="session", autouse=True) +def patch_zeroconf_multiple_catcher(): + """Patch zeroconf wrapper that detects if multiple instances are used.""" + with patch( + "homeassistant.components.zeroconf.install_multiple_zeroconf_catcher", + side_effect=lambda zc: None, + ): + yield @pytest.fixture(autouse=True) From 7b33ed11c2e5257ce1f040a00ae3315edf708a5b Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 25 Apr 2021 20:28:40 +0200 Subject: [PATCH 0516/1317] Fix missing default value in fritz scan_devices (#49668) --- homeassistant/components/fritz/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index f05fea0dcd499..1958dd51f38ca 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -135,7 +135,7 @@ def _update_info(self): """Retrieve latest information from the FRITZ!Box.""" return self.fritzhosts.get_hosts_info() - def scan_devices(self, now: datetime | None) -> None: + def scan_devices(self, now: datetime | None = None) -> None: """Scan for new devices and return a list of found device ids.""" _LOGGER.debug("Checking devices for FRITZ!Box router %s", self.host) From 85438db1eca3b539f9aef17c12f1c89da6c1f488 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sun, 25 Apr 2021 21:07:31 +0200 Subject: [PATCH 0517/1317] Fix Fritz unload (#49669) --- homeassistant/components/fritz/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 1958dd51f38ca..45f211b352d17 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -94,7 +94,7 @@ async def async_start(self): ) @callback - async def async_unload(self): + def async_unload(self): """Unload FritzboxTools class.""" _LOGGER.debug("Unloading FRITZ!Box router integration") if self._cancel_scan is not None: From 631ab367e2ff3827b4b83e7bf1582d9ae7451031 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sun, 25 Apr 2021 22:36:21 +0300 Subject: [PATCH 0518/1317] Fix typing.Any spelling (#49673) --- .../components/asuswrt/device_tracker.py | 6 ++++-- homeassistant/components/asuswrt/sensor.py | 7 ++++--- homeassistant/components/dsmr/sensor.py | 3 ++- .../components/freebox/device_tracker.py | 7 ++++--- homeassistant/components/freebox/sensor.py | 17 +++++++++-------- homeassistant/components/freebox/switch.py | 3 ++- homeassistant/components/icloud/account.py | 7 ++++--- .../components/icloud/device_tracker.py | 6 ++++-- homeassistant/components/icloud/sensor.py | 6 ++++-- homeassistant/components/plugwise/gateway.py | 3 ++- .../components/synology_dsm/__init__.py | 7 ++++--- homeassistant/components/synology_dsm/camera.py | 3 ++- homeassistant/components/synology_dsm/switch.py | 3 ++- homeassistant/components/upnp/device.py | 3 ++- tests/components/mysensors/test_config_flow.py | 3 ++- tests/components/mysensors/test_init.py | 3 ++- tests/components/upnp/mock_device.py | 4 ++-- 17 files changed, 55 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/asuswrt/device_tracker.py b/homeassistant/components/asuswrt/device_tracker.py index dabbc25ba107e..abaa6c1965d73 100644 --- a/homeassistant/components/asuswrt/device_tracker.py +++ b/homeassistant/components/asuswrt/device_tracker.py @@ -1,6 +1,8 @@ """Support for ASUSWRT routers.""" from __future__ import annotations +from typing import Any + from homeassistant.components.device_tracker import SOURCE_TYPE_ROUTER from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.config_entries import ConfigEntry @@ -78,7 +80,7 @@ def source_type(self) -> str: return SOURCE_TYPE_ROUTER @property - def extra_state_attributes(self) -> dict[str, any]: + def extra_state_attributes(self) -> dict[str, Any]: """Return the attributes.""" attrs = {} if self._device.last_activity: @@ -103,7 +105,7 @@ def mac_address(self) -> str: return self._device.mac @property - def device_info(self) -> dict[str, any]: + def device_info(self) -> dict[str, Any]: """Return the device information.""" data = { "connections": {(CONNECTION_NETWORK_MAC, self._device.mac)}, diff --git a/homeassistant/components/asuswrt/sensor.py b/homeassistant/components/asuswrt/sensor.py index a1a9b2ff3e8ea..7a3ffccc00b8d 100644 --- a/homeassistant/components/asuswrt/sensor.py +++ b/homeassistant/components/asuswrt/sensor.py @@ -3,6 +3,7 @@ import logging from numbers import Number +from typing import Any from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry @@ -106,7 +107,7 @@ def __init__( coordinator: DataUpdateCoordinator, router: AsusWrtRouter, sensor_type: str, - sensor: dict[str, any], + sensor: dict[str, Any], ) -> None: """Initialize a AsusWrt sensor.""" super().__init__(coordinator) @@ -161,11 +162,11 @@ def device_class(self) -> str: return self._device_class @property - def extra_state_attributes(self) -> dict[str, any]: + def extra_state_attributes(self) -> dict[str, Any]: """Return the attributes.""" return {"hostname": self._router.host} @property - def device_info(self) -> dict[str, any]: + def device_info(self) -> dict[str, Any]: """Return the device information.""" return self._router.device_info diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 656c066b980bd..3885302329a21 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -7,6 +7,7 @@ from datetime import timedelta from functools import partial import logging +from typing import Any from dsmr_parser import obis_references as obis_ref from dsmr_parser.clients.protocol import create_dsmr_reader, create_tcp_dsmr_reader @@ -361,7 +362,7 @@ def unique_id(self) -> str: return self._unique_id @property - def device_info(self) -> dict[str, any]: + def device_info(self) -> dict[str, Any]: """Return the device information.""" return { "identifiers": {(DOMAIN, self._device_serial)}, diff --git a/homeassistant/components/freebox/device_tracker.py b/homeassistant/components/freebox/device_tracker.py index 7485c9da85676..d2814a1c12613 100644 --- a/homeassistant/components/freebox/device_tracker.py +++ b/homeassistant/components/freebox/device_tracker.py @@ -2,6 +2,7 @@ from __future__ import annotations from datetime import datetime +from typing import Any from homeassistant.components.device_tracker import SOURCE_TYPE_ROUTER from homeassistant.components.device_tracker.config_entry import ScannerEntity @@ -52,7 +53,7 @@ def add_entities(router, async_add_entities, tracked): class FreeboxDevice(ScannerEntity): """Representation of a Freebox device.""" - def __init__(self, router: FreeboxRouter, device: dict[str, any]) -> None: + def __init__(self, router: FreeboxRouter, device: dict[str, Any]) -> None: """Initialize a Freebox device.""" self._router = router self._name = device["primary_name"].strip() or DEFAULT_DEVICE_NAME @@ -105,12 +106,12 @@ def icon(self) -> str: return self._icon @property - def extra_state_attributes(self) -> dict[str, any]: + def extra_state_attributes(self) -> dict[str, Any]: """Return the attributes.""" return self._attrs @property - def device_info(self) -> dict[str, any]: + def device_info(self) -> dict[str, Any]: """Return the device information.""" return { "connections": {(CONNECTION_NETWORK_MAC, self._mac)}, diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py index c121974f1faa4..8f097b2d73a11 100644 --- a/homeassistant/components/freebox/sensor.py +++ b/homeassistant/components/freebox/sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry @@ -77,7 +78,7 @@ class FreeboxSensor(SensorEntity): """Representation of a Freebox sensor.""" def __init__( - self, router: FreeboxRouter, sensor_type: str, sensor: dict[str, any] + self, router: FreeboxRouter, sensor_type: str, sensor: dict[str, Any] ) -> None: """Initialize a Freebox sensor.""" self._state = None @@ -129,7 +130,7 @@ def device_class(self) -> str: return self._device_class @property - def device_info(self) -> dict[str, any]: + def device_info(self) -> dict[str, Any]: """Return the device information.""" return self._router.device_info @@ -160,7 +161,7 @@ class FreeboxCallSensor(FreeboxSensor): """Representation of a Freebox call sensor.""" def __init__( - self, router: FreeboxRouter, sensor_type: str, sensor: dict[str, any] + self, router: FreeboxRouter, sensor_type: str, sensor: dict[str, Any] ) -> None: """Initialize a Freebox call sensor.""" super().__init__(router, sensor_type, sensor) @@ -180,7 +181,7 @@ def async_update_state(self) -> None: self._state = len(self._call_list_for_type) @property - def extra_state_attributes(self) -> dict[str, any]: + def extra_state_attributes(self) -> dict[str, Any]: """Return device specific state attributes.""" return { dt_util.utc_from_timestamp(call["datetime"]).isoformat(): call["name"] @@ -194,10 +195,10 @@ class FreeboxDiskSensor(FreeboxSensor): def __init__( self, router: FreeboxRouter, - disk: dict[str, any], - partition: dict[str, any], + disk: dict[str, Any], + partition: dict[str, Any], sensor_type: str, - sensor: dict[str, any], + sensor: dict[str, Any], ) -> None: """Initialize a Freebox disk sensor.""" super().__init__(router, sensor_type, sensor) @@ -207,7 +208,7 @@ def __init__( self._unique_id = f"{self._router.mac} {sensor_type} {self._disk['id']} {self._partition['id']}" @property - def device_info(self) -> dict[str, any]: + def device_info(self) -> dict[str, Any]: """Return the device information.""" return { "identifiers": {(DOMAIN, self._disk["id"])}, diff --git a/homeassistant/components/freebox/switch.py b/homeassistant/components/freebox/switch.py index f309524ceb44e..ebe573be9edde 100644 --- a/homeassistant/components/freebox/switch.py +++ b/homeassistant/components/freebox/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from freebox_api.exceptions import InsufficientPermissionsError @@ -49,7 +50,7 @@ def is_on(self) -> bool: return self._state @property - def device_info(self) -> dict[str, any]: + def device_info(self) -> dict[str, Any]: """Return the device information.""" return self._router.device_info diff --git a/homeassistant/components/icloud/account.py b/homeassistant/components/icloud/account.py index 55fd661768d62..5a33b5d950864 100644 --- a/homeassistant/components/icloud/account.py +++ b/homeassistant/components/icloud/account.py @@ -4,6 +4,7 @@ from datetime import timedelta import logging import operator +from typing import Any from pyicloud import PyiCloudService from pyicloud.exceptions import ( @@ -355,7 +356,7 @@ def fetch_interval(self) -> int: return self._fetch_interval @property - def devices(self) -> dict[str, any]: + def devices(self) -> dict[str, Any]: """Return the account devices.""" return self._devices @@ -496,11 +497,11 @@ def battery_status(self) -> str: return self._battery_status @property - def location(self) -> dict[str, any]: + def location(self) -> dict[str, Any]: """Return the Apple device location.""" return self._location @property - def extra_state_attributes(self) -> dict[str, any]: + def extra_state_attributes(self) -> dict[str, Any]: """Return the attributes.""" return self._attrs diff --git a/homeassistant/components/icloud/device_tracker.py b/homeassistant/components/icloud/device_tracker.py index 131f9335b4301..0615d6fcc7fd0 100644 --- a/homeassistant/components/icloud/device_tracker.py +++ b/homeassistant/components/icloud/device_tracker.py @@ -1,6 +1,8 @@ """Support for tracking for iCloud devices.""" from __future__ import annotations +from typing import Any + from homeassistant.components.device_tracker import SOURCE_TYPE_GPS from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.config_entries import ConfigEntry @@ -105,12 +107,12 @@ def icon(self) -> str: return icon_for_icloud_device(self._device) @property - def extra_state_attributes(self) -> dict[str, any]: + def extra_state_attributes(self) -> dict[str, Any]: """Return the device state attributes.""" return self._device.extra_state_attributes @property - def device_info(self) -> dict[str, any]: + def device_info(self) -> dict[str, Any]: """Return the device information.""" return { "identifiers": {(DOMAIN, self._device.unique_id)}, diff --git a/homeassistant/components/icloud/sensor.py b/homeassistant/components/icloud/sensor.py index 3a875db81ed1a..7c13171688e35 100644 --- a/homeassistant/components/icloud/sensor.py +++ b/homeassistant/components/icloud/sensor.py @@ -1,6 +1,8 @@ """Support for iCloud sensors.""" from __future__ import annotations +from typing import Any + from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE @@ -90,12 +92,12 @@ def icon(self) -> str: ) @property - def extra_state_attributes(self) -> dict[str, any]: + def extra_state_attributes(self) -> dict[str, Any]: """Return default attributes for the iCloud device entity.""" return self._device.extra_state_attributes @property - def device_info(self) -> dict[str, any]: + def device_info(self) -> dict[str, Any]: """Return the device information.""" return { "identifiers": {(DOMAIN, self._device.unique_id)}, diff --git a/homeassistant/components/plugwise/gateway.py b/homeassistant/components/plugwise/gateway.py index 70a4a822431b7..3f805f1475d0f 100644 --- a/homeassistant/components/plugwise/gateway.py +++ b/homeassistant/components/plugwise/gateway.py @@ -4,6 +4,7 @@ import asyncio from datetime import timedelta import logging +from typing import Any import async_timeout from plugwise.exceptions import ( @@ -197,7 +198,7 @@ def name(self): return self._name @property - def device_info(self) -> dict[str, any]: + def device_info(self) -> dict[str, Any]: """Return the device information.""" device_information = { "identifiers": {(DOMAIN, self._dev_id)}, diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index 74cf8775b1c9c..cdfad25e97237 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -4,6 +4,7 @@ import asyncio from datetime import timedelta import logging +from typing import Any import async_timeout from synology_dsm import SynologyDSM @@ -626,12 +627,12 @@ def device_class(self) -> str: return self._class @property - def extra_state_attributes(self) -> dict[str, any]: + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" return {ATTR_ATTRIBUTION: ATTRIBUTION} @property - def device_info(self) -> dict[str, any]: + def device_info(self) -> dict[str, Any]: """Return the device information.""" return { "identifiers": {(DOMAIN, self._api.information.serial)}, @@ -701,7 +702,7 @@ def available(self) -> bool: return bool(self._api.storage) @property - def device_info(self) -> dict[str, any]: + def device_info(self) -> dict[str, Any]: """Return the device information.""" return { "identifiers": {(DOMAIN, self._api.information.serial, self._device_id)}, diff --git a/homeassistant/components/synology_dsm/camera.py b/homeassistant/components/synology_dsm/camera.py index cdd4b88186a93..80cf70de8a916 100644 --- a/homeassistant/components/synology_dsm/camera.py +++ b/homeassistant/components/synology_dsm/camera.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from synology_dsm.api.surveillance_station import SynoSurveillanceStation from synology_dsm.exceptions import ( @@ -80,7 +81,7 @@ def camera_data(self): return self.coordinator.data["cameras"][self._camera_id] @property - def device_info(self) -> dict[str, any]: + def device_info(self) -> dict[str, Any]: """Return the device information.""" return { "identifiers": { diff --git a/homeassistant/components/synology_dsm/switch.py b/homeassistant/components/synology_dsm/switch.py index 3b71e481d6e46..51736663d50fe 100644 --- a/homeassistant/components/synology_dsm/switch.py +++ b/homeassistant/components/synology_dsm/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from synology_dsm.api.surveillance_station import SynoSurveillanceStation @@ -96,7 +97,7 @@ def available(self) -> bool: return bool(self._api.surveillance_station) @property - def device_info(self) -> dict[str, any]: + def device_info(self) -> dict[str, Any]: """Return the device information.""" return { "identifiers": { diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index e5b6099e9f385..496293926d3e6 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -4,6 +4,7 @@ import asyncio from collections.abc import Mapping from ipaddress import IPv4Address +from typing import Any from urllib.parse import urlparse from async_upnp_client import UpnpFactory @@ -162,7 +163,7 @@ def __str__(self) -> str: """Get string representation.""" return f"IGD Device: {self.name}/{self.udn}::{self.device_type}" - async def async_get_traffic_data(self) -> Mapping[str, any]: + async def async_get_traffic_data(self) -> Mapping[str, Any]: """ Get all traffic data in one go. diff --git a/tests/components/mysensors/test_config_flow.py b/tests/components/mysensors/test_config_flow.py index dfad2b50558f4..66900066cd148 100644 --- a/tests/components/mysensors/test_config_flow.py +++ b/tests/components/mysensors/test_config_flow.py @@ -1,6 +1,7 @@ """Test the MySensors config flow.""" from __future__ import annotations +from typing import Any from unittest.mock import patch import pytest @@ -369,7 +370,7 @@ async def test_config_invalid( mqtt: config_entries.ConfigEntry, gateway_type: ConfGatewayType, expected_step_id: str, - user_input: dict[str, any], + user_input: dict[str, Any], err_field, err_string, ) -> None: diff --git a/tests/components/mysensors/test_init.py b/tests/components/mysensors/test_init.py index 30fbf3ea686bb..4fb51d6c17a10 100644 --- a/tests/components/mysensors/test_init.py +++ b/tests/components/mysensors/test_init.py @@ -1,6 +1,7 @@ """Test function in __init__.py.""" from __future__ import annotations +from typing import Any from unittest.mock import patch import pytest @@ -232,7 +233,7 @@ async def test_import( config: ConfigType, expected_calls: int, expected_to_succeed: bool, - expected_config_flow_user_input: dict[str, any], + expected_config_flow_user_input: dict[str, Any], ) -> None: """Test importing a gateway.""" await async_setup_component(hass, "persistent_notification", {}) diff --git a/tests/components/upnp/mock_device.py b/tests/components/upnp/mock_device.py index d2ef9ad41e3bc..7161ae6959856 100644 --- a/tests/components/upnp/mock_device.py +++ b/tests/components/upnp/mock_device.py @@ -1,6 +1,6 @@ """Mock device for testing purposes.""" -from typing import Mapping +from typing import Any, Mapping from unittest.mock import AsyncMock from homeassistant.components.upnp.const import ( @@ -60,7 +60,7 @@ def hostname(self) -> str: """Get the hostname.""" return "mock-hostname" - async def async_get_traffic_data(self) -> Mapping[str, any]: + async def async_get_traffic_data(self) -> Mapping[str, Any]: """Get traffic data.""" self.times_polled += 1 return { From a5e25e519fba091a5790ed8237ef66eda9e962ee Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 25 Apr 2021 21:49:08 +0200 Subject: [PATCH 0519/1317] Remove yaml configuration from fritzbox (#49663) --- CODEOWNERS | 1 + homeassistant/components/fritzbox/__init__.py | 68 +-------------- .../components/fritzbox/config_flow.py | 4 - .../components/fritzbox/manifest.json | 2 +- tests/components/fritzbox/__init__.py | 28 ++++++ tests/components/fritzbox/conftest.py | 7 +- .../components/fritzbox/test_binary_sensor.py | 34 +++----- tests/components/fritzbox/test_climate.py | 85 +++++++++---------- tests/components/fritzbox/test_config_flow.py | 20 +---- tests/components/fritzbox/test_init.py | 30 ++----- tests/components/fritzbox/test_sensor.py | 29 +++---- tests/components/fritzbox/test_switch.py | 40 ++++----- 12 files changed, 126 insertions(+), 222 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 976a5c7d6eff5..4bd020ffb1225 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -165,6 +165,7 @@ homeassistant/components/fortios/* @kimfrellsen homeassistant/components/foscam/* @skgsergio homeassistant/components/freebox/* @hacf-fr @Quentame homeassistant/components/fritz/* @mammuth @AaronDavidSchneider @chemelli74 +homeassistant/components/fritzbox/* @mib1185 homeassistant/components/fronius/* @nielstron homeassistant/components/frontend/* @home-assistant/frontend homeassistant/components/garmin_connect/* @cyberjunky diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index 7201c171c6a21..b398a1ee775ad 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -3,19 +3,16 @@ import asyncio from datetime import timedelta -import socket from pyfritzhome import Fritzhome, FritzhomeDevice, LoginError import requests -import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, ATTR_NAME, ATTR_UNIT_OF_MEASUREMENT, - CONF_DEVICES, CONF_HOST, CONF_PASSWORD, CONF_USERNAME, @@ -23,73 +20,12 @@ ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, ) -from .const import ( - CONF_CONNECTIONS, - CONF_COORDINATOR, - DEFAULT_HOST, - DEFAULT_USERNAME, - DOMAIN, - LOGGER, - PLATFORMS, -) - - -def ensure_unique_hosts(value): - """Validate that all configs have a unique host.""" - vol.Schema(vol.Unique("duplicate host entries found"))( - [socket.gethostbyname(entry[CONF_HOST]) for entry in value] - ) - return value - - -CONFIG_SCHEMA = vol.Schema( - vol.All( - cv.deprecated(DOMAIN), - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_DEVICES): vol.All( - cv.ensure_list, - [ - vol.Schema( - { - vol.Required( - CONF_HOST, default=DEFAULT_HOST - ): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required( - CONF_USERNAME, default=DEFAULT_USERNAME - ): cv.string, - } - ) - ], - ensure_unique_hosts, - ) - } - ) - }, - ), - extra=vol.ALLOW_EXTRA, -) - - -async def async_setup(hass: HomeAssistant, config: dict[str, str]) -> bool: - """Set up the AVM Fritz!Box integration.""" - if DOMAIN in config: - for entry_config in config[DOMAIN][CONF_DEVICES]: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=entry_config - ) - ) - - return True +from .const import CONF_CONNECTIONS, CONF_COORDINATOR, DOMAIN, LOGGER, PLATFORMS async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/fritzbox/config_flow.py b/homeassistant/components/fritzbox/config_flow.py index 6a200ff22e48e..2472e50278701 100644 --- a/homeassistant/components/fritzbox/config_flow.py +++ b/homeassistant/components/fritzbox/config_flow.py @@ -88,10 +88,6 @@ def _try_connect(self): except OSError: return RESULT_NO_DEVICES_FOUND - async def async_step_import(self, user_input=None): - """Handle configuration by yaml file.""" - return await self.async_step_user(user_input) - async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" errors = {} diff --git a/homeassistant/components/fritzbox/manifest.json b/homeassistant/components/fritzbox/manifest.json index 4a56d68e1700e..3daecb1980dcf 100644 --- a/homeassistant/components/fritzbox/manifest.json +++ b/homeassistant/components/fritzbox/manifest.json @@ -8,7 +8,7 @@ "st": "urn:schemas-upnp-org:device:fritzbox:1" } ], - "codeowners": [], + "codeowners": ["@mib1185"], "config_flow": true, "iot_class": "local_polling" } diff --git a/tests/components/fritzbox/__init__.py b/tests/components/fritzbox/__init__.py index 8e0932b9000b3..ee5d15bd1b84e 100644 --- a/tests/components/fritzbox/__init__.py +++ b/tests/components/fritzbox/__init__.py @@ -1,8 +1,14 @@ """Tests for the AVM Fritz!Box integration.""" +from __future__ import annotations + +from typing import Any from unittest.mock import Mock from homeassistant.components.fritzbox.const import DOMAIN from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry MOCK_CONFIG = { DOMAIN: { @@ -17,6 +23,28 @@ } +async def setup_config_entry( + hass: HomeAssistant, + data: dict[str, Any], + unique_id: str = "any", + device: Mock = None, + fritz: Mock = None, +) -> bool: + """Do setup of a MockConfigEntry.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=data, + unique_id=unique_id, + ) + entry.add_to_hass(hass) + if device is not None and fritz is not None: + fritz().get_devices.return_value = [device] + result = await hass.config_entries.async_setup(entry.entry_id) + if device is not None: + await hass.async_block_till_done() + return result + + class FritzDeviceBinarySensorMock(Mock): """Mock of a AVM Fritz!Box binary sensor device.""" diff --git a/tests/components/fritzbox/conftest.py b/tests/components/fritzbox/conftest.py index 591c10375256d..50fca4581b35f 100644 --- a/tests/components/fritzbox/conftest.py +++ b/tests/components/fritzbox/conftest.py @@ -7,8 +7,7 @@ @pytest.fixture(name="fritz") def fritz_fixture() -> Mock: """Patch libraries.""" - with patch("homeassistant.components.fritzbox.socket") as socket, patch( - "homeassistant.components.fritzbox.Fritzhome" - ) as fritz, patch("homeassistant.components.fritzbox.config_flow.Fritzhome"): - socket.gethostbyname.return_value = "FAKE_IP_ADDRESS" + with patch("homeassistant.components.fritzbox.Fritzhome") as fritz, patch( + "homeassistant.components.fritzbox.config_flow.Fritzhome" + ): yield fritz diff --git a/tests/components/fritzbox/test_binary_sensor.py b/tests/components/fritzbox/test_binary_sensor.py index f3334086d7930..7a2d234700452 100644 --- a/tests/components/fritzbox/test_binary_sensor.py +++ b/tests/components/fritzbox/test_binary_sensor.py @@ -10,34 +10,28 @@ from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, + CONF_DEVICES, STATE_OFF, STATE_ON, ) from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from . import MOCK_CONFIG, FritzDeviceBinarySensorMock +from . import MOCK_CONFIG, FritzDeviceBinarySensorMock, setup_config_entry from tests.common import async_fire_time_changed ENTITY_ID = f"{DOMAIN}.fake_name" -async def setup_fritzbox(hass: HomeAssistant, config: dict): - """Set up mock AVM Fritz!Box.""" - assert await async_setup_component(hass, FB_DOMAIN, config) - await hass.async_block_till_done() - - async def test_setup(hass: HomeAssistant, fritz: Mock): """Test setup of platform.""" device = FritzDeviceBinarySensorMock() - fritz().get_devices.return_value = [device] + assert await setup_config_entry( + hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz + ) - await setup_fritzbox(hass, MOCK_CONFIG) state = hass.states.get(ENTITY_ID) - assert state assert state.state == STATE_ON assert state.attributes[ATTR_FRIENDLY_NAME] == "fake_name" @@ -48,11 +42,11 @@ async def test_is_off(hass: HomeAssistant, fritz: Mock): """Test state of platform.""" device = FritzDeviceBinarySensorMock() device.present = False - fritz().get_devices.return_value = [device] + assert await setup_config_entry( + hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz + ) - await setup_fritzbox(hass, MOCK_CONFIG) state = hass.states.get(ENTITY_ID) - assert state assert state.state == STATE_OFF @@ -60,9 +54,9 @@ async def test_is_off(hass: HomeAssistant, fritz: Mock): async def test_update(hass: HomeAssistant, fritz: Mock): """Test update without error.""" device = FritzDeviceBinarySensorMock() - fritz().get_devices.return_value = [device] - - await setup_fritzbox(hass, MOCK_CONFIG) + assert await setup_config_entry( + hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz + ) assert device.update.call_count == 1 assert fritz().login.call_count == 1 @@ -79,9 +73,9 @@ async def test_update_error(hass: HomeAssistant, fritz: Mock): """Test update with error.""" device = FritzDeviceBinarySensorMock() device.update.side_effect = [mock.DEFAULT, HTTPError("Boom")] - fritz().get_devices.return_value = [device] - - await setup_fritzbox(hass, MOCK_CONFIG) + assert await setup_config_entry( + hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz + ) assert device.update.call_count == 1 assert fritz().login.call_count == 1 diff --git a/tests/components/fritzbox/test_climate.py b/tests/components/fritzbox/test_climate.py index f6fa802a22ee4..59d32e18c3447 100644 --- a/tests/components/fritzbox/test_climate.py +++ b/tests/components/fritzbox/test_climate.py @@ -35,32 +35,26 @@ ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, ATTR_TEMPERATURE, + CONF_DEVICES, ) from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from . import MOCK_CONFIG, FritzDeviceClimateMock +from . import MOCK_CONFIG, FritzDeviceClimateMock, setup_config_entry from tests.common import async_fire_time_changed ENTITY_ID = f"{DOMAIN}.fake_name" -async def setup_fritzbox(hass: HomeAssistant, config: dict): - """Set up mock AVM Fritz!Box.""" - assert await async_setup_component(hass, FB_DOMAIN, config) is True - await hass.async_block_till_done() - - async def test_setup(hass: HomeAssistant, fritz: Mock): """Test setup of platform.""" device = FritzDeviceClimateMock() - fritz().get_devices.return_value = [device] + assert await setup_config_entry( + hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz + ) - await setup_fritzbox(hass, MOCK_CONFIG) state = hass.states.get(ENTITY_ID) - assert state assert state.attributes[ATTR_BATTERY_LEVEL] == 23 assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 18 @@ -83,10 +77,11 @@ async def test_setup(hass: HomeAssistant, fritz: Mock): async def test_target_temperature_on(hass: HomeAssistant, fritz: Mock): """Test turn device on.""" device = FritzDeviceClimateMock() - fritz().get_devices.return_value = [device] device.target_temperature = 127.0 + assert await setup_config_entry( + hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz + ) - await setup_fritzbox(hass, MOCK_CONFIG) state = hass.states.get(ENTITY_ID) assert state assert state.attributes[ATTR_TEMPERATURE] == 30 @@ -95,10 +90,11 @@ async def test_target_temperature_on(hass: HomeAssistant, fritz: Mock): async def test_target_temperature_off(hass: HomeAssistant, fritz: Mock): """Test turn device on.""" device = FritzDeviceClimateMock() - fritz().get_devices.return_value = [device] device.target_temperature = 126.5 + assert await setup_config_entry( + hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz + ) - await setup_fritzbox(hass, MOCK_CONFIG) state = hass.states.get(ENTITY_ID) assert state assert state.attributes[ATTR_TEMPERATURE] == 0 @@ -107,11 +103,11 @@ async def test_target_temperature_off(hass: HomeAssistant, fritz: Mock): async def test_update(hass: HomeAssistant, fritz: Mock): """Test update without error.""" device = FritzDeviceClimateMock() - fritz().get_devices.return_value = [device] + assert await setup_config_entry( + hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz + ) - await setup_fritzbox(hass, MOCK_CONFIG) state = hass.states.get(ENTITY_ID) - assert state assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 18 assert state.attributes[ATTR_MAX_TEMP] == 28 @@ -136,9 +132,10 @@ async def test_update_error(hass: HomeAssistant, fritz: Mock): """Test update with error.""" device = FritzDeviceClimateMock() device.update.side_effect = HTTPError("Boom") - fritz().get_devices.return_value = [device] + assert not await setup_config_entry( + hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz + ) - await setup_fritzbox(hass, MOCK_CONFIG) assert device.update.call_count == 1 assert fritz().login.call_count == 1 @@ -153,9 +150,9 @@ async def test_update_error(hass: HomeAssistant, fritz: Mock): async def test_set_temperature_temperature(hass: HomeAssistant, fritz: Mock): """Test setting temperature by temperature.""" device = FritzDeviceClimateMock() - fritz().get_devices.return_value = [device] - - await setup_fritzbox(hass, MOCK_CONFIG) + assert await setup_config_entry( + hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz + ) assert await hass.services.async_call( DOMAIN, @@ -169,9 +166,9 @@ async def test_set_temperature_temperature(hass: HomeAssistant, fritz: Mock): async def test_set_temperature_mode_off(hass: HomeAssistant, fritz: Mock): """Test setting temperature by mode.""" device = FritzDeviceClimateMock() - fritz().get_devices.return_value = [device] - - await setup_fritzbox(hass, MOCK_CONFIG) + assert await setup_config_entry( + hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz + ) assert await hass.services.async_call( DOMAIN, @@ -189,9 +186,9 @@ async def test_set_temperature_mode_off(hass: HomeAssistant, fritz: Mock): async def test_set_temperature_mode_heat(hass: HomeAssistant, fritz: Mock): """Test setting temperature by mode.""" device = FritzDeviceClimateMock() - fritz().get_devices.return_value = [device] - - await setup_fritzbox(hass, MOCK_CONFIG) + assert await setup_config_entry( + hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz + ) assert await hass.services.async_call( DOMAIN, @@ -209,9 +206,9 @@ async def test_set_temperature_mode_heat(hass: HomeAssistant, fritz: Mock): async def test_set_hvac_mode_off(hass: HomeAssistant, fritz: Mock): """Test setting hvac mode.""" device = FritzDeviceClimateMock() - fritz().get_devices.return_value = [device] - - await setup_fritzbox(hass, MOCK_CONFIG) + assert await setup_config_entry( + hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz + ) assert await hass.services.async_call( DOMAIN, @@ -225,9 +222,9 @@ async def test_set_hvac_mode_off(hass: HomeAssistant, fritz: Mock): async def test_set_hvac_mode_heat(hass: HomeAssistant, fritz: Mock): """Test setting hvac mode.""" device = FritzDeviceClimateMock() - fritz().get_devices.return_value = [device] - - await setup_fritzbox(hass, MOCK_CONFIG) + assert await setup_config_entry( + hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz + ) assert await hass.services.async_call( DOMAIN, @@ -241,9 +238,9 @@ async def test_set_hvac_mode_heat(hass: HomeAssistant, fritz: Mock): async def test_set_preset_mode_comfort(hass: HomeAssistant, fritz: Mock): """Test setting preset mode.""" device = FritzDeviceClimateMock() - fritz().get_devices.return_value = [device] - - await setup_fritzbox(hass, MOCK_CONFIG) + assert await setup_config_entry( + hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz + ) assert await hass.services.async_call( DOMAIN, @@ -257,9 +254,9 @@ async def test_set_preset_mode_comfort(hass: HomeAssistant, fritz: Mock): async def test_set_preset_mode_eco(hass: HomeAssistant, fritz: Mock): """Test setting preset mode.""" device = FritzDeviceClimateMock() - fritz().get_devices.return_value = [device] - - await setup_fritzbox(hass, MOCK_CONFIG) + assert await setup_config_entry( + hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz + ) assert await hass.services.async_call( DOMAIN, @@ -275,11 +272,11 @@ async def test_preset_mode_update(hass: HomeAssistant, fritz: Mock): device = FritzDeviceClimateMock() device.comfort_temperature = 98 device.eco_temperature = 99 - fritz().get_devices.return_value = [device] + assert await setup_config_entry( + hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz + ) - await setup_fritzbox(hass, MOCK_CONFIG) state = hass.states.get(ENTITY_ID) - assert state assert state.attributes[ATTR_PRESET_MODE] is None diff --git a/tests/components/fritzbox/test_config_flow.py b/tests/components/fritzbox/test_config_flow.py index 64e8c691638b0..a9de92060eca4 100644 --- a/tests/components/fritzbox/test_config_flow.py +++ b/tests/components/fritzbox/test_config_flow.py @@ -12,12 +12,7 @@ ATTR_UPNP_FRIENDLY_NAME, ATTR_UPNP_UDN, ) -from homeassistant.config_entries import ( - SOURCE_IMPORT, - SOURCE_REAUTH, - SOURCE_SSDP, - SOURCE_USER, -) +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_SSDP, SOURCE_USER from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import ( @@ -184,19 +179,6 @@ async def test_reauth_not_successful(hass: HomeAssistant, fritz: Mock): assert result["reason"] == "no_devices_found" -async def test_import(hass: HomeAssistant, fritz: Mock): - """Test starting a flow by import.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_USER_DATA - ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "fake_host" - assert result["data"][CONF_HOST] == "fake_host" - assert result["data"][CONF_PASSWORD] == "fake_pass" - assert result["data"][CONF_USERNAME] == "fake_user" - assert not result["result"].unique_id - - async def test_ssdp(hass: HomeAssistant, fritz: Mock): """Test starting a flow from discovery.""" result = await hass.config_entries.flow.async_init( diff --git a/tests/components/fritzbox/test_init.py b/tests/components/fritzbox/test_init.py index 75d544ec21c21..14df6f869f865 100644 --- a/tests/components/fritzbox/test_init.py +++ b/tests/components/fritzbox/test_init.py @@ -1,4 +1,6 @@ """Tests for the AVM Fritz!Box integration.""" +from __future__ import annotations + from unittest.mock import Mock, call, patch from pyfritzhome import LoginError @@ -19,19 +21,18 @@ STATE_UNAVAILABLE, ) from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component -from . import MOCK_CONFIG, FritzDeviceSwitchMock +from . import MOCK_CONFIG, FritzDeviceSwitchMock, setup_config_entry from tests.common import MockConfigEntry async def test_setup(hass: HomeAssistant, fritz: Mock): """Test setup of integration.""" - assert await async_setup_component(hass, FB_DOMAIN, MOCK_CONFIG) - await hass.async_block_till_done() + assert await setup_config_entry(hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0]) entries = hass.config_entries.async_entries() assert entries + assert len(entries) == 1 assert entries[0].data[CONF_HOST] == "fake_host" assert entries[0].data[CONF_PASSWORD] == "fake_pass" assert entries[0].data[CONF_USERNAME] == "fake_user" @@ -41,23 +42,6 @@ async def test_setup(hass: HomeAssistant, fritz: Mock): ] -async def test_setup_duplicate_config(hass: HomeAssistant, fritz: Mock, caplog): - """Test duplicate config of integration.""" - DUPLICATE = { - FB_DOMAIN: { - CONF_DEVICES: [ - MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], - MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], - ] - } - } - assert not await async_setup_component(hass, FB_DOMAIN, DUPLICATE) - await hass.async_block_till_done() - assert not hass.states.async_entity_ids() - assert not hass.states.async_all() - assert "duplicate host entries found" in caplog.text - - async def test_coordinator_update_after_reboot(hass: HomeAssistant, fritz: Mock): """Test coordinator after reboot.""" entry = MockConfigEntry( @@ -107,7 +91,7 @@ async def test_unload_remove(hass: HomeAssistant, fritz: Mock): assert len(config_entries) == 1 assert entry is config_entries[0] - assert await async_setup_component(hass, FB_DOMAIN, {}) is True + assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() assert entry.state == ENTRY_STATE_LOADED @@ -130,7 +114,7 @@ async def test_unload_remove(hass: HomeAssistant, fritz: Mock): assert state is None -async def test_raise_config_entry_not_ready_when_offline(hass): +async def test_raise_config_entry_not_ready_when_offline(hass: HomeAssistant): """Config entry state is ENTRY_STATE_SETUP_RETRY when fritzbox is offline.""" entry = MockConfigEntry( domain=FB_DOMAIN, diff --git a/tests/components/fritzbox/test_sensor.py b/tests/components/fritzbox/test_sensor.py index 331babe8af7ee..c1d82a9318944 100644 --- a/tests/components/fritzbox/test_sensor.py +++ b/tests/components/fritzbox/test_sensor.py @@ -13,34 +13,28 @@ from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, + CONF_DEVICES, PERCENTAGE, TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from . import MOCK_CONFIG, FritzDeviceSensorMock +from . import MOCK_CONFIG, FritzDeviceSensorMock, setup_config_entry from tests.common import async_fire_time_changed ENTITY_ID = f"{DOMAIN}.fake_name" -async def setup_fritzbox(hass: HomeAssistant, config: dict): - """Set up mock AVM Fritz!Box.""" - assert await async_setup_component(hass, FB_DOMAIN, config) - await hass.async_block_till_done() - - async def test_setup(hass: HomeAssistant, fritz: Mock): """Test setup of platform.""" device = FritzDeviceSensorMock() - fritz().get_devices.return_value = [device] + assert await setup_config_entry( + hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz + ) - await setup_fritzbox(hass, MOCK_CONFIG) state = hass.states.get(ENTITY_ID) - assert state assert state.state == "1.23" assert state.attributes[ATTR_FRIENDLY_NAME] == "fake_name" @@ -49,7 +43,6 @@ async def test_setup(hass: HomeAssistant, fritz: Mock): assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS state = hass.states.get(f"{ENTITY_ID}_battery") - assert state assert state.state == "23" assert state.attributes[ATTR_FRIENDLY_NAME] == "fake_name Battery" @@ -59,9 +52,9 @@ async def test_setup(hass: HomeAssistant, fritz: Mock): async def test_update(hass: HomeAssistant, fritz: Mock): """Test update without error.""" device = FritzDeviceSensorMock() - fritz().get_devices.return_value = [device] - - await setup_fritzbox(hass, MOCK_CONFIG) + assert await setup_config_entry( + hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz + ) assert device.update.call_count == 1 assert fritz().login.call_count == 1 @@ -77,9 +70,9 @@ async def test_update_error(hass: HomeAssistant, fritz: Mock): """Test update with error.""" device = FritzDeviceSensorMock() device.update.side_effect = HTTPError("Boom") - fritz().get_devices.return_value = [device] - - await setup_fritzbox(hass, MOCK_CONFIG) + assert not await setup_config_entry( + hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz + ) assert device.update.call_count == 1 assert fritz().login.call_count == 1 diff --git a/tests/components/fritzbox/test_switch.py b/tests/components/fritzbox/test_switch.py index 8546b6bf10a55..cc0caeafa69c8 100644 --- a/tests/components/fritzbox/test_switch.py +++ b/tests/components/fritzbox/test_switch.py @@ -17,6 +17,7 @@ ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, ATTR_TEMPERATURE, + CONF_DEVICES, ENERGY_KILO_WATT_HOUR, SERVICE_TURN_OFF, SERVICE_TURN_ON, @@ -24,30 +25,23 @@ TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from . import MOCK_CONFIG, FritzDeviceSwitchMock +from . import MOCK_CONFIG, FritzDeviceSwitchMock, setup_config_entry from tests.common import async_fire_time_changed ENTITY_ID = f"{DOMAIN}.fake_name" -async def setup_fritzbox(hass: HomeAssistant, config: dict): - """Set up mock AVM Fritz!Box.""" - assert await async_setup_component(hass, FB_DOMAIN, config) - await hass.async_block_till_done() - - async def test_setup(hass: HomeAssistant, fritz: Mock): """Test setup of platform.""" device = FritzDeviceSwitchMock() - fritz().get_devices.return_value = [device] + assert await setup_config_entry( + hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz + ) - await setup_fritzbox(hass, MOCK_CONFIG) state = hass.states.get(ENTITY_ID) - assert state assert state.state == STATE_ON assert state.attributes[ATTR_CURRENT_POWER_W] == 5.678 @@ -63,9 +57,9 @@ async def test_setup(hass: HomeAssistant, fritz: Mock): async def test_turn_on(hass: HomeAssistant, fritz: Mock): """Test turn device on.""" device = FritzDeviceSwitchMock() - fritz().get_devices.return_value = [device] - - await setup_fritzbox(hass, MOCK_CONFIG) + assert await setup_config_entry( + hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz + ) assert await hass.services.async_call( DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_ID}, True @@ -76,9 +70,9 @@ async def test_turn_on(hass: HomeAssistant, fritz: Mock): async def test_turn_off(hass: HomeAssistant, fritz: Mock): """Test turn device off.""" device = FritzDeviceSwitchMock() - fritz().get_devices.return_value = [device] - - await setup_fritzbox(hass, MOCK_CONFIG) + assert await setup_config_entry( + hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz + ) assert await hass.services.async_call( DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True @@ -89,9 +83,9 @@ async def test_turn_off(hass: HomeAssistant, fritz: Mock): async def test_update(hass: HomeAssistant, fritz: Mock): """Test update without error.""" device = FritzDeviceSwitchMock() - fritz().get_devices.return_value = [device] - - await setup_fritzbox(hass, MOCK_CONFIG) + assert await setup_config_entry( + hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz + ) assert device.update.call_count == 1 assert fritz().login.call_count == 1 @@ -107,9 +101,9 @@ async def test_update_error(hass: HomeAssistant, fritz: Mock): """Test update with error.""" device = FritzDeviceSwitchMock() device.update.side_effect = HTTPError("Boom") - fritz().get_devices.return_value = [device] - - await setup_fritzbox(hass, MOCK_CONFIG) + assert not await setup_config_entry( + hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz + ) assert device.update.call_count == 1 assert fritz().login.call_count == 1 From f7b72669dc47fd180f47294a6c1b161483d2ea3b Mon Sep 17 00:00:00 2001 From: Thibaut Date: Sun, 25 Apr 2021 22:28:31 +0200 Subject: [PATCH 0520/1317] Don't mark Somfy devices as unavailable (#49662) Co-authored-by: J. Nick Koston --- homeassistant/components/somfy/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/somfy/__init__.py b/homeassistant/components/somfy/__init__.py index e7a8d71824723..80cf20a95c40a 100644 --- a/homeassistant/components/somfy/__init__.py +++ b/homeassistant/components/somfy/__init__.py @@ -20,7 +20,6 @@ from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, - UpdateFailed, ) from . import api @@ -95,7 +94,10 @@ async def _update_all_devices(): previous_devices = data[COORDINATOR].data # Sometimes Somfy returns an empty list. if not devices and previous_devices: - raise UpdateFailed("No devices returned") + _LOGGER.debug( + "No devices returned. Assuming the previous ones are still valid" + ) + return previous_devices return {dev.id: dev for dev in devices} coordinator = DataUpdateCoordinator( From d24cbde91356ba50a248ea27e00e2d37ab29ce74 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Sun, 25 Apr 2021 16:28:42 -0400 Subject: [PATCH 0521/1317] Add target and selectors to sonos services (#49536) --- homeassistant/components/sonos/services.yaml | 114 +++++++++++-------- 1 file changed, 69 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/sonos/services.yaml b/homeassistant/components/sonos/services.yaml index 99b430e46806a..5c9ebed36f76c 100644 --- a/homeassistant/components/sonos/services.yaml +++ b/homeassistant/components/sonos/services.yaml @@ -3,6 +3,7 @@ join: description: Group player together. fields: master: + name: Master description: Entity ID of the player that should become the coordinator of the group. example: "media_player.living_room_sonos" @@ -11,7 +12,8 @@ join: integration: sonos domain: media_player entity_id: - description: Name(s) of entities that will join the master. + name: Entity + description: Name of entity that will join the master. example: "media_player.living_room_sonos" selector: entity: @@ -23,7 +25,8 @@ unjoin: description: Unjoin the player from a group. fields: entity_id: - description: Name(s) of entities that will be unjoined from their group. + name: Entity + description: Name of entity that will be unjoined from their group. example: "media_player.living_room_sonos" selector: entity: @@ -35,49 +38,56 @@ snapshot: description: Take a snapshot of the media player. fields: entity_id: - description: Name(s) of entities that will be snapshot. + name: Entity + description: Name of entity that will be snapshot. example: "media_player.living_room_sonos" selector: entity: integration: sonos domain: media_player with_group: - description: True (default) or False. Also snapshot the group layout. + name: With group + description: True or False. Also snapshot the group layout. example: "true" + default: true + selector: + boolean: restore: name: Restore description: Restore a snapshot of the media player. fields: entity_id: - description: Name(s) of entities that will be restored. + name: Entity + description: Name of entity that will be restored. example: "media_player.living_room_sonos" selector: entity: integration: sonos domain: media_player with_group: - description: True (default) or False. Also restore the group layout. + name: With group + description: True or False. Also restore the group layout. example: "true" + default: true + selector: + boolean: set_sleep_timer: name: Set timer description: Set a Sonos timer. + target: + device: + integration: sonos fields: - entity_id: - description: Name(s) of entities that will have a timer set. - example: "media_player.living_room_sonos" - selector: - entity: - integration: sonos - domain: media_player sleep_time: + name: Sleep Time description: Number of seconds to set the timer. example: "900" selector: number: min: 0 - max: 3600 + max: 7200 step: 1 unit_of_measurement: seconds mode: slider @@ -85,37 +95,31 @@ set_sleep_timer: clear_sleep_timer: name: Clear timer description: Clear a Sonos timer. - fields: - entity_id: - description: Name(s) of entities that will have the timer cleared. - example: "media_player.living_room_sonos" - selector: - entity: - integration: sonos - domain: media_player + target: + device: + integration: sonos set_option: name: Set option description: Set Sonos sound options. + target: + device: + integration: sonos fields: - entity_id: - description: Name(s) of entities that will have options set. - example: "media_player.living_room_sonos" - selector: - entity: - integration: sonos - domain: media_player night_sound: + name: Night sound description: Enable Night Sound mode example: "true" selector: boolean: speech_enhance: + name: Speech enhance description: Enable Speech Enhancement mode example: "true" selector: boolean: status_light: + name: Status light description: Enable Status (LED) Light example: "true" selector: @@ -124,59 +128,79 @@ set_option: play_queue: name: Play queue description: Start playing the queue from the first item. + target: + device: + integration: sonos fields: - entity_id: - description: Name(s) of entities that will start playing. - example: "media_player.living_room_sonos" - selector: - entity: - integration: sonos - domain: media_player queue_position: + name: Queue position description: Position of the song in the queue to start playing from. example: "0" selector: number: min: 0 - max: 100000000 + max: 10000 mode: box remove_from_queue: name: Remove from queue description: Removes an item from the queue. + target: + device: + integration: sonos fields: - entity_id: - description: Name(s) of entities that will remove an item. - example: "media_player.living_room_sonos" - selector: - entity: - integration: sonos - domain: media_player queue_position: + name: Queue position description: Position in the queue to remove. example: "0" selector: number: min: 0 - max: 100000000 + max: 10000 mode: box update_alarm: name: Update alarm description: Updates an alarm with new time and volume settings. + target: + device: + integration: sonos fields: alarm_id: + name: Alarm ID description: ID for the alarm to be updated. example: "1" + required: true + selector: + number: + min: 1 + max: 1440 + mode: box time: + name: Time description: Set time for the alarm. example: "07:00" + selector: + time: volume: + name: Volume description: Set alarm volume level. example: "0.75" + selector: + number: + min: 0 + max: 1 + step: 0.01 + mode: slider enabled: + name: Alarm enabled description: Enable or disable the alarm. example: "true" + selector: + boolean: include_linked_zones: + name: Include linked zones description: Enable or disable including grouped rooms. example: "true" + selector: + boolean: From 9689e06d3c8077694ce6e8592c1ab7494b151199 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 25 Apr 2021 10:32:39 -1000 Subject: [PATCH 0522/1317] Bump async-upnp-client to 0.16.2 (#49671) --- homeassistant/components/dlna_dmr/manifest.json | 2 +- homeassistant/components/ssdp/manifest.json | 2 +- homeassistant/components/upnp/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index ee4b5b26ab6b4..5a00ae0001ee3 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -2,7 +2,7 @@ "domain": "dlna_dmr", "name": "DLNA Digital Media Renderer", "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", - "requirements": ["async-upnp-client==0.16.1"], + "requirements": ["async-upnp-client==0.16.2"], "codeowners": [], "iot_class": "local_push" } diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index 6188f4aa24701..5351fc8f7ea44 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -5,7 +5,7 @@ "requirements": [ "defusedxml==0.6.0", "netdisco==2.8.2", - "async-upnp-client==0.16.1" + "async-upnp-client==0.16.2" ], "after_dependencies": ["zeroconf"], "codeowners": [], diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index 5c4e7a0c357a9..e397d97f468b7 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -3,7 +3,7 @@ "name": "UPnP", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/upnp", - "requirements": ["async-upnp-client==0.16.1"], + "requirements": ["async-upnp-client==0.16.2"], "codeowners": ["@StevenLooman"], "ssdp": [ { diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 6b8449f012099..c7f72b6dcf5b2 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodiscover==1.4.0 aiohttp==3.7.4.post0 aiohttp_cors==0.7.0 astral==2.2 -async-upnp-client==0.16.1 +async-upnp-client==0.16.2 async_timeout==3.0.1 attrs==20.3.0 awesomeversion==21.2.3 diff --git a/requirements_all.txt b/requirements_all.txt index 47bd4ab74991b..2d3f5efc51ded 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -292,7 +292,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.dlna_dmr # homeassistant.components.ssdp # homeassistant.components.upnp -async-upnp-client==0.16.1 +async-upnp-client==0.16.2 # homeassistant.components.supla asyncpysupla==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7ca01cdfe682a..34cc45f16893f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -184,7 +184,7 @@ arcam-fmj==0.5.3 # homeassistant.components.dlna_dmr # homeassistant.components.ssdp # homeassistant.components.upnp -async-upnp-client==0.16.1 +async-upnp-client==0.16.2 # homeassistant.components.aurora auroranoaa==0.0.2 From 33e8553d92db1d3b97f2cabc3427bf0e54030c48 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sun, 25 Apr 2021 23:11:01 +0200 Subject: [PATCH 0523/1317] Fix frontend freeze due to modbus device not responding (#49651) Changing the timeout from package default, secures SENDING will timeout, and after 3 retries break off. Remark: this commit is tested with pymodbus v2.5.1 the old version v2.3.0 have several problems in this area. self._value = await self.async_get_last_state() pymodbus v2.5.1 is active on DEV (bumped in an earlier PR). --- homeassistant/components/modbus/modbus.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index 44dd330f6ef94..ad53bd2aa5345 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -3,6 +3,7 @@ import threading from pymodbus.client.sync import ModbusSerialClient, ModbusTcpClient, ModbusUdpClient +from pymodbus.constants import Defaults from pymodbus.exceptions import ModbusException from pymodbus.transaction import ModbusRtuFramer @@ -138,6 +139,7 @@ def __init__(self, client_config): self._config_timeout = client_config[CONF_TIMEOUT] self._config_delay = 0 + Defaults.Timeout = 10 if self._config_type == "serial": # serial configuration self._config_method = client_config[CONF_METHOD] From 855559004bec7eccb79594c056ec6e17c41be1c8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 25 Apr 2021 11:13:54 -1000 Subject: [PATCH 0524/1317] Drop unneeded async_setup from august (#49675) --- homeassistant/components/august/__init__.py | 7 +------ tests/components/august/test_config_flow.py | 14 -------------- 2 files changed, 1 insertion(+), 20 deletions(-) diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index 041f24cc44fe7..7872c4e030738 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -33,12 +33,6 @@ ) -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the August component from YAML.""" - hass.data.setdefault(DOMAIN, {}) - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up August from a config entry.""" @@ -85,6 +79,7 @@ async def async_setup_august(hass, config_entry, august_gateway): await august_gateway.async_authenticate() + hass.data.setdefault(DOMAIN, {}) data = hass.data[DOMAIN][config_entry.entry_id] = { DATA_AUGUST: AugustData(hass, august_gateway) } diff --git a/tests/components/august/test_config_flow.py b/tests/components/august/test_config_flow.py index 53dac38fb1e3b..ab5ea1e216b9b 100644 --- a/tests/components/august/test_config_flow.py +++ b/tests/components/august/test_config_flow.py @@ -34,8 +34,6 @@ async def test_form(hass): "homeassistant.components.august.config_flow.AugustGateway.async_authenticate", return_value=True, ), patch( - "homeassistant.components.august.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.august.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -57,7 +55,6 @@ async def test_form(hass): CONF_INSTALL_ID: None, CONF_ACCESS_TOKEN_CACHE_FILE: ".my@email.tld.august.conf", } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -168,8 +165,6 @@ async def test_form_needs_validate(hass): "homeassistant.components.august.gateway.AuthenticatorAsync.async_send_verification_code", return_value=True, ) as mock_send_verification_code, patch( - "homeassistant.components.august.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.august.async_setup_entry", return_value=True ) as mock_setup_entry: result3 = await hass.config_entries.flow.async_configure( @@ -196,8 +191,6 @@ async def test_form_needs_validate(hass): "homeassistant.components.august.gateway.AuthenticatorAsync.async_send_verification_code", return_value=True, ) as mock_send_verification_code, patch( - "homeassistant.components.august.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.august.async_setup_entry", return_value=True ) as mock_setup_entry: result4 = await hass.config_entries.flow.async_configure( @@ -216,7 +209,6 @@ async def test_form_needs_validate(hass): CONF_INSTALL_ID: None, CONF_ACCESS_TOKEN_CACHE_FILE: ".my@email.tld.august.conf", } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -247,8 +239,6 @@ async def test_form_reauth(hass): "homeassistant.components.august.config_flow.AugustGateway.async_authenticate", return_value=True, ), patch( - "homeassistant.components.august.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.august.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -262,7 +252,6 @@ async def test_form_reauth(hass): assert result2["type"] == "abort" assert result2["reason"] == "reauth_successful" - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -320,8 +309,6 @@ async def test_form_reauth_with_2fa(hass): "homeassistant.components.august.gateway.AuthenticatorAsync.async_send_verification_code", return_value=True, ) as mock_send_verification_code, patch( - "homeassistant.components.august.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.august.async_setup_entry", return_value=True ) as mock_setup_entry: result3 = await hass.config_entries.flow.async_configure( @@ -334,5 +321,4 @@ async def test_form_reauth_with_2fa(hass): assert len(mock_send_verification_code.mock_calls) == 0 assert result3["type"] == "abort" assert result3["reason"] == "reauth_successful" - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 From 73b7a68e974faf3a6c16a6aeabe2671853f25e78 Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Mon, 26 Apr 2021 00:47:36 +0200 Subject: [PATCH 0525/1317] Fix Rituals battery sensor KeyError (#49661) --- homeassistant/components/rituals_perfume_genie/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/rituals_perfume_genie/sensor.py b/homeassistant/components/rituals_perfume_genie/sensor.py index acdb2331e71e5..388932be74c79 100644 --- a/homeassistant/components/rituals_perfume_genie/sensor.py +++ b/homeassistant/components/rituals_perfume_genie/sensor.py @@ -105,7 +105,7 @@ def state(self) -> int: return { "battery-charge.png": 100, "battery-full.png": 100, - "battery-75.png": 50, + "Battery-75.png": 50, "battery-50.png": 25, "battery-low.png": 10, }[self.coordinator.data[HUB][SENSORS][BATTERY][ICON]] From 6f1273cf1cafb9512ef2cddf09cbde4876df484f Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Sun, 25 Apr 2021 16:17:42 -0700 Subject: [PATCH 0526/1317] Refactor screenlogic API data selection (#49682) --- .../components/screenlogic/__init__.py | 24 -------- .../components/screenlogic/binary_sensor.py | 14 ++--- .../components/screenlogic/climate.py | 10 ++-- .../components/screenlogic/sensor.py | 59 ++++++++----------- .../components/screenlogic/switch.py | 12 ++-- 5 files changed, 41 insertions(+), 78 deletions(-) diff --git a/homeassistant/components/screenlogic/__init__.py b/homeassistant/components/screenlogic/__init__.py index cb747b3ed84b9..6fa19582a46d4 100644 --- a/homeassistant/components/screenlogic/__init__.py +++ b/homeassistant/components/screenlogic/__init__.py @@ -1,6 +1,5 @@ """The Screenlogic integration.""" import asyncio -from collections import defaultdict from datetime import timedelta import logging @@ -73,31 +72,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): await coordinator.async_config_entry_first_refresh() - device_data = defaultdict(list) - - for circuit in coordinator.data["circuits"]: - device_data["switch"].append(circuit) - - for sensor in coordinator.data["sensors"]: - if sensor == "chem_alarm": - device_data["binary_sensor"].append(sensor) - else: - if coordinator.data["sensors"][sensor]["value"] != 0: - device_data["sensor"].append(sensor) - - for pump in coordinator.data["pumps"]: - if ( - coordinator.data["pumps"][pump]["data"] != 0 - and "currentWatts" in coordinator.data["pumps"][pump] - ): - device_data["pump"].append(pump) - - for body in coordinator.data["bodies"]: - device_data["body"].append(body) - hass.data[DOMAIN][entry.entry_id] = { "coordinator": coordinator, - "devices": device_data, "listener": entry.add_update_listener(async_update_listener), } diff --git a/homeassistant/components/screenlogic/binary_sensor.py b/homeassistant/components/screenlogic/binary_sensor.py index 0001223030a68..bcff3e18bb208 100644 --- a/homeassistant/components/screenlogic/binary_sensor.py +++ b/homeassistant/components/screenlogic/binary_sensor.py @@ -1,7 +1,7 @@ """Support for a ScreenLogic Binary Sensor.""" import logging -from screenlogicpy.const import DEVICE_TYPE, ON_OFF +from screenlogicpy.const import DATA as SL_DATA, DEVICE_TYPE, ON_OFF from homeassistant.components.binary_sensor import ( DEVICE_CLASS_PROBLEM, @@ -19,16 +19,16 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up entry.""" entities = [] - data = hass.data[DOMAIN][config_entry.entry_id] - coordinator = data["coordinator"] + coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"] + + # Generic binary sensor + entities.append(ScreenLogicBinarySensor(coordinator, "chem_alarm")) - for binary_sensor in data["devices"]["binary_sensor"]: - entities.append(ScreenLogicBinarySensor(coordinator, binary_sensor)) async_add_entities(entities) class ScreenLogicBinarySensor(ScreenlogicEntity, BinarySensorEntity): - """Representation of a ScreenLogic binary sensor entity.""" + """Representation of the basic ScreenLogic binary sensor entity.""" @property def name(self): @@ -49,4 +49,4 @@ def is_on(self) -> bool: @property def sensor(self): """Shortcut to access the sensor data.""" - return self.coordinator.data["sensors"][self._data_key] + return self.coordinator.data[SL_DATA.KEY_SENSORS][self._data_key] diff --git a/homeassistant/components/screenlogic/climate.py b/homeassistant/components/screenlogic/climate.py index fac03ea577a85..b83d2fe03caba 100644 --- a/homeassistant/components/screenlogic/climate.py +++ b/homeassistant/components/screenlogic/climate.py @@ -1,7 +1,7 @@ """Support for a ScreenLogic heating device.""" import logging -from screenlogicpy.const import EQUIPMENT, HEAT_MODE +from screenlogicpy.const import DATA as SL_DATA, EQUIPMENT, HEAT_MODE from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( @@ -37,11 +37,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up entry.""" entities = [] - data = hass.data[DOMAIN][config_entry.entry_id] - coordinator = data["coordinator"] + coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"] - for body in data["devices"]["body"]: + for body in coordinator.data[SL_DATA.KEY_BODIES]: entities.append(ScreenLogicClimate(coordinator, body)) + async_add_entities(entities) @@ -217,4 +217,4 @@ async def async_added_to_hass(self): @property def body(self): """Shortcut to access body data.""" - return self.coordinator.data["bodies"][self._data_key] + return self.coordinator.data[SL_DATA.KEY_BODIES][self._data_key] diff --git a/homeassistant/components/screenlogic/sensor.py b/homeassistant/components/screenlogic/sensor.py index 38bde2afd760e..acb30b08f97a2 100644 --- a/homeassistant/components/screenlogic/sensor.py +++ b/homeassistant/components/screenlogic/sensor.py @@ -1,7 +1,7 @@ """Support for a ScreenLogic Sensor.""" import logging -from screenlogicpy.const import DEVICE_TYPE +from screenlogicpy.const import DATA as SL_DATA, DEVICE_TYPE from homeassistant.components.sensor import ( DEVICE_CLASS_POWER, @@ -25,21 +25,29 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up entry.""" entities = [] - data = hass.data[DOMAIN][config_entry.entry_id] - coordinator = data["coordinator"] + coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"] + # Generic sensors - for sensor in data["devices"]["sensor"]: - entities.append(ScreenLogicSensor(coordinator, sensor)) + for sensor in coordinator.data[SL_DATA.KEY_SENSORS]: + if sensor == "chem_alarm": + continue + if coordinator.data[SL_DATA.KEY_SENSORS][sensor]["value"] != 0: + entities.append(ScreenLogicSensor(coordinator, sensor)) + # Pump sensors - for pump in data["devices"]["pump"]: - for pump_key in PUMP_SENSORS: - entities.append(ScreenLogicPumpSensor(coordinator, pump, pump_key)) + for pump in coordinator.data[SL_DATA.KEY_PUMPS]: + if ( + coordinator.data[SL_DATA.KEY_PUMPS][pump]["data"] != 0 + and "currentWatts" in coordinator.data[SL_DATA.KEY_PUMPS][pump] + ): + for pump_key in PUMP_SENSORS: + entities.append(ScreenLogicPumpSensor(coordinator, pump, pump_key)) async_add_entities(entities) class ScreenLogicSensor(ScreenlogicEntity, SensorEntity): - """Representation of a ScreenLogic sensor entity.""" + """Representation of the basic ScreenLogic sensor entity.""" @property def name(self): @@ -54,8 +62,8 @@ def unit_of_measurement(self): @property def device_class(self): """Device class of the sensor.""" - device_class = self.sensor.get("device_type") - return SL_DEVICE_TYPE_TO_HA_DEVICE_CLASS.get(device_class) + device_type = self.sensor.get("device_type") + return SL_DEVICE_TYPE_TO_HA_DEVICE_CLASS.get(device_type) @property def state(self): @@ -66,10 +74,10 @@ def state(self): @property def sensor(self): """Shortcut to access the sensor data.""" - return self.coordinator.data["sensors"][self._data_key] + return self.coordinator.data[SL_DATA.KEY_SENSORS][self._data_key] -class ScreenLogicPumpSensor(ScreenlogicEntity, SensorEntity): +class ScreenLogicPumpSensor(ScreenLogicSensor): """Representation of a ScreenLogic pump sensor entity.""" def __init__(self, coordinator, pump, key): @@ -79,27 +87,6 @@ def __init__(self, coordinator, pump, key): self._key = key @property - def name(self): - """Return the pump sensor name.""" - return f"{self.gateway_name} {self.pump_sensor['name']}" - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return self.pump_sensor.get("unit") - - @property - def device_class(self): - """Return the device class.""" - device_class = self.pump_sensor.get("device_type") - return SL_DEVICE_TYPE_TO_HA_DEVICE_CLASS.get(device_class) - - @property - def state(self): - """State of the pump sensor.""" - return self.pump_sensor["value"] - - @property - def pump_sensor(self): + def sensor(self): """Shortcut to access the pump sensor data.""" - return self.coordinator.data["pumps"][self._pump_id][self._key] + return self.coordinator.data[SL_DATA.KEY_PUMPS][self._pump_id][self._key] diff --git a/homeassistant/components/screenlogic/switch.py b/homeassistant/components/screenlogic/switch.py index e0077b1d62dc6..e8824b8bd92cd 100644 --- a/homeassistant/components/screenlogic/switch.py +++ b/homeassistant/components/screenlogic/switch.py @@ -1,7 +1,7 @@ """Support for a ScreenLogic 'circuit' switch.""" import logging -from screenlogicpy.const import ON_OFF +from screenlogicpy.const import DATA as SL_DATA, ON_OFF from homeassistant.components.switch import SwitchEntity @@ -14,11 +14,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up entry.""" entities = [] - data = hass.data[DOMAIN][config_entry.entry_id] - coordinator = data["coordinator"] + coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"] + + for circuit in coordinator.data[SL_DATA.KEY_CIRCUITS]: + entities.append(ScreenLogicSwitch(coordinator, circuit)) - for switch in data["devices"]["switch"]: - entities.append(ScreenLogicSwitch(coordinator, switch)) async_add_entities(entities) @@ -60,4 +60,4 @@ async def _async_set_circuit(self, circuit_value) -> None: @property def circuit(self): """Shortcut to access the circuit.""" - return self.coordinator.data["circuits"][self._data_key] + return self.coordinator.data[SL_DATA.KEY_CIRCUITS][self._data_key] From e5e71c2026d304ffd2b9d26c89f2cc28eadb5505 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Mon, 26 Apr 2021 00:04:21 +0000 Subject: [PATCH 0527/1317] [ci skip] Translation update --- .../components/denonavr/translations/it.json | 1 + .../devolo_home_control/translations/it.json | 7 +++ .../components/fritz/translations/ca.json | 44 +++++++++++++++++++ .../components/fritz/translations/et.json | 44 +++++++++++++++++++ .../components/fritz/translations/it.json | 44 +++++++++++++++++++ .../components/fritz/translations/ru.json | 44 +++++++++++++++++++ .../fritz/translations/zh-Hant.json | 44 +++++++++++++++++++ .../components/motioneye/translations/it.json | 25 +++++++++++ .../components/picnic/translations/it.json | 22 ++++++++++ .../components/smarttub/translations/it.json | 6 ++- 10 files changed, 280 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/fritz/translations/ca.json create mode 100644 homeassistant/components/fritz/translations/et.json create mode 100644 homeassistant/components/fritz/translations/it.json create mode 100644 homeassistant/components/fritz/translations/ru.json create mode 100644 homeassistant/components/fritz/translations/zh-Hant.json create mode 100644 homeassistant/components/motioneye/translations/it.json create mode 100644 homeassistant/components/picnic/translations/it.json diff --git a/homeassistant/components/denonavr/translations/it.json b/homeassistant/components/denonavr/translations/it.json index 3d994456f8d39..6f6714387776a 100644 --- a/homeassistant/components/denonavr/translations/it.json +++ b/homeassistant/components/denonavr/translations/it.json @@ -37,6 +37,7 @@ "init": { "data": { "show_all_sources": "Mostra tutte le fonti", + "update_audyssey": "Aggiorna le impostazioni di Audyssey", "zone2": "Imposta la Zona 2", "zone3": "Imposta la Zona 3" }, diff --git a/homeassistant/components/devolo_home_control/translations/it.json b/homeassistant/components/devolo_home_control/translations/it.json index 1dcdb6cbcb091..a0cba314ea668 100644 --- a/homeassistant/components/devolo_home_control/translations/it.json +++ b/homeassistant/components/devolo_home_control/translations/it.json @@ -14,6 +14,13 @@ "password": "Password", "username": "E-mail / devolo ID" } + }, + "zeroconf_confirm": { + "data": { + "mydevolo_url": "URL mydevolo", + "password": "Password", + "username": "E-mail / ID devolo" + } } } } diff --git a/homeassistant/components/fritz/translations/ca.json b/homeassistant/components/fritz/translations/ca.json new file mode 100644 index 0000000000000..1b55ba3e23d8c --- /dev/null +++ b/homeassistant/components/fritz/translations/ca.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" + }, + "error": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "connection_error": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" + }, + "flow_title": "FRITZ!Box Tools: {name}", + "step": { + "confirm": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + }, + "description": "S'ha descobert FRITZ!Box: {name} \n\nConfigura FRITZ!Box Tools per controlar {name}", + "title": "Configuraci\u00f3 de FRITZ!Box Tools" + }, + "reauth_confirm": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + }, + "description": "Actualitza les credencials de FRITZ!Box Tools de: {host}.\n\nFRITZ!Box Tools no pot iniciar sessi\u00f3 a FRITZ!Box.", + "title": "Actualitzant les credencials de FRITZ!Box Tools" + }, + "start_config": { + "data": { + "host": "Amfitri\u00f3", + "password": "Contrasenya", + "port": "Port", + "username": "Nom d'usuari" + }, + "description": "Configura FRITZ!Box Tools per poder controlar FRITZ!Box.\nEl m\u00ednim necessari \u00e9s: nom d'usuari i contrasenya.", + "title": "Configuraci\u00f3 de FRITZ!Box Tools - obligatori" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritz/translations/et.json b/homeassistant/components/fritz/translations/et.json new file mode 100644 index 0000000000000..e996efd435bf9 --- /dev/null +++ b/homeassistant/components/fritz/translations/et.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "already_in_progress": "H\u00e4\u00e4lestamine on k\u00e4imas", + "reauth_successful": "Taastuvastamine \u00f5nnestus" + }, + "error": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "already_in_progress": "Seadistamine on juba k\u00e4ivitatud", + "connection_error": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus" + }, + "flow_title": "FRITZ!Box t\u00f6\u00f6riistad: {nimi}", + "step": { + "confirm": { + "data": { + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + }, + "description": "Avasti FRITZ! Box: {name} \n\n Seadista FRITZ! Boxi t\u00f6\u00f6riistad oma {name} juhtimiseks", + "title": "FRITZ! Boxi t\u00f6\u00f6riistade seadistamine" + }, + "reauth_confirm": { + "data": { + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + }, + "description": "V\u00e4rskenda FRITZ!Box Tools'i volitusi: {host}.\n\nFRITZ!Box Tools ei saa FRITZ!Boxi sisse logida.", + "title": "FRITZ!Boxi t\u00f6\u00f6riistade uuendamine - volitused" + }, + "start_config": { + "data": { + "host": "Host", + "password": "Salas\u00f5na", + "port": "Port", + "username": "Kasutajanimi" + }, + "description": "Seadista FRITZ!Boxi t\u00f6\u00f6riistad oma FRITZ!Boxi juhtimiseks.\n Minimaalselt vaja: kasutajanimi ja salas\u00f5na.", + "title": "FRITZ! Boxi t\u00f6\u00f6riistade seadistamine - kohustuslik" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritz/translations/it.json b/homeassistant/components/fritz/translations/it.json new file mode 100644 index 0000000000000..39da67b87289b --- /dev/null +++ b/homeassistant/components/fritz/translations/it.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" + }, + "error": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "connection_error": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida" + }, + "flow_title": "Strumenti FRITZ! Box: {name}", + "step": { + "confirm": { + "data": { + "password": "Password", + "username": "Nome utente" + }, + "description": "FRITZ! Box rilevato: {name} \n\n Configura gli strumenti del FRITZ! Box per controllare il tuo {name}", + "title": "Configura gli strumenti del FRITZ! Box" + }, + "reauth_confirm": { + "data": { + "password": "Password", + "username": "Nome utente" + }, + "description": "Aggiorna le credenziali di FRITZ! Box Tools per: {host} . \n\n FRITZ! Box Tools non riesce ad accedere al tuo FRITZ! Box.", + "title": "Aggiornamento degli strumenti del FRITZ! Box - credenziali" + }, + "start_config": { + "data": { + "host": "Host", + "password": "Password", + "port": "Porta", + "username": "Nome utente" + }, + "description": "Configura gli strumenti FRITZ! Box per controllare il tuo FRITZ! Box.\n Minimo necessario: nome utente, password.", + "title": "Configurazione degli strumenti FRITZ! Box - obbligatorio" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritz/translations/ru.json b/homeassistant/components/fritz/translations/ru.json new file mode 100644 index 0000000000000..b50c42c4bfcf7 --- /dev/null +++ b/homeassistant/components/fritz/translations/ru.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + }, + "error": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", + "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." + }, + "flow_title": "FRITZ!Box Tools: {name}", + "step": { + "confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + }, + "description": "\u0412 \u0441\u0435\u0442\u0438 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d FRITZ!Box: {name}\n\n\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 FRITZ!Box Tools, \u0447\u0442\u043e\u0431\u044b \u0443\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u044c \u0412\u0430\u0448\u0438\u043c {name}", + "title": "FRITZ!Box Tools" + }, + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + }, + "description": "\u041e\u0431\u043d\u043e\u0432\u0438\u0442\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 FRITZ!Box Tools \u0434\u043b\u044f {host}.\n\n\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e FRITZ!Box Tools \u043d\u0430 \u0412\u0430\u0448\u0435\u043c FRITZ!Box.", + "title": "FRITZ!Box Tools" + }, + "start_config": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 FRITZ!Box Tools \u0434\u043b\u044f \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u0412\u0430\u0448\u0438\u043c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c FRITZ!Box.\n\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0443\u043a\u0430\u0437\u0430\u0442\u044c \u043a\u0430\u043a \u043c\u0438\u043d\u0438\u043c\u0443\u043c \u043b\u043e\u0433\u0438\u043d \u0438 \u043f\u0430\u0440\u043e\u043b\u044c.", + "title": "FRITZ!Box Tools" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritz/translations/zh-Hant.json b/homeassistant/components/fritz/translations/zh-Hant.json new file mode 100644 index 0000000000000..29872e14868fe --- /dev/null +++ b/homeassistant/components/fritz/translations/zh-Hant.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" + }, + "error": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "connection_error": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" + }, + "flow_title": "FRITZ!Box Tools\uff1a{name}", + "step": { + "confirm": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "\u767c\u73fe\u7684 FRITZ!Box\uff1a{name}\n\n\u8a2d\u5b9a FRITZ!Box Tools \u4ee5\u63a7\u5236 {name}", + "title": "\u8a2d\u5b9a FRITZ!Box Tools" + }, + "reauth_confirm": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "\u66f4\u65b0 FRITZ!Box Tools \u6191\u8b49\uff1a{host}\u3002\n\nFRITZ!Box Tools \u7121\u6cd5\u767b\u5165 FRITZ!Box\u3002", + "title": "\u66f4\u65b0 FRITZ!Box Tools - \u6191\u8b49" + }, + "start_config": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "password": "\u5bc6\u78bc", + "port": "\u901a\u8a0a\u57e0", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "\u8a2d\u5b9a FRITZ!Box Tools \u4ee5\u63a7\u5236 FRITZ!Box\u3002\n\u9700\u8981\u8f38\u5165\uff1a\u4f7f\u7528\u8005\u540d\u7a31\u3001\u5bc6\u78bc\u3002", + "title": "\u8a2d\u5b9a FRITZ!Box Tools - \u5f37\u5236" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/it.json b/homeassistant/components/motioneye/translations/it.json new file mode 100644 index 0000000000000..af07fac1a943b --- /dev/null +++ b/homeassistant/components/motioneye/translations/it.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "invalid_url": "URL non valido", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "admin_password": "Amministratore Password", + "admin_username": "Amministratore Nome utente", + "surveillance_password": "Sorveglianza Password", + "surveillance_username": "Sorveglianza Nome utente", + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/it.json b/homeassistant/components/picnic/translations/it.json new file mode 100644 index 0000000000000..e77faae817d10 --- /dev/null +++ b/homeassistant/components/picnic/translations/it.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "country_code": "Prefisso internazionale", + "password": "Password", + "username": "Nome utente" + } + } + } + }, + "title": "Picnic" +} \ No newline at end of file diff --git a/homeassistant/components/smarttub/translations/it.json b/homeassistant/components/smarttub/translations/it.json index 64aed0996f3fe..bbc778a7af2fa 100644 --- a/homeassistant/components/smarttub/translations/it.json +++ b/homeassistant/components/smarttub/translations/it.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "already_configured": "L'account \u00e8 gi\u00e0 configurato", "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { @@ -9,6 +9,10 @@ "unknown": "Errore imprevisto" }, "step": { + "reauth_confirm": { + "description": "L'integrazione di SmartTub deve autenticare nuovamente il tuo account", + "title": "Autenticare nuovamente l'integrazione" + }, "user": { "data": { "email": "E-mail", From 4a6bb96a0fc5f2c7027715a54065bf74252983d8 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sun, 25 Apr 2021 21:15:04 -0400 Subject: [PATCH 0528/1317] Stop fast polling of a Zigbee device after a check-in command (#49685) * Stop fast polling after a check-in * Update tests --- homeassistant/components/zha/core/channels/general.py | 1 + tests/components/zha/test_channels.py | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py index 3bd08e6f93e5d..881a4512befdb 100644 --- a/homeassistant/components/zha/core/channels/general.py +++ b/homeassistant/components/zha/core/channels/general.py @@ -422,6 +422,7 @@ async def check_in_response(self, tsn: int) -> None: await self.checkin_response(True, self.CHECKIN_FAST_POLL_TIMEOUT, tsn=tsn) if self._ch_pool.manufacturer_code not in self._IGNORED_MANUFACTURER_ID: await self.set_long_poll_interval(self.LONG_POLL) + await self.fast_poll_stop() @callback def skip_manufacturer_id(self, manufacturer_code: int) -> None: diff --git a/tests/components/zha/test_channels.py b/tests/components/zha/test_channels.py index a391439a23973..f60b7d4859acb 100644 --- a/tests/components/zha/test_channels.py +++ b/tests/components/zha/test_channels.py @@ -445,19 +445,22 @@ async def test_poll_control_checkin_response(poll_control_ch): """Test poll control channel checkin response.""" rsp_mock = AsyncMock() set_interval_mock = AsyncMock() + fast_poll_mock = AsyncMock() cluster = poll_control_ch.cluster patch_1 = mock.patch.object(cluster, "checkin_response", rsp_mock) patch_2 = mock.patch.object(cluster, "set_long_poll_interval", set_interval_mock) + patch_3 = mock.patch.object(cluster, "fast_poll_stop", fast_poll_mock) - with patch_1, patch_2: + with patch_1, patch_2, patch_3: await poll_control_ch.check_in_response(33) assert rsp_mock.call_count == 1 assert set_interval_mock.call_count == 1 + assert fast_poll_mock.call_count == 1 await poll_control_ch.check_in_response(33) - assert cluster.endpoint.request.call_count == 2 - assert cluster.endpoint.request.await_count == 2 + assert cluster.endpoint.request.call_count == 3 + assert cluster.endpoint.request.await_count == 3 assert cluster.endpoint.request.call_args_list[0][0][1] == 33 assert cluster.endpoint.request.call_args_list[0][0][0] == 0x0020 assert cluster.endpoint.request.call_args_list[1][0][0] == 0x0020 From 5a993a3ff34a6c0fe58112b24efb5424e881cda0 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Sun, 25 Apr 2021 21:22:01 -0400 Subject: [PATCH 0529/1317] Use core constants for apprise (#49683) --- homeassistant/components/apprise/notify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/apprise/notify.py b/homeassistant/components/apprise/notify.py index 5f4a6b666430f..2aeeb62b00b23 100644 --- a/homeassistant/components/apprise/notify.py +++ b/homeassistant/components/apprise/notify.py @@ -11,12 +11,12 @@ PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import CONF_URL import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) CONF_FILE = "config" -CONF_URL = "url" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { From 9222d3e9f9c3c4a18716a406c3af395427a3fdd0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 25 Apr 2021 16:42:45 -1000 Subject: [PATCH 0530/1317] Ensure hue connection errors are passed to ConfigEntryNotReady (#49674) - Limits log spam on retry --- homeassistant/components/hue/bridge.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index 2a306fe77bb2d..801f2a33b7089 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -90,8 +90,9 @@ async def async_setup(self, tries=0): return False except CannotConnect as err: - LOGGER.error("Error connecting to the Hue bridge at %s", host) - raise ConfigEntryNotReady from err + raise ConfigEntryNotReady( + f"Error connecting to the Hue bridge at {host}" + ) from err except Exception: # pylint: disable=broad-except LOGGER.exception("Unknown error connecting with Hue bridge at %s", host) From 940d28960b58406b8d7b03e70509e6a515a48be4 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 26 Apr 2021 07:43:52 +0200 Subject: [PATCH 0531/1317] Upgrade TwitterAPI to 2.7.2 (#49680) --- homeassistant/components/twitter/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/twitter/manifest.json b/homeassistant/components/twitter/manifest.json index 79d3b58b2bd3d..c25ce304ae015 100644 --- a/homeassistant/components/twitter/manifest.json +++ b/homeassistant/components/twitter/manifest.json @@ -2,7 +2,7 @@ "domain": "twitter", "name": "Twitter", "documentation": "https://www.home-assistant.io/integrations/twitter", - "requirements": ["TwitterAPI==2.6.8"], + "requirements": ["TwitterAPI==2.7.2"], "codeowners": [], "iot_class": "cloud_push" } diff --git a/requirements_all.txt b/requirements_all.txt index 2d3f5efc51ded..3ee97e9a4fa47 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -81,7 +81,7 @@ RtmAPI==0.7.2 TravisPy==0.3.5 # homeassistant.components.twitter -TwitterAPI==2.6.8 +TwitterAPI==2.7.2 # homeassistant.components.tof # VL53L1X2==0.1.5 From 1b14a2f54f7840819ef339beb0d50bb17b8a9d33 Mon Sep 17 00:00:00 2001 From: MarBra <16831559+MarBra@users.noreply.github.com> Date: Mon, 26 Apr 2021 11:22:07 +0200 Subject: [PATCH 0532/1317] Address late review comments for denonavr (#49666) * denonavr: Add DynamicEQ and Audyssey service * Remove useless return and entry.option in hass.data * Remove duplicate translation --- homeassistant/components/denonavr/__init__.py | 5 ----- homeassistant/components/denonavr/media_player.py | 9 +++++---- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/denonavr/__init__.py b/homeassistant/components/denonavr/__init__.py index 853ade1f8a634..fa4d161269731 100644 --- a/homeassistant/components/denonavr/__init__.py +++ b/homeassistant/components/denonavr/__init__.py @@ -11,12 +11,10 @@ from .config_flow import ( CONF_SHOW_ALL_SOURCES, - CONF_UPDATE_AUDYSSEY, CONF_ZONE2, CONF_ZONE3, DEFAULT_SHOW_SOURCES, DEFAULT_TIMEOUT, - DEFAULT_UPDATE_AUDYSSEY, DEFAULT_ZONE2, DEFAULT_ZONE3, DOMAIN, @@ -55,9 +53,6 @@ async def async_setup_entry( hass.data[DOMAIN][entry.entry_id] = { CONF_RECEIVER: receiver, - CONF_UPDATE_AUDYSSEY: entry.options.get( - CONF_UPDATE_AUDYSSEY, DEFAULT_UPDATE_AUDYSSEY - ), UNDO_UPDATE_LISTENER: undo_listener, } diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index a3e35d42242b9..14520f0ddaf98 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -91,7 +91,9 @@ async def async_setup_entry( entities = [] data = hass.data[DOMAIN][config_entry.entry_id] receiver = data[CONF_RECEIVER] - update_audyssey = data.get(CONF_UPDATE_AUDYSSEY, DEFAULT_UPDATE_AUDYSSEY) + update_audyssey = config_entry.options.get( + CONF_UPDATE_AUDYSSEY, DEFAULT_UPDATE_AUDYSSEY + ) for receiver_zone in receiver.zones.values(): if config_entry.data[CONF_SERIAL_NUMBER] is not None: unique_id = f"{config_entry.unique_id}-{receiver_zone.zone}" @@ -482,13 +484,12 @@ async def async_update_audyssey(self): async def async_set_dynamic_eq(self, dynamic_eq: bool): """Turn DynamicEQ on or off.""" if dynamic_eq: - result = await self._receiver.async_dynamic_eq_on() + await self._receiver.async_dynamic_eq_on() else: - result = await self._receiver.async_dynamic_eq_off() + await self._receiver.async_dynamic_eq_off() if self._update_audyssey: await self._receiver.async_update_audyssey() - return result # Decorator defined before is a staticmethod async_log_errors = staticmethod( # pylint: disable=no-staticmethod-decorator From 9a6402c1ae50c4e9b1bb24426ea5497c0d1a6b7a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 26 Apr 2021 00:37:13 -1000 Subject: [PATCH 0533/1317] Only compile esphome icon schema once (#49688) --- homeassistant/components/esphome/sensor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/esphome/sensor.py b/homeassistant/components/esphome/sensor.py index 045f74d3e4ab4..ceb391f6bda7f 100644 --- a/homeassistant/components/esphome/sensor.py +++ b/homeassistant/components/esphome/sensor.py @@ -13,6 +13,8 @@ from . import EsphomeEntity, esphome_state_property, platform_async_setup_entry +ICON_SCHEMA = vol.Schema(cv.icon) + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities @@ -58,7 +60,7 @@ def icon(self) -> str: """Return the icon.""" if not self._static_info.icon or self._static_info.device_class: return None - return vol.Schema(cv.icon)(self._static_info.icon) + return ICON_SCHEMA(self._static_info.icon) @property def force_update(self) -> bool: From 0f220001a0a50f44e2a6963bde8325c13d938d38 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Mon, 26 Apr 2021 07:52:17 -0400 Subject: [PATCH 0534/1317] Add selectors to ecobee services (#49499) --- homeassistant/components/ecobee/services.yaml | 143 +++++++++++++++--- 1 file changed, 125 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/ecobee/services.yaml b/homeassistant/components/ecobee/services.yaml index dd848d09d563a..d88088849b1bd 100644 --- a/homeassistant/components/ecobee/services.yaml +++ b/homeassistant/components/ecobee/services.yaml @@ -1,104 +1,211 @@ create_vacation: + name: Create vacation description: >- Create a vacation on the selected thermostat. Note: start/end date and time must all be specified together for these parameters to have an effect. If start/end date and time are not specified, the vacation will start immediately and last 14 days (unless deleted earlier). fields: entity_id: - description: ecobee thermostat on which to create the vacation (required). + name: Entity + description: ecobee thermostat on which to create the vacation. + required: true example: "climate.kitchen" + selector: + entity: + integration: ecobee + domain: climate vacation_name: - description: Name of the vacation to create; must be unique on the thermostat (required). + name: Vacation name + description: Name of the vacation to create; must be unique on the thermostat. + required: true example: "Skiing" + selector: + text: cool_temp: - description: Cooling temperature during the vacation (required). + name: Cool temperature + description: Cooling temperature during the vacation. + required: true example: 23 + selector: + number: + min: 7 + max: 95 + step: 0.5 + unit_of_measurement: "°" heat_temp: - description: Heating temperature during the vacation (required). + name: Heat temperature + description: Heating temperature during the vacation. + required: true example: 25 + selector: + number: + min: 7 + max: 95 + step: 0.5 + unit_of_measurement: "°" start_date: + name: Start date description: >- Date the vacation starts in the YYYY-MM-DD format (optional, immediately if not provided along with start_time, end_date, and end_time). example: "2019-03-15" + selector: + text: start_time: + name: start time description: Time the vacation starts, in the local time of the thermostat, in the 24-hour format "HH:MM:SS" example: "20:00:00" + selector: + time: end_date: + name: End date description: >- Date the vacation ends in the YYYY-MM-DD format (optional, 14 days from now if not provided along with start_date, start_time, and end_time). example: "2019-03-20" + selector: + text: end_time: + name: End time description: Time the vacation ends, in the local time of the thermostat, in the 24-hour format "HH:MM:SS" example: "20:00:00" + selector: + time: fan_mode: - description: Fan mode of the thermostat during the vacation (auto or on) (optional, auto if not provided). + name: Fan mode + description: Fan mode of the thermostat during the vacation. example: "on" + default: "auto" + selector: + select: + options: + - "on" + - "auto" fan_min_on_time: - description: Minimum number of minutes to run the fan each hour (0 to 60) during the vacation (optional, 0 if not provided). + name: Fan minimum on time + description: Minimum number of minutes to run the fan each hour (0 to 60) during the vacation. example: 30 + default: 0 + selector: + number: + min: 0 + max: 60 + unit_of_measurement: minutes delete_vacation: + name: Delete vacation description: >- Delete a vacation on the selected thermostat. fields: entity_id: - description: ecobee thermostat on which to delete the vacation (required). + name: Entity + description: ecobee thermostat on which to delete the vacation. + required: true example: "climate.kitchen" + selector: + entity: + integration: ecobee + domain: climate vacation_name: - description: Name of the vacation to delete (required). + name: Vacation name + description: Name of the vacation to delete. + required: true example: "Skiing" + selector: + text: resume_program: + name: Resume program description: Resume the programmed schedule. fields: entity_id: + name: Entity description: Name(s) of entities to change. example: "climate.kitchen" + selector: + entity: + integration: ecobee + domain: climate resume_all: - description: Resume all events and return to the scheduled program. This default to false which removes only the top event. + name: Resume all + description: Resume all events and return to the scheduled program. example: true + default: false + selector: + boolean: set_fan_min_on_time: + name: Set fan minimum on time description: Set the minimum fan on time. fields: entity_id: + name: Entity description: Name(s) of entities to change. example: "climate.kitchen" + selector: + entity: + integration: ecobee + domain: climate fan_min_on_time: + name: Fan minimum on time description: New value of fan min on time. + required: true example: 5 + selector: + number: + min: 0 + max: 60 + unit_of_measurement: minutes set_dst_mode: + name: Set Daylight savings time mode description: Enable/disable automatic daylight savings time. + target: + entity: + integration: ecobee + domain: climate fields: - entity_id: - description: Name(s) of entities to change. - example: "climate.kitchen" dst_enabled: + name: Daylight savings time enabled description: Enable automatic daylight savings time. + required: true example: "true" + selector: + boolean: set_mic_mode: + name: Set mic mode description: Enable/disable Alexa mic (only for Ecobee 4). + target: + entity: + integration: ecobee + domain: climate fields: - entity_id: - description: Name(s) of entities to change. - example: "climate.kitchen" mic_enabled: + name: Mic enabled description: Enable Alexa mic. + required: true example: "true" + selector: + boolean: set_occupancy_modes: + name: Set occupancy modes description: Enable/disable Smart Home/Away and Follow Me modes. + target: + entity: + integration: ecobee + domain: climate fields: - entity_id: - description: Name(s) of entities to change. - example: "climate.kitchen" auto_away: + name: Auto away description: Enable Smart Home/Away mode. example: "true" + selector: + boolean: follow_me: + name: Follow me description: Enable Follow Me mode. example: "true" + selector: + boolean: From 37466ae4233ee8f18407b59cfa8d4863149e4b96 Mon Sep 17 00:00:00 2001 From: Ruslan Sayfutdinov Date: Mon, 26 Apr 2021 13:23:21 +0100 Subject: [PATCH 0535/1317] Don't ignore mypy errors by default (#49270) --- .no-strict-typing | 950 ++++++++++++++++++ .../components/automation/__init__.py | 6 +- .../components/automation/helpers.py | 4 +- .../components/coronavirus/config_flow.py | 4 +- homeassistant/components/knx/__init__.py | 2 +- homeassistant/components/picnic/sensor.py | 6 +- homeassistant/util/ruamel_yaml.py | 4 +- mypy.ini | 39 + script/hassfest/__main__.py | 2 + script/hassfest/model.py | 4 +- script/hassfest/mypy_config.py | 366 +++++++ setup.cfg | 19 - 12 files changed, 1373 insertions(+), 33 deletions(-) create mode 100644 .no-strict-typing create mode 100644 mypy.ini create mode 100644 script/hassfest/mypy_config.py diff --git a/.no-strict-typing b/.no-strict-typing new file mode 100644 index 0000000000000..a24f7bcf8e376 --- /dev/null +++ b/.no-strict-typing @@ -0,0 +1,950 @@ +# Used by hassfest for generating mypy.ini. +# Components listed here will be excluded from strict mypy checks. +# But basic checks for existing type annotations will still be applied. + +homeassistant.components.abode.* +homeassistant.components.accuweather.* +homeassistant.components.acer_projector.* +homeassistant.components.acmeda.* +homeassistant.components.actiontec.* +homeassistant.components.adguard.* +homeassistant.components.ads.* +homeassistant.components.advantage_air.* +homeassistant.components.aemet.* +homeassistant.components.aftership.* +homeassistant.components.agent_dvr.* +homeassistant.components.air_quality.* +homeassistant.components.airly.* +homeassistant.components.airnow.* +homeassistant.components.airvisual.* +homeassistant.components.aladdin_connect.* +homeassistant.components.alarm_control_panel.* +homeassistant.components.alarmdecoder.* +homeassistant.components.alert.* +homeassistant.components.alexa.* +homeassistant.components.almond.* +homeassistant.components.alpha_vantage.* +homeassistant.components.amazon_polly.* +homeassistant.components.ambiclimate.* +homeassistant.components.ambient_station.* +homeassistant.components.amcrest.* +homeassistant.components.ampio.* +homeassistant.components.analytics.* +homeassistant.components.android_ip_webcam.* +homeassistant.components.androidtv.* +homeassistant.components.anel_pwrctrl.* +homeassistant.components.anthemav.* +homeassistant.components.apache_kafka.* +homeassistant.components.apcupsd.* +homeassistant.components.api.* +homeassistant.components.apns.* +homeassistant.components.apple_tv.* +homeassistant.components.apprise.* +homeassistant.components.aprs.* +homeassistant.components.aqualogic.* +homeassistant.components.aquostv.* +homeassistant.components.arcam_fmj.* +homeassistant.components.arduino.* +homeassistant.components.arest.* +homeassistant.components.arlo.* +homeassistant.components.arris_tg2492lg.* +homeassistant.components.aruba.* +homeassistant.components.arwn.* +homeassistant.components.asterisk_cdr.* +homeassistant.components.asterisk_mbox.* +homeassistant.components.asuswrt.* +homeassistant.components.atag.* +homeassistant.components.aten_pe.* +homeassistant.components.atome.* +homeassistant.components.august.* +homeassistant.components.aurora.* +homeassistant.components.aurora_abb_powerone.* +homeassistant.components.auth.* +homeassistant.components.avea.* +homeassistant.components.avion.* +homeassistant.components.awair.* +homeassistant.components.aws.* +homeassistant.components.axis.* +homeassistant.components.azure_devops.* +homeassistant.components.azure_event_hub.* +homeassistant.components.azure_service_bus.* +homeassistant.components.baidu.* +homeassistant.components.bayesian.* +homeassistant.components.bbb_gpio.* +homeassistant.components.bbox.* +homeassistant.components.beewi_smartclim.* +homeassistant.components.bh1750.* +homeassistant.components.bitcoin.* +homeassistant.components.bizkaibus.* +homeassistant.components.blackbird.* +homeassistant.components.blebox.* +homeassistant.components.blink.* +homeassistant.components.blinksticklight.* +homeassistant.components.blinkt.* +homeassistant.components.blockchain.* +homeassistant.components.bloomsky.* +homeassistant.components.blueprint.* +homeassistant.components.bluesound.* +homeassistant.components.bluetooth_le_tracker.* +homeassistant.components.bluetooth_tracker.* +homeassistant.components.bme280.* +homeassistant.components.bme680.* +homeassistant.components.bmp280.* +homeassistant.components.bmw_connected_drive.* +homeassistant.components.braviatv.* +homeassistant.components.broadlink.* +homeassistant.components.brother.* +homeassistant.components.brottsplatskartan.* +homeassistant.components.browser.* +homeassistant.components.brunt.* +homeassistant.components.bsblan.* +homeassistant.components.bt_home_hub_5.* +homeassistant.components.bt_smarthub.* +homeassistant.components.buienradar.* +homeassistant.components.caldav.* +homeassistant.components.camera.* +homeassistant.components.canary.* +homeassistant.components.cast.* +homeassistant.components.cert_expiry.* +homeassistant.components.channels.* +homeassistant.components.circuit.* +homeassistant.components.cisco_ios.* +homeassistant.components.cisco_mobility_express.* +homeassistant.components.cisco_webex_teams.* +homeassistant.components.citybikes.* +homeassistant.components.clementine.* +homeassistant.components.clickatell.* +homeassistant.components.clicksend.* +homeassistant.components.clicksend_tts.* +homeassistant.components.climacell.* +homeassistant.components.climate.* +homeassistant.components.cloud.* +homeassistant.components.cloudflare.* +homeassistant.components.cmus.* +homeassistant.components.co2signal.* +homeassistant.components.coinbase.* +homeassistant.components.color_extractor.* +homeassistant.components.comed_hourly_pricing.* +homeassistant.components.comfoconnect.* +homeassistant.components.command_line.* +homeassistant.components.compensation.* +homeassistant.components.concord232.* +homeassistant.components.config.* +homeassistant.components.configurator.* +homeassistant.components.control4.* +homeassistant.components.conversation.* +homeassistant.components.coolmaster.* +homeassistant.components.coronavirus.* +homeassistant.components.counter.* +homeassistant.components.cppm_tracker.* +homeassistant.components.cpuspeed.* +homeassistant.components.cups.* +homeassistant.components.currencylayer.* +homeassistant.components.daikin.* +homeassistant.components.danfoss_air.* +homeassistant.components.darksky.* +homeassistant.components.datadog.* +homeassistant.components.ddwrt.* +homeassistant.components.debugpy.* +homeassistant.components.deconz.* +homeassistant.components.decora.* +homeassistant.components.decora_wifi.* +homeassistant.components.default_config.* +homeassistant.components.delijn.* +homeassistant.components.deluge.* +homeassistant.components.demo.* +homeassistant.components.denon.* +homeassistant.components.denonavr.* +homeassistant.components.deutsche_bahn.* +homeassistant.components.device_sun_light_trigger.* +homeassistant.components.device_tracker.* +homeassistant.components.devolo_home_control.* +homeassistant.components.dexcom.* +homeassistant.components.dhcp.* +homeassistant.components.dht.* +homeassistant.components.dialogflow.* +homeassistant.components.digital_ocean.* +homeassistant.components.digitalloggers.* +homeassistant.components.directv.* +homeassistant.components.discogs.* +homeassistant.components.discord.* +homeassistant.components.discovery.* +homeassistant.components.dlib_face_detect.* +homeassistant.components.dlib_face_identify.* +homeassistant.components.dlink.* +homeassistant.components.dlna_dmr.* +homeassistant.components.dnsip.* +homeassistant.components.dominos.* +homeassistant.components.doods.* +homeassistant.components.doorbird.* +homeassistant.components.dovado.* +homeassistant.components.downloader.* +homeassistant.components.dsmr.* +homeassistant.components.dsmr_reader.* +homeassistant.components.dte_energy_bridge.* +homeassistant.components.dublin_bus_transport.* +homeassistant.components.duckdns.* +homeassistant.components.dunehd.* +homeassistant.components.dwd_weather_warnings.* +homeassistant.components.dweet.* +homeassistant.components.dynalite.* +homeassistant.components.dyson.* +homeassistant.components.eafm.* +homeassistant.components.ebox.* +homeassistant.components.ebusd.* +homeassistant.components.ecoal_boiler.* +homeassistant.components.ecobee.* +homeassistant.components.econet.* +homeassistant.components.ecovacs.* +homeassistant.components.eddystone_temperature.* +homeassistant.components.edimax.* +homeassistant.components.edl21.* +homeassistant.components.ee_brightbox.* +homeassistant.components.efergy.* +homeassistant.components.egardia.* +homeassistant.components.eight_sleep.* +homeassistant.components.elgato.* +homeassistant.components.eliqonline.* +homeassistant.components.elkm1.* +homeassistant.components.elv.* +homeassistant.components.emby.* +homeassistant.components.emoncms.* +homeassistant.components.emoncms_history.* +homeassistant.components.emonitor.* +homeassistant.components.emulated_hue.* +homeassistant.components.emulated_kasa.* +homeassistant.components.emulated_roku.* +homeassistant.components.enigma2.* +homeassistant.components.enocean.* +homeassistant.components.enphase_envoy.* +homeassistant.components.entur_public_transport.* +homeassistant.components.environment_canada.* +homeassistant.components.envirophat.* +homeassistant.components.envisalink.* +homeassistant.components.ephember.* +homeassistant.components.epson.* +homeassistant.components.epsonworkforce.* +homeassistant.components.eq3btsmart.* +homeassistant.components.esphome.* +homeassistant.components.essent.* +homeassistant.components.etherscan.* +homeassistant.components.eufy.* +homeassistant.components.everlights.* +homeassistant.components.evohome.* +homeassistant.components.ezviz.* +homeassistant.components.faa_delays.* +homeassistant.components.facebook.* +homeassistant.components.facebox.* +homeassistant.components.fail2ban.* +homeassistant.components.familyhub.* +homeassistant.components.fan.* +homeassistant.components.fastdotcom.* +homeassistant.components.feedreader.* +homeassistant.components.ffmpeg.* +homeassistant.components.ffmpeg_motion.* +homeassistant.components.ffmpeg_noise.* +homeassistant.components.fibaro.* +homeassistant.components.fido.* +homeassistant.components.file.* +homeassistant.components.filesize.* +homeassistant.components.filter.* +homeassistant.components.fints.* +homeassistant.components.fireservicerota.* +homeassistant.components.firmata.* +homeassistant.components.fitbit.* +homeassistant.components.fixer.* +homeassistant.components.fleetgo.* +homeassistant.components.flexit.* +homeassistant.components.flic.* +homeassistant.components.flick_electric.* +homeassistant.components.flo.* +homeassistant.components.flock.* +homeassistant.components.flume.* +homeassistant.components.flunearyou.* +homeassistant.components.flux.* +homeassistant.components.flux_led.* +homeassistant.components.folder.* +homeassistant.components.folder_watcher.* +homeassistant.components.foobot.* +homeassistant.components.forked_daapd.* +homeassistant.components.fortios.* +homeassistant.components.foscam.* +homeassistant.components.foursquare.* +homeassistant.components.free_mobile.* +homeassistant.components.freebox.* +homeassistant.components.freedns.* +homeassistant.components.fritz.* +homeassistant.components.fritzbox.* +homeassistant.components.fritzbox_callmonitor.* +homeassistant.components.fritzbox_netmonitor.* +homeassistant.components.fronius.* +homeassistant.components.frontier_silicon.* +homeassistant.components.futurenow.* +homeassistant.components.garadget.* +homeassistant.components.garmin_connect.* +homeassistant.components.gc100.* +homeassistant.components.gdacs.* +homeassistant.components.generic.* +homeassistant.components.generic_thermostat.* +homeassistant.components.geniushub.* +homeassistant.components.geo_json_events.* +homeassistant.components.geo_rss_events.* +homeassistant.components.geofency.* +homeassistant.components.geonetnz_quakes.* +homeassistant.components.geonetnz_volcano.* +homeassistant.components.gios.* +homeassistant.components.github.* +homeassistant.components.gitlab_ci.* +homeassistant.components.gitter.* +homeassistant.components.glances.* +homeassistant.components.gntp.* +homeassistant.components.goalfeed.* +homeassistant.components.goalzero.* +homeassistant.components.gogogate2.* +homeassistant.components.google.* +homeassistant.components.google_assistant.* +homeassistant.components.google_cloud.* +homeassistant.components.google_domains.* +homeassistant.components.google_maps.* +homeassistant.components.google_pubsub.* +homeassistant.components.google_translate.* +homeassistant.components.google_travel_time.* +homeassistant.components.google_wifi.* +homeassistant.components.gpmdp.* +homeassistant.components.gpsd.* +homeassistant.components.gpslogger.* +homeassistant.components.graphite.* +homeassistant.components.gree.* +homeassistant.components.greeneye_monitor.* +homeassistant.components.greenwave.* +homeassistant.components.growatt_server.* +homeassistant.components.gstreamer.* +homeassistant.components.gtfs.* +homeassistant.components.guardian.* +homeassistant.components.habitica.* +homeassistant.components.hangouts.* +homeassistant.components.harman_kardon_avr.* +homeassistant.components.harmony.* +homeassistant.components.hassio.* +homeassistant.components.haveibeenpwned.* +homeassistant.components.hddtemp.* +homeassistant.components.hdmi_cec.* +homeassistant.components.heatmiser.* +homeassistant.components.heos.* +homeassistant.components.here_travel_time.* +homeassistant.components.hikvision.* +homeassistant.components.hikvisioncam.* +homeassistant.components.hisense_aehw4a1.* +homeassistant.components.history_stats.* +homeassistant.components.hitron_coda.* +homeassistant.components.hive.* +homeassistant.components.hlk_sw16.* +homeassistant.components.home_connect.* +homeassistant.components.home_plus_control.* +homeassistant.components.homeassistant.* +homeassistant.components.homekit.* +homeassistant.components.homekit_controller.* +homeassistant.components.homematic.* +homeassistant.components.homematicip_cloud.* +homeassistant.components.homeworks.* +homeassistant.components.honeywell.* +homeassistant.components.horizon.* +homeassistant.components.hp_ilo.* +homeassistant.components.html5.* +homeassistant.components.htu21d.* +homeassistant.components.huawei_router.* +homeassistant.components.hue.* +homeassistant.components.huisbaasje.* +homeassistant.components.humidifier.* +homeassistant.components.hunterdouglas_powerview.* +homeassistant.components.hvv_departures.* +homeassistant.components.hydrawise.* +homeassistant.components.ialarm.* +homeassistant.components.iammeter.* +homeassistant.components.iaqualink.* +homeassistant.components.icloud.* +homeassistant.components.idteck_prox.* +homeassistant.components.ifttt.* +homeassistant.components.iglo.* +homeassistant.components.ign_sismologia.* +homeassistant.components.ihc.* +homeassistant.components.image.* +homeassistant.components.imap.* +homeassistant.components.imap_email_content.* +homeassistant.components.incomfort.* +homeassistant.components.influxdb.* +homeassistant.components.input_boolean.* +homeassistant.components.input_datetime.* +homeassistant.components.input_number.* +homeassistant.components.input_select.* +homeassistant.components.input_text.* +homeassistant.components.insteon.* +homeassistant.components.intent.* +homeassistant.components.intent_script.* +homeassistant.components.intesishome.* +homeassistant.components.ios.* +homeassistant.components.iota.* +homeassistant.components.iperf3.* +homeassistant.components.ipma.* +homeassistant.components.ipp.* +homeassistant.components.iqvia.* +homeassistant.components.irish_rail_transport.* +homeassistant.components.islamic_prayer_times.* +homeassistant.components.iss.* +homeassistant.components.isy994.* +homeassistant.components.itach.* +homeassistant.components.itunes.* +homeassistant.components.izone.* +homeassistant.components.jewish_calendar.* +homeassistant.components.joaoapps_join.* +homeassistant.components.juicenet.* +homeassistant.components.kaiterra.* +homeassistant.components.kankun.* +homeassistant.components.keba.* +homeassistant.components.keenetic_ndms2.* +homeassistant.components.kef.* +homeassistant.components.keyboard.* +homeassistant.components.keyboard_remote.* +homeassistant.components.kira.* +homeassistant.components.kiwi.* +homeassistant.components.kmtronic.* +homeassistant.components.kodi.* +homeassistant.components.konnected.* +homeassistant.components.kostal_plenticore.* +homeassistant.components.kulersky.* +homeassistant.components.kwb.* +homeassistant.components.lacrosse.* +homeassistant.components.lametric.* +homeassistant.components.lannouncer.* +homeassistant.components.lastfm.* +homeassistant.components.launch_library.* +homeassistant.components.lcn.* +homeassistant.components.lg_netcast.* +homeassistant.components.lg_soundbar.* +homeassistant.components.life360.* +homeassistant.components.lifx.* +homeassistant.components.lifx_cloud.* +homeassistant.components.lifx_legacy.* +homeassistant.components.lightwave.* +homeassistant.components.limitlessled.* +homeassistant.components.linksys_smart.* +homeassistant.components.linode.* +homeassistant.components.linux_battery.* +homeassistant.components.lirc.* +homeassistant.components.litejet.* +homeassistant.components.litterrobot.* +homeassistant.components.llamalab_automate.* +homeassistant.components.local_file.* +homeassistant.components.local_ip.* +homeassistant.components.locative.* +homeassistant.components.logbook.* +homeassistant.components.logentries.* +homeassistant.components.logger.* +homeassistant.components.logi_circle.* +homeassistant.components.london_air.* +homeassistant.components.london_underground.* +homeassistant.components.loopenergy.* +homeassistant.components.lovelace.* +homeassistant.components.luci.* +homeassistant.components.luftdaten.* +homeassistant.components.lupusec.* +homeassistant.components.lutron.* +homeassistant.components.lutron_caseta.* +homeassistant.components.lw12wifi.* +homeassistant.components.lyft.* +homeassistant.components.lyric.* +homeassistant.components.magicseaweed.* +homeassistant.components.mailgun.* +homeassistant.components.manual.* +homeassistant.components.manual_mqtt.* +homeassistant.components.map.* +homeassistant.components.marytts.* +homeassistant.components.mastodon.* +homeassistant.components.matrix.* +homeassistant.components.maxcube.* +homeassistant.components.mazda.* +homeassistant.components.mcp23017.* +homeassistant.components.media_extractor.* +homeassistant.components.media_source.* +homeassistant.components.mediaroom.* +homeassistant.components.melcloud.* +homeassistant.components.melissa.* +homeassistant.components.meraki.* +homeassistant.components.message_bird.* +homeassistant.components.met.* +homeassistant.components.met_eireann.* +homeassistant.components.meteo_france.* +homeassistant.components.meteoalarm.* +homeassistant.components.metoffice.* +homeassistant.components.mfi.* +homeassistant.components.mhz19.* +homeassistant.components.microsoft.* +homeassistant.components.microsoft_face.* +homeassistant.components.microsoft_face_detect.* +homeassistant.components.microsoft_face_identify.* +homeassistant.components.miflora.* +homeassistant.components.mikrotik.* +homeassistant.components.mill.* +homeassistant.components.min_max.* +homeassistant.components.minecraft_server.* +homeassistant.components.minio.* +homeassistant.components.mitemp_bt.* +homeassistant.components.mjpeg.* +homeassistant.components.mobile_app.* +homeassistant.components.mochad.* +homeassistant.components.modbus.* +homeassistant.components.modem_callerid.* +homeassistant.components.mold_indicator.* +homeassistant.components.monoprice.* +homeassistant.components.moon.* +homeassistant.components.motion_blinds.* +homeassistant.components.motioneye.* +homeassistant.components.mpchc.* +homeassistant.components.mpd.* +homeassistant.components.mqtt.* +homeassistant.components.mqtt_eventstream.* +homeassistant.components.mqtt_json.* +homeassistant.components.mqtt_room.* +homeassistant.components.mqtt_statestream.* +homeassistant.components.msteams.* +homeassistant.components.mullvad.* +homeassistant.components.mvglive.* +homeassistant.components.my.* +homeassistant.components.mychevy.* +homeassistant.components.mycroft.* +homeassistant.components.myq.* +homeassistant.components.mysensors.* +homeassistant.components.mystrom.* +homeassistant.components.mythicbeastsdns.* +homeassistant.components.n26.* +homeassistant.components.nad.* +homeassistant.components.namecheapdns.* +homeassistant.components.nanoleaf.* +homeassistant.components.neato.* +homeassistant.components.nederlandse_spoorwegen.* +homeassistant.components.nello.* +homeassistant.components.ness_alarm.* +homeassistant.components.nest.* +homeassistant.components.netatmo.* +homeassistant.components.netdata.* +homeassistant.components.netgear.* +homeassistant.components.netgear_lte.* +homeassistant.components.netio.* +homeassistant.components.neurio_energy.* +homeassistant.components.nexia.* +homeassistant.components.nextbus.* +homeassistant.components.nextcloud.* +homeassistant.components.nfandroidtv.* +homeassistant.components.nightscout.* +homeassistant.components.niko_home_control.* +homeassistant.components.nilu.* +homeassistant.components.nissan_leaf.* +homeassistant.components.nmap_tracker.* +homeassistant.components.nmbs.* +homeassistant.components.no_ip.* +homeassistant.components.noaa_tides.* +homeassistant.components.norway_air.* +homeassistant.components.notify_events.* +homeassistant.components.notion.* +homeassistant.components.nsw_fuel_station.* +homeassistant.components.nsw_rural_fire_service_feed.* +homeassistant.components.nuheat.* +homeassistant.components.nuki.* +homeassistant.components.numato.* +homeassistant.components.nut.* +homeassistant.components.nws.* +homeassistant.components.nx584.* +homeassistant.components.nzbget.* +homeassistant.components.oasa_telematics.* +homeassistant.components.obihai.* +homeassistant.components.octoprint.* +homeassistant.components.oem.* +homeassistant.components.ohmconnect.* +homeassistant.components.ombi.* +homeassistant.components.omnilogic.* +homeassistant.components.onboarding.* +homeassistant.components.ondilo_ico.* +homeassistant.components.onewire.* +homeassistant.components.onkyo.* +homeassistant.components.onvif.* +homeassistant.components.openalpr_cloud.* +homeassistant.components.openalpr_local.* +homeassistant.components.opencv.* +homeassistant.components.openerz.* +homeassistant.components.openevse.* +homeassistant.components.openexchangerates.* +homeassistant.components.opengarage.* +homeassistant.components.openhardwaremonitor.* +homeassistant.components.openhome.* +homeassistant.components.opensensemap.* +homeassistant.components.opensky.* +homeassistant.components.opentherm_gw.* +homeassistant.components.openuv.* +homeassistant.components.openweathermap.* +homeassistant.components.opnsense.* +homeassistant.components.opple.* +homeassistant.components.orangepi_gpio.* +homeassistant.components.oru.* +homeassistant.components.orvibo.* +homeassistant.components.osramlightify.* +homeassistant.components.otp.* +homeassistant.components.ovo_energy.* +homeassistant.components.owntracks.* +homeassistant.components.ozw.* +homeassistant.components.panasonic_bluray.* +homeassistant.components.panasonic_viera.* +homeassistant.components.pandora.* +homeassistant.components.panel_custom.* +homeassistant.components.panel_iframe.* +homeassistant.components.pcal9535a.* +homeassistant.components.pencom.* +homeassistant.components.person.* +homeassistant.components.philips_js.* +homeassistant.components.pi4ioe5v9xxxx.* +homeassistant.components.pi_hole.* +homeassistant.components.picnic.* +homeassistant.components.picotts.* +homeassistant.components.piglow.* +homeassistant.components.pilight.* +homeassistant.components.ping.* +homeassistant.components.pioneer.* +homeassistant.components.pjlink.* +homeassistant.components.plaato.* +homeassistant.components.plant.* +homeassistant.components.plex.* +homeassistant.components.plugwise.* +homeassistant.components.plum_lightpad.* +homeassistant.components.pocketcasts.* +homeassistant.components.point.* +homeassistant.components.poolsense.* +homeassistant.components.powerwall.* +homeassistant.components.profiler.* +homeassistant.components.progettihwsw.* +homeassistant.components.proliphix.* +homeassistant.components.prometheus.* +homeassistant.components.prowl.* +homeassistant.components.proxmoxve.* +homeassistant.components.proxy.* +homeassistant.components.ps4.* +homeassistant.components.pulseaudio_loopback.* +homeassistant.components.push.* +homeassistant.components.pushbullet.* +homeassistant.components.pushover.* +homeassistant.components.pushsafer.* +homeassistant.components.pvoutput.* +homeassistant.components.pvpc_hourly_pricing.* +homeassistant.components.pyload.* +homeassistant.components.python_script.* +homeassistant.components.qbittorrent.* +homeassistant.components.qld_bushfire.* +homeassistant.components.qnap.* +homeassistant.components.qrcode.* +homeassistant.components.quantum_gateway.* +homeassistant.components.qvr_pro.* +homeassistant.components.qwikswitch.* +homeassistant.components.rachio.* +homeassistant.components.radarr.* +homeassistant.components.radiotherm.* +homeassistant.components.rainbird.* +homeassistant.components.raincloud.* +homeassistant.components.rainforest_eagle.* +homeassistant.components.rainmachine.* +homeassistant.components.random.* +homeassistant.components.raspihats.* +homeassistant.components.raspyrfm.* +homeassistant.components.recollect_waste.* +homeassistant.components.recorder.* +homeassistant.components.recswitch.* +homeassistant.components.reddit.* +homeassistant.components.rejseplanen.* +homeassistant.components.remember_the_milk.* +homeassistant.components.remote_rpi_gpio.* +homeassistant.components.repetier.* +homeassistant.components.rest.* +homeassistant.components.rest_command.* +homeassistant.components.rflink.* +homeassistant.components.rfxtrx.* +homeassistant.components.ring.* +homeassistant.components.ripple.* +homeassistant.components.risco.* +homeassistant.components.rituals_perfume_genie.* +homeassistant.components.rmvtransport.* +homeassistant.components.rocketchat.* +homeassistant.components.roku.* +homeassistant.components.roomba.* +homeassistant.components.roon.* +homeassistant.components.route53.* +homeassistant.components.rova.* +homeassistant.components.rpi_camera.* +homeassistant.components.rpi_gpio.* +homeassistant.components.rpi_gpio_pwm.* +homeassistant.components.rpi_pfio.* +homeassistant.components.rpi_power.* +homeassistant.components.rpi_rf.* +homeassistant.components.rss_feed_template.* +homeassistant.components.rtorrent.* +homeassistant.components.ruckus_unleashed.* +homeassistant.components.russound_rio.* +homeassistant.components.russound_rnet.* +homeassistant.components.sabnzbd.* +homeassistant.components.safe_mode.* +homeassistant.components.saj.* +homeassistant.components.samsungtv.* +homeassistant.components.satel_integra.* +homeassistant.components.schluter.* +homeassistant.components.scrape.* +homeassistant.components.screenlogic.* +homeassistant.components.script.* +homeassistant.components.scsgate.* +homeassistant.components.search.* +homeassistant.components.season.* +homeassistant.components.sendgrid.* +homeassistant.components.sense.* +homeassistant.components.sensehat.* +homeassistant.components.sensibo.* +homeassistant.components.sentry.* +homeassistant.components.serial.* +homeassistant.components.serial_pm.* +homeassistant.components.sesame.* +homeassistant.components.seven_segments.* +homeassistant.components.seventeentrack.* +homeassistant.components.sharkiq.* +homeassistant.components.shell_command.* +homeassistant.components.shelly.* +homeassistant.components.shiftr.* +homeassistant.components.shodan.* +homeassistant.components.shopping_list.* +homeassistant.components.sht31.* +homeassistant.components.sigfox.* +homeassistant.components.sighthound.* +homeassistant.components.signal_messenger.* +homeassistant.components.simplepush.* +homeassistant.components.simplisafe.* +homeassistant.components.simulated.* +homeassistant.components.sinch.* +homeassistant.components.sisyphus.* +homeassistant.components.sky_hub.* +homeassistant.components.skybeacon.* +homeassistant.components.skybell.* +homeassistant.components.sleepiq.* +homeassistant.components.slide.* +homeassistant.components.sma.* +homeassistant.components.smappee.* +homeassistant.components.smart_meter_texas.* +homeassistant.components.smarthab.* +homeassistant.components.smartthings.* +homeassistant.components.smarttub.* +homeassistant.components.smarty.* +homeassistant.components.smhi.* +homeassistant.components.sms.* +homeassistant.components.smtp.* +homeassistant.components.snapcast.* +homeassistant.components.snips.* +homeassistant.components.snmp.* +homeassistant.components.sochain.* +homeassistant.components.solaredge.* +homeassistant.components.solaredge_local.* +homeassistant.components.solarlog.* +homeassistant.components.solax.* +homeassistant.components.soma.* +homeassistant.components.somfy.* +homeassistant.components.somfy_mylink.* +homeassistant.components.sonarr.* +homeassistant.components.songpal.* +homeassistant.components.sonos.* +homeassistant.components.sony_projector.* +homeassistant.components.soundtouch.* +homeassistant.components.spaceapi.* +homeassistant.components.spc.* +homeassistant.components.speedtestdotnet.* +homeassistant.components.spider.* +homeassistant.components.splunk.* +homeassistant.components.spotcrime.* +homeassistant.components.spotify.* +homeassistant.components.sql.* +homeassistant.components.squeezebox.* +homeassistant.components.srp_energy.* +homeassistant.components.ssdp.* +homeassistant.components.starline.* +homeassistant.components.starlingbank.* +homeassistant.components.startca.* +homeassistant.components.statistics.* +homeassistant.components.statsd.* +homeassistant.components.steam_online.* +homeassistant.components.stiebel_eltron.* +homeassistant.components.stookalert.* +homeassistant.components.stream.* +homeassistant.components.streamlabswater.* +homeassistant.components.stt.* +homeassistant.components.subaru.* +homeassistant.components.suez_water.* +homeassistant.components.supervisord.* +homeassistant.components.supla.* +homeassistant.components.surepetcare.* +homeassistant.components.swiss_hydrological_data.* +homeassistant.components.swiss_public_transport.* +homeassistant.components.swisscom.* +homeassistant.components.switchbot.* +homeassistant.components.switcher_kis.* +homeassistant.components.switchmate.* +homeassistant.components.syncthru.* +homeassistant.components.synology_chat.* +homeassistant.components.synology_dsm.* +homeassistant.components.synology_srm.* +homeassistant.components.syslog.* +homeassistant.components.system_health.* +homeassistant.components.system_log.* +homeassistant.components.tado.* +homeassistant.components.tag.* +homeassistant.components.tahoma.* +homeassistant.components.tank_utility.* +homeassistant.components.tankerkoenig.* +homeassistant.components.tapsaff.* +homeassistant.components.tasmota.* +homeassistant.components.tautulli.* +homeassistant.components.tcp.* +homeassistant.components.ted5000.* +homeassistant.components.telegram.* +homeassistant.components.telegram_bot.* +homeassistant.components.tellduslive.* +homeassistant.components.tellstick.* +homeassistant.components.telnet.* +homeassistant.components.temper.* +homeassistant.components.template.* +homeassistant.components.tensorflow.* +homeassistant.components.tesla.* +homeassistant.components.tfiac.* +homeassistant.components.thermoworks_smoke.* +homeassistant.components.thethingsnetwork.* +homeassistant.components.thingspeak.* +homeassistant.components.thinkingcleaner.* +homeassistant.components.thomson.* +homeassistant.components.threshold.* +homeassistant.components.tibber.* +homeassistant.components.tikteck.* +homeassistant.components.tile.* +homeassistant.components.time_date.* +homeassistant.components.timer.* +homeassistant.components.tmb.* +homeassistant.components.tod.* +homeassistant.components.todoist.* +homeassistant.components.tof.* +homeassistant.components.tomato.* +homeassistant.components.toon.* +homeassistant.components.torque.* +homeassistant.components.totalconnect.* +homeassistant.components.touchline.* +homeassistant.components.tplink.* +homeassistant.components.tplink_lte.* +homeassistant.components.traccar.* +homeassistant.components.trace.* +homeassistant.components.trackr.* +homeassistant.components.tradfri.* +homeassistant.components.trafikverket_train.* +homeassistant.components.trafikverket_weatherstation.* +homeassistant.components.transmission.* +homeassistant.components.transport_nsw.* +homeassistant.components.travisci.* +homeassistant.components.trend.* +homeassistant.components.tuya.* +homeassistant.components.twentemilieu.* +homeassistant.components.twilio.* +homeassistant.components.twilio_call.* +homeassistant.components.twilio_sms.* +homeassistant.components.twinkly.* +homeassistant.components.twitch.* +homeassistant.components.twitter.* +homeassistant.components.ubus.* +homeassistant.components.ue_smart_radio.* +homeassistant.components.uk_transport.* +homeassistant.components.unifi.* +homeassistant.components.unifi_direct.* +homeassistant.components.unifiled.* +homeassistant.components.universal.* +homeassistant.components.upb.* +homeassistant.components.upc_connect.* +homeassistant.components.upcloud.* +homeassistant.components.updater.* +homeassistant.components.upnp.* +homeassistant.components.uptime.* +homeassistant.components.uptimerobot.* +homeassistant.components.uscis.* +homeassistant.components.usgs_earthquakes_feed.* +homeassistant.components.utility_meter.* +homeassistant.components.uvc.* +homeassistant.components.vallox.* +homeassistant.components.vasttrafik.* +homeassistant.components.velbus.* +homeassistant.components.velux.* +homeassistant.components.venstar.* +homeassistant.components.vera.* +homeassistant.components.verisure.* +homeassistant.components.versasense.* +homeassistant.components.version.* +homeassistant.components.vesync.* +homeassistant.components.viaggiatreno.* +homeassistant.components.vicare.* +homeassistant.components.vilfo.* +homeassistant.components.vivotek.* +homeassistant.components.vizio.* +homeassistant.components.vlc.* +homeassistant.components.vlc_telnet.* +homeassistant.components.voicerss.* +homeassistant.components.volkszaehler.* +homeassistant.components.volumio.* +homeassistant.components.volvooncall.* +homeassistant.components.vultr.* +homeassistant.components.w800rf32.* +homeassistant.components.wake_on_lan.* +homeassistant.components.waqi.* +homeassistant.components.waterfurnace.* +homeassistant.components.watson_iot.* +homeassistant.components.watson_tts.* +homeassistant.components.waze_travel_time.* +homeassistant.components.webhook.* +homeassistant.components.webostv.* +homeassistant.components.wemo.* +homeassistant.components.whois.* +homeassistant.components.wiffi.* +homeassistant.components.wilight.* +homeassistant.components.wink.* +homeassistant.components.wirelesstag.* +homeassistant.components.withings.* +homeassistant.components.wled.* +homeassistant.components.wolflink.* +homeassistant.components.workday.* +homeassistant.components.worldclock.* +homeassistant.components.worldtidesinfo.* +homeassistant.components.worxlandroid.* +homeassistant.components.wsdot.* +homeassistant.components.wunderground.* +homeassistant.components.x10.* +homeassistant.components.xbee.* +homeassistant.components.xbox.* +homeassistant.components.xbox_live.* +homeassistant.components.xeoma.* +homeassistant.components.xiaomi.* +homeassistant.components.xiaomi_aqara.* +homeassistant.components.xiaomi_miio.* +homeassistant.components.xiaomi_tv.* +homeassistant.components.xmpp.* +homeassistant.components.xs1.* +homeassistant.components.yale_smart_alarm.* +homeassistant.components.yamaha.* +homeassistant.components.yamaha_musiccast.* +homeassistant.components.yandex_transport.* +homeassistant.components.yandextts.* +homeassistant.components.yeelight.* +homeassistant.components.yeelightsunflower.* +homeassistant.components.yi.* +homeassistant.components.zabbix.* +homeassistant.components.zamg.* +homeassistant.components.zengge.* +homeassistant.components.zerproc.* +homeassistant.components.zestimate.* +homeassistant.components.zha.* +homeassistant.components.zhong_hong.* +homeassistant.components.ziggo_mediabox_xl.* +homeassistant.components.zodiac.* +homeassistant.components.zoneminder.* +homeassistant.components.zwave.* diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 36b7f1688f856..4493dc23e0da0 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -604,14 +604,12 @@ async def _async_process_config( blueprints_used = False for config_key in extract_domain_configs(config, DOMAIN): - conf: list[dict[str, Any] | blueprint.BlueprintInputs] = config[ # type: ignore - config_key - ] + conf: list[dict[str, Any] | blueprint.BlueprintInputs] = config[config_key] for list_no, config_block in enumerate(conf): raw_blueprint_inputs = None raw_config = None - if isinstance(config_block, blueprint.BlueprintInputs): # type: ignore + if isinstance(config_block, blueprint.BlueprintInputs): blueprints_used = True blueprint_inputs = config_block raw_blueprint_inputs = blueprint_inputs.config_with_inputs diff --git a/homeassistant/components/automation/helpers.py b/homeassistant/components/automation/helpers.py index 688f051861ea4..3be11afe18b53 100644 --- a/homeassistant/components/automation/helpers.py +++ b/homeassistant/components/automation/helpers.py @@ -10,6 +10,6 @@ @singleton(DATA_BLUEPRINTS) @callback -def async_get_blueprints(hass: HomeAssistant) -> blueprint.DomainBlueprints: # type: ignore +def async_get_blueprints(hass: HomeAssistant) -> blueprint.DomainBlueprints: """Get automation blueprints.""" - return blueprint.DomainBlueprints(hass, DOMAIN, LOGGER) # type: ignore + return blueprint.DomainBlueprints(hass, DOMAIN, LOGGER) diff --git a/homeassistant/components/coronavirus/config_flow.py b/homeassistant/components/coronavirus/config_flow.py index 4bf1dcd56b9ac..85027b35d929d 100644 --- a/homeassistant/components/coronavirus/config_flow.py +++ b/homeassistant/components/coronavirus/config_flow.py @@ -24,11 +24,11 @@ async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResultDict: """Handle the initial step.""" - errors = {} + errors: dict[str, str] = {} if self._options is None: coordinator = await get_coordinator(self.hass) - if not coordinator.last_update_success: + if not coordinator.last_update_success or coordinator.data is None: return self.async_abort(reason="cannot_connect") self._options = {OPTION_WORLDWIDE: "Worldwide"} diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index a8a923dc00aed..a52163cfca333 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -403,7 +403,7 @@ def register_callback(self) -> TelegramQueue.Callback: address_filters = list( map(AddressFilter, self.config[DOMAIN][CONF_KNX_EVENT_FILTER]) ) - return self.xknx.telegram_queue.register_telegram_received_cb( # type: ignore[no-any-return] + return self.xknx.telegram_queue.register_telegram_received_cb( self.telegram_received_cb, address_filters=address_filters, group_addresses=[], diff --git a/homeassistant/components/picnic/sensor.py b/homeassistant/components/picnic/sensor.py index 3e30582b5c24a..3a4d3582f9cda 100644 --- a/homeassistant/components/picnic/sensor.py +++ b/homeassistant/components/picnic/sensor.py @@ -66,7 +66,11 @@ def name(self) -> str | None: @property def state(self) -> StateType: """Return the state of the entity.""" - data_set = self.coordinator.data.get(self.properties["data_type"], {}) + data_set = ( + self.coordinator.data.get(self.properties["data_type"], {}) + if self.coordinator.data is not None + else {} + ) return self.properties["state"](data_set) @property diff --git a/homeassistant/util/ruamel_yaml.py b/homeassistant/util/ruamel_yaml.py index 74d71678a6f2a..b9f69b15578ee 100644 --- a/homeassistant/util/ruamel_yaml.py +++ b/homeassistant/util/ruamel_yaml.py @@ -9,7 +9,7 @@ from typing import Union import ruamel.yaml -from ruamel.yaml import YAML # type: ignore +from ruamel.yaml import YAML from ruamel.yaml.compat import StringIO from ruamel.yaml.constructor import SafeConstructor from ruamel.yaml.error import YAMLError @@ -91,7 +91,7 @@ def load_yaml(fname: str, round_trip: bool = False) -> JSON_TYPE: """Load a YAML file.""" if round_trip: yaml = YAML(typ="rt") - yaml.preserve_quotes = True + yaml.preserve_quotes = True # type: ignore[assignment] else: if ExtSafeConstructor.name is None: ExtSafeConstructor.name = fname diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000000000..f80dbf0b75e34 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,39 @@ +# Automatically generated by hassfest. +# +# To update, run python3 -m script.hassfest + +[mypy] +python_version = 3.8 +show_error_codes = true +follow_imports = silent +ignore_missing_imports = true +warn_incomplete_stub = true +warn_redundant_casts = true +warn_unused_configs = true +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +strict_equality = true +warn_return_any = true +warn_unreachable = true +warn_unused_ignores = true + +[mypy-homeassistant.components.abode.*,homeassistant.components.accuweather.*,homeassistant.components.acer_projector.*,homeassistant.components.acmeda.*,homeassistant.components.actiontec.*,homeassistant.components.adguard.*,homeassistant.components.ads.*,homeassistant.components.advantage_air.*,homeassistant.components.aemet.*,homeassistant.components.aftership.*,homeassistant.components.agent_dvr.*,homeassistant.components.air_quality.*,homeassistant.components.airly.*,homeassistant.components.airnow.*,homeassistant.components.airvisual.*,homeassistant.components.aladdin_connect.*,homeassistant.components.alarm_control_panel.*,homeassistant.components.alarmdecoder.*,homeassistant.components.alert.*,homeassistant.components.alexa.*,homeassistant.components.almond.*,homeassistant.components.alpha_vantage.*,homeassistant.components.amazon_polly.*,homeassistant.components.ambiclimate.*,homeassistant.components.ambient_station.*,homeassistant.components.amcrest.*,homeassistant.components.ampio.*,homeassistant.components.analytics.*,homeassistant.components.android_ip_webcam.*,homeassistant.components.androidtv.*,homeassistant.components.anel_pwrctrl.*,homeassistant.components.anthemav.*,homeassistant.components.apache_kafka.*,homeassistant.components.apcupsd.*,homeassistant.components.api.*,homeassistant.components.apns.*,homeassistant.components.apple_tv.*,homeassistant.components.apprise.*,homeassistant.components.aprs.*,homeassistant.components.aqualogic.*,homeassistant.components.aquostv.*,homeassistant.components.arcam_fmj.*,homeassistant.components.arduino.*,homeassistant.components.arest.*,homeassistant.components.arlo.*,homeassistant.components.arris_tg2492lg.*,homeassistant.components.aruba.*,homeassistant.components.arwn.*,homeassistant.components.asterisk_cdr.*,homeassistant.components.asterisk_mbox.*,homeassistant.components.asuswrt.*,homeassistant.components.atag.*,homeassistant.components.aten_pe.*,homeassistant.components.atome.*,homeassistant.components.august.*,homeassistant.components.aurora.*,homeassistant.components.aurora_abb_powerone.*,homeassistant.components.auth.*,homeassistant.components.avea.*,homeassistant.components.avion.*,homeassistant.components.awair.*,homeassistant.components.aws.*,homeassistant.components.axis.*,homeassistant.components.azure_devops.*,homeassistant.components.azure_event_hub.*,homeassistant.components.azure_service_bus.*,homeassistant.components.baidu.*,homeassistant.components.bayesian.*,homeassistant.components.bbb_gpio.*,homeassistant.components.bbox.*,homeassistant.components.beewi_smartclim.*,homeassistant.components.bh1750.*,homeassistant.components.bitcoin.*,homeassistant.components.bizkaibus.*,homeassistant.components.blackbird.*,homeassistant.components.blebox.*,homeassistant.components.blink.*,homeassistant.components.blinksticklight.*,homeassistant.components.blinkt.*,homeassistant.components.blockchain.*,homeassistant.components.bloomsky.*,homeassistant.components.blueprint.*,homeassistant.components.bluesound.*,homeassistant.components.bluetooth_le_tracker.*,homeassistant.components.bluetooth_tracker.*,homeassistant.components.bme280.*,homeassistant.components.bme680.*,homeassistant.components.bmp280.*,homeassistant.components.bmw_connected_drive.*,homeassistant.components.braviatv.*,homeassistant.components.broadlink.*,homeassistant.components.brother.*,homeassistant.components.brottsplatskartan.*,homeassistant.components.browser.*,homeassistant.components.brunt.*,homeassistant.components.bsblan.*,homeassistant.components.bt_home_hub_5.*,homeassistant.components.bt_smarthub.*,homeassistant.components.buienradar.*,homeassistant.components.caldav.*,homeassistant.components.camera.*,homeassistant.components.canary.*,homeassistant.components.cast.*,homeassistant.components.cert_expiry.*,homeassistant.components.channels.*,homeassistant.components.circuit.*,homeassistant.components.cisco_ios.*,homeassistant.components.cisco_mobility_express.*,homeassistant.components.cisco_webex_teams.*,homeassistant.components.citybikes.*,homeassistant.components.clementine.*,homeassistant.components.clickatell.*,homeassistant.components.clicksend.*,homeassistant.components.clicksend_tts.*,homeassistant.components.climacell.*,homeassistant.components.climate.*,homeassistant.components.cloud.*,homeassistant.components.cloudflare.*,homeassistant.components.cmus.*,homeassistant.components.co2signal.*,homeassistant.components.coinbase.*,homeassistant.components.color_extractor.*,homeassistant.components.comed_hourly_pricing.*,homeassistant.components.comfoconnect.*,homeassistant.components.command_line.*,homeassistant.components.compensation.*,homeassistant.components.concord232.*,homeassistant.components.config.*,homeassistant.components.configurator.*,homeassistant.components.control4.*,homeassistant.components.conversation.*,homeassistant.components.coolmaster.*,homeassistant.components.coronavirus.*,homeassistant.components.counter.*,homeassistant.components.cppm_tracker.*,homeassistant.components.cpuspeed.*,homeassistant.components.cups.*,homeassistant.components.currencylayer.*,homeassistant.components.daikin.*,homeassistant.components.danfoss_air.*,homeassistant.components.darksky.*,homeassistant.components.datadog.*,homeassistant.components.ddwrt.*,homeassistant.components.debugpy.*,homeassistant.components.deconz.*,homeassistant.components.decora.*,homeassistant.components.decora_wifi.*,homeassistant.components.default_config.*,homeassistant.components.delijn.*,homeassistant.components.deluge.*,homeassistant.components.demo.*,homeassistant.components.denon.*,homeassistant.components.denonavr.*,homeassistant.components.deutsche_bahn.*,homeassistant.components.device_sun_light_trigger.*,homeassistant.components.device_tracker.*,homeassistant.components.devolo_home_control.*,homeassistant.components.dexcom.*,homeassistant.components.dhcp.*,homeassistant.components.dht.*,homeassistant.components.dialogflow.*,homeassistant.components.digital_ocean.*,homeassistant.components.digitalloggers.*,homeassistant.components.directv.*,homeassistant.components.discogs.*,homeassistant.components.discord.*,homeassistant.components.discovery.*,homeassistant.components.dlib_face_detect.*,homeassistant.components.dlib_face_identify.*,homeassistant.components.dlink.*,homeassistant.components.dlna_dmr.*,homeassistant.components.dnsip.*,homeassistant.components.dominos.*,homeassistant.components.doods.*,homeassistant.components.doorbird.*,homeassistant.components.dovado.*,homeassistant.components.downloader.*,homeassistant.components.dsmr.*,homeassistant.components.dsmr_reader.*,homeassistant.components.dte_energy_bridge.*,homeassistant.components.dublin_bus_transport.*,homeassistant.components.duckdns.*,homeassistant.components.dunehd.*,homeassistant.components.dwd_weather_warnings.*,homeassistant.components.dweet.*,homeassistant.components.dynalite.*,homeassistant.components.dyson.*,homeassistant.components.eafm.*,homeassistant.components.ebox.*,homeassistant.components.ebusd.*,homeassistant.components.ecoal_boiler.*,homeassistant.components.ecobee.*,homeassistant.components.econet.*,homeassistant.components.ecovacs.*,homeassistant.components.eddystone_temperature.*,homeassistant.components.edimax.*,homeassistant.components.edl21.*,homeassistant.components.ee_brightbox.*,homeassistant.components.efergy.*,homeassistant.components.egardia.*,homeassistant.components.eight_sleep.*,homeassistant.components.elgato.*,homeassistant.components.eliqonline.*,homeassistant.components.elkm1.*,homeassistant.components.elv.*,homeassistant.components.emby.*,homeassistant.components.emoncms.*,homeassistant.components.emoncms_history.*,homeassistant.components.emonitor.*,homeassistant.components.emulated_hue.*,homeassistant.components.emulated_kasa.*,homeassistant.components.emulated_roku.*,homeassistant.components.enigma2.*,homeassistant.components.enocean.*,homeassistant.components.enphase_envoy.*,homeassistant.components.entur_public_transport.*,homeassistant.components.environment_canada.*,homeassistant.components.envirophat.*,homeassistant.components.envisalink.*,homeassistant.components.ephember.*,homeassistant.components.epson.*,homeassistant.components.epsonworkforce.*,homeassistant.components.eq3btsmart.*,homeassistant.components.esphome.*,homeassistant.components.essent.*,homeassistant.components.etherscan.*,homeassistant.components.eufy.*,homeassistant.components.everlights.*,homeassistant.components.evohome.*,homeassistant.components.ezviz.*,homeassistant.components.faa_delays.*,homeassistant.components.facebook.*,homeassistant.components.facebox.*,homeassistant.components.fail2ban.*,homeassistant.components.familyhub.*,homeassistant.components.fan.*,homeassistant.components.fastdotcom.*,homeassistant.components.feedreader.*,homeassistant.components.ffmpeg.*,homeassistant.components.ffmpeg_motion.*,homeassistant.components.ffmpeg_noise.*,homeassistant.components.fibaro.*,homeassistant.components.fido.*,homeassistant.components.file.*,homeassistant.components.filesize.*,homeassistant.components.filter.*,homeassistant.components.fints.*,homeassistant.components.fireservicerota.*,homeassistant.components.firmata.*,homeassistant.components.fitbit.*,homeassistant.components.fixer.*,homeassistant.components.fleetgo.*,homeassistant.components.flexit.*,homeassistant.components.flic.*,homeassistant.components.flick_electric.*,homeassistant.components.flo.*,homeassistant.components.flock.*,homeassistant.components.flume.*,homeassistant.components.flunearyou.*,homeassistant.components.flux.*,homeassistant.components.flux_led.*,homeassistant.components.folder.*,homeassistant.components.folder_watcher.*,homeassistant.components.foobot.*,homeassistant.components.forked_daapd.*,homeassistant.components.fortios.*,homeassistant.components.foscam.*,homeassistant.components.foursquare.*,homeassistant.components.free_mobile.*,homeassistant.components.freebox.*,homeassistant.components.freedns.*,homeassistant.components.fritz.*,homeassistant.components.fritzbox.*,homeassistant.components.fritzbox_callmonitor.*,homeassistant.components.fritzbox_netmonitor.*,homeassistant.components.fronius.*,homeassistant.components.frontier_silicon.*,homeassistant.components.futurenow.*,homeassistant.components.garadget.*,homeassistant.components.garmin_connect.*,homeassistant.components.gc100.*,homeassistant.components.gdacs.*,homeassistant.components.generic.*,homeassistant.components.generic_thermostat.*,homeassistant.components.geniushub.*,homeassistant.components.geo_json_events.*,homeassistant.components.geo_rss_events.*,homeassistant.components.geofency.*,homeassistant.components.geonetnz_quakes.*,homeassistant.components.geonetnz_volcano.*,homeassistant.components.gios.*,homeassistant.components.github.*,homeassistant.components.gitlab_ci.*,homeassistant.components.gitter.*,homeassistant.components.glances.*,homeassistant.components.gntp.*,homeassistant.components.goalfeed.*,homeassistant.components.goalzero.*,homeassistant.components.gogogate2.*,homeassistant.components.google.*,homeassistant.components.google_assistant.*,homeassistant.components.google_cloud.*,homeassistant.components.google_domains.*,homeassistant.components.google_maps.*,homeassistant.components.google_pubsub.*,homeassistant.components.google_translate.*,homeassistant.components.google_travel_time.*,homeassistant.components.google_wifi.*,homeassistant.components.gpmdp.*,homeassistant.components.gpsd.*,homeassistant.components.gpslogger.*,homeassistant.components.graphite.*,homeassistant.components.gree.*,homeassistant.components.greeneye_monitor.*,homeassistant.components.greenwave.*,homeassistant.components.growatt_server.*,homeassistant.components.gstreamer.*,homeassistant.components.gtfs.*,homeassistant.components.guardian.*,homeassistant.components.habitica.*,homeassistant.components.hangouts.*,homeassistant.components.harman_kardon_avr.*,homeassistant.components.harmony.*,homeassistant.components.hassio.*,homeassistant.components.haveibeenpwned.*,homeassistant.components.hddtemp.*,homeassistant.components.hdmi_cec.*,homeassistant.components.heatmiser.*,homeassistant.components.heos.*,homeassistant.components.here_travel_time.*,homeassistant.components.hikvision.*,homeassistant.components.hikvisioncam.*,homeassistant.components.hisense_aehw4a1.*,homeassistant.components.history_stats.*,homeassistant.components.hitron_coda.*,homeassistant.components.hive.*,homeassistant.components.hlk_sw16.*,homeassistant.components.home_connect.*,homeassistant.components.home_plus_control.*,homeassistant.components.homeassistant.*,homeassistant.components.homekit.*,homeassistant.components.homekit_controller.*,homeassistant.components.homematic.*,homeassistant.components.homematicip_cloud.*,homeassistant.components.homeworks.*,homeassistant.components.honeywell.*,homeassistant.components.horizon.*,homeassistant.components.hp_ilo.*,homeassistant.components.html5.*,homeassistant.components.htu21d.*,homeassistant.components.huawei_router.*,homeassistant.components.hue.*,homeassistant.components.huisbaasje.*,homeassistant.components.humidifier.*,homeassistant.components.hunterdouglas_powerview.*,homeassistant.components.hvv_departures.*,homeassistant.components.hydrawise.*,homeassistant.components.ialarm.*,homeassistant.components.iammeter.*,homeassistant.components.iaqualink.*,homeassistant.components.icloud.*,homeassistant.components.idteck_prox.*,homeassistant.components.ifttt.*,homeassistant.components.iglo.*,homeassistant.components.ign_sismologia.*,homeassistant.components.ihc.*,homeassistant.components.image.*,homeassistant.components.imap.*,homeassistant.components.imap_email_content.*,homeassistant.components.incomfort.*,homeassistant.components.influxdb.*,homeassistant.components.input_boolean.*,homeassistant.components.input_datetime.*,homeassistant.components.input_number.*,homeassistant.components.input_select.*,homeassistant.components.input_text.*,homeassistant.components.insteon.*,homeassistant.components.intent.*,homeassistant.components.intent_script.*,homeassistant.components.intesishome.*,homeassistant.components.ios.*,homeassistant.components.iota.*,homeassistant.components.iperf3.*,homeassistant.components.ipma.*,homeassistant.components.ipp.*,homeassistant.components.iqvia.*,homeassistant.components.irish_rail_transport.*,homeassistant.components.islamic_prayer_times.*,homeassistant.components.iss.*,homeassistant.components.isy994.*,homeassistant.components.itach.*,homeassistant.components.itunes.*,homeassistant.components.izone.*,homeassistant.components.jewish_calendar.*,homeassistant.components.joaoapps_join.*,homeassistant.components.juicenet.*,homeassistant.components.kaiterra.*,homeassistant.components.kankun.*,homeassistant.components.keba.*,homeassistant.components.keenetic_ndms2.*,homeassistant.components.kef.*,homeassistant.components.keyboard.*,homeassistant.components.keyboard_remote.*,homeassistant.components.kira.*,homeassistant.components.kiwi.*,homeassistant.components.kmtronic.*,homeassistant.components.kodi.*,homeassistant.components.konnected.*,homeassistant.components.kostal_plenticore.*,homeassistant.components.kulersky.*,homeassistant.components.kwb.*,homeassistant.components.lacrosse.*,homeassistant.components.lametric.*,homeassistant.components.lannouncer.*,homeassistant.components.lastfm.*,homeassistant.components.launch_library.*,homeassistant.components.lcn.*,homeassistant.components.lg_netcast.*,homeassistant.components.lg_soundbar.*,homeassistant.components.life360.*,homeassistant.components.lifx.*,homeassistant.components.lifx_cloud.*,homeassistant.components.lifx_legacy.*,homeassistant.components.lightwave.*,homeassistant.components.limitlessled.*,homeassistant.components.linksys_smart.*,homeassistant.components.linode.*,homeassistant.components.linux_battery.*,homeassistant.components.lirc.*,homeassistant.components.litejet.*,homeassistant.components.litterrobot.*,homeassistant.components.llamalab_automate.*,homeassistant.components.local_file.*,homeassistant.components.local_ip.*,homeassistant.components.locative.*,homeassistant.components.logbook.*,homeassistant.components.logentries.*,homeassistant.components.logger.*,homeassistant.components.logi_circle.*,homeassistant.components.london_air.*,homeassistant.components.london_underground.*,homeassistant.components.loopenergy.*,homeassistant.components.lovelace.*,homeassistant.components.luci.*,homeassistant.components.luftdaten.*,homeassistant.components.lupusec.*,homeassistant.components.lutron.*,homeassistant.components.lutron_caseta.*,homeassistant.components.lw12wifi.*,homeassistant.components.lyft.*,homeassistant.components.lyric.*,homeassistant.components.magicseaweed.*,homeassistant.components.mailgun.*,homeassistant.components.manual.*,homeassistant.components.manual_mqtt.*,homeassistant.components.map.*,homeassistant.components.marytts.*,homeassistant.components.mastodon.*,homeassistant.components.matrix.*,homeassistant.components.maxcube.*,homeassistant.components.mazda.*,homeassistant.components.mcp23017.*,homeassistant.components.media_extractor.*,homeassistant.components.media_source.*,homeassistant.components.mediaroom.*,homeassistant.components.melcloud.*,homeassistant.components.melissa.*,homeassistant.components.meraki.*,homeassistant.components.message_bird.*,homeassistant.components.met.*,homeassistant.components.met_eireann.*,homeassistant.components.meteo_france.*,homeassistant.components.meteoalarm.*,homeassistant.components.metoffice.*,homeassistant.components.mfi.*,homeassistant.components.mhz19.*,homeassistant.components.microsoft.*,homeassistant.components.microsoft_face.*,homeassistant.components.microsoft_face_detect.*,homeassistant.components.microsoft_face_identify.*,homeassistant.components.miflora.*,homeassistant.components.mikrotik.*,homeassistant.components.mill.*,homeassistant.components.min_max.*,homeassistant.components.minecraft_server.*,homeassistant.components.minio.*,homeassistant.components.mitemp_bt.*,homeassistant.components.mjpeg.*,homeassistant.components.mobile_app.*,homeassistant.components.mochad.*,homeassistant.components.modbus.*,homeassistant.components.modem_callerid.*,homeassistant.components.mold_indicator.*,homeassistant.components.monoprice.*,homeassistant.components.moon.*,homeassistant.components.motion_blinds.*,homeassistant.components.motioneye.*,homeassistant.components.mpchc.*,homeassistant.components.mpd.*,homeassistant.components.mqtt.*,homeassistant.components.mqtt_eventstream.*,homeassistant.components.mqtt_json.*,homeassistant.components.mqtt_room.*,homeassistant.components.mqtt_statestream.*,homeassistant.components.msteams.*,homeassistant.components.mullvad.*,homeassistant.components.mvglive.*,homeassistant.components.my.*,homeassistant.components.mychevy.*,homeassistant.components.mycroft.*,homeassistant.components.myq.*,homeassistant.components.mysensors.*,homeassistant.components.mystrom.*,homeassistant.components.mythicbeastsdns.*,homeassistant.components.n26.*,homeassistant.components.nad.*,homeassistant.components.namecheapdns.*,homeassistant.components.nanoleaf.*,homeassistant.components.neato.*,homeassistant.components.nederlandse_spoorwegen.*,homeassistant.components.nello.*,homeassistant.components.ness_alarm.*,homeassistant.components.nest.*,homeassistant.components.netatmo.*,homeassistant.components.netdata.*,homeassistant.components.netgear.*,homeassistant.components.netgear_lte.*,homeassistant.components.netio.*,homeassistant.components.neurio_energy.*,homeassistant.components.nexia.*,homeassistant.components.nextbus.*,homeassistant.components.nextcloud.*,homeassistant.components.nfandroidtv.*,homeassistant.components.nightscout.*,homeassistant.components.niko_home_control.*,homeassistant.components.nilu.*,homeassistant.components.nissan_leaf.*,homeassistant.components.nmap_tracker.*,homeassistant.components.nmbs.*,homeassistant.components.no_ip.*,homeassistant.components.noaa_tides.*,homeassistant.components.norway_air.*,homeassistant.components.notify_events.*,homeassistant.components.notion.*,homeassistant.components.nsw_fuel_station.*,homeassistant.components.nsw_rural_fire_service_feed.*,homeassistant.components.nuheat.*,homeassistant.components.nuki.*,homeassistant.components.numato.*,homeassistant.components.nut.*,homeassistant.components.nws.*,homeassistant.components.nx584.*,homeassistant.components.nzbget.*,homeassistant.components.oasa_telematics.*,homeassistant.components.obihai.*,homeassistant.components.octoprint.*,homeassistant.components.oem.*,homeassistant.components.ohmconnect.*,homeassistant.components.ombi.*,homeassistant.components.omnilogic.*,homeassistant.components.onboarding.*,homeassistant.components.ondilo_ico.*,homeassistant.components.onewire.*,homeassistant.components.onkyo.*,homeassistant.components.onvif.*,homeassistant.components.openalpr_cloud.*,homeassistant.components.openalpr_local.*,homeassistant.components.opencv.*,homeassistant.components.openerz.*,homeassistant.components.openevse.*,homeassistant.components.openexchangerates.*,homeassistant.components.opengarage.*,homeassistant.components.openhardwaremonitor.*,homeassistant.components.openhome.*,homeassistant.components.opensensemap.*,homeassistant.components.opensky.*,homeassistant.components.opentherm_gw.*,homeassistant.components.openuv.*,homeassistant.components.openweathermap.*,homeassistant.components.opnsense.*,homeassistant.components.opple.*,homeassistant.components.orangepi_gpio.*,homeassistant.components.oru.*,homeassistant.components.orvibo.*,homeassistant.components.osramlightify.*,homeassistant.components.otp.*,homeassistant.components.ovo_energy.*,homeassistant.components.owntracks.*,homeassistant.components.ozw.*,homeassistant.components.panasonic_bluray.*,homeassistant.components.panasonic_viera.*,homeassistant.components.pandora.*,homeassistant.components.panel_custom.*,homeassistant.components.panel_iframe.*,homeassistant.components.pcal9535a.*,homeassistant.components.pencom.*,homeassistant.components.person.*,homeassistant.components.philips_js.*,homeassistant.components.pi4ioe5v9xxxx.*,homeassistant.components.pi_hole.*,homeassistant.components.picnic.*,homeassistant.components.picotts.*,homeassistant.components.piglow.*,homeassistant.components.pilight.*,homeassistant.components.ping.*,homeassistant.components.pioneer.*,homeassistant.components.pjlink.*,homeassistant.components.plaato.*,homeassistant.components.plant.*,homeassistant.components.plex.*,homeassistant.components.plugwise.*,homeassistant.components.plum_lightpad.*,homeassistant.components.pocketcasts.*,homeassistant.components.point.*,homeassistant.components.poolsense.*,homeassistant.components.powerwall.*,homeassistant.components.profiler.*,homeassistant.components.progettihwsw.*,homeassistant.components.proliphix.*,homeassistant.components.prometheus.*,homeassistant.components.prowl.*,homeassistant.components.proxmoxve.*,homeassistant.components.proxy.*,homeassistant.components.ps4.*,homeassistant.components.pulseaudio_loopback.*,homeassistant.components.push.*,homeassistant.components.pushbullet.*,homeassistant.components.pushover.*,homeassistant.components.pushsafer.*,homeassistant.components.pvoutput.*,homeassistant.components.pvpc_hourly_pricing.*,homeassistant.components.pyload.*,homeassistant.components.python_script.*,homeassistant.components.qbittorrent.*,homeassistant.components.qld_bushfire.*,homeassistant.components.qnap.*,homeassistant.components.qrcode.*,homeassistant.components.quantum_gateway.*,homeassistant.components.qvr_pro.*,homeassistant.components.qwikswitch.*,homeassistant.components.rachio.*,homeassistant.components.radarr.*,homeassistant.components.radiotherm.*,homeassistant.components.rainbird.*,homeassistant.components.raincloud.*,homeassistant.components.rainforest_eagle.*,homeassistant.components.rainmachine.*,homeassistant.components.random.*,homeassistant.components.raspihats.*,homeassistant.components.raspyrfm.*,homeassistant.components.recollect_waste.*,homeassistant.components.recorder.*,homeassistant.components.recswitch.*,homeassistant.components.reddit.*,homeassistant.components.rejseplanen.*,homeassistant.components.remember_the_milk.*,homeassistant.components.remote_rpi_gpio.*,homeassistant.components.repetier.*,homeassistant.components.rest.*,homeassistant.components.rest_command.*,homeassistant.components.rflink.*,homeassistant.components.rfxtrx.*,homeassistant.components.ring.*,homeassistant.components.ripple.*,homeassistant.components.risco.*,homeassistant.components.rituals_perfume_genie.*,homeassistant.components.rmvtransport.*,homeassistant.components.rocketchat.*,homeassistant.components.roku.*,homeassistant.components.roomba.*,homeassistant.components.roon.*,homeassistant.components.route53.*,homeassistant.components.rova.*,homeassistant.components.rpi_camera.*,homeassistant.components.rpi_gpio.*,homeassistant.components.rpi_gpio_pwm.*,homeassistant.components.rpi_pfio.*,homeassistant.components.rpi_power.*,homeassistant.components.rpi_rf.*,homeassistant.components.rss_feed_template.*,homeassistant.components.rtorrent.*,homeassistant.components.ruckus_unleashed.*,homeassistant.components.russound_rio.*,homeassistant.components.russound_rnet.*,homeassistant.components.sabnzbd.*,homeassistant.components.safe_mode.*,homeassistant.components.saj.*,homeassistant.components.samsungtv.*,homeassistant.components.satel_integra.*,homeassistant.components.schluter.*,homeassistant.components.scrape.*,homeassistant.components.screenlogic.*,homeassistant.components.script.*,homeassistant.components.scsgate.*,homeassistant.components.search.*,homeassistant.components.season.*,homeassistant.components.sendgrid.*,homeassistant.components.sense.*,homeassistant.components.sensehat.*,homeassistant.components.sensibo.*,homeassistant.components.sentry.*,homeassistant.components.serial.*,homeassistant.components.serial_pm.*,homeassistant.components.sesame.*,homeassistant.components.seven_segments.*,homeassistant.components.seventeentrack.*,homeassistant.components.sharkiq.*,homeassistant.components.shell_command.*,homeassistant.components.shelly.*,homeassistant.components.shiftr.*,homeassistant.components.shodan.*,homeassistant.components.shopping_list.*,homeassistant.components.sht31.*,homeassistant.components.sigfox.*,homeassistant.components.sighthound.*,homeassistant.components.signal_messenger.*,homeassistant.components.simplepush.*,homeassistant.components.simplisafe.*,homeassistant.components.simulated.*,homeassistant.components.sinch.*,homeassistant.components.sisyphus.*,homeassistant.components.sky_hub.*,homeassistant.components.skybeacon.*,homeassistant.components.skybell.*,homeassistant.components.sleepiq.*,homeassistant.components.slide.*,homeassistant.components.sma.*,homeassistant.components.smappee.*,homeassistant.components.smart_meter_texas.*,homeassistant.components.smarthab.*,homeassistant.components.smartthings.*,homeassistant.components.smarttub.*,homeassistant.components.smarty.*,homeassistant.components.smhi.*,homeassistant.components.sms.*,homeassistant.components.smtp.*,homeassistant.components.snapcast.*,homeassistant.components.snips.*,homeassistant.components.snmp.*,homeassistant.components.sochain.*,homeassistant.components.solaredge.*,homeassistant.components.solaredge_local.*,homeassistant.components.solarlog.*,homeassistant.components.solax.*,homeassistant.components.soma.*,homeassistant.components.somfy.*,homeassistant.components.somfy_mylink.*,homeassistant.components.sonarr.*,homeassistant.components.songpal.*,homeassistant.components.sonos.*,homeassistant.components.sony_projector.*,homeassistant.components.soundtouch.*,homeassistant.components.spaceapi.*,homeassistant.components.spc.*,homeassistant.components.speedtestdotnet.*,homeassistant.components.spider.*,homeassistant.components.splunk.*,homeassistant.components.spotcrime.*,homeassistant.components.spotify.*,homeassistant.components.sql.*,homeassistant.components.squeezebox.*,homeassistant.components.srp_energy.*,homeassistant.components.ssdp.*,homeassistant.components.starline.*,homeassistant.components.starlingbank.*,homeassistant.components.startca.*,homeassistant.components.statistics.*,homeassistant.components.statsd.*,homeassistant.components.steam_online.*,homeassistant.components.stiebel_eltron.*,homeassistant.components.stookalert.*,homeassistant.components.stream.*,homeassistant.components.streamlabswater.*,homeassistant.components.stt.*,homeassistant.components.subaru.*,homeassistant.components.suez_water.*,homeassistant.components.supervisord.*,homeassistant.components.supla.*,homeassistant.components.surepetcare.*,homeassistant.components.swiss_hydrological_data.*,homeassistant.components.swiss_public_transport.*,homeassistant.components.swisscom.*,homeassistant.components.switchbot.*,homeassistant.components.switcher_kis.*,homeassistant.components.switchmate.*,homeassistant.components.syncthru.*,homeassistant.components.synology_chat.*,homeassistant.components.synology_dsm.*,homeassistant.components.synology_srm.*,homeassistant.components.syslog.*,homeassistant.components.system_health.*,homeassistant.components.system_log.*,homeassistant.components.tado.*,homeassistant.components.tag.*,homeassistant.components.tahoma.*,homeassistant.components.tank_utility.*,homeassistant.components.tankerkoenig.*,homeassistant.components.tapsaff.*,homeassistant.components.tasmota.*,homeassistant.components.tautulli.*,homeassistant.components.tcp.*,homeassistant.components.ted5000.*,homeassistant.components.telegram.*,homeassistant.components.telegram_bot.*,homeassistant.components.tellduslive.*,homeassistant.components.tellstick.*,homeassistant.components.telnet.*,homeassistant.components.temper.*,homeassistant.components.template.*,homeassistant.components.tensorflow.*,homeassistant.components.tesla.*,homeassistant.components.tfiac.*,homeassistant.components.thermoworks_smoke.*,homeassistant.components.thethingsnetwork.*,homeassistant.components.thingspeak.*,homeassistant.components.thinkingcleaner.*,homeassistant.components.thomson.*,homeassistant.components.threshold.*,homeassistant.components.tibber.*,homeassistant.components.tikteck.*,homeassistant.components.tile.*,homeassistant.components.time_date.*,homeassistant.components.timer.*,homeassistant.components.tmb.*,homeassistant.components.tod.*,homeassistant.components.todoist.*,homeassistant.components.tof.*,homeassistant.components.tomato.*,homeassistant.components.toon.*,homeassistant.components.torque.*,homeassistant.components.totalconnect.*,homeassistant.components.touchline.*,homeassistant.components.tplink.*,homeassistant.components.tplink_lte.*,homeassistant.components.traccar.*,homeassistant.components.trace.*,homeassistant.components.trackr.*,homeassistant.components.tradfri.*,homeassistant.components.trafikverket_train.*,homeassistant.components.trafikverket_weatherstation.*,homeassistant.components.transmission.*,homeassistant.components.transport_nsw.*,homeassistant.components.travisci.*,homeassistant.components.trend.*,homeassistant.components.tuya.*,homeassistant.components.twentemilieu.*,homeassistant.components.twilio.*,homeassistant.components.twilio_call.*,homeassistant.components.twilio_sms.*,homeassistant.components.twinkly.*,homeassistant.components.twitch.*,homeassistant.components.twitter.*,homeassistant.components.ubus.*,homeassistant.components.ue_smart_radio.*,homeassistant.components.uk_transport.*,homeassistant.components.unifi.*,homeassistant.components.unifi_direct.*,homeassistant.components.unifiled.*,homeassistant.components.universal.*,homeassistant.components.upb.*,homeassistant.components.upc_connect.*,homeassistant.components.upcloud.*,homeassistant.components.updater.*,homeassistant.components.upnp.*,homeassistant.components.uptime.*,homeassistant.components.uptimerobot.*,homeassistant.components.uscis.*,homeassistant.components.usgs_earthquakes_feed.*,homeassistant.components.utility_meter.*,homeassistant.components.uvc.*,homeassistant.components.vallox.*,homeassistant.components.vasttrafik.*,homeassistant.components.velbus.*,homeassistant.components.velux.*,homeassistant.components.venstar.*,homeassistant.components.vera.*,homeassistant.components.verisure.*,homeassistant.components.versasense.*,homeassistant.components.version.*,homeassistant.components.vesync.*,homeassistant.components.viaggiatreno.*,homeassistant.components.vicare.*,homeassistant.components.vilfo.*,homeassistant.components.vivotek.*,homeassistant.components.vizio.*,homeassistant.components.vlc.*,homeassistant.components.vlc_telnet.*,homeassistant.components.voicerss.*,homeassistant.components.volkszaehler.*,homeassistant.components.volumio.*,homeassistant.components.volvooncall.*,homeassistant.components.vultr.*,homeassistant.components.w800rf32.*,homeassistant.components.wake_on_lan.*,homeassistant.components.waqi.*,homeassistant.components.waterfurnace.*,homeassistant.components.watson_iot.*,homeassistant.components.watson_tts.*,homeassistant.components.waze_travel_time.*,homeassistant.components.webhook.*,homeassistant.components.webostv.*,homeassistant.components.wemo.*,homeassistant.components.whois.*,homeassistant.components.wiffi.*,homeassistant.components.wilight.*,homeassistant.components.wink.*,homeassistant.components.wirelesstag.*,homeassistant.components.withings.*,homeassistant.components.wled.*,homeassistant.components.wolflink.*,homeassistant.components.workday.*,homeassistant.components.worldclock.*,homeassistant.components.worldtidesinfo.*,homeassistant.components.worxlandroid.*,homeassistant.components.wsdot.*,homeassistant.components.wunderground.*,homeassistant.components.x10.*,homeassistant.components.xbee.*,homeassistant.components.xbox.*,homeassistant.components.xbox_live.*,homeassistant.components.xeoma.*,homeassistant.components.xiaomi.*,homeassistant.components.xiaomi_aqara.*,homeassistant.components.xiaomi_miio.*,homeassistant.components.xiaomi_tv.*,homeassistant.components.xmpp.*,homeassistant.components.xs1.*,homeassistant.components.yale_smart_alarm.*,homeassistant.components.yamaha.*,homeassistant.components.yamaha_musiccast.*,homeassistant.components.yandex_transport.*,homeassistant.components.yandextts.*,homeassistant.components.yeelight.*,homeassistant.components.yeelightsunflower.*,homeassistant.components.yi.*,homeassistant.components.zabbix.*,homeassistant.components.zamg.*,homeassistant.components.zengge.*,homeassistant.components.zerproc.*,homeassistant.components.zestimate.*,homeassistant.components.zha.*,homeassistant.components.zhong_hong.*,homeassistant.components.ziggo_mediabox_xl.*,homeassistant.components.zodiac.*,homeassistant.components.zoneminder.*,homeassistant.components.zwave.*] +check_untyped_defs = false +disallow_incomplete_defs = false +disallow_subclassing_any = false +disallow_untyped_calls = false +disallow_untyped_decorators = false +disallow_untyped_defs = false +no_implicit_optional = false +strict_equality = false +warn_return_any = false +warn_unreachable = false +warn_unused_ignores = false + +[mypy-homeassistant.components.adguard.*,homeassistant.components.aemet.*,homeassistant.components.airly.*,homeassistant.components.alarmdecoder.*,homeassistant.components.alexa.*,homeassistant.components.almond.*,homeassistant.components.amcrest.*,homeassistant.components.analytics.*,homeassistant.components.asuswrt.*,homeassistant.components.atag.*,homeassistant.components.aurora.*,homeassistant.components.awair.*,homeassistant.components.axis.*,homeassistant.components.azure_devops.*,homeassistant.components.azure_event_hub.*,homeassistant.components.blueprint.*,homeassistant.components.bluetooth_tracker.*,homeassistant.components.bmw_connected_drive.*,homeassistant.components.bsblan.*,homeassistant.components.camera.*,homeassistant.components.canary.*,homeassistant.components.cast.*,homeassistant.components.cert_expiry.*,homeassistant.components.climacell.*,homeassistant.components.climate.*,homeassistant.components.cloud.*,homeassistant.components.cloudflare.*,homeassistant.components.config.*,homeassistant.components.control4.*,homeassistant.components.conversation.*,homeassistant.components.deconz.*,homeassistant.components.demo.*,homeassistant.components.denonavr.*,homeassistant.components.device_tracker.*,homeassistant.components.devolo_home_control.*,homeassistant.components.dhcp.*,homeassistant.components.directv.*,homeassistant.components.doorbird.*,homeassistant.components.dsmr.*,homeassistant.components.dynalite.*,homeassistant.components.eafm.*,homeassistant.components.edl21.*,homeassistant.components.elgato.*,homeassistant.components.elkm1.*,homeassistant.components.emonitor.*,homeassistant.components.enphase_envoy.*,homeassistant.components.entur_public_transport.*,homeassistant.components.esphome.*,homeassistant.components.evohome.*,homeassistant.components.fan.*,homeassistant.components.filter.*,homeassistant.components.fints.*,homeassistant.components.fireservicerota.*,homeassistant.components.firmata.*,homeassistant.components.fitbit.*,homeassistant.components.flo.*,homeassistant.components.fortios.*,homeassistant.components.foscam.*,homeassistant.components.freebox.*,homeassistant.components.fritz.*,homeassistant.components.fritzbox.*,homeassistant.components.garmin_connect.*,homeassistant.components.geniushub.*,homeassistant.components.gios.*,homeassistant.components.glances.*,homeassistant.components.gogogate2.*,homeassistant.components.google_assistant.*,homeassistant.components.google_maps.*,homeassistant.components.google_pubsub.*,homeassistant.components.gpmdp.*,homeassistant.components.gree.*,homeassistant.components.growatt_server.*,homeassistant.components.gtfs.*,homeassistant.components.guardian.*,homeassistant.components.habitica.*,homeassistant.components.harmony.*,homeassistant.components.hassio.*,homeassistant.components.hdmi_cec.*,homeassistant.components.here_travel_time.*,homeassistant.components.hisense_aehw4a1.*,homeassistant.components.home_connect.*,homeassistant.components.home_plus_control.*,homeassistant.components.homeassistant.*,homeassistant.components.homekit.*,homeassistant.components.homekit_controller.*,homeassistant.components.homematicip_cloud.*,homeassistant.components.honeywell.*,homeassistant.components.hue.*,homeassistant.components.huisbaasje.*,homeassistant.components.humidifier.*,homeassistant.components.iaqualink.*,homeassistant.components.icloud.*,homeassistant.components.ihc.*,homeassistant.components.image.*,homeassistant.components.incomfort.*,homeassistant.components.influxdb.*,homeassistant.components.input_boolean.*,homeassistant.components.input_datetime.*,homeassistant.components.input_number.*,homeassistant.components.insteon.*,homeassistant.components.ipp.*,homeassistant.components.isy994.*,homeassistant.components.izone.*,homeassistant.components.kaiterra.*,homeassistant.components.keenetic_ndms2.*,homeassistant.components.kodi.*,homeassistant.components.konnected.*,homeassistant.components.kostal_plenticore.*,homeassistant.components.kulersky.*,homeassistant.components.lifx.*,homeassistant.components.litejet.*,homeassistant.components.litterrobot.*,homeassistant.components.lovelace.*,homeassistant.components.luftdaten.*,homeassistant.components.lutron_caseta.*,homeassistant.components.lyric.*,homeassistant.components.marytts.*,homeassistant.components.media_source.*,homeassistant.components.melcloud.*,homeassistant.components.meteo_france.*,homeassistant.components.metoffice.*,homeassistant.components.minecraft_server.*,homeassistant.components.mobile_app.*,homeassistant.components.modbus.*,homeassistant.components.motion_blinds.*,homeassistant.components.motioneye.*,homeassistant.components.mqtt.*,homeassistant.components.mullvad.*,homeassistant.components.mysensors.*,homeassistant.components.n26.*,homeassistant.components.neato.*,homeassistant.components.ness_alarm.*,homeassistant.components.nest.*,homeassistant.components.netatmo.*,homeassistant.components.netio.*,homeassistant.components.nightscout.*,homeassistant.components.nilu.*,homeassistant.components.nmap_tracker.*,homeassistant.components.norway_air.*,homeassistant.components.notion.*,homeassistant.components.nsw_fuel_station.*,homeassistant.components.nuki.*,homeassistant.components.nws.*,homeassistant.components.nzbget.*,homeassistant.components.omnilogic.*,homeassistant.components.onboarding.*,homeassistant.components.ondilo_ico.*,homeassistant.components.onewire.*,homeassistant.components.onvif.*,homeassistant.components.ovo_energy.*,homeassistant.components.ozw.*,homeassistant.components.panasonic_viera.*,homeassistant.components.philips_js.*,homeassistant.components.pilight.*,homeassistant.components.ping.*,homeassistant.components.pioneer.*,homeassistant.components.plaato.*,homeassistant.components.plex.*,homeassistant.components.plugwise.*,homeassistant.components.plum_lightpad.*,homeassistant.components.point.*,homeassistant.components.profiler.*,homeassistant.components.proxmoxve.*,homeassistant.components.rachio.*,homeassistant.components.rainmachine.*,homeassistant.components.recollect_waste.*,homeassistant.components.recorder.*,homeassistant.components.reddit.*,homeassistant.components.ring.*,homeassistant.components.rituals_perfume_genie.*,homeassistant.components.roku.*,homeassistant.components.rpi_power.*,homeassistant.components.ruckus_unleashed.*,homeassistant.components.sabnzbd.*,homeassistant.components.screenlogic.*,homeassistant.components.script.*,homeassistant.components.search.*,homeassistant.components.sense.*,homeassistant.components.sentry.*,homeassistant.components.sesame.*,homeassistant.components.sharkiq.*,homeassistant.components.shell_command.*,homeassistant.components.shelly.*,homeassistant.components.sma.*,homeassistant.components.smart_meter_texas.*,homeassistant.components.smartthings.*,homeassistant.components.smarttub.*,homeassistant.components.smarty.*,homeassistant.components.smhi.*,homeassistant.components.solaredge.*,homeassistant.components.solarlog.*,homeassistant.components.somfy.*,homeassistant.components.somfy_mylink.*,homeassistant.components.sonarr.*,homeassistant.components.songpal.*,homeassistant.components.sonos.*,homeassistant.components.spotify.*,homeassistant.components.stream.*,homeassistant.components.stt.*,homeassistant.components.surepetcare.*,homeassistant.components.switchbot.*,homeassistant.components.switcher_kis.*,homeassistant.components.synology_dsm.*,homeassistant.components.synology_srm.*,homeassistant.components.system_health.*,homeassistant.components.system_log.*,homeassistant.components.tado.*,homeassistant.components.tasmota.*,homeassistant.components.tcp.*,homeassistant.components.telegram_bot.*,homeassistant.components.template.*,homeassistant.components.tesla.*,homeassistant.components.timer.*,homeassistant.components.todoist.*,homeassistant.components.toon.*,homeassistant.components.tplink.*,homeassistant.components.trace.*,homeassistant.components.tradfri.*,homeassistant.components.tuya.*,homeassistant.components.twentemilieu.*,homeassistant.components.unifi.*,homeassistant.components.upcloud.*,homeassistant.components.updater.*,homeassistant.components.upnp.*,homeassistant.components.velbus.*,homeassistant.components.vera.*,homeassistant.components.verisure.*,homeassistant.components.vizio.*,homeassistant.components.volumio.*,homeassistant.components.webostv.*,homeassistant.components.wemo.*,homeassistant.components.wink.*,homeassistant.components.withings.*,homeassistant.components.wled.*,homeassistant.components.wunderground.*,homeassistant.components.xbox.*,homeassistant.components.xiaomi_aqara.*,homeassistant.components.xiaomi_miio.*,homeassistant.components.yamaha.*,homeassistant.components.yeelight.*,homeassistant.components.zerproc.*,homeassistant.components.zha.*,homeassistant.components.zwave.*] +ignore_errors = true diff --git a/script/hassfest/__main__.py b/script/hassfest/__main__.py index 8edc3ec6eb6c0..f9a1aa54c6984 100644 --- a/script/hassfest/__main__.py +++ b/script/hassfest/__main__.py @@ -13,6 +13,7 @@ json, manifest, mqtt, + mypy_config, requirements, services, ssdp, @@ -36,6 +37,7 @@ ] HASS_PLUGINS = [ coverage, + mypy_config, ] diff --git a/script/hassfest/model.py b/script/hassfest/model.py index 3bb46d4c230bb..eee25df079de8 100644 --- a/script/hassfest/model.py +++ b/script/hassfest/model.py @@ -33,7 +33,7 @@ class Config: errors: list[Error] = attr.ib(factory=list) cache: dict[str, Any] = attr.ib(factory=dict) - def add_error(self, *args, **kwargs): + def add_error(self, *args: Any, **kwargs: Any) -> None: """Add an error.""" self.errors.append(Error(*args, **kwargs)) @@ -96,7 +96,7 @@ def dependencies(self) -> list[str]: """List of dependencies.""" return self.manifest.get("dependencies", []) - def add_error(self, *args, **kwargs): + def add_error(self, *args: Any, **kwargs: Any) -> None: """Add an error.""" self.errors.append(Error(*args, **kwargs)) diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py new file mode 100644 index 0000000000000..a5ca0fbfc3b0e --- /dev/null +++ b/script/hassfest/mypy_config.py @@ -0,0 +1,366 @@ +"""Generate mypy config.""" +from __future__ import annotations + +import configparser +import io +from typing import Final + +from .model import Config, Integration + +# Modules which have type hints which known to be broken. +# If you are an author of component listed here, please fix these errors and +# remove your component from this list to enable type checks. +# Do your best to not add anything new here. +IGNORED_MODULES: Final[list[str]] = [ + "homeassistant.components.adguard.*", + "homeassistant.components.aemet.*", + "homeassistant.components.airly.*", + "homeassistant.components.alarmdecoder.*", + "homeassistant.components.alexa.*", + "homeassistant.components.almond.*", + "homeassistant.components.amcrest.*", + "homeassistant.components.analytics.*", + "homeassistant.components.asuswrt.*", + "homeassistant.components.atag.*", + "homeassistant.components.aurora.*", + "homeassistant.components.awair.*", + "homeassistant.components.axis.*", + "homeassistant.components.azure_devops.*", + "homeassistant.components.azure_event_hub.*", + "homeassistant.components.blueprint.*", + "homeassistant.components.bluetooth_tracker.*", + "homeassistant.components.bmw_connected_drive.*", + "homeassistant.components.bsblan.*", + "homeassistant.components.camera.*", + "homeassistant.components.canary.*", + "homeassistant.components.cast.*", + "homeassistant.components.cert_expiry.*", + "homeassistant.components.climacell.*", + "homeassistant.components.climate.*", + "homeassistant.components.cloud.*", + "homeassistant.components.cloudflare.*", + "homeassistant.components.config.*", + "homeassistant.components.control4.*", + "homeassistant.components.conversation.*", + "homeassistant.components.deconz.*", + "homeassistant.components.demo.*", + "homeassistant.components.denonavr.*", + "homeassistant.components.device_tracker.*", + "homeassistant.components.devolo_home_control.*", + "homeassistant.components.dhcp.*", + "homeassistant.components.directv.*", + "homeassistant.components.doorbird.*", + "homeassistant.components.dsmr.*", + "homeassistant.components.dynalite.*", + "homeassistant.components.eafm.*", + "homeassistant.components.edl21.*", + "homeassistant.components.elgato.*", + "homeassistant.components.elkm1.*", + "homeassistant.components.emonitor.*", + "homeassistant.components.enphase_envoy.*", + "homeassistant.components.entur_public_transport.*", + "homeassistant.components.esphome.*", + "homeassistant.components.evohome.*", + "homeassistant.components.fan.*", + "homeassistant.components.filter.*", + "homeassistant.components.fints.*", + "homeassistant.components.fireservicerota.*", + "homeassistant.components.firmata.*", + "homeassistant.components.fitbit.*", + "homeassistant.components.flo.*", + "homeassistant.components.fortios.*", + "homeassistant.components.foscam.*", + "homeassistant.components.freebox.*", + "homeassistant.components.fritz.*", + "homeassistant.components.fritzbox.*", + "homeassistant.components.garmin_connect.*", + "homeassistant.components.geniushub.*", + "homeassistant.components.gios.*", + "homeassistant.components.glances.*", + "homeassistant.components.gogogate2.*", + "homeassistant.components.google_assistant.*", + "homeassistant.components.google_maps.*", + "homeassistant.components.google_pubsub.*", + "homeassistant.components.gpmdp.*", + "homeassistant.components.gree.*", + "homeassistant.components.growatt_server.*", + "homeassistant.components.gtfs.*", + "homeassistant.components.guardian.*", + "homeassistant.components.habitica.*", + "homeassistant.components.harmony.*", + "homeassistant.components.hassio.*", + "homeassistant.components.hdmi_cec.*", + "homeassistant.components.here_travel_time.*", + "homeassistant.components.hisense_aehw4a1.*", + "homeassistant.components.home_connect.*", + "homeassistant.components.home_plus_control.*", + "homeassistant.components.homeassistant.*", + "homeassistant.components.homekit.*", + "homeassistant.components.homekit_controller.*", + "homeassistant.components.homematicip_cloud.*", + "homeassistant.components.honeywell.*", + "homeassistant.components.hue.*", + "homeassistant.components.huisbaasje.*", + "homeassistant.components.humidifier.*", + "homeassistant.components.iaqualink.*", + "homeassistant.components.icloud.*", + "homeassistant.components.ihc.*", + "homeassistant.components.image.*", + "homeassistant.components.incomfort.*", + "homeassistant.components.influxdb.*", + "homeassistant.components.input_boolean.*", + "homeassistant.components.input_datetime.*", + "homeassistant.components.input_number.*", + "homeassistant.components.insteon.*", + "homeassistant.components.ipp.*", + "homeassistant.components.isy994.*", + "homeassistant.components.izone.*", + "homeassistant.components.kaiterra.*", + "homeassistant.components.keenetic_ndms2.*", + "homeassistant.components.kodi.*", + "homeassistant.components.konnected.*", + "homeassistant.components.kostal_plenticore.*", + "homeassistant.components.kulersky.*", + "homeassistant.components.lifx.*", + "homeassistant.components.litejet.*", + "homeassistant.components.litterrobot.*", + "homeassistant.components.lovelace.*", + "homeassistant.components.luftdaten.*", + "homeassistant.components.lutron_caseta.*", + "homeassistant.components.lyric.*", + "homeassistant.components.marytts.*", + "homeassistant.components.media_source.*", + "homeassistant.components.melcloud.*", + "homeassistant.components.meteo_france.*", + "homeassistant.components.metoffice.*", + "homeassistant.components.minecraft_server.*", + "homeassistant.components.mobile_app.*", + "homeassistant.components.modbus.*", + "homeassistant.components.motion_blinds.*", + "homeassistant.components.motioneye.*", + "homeassistant.components.mqtt.*", + "homeassistant.components.mullvad.*", + "homeassistant.components.mysensors.*", + "homeassistant.components.n26.*", + "homeassistant.components.neato.*", + "homeassistant.components.ness_alarm.*", + "homeassistant.components.nest.*", + "homeassistant.components.netatmo.*", + "homeassistant.components.netio.*", + "homeassistant.components.nightscout.*", + "homeassistant.components.nilu.*", + "homeassistant.components.nmap_tracker.*", + "homeassistant.components.norway_air.*", + "homeassistant.components.notion.*", + "homeassistant.components.nsw_fuel_station.*", + "homeassistant.components.nuki.*", + "homeassistant.components.nws.*", + "homeassistant.components.nzbget.*", + "homeassistant.components.omnilogic.*", + "homeassistant.components.onboarding.*", + "homeassistant.components.ondilo_ico.*", + "homeassistant.components.onewire.*", + "homeassistant.components.onvif.*", + "homeassistant.components.ovo_energy.*", + "homeassistant.components.ozw.*", + "homeassistant.components.panasonic_viera.*", + "homeassistant.components.philips_js.*", + "homeassistant.components.pilight.*", + "homeassistant.components.ping.*", + "homeassistant.components.pioneer.*", + "homeassistant.components.plaato.*", + "homeassistant.components.plex.*", + "homeassistant.components.plugwise.*", + "homeassistant.components.plum_lightpad.*", + "homeassistant.components.point.*", + "homeassistant.components.profiler.*", + "homeassistant.components.proxmoxve.*", + "homeassistant.components.rachio.*", + "homeassistant.components.rainmachine.*", + "homeassistant.components.recollect_waste.*", + "homeassistant.components.recorder.*", + "homeassistant.components.reddit.*", + "homeassistant.components.ring.*", + "homeassistant.components.rituals_perfume_genie.*", + "homeassistant.components.roku.*", + "homeassistant.components.rpi_power.*", + "homeassistant.components.ruckus_unleashed.*", + "homeassistant.components.sabnzbd.*", + "homeassistant.components.screenlogic.*", + "homeassistant.components.script.*", + "homeassistant.components.search.*", + "homeassistant.components.sense.*", + "homeassistant.components.sentry.*", + "homeassistant.components.sesame.*", + "homeassistant.components.sharkiq.*", + "homeassistant.components.shell_command.*", + "homeassistant.components.shelly.*", + "homeassistant.components.sma.*", + "homeassistant.components.smart_meter_texas.*", + "homeassistant.components.smartthings.*", + "homeassistant.components.smarttub.*", + "homeassistant.components.smarty.*", + "homeassistant.components.smhi.*", + "homeassistant.components.solaredge.*", + "homeassistant.components.solarlog.*", + "homeassistant.components.somfy.*", + "homeassistant.components.somfy_mylink.*", + "homeassistant.components.sonarr.*", + "homeassistant.components.songpal.*", + "homeassistant.components.sonos.*", + "homeassistant.components.spotify.*", + "homeassistant.components.stream.*", + "homeassistant.components.stt.*", + "homeassistant.components.surepetcare.*", + "homeassistant.components.switchbot.*", + "homeassistant.components.switcher_kis.*", + "homeassistant.components.synology_dsm.*", + "homeassistant.components.synology_srm.*", + "homeassistant.components.system_health.*", + "homeassistant.components.system_log.*", + "homeassistant.components.tado.*", + "homeassistant.components.tasmota.*", + "homeassistant.components.tcp.*", + "homeassistant.components.telegram_bot.*", + "homeassistant.components.template.*", + "homeassistant.components.tesla.*", + "homeassistant.components.timer.*", + "homeassistant.components.todoist.*", + "homeassistant.components.toon.*", + "homeassistant.components.tplink.*", + "homeassistant.components.trace.*", + "homeassistant.components.tradfri.*", + "homeassistant.components.tuya.*", + "homeassistant.components.twentemilieu.*", + "homeassistant.components.unifi.*", + "homeassistant.components.upcloud.*", + "homeassistant.components.updater.*", + "homeassistant.components.upnp.*", + "homeassistant.components.velbus.*", + "homeassistant.components.vera.*", + "homeassistant.components.verisure.*", + "homeassistant.components.vizio.*", + "homeassistant.components.volumio.*", + "homeassistant.components.webostv.*", + "homeassistant.components.wemo.*", + "homeassistant.components.wink.*", + "homeassistant.components.withings.*", + "homeassistant.components.wled.*", + "homeassistant.components.wunderground.*", + "homeassistant.components.xbox.*", + "homeassistant.components.xiaomi_aqara.*", + "homeassistant.components.xiaomi_miio.*", + "homeassistant.components.yamaha.*", + "homeassistant.components.yeelight.*", + "homeassistant.components.zerproc.*", + "homeassistant.components.zha.*", + "homeassistant.components.zwave.*", +] + +HEADER: Final = """ +# Automatically generated by hassfest. +# +# To update, run python3 -m script.hassfest + +""".lstrip() + +GENERAL_SETTINGS: Final[dict[str, str]] = { + "python_version": "3.8", + "show_error_codes": "true", + "follow_imports": "silent", + "ignore_missing_imports": "true", + "warn_incomplete_stub": "true", + "warn_redundant_casts": "true", + "warn_unused_configs": "true", +} + +# This is basically the list of checks which is enabled for "strict=true". +# But "strict=true" is applied globally, so we need to list all checks manually. +STRICT_SETTINGS: Final[list[str]] = [ + "check_untyped_defs", + "disallow_incomplete_defs", + "disallow_subclassing_any", + "disallow_untyped_calls", + "disallow_untyped_decorators", + "disallow_untyped_defs", + "no_implicit_optional", + "strict_equality", + "warn_return_any", + "warn_unreachable", + "warn_unused_ignores", + # TODO: turn these on, address issues + # "disallow_any_generics", + # "no_implicit_reexport", +] + + +def generate_and_validate(config: Config) -> str: + """Validate and generate mypy config.""" + + strict_disabled_path = config.root / ".no-strict-typing" + + with strict_disabled_path.open() as fp: + lines = fp.readlines() + + # Filter empty and commented lines. + not_strict_modules: list[str] = [ + line.strip() + for line in lines + if line.strip() != "" and not line.startswith("#") + ] + for module in not_strict_modules: + if not module.startswith("homeassistant.components."): + config.add_error( + "mypy_config", f"Only components should be added: {module}" + ) + not_strict_modules_set: set[str] = set(not_strict_modules) + for module in IGNORED_MODULES: + if module not in not_strict_modules_set: + config.add_error( + "mypy_config", + f"Ignored module '{module} must be excluded from strict typing", + ) + + mypy_config = configparser.ConfigParser() + + general_section = "mypy" + mypy_config.add_section(general_section) + for key, value in GENERAL_SETTINGS.items(): + mypy_config.set(general_section, key, value) + for key in STRICT_SETTINGS: + mypy_config.set(general_section, key, "true") + + strict_disabled_section = "mypy-" + ",".join(not_strict_modules) + mypy_config.add_section(strict_disabled_section) + for key in STRICT_SETTINGS: + mypy_config.set(strict_disabled_section, key, "false") + + ignored_section = "mypy-" + ",".join(IGNORED_MODULES) + mypy_config.add_section(ignored_section) + mypy_config.set(ignored_section, "ignore_errors", "true") + + with io.StringIO() as fp: + mypy_config.write(fp) + fp.seek(0) + return HEADER + fp.read().strip() + + +def validate(integrations: dict[str, Integration], config: Config) -> None: + """Validate mypy config.""" + config_path = config.root / "mypy.ini" + config.cache["mypy_config"] = content = generate_and_validate(config) + + with open(str(config_path)) as fp: + if fp.read().strip() != content: + config.add_error( + "mypy_config", + "File mypy.ini is not up to date. Run python3 -m script.hassfest", + fixable=True, + ) + + +def generate(integrations: dict[str, Integration], config: Config) -> None: + """Generate mypy config.""" + config_path = config.root / "mypy.ini" + with open(str(config_path), "w") as fp: + fp.write(f"{config.cache['mypy_config']}\n") diff --git a/setup.cfg b/setup.cfg index 3efd58e5ac9a0..ad1e6650a59b3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -32,22 +32,3 @@ ignore = D202, W504 noqa-require-code = True - -[mypy] -python_version = 3.8 -show_error_codes = true -ignore_errors = true -follow_imports = silent -ignore_missing_imports = true -warn_incomplete_stub = true -warn_redundant_casts = true -warn_unused_configs = true - - -[mypy-homeassistant.block_async_io,homeassistant.bootstrap,homeassistant.components,homeassistant.config_entries,homeassistant.config,homeassistant.const,homeassistant.core,homeassistant.data_entry_flow,homeassistant.exceptions,homeassistant.__init__,homeassistant.loader,homeassistant.__main__,homeassistant.requirements,homeassistant.runner,homeassistant.setup,homeassistant.util,homeassistant.auth.*,homeassistant.components.automation.*,homeassistant.components.binary_sensor.*,homeassistant.components.bond.*,homeassistant.components.calendar.*,homeassistant.components.cover.*,homeassistant.components.device_automation.*,homeassistant.components.frontend.*,homeassistant.components.geo_location.*,homeassistant.components.group.*,homeassistant.components.history.*,homeassistant.components.http.*,homeassistant.components.huawei_lte.*,homeassistant.components.hyperion.*,homeassistant.components.image_processing.*,homeassistant.components.integration.*,homeassistant.components.knx.*,homeassistant.components.light.*,homeassistant.components.lock.*,homeassistant.components.mailbox.*,homeassistant.components.media_player.*,homeassistant.components.notify.*,homeassistant.components.number.*,homeassistant.components.persistent_notification.*,homeassistant.components.proximity.*,homeassistant.components.recorder.purge,homeassistant.components.recorder.repack,homeassistant.components.remote.*,homeassistant.components.scene.*,homeassistant.components.sensor.*,homeassistant.components.slack.*,homeassistant.components.sonos.media_player,homeassistant.components.sun.*,homeassistant.components.switch.*,homeassistant.components.systemmonitor.*,homeassistant.components.tts.*,homeassistant.components.vacuum.*,homeassistant.components.water_heater.*,homeassistant.components.weather.*,homeassistant.components.websocket_api.*,homeassistant.components.zeroconf.*,homeassistant.components.zone.*,homeassistant.components.zwave_js.*,homeassistant.helpers.*,homeassistant.scripts.*,homeassistant.util.*,tests.components.hyperion.*] -strict = true -ignore_errors = false -warn_unreachable = true -# TODO: turn these off, address issues -allow_any_generics = true -implicit_reexport = true From 70be0561d020e9bc8bb7389e3fcdb33bd211af64 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Mon, 26 Apr 2021 08:29:38 -0400 Subject: [PATCH 0536/1317] Add selectors to cast services (#49684) --- homeassistant/components/cast/services.yaml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/homeassistant/components/cast/services.yaml b/homeassistant/components/cast/services.yaml index 8e4466c349ce4..9b2b0a739b04b 100644 --- a/homeassistant/components/cast/services.yaml +++ b/homeassistant/components/cast/services.yaml @@ -1,12 +1,26 @@ show_lovelace_view: + name: Show lovelace view description: Show a Lovelace view on a Chromecast. fields: entity_id: + name: Entity description: Media Player entity to show the Lovelace view on. + required: true example: "media_player.kitchen" + selector: + entity: + integration: cast + domain: media_player dashboard_path: + name: Dashboard path description: The URL path of the Lovelace dashboard to show. + required: true example: lovelace-cast + selector: + text: view_path: + name: View Path description: The path of the Lovelace view to show. example: downstairs + selector: + text: From 7acb16e2afa7b3d1e8a32d631c1b78b96b4b66f6 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Mon, 26 Apr 2021 14:36:01 +0200 Subject: [PATCH 0537/1317] KNX Schema improvements (#49678) --- homeassistant/components/knx/schema.py | 57 ++++++++++++++++---------- 1 file changed, 36 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index dc5a09534ecdd..dddcabc767b95 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -106,6 +106,7 @@ class BinarySensorSchema: DEFAULT_NAME = "KNX Binary Sensor" SCHEMA = vol.All( + # deprecated since September 2020 cv.deprecated("significant_bit"), cv.deprecated("automation"), vol.Schema( @@ -168,6 +169,7 @@ class ClimateSchema: DEFAULT_ON_OFF_INVERT = False SCHEMA = vol.All( + # deprecated since September 2020 cv.deprecated("setpoint_shift_step", replacement_key=CONF_TEMPERATURE_STEP), vol.Schema( { @@ -242,26 +244,37 @@ class CoverSchema: DEFAULT_TRAVEL_TIME = 25 DEFAULT_NAME = "KNX Cover" - SCHEMA = vol.Schema( - { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_MOVE_LONG_ADDRESS): ga_list_validator, - vol.Optional(CONF_MOVE_SHORT_ADDRESS): ga_list_validator, - vol.Optional(CONF_STOP_ADDRESS): ga_list_validator, - vol.Optional(CONF_POSITION_ADDRESS): ga_list_validator, - vol.Optional(CONF_POSITION_STATE_ADDRESS): ga_list_validator, - vol.Optional(CONF_ANGLE_ADDRESS): ga_list_validator, - vol.Optional(CONF_ANGLE_STATE_ADDRESS): ga_list_validator, - vol.Optional( - CONF_TRAVELLING_TIME_DOWN, default=DEFAULT_TRAVEL_TIME - ): cv.positive_float, - vol.Optional( - CONF_TRAVELLING_TIME_UP, default=DEFAULT_TRAVEL_TIME - ): cv.positive_float, - vol.Optional(CONF_INVERT_POSITION, default=False): cv.boolean, - vol.Optional(CONF_INVERT_ANGLE, default=False): cv.boolean, - vol.Optional(CONF_DEVICE_CLASS): cv.string, - } + SCHEMA = vol.All( + vol.Schema( + { + vol.Required( + vol.Any(CONF_MOVE_LONG_ADDRESS, CONF_POSITION_ADDRESS), + msg=f"At least one of '{CONF_MOVE_LONG_ADDRESS}' or '{CONF_POSITION_ADDRESS}' is required.", + ): object, + }, + extra=vol.ALLOW_EXTRA, + ), + vol.Schema( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_MOVE_LONG_ADDRESS): ga_list_validator, + vol.Optional(CONF_MOVE_SHORT_ADDRESS): ga_list_validator, + vol.Optional(CONF_STOP_ADDRESS): ga_list_validator, + vol.Optional(CONF_POSITION_ADDRESS): ga_list_validator, + vol.Optional(CONF_POSITION_STATE_ADDRESS): ga_list_validator, + vol.Optional(CONF_ANGLE_ADDRESS): ga_list_validator, + vol.Optional(CONF_ANGLE_STATE_ADDRESS): ga_list_validator, + vol.Optional( + CONF_TRAVELLING_TIME_DOWN, default=DEFAULT_TRAVEL_TIME + ): cv.positive_float, + vol.Optional( + CONF_TRAVELLING_TIME_UP, default=DEFAULT_TRAVEL_TIME + ): cv.positive_float, + vol.Optional(CONF_INVERT_POSITION, default=False): cv.boolean, + vol.Optional(CONF_INVERT_ANGLE, default=False): cv.boolean, + vol.Optional(CONF_DEVICE_CLASS): cv.string, + } + ), ) @@ -431,7 +444,9 @@ class SceneSchema: { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Required(KNX_ADDRESS): ga_list_validator, - vol.Required(CONF_SCENE_NUMBER): cv.positive_int, + vol.Required(CONF_SCENE_NUMBER): vol.All( + vol.Coerce(int), vol.Range(min=1, max=64) + ), } ) From 639dac1eaac873da81f4a5ee95809583e836f326 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Mon, 26 Apr 2021 08:51:28 -0400 Subject: [PATCH 0538/1317] Add selector to tts services (#49703) --- homeassistant/components/tts/services.yaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tts/services.yaml b/homeassistant/components/tts/services.yaml index 2b48dd39dee37..f5a5154a029b1 100644 --- a/homeassistant/components/tts/services.yaml +++ b/homeassistant/components/tts/services.yaml @@ -1,7 +1,7 @@ # Describes the format for available TTS services say: - name: Say an TTS message + name: Say a TTS message description: Say something using text-to-speech on a media player. fields: entity_id: @@ -33,10 +33,14 @@ say: selector: text: options: + name: Options description: A dictionary containing platform-specific options. Optional depending on the platform. + advanced: true example: platform specific + selector: + object: clear_cache: name: Clear TTS cache From 5b1ed44613b3d1396ba73edeed37170c5da2603c Mon Sep 17 00:00:00 2001 From: tkdrob Date: Mon, 26 Apr 2021 09:35:45 -0400 Subject: [PATCH 0539/1317] Add selectors to ps4 services (#49702) Co-authored-by: Franck Nijhof --- homeassistant/components/ps4/services.yaml | 23 +++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ps4/services.yaml b/homeassistant/components/ps4/services.yaml index e1af6543a6507..fe7641357bf1d 100644 --- a/homeassistant/components/ps4/services.yaml +++ b/homeassistant/components/ps4/services.yaml @@ -1,9 +1,30 @@ send_command: + name: Send command description: Emulate button press for PlayStation 4. fields: entity_id: - description: Name(s) of entities to send command. + name: Entity + description: Name of entity to send command. + required: true example: "media_player.playstation_4" + selector: + entity: + integration: ps4 + domain: media_player command: + name: Command description: Button to press. + required: true example: "ps" + selector: + select: + options: + - "back" + - "down" + - "enter" + - "left" + - "option" + - "ps_hold" + - "ps" + - "right" + - "up" From c4f0f818c7df1546ae59b1ad6e2076d1611406ae Mon Sep 17 00:00:00 2001 From: tkdrob Date: Mon, 26 Apr 2021 09:36:36 -0400 Subject: [PATCH 0540/1317] Add selectors to frontend services (#49701) Co-authored-by: Franck Nijhof --- homeassistant/components/frontend/services.yaml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/frontend/services.yaml b/homeassistant/components/frontend/services.yaml index 075b73986ffc1..85d3cf2a8217e 100644 --- a/homeassistant/components/frontend/services.yaml +++ b/homeassistant/components/frontend/services.yaml @@ -1,14 +1,26 @@ # Describes the format for available frontend services set_theme: + name: Set theme description: Set a theme unless the client selected per-device theme. fields: name: + name: Name description: Name of a predefined theme, 'default' or 'none'. + required: true example: "default" + selector: + text: mode: - description: The mode the theme is for, either 'dark' or 'light' (default). + name: Mode + description: The mode the theme is for, either 'dark' or 'light'. + default: "light" example: "dark" + selector: + options: + - "dark" + - "light" reload_themes: + name: Reload themes description: Reload themes from YAML configuration. From a7393cd8b46482bcfadc772ee788be321f824792 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Mon, 26 Apr 2021 09:47:25 -0400 Subject: [PATCH 0541/1317] Add selectors to plex services (#49706) --- homeassistant/components/plex/services.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/homeassistant/components/plex/services.yaml b/homeassistant/components/plex/services.yaml index 5412a4180e637..782a4d17c189f 100644 --- a/homeassistant/components/plex/services.yaml +++ b/homeassistant/components/plex/services.yaml @@ -1,12 +1,21 @@ refresh_library: + name: Refresh library description: Refresh a Plex library to scan for new and updated media. fields: server_name: + name: Server name description: Name of a Plex server if multiple Plex servers configured. example: "My Plex Server" + selector: + text: library_name: + name: Library name description: Name of the Plex library to refresh. + required: true example: "TV Shows" + selector: + text: scan_for_clients: + name: Scan for clients description: Scan for available clients from the Plex server(s), local network, and plex.tv. From 41d6d64ca46438e22ee69514a84357402c8d7869 Mon Sep 17 00:00:00 2001 From: Doomic Date: Mon, 26 Apr 2021 15:55:41 +0200 Subject: [PATCH 0542/1317] Add unique_id to WOL integration (#49604) Co-authored-by: Franck Nijhof --- homeassistant/components/wake_on_lan/switch.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/wake_on_lan/switch.py b/homeassistant/components/wake_on_lan/switch.py index eba6897647b59..4bbd1522c91bb 100644 --- a/homeassistant/components/wake_on_lan/switch.py +++ b/homeassistant/components/wake_on_lan/switch.py @@ -14,6 +14,7 @@ CONF_MAC, CONF_NAME, ) +from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv from homeassistant.helpers.script import Script @@ -87,6 +88,7 @@ def __init__( ) self._state = False self._assumed_state = host is None + self._unique_id = dr.format_mac(mac_address) @property def is_on(self): @@ -108,6 +110,11 @@ def should_poll(self): """Return false if assumed state is true.""" return not self._assumed_state + @property + def unique_id(self): + """Return the unique id of this switch.""" + return self._unique_id + def turn_on(self, **kwargs): """Turn the device on.""" service_kwargs = {} From 922eec09098e0462b64bea91210a674512b8530a Mon Sep 17 00:00:00 2001 From: tkdrob Date: Mon, 26 Apr 2021 11:12:36 -0400 Subject: [PATCH 0543/1317] Use core constants for kwb (#49708) --- homeassistant/components/kwb/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/kwb/sensor.py b/homeassistant/components/kwb/sensor.py index eb96b20665342..1b56803fae67c 100644 --- a/homeassistant/components/kwb/sensor.py +++ b/homeassistant/components/kwb/sensor.py @@ -8,6 +8,7 @@ CONF_HOST, CONF_NAME, CONF_PORT, + CONF_TYPE, EVENT_HOMEASSISTANT_STOP, ) import homeassistant.helpers.config_validation as cv @@ -18,7 +19,6 @@ MODE_SERIAL = 0 MODE_TCP = 1 -CONF_TYPE = "type" CONF_RAW = "raw" SERIAL_SCHEMA = PLATFORM_SCHEMA.extend( From 51be2f860a253b97fae4d82897aea39b0b375c8c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 26 Apr 2021 07:46:55 -1000 Subject: [PATCH 0544/1317] Reduce boilerplate to setup config entry platforms A-C (#49681) Co-authored-by: Franck Nijhof --- homeassistant/components/abode/__init__.py | 19 ++++----------- .../components/accuweather/__init__.py | 15 +++--------- homeassistant/components/acmeda/__init__.py | 16 ++++--------- .../components/advantage_air/__init__.py | 15 ++---------- homeassistant/components/aemet/__init__.py | 16 ++++--------- .../components/agent_dvr/__init__.py | 15 ++---------- homeassistant/components/airly/__init__.py | 16 ++++--------- homeassistant/components/airnow/__init__.py | 16 +++---------- .../components/airvisual/__init__.py | 16 ++++--------- .../components/alarmdecoder/__init__.py | 16 +++---------- .../components/ambient_station/__init__.py | 19 ++++----------- homeassistant/components/apple_tv/__init__.py | 10 ++------ homeassistant/components/asuswrt/__init__.py | 16 +++---------- homeassistant/components/atag/__init__.py | 17 ++++---------- homeassistant/components/august/__init__.py | 14 ++--------- homeassistant/components/aurora/__init__.py | 16 +++---------- homeassistant/components/awair/__init__.py | 14 ++++------- homeassistant/components/axis/device.py | 15 ++---------- homeassistant/components/blebox/__init__.py | 15 ++---------- homeassistant/components/blink/__init__.py | 15 ++---------- .../bmw_connected_drive/__init__.py | 19 ++++----------- homeassistant/components/bond/__init__.py | 15 ++---------- homeassistant/components/braviatv/__init__.py | 15 +++--------- homeassistant/components/broadlink/device.py | 15 ++++-------- homeassistant/components/brother/__init__.py | 16 +++---------- homeassistant/components/canary/__init__.py | 15 ++---------- .../components/climacell/__init__.py | 15 +++--------- homeassistant/components/control4/__init__.py | 16 +++---------- .../components/coronavirus/__init__.py | 15 ++---------- homeassistant/config_entries.py | 23 ++++++++++++++++++- .../config_flow/integration/__init__.py | 16 ++----------- .../integration/__init__.py | 16 ++----------- .../integration/__init__.py | 15 ++---------- tests/test_config_entries.py | 6 ++--- 34 files changed, 119 insertions(+), 409 deletions(-) diff --git a/homeassistant/components/abode/__init__.py b/homeassistant/components/abode/__init__.py index 329a0a679bca3..22e22efd82ed2 100644 --- a/homeassistant/components/abode/__init__.py +++ b/homeassistant/components/abode/__init__.py @@ -1,5 +1,4 @@ """Support for the Abode Security System.""" -from asyncio import gather from copy import deepcopy from functools import partial @@ -131,10 +130,7 @@ async def async_setup_entry(hass, config_entry): hass.data[DOMAIN] = AbodeSystem(abode, polling) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) await setup_hass_events(hass) await hass.async_add_executor_job(setup_hass_services, hass) @@ -149,14 +145,9 @@ async def async_unload_entry(hass, config_entry): hass.services.async_remove(DOMAIN, SERVICE_CAPTURE_IMAGE) hass.services.async_remove(DOMAIN, SERVICE_TRIGGER_AUTOMATION) - tasks = [] - - for platform in PLATFORMS: - tasks.append( - hass.config_entries.async_forward_entry_unload(config_entry, platform) - ) - - await gather(*tasks) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS + ) await hass.async_add_executor_job(hass.data[DOMAIN].abode.events.stop) await hass.async_add_executor_job(hass.data[DOMAIN].abode.logout) @@ -164,7 +155,7 @@ async def async_unload_entry(hass, config_entry): hass.data[DOMAIN].logout_listener() hass.data.pop(DOMAIN) - return True + return unload_ok def setup_hass_services(hass): diff --git a/homeassistant/components/accuweather/__init__.py b/homeassistant/components/accuweather/__init__.py index 4ed471a50f592..f6f124b2d4d07 100644 --- a/homeassistant/components/accuweather/__init__.py +++ b/homeassistant/components/accuweather/__init__.py @@ -1,5 +1,4 @@ """The AccuWeather component.""" -import asyncio from datetime import timedelta import logging @@ -46,23 +45,15 @@ async def async_setup_entry(hass, config_entry) -> bool: UNDO_UPDATE_LISTENER: undo_listener, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) return True async def async_unload_entry(hass, config_entry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS ) hass.data[DOMAIN][config_entry.entry_id][UNDO_UPDATE_LISTENER]() diff --git a/homeassistant/components/acmeda/__init__.py b/homeassistant/components/acmeda/__init__.py index 926208fba40d5..078c499f2be9b 100644 --- a/homeassistant/components/acmeda/__init__.py +++ b/homeassistant/components/acmeda/__init__.py @@ -1,5 +1,4 @@ """The Rollease Acmeda Automate integration.""" -import asyncio from homeassistant import config_entries, core @@ -23,10 +22,7 @@ async def async_setup_entry( hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][config_entry.entry_id] = hub - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) return True @@ -37,14 +33,10 @@ async def async_unload_entry( """Unload a config entry.""" hub = hass.data[DOMAIN][config_entry.entry_id] - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS ) + if not await hub.async_reset(): return False diff --git a/homeassistant/components/advantage_air/__init__.py b/homeassistant/components/advantage_air/__init__.py index 98c6c401810e7..ad3a95123c750 100644 --- a/homeassistant/components/advantage_air/__init__.py +++ b/homeassistant/components/advantage_air/__init__.py @@ -1,6 +1,5 @@ """Advantage Air climate integration.""" -import asyncio from datetime import timedelta import logging @@ -58,24 +57,14 @@ async def async_change(change): "async_change": async_change, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass, entry): """Unload Advantage Air Config.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/aemet/__init__.py b/homeassistant/components/aemet/__init__.py index 4c1315d187df2..a4a0526062db3 100644 --- a/homeassistant/components/aemet/__init__.py +++ b/homeassistant/components/aemet/__init__.py @@ -1,5 +1,4 @@ """The AEMET OpenData component.""" -import asyncio import logging from aemet_opendata.interface import AEMET @@ -32,24 +31,17 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): ENTRY_WEATHER_COORDINATOR: weather_coordinator, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS ) + if unload_ok: hass.data[DOMAIN].pop(config_entry.entry_id) diff --git a/homeassistant/components/agent_dvr/__init__.py b/homeassistant/components/agent_dvr/__init__.py index 3623f4f702a90..5b765da7f8ebf 100644 --- a/homeassistant/components/agent_dvr/__init__.py +++ b/homeassistant/components/agent_dvr/__init__.py @@ -1,5 +1,4 @@ """Support for Agent.""" -import asyncio from agent import AgentError from agent.a import Agent @@ -47,24 +46,14 @@ async def async_setup_entry(hass, config_entry): sw_version=agent_client.version, ) - for forward in FORWARDS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, forward) - ) + hass.config_entries.async_setup_platforms(config_entry, FORWARDS) return True async def async_unload_entry(hass, config_entry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, forward) - for forward in FORWARDS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(config_entry, FORWARDS) await hass.data[AGENT_DOMAIN][config_entry.entry_id][CONNECTION].close() diff --git a/homeassistant/components/airly/__init__.py b/homeassistant/components/airly/__init__.py index 41a7c03e636a2..b0aa617995267 100644 --- a/homeassistant/components/airly/__init__.py +++ b/homeassistant/components/airly/__init__.py @@ -1,5 +1,4 @@ """The Airly integration.""" -import asyncio from datetime import timedelta import logging from math import ceil @@ -69,24 +68,17 @@ async def async_setup_entry(hass, config_entry): hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][config_entry.entry_id] = coordinator - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) return True async def async_unload_entry(hass, config_entry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS ) + if unload_ok: hass.data[DOMAIN].pop(config_entry.entry_id) diff --git a/homeassistant/components/airnow/__init__.py b/homeassistant/components/airnow/__init__.py index b1770dcbde788..0b27a4a9dfdf9 100644 --- a/homeassistant/components/airnow/__init__.py +++ b/homeassistant/components/airnow/__init__.py @@ -1,5 +1,4 @@ """The AirNow integration.""" -import asyncio import datetime import logging @@ -60,24 +59,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py index 8447e62a15b29..ac34c16d3d023 100644 --- a/homeassistant/components/airvisual/__init__.py +++ b/homeassistant/components/airvisual/__init__.py @@ -1,5 +1,4 @@ """The airvisual component.""" -import asyncio from datetime import timedelta from math import ceil @@ -258,10 +257,7 @@ async def async_update_data(): hass, config_entry.data[CONF_API_KEY] ) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) return True @@ -310,14 +306,10 @@ async def async_migrate_entry(hass, config_entry): async def async_unload_entry(hass, config_entry): """Unload an AirVisual config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS ) + if unload_ok: hass.data[DOMAIN][DATA_COORDINATOR].pop(config_entry.entry_id) remove_listener = hass.data[DOMAIN][DATA_LISTENER].pop(config_entry.entry_id) diff --git a/homeassistant/components/alarmdecoder/__init__.py b/homeassistant/components/alarmdecoder/__init__.py index 09afa84f7f56f..aff7dd8c5ba40 100644 --- a/homeassistant/components/alarmdecoder/__init__.py +++ b/homeassistant/components/alarmdecoder/__init__.py @@ -1,5 +1,4 @@ """Support for AlarmDecoder devices.""" -import asyncio from datetime import timedelta import logging @@ -125,10 +124,8 @@ def handle_rel_message(sender, message): await open_connection() - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True @@ -136,14 +133,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a AlarmDecoder entry.""" hass.data[DOMAIN][entry.entry_id][DATA_RESTART] = False - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if not unload_ok: return False diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index 4879f68f07998..9036a4d89a213 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -1,5 +1,4 @@ """Support for Ambient Weather Station Service.""" -import asyncio from aioambient import Client from aioambient.errors import WebsocketError @@ -369,14 +368,7 @@ async def async_unload_entry(hass, config_entry): ambient = hass.data[DOMAIN][DATA_CLIENT].pop(config_entry.entry_id) hass.async_create_task(ambient.ws_disconnect()) - tasks = [ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - - await asyncio.gather(*tasks) - - return True + return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) async def async_migrate_entry(hass, config_entry): @@ -475,12 +467,9 @@ def on_subscribed(data): # attempt forward setup of the config entry (because it will have # already been done): if not self._entry_setup_complete: - for platform in PLATFORMS: - self._hass.async_create_task( - self._hass.config_entries.async_forward_entry_setup( - self._config_entry, platform - ) - ) + self._hass.config_entries.async_setup_platforms( + self._config_entry, PLATFORMS + ) self._entry_setup_complete = True self._ws_reconnect_delay = DEFAULT_SOCKET_MIN_RETRY diff --git a/homeassistant/components/apple_tv/__init__.py b/homeassistant/components/apple_tv/__init__.py index d7b5054683279..a1bd50ab2217c 100644 --- a/homeassistant/components/apple_tv/__init__.py +++ b/homeassistant/components/apple_tv/__init__.py @@ -71,14 +71,8 @@ async def setup_platforms(): async def async_unload_entry(hass, entry): """Unload an Apple TV config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: manager = hass.data[DOMAIN].pop(entry.unique_id) await manager.disconnect() diff --git a/homeassistant/components/asuswrt/__init__.py b/homeassistant/components/asuswrt/__init__.py index a736a0996d23d..ad3cea1106b18 100644 --- a/homeassistant/components/asuswrt/__init__.py +++ b/homeassistant/components/asuswrt/__init__.py @@ -1,5 +1,4 @@ """Support for ASUSWRT devices.""" -import asyncio import voluptuous as vol @@ -125,10 +124,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): router.async_on_close(entry.add_update_listener(update_listener)) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) async def async_close_connection(event): """Close AsusWrt connection on HA Stop.""" @@ -148,14 +144,8 @@ async def async_close_connection(event): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: hass.data[DOMAIN][entry.entry_id]["stop_listener"]() router = hass.data[DOMAIN][entry.entry_id][DATA_ASUSWRT] diff --git a/homeassistant/components/atag/__init__.py b/homeassistant/components/atag/__init__.py index 017e9968d1ec8..710685f91aee7 100644 --- a/homeassistant/components/atag/__init__.py +++ b/homeassistant/components/atag/__init__.py @@ -9,7 +9,7 @@ from homeassistant.components.sensor import DOMAIN as SENSOR from homeassistant.components.water_heater import DOMAIN as WATER_HEATER from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, asyncio +from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -52,24 +52,15 @@ async def _async_update_data(): if entry.unique_id is None: hass.config_entries.async_update_entry(entry, unique_id=atag.id) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass, entry): """Unload Atag config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index 7872c4e030738..30374dcb2202c 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -52,14 +52,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data[DOMAIN][entry.entry_id][DATA_AUGUST].async_stop() - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) @@ -85,10 +78,7 @@ async def async_setup_august(hass, config_entry, august_gateway): } await data[DATA_AUGUST].async_setup() - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) return True diff --git a/homeassistant/components/aurora/__init__.py b/homeassistant/components/aurora/__init__.py index 8823cf1c8ec3e..e565071eae269 100644 --- a/homeassistant/components/aurora/__init__.py +++ b/homeassistant/components/aurora/__init__.py @@ -1,6 +1,5 @@ """The aurora component.""" -import asyncio from datetime import timedelta import logging @@ -69,24 +68,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): AURORA_API: api, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/awair/__init__.py b/homeassistant/components/awair/__init__.py index 5b59e4d83aca5..6af2850ea31d2 100644 --- a/homeassistant/components/awair/__init__.py +++ b/homeassistant/components/awair/__init__.py @@ -28,23 +28,17 @@ async def async_setup_entry(hass, config_entry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][config_entry.entry_id] = coordinator - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) return True async def async_unload_entry(hass, config_entry) -> bool: """Unload Awair configuration.""" - tasks = [] - for platform in PLATFORMS: - tasks.append( - hass.config_entries.async_forward_entry_unload(config_entry, platform) - ) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS + ) - unload_ok = all(await gather(*tasks)) if unload_ok: hass.data[DOMAIN].pop(config_entry.entry_id) diff --git a/homeassistant/components/axis/device.py b/homeassistant/components/axis/device.py index cc9922b290c10..f1a57eec33c93 100644 --- a/homeassistant/components/axis/device.py +++ b/homeassistant/components/axis/device.py @@ -264,20 +264,9 @@ async def async_reset(self): """Reset this device to default state.""" self.disconnect_from_stream() - unload_ok = all( - await asyncio.gather( - *[ - self.hass.config_entries.async_forward_entry_unload( - self.config_entry, platform - ) - for platform in PLATFORMS - ] - ) + return await self.hass.config_entries.async_unload_platforms( + self.config_entry, PLATFORMS ) - if not unload_ok: - return False - - return True async def get_device(hass, host, port, username, password): diff --git a/homeassistant/components/blebox/__init__.py b/homeassistant/components/blebox/__init__.py index c5f723b68588e..fe2265ed78d28 100644 --- a/homeassistant/components/blebox/__init__.py +++ b/homeassistant/components/blebox/__init__.py @@ -1,5 +1,4 @@ """The BleBox devices integration.""" -import asyncio import logging from blebox_uniapi.error import Error @@ -43,24 +42,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): domain_entry = domain.setdefault(entry.entry_id, {}) product = domain_entry.setdefault(PRODUCT, product) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/blink/__init__.py b/homeassistant/components/blink/__init__.py index 9c73ee6f99536..ce47fcf79087b 100644 --- a/homeassistant/components/blink/__init__.py +++ b/homeassistant/components/blink/__init__.py @@ -1,5 +1,4 @@ """Support for Blink Home Camera System.""" -import asyncio from copy import deepcopy import logging @@ -86,10 +85,7 @@ async def async_setup_entry(hass, entry): if not hass.data[DOMAIN][entry.entry_id].available: raise ConfigEntryNotReady - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) def blink_refresh(event_time=None): """Call blink to refresh info.""" @@ -130,14 +126,7 @@ def _async_import_options_from_data_if_missing(hass, entry): async def async_unload_entry(hass, entry): """Unload Blink entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if not unload_ok: return False diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index ebf1fd6f74ea8..d513ae7c460d7 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -1,7 +1,6 @@ """Reads vehicle status from BMW connected drive portal.""" from __future__ import annotations -import asyncio import logging from bimmer_connected.account import ConnectedDriveAccount @@ -138,11 +137,9 @@ def _update_all() -> None: await _async_update_all() - for platform in PLATFORMS: - if platform != NOTIFY_DOMAIN: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms( + entry, [platform for platform in PLATFORMS if platform != NOTIFY_DOMAIN] + ) # set up notify platform, no entry support for notify platform yet, # have to use discovery to load platform. @@ -161,14 +158,8 @@ def _update_all() -> None: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - if platform != NOTIFY_DOMAIN - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + entry, [platform for platform in PLATFORMS if platform != NOTIFY_DOMAIN] ) # Only remove services if it is the last account and not read only diff --git a/homeassistant/components/bond/__init__.py b/homeassistant/components/bond/__init__.py index c14c50d7c5294..93a927d21f320 100644 --- a/homeassistant/components/bond/__init__.py +++ b/homeassistant/components/bond/__init__.py @@ -1,5 +1,4 @@ """The Bond integration.""" -import asyncio from asyncio import TimeoutError as AsyncIOTimeoutError from aiohttp import ClientError, ClientTimeout @@ -75,24 +74,14 @@ def _async_stop_event(event: Event) -> None: _async_remove_old_device_identifiers(config_entry_id, device_registry, hub) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) data = hass.data[DOMAIN][entry.entry_id] data[_STOP_CANCEL]() diff --git a/homeassistant/components/braviatv/__init__.py b/homeassistant/components/braviatv/__init__.py index d8f6d64b15f18..0097964e29875 100644 --- a/homeassistant/components/braviatv/__init__.py +++ b/homeassistant/components/braviatv/__init__.py @@ -1,5 +1,4 @@ """The Bravia TV component.""" -import asyncio from bravia_tv import BraviaRC @@ -23,23 +22,15 @@ async def async_setup_entry(hass, config_entry): UNDO_UPDATE_LISTENER: undo_listener, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) return True async def async_unload_entry(hass, config_entry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS ) hass.data[DOMAIN][config_entry.entry_id][UNDO_UPDATE_LISTENER]() diff --git a/homeassistant/components/broadlink/device.py b/homeassistant/components/broadlink/device.py index fd9c6dcd9d30c..b18d64c327fcd 100644 --- a/homeassistant/components/broadlink/device.py +++ b/homeassistant/components/broadlink/device.py @@ -1,5 +1,4 @@ """Support for Broadlink devices.""" -import asyncio from contextlib import suppress from functools import partial import logging @@ -112,12 +111,9 @@ async def async_setup(self): self.reset_jobs.append(config.add_update_listener(self.async_update)) # Forward entry setup to related domains. - tasks = ( - self.hass.config_entries.async_forward_entry_setup(config, domain) - for domain in get_domains(self.api.type) + self.hass.config_entries.async_setup_platforms( + config, get_domains(self.api.type) ) - for entry_setup in tasks: - self.hass.async_create_task(entry_setup) return True @@ -129,12 +125,9 @@ async def async_unload(self): while self.reset_jobs: self.reset_jobs.pop()() - tasks = ( - self.hass.config_entries.async_forward_entry_unload(self.config, domain) - for domain in get_domains(self.api.type) + return await self.hass.config_entries.async_unload_platforms( + self.config, get_domains(self.api.type) ) - results = await asyncio.gather(*tasks) - return all(results) async def async_auth(self): """Authenticate to the device.""" diff --git a/homeassistant/components/brother/__init__.py b/homeassistant/components/brother/__init__.py index f3c7678f3e33f..b4994688cf4b3 100644 --- a/homeassistant/components/brother/__init__.py +++ b/homeassistant/components/brother/__init__.py @@ -1,5 +1,4 @@ """The Brother component.""" -import asyncio from datetime import timedelta import logging @@ -37,24 +36,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id] = coordinator hass.data[DOMAIN][SNMP] = snmp_engine - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: hass.data[DOMAIN][DATA_CONFIG_ENTRY].pop(entry.entry_id) if not hass.data[DOMAIN][DATA_CONFIG_ENTRY]: diff --git a/homeassistant/components/canary/__init__.py b/homeassistant/components/canary/__init__.py index 04290711cb90b..90854cb3fa398 100644 --- a/homeassistant/components/canary/__init__.py +++ b/homeassistant/components/canary/__init__.py @@ -1,5 +1,4 @@ """Support for Canary devices.""" -import asyncio from datetime import timedelta import logging @@ -104,24 +103,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_UNDO_UPDATE_LISTENER: undo_listener, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN][entry.entry_id][DATA_UNDO_UPDATE_LISTENER]() diff --git a/homeassistant/components/climacell/__init__.py b/homeassistant/components/climacell/__init__.py index 74555e86af844..81198f8d98c2e 100644 --- a/homeassistant/components/climacell/__init__.py +++ b/homeassistant/components/climacell/__init__.py @@ -1,7 +1,6 @@ """The ClimaCell integration.""" from __future__ import annotations -import asyncio from datetime import timedelta import logging from math import ceil @@ -162,23 +161,15 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b hass.data[DOMAIN][config_entry.entry_id] = coordinator - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS ) hass.data[DOMAIN].pop(config_entry.entry_id) diff --git a/homeassistant/components/control4/__init__.py b/homeassistant/components/control4/__init__.py index d7f8ec52f7a43..01958ef3453ff 100644 --- a/homeassistant/components/control4/__init__.py +++ b/homeassistant/components/control4/__init__.py @@ -1,5 +1,4 @@ """The Control4 integration.""" -import asyncio import json import logging @@ -107,10 +106,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): entry_data[CONF_CONFIG_LISTENER] = entry.add_update_listener(update_listener) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -123,14 +119,8 @@ async def update_listener(hass, config_entry): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + hass.data[DOMAIN][entry.entry_id][CONF_CONFIG_LISTENER]() if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/coronavirus/__init__.py b/homeassistant/components/coronavirus/__init__.py index 4bda4edcd3767..c855137fcbf6e 100644 --- a/homeassistant/components/coronavirus/__init__.py +++ b/homeassistant/components/coronavirus/__init__.py @@ -1,5 +1,4 @@ """The Coronavirus integration.""" -import asyncio from datetime import timedelta import logging @@ -48,24 +47,14 @@ def _async_migrator(entity_entry: entity_registry.RegistryEntry): if not coordinator.last_update_success: await coordinator.async_config_entry_first_refresh() - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - return all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) async def get_coordinator( diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 5b35b7ef65c07..5ad04ac96cfa7 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from collections.abc import Mapping +from collections.abc import Iterable, Mapping from contextvars import ContextVar import functools import logging @@ -999,6 +999,14 @@ def async_update_entry( return True + @callback + def async_setup_platforms( + self, entry: ConfigEntry, platforms: Iterable[str] + ) -> None: + """Forward the setup of an entry to platforms.""" + for platform in platforms: + self.hass.async_create_task(self.async_forward_entry_setup(entry, platform)) + async def async_forward_entry_setup(self, entry: ConfigEntry, domain: str) -> bool: """Forward the setup of an entry to a different component. @@ -1021,6 +1029,19 @@ async def async_forward_entry_setup(self, entry: ConfigEntry, domain: str) -> bo await entry.async_setup(self.hass, integration=integration) return True + async def async_unload_platforms( + self, entry: ConfigEntry, platforms: Iterable[str] + ) -> bool: + """Forward the unloading of an entry to platforms.""" + return all( + await asyncio.gather( + *[ + self.async_forward_entry_unload(entry, platform) + for platform in platforms + ] + ) + ) + async def async_forward_entry_unload(self, entry: ConfigEntry, domain: str) -> bool: """Forward the unloading of an entry to a different component.""" # It was never loaded. diff --git a/script/scaffold/templates/config_flow/integration/__init__.py b/script/scaffold/templates/config_flow/integration/__init__.py index 6c187d1dafece..2f146dfe6e3ef 100644 --- a/script/scaffold/templates/config_flow/integration/__init__.py +++ b/script/scaffold/templates/config_flow/integration/__init__.py @@ -1,8 +1,6 @@ """The NEW_NAME integration.""" from __future__ import annotations -import asyncio - from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -18,24 +16,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # TODO Store an API object for your platforms to access # hass.data[DOMAIN][entry.entry_id] = MyApi(...) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/script/scaffold/templates/config_flow_discovery/integration/__init__.py b/script/scaffold/templates/config_flow_discovery/integration/__init__.py index 773bf594838db..c9f56b3919b36 100644 --- a/script/scaffold/templates/config_flow_discovery/integration/__init__.py +++ b/script/scaffold/templates/config_flow_discovery/integration/__init__.py @@ -1,8 +1,6 @@ """The NEW_NAME integration.""" from __future__ import annotations -import asyncio - from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -18,24 +16,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # TODO Store an API object for your platforms to access # hass.data[DOMAIN][entry.entry_id] = MyApi(...) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/script/scaffold/templates/config_flow_oauth2/integration/__init__.py b/script/scaffold/templates/config_flow_oauth2/integration/__init__.py index 304df8f9c799c..f597ef609ea8d 100644 --- a/script/scaffold/templates/config_flow_oauth2/integration/__init__.py +++ b/script/scaffold/templates/config_flow_oauth2/integration/__init__.py @@ -1,7 +1,6 @@ """The NEW_NAME integration.""" from __future__ import annotations -import asyncio from typing import Any import voluptuous as vol @@ -75,24 +74,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: aiohttp_client.async_get_clientsession(hass), session ) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 741953f552be8..b9f9424b6f03e 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -257,14 +257,12 @@ async def test_remove_entry(hass, manager): async def mock_setup_entry(hass, entry): """Mock setting up entry.""" - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "light") - ) + hass.config_entries.async_setup_platforms(entry, ["light"]) return True async def mock_unload_entry(hass, entry): """Mock unloading an entry.""" - result = await hass.config_entries.async_forward_entry_unload(entry, "light") + result = await hass.config_entries.async_unload_platforms(entry, ["light"]) assert result return result From 9c3c67b71b809a33f3880f0cbd9e8d19b10edb58 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 26 Apr 2021 22:18:30 +0200 Subject: [PATCH 0545/1317] Upgrade black to 21.4b0 (#49715) --- .pre-commit-config.yaml | 2 +- requirements_test_pre_commit.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 29a46279f22c5..20792593114d9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,7 +5,7 @@ repos: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/psf/black - rev: 20.8b1 + rev: 21.4b0 hooks: - id: black args: diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 3a146eb425e49..5d646cc81f340 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,7 +1,7 @@ # Automatically generated from .pre-commit-config.yaml by gen_requirements_all.py, do not edit bandit==1.7.0 -black==20.8b1 +black==21.4b0 codespell==2.0.0 flake8-comprehensions==3.4.0 flake8-docstrings==1.6.0 From 1527b9cad715550d055f74041e06688d6921225d Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 26 Apr 2021 22:19:40 +0200 Subject: [PATCH 0546/1317] Build images on GitHub actions (#48318) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Joakim Sørensen Co-authored-by: Franck Nijhof --- .github/workflows/builder.yml | 310 ++++++++++++++++++++++ azure-pipelines-release.yml | 323 ----------------------- build.json | 20 +- machine/build.json | 16 ++ rootfs/etc/services.d/home-assistant/run | 6 +- 5 files changed, 343 insertions(+), 332 deletions(-) create mode 100644 .github/workflows/builder.yml delete mode 100644 azure-pipelines-release.yml create mode 100644 machine/build.json diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml new file mode 100644 index 0000000000000..c5eccbf9b2a43 --- /dev/null +++ b/.github/workflows/builder.yml @@ -0,0 +1,310 @@ +name: Build images + +# yamllint disable-line rule:truthy +on: + release: + types: ["published"] + schedule: + - cron: "0 2 * * *" + +env: + BUILD_TYPE: core + DEFAULT_PYTHON: 3.8 + +jobs: + init: + name: Initialize build + runs-on: ubuntu-latest + outputs: + architectures: ${{ steps.info.outputs.architectures }} + version: ${{ steps.version.outputs.version }} + channel: ${{ steps.version.outputs.channel }} + publish: ${{ steps.version.outputs.publish }} + steps: + - name: Checkout the repository + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + uses: actions/setup-python@v2.2.1 + with: + python-version: ${{ env.DEFAULT_PYTHON }} + + - name: Get information + id: info + uses: home-assistant/actions/helpers/info@master + + - name: Get version + id: version + uses: home-assistant/actions/helpers/version@master + with: + type: ${{ env.BUILD_TYPE }} + + - name: Verify version + uses: home-assistant/actions/helpers/verify-version@master + with: + ignore-dev: true + + build_python: + name: Build PyPi package + needs: init + runs-on: ubuntu-latest + if: needs.init.outputs.publish == "true" + steps: + - name: Checkout the repository + uses: actions/checkout@v2 + + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + uses: actions/setup-python@v2.2.1 + with: + python-version: ${{ env.DEFAULT_PYTHON }} + + - name: Build package + shell: bash + run: | + pip install twine wheel + python setup.py sdist bdist_wheel + + - name: Upload package + shell: bash + run: | + export TWINE_USERNAME="__token__" + export TWINE_PASSWORD="${{ secrets.TWINE_TOKEN }}" + + twine upload dist/* --skip-existing + + build_base: + name: Build ${{ matrix.arch }} base core image + needs: init + runs-on: ubuntu-latest + strategy: + matrix: + arch: ${{ fromJson(needs.init.outputs.architectures) }} + steps: + - name: Checkout the repository + uses: actions/checkout@v2 + + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + if: needs.init.outputs.channel == "dev" + uses: actions/setup-python@v2.2.1 + with: + python-version: ${{ env.DEFAULT_PYTHON }} + + - name: Adjust nightly version + if: needs.init.outputs.channel == "dev" + shell: bash + run: | + python3 -m pip install packaging + python3 -m pip install . + python3 script/version_bump.py nightly + version="$(python setup.py -V)" + + - name: Login to DockerHub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Login to GitHub Container Registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build base image + uses: home-assistant/builder@2021.04.2 + with: + args: | + $BUILD_ARGS \ + --${{ matrix.arch }} \ + --target /data \ + --with-codenotary "${{ secrets.VCN_USER }}" "${{ secrets.VCN_PASSWORD }}" "${{ secrets.VCN_ORG }}" \ + --validate-from "${{ secrets.VCN_ORG }}" \ + --generic ${{ needs.init.outputs.version }} + + build_machine: + name: Build ${{ matrix.arch }} machine core image + needs: ["init", "build_base"] + runs-on: ubuntu-latest + strategy: + matrix: + machine: + - generic-x86-64 + - intel-nuc + - odroid-c4 + - odroid-n2 + - odroid-xu + - qemuarm + - qemuarm-64 + - qemux86 + - qemux86-64 + - raspberrypi + - raspberrypi2 + - raspberrypi3 + - raspberrypi3-64 + - raspberrypi4 + - raspberrypi4-64 + - tinker + steps: + - name: Checkout the repository + uses: actions/checkout@v2 + + - name: Login to DockerHub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Login to GitHub Container Registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build base image + uses: home-assistant/builder@2021.04.2 + with: + args: | + $BUILD_ARGS \ + --${{ matrix.arch }} \ + --target /data/machine \ + --with-codenotary "${{ secrets.VCN_USER }}" "${{ secrets.VCN_PASSWORD }}" "${{ secrets.VCN_ORG }}" \ + --validate-from "${{ secrets.VCN_ORG }}" \ + --machine "${{ needs.init.outputs.version }}=${{ matrix.machine }}"" + + publish_ha: + name: Publish version files + needs: ["init", "build_machine"] + runs-on: ubuntu-latest + steps: + - name: Checkout the repository + uses: actions/checkout@v2 + + - name: Initialize git + uses: home-assistant/actions/helpers/git-init@master + with: + name: ${{ secrets.GIT_NAME }} + email: ${{ secrets.GIT_EMAIL }} + token: ${{ secrets.GIT_TOKEN }} + + - name: Update version file + uses: home-assistant/actions/helpers/version-push@master + with: + key: "homeassistant[]" + key-description: "Home Assistant Core" + version: ${{ needs.init.outputs.version }} + channel: ${{ needs.init.outputs.channel }} + + - name: Update version file (stable -> beta) + if: needs.init.outputs.channel == "stable" + uses: home-assistant/actions/helpers/version-push@master + with: + key: "homeassistant[]" + key-description: "Home Assistant Core" + version: ${{ needs.init.outputs.version }} + channel: beta + + publish_container: + name: Publish meta container + needs: ["init", "build_base"] + runs-on: ubuntu-latest + steps: + - name: Checkout the repository + uses: actions/checkout@v2 + + - name: Login to DockerHub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Login to GitHub Container Registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build Meta Image + shell: bash + run: | + bash <(curl https://getvcn.codenotary.com -L) + + export DOCKER_CLI_EXPERIMENTAL=enabled + + function create_manifest() { + local docker_reg={1} + local tag_l=${2} + local tag_r=${3} + + docker manifest create "${docker_reg}/home-assistant:${tag_l}" \ + "${docker_reg}/amd64-homeassistant:${tag_r}" \ + "${docker_reg}/i386-homeassistant:${tag_r}" \ + "${docker_reg}/armhf-homeassistant:${tag_r}" \ + "${docker_reg}/armv7-homeassistant:${tag_r}" \ + "${docker_reg}/aarch64-homeassistant:${tag_r}" + + docker manifest annotate "${docker_reg}/home-assistant:${tag_l}" \ + "${docker_reg}/amd64-homeassistant:${tag_r}" \ + --os linux --arch amd64 + + docker manifest annotate "${docker_reg}/home-assistant:${tag_l}" \ + "${docker_reg}/i386-homeassistant:${tag_r}" \ + --os linux --arch 386 + + docker manifest annotate "${docker_reg}/home-assistant:${tag_l}" \ + "${docker_reg}/armhf-homeassistant:${tag_r}" \ + --os linux --arch arm --variant=v6 + + docker manifest annotate "${docker_reg}/home-assistant:${tag_l}" \ + "${docker_reg}/armv7-homeassistant:${tag_r}" \ + --os linux --arch arm --variant=v7 + + docker manifest annotate "${docker_reg}/home-assistant:${tag_l}" \ + "${docker_reg}/aarch64-homeassistant:${tag_r}" \ + --os linux --arch arm64 --variant=v8 + + docker manifest push --purge "${docker_reg}/home-assistant:${tag_l}" + } + + function validate_image() { + local image={1} + state="$(vcn authenticate --org home-assistant.io --output json docker://${image} | jq '.verification.status // 2')" + if [[ "${state}" != "0" ]]; then + echo "Invalid signature!" + exit 1 + fi + } + + for docker_reg in "homeassistant" "ghcr.io/home-assistant"; do + docker pull "${docker_reg}/amd64-homeassistant:${{ needs.init.outputs.version }}" + docker pull "${docker_reg}/i386-homeassistant:${{ needs.init.outputs.version }}" + docker pull "${docker_reg}/armhf-homeassistant:${{ needs.init.outputs.version }}" + docker pull "${docker_reg}/armv7-homeassistant:${{ needs.init.outputs.version }}" + docker pull "${docker_reg}/aarch64-homeassistant:${{ needs.init.outputs.version }}" + + validate_image "${docker_reg}/amd64-homeassistant:${{ needs.init.outputs.version }}" + validate_image "${docker_reg}/i386-homeassistant:${{ needs.init.outputs.version }}" + validate_image "${docker_reg}/armhf-homeassistant:${{ needs.init.outputs.version }}" + validate_image "${docker_reg}/armv7-homeassistant:${{ needs.init.outputs.version }}" + validate_image "${docker_reg}/aarch64-homeassistant:${{ needs.init.outputs.version }}" + + # Create version tag + create_manifest "${docker_reg}" "${{ needs.init.outputs.version }}" "${{ needs.init.outputs.version }}" + + # Create general tags + if [[ "${{ needs.init.outputs.version }}" =~ d ]]; then + create_manifest "${docker_reg}" "dev" "${{ needs.init.outputs.version }}" + elif [[ "${{ needs.init.outputs.version }}" =~ b ]]; then + create_manifest "${docker_reg}" "beta" "${{ needs.init.outputs.version }}" + create_manifest "${docker_reg}" "rc" "${{ needs.init.outputs.version }}" + else + create_manifest "${docker_reg}" "stable" "${{ needs.init.outputs.version }}" + create_manifest "${docker_reg}" "latest" "${{ needs.init.outputs.version }}" + create_manifest "${docker_reg}" "beta" "${{ needs.init.outputs.version }}" + create_manifest "${docker_reg}" "rc" "${{ needs.init.outputs.version }}" + fi + done diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml deleted file mode 100644 index 74aa05e58f33a..0000000000000 --- a/azure-pipelines-release.yml +++ /dev/null @@ -1,323 +0,0 @@ -# https://dev.azure.com/home-assistant - -trigger: - tags: - include: - - '*' -pr: none -schedules: - - cron: "0 1 * * *" - displayName: "nightly builds" - branches: - include: - - dev - always: true -variables: - - name: versionBuilder - value: '2021.02.0' - - group: docker - - group: github - - group: twine -resources: - repositories: - - repository: azure - type: github - name: 'home-assistant/ci-azure' - endpoint: 'home-assistant' - -stages: - -- stage: 'Validate' - jobs: - - template: templates/azp-job-version.yaml@azure - parameters: - ignoreDev: true - - job: 'Permission' - pool: - vmImage: 'ubuntu-latest' - steps: - - script: | - sudo apt-get install -y --no-install-recommends \ - jq curl - - release="$(Build.SourceBranchName)" - created_by="$(curl -s https://api.github.com/repos/home-assistant/core/releases/tags/${release} | jq --raw-output '.author.login')" - - if [[ "${created_by}" =~ ^(balloob|pvizeli|fabaff|robbiet480|bramkragten|frenck)$ ]]; then - exit 0 - fi - - echo "${created_by} is not allowed to create an release!" - exit 1 - displayName: 'Check rights' - condition: and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags')) - -- stage: 'Build' - jobs: - - job: 'ReleasePython' - condition: startsWith(variables['Build.SourceBranch'], 'refs/tags') - pool: - vmImage: 'ubuntu-latest' - steps: - - task: UsePythonVersion@0 - displayName: 'Use Python 3.8' - inputs: - versionSpec: '3.8' - - script: pip install twine wheel - displayName: 'Install tools' - - script: python setup.py sdist bdist_wheel - displayName: 'Build package' - - script: | - export TWINE_USERNAME="$(twineUser)" - export TWINE_PASSWORD="$(twinePassword)" - - twine upload dist/* --skip-existing - displayName: 'Upload pypi' - - job: 'ReleaseDocker' - timeoutInMinutes: 240 - pool: - vmImage: 'ubuntu-latest' - strategy: - maxParallel: 5 - matrix: - amd64: - buildArch: 'amd64' - i386: - buildArch: 'i386' - armhf: - buildArch: 'armhf' - armv7: - buildArch: 'armv7' - aarch64: - buildArch: 'aarch64' - steps: - - template: templates/azp-step-ha-version.yaml@azure - - script: | - docker login -u $(dockerUser) -p $(dockerPassword) - displayName: 'Docker hub login' - - script: docker pull homeassistant/amd64-builder:$(versionBuilder) - displayName: 'Install Builder' - - script: | - set -e - - docker run --rm --privileged \ - -v ~/.docker:/root/.docker:rw \ - -v /run/docker.sock:/run/docker.sock:rw \ - -v $(pwd):/data:ro \ - homeassistant/amd64-builder:$(versionBuilder) \ - --generic $(homeassistantRelease) "--$(buildArch)" -t /data \ - displayName: 'Build Release' - - job: 'ReleaseMachine' - dependsOn: - - ReleaseDocker - timeoutInMinutes: 240 - pool: - vmImage: 'ubuntu-latest' - strategy: - maxParallel: 17 - matrix: - qemux86-64: - buildMachine: 'qemux86-64' - generic-x86-64: - buildMachine: 'generic-x86-64' - intel-nuc: - buildMachine: 'intel-nuc' - qemux86: - buildMachine: 'qemux86' - qemuarm: - buildMachine: 'qemuarm' - raspberrypi: - buildMachine: 'raspberrypi' - raspberrypi2: - buildMachine: 'raspberrypi2' - raspberrypi3: - buildMachine: 'raspberrypi3' - raspberrypi4: - buildMachine: 'raspberrypi4' - odroid-xu: - buildMachine: 'odroid-xu' - tinker: - buildMachine: 'tinker' - qemuarm-64: - buildMachine: 'qemuarm-64' - raspberrypi3-64: - buildMachine: 'raspberrypi3-64' - raspberrypi4-64: - buildMachine: 'raspberrypi4-64' - odroid-c2: - buildMachine: 'odroid-c2' - odroid-c4: - buildMachine: 'odroid-c4' - odroid-n2: - buildMachine: 'odroid-n2' - steps: - - template: templates/azp-step-ha-version.yaml@azure - - script: | - docker login -u $(dockerUser) -p $(dockerPassword) - displayName: 'Docker hub login' - - script: docker pull homeassistant/amd64-builder:$(versionBuilder) - displayName: 'Install Builder' - - script: | - set -e - - docker run --rm --privileged \ - -v ~/.docker:/root/.docker \ - -v /run/docker.sock:/run/docker.sock:rw \ - -v $(pwd):/data:ro \ - homeassistant/amd64-builder:$(versionBuilder) \ - --homeassistant-machine "$(homeassistantRelease)=$(buildMachine)" \ - -t /data/machine --docker-hub homeassistant - displayName: 'Build Machine' - -- stage: 'Publish' - jobs: - - job: 'ReleaseHassio' - pool: - vmImage: 'ubuntu-latest' - steps: - - template: templates/azp-step-ha-version.yaml@azure - - script: | - sudo apt-get install -y --no-install-recommends \ - git jq curl - - git config --global user.name "Pascal Vizeli" - git config --global user.email "pvizeli@syshack.ch" - git config --global credential.helper store - - echo "https://$(githubToken):x-oauth-basic@github.com" > $HOME/.git-credentials - displayName: 'Install requirements' - - script: | - set -e - - version="$(homeassistantRelease)" - - git clone https://github.com/home-assistant/version - cd version - - dev_version="$(jq --raw-output '.homeassistant.default' dev.json)" - beta_version="$(jq --raw-output '.homeassistant.default' beta.json)" - stable_version="$(jq --raw-output '.homeassistant.default' stable.json)" - - if [[ "$version" =~ d ]]; then - sed -i "s|$dev_version|$version|g" dev.json - elif [[ "$version" =~ b ]]; then - sed -i "s|$beta_version|$version|g" beta.json - else - sed -i "s|$beta_version|$version|g" beta.json - sed -i "s|$stable_version|$version|g" stable.json - fi - - git commit -am "Bump Home Assistant $version" - git push - displayName: "Update version files" - - job: 'ReleaseDocker' - pool: - vmImage: 'ubuntu-latest' - steps: - - template: templates/azp-step-ha-version.yaml@azure - - script: | - docker login -u $(dockerUser) -p $(dockerPassword) - displayName: 'Docker login' - - script: | - set -e - export DOCKER_CLI_EXPERIMENTAL=enabled - - function create_manifest() { - local tag_l=$1 - local tag_r=$2 - - docker manifest create homeassistant/home-assistant:${tag_l} \ - homeassistant/amd64-homeassistant:${tag_r} \ - homeassistant/i386-homeassistant:${tag_r} \ - homeassistant/armhf-homeassistant:${tag_r} \ - homeassistant/armv7-homeassistant:${tag_r} \ - homeassistant/aarch64-homeassistant:${tag_r} - - docker manifest annotate homeassistant/home-assistant:${tag_l} \ - homeassistant/amd64-homeassistant:${tag_r} \ - --os linux --arch amd64 - - docker manifest annotate homeassistant/home-assistant:${tag_l} \ - homeassistant/i386-homeassistant:${tag_r} \ - --os linux --arch 386 - - docker manifest annotate homeassistant/home-assistant:${tag_l} \ - homeassistant/armhf-homeassistant:${tag_r} \ - --os linux --arch arm --variant=v6 - - docker manifest annotate homeassistant/home-assistant:${tag_l} \ - homeassistant/armv7-homeassistant:${tag_r} \ - --os linux --arch arm --variant=v7 - - docker manifest annotate homeassistant/home-assistant:${tag_l} \ - homeassistant/aarch64-homeassistant:${tag_r} \ - --os linux --arch arm64 --variant=v8 - - docker manifest push --purge homeassistant/home-assistant:${tag_l} - } - - docker pull homeassistant/amd64-homeassistant:$(homeassistantRelease) - docker pull homeassistant/i386-homeassistant:$(homeassistantRelease) - docker pull homeassistant/armhf-homeassistant:$(homeassistantRelease) - docker pull homeassistant/armv7-homeassistant:$(homeassistantRelease) - docker pull homeassistant/aarch64-homeassistant:$(homeassistantRelease) - - # Create version tag - create_manifest "$(homeassistantRelease)" "$(homeassistantRelease)" - - # Create general tags - if [[ "$(homeassistantRelease)" =~ d ]]; then - create_manifest "dev" "$(homeassistantRelease)" - elif [[ "$(homeassistantRelease)" =~ b ]]; then - create_manifest "beta" "$(homeassistantRelease)" - create_manifest "rc" "$(homeassistantRelease)" - else - create_manifest "stable" "$(homeassistantRelease)" - create_manifest "latest" "$(homeassistantRelease)" - create_manifest "beta" "$(homeassistantRelease)" - create_manifest "rc" "$(homeassistantRelease)" - fi - - displayName: 'Create Meta-Image' - -- stage: 'Addidional' - jobs: - - job: 'Updater' - pool: - vmImage: 'ubuntu-latest' - variables: - - group: gcloud - steps: - - template: templates/azp-step-ha-version.yaml@azure - - script: | - set -e - - export CLOUDSDK_CORE_DISABLE_PROMPTS=1 - - curl -o google-cloud-sdk.tar.gz https://dl.google.com/dl/cloudsdk/release/google-cloud-sdk.tar.gz - tar -C . -xvf google-cloud-sdk.tar.gz - rm -f google-cloud-sdk.tar.gz - ./google-cloud-sdk/install.sh - displayName: 'Setup gCloud' - condition: eq(variables['homeassistantReleaseStable'], 'true') - - script: | - set -e - - export CLOUDSDK_CORE_DISABLE_PROMPTS=1 - - echo "$(gcloudAnalytic)" > gcloud_auth.json - ./google-cloud-sdk/bin/gcloud auth activate-service-account --key-file gcloud_auth.json - rm -f gcloud_auth.json - displayName: 'Auth gCloud' - condition: eq(variables['homeassistantReleaseStable'], 'true') - - script: | - set -e - - export CLOUDSDK_CORE_DISABLE_PROMPTS=1 - - ./google-cloud-sdk/bin/gcloud functions deploy Analytics-Receiver \ - --project home-assistant-analytics \ - --update-env-vars VERSION=$(homeassistantRelease) \ - --source gs://analytics-src/function-source.zip - displayName: 'Push details to updater' - condition: eq(variables['homeassistantReleaseStable'], 'true') diff --git a/build.json b/build.json index 0183b61c67c35..eeeb1f9150d1b 100644 --- a/build.json +++ b/build.json @@ -1,14 +1,22 @@ { "image": "homeassistant/{arch}-homeassistant", + "shadow_repository": "ghcr.io/home-assistant", "build_from": { - "aarch64": "homeassistant/aarch64-homeassistant-base:2021.02.0", - "armhf": "homeassistant/armhf-homeassistant-base:2021.02.0", - "armv7": "homeassistant/armv7-homeassistant-base:2021.02.0", - "amd64": "homeassistant/amd64-homeassistant-base:2021.02.0", - "i386": "homeassistant/i386-homeassistant-base:2021.02.0" + "aarch64": "ghcr.io/home-assistant/aarch64-homeassistant-base:2021.04.2", + "armhf": "ghcr.io/home-assistant/armhf-homeassistant-base:2021.04.2", + "armv7": "ghcr.io/home-assistant/armv7-homeassistant-base:2021.04.2", + "amd64": "ghcr.io/home-assistant/amd64-homeassistant-base:2021.04.2", + "i386": "ghcr.io/home-assistant/i386-homeassistant-base:2021.04.2" }, "labels": { - "io.hass.type": "core" + "io.hass.type": "core", + "org.opencontainers.image.title": "Home Assistant", + "org.opencontainers.image.description": "Open-source home automation platform running on Python 3", + "org.opencontainers.image.source": "https://github.com/home-assistant/core", + "org.opencontainers.image.authors": "The Home Assistant Authors", + "org.opencontainers.image.url": "https://www.home-assistant.io/", + "org.opencontainers.image.documentation": "https://www.home-assistant.io/docs/", + "org.opencontainers.image.licenses": "Apache License 2.0" }, "version_tag": true } diff --git a/machine/build.json b/machine/build.json new file mode 100644 index 0000000000000..3b4d804dc1c1b --- /dev/null +++ b/machine/build.json @@ -0,0 +1,16 @@ +{ + "image": "homeassistant/{machine}-homeassistant", + "shadow_repository": "ghcr.io/home-assistant", + "build_from": { + "aarch64": "ghcr.io/home-assistant/aarch64-homeassistant:", + "armv7": "ghcr.io/home-assistant/armv7-homeassistant:", + "armhf": "ghcr.io/home-assistant/armhf-homeassistant:", + "amd64": "ghcr.io/home-assistant/amd64-homeassistant:", + "i386": "ghcr.io/home-assistant/i386-homeassistant:" + }, + "labels": { + "io.hass.type": "core", + "org.opencontainers.image.source": "https://github.com/home-assistant/core" + }, + "version_tag": true +} diff --git a/rootfs/etc/services.d/home-assistant/run b/rootfs/etc/services.d/home-assistant/run index 11af113e4b9eb..e1e1f075fb9d0 100644 --- a/rootfs/etc/services.d/home-assistant/run +++ b/rootfs/etc/services.d/home-assistant/run @@ -5,8 +5,8 @@ cd /config || bashio::exit.nok "Can't find config folder!" -# Enable Jemalloc for Home Assistant Core, unless disabled -if [[ -z "${DISABLE_JEMALLOC+x}" ]]; then - export LD_PRELOAD="/usr/local/lib/libjemalloc.so.2" +# Enable mimalloc for Home Assistant Core, unless disabled +if [[ -z "${DISABLE_MIMALLOC+x}" ]]; then + export LD_PRELOAD="/usr/local/lib/libmimalloc.so" fi exec python3 -m homeassistant --config /config From 9e7d83b2d52feacde1d7b05d141ba375513a20de Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 26 Apr 2021 23:38:30 +0200 Subject: [PATCH 0547/1317] Don't combine old and new value on scene update (#49248) --- homeassistant/components/config/scene.py | 4 +--- homeassistant/components/config/script.py | 10 +++++++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/config/scene.py b/homeassistant/components/config/scene.py index 19cfb7cd31a2b..8507fbbe47d4f 100644 --- a/homeassistant/components/config/scene.py +++ b/homeassistant/components/config/scene.py @@ -66,13 +66,11 @@ def _write_value(self, hass, data, config_key, new_value): # Iterate through some keys that we want to have ordered in the output updated_value = OrderedDict() for key in ("id", "name", "entities"): - if key in cur_value: - updated_value[key] = cur_value[key] if key in new_value: updated_value[key] = new_value[key] # We cover all current fields above, but just in case we start # supporting more fields in the future. - updated_value.update(cur_value) updated_value.update(new_value) + data[index] = updated_value diff --git a/homeassistant/components/config/script.py b/homeassistant/components/config/script.py index a5d1bb2037b10..73b1ee0be5c97 100644 --- a/homeassistant/components/config/script.py +++ b/homeassistant/components/config/script.py @@ -16,7 +16,7 @@ async def hook(action, config_key): await hass.services.async_call(DOMAIN, SERVICE_RELOAD) hass.http.register_view( - EditKeyBasedConfigView( + EditScriptConfigView( DOMAIN, "config", SCRIPT_CONFIG_PATH, @@ -27,3 +27,11 @@ async def hook(action, config_key): ) ) return True + + +class EditScriptConfigView(EditKeyBasedConfigView): + """Edit script config.""" + + def _write_value(self, hass, data, config_key, new_value): + """Set value.""" + data[config_key] = new_value From dc50524f328030ae2c47caeaf97757916533cbe8 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 26 Apr 2021 16:59:04 -0500 Subject: [PATCH 0548/1317] Cleanup implementation of new Sonos sensors (#49716) --- homeassistant/components/sonos/__init__.py | 43 +++++++++---------- homeassistant/components/sonos/config_flow.py | 6 ++- homeassistant/components/sonos/entity.py | 10 +---- homeassistant/components/sonos/sensor.py | 16 +++---- homeassistant/components/sonos/speaker.py | 6 ++- tests/components/sonos/test_sensor.py | 1 - 6 files changed, 35 insertions(+), 47 deletions(-) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index 7a9d994737d83..dbbeecdcdb359 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -3,7 +3,6 @@ import asyncio import datetime -from functools import partial import logging import socket @@ -64,14 +63,13 @@ class SonosData: """Storage class for platform global data.""" - def __init__(self): + def __init__(self) -> None: """Initialize the data.""" - self.discovered = {} + self.discovered: dict[str, SonosSpeaker] = {} self.media_player_entities = {} self.topology_condition = asyncio.Condition() self.discovery_thread = None self.hosts_heartbeat = None - self.platforms_ready = set() async def async_setup(hass, config): @@ -90,7 +88,7 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Sonos from a config entry.""" pysonos.config.EVENTS_MODULE = events_asyncio @@ -168,25 +166,24 @@ def _discovered_player(soco: SoCo) -> None: def _async_signal_update_groups(event): async_dispatcher_send(hass, SONOS_GROUP_UPDATE) - @callback - def start_discovery(): - _LOGGER.debug("Adding discovery job") - hass.async_add_executor_job(_discovery) - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop_discovery) - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, _async_signal_update_groups + async def setup_platforms_and_discovery(): + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_setup(entry, platform) + for platform in PLATFORMS + ] ) - - @callback - def platform_ready(platform, _): - hass.data[DATA_SONOS].platforms_ready.add(platform) - if hass.data[DATA_SONOS].platforms_ready == PLATFORMS: - start_discovery() - - for platform in PLATFORMS: - task = hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop_discovery) ) - task.add_done_callback(partial(platform_ready, platform)) + entry.async_on_unload( + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_START, _async_signal_update_groups + ) + ) + _LOGGER.debug("Adding discovery job") + await hass.async_add_executor_job(_discovery) + + hass.async_create_task(setup_platforms_and_discovery()) return True diff --git a/homeassistant/components/sonos/config_flow.py b/homeassistant/components/sonos/config_flow.py index 42ac32163a4b9..6807cffa373b2 100644 --- a/homeassistant/components/sonos/config_flow.py +++ b/homeassistant/components/sonos/config_flow.py @@ -2,14 +2,16 @@ import pysonos from homeassistant import config_entries +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_entry_flow from .const import DOMAIN -async def _async_has_devices(hass): +async def _async_has_devices(hass: HomeAssistant) -> bool: """Return if there are devices that can be discovered.""" - return await hass.async_add_executor_job(pysonos.discover) + result = await hass.async_add_executor_job(pysonos.discover) + return bool(result) config_entry_flow.register_discovery_flow( diff --git a/homeassistant/components/sonos/entity.py b/homeassistant/components/sonos/entity.py index 69a88077e3189..159b3fb348adf 100644 --- a/homeassistant/components/sonos/entity.py +++ b/homeassistant/components/sonos/entity.py @@ -6,7 +6,6 @@ from pysonos.core import SoCo -from homeassistant.core import callback import homeassistant.helpers.device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity @@ -21,7 +20,7 @@ class SonosEntity(Entity): """Representation of a Sonos entity.""" - def __init__(self, speaker: SonosSpeaker, sonos_data: SonosData): + def __init__(self, speaker: SonosSpeaker, sonos_data: SonosData) -> None: """Initialize a SonosEntity.""" self.speaker = speaker self.data = sonos_data @@ -41,7 +40,7 @@ async def async_added_to_hass(self) -> None: async_dispatcher_connect( self.hass, f"{SONOS_STATE_UPDATED}-{self.soco.uid}", - self.async_write_state, + self.async_write_ha_state, ) ) @@ -72,8 +71,3 @@ def available(self) -> bool: def should_poll(self) -> bool: """Return that we should not be polled (we handle that internally).""" return False - - @callback - def async_write_state(self) -> None: - """Flush the current entity state.""" - self.async_write_ha_state() diff --git a/homeassistant/components/sonos/sensor.py b/homeassistant/components/sonos/sensor.py index 67c5040a4a411..2ca5e0979dcaa 100644 --- a/homeassistant/components/sonos/sensor.py +++ b/homeassistant/components/sonos/sensor.py @@ -10,14 +10,12 @@ from pysonos.events_base import Event as SonosEvent from pysonos.exceptions import SoCoException -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorEntity from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE, STATE_UNKNOWN from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.icon import icon_for_battery_level from homeassistant.util import dt as dt_util from . import SonosData @@ -51,6 +49,7 @@ def fetch_battery_info_or_none(soco: SoCo) -> dict[str, Any] | None: """ with contextlib.suppress(ConnectionError, TimeoutError, SoCoException): return soco.get_battery_info() + return None async def async_setup_entry(hass, config_entry, async_add_entities): @@ -76,16 +75,16 @@ async def _async_create_entities(speaker: SonosSpeaker): async_dispatcher_connect(hass, SONOS_DISCOVERY_UPDATE, _async_create_entities) -class SonosBatteryEntity(SonosEntity, Entity): +class SonosBatteryEntity(SonosEntity, SensorEntity): """Representation of a Sonos Battery entity.""" def __init__( self, speaker: SonosSpeaker, sonos_data: SonosData, battery_info: dict[str, Any] - ): + ) -> None: """Initialize a SonosBatteryEntity.""" super().__init__(speaker, sonos_data) self._battery_info: dict[str, Any] = battery_info - self._last_event: datetime.datetime = None + self._last_event: datetime.datetime | None = None async def async_added_to_hass(self) -> None: """Register polling callback when added to hass.""" @@ -185,11 +184,6 @@ def charging(self) -> bool: """Return the charging status of this battery.""" return self.power_source not in ("BATTERY", STATE_UNKNOWN) - @property - def icon(self) -> str: - """Return the icon of the sensor.""" - return icon_for_battery_level(self.battery_level, self.charging) - @property def state(self) -> int | None: """Return the state of the sensor.""" diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index b2e53755da5bd..2d67cf8041fe5 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -40,7 +40,9 @@ class SonosSpeaker: """Representation of a Sonos speaker.""" - def __init__(self, hass: HomeAssistant, soco: SoCo, speaker_info: dict[str, Any]): + def __init__( + self, hass: HomeAssistant, soco: SoCo, speaker_info: dict[str, Any] + ) -> None: """Initialize a SonosSpeaker.""" self._is_ready: bool = False self._subscriptions: list[SubscriptionBase] = [] @@ -78,7 +80,7 @@ async def async_handle_new_entity(self, entity_type: str) -> None: self._is_ready = True @callback - def async_write_entity_states(self) -> bool: + def async_write_entity_states(self) -> None: """Write states for associated SonosEntity instances.""" async_dispatcher_send(self.hass, f"{SONOS_STATE_UPDATED}-{self.soco.uid}") diff --git a/tests/components/sonos/test_sensor.py b/tests/components/sonos/test_sensor.py index 3752af7f377d6..a1fc1d7efd8db 100644 --- a/tests/components/sonos/test_sensor.py +++ b/tests/components/sonos/test_sensor.py @@ -57,6 +57,5 @@ async def test_battery_attributes(hass, config_entry, config, soco): # confirm initial state from conftest assert battery_state.state == "100" assert battery_state.attributes.get("unit_of_measurement") == "%" - assert battery_state.attributes.get("icon") == "mdi:battery-charging-100" assert battery_state.attributes.get("charging") assert battery_state.attributes.get("power_source") == "SONOS_CHARGING_RING" From 2a2e573987351dc48c9937d6b9240a800558207b Mon Sep 17 00:00:00 2001 From: djtimca <60706061+djtimca@users.noreply.github.com> Date: Mon, 26 Apr 2021 18:02:39 -0400 Subject: [PATCH 0549/1317] Bump omnilogic dependency to 0.4.5 (#49526) --- homeassistant/components/omnilogic/manifest.json | 2 +- homeassistant/components/omnilogic/sensor.py | 6 +++++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/omnilogic/manifest.json b/homeassistant/components/omnilogic/manifest.json index c6de70d0b33cd..ea2e951d08481 100644 --- a/homeassistant/components/omnilogic/manifest.json +++ b/homeassistant/components/omnilogic/manifest.json @@ -3,7 +3,7 @@ "name": "Hayward Omnilogic", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/omnilogic", - "requirements": ["omnilogic==0.4.3"], + "requirements": ["omnilogic==0.4.5"], "codeowners": ["@oliver84", "@djtimca", "@gentoosu"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/omnilogic/sensor.py b/homeassistant/components/omnilogic/sensor.py index 25457224e9f00..6e3d1593fe906 100644 --- a/homeassistant/components/omnilogic/sensor.py +++ b/homeassistant/components/omnilogic/sensor.py @@ -136,7 +136,11 @@ class OmniLogicPumpSpeedSensor(OmnilogicSensor): def state(self): """Return the state for the pump speed sensor.""" - pump_type = PUMP_TYPES[self.coordinator.data[self._item_id]["Filter-Type"]] + pump_type = PUMP_TYPES[ + self.coordinator.data[self._item_id].get( + "Filter-Type", self.coordinator.data[self._item_id].get("Type", {}) + ) + ] pump_speed = self.coordinator.data[self._item_id][self._state_key] if pump_type == "VARIABLE": diff --git a/requirements_all.txt b/requirements_all.txt index 3ee97e9a4fa47..f3e9a2c0e88d6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1040,7 +1040,7 @@ objgraph==3.4.1 oemthermostat==1.1.1 # homeassistant.components.omnilogic -omnilogic==0.4.3 +omnilogic==0.4.5 # homeassistant.components.ondilo_ico ondilo==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 34cc45f16893f..4c24079645a66 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -561,7 +561,7 @@ oauth2client==4.0.0 objgraph==3.4.1 # homeassistant.components.omnilogic -omnilogic==0.4.3 +omnilogic==0.4.5 # homeassistant.components.ondilo_ico ondilo==0.2.0 From 9d3b5cd0de25cfefdf1a87bd68ed40787554dc8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 27 Apr 2021 00:04:39 +0200 Subject: [PATCH 0550/1317] Change log severity from warn to error for custom integration version (#49726) --- homeassistant/loader.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 51bd0c2da1f50..cdf9a83145073 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -791,12 +791,12 @@ def custom_integration_warning(integration: Integration) -> None: _LOGGER.warning(CUSTOM_WARNING, integration.domain) if integration.manifest.get("version") is None: - _LOGGER.warning( + _LOGGER.error( CUSTOM_WARNING_VERSION_MISSING, integration.domain, integration.domain ) else: if not validate_custom_integration_version(integration.manifest["version"]): - _LOGGER.warning( + _LOGGER.error( CUSTOM_WARNING_VERSION_TYPE, integration.manifest["version"], integration.domain, From 677d8e9a896d0d78072c2a973c1929194643c945 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Tue, 27 Apr 2021 00:20:50 +0200 Subject: [PATCH 0551/1317] Add restore last state test to modbus sensor (#49721) --- tests/components/modbus/test_modbus_sensor.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/components/modbus/test_modbus_sensor.py b/tests/components/modbus/test_modbus_sensor.py index 59bb81f8baa19..fb8a00f8c07cb 100644 --- a/tests/components/modbus/test_modbus_sensor.py +++ b/tests/components/modbus/test_modbus_sensor.py @@ -1,4 +1,6 @@ """The tests for the Modbus sensor component.""" +from unittest import mock + import pytest from homeassistant.components.modbus.const import ( @@ -30,6 +32,7 @@ CONF_STRUCTURE, STATE_UNAVAILABLE, ) +from homeassistant.core import State from .conftest import base_config_test, base_test @@ -479,3 +482,29 @@ async def test_struct_sensor(hass, cfg, regs, expected): scan_interval=5, ) assert state == expected + + +async def test_restore_state_sensor(hass): + """Run test for sensor restore state.""" + + sensor_name = "test_sensor" + test_value = "117" + config_sensor = {CONF_NAME: sensor_name, CONF_ADDRESS: 17} + with mock.patch( + "homeassistant.components.modbus.sensor.ModbusRegisterSensor.async_get_last_state" + ) as mock_get_last_state: + mock_get_last_state.return_value = State( + f"{SENSOR_DOMAIN}.{sensor_name}", f"{test_value}" + ) + + await base_config_test( + hass, + config_sensor, + sensor_name, + SENSOR_DOMAIN, + CONF_SENSORS, + None, + method_discovery=True, + ) + entity_id = f"{SENSOR_DOMAIN}.{sensor_name}" + assert hass.states.get(entity_id).state == test_value From cd7d3ed12a754d437e224ec6501192e1397b3c32 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Tue, 27 Apr 2021 00:04:45 +0000 Subject: [PATCH 0552/1317] [ci skip] Translation update --- .../devolo_home_control/translations/nl.json | 7 +++ .../devolo_home_control/translations/no.json | 7 +++ .../components/fritz/translations/it.json | 10 ++--- .../components/fritz/translations/nl.json | 44 +++++++++++++++++++ .../components/fritz/translations/no.json | 44 +++++++++++++++++++ .../components/insteon/translations/nl.json | 2 +- .../meteo_france/translations/nl.json | 2 +- .../components/motioneye/translations/nl.json | 25 +++++++++++ .../components/motioneye/translations/no.json | 25 +++++++++++ .../simplisafe/translations/nl.json | 2 +- .../components/smarttub/translations/nl.json | 4 ++ .../components/tuya/translations/nl.json | 2 +- .../components/volumio/translations/nl.json | 2 +- .../xiaomi_aqara/translations/nl.json | 2 +- 14 files changed, 167 insertions(+), 11 deletions(-) create mode 100644 homeassistant/components/fritz/translations/nl.json create mode 100644 homeassistant/components/fritz/translations/no.json create mode 100644 homeassistant/components/motioneye/translations/nl.json create mode 100644 homeassistant/components/motioneye/translations/no.json diff --git a/homeassistant/components/devolo_home_control/translations/nl.json b/homeassistant/components/devolo_home_control/translations/nl.json index 5d79d2ec9e9c3..0ae5696a23a41 100644 --- a/homeassistant/components/devolo_home_control/translations/nl.json +++ b/homeassistant/components/devolo_home_control/translations/nl.json @@ -14,6 +14,13 @@ "password": "Wachtwoord", "username": "E-mail adres / devolo ID" } + }, + "zeroconf_confirm": { + "data": { + "mydevolo_url": "mydevolo URL", + "password": "Wachtwoord", + "username": "E-mail / devolo ID" + } } } } diff --git a/homeassistant/components/devolo_home_control/translations/no.json b/homeassistant/components/devolo_home_control/translations/no.json index ec0b9f4c3868f..3076e4679e01e 100644 --- a/homeassistant/components/devolo_home_control/translations/no.json +++ b/homeassistant/components/devolo_home_control/translations/no.json @@ -14,6 +14,13 @@ "password": "Passord", "username": "E-post / devolo ID" } + }, + "zeroconf_confirm": { + "data": { + "mydevolo_url": "mydevolo URL", + "password": "Passord", + "username": "E-post / devolo ID" + } } } } diff --git a/homeassistant/components/fritz/translations/it.json b/homeassistant/components/fritz/translations/it.json index 39da67b87289b..257198cf6848e 100644 --- a/homeassistant/components/fritz/translations/it.json +++ b/homeassistant/components/fritz/translations/it.json @@ -19,15 +19,15 @@ "username": "Nome utente" }, "description": "FRITZ! Box rilevato: {name} \n\n Configura gli strumenti del FRITZ! Box per controllare il tuo {name}", - "title": "Configura gli strumenti del FRITZ! Box" + "title": "Configura gli strumenti del FRITZ!Box" }, "reauth_confirm": { "data": { "password": "Password", "username": "Nome utente" }, - "description": "Aggiorna le credenziali di FRITZ! Box Tools per: {host} . \n\n FRITZ! Box Tools non riesce ad accedere al tuo FRITZ! Box.", - "title": "Aggiornamento degli strumenti del FRITZ! Box - credenziali" + "description": "Aggiorna le credenziali di FRITZ!Box Tools per: {host} . \n\n FRITZ!Box Tools non riesce ad accedere al tuo FRITZ! Box.", + "title": "Aggiornamento degli strumenti del FRITZ!Box - credenziali" }, "start_config": { "data": { @@ -36,8 +36,8 @@ "port": "Porta", "username": "Nome utente" }, - "description": "Configura gli strumenti FRITZ! Box per controllare il tuo FRITZ! Box.\n Minimo necessario: nome utente, password.", - "title": "Configurazione degli strumenti FRITZ! Box - obbligatorio" + "description": "Configura gli strumenti FRITZ!Box per controllare il tuo FRITZ!Box.\n Minimo necessario: nome utente, password.", + "title": "Configurazione degli strumenti FRITZ!Box - obbligatorio" } } } diff --git a/homeassistant/components/fritz/translations/nl.json b/homeassistant/components/fritz/translations/nl.json new file mode 100644 index 0000000000000..563603aef5fb8 --- /dev/null +++ b/homeassistant/components/fritz/translations/nl.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "already_in_progress": "De configuratiestroom is al aan de gang", + "reauth_successful": "Herauthenticatie was succesvol" + }, + "error": { + "already_configured": "Apparaat is al geconfigureerd", + "already_in_progress": "De configuratiestroom is al aan de gang", + "connection_error": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie" + }, + "flow_title": "FRITZ!Box Tools: {name}", + "step": { + "confirm": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam" + }, + "description": "Ontdekt FRITZ!Box: {name}\n\nStel FRITZ!box Tools in om {name} te beheren", + "title": "Setup FRITZ!Box Tools" + }, + "reauth_confirm": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam" + }, + "description": "Update FRITZ! Box Tools-inloggegevens voor: {host}. \n\n FRITZ! Box Tools kan niet inloggen op uw FRITZ!Box.", + "title": "Updating FRITZ!Box Tools - referenties" + }, + "start_config": { + "data": { + "host": "Host", + "password": "Wachtwoord", + "port": "Poort", + "username": "Gebruikersnaam" + }, + "description": "Stel FRITZ!Box Tools in om uw FRITZ!Box te bedienen.\nMinimaal nodig: gebruikersnaam, wachtwoord.", + "title": "Configureer FRITZ! Box Tools - verplicht" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritz/translations/no.json b/homeassistant/components/fritz/translations/no.json new file mode 100644 index 0000000000000..e3b642a159437 --- /dev/null +++ b/homeassistant/components/fritz/translations/no.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + }, + "error": { + "already_configured": "Enheten er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "connection_error": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning" + }, + "flow_title": "FRITZ!Box Verkt\u00f8y: {name}", + "step": { + "confirm": { + "data": { + "password": "Passord", + "username": "Brukernavn" + }, + "description": "Oppdaget FRITZ!Box: {name} \n\n Konfigurer FRITZ!Box-verkt\u00f8y for \u00e5 kontrollere {name}", + "title": "Sett opp FRITZ!Box verkt\u00f8y" + }, + "reauth_confirm": { + "data": { + "password": "Passord", + "username": "Brukernavn" + }, + "description": "Oppdater legitimasjonen til FRITZ!Box Tools for: {host} . \n\n FRITZ!Box Tools kan ikke logge p\u00e5 FRITZ! Box.", + "title": "Oppdaterer FRITZ!Box verkt\u00f8y - legitimasjon" + }, + "start_config": { + "data": { + "host": "Vert", + "password": "Passord", + "port": "Port", + "username": "Brukernavn" + }, + "description": "Sett opp FRITZ!Box verkt\u00f8y for \u00e5 kontrollere fritz! Boksen.\nMinimum n\u00f8dvendig: brukernavn, passord.", + "title": "Sett opp FRITZ!Box verkt\u00f8y - obligatorisk" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/insteon/translations/nl.json b/homeassistant/components/insteon/translations/nl.json index 0c9191e807791..63a0bb059d51b 100644 --- a/homeassistant/components/insteon/translations/nl.json +++ b/homeassistant/components/insteon/translations/nl.json @@ -101,7 +101,7 @@ "data": { "address": "Selecteer een apparaatadres om te verwijderen" }, - "description": "Een X10 apparaat verwijderen", + "description": "Verwijder een X10 apparaat", "title": "Insteon" } } diff --git a/homeassistant/components/meteo_france/translations/nl.json b/homeassistant/components/meteo_france/translations/nl.json index f69db3ed47e6b..11b0f56777688 100644 --- a/homeassistant/components/meteo_france/translations/nl.json +++ b/homeassistant/components/meteo_france/translations/nl.json @@ -5,7 +5,7 @@ "unknown": "Onverwachte fout" }, "error": { - "empty": "Geen resultaat bij het zoeken naar een stad: controleer de invoer: stad" + "empty": "Geen resultaat bij het zoeken naar een stad: controleer het veld stad" }, "step": { "cities": { diff --git a/homeassistant/components/motioneye/translations/nl.json b/homeassistant/components/motioneye/translations/nl.json new file mode 100644 index 0000000000000..07d8dc71a103d --- /dev/null +++ b/homeassistant/components/motioneye/translations/nl.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Service is al geconfigureerd", + "reauth_successful": "Herauthenticatie was succesvol" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "invalid_url": "Ongeldige URL", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "admin_password": "Admin Wachtwoord", + "admin_username": "Admin Gebruikersnaam", + "surveillance_password": "Surveillance Wachtwoord", + "surveillance_username": "Surveillance Gebruikersnaam", + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/no.json b/homeassistant/components/motioneye/translations/no.json new file mode 100644 index 0000000000000..5b7f6538bb89a --- /dev/null +++ b/homeassistant/components/motioneye/translations/no.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Tjenesten er allerede konfigurert", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "invalid_url": "Ugyldig URL-adresse", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "admin_password": "Admin Passord", + "admin_username": "Administrator Brukernavn", + "surveillance_password": "Overv\u00e5king Passord", + "surveillance_username": "Overv\u00e5king Brukernavn", + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/nl.json b/homeassistant/components/simplisafe/translations/nl.json index 7a6d9c5c4e32b..8fa91994acaa7 100644 --- a/homeassistant/components/simplisafe/translations/nl.json +++ b/homeassistant/components/simplisafe/translations/nl.json @@ -12,7 +12,7 @@ }, "step": { "mfa": { - "description": "Controleer uw e-mail voor een link van SimpliSafe. Nadat u de link hebt geverifieerd, gaat u hier terug om de installatie van de integratie te voltooien.", + "description": "Controleer uw e-mail voor een link van SimpliSafe. Nadat u de link hebt geverifieerd, kom hier terug om de installatie van de integratie te voltooien.", "title": "SimpliSafe Multi-Factor Authenticatie" }, "reauth_confirm": { diff --git a/homeassistant/components/smarttub/translations/nl.json b/homeassistant/components/smarttub/translations/nl.json index 7ef935d8cee18..d434c22b398fc 100644 --- a/homeassistant/components/smarttub/translations/nl.json +++ b/homeassistant/components/smarttub/translations/nl.json @@ -9,6 +9,10 @@ "unknown": "Onverwachte fout" }, "step": { + "reauth_confirm": { + "description": "De SmartTub-integratie moet uw account opnieuw verifi\u00ebren", + "title": "Verifieer de integratie opnieuw" + }, "user": { "data": { "email": "E-mail", diff --git a/homeassistant/components/tuya/translations/nl.json b/homeassistant/components/tuya/translations/nl.json index b42922822f0fe..e0374fd392640 100644 --- a/homeassistant/components/tuya/translations/nl.json +++ b/homeassistant/components/tuya/translations/nl.json @@ -17,7 +17,7 @@ "platform": "De app waar uw account is geregistreerd", "username": "Gebruikersnaam" }, - "description": "Voer uw Tuya-referentie in.", + "description": "Voer uw Tuya-inloggegevens in.", "title": "Tuya" } } diff --git a/homeassistant/components/volumio/translations/nl.json b/homeassistant/components/volumio/translations/nl.json index 96538422fe0c8..c7a2f6b44c46d 100644 --- a/homeassistant/components/volumio/translations/nl.json +++ b/homeassistant/components/volumio/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "cannot_connect": "Kan geen verbinding maken met Volumio" + "cannot_connect": "Kan geen verbinding maken met ontdekte Volumio" }, "error": { "cannot_connect": "Kan geen verbinding maken", diff --git a/homeassistant/components/xiaomi_aqara/translations/nl.json b/homeassistant/components/xiaomi_aqara/translations/nl.json index a356ed36e1bba..45a531249a404 100644 --- a/homeassistant/components/xiaomi_aqara/translations/nl.json +++ b/homeassistant/components/xiaomi_aqara/translations/nl.json @@ -6,7 +6,7 @@ "not_xiaomi_aqara": "Geen Xiaomi Aqara Gateway, ontdekt apparaat kwam niet overeen met bekende gateways" }, "error": { - "discovery_error": "Het is niet gelukt om een Xiaomi Aqara Gateway te vinden, probeer het IP van het apparaat waarop HomeAssistant draait als interface te gebruiken", + "discovery_error": "Het is niet gelukt een Xiaomi Aqara Gateway te vinden, probeer het IP van het apparaat waarop HomeAssistant draait als interface te gebruiken", "invalid_host": "Ongeldige hostnaam of IP-adres, zie https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem", "invalid_interface": "Ongeldige netwerkinterface", "invalid_key": "Ongeldige gatewaysleutel", From d9714e6b79ef42414c3e8d4a1baf7355ebc980b7 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Mon, 26 Apr 2021 22:21:41 -0400 Subject: [PATCH 0553/1317] Use core constants for nad (#49709) --- homeassistant/components/nad/media_player.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nad/media_player.py b/homeassistant/components/nad/media_player.py index e7f83c66efac9..ef8a9de37ee3f 100644 --- a/homeassistant/components/nad/media_player.py +++ b/homeassistant/components/nad/media_player.py @@ -11,7 +11,14 @@ SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, ) -from homeassistant.const import CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PORT, + CONF_TYPE, + STATE_OFF, + STATE_ON, +) import homeassistant.helpers.config_validation as cv DEFAULT_TYPE = "RS232" @@ -31,9 +38,7 @@ | SUPPORT_SELECT_SOURCE ) -CONF_TYPE = "type" CONF_SERIAL_PORT = "serial_port" # for NADReceiver -CONF_PORT = "port" # for NADReceiverTelnet CONF_MIN_VOLUME = "min_volume" CONF_MAX_VOLUME = "max_volume" CONF_VOLUME_STEP = "volume_step" # for NADReceiverTCP From b27e9e376dffd4d26fc86914e83d09e59e46c3a2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 26 Apr 2021 19:20:31 -1000 Subject: [PATCH 0554/1317] Use StaticPool for recorder and NullPool for all other threads with sqlite3 (#49693) --- homeassistant/components/recorder/__init__.py | 3 ++ homeassistant/components/recorder/pool.py | 34 +++++++++++++++++++ tests/components/recorder/test_init.py | 21 ++++++++---- tests/components/recorder/test_pool.py | 34 +++++++++++++++++++ 4 files changed, 86 insertions(+), 6 deletions(-) create mode 100644 homeassistant/components/recorder/pool.py create mode 100644 tests/components/recorder/test_pool.py diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 9e9592f868705..b4be8852f551b 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -43,6 +43,7 @@ from . import migration, purge from .const import CONF_DB_INTEGRITY_CHECK, DATA_INSTANCE, DOMAIN, SQLITE_URL_PREFIX from .models import Base, Events, RecorderRuns, States +from .pool import RecorderPool from .util import ( dburl_to_path, end_incomplete_runs, @@ -783,6 +784,8 @@ def setup_recorder_connection(dbapi_connection, connection_record): kwargs["connect_args"] = {"check_same_thread": False} kwargs["poolclass"] = StaticPool kwargs["pool_reset_on_return"] = None + elif self.db_url.startswith(SQLITE_URL_PREFIX): + kwargs["poolclass"] = RecorderPool else: kwargs["echo"] = False diff --git a/homeassistant/components/recorder/pool.py b/homeassistant/components/recorder/pool.py new file mode 100644 index 0000000000000..9ee89d248cced --- /dev/null +++ b/homeassistant/components/recorder/pool.py @@ -0,0 +1,34 @@ +"""A pool for sqlite connections.""" +import threading + +from sqlalchemy.pool import NullPool, StaticPool + + +class RecorderPool(StaticPool, NullPool): + """A hybird of NullPool and StaticPool. + + When called from the creating thread acts like StaticPool + When called from any other thread, acts like NullPool + """ + + def __init__(self, *args, **kw): # pylint: disable=super-init-not-called + """Create the pool.""" + self._tid = threading.current_thread().ident + StaticPool.__init__(self, *args, **kw) + + def _do_return_conn(self, conn): + if threading.current_thread().ident == self._tid: + return super()._do_return_conn(conn) + conn.close() + + def dispose(self): + """Dispose of the connection.""" + if threading.current_thread().ident == self._tid: + return super().dispose() + + def _do_get(self): + if threading.current_thread().ident == self._tid: + return super()._do_get() + return super( # pylint: disable=bad-super-call + NullPool, self + )._create_connection() diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index dddba971aadc4..70271634ff525 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -1,9 +1,10 @@ """The tests for the Recorder component.""" # pylint: disable=protected-access from datetime import datetime, timedelta +import sqlite3 from unittest.mock import patch -from sqlalchemy.exc import OperationalError, SQLAlchemyError +from sqlalchemy.exc import DatabaseError, OperationalError, SQLAlchemyError from homeassistant.components import recorder from homeassistant.components.recorder import ( @@ -885,6 +886,9 @@ def _create_tmpdir_for_test_db(): hass.states.async_set("test.lost", "on", {}) + sqlite3_exception = DatabaseError("statement", {}, []) + sqlite3_exception.__cause__ = sqlite3.DatabaseError() + with patch.object( hass.data[DATA_INSTANCE].event_session, "close", @@ -894,11 +898,16 @@ def _create_tmpdir_for_test_db(): await hass.async_add_executor_job(corrupt_db_file, test_db_file) await async_wait_recording_done_without_instance(hass) - # This state will not be recorded because - # the database corruption will be discovered - # and we will have to rollback to recover - hass.states.async_set("test.one", "off", {}) - await async_wait_recording_done_without_instance(hass) + with patch.object( + hass.data[DATA_INSTANCE].event_session, + "commit", + side_effect=[sqlite3_exception, None], + ): + # This state will not be recorded because + # the database corruption will be discovered + # and we will have to rollback to recover + hass.states.async_set("test.one", "off", {}) + await async_wait_recording_done_without_instance(hass) assert "Unrecoverable sqlite3 database corruption detected" in caplog.text assert "The system will rename the corrupt database file" in caplog.text diff --git a/tests/components/recorder/test_pool.py b/tests/components/recorder/test_pool.py new file mode 100644 index 0000000000000..e59dc18fc8bc4 --- /dev/null +++ b/tests/components/recorder/test_pool.py @@ -0,0 +1,34 @@ +"""Test pool.""" +import threading + +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker + +from homeassistant.components.recorder.pool import RecorderPool + + +def test_recorder_pool(): + """Test RecorderPool gives the same connection in the creating thread.""" + + engine = create_engine("sqlite://", poolclass=RecorderPool) + get_session = sessionmaker(bind=engine) + + connections = [] + + def _get_connection_twice(): + session = get_session() + connections.append(session.connection().connection.connection) + session.close() + + session = get_session() + connections.append(session.connection().connection.connection) + session.close() + + _get_connection_twice() + assert connections[0] == connections[1] + + new_thread = threading.Thread(target=_get_connection_twice) + new_thread.start() + new_thread.join() + + assert connections[2] != connections[3] From 58ad3b61f75ff3dbdedf20fdabc8f5b435c0e498 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 27 Apr 2021 08:43:06 +0200 Subject: [PATCH 0555/1317] Entities for secondary temperature values created by certain Xiaomi devices in deCONZ (#49724) * Create sensors for secondary temperature values created by certain Xiaomi devices * Fix tests --- homeassistant/components/deconz/sensor.py | 47 +++++++++++++++++++ tests/components/deconz/test_binary_sensor.py | 20 ++++++-- tests/components/deconz/test_sensor.py | 30 +++++++++--- 3 files changed, 87 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 92686892d6a47..ba3be37da4205 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -114,6 +114,12 @@ def async_add_sensor(sensors=gateway.api.sensors.values()): ): entities.append(DeconzSensor(sensor, gateway)) + if sensor.secondary_temperature: + known_temperature_sensors = set(gateway.entities[DOMAIN]) + new_temperature_sensor = DeconzTemperature(sensor, gateway) + if new_temperature_sensor.unique_id not in known_temperature_sensors: + entities.append(new_temperature_sensor) + if entities: async_add_entities(entities) @@ -192,6 +198,47 @@ def extra_state_attributes(self): return attr +class DeconzTemperature(DeconzDevice, SensorEntity): + """Representation of a deCONZ temperature sensor. + + Extra temperature sensor on certain Xiaomi devices. + """ + + TYPE = DOMAIN + + @property + def unique_id(self): + """Return a unique identifier for this device.""" + return f"{self.serial}-temperature" + + @callback + def async_update_callback(self, force_update=False): + """Update the sensor's state.""" + keys = {"temperature", "reachable"} + if force_update or self._device.changed_keys.intersection(keys): + super().async_update_callback(force_update=force_update) + + @property + def state(self): + """Return the state of the sensor.""" + return self._device.secondary_temperature + + @property + def name(self): + """Return the name of the temperature sensor.""" + return f"{self._device.name} Temperature" + + @property + def device_class(self): + """Return the class of the sensor.""" + return DEVICE_CLASS_TEMPERATURE + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this sensor.""" + return TEMP_CELSIUS + + class DeconzBattery(DeconzDevice, SensorEntity): """Battery class for when a device is only represented as an event.""" diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py index 9d4c86ead6c9f..6ba79dfe4abb7 100644 --- a/tests/components/deconz/test_binary_sensor.py +++ b/tests/components/deconz/test_binary_sensor.py @@ -13,7 +13,13 @@ DOMAIN as DECONZ_DOMAIN, ) from homeassistant.components.deconz.services import SERVICE_DEVICE_REFRESH -from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + DEVICE_CLASS_TEMPERATURE, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, +) from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_registry import async_entries_for_config_entry @@ -72,15 +78,21 @@ async def test_binary_sensors(hass, aioclient_mock, mock_deconz_websocket): with patch.dict(DECONZ_WEB_REQUEST, data): config_entry = await setup_deconz_integration(hass, aioclient_mock) - assert len(hass.states.async_all()) == 3 + assert len(hass.states.async_all()) == 5 presence_sensor = hass.states.get("binary_sensor.presence_sensor") assert presence_sensor.state == STATE_OFF - assert presence_sensor.attributes["device_class"] == DEVICE_CLASS_MOTION + assert presence_sensor.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_MOTION + presence_temp = hass.states.get("sensor.presence_sensor_temperature") + assert presence_temp.state == "0.1" + assert presence_temp.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_TEMPERATURE assert hass.states.get("binary_sensor.temperature_sensor") is None assert hass.states.get("binary_sensor.clip_presence_sensor") is None vibration_sensor = hass.states.get("binary_sensor.vibration_sensor") assert vibration_sensor.state == STATE_ON - assert vibration_sensor.attributes["device_class"] == DEVICE_CLASS_VIBRATION + assert vibration_sensor.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_VIBRATION + vibration_temp = hass.states.get("sensor.vibration_sensor_temperature") + assert vibration_temp.state == "0.1" + assert vibration_temp.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_TEMPERATURE event_changed_sensor = { "t": "event", diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index a4d4e0063664e..d9c4adf138859 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -12,6 +12,7 @@ DEVICE_CLASS_BATTERY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_POWER, + DEVICE_CLASS_TEMPERATURE, STATE_UNAVAILABLE, STATE_UNKNOWN, ) @@ -89,13 +90,17 @@ async def test_sensors(hass, aioclient_mock, mock_deconz_websocket): with patch.dict(DECONZ_WEB_REQUEST, data): config_entry = await setup_deconz_integration(hass, aioclient_mock) - assert len(hass.states.async_all()) == 5 + assert len(hass.states.async_all()) == 6 light_level_sensor = hass.states.get("sensor.light_level_sensor") assert light_level_sensor.state == "999.8" assert light_level_sensor.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_ILLUMINANCE assert light_level_sensor.attributes[ATTR_DAYLIGHT] == 6955 + light_level_temp = hass.states.get("sensor.light_level_sensor_temperature") + assert light_level_temp.state == "0.1" + assert light_level_temp.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_TEMPERATURE + assert not hass.states.get("sensor.presence_sensor") assert not hass.states.get("sensor.switch_1") assert not hass.states.get("sensor.switch_1_battery_level") @@ -130,6 +135,19 @@ async def test_sensors(hass, aioclient_mock, mock_deconz_websocket): assert hass.states.get("sensor.light_level_sensor").state == "1.6" + # Event signals new temperature value + + event_changed_sensor = { + "t": "event", + "e": "changed", + "r": "sensors", + "id": "1", + "config": {"temperature": 100}, + } + await mock_deconz_websocket(data=event_changed_sensor) + + assert hass.states.get("sensor.light_level_sensor_temperature").state == "1.0" + # Event signals new battery level event_changed_sensor = { @@ -148,7 +166,7 @@ async def test_sensors(hass, aioclient_mock, mock_deconz_websocket): await hass.config_entries.async_unload(config_entry.entry_id) states = hass.states.async_all() - assert len(states) == 5 + assert len(states) == 6 for state in states: assert state.state == STATE_UNAVAILABLE @@ -187,7 +205,7 @@ async def test_allow_clip_sensors(hass, aioclient_mock): options={CONF_ALLOW_CLIP_SENSOR: True}, ) - assert len(hass.states.async_all()) == 2 + assert len(hass.states.async_all()) == 3 assert hass.states.get("sensor.clip_light_level_sensor").state == "999.8" # Disallow clip sensors @@ -197,7 +215,7 @@ async def test_allow_clip_sensors(hass, aioclient_mock): ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all()) == 2 assert not hass.states.get("sensor.clip_light_level_sensor") # Allow clip sensors @@ -207,7 +225,7 @@ async def test_allow_clip_sensors(hass, aioclient_mock): ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 2 + assert len(hass.states.async_all()) == 3 assert hass.states.get("sensor.clip_light_level_sensor").state == "999.8" @@ -235,7 +253,7 @@ async def test_add_new_sensor(hass, aioclient_mock, mock_deconz_websocket): await mock_deconz_websocket(data=event_added_sensor) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all()) == 2 assert hass.states.get("sensor.light_level_sensor").state == "999.8" From a67b9eff174c79e628e7d1f5b8275c8652cad81b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 26 Apr 2021 20:46:49 -1000 Subject: [PATCH 0556/1317] Reduce config entry setup/unload boilerplate D-F (#49733) --- homeassistant/components/adguard/__init__.py | 14 ++++------ .../components/arcam_fmj/__init__.py | 10 +++---- .../components/azure_devops/__init__.py | 11 ++++---- homeassistant/components/bsblan/__init__.py | 20 ++++++------- .../components/cert_expiry/__init__.py | 9 +++--- .../components/coolmaster/__init__.py | 10 +++---- homeassistant/components/daikin/__init__.py | 23 ++++++--------- homeassistant/components/deconz/gateway.py | 14 +++------- homeassistant/components/denonavr/__init__.py | 9 +++--- .../devolo_home_control/__init__.py | 14 ++-------- homeassistant/components/dexcom/__init__.py | 15 ++-------- homeassistant/components/directv/__init__.py | 16 ++--------- homeassistant/components/doorbird/__init__.py | 15 ++-------- homeassistant/components/dsmr/__init__.py | 15 ++-------- homeassistant/components/dunehd/__init__.py | 27 +++++------------- homeassistant/components/dynalite/__init__.py | 19 ++++--------- homeassistant/components/eafm/__init__.py | 16 ++++------- homeassistant/components/ecobee/__init__.py | 21 ++++---------- homeassistant/components/econet/__init__.py | 28 +++++++------------ homeassistant/components/elgato/__init__.py | 22 +++++++-------- homeassistant/components/elkm1/__init__.py | 14 ++-------- homeassistant/components/emonitor/__init__.py | 16 ++--------- .../components/enphase_envoy/__init__.py | 16 ++--------- homeassistant/components/epson/__init__.py | 16 ++--------- homeassistant/components/esphome/__init__.py | 9 ++---- homeassistant/components/ezviz/__init__.py | 17 ++--------- .../components/faa_delays/__init__.py | 16 ++--------- .../components/fireservicerota/__init__.py | 18 ++---------- .../components/flick_electric/__init__.py | 13 ++++----- homeassistant/components/flo/__init__.py | 15 ++-------- homeassistant/components/flume/__init__.py | 15 ++-------- .../components/flunearyou/__init__.py | 28 ++++++------------- .../components/forked_daapd/__init__.py | 8 +++--- homeassistant/components/foscam/__init__.py | 23 ++------------- homeassistant/components/freebox/__init__.py | 15 ++-------- homeassistant/components/fritz/__init__.py | 15 ++-------- homeassistant/components/fritzbox/__init__.py | 15 ++-------- .../fritzbox_callmonitor/__init__.py | 15 ++-------- tests/components/foscam/test_config_flow.py | 9 ------ 39 files changed, 157 insertions(+), 464 deletions(-) diff --git a/homeassistant/components/adguard/__init__.py b/homeassistant/components/adguard/__init__.py index 2cda6d92556df..b848dcefc8ccf 100644 --- a/homeassistant/components/adguard/__init__.py +++ b/homeassistant/components/adguard/__init__.py @@ -68,10 +68,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except AdGuardHomeConnectionError as exception: raise ConfigEntryNotReady from exception - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) async def add_url(call) -> None: """Service call to add a new filter subscription to AdGuard Home.""" @@ -126,12 +123,11 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.services.async_remove(DOMAIN, SERVICE_DISABLE_URL) hass.services.async_remove(DOMAIN, SERVICE_REFRESH) - for component in PLATFORMS: - await hass.config_entries.async_forward_entry_unload(entry, component) - - del hass.data[DOMAIN] + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + del hass.data[DOMAIN] - return True + return unload_ok class AdGuardHomeEntity(Entity): diff --git a/homeassistant/components/arcam_fmj/__init__.py b/homeassistant/components/arcam_fmj/__init__.py index 8d22cb7723f0b..e1dfac09d76ff 100644 --- a/homeassistant/components/arcam_fmj/__init__.py +++ b/homeassistant/components/arcam_fmj/__init__.py @@ -27,6 +27,8 @@ CONFIG_SCHEMA = cv.deprecated(DOMAIN) +PLATFORMS = ["media_player"] + async def _await_cancel(task): task.cancel() @@ -60,23 +62,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: config_entries.ConfigEnt task = asyncio.create_task(_run_client(hass, client, DEFAULT_SCAN_INTERVAL)) tasks[entry.entry_id] = task - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "media_player") - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass, entry): """Cleanup before removing config entry.""" - await hass.config_entries.async_forward_entry_unload(entry, "media_player") + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) task = hass.data[DOMAIN_DATA_TASKS].pop(entry.entry_id) await _await_cancel(task) hass.data[DOMAIN_DATA_ENTRIES].pop(entry.entry_id) - return True + return unload_ok async def _run_client(hass, client, interval): diff --git a/homeassistant/components/azure_devops/__init__.py b/homeassistant/components/azure_devops/__init__.py index 5b0a42bb2a194..017b1246503c7 100644 --- a/homeassistant/components/azure_devops/__init__.py +++ b/homeassistant/components/azure_devops/__init__.py @@ -18,10 +18,11 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) +PLATFORMS = ["sensor"] + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Azure DevOps from a config entry.""" @@ -43,18 +44,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(instance_key, {})[DATA_AZURE_DEVOPS_CLIENT] = client # Setup components - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "sensor") - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigType) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload Azure DevOps config entry.""" del hass.data[f"{DOMAIN}_{entry.data[CONF_ORG]}_{entry.data[CONF_PROJECT]}"] - return await hass.config_entries.async_forward_entry_unload(entry, "sensor") + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) class AzureDevOpsEntity(Entity): diff --git a/homeassistant/components/bsblan/__init__.py b/homeassistant/components/bsblan/__init__.py index f452451050b63..6c6c8a183360c 100644 --- a/homeassistant/components/bsblan/__init__.py +++ b/homeassistant/components/bsblan/__init__.py @@ -14,6 +14,8 @@ SCAN_INTERVAL = timedelta(seconds=30) +PLATFORMS = [CLIMATE_DOMAIN] + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up BSB-Lan from a config entry.""" @@ -36,9 +38,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = {DATA_BSBLAN_CLIENT: bsblan} - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, CLIMATE_DOMAIN) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -46,11 +46,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload BSBLan config entry.""" - await hass.config_entries.async_forward_entry_unload(entry, CLIMATE_DOMAIN) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + # Cleanup + del hass.data[DOMAIN][entry.entry_id] + if not hass.data[DOMAIN]: + del hass.data[DOMAIN] - # Cleanup - del hass.data[DOMAIN][entry.entry_id] - if not hass.data[DOMAIN]: - del hass.data[DOMAIN] - - return True + return unload_ok diff --git a/homeassistant/components/cert_expiry/__init__.py b/homeassistant/components/cert_expiry/__init__.py index aab996873ca89..f91eaab49b66c 100644 --- a/homeassistant/components/cert_expiry/__init__.py +++ b/homeassistant/components/cert_expiry/__init__.py @@ -17,6 +17,8 @@ SCAN_INTERVAL = timedelta(hours=12) +PLATFORMS = ["sensor"] + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Load the saved entities.""" @@ -32,15 +34,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): if entry.unique_id is None: hass.config_entries.async_update_entry(entry, unique_id=f"{host}:{port}") - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "sensor") - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True async def async_unload_entry(hass, entry): """Unload a config entry.""" - return await hass.config_entries.async_forward_entry_unload(entry, "sensor") + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) class CertExpiryDataUpdateCoordinator(DataUpdateCoordinator[datetime]): diff --git a/homeassistant/components/coolmaster/__init__.py b/homeassistant/components/coolmaster/__init__.py index 2b092935bb04a..e6cf6f362777a 100644 --- a/homeassistant/components/coolmaster/__init__.py +++ b/homeassistant/components/coolmaster/__init__.py @@ -12,6 +12,8 @@ _LOGGER = logging.getLogger(__name__) +PLATFORMS = ["climate"] + async def async_setup_entry(hass, entry): """Set up Coolmaster from a config entry.""" @@ -31,20 +33,16 @@ async def async_setup_entry(hass, entry): DATA_INFO: info, DATA_COORDINATOR: coordinator, } - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "climate") - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass, entry): """Unload a Coolmaster config entry.""" - unload_ok = await hass.config_entries.async_forward_entry_unload(entry, "climate") - + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok diff --git a/homeassistant/components/daikin/__init__.py b/homeassistant/components/daikin/__init__.py index eb013e2ba30a1..fb38c38db0a48 100644 --- a/homeassistant/components/daikin/__init__.py +++ b/homeassistant/components/daikin/__init__.py @@ -81,25 +81,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): if not daikin_api: return False hass.data.setdefault(DOMAIN, {}).update({entry.entry_id: daikin_api}) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True -async def async_unload_entry(hass, config_entry): +async def async_unload_entry(hass, entry): """Unload a config entry.""" - await asyncio.wait( - [ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - ) - hass.data[DOMAIN].pop(config_entry.entry_id) - if not hass.data[DOMAIN]: - hass.data.pop(DOMAIN) - return True + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + if not hass.data[DOMAIN]: + hass.data.pop(DOMAIN) + return unload_ok async def daikin_api_setup(hass, host, key, uuid, password): diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index fa674727a8075..8b057ab9e51f3 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -175,12 +175,7 @@ async def async_setup(self) -> bool: except AuthenticationRequired as err: raise ConfigEntryAuthFailed from err - for platform in PLATFORMS: - self.hass.async_create_task( - self.hass.config_entries.async_forward_entry_setup( - self.config_entry, platform - ) - ) + self.hass.config_entries.async_setup_platforms(self.config_entry, PLATFORMS) await async_setup_events(self) @@ -250,10 +245,9 @@ async def async_reset(self): self.api.async_connection_status_callback = None self.api.close() - for platform in PLATFORMS: - await self.hass.config_entries.async_forward_entry_unload( - self.config_entry, platform - ) + await self.hass.config_entries.async_unload_platforms( + self.config_entry, PLATFORMS + ) async_unload_events(self) diff --git a/homeassistant/components/denonavr/__init__.py b/homeassistant/components/denonavr/__init__.py index fa4d161269731..76baf73c3e508 100644 --- a/homeassistant/components/denonavr/__init__.py +++ b/homeassistant/components/denonavr/__init__.py @@ -23,6 +23,7 @@ CONF_RECEIVER = "receiver" UNDO_UPDATE_LISTENER = "undo_update_listener" +PLATFORMS = ["media_player"] _LOGGER = logging.getLogger(__name__) @@ -56,9 +57,7 @@ async def async_setup_entry( UNDO_UPDATE_LISTENER: undo_listener, } - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "media_player") - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -67,8 +66,8 @@ async def async_unload_entry( hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry ): """Unload a config entry.""" - unload_ok = await hass.config_entries.async_forward_entry_unload( - config_entry, "media_player" + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS ) hass.data[DOMAIN][config_entry.entry_id][UNDO_UPDATE_LISTENER]() diff --git a/homeassistant/components/devolo_home_control/__init__.py b/homeassistant/components/devolo_home_control/__init__.py index a6918e819987d..ded30d75de989 100644 --- a/homeassistant/components/devolo_home_control/__init__.py +++ b/homeassistant/components/devolo_home_control/__init__.py @@ -58,10 +58,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except GatewayOfflineError as err: raise ConfigEntryNotReady from err - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) def shutdown(event): for gateway in hass.data[DOMAIN][entry.entry_id]["gateways"]: @@ -79,14 +76,7 @@ def shutdown(event): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) await asyncio.gather( *[ hass.async_add_executor_job(gateway.websocket_disconnect) diff --git a/homeassistant/components/dexcom/__init__.py b/homeassistant/components/dexcom/__init__.py index 1630d4b9dfdb5..1c02a86ca4286 100644 --- a/homeassistant/components/dexcom/__init__.py +++ b/homeassistant/components/dexcom/__init__.py @@ -1,5 +1,4 @@ """The Dexcom integration.""" -import asyncio from datetime import timedelta import logging @@ -67,24 +66,14 @@ async def async_update_data(): COORDINATOR ].async_config_entry_first_refresh() - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() if unload_ok: diff --git a/homeassistant/components/directv/__init__.py b/homeassistant/components/directv/__init__.py index f1f05e815a88b..45f4eeeda371c 100644 --- a/homeassistant/components/directv/__init__.py +++ b/homeassistant/components/directv/__init__.py @@ -1,7 +1,6 @@ """The DirecTV integration.""" from __future__ import annotations -import asyncio from datetime import timedelta from typing import Any @@ -42,25 +41,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = dtv - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) - + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py index 3e8e59df203f2..9a21c1b34396b 100644 --- a/homeassistant/components/doorbird/__init__.py +++ b/homeassistant/components/doorbird/__init__.py @@ -1,5 +1,4 @@ """Support for DoorBird devices.""" -import asyncio import logging from aiohttp import web @@ -167,10 +166,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): UNDO_UPDATE_LISTENER: undo_listener, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -184,14 +180,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/dsmr/__init__.py b/homeassistant/components/dsmr/__init__.py index f130f500545d6..3af620df19ca0 100644 --- a/homeassistant/components/dsmr/__init__.py +++ b/homeassistant/components/dsmr/__init__.py @@ -1,5 +1,4 @@ """The dsmr component.""" -import asyncio from asyncio import CancelledError from contextlib import suppress @@ -14,10 +13,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = {} - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) listener = entry.add_update_listener(async_update_options) hass.data[DOMAIN][entry.entry_id][DATA_LISTENER] = listener @@ -35,14 +31,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): with suppress(CancelledError): await task - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: listener() diff --git a/homeassistant/components/dunehd/__init__.py b/homeassistant/components/dunehd/__init__.py index 10c66c3bfb06f..af81b60b38e3b 100644 --- a/homeassistant/components/dunehd/__init__.py +++ b/homeassistant/components/dunehd/__init__.py @@ -1,6 +1,4 @@ """The Dune HD component.""" -import asyncio - from pdunehd import DuneHDPlayer from homeassistant.const import CONF_HOST @@ -10,35 +8,24 @@ PLATFORMS = ["media_player"] -async def async_setup_entry(hass, config_entry): +async def async_setup_entry(hass, entry): """Set up a config entry.""" - host = config_entry.data[CONF_HOST] + host = entry.data[CONF_HOST] player = DuneHDPlayer(host) hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][config_entry.entry_id] = player + hass.data[DOMAIN][entry.entry_id] = player - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True -async def async_unload_entry(hass, config_entry): +async def async_unload_entry(hass, entry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - ) - ) - + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: - hass.data[DOMAIN].pop(config_entry.entry_id) + hass.data[DOMAIN].pop(entry.entry_id) return unload_ok diff --git a/homeassistant/components/dynalite/__init__.py b/homeassistant/components/dynalite/__init__.py index 92392e4b51a89..1ee609961cc8a 100644 --- a/homeassistant/components/dynalite/__init__.py +++ b/homeassistant/components/dynalite/__init__.py @@ -1,7 +1,6 @@ """Support for the Dynalite networks.""" from __future__ import annotations -import asyncio from typing import Any import voluptuous as vol @@ -267,17 +266,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: bridge = DynaliteBridge(hass, entry.data) # need to do it before the listener hass.data[DOMAIN][entry.entry_id] = bridge - entry.add_update_listener(async_entry_changed) + entry.async_on_unload(entry.add_update_listener(async_entry_changed)) if not await bridge.async_setup(): LOGGER.error("Could not set up bridge for entry %s", entry.data) hass.data[DOMAIN][entry.entry_id] = None raise ConfigEntryNotReady - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -285,10 +281,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" LOGGER.debug("Unloading entry %s", entry.data) - hass.data[DOMAIN].pop(entry.entry_id) - tasks = [ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - results = await asyncio.gather(*tasks) - return False not in results + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + return unload_ok diff --git a/homeassistant/components/eafm/__init__.py b/homeassistant/components/eafm/__init__.py index f0ce512862448..7d2853266ff78 100644 --- a/homeassistant/components/eafm/__init__.py +++ b/homeassistant/components/eafm/__init__.py @@ -2,22 +2,16 @@ from .const import DOMAIN - -async def async_setup(hass, config): - """Set up devices.""" - hass.data[DOMAIN] = {} - return True +PLATFORMS = ["sensor"] async def async_setup_entry(hass, entry): """Set up flood monitoring sensors for this config entry.""" - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "sensor") - ) - + hass.data.setdefault(DOMAIN, {}) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True -async def async_unload_entry(hass, config_entry): +async def async_unload_entry(hass, entry): """Unload flood monitoring sensors.""" - return await hass.config_entries.async_forward_entry_unload(config_entry, "sensor") + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/ecobee/__init__.py b/homeassistant/components/ecobee/__init__.py index 015ee1fbf6cab..28aec51e81fad 100644 --- a/homeassistant/components/ecobee/__init__.py +++ b/homeassistant/components/ecobee/__init__.py @@ -1,5 +1,4 @@ """Support for ecobee.""" -import asyncio from datetime import timedelta from pyecobee import ECOBEE_API_KEY, ECOBEE_REFRESH_TOKEN, Ecobee, ExpiredTokenError @@ -60,10 +59,7 @@ async def async_setup_entry(hass, entry): hass.data[DOMAIN] = data - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -109,14 +105,9 @@ async def refresh(self) -> bool: return False -async def async_unload_entry(hass, config_entry): +async def async_unload_entry(hass, entry): """Unload the config entry and platforms.""" - hass.data.pop(DOMAIN) - - tasks = [] - for platform in PLATFORMS: - tasks.append( - hass.config_entries.async_forward_entry_unload(config_entry, platform) - ) - - return all(await asyncio.gather(*tasks)) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data.pop(DOMAIN) + return unload_ok diff --git a/homeassistant/components/econet/__init__.py b/homeassistant/components/econet/__init__.py index e605b16a2378b..5a20337e45446 100644 --- a/homeassistant/components/econet/__init__.py +++ b/homeassistant/components/econet/__init__.py @@ -1,5 +1,4 @@ """Support for EcoNet products.""" -import asyncio from datetime import timedelta import logging @@ -62,10 +61,7 @@ async def async_setup_entry(hass, config_entry): hass.data[DOMAIN][API_CLIENT][config_entry.entry_id] = api hass.data[DOMAIN][EQUIPMENT][config_entry.entry_id] = equipment - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) api.subscribe() @@ -88,25 +84,21 @@ async def fetch_update(now): """Fetch the latest changes from the API.""" await api.refresh_equipment() - async_track_time_interval(hass, resubscribe, INTERVAL) - async_track_time_interval(hass, fetch_update, INTERVAL + timedelta(minutes=1)) + config_entry.async_on_unload(async_track_time_interval(hass, resubscribe, INTERVAL)) + config_entry.async_on_unload( + async_track_time_interval(hass, fetch_update, INTERVAL + timedelta(minutes=1)) + ) return True async def async_unload_entry(hass, entry): """Unload a EcoNet config entry.""" - tasks = [ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - - await asyncio.gather(*tasks) - - hass.data[DOMAIN][API_CLIENT].pop(entry.entry_id) - hass.data[DOMAIN][EQUIPMENT].pop(entry.entry_id) - - return True + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[DOMAIN][API_CLIENT].pop(entry.entry_id) + hass.data[DOMAIN][EQUIPMENT].pop(entry.entry_id) + return unload_ok class EcoNetEntity(Entity): diff --git a/homeassistant/components/elgato/__init__.py b/homeassistant/components/elgato/__init__.py index 22d6040678008..1c83844debcec 100644 --- a/homeassistant/components/elgato/__init__.py +++ b/homeassistant/components/elgato/__init__.py @@ -12,6 +12,8 @@ from .const import DATA_ELGATO_CLIENT, DOMAIN +PLATFORMS = [LIGHT_DOMAIN] + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Elgato Key Light from a config entry.""" @@ -31,10 +33,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = {DATA_ELGATO_CLIENT: elgato} - - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, LIGHT_DOMAIN) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -42,11 +41,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload Elgato Key Light config entry.""" # Unload entities for this entry/device. - await hass.config_entries.async_forward_entry_unload(entry, LIGHT_DOMAIN) - - # Cleanup - del hass.data[DOMAIN][entry.entry_id] - if not hass.data[DOMAIN]: - del hass.data[DOMAIN] - - return True + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + # Cleanup + del hass.data[DOMAIN][entry.entry_id] + if not hass.data[DOMAIN]: + del hass.data[DOMAIN] + return unload_ok diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index 568b3109227cf..ff2f2533d24e9 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -262,10 +262,7 @@ def _element_changed(element, changeset): "keypads": {}, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -286,14 +283,7 @@ def _find_elk_by_prefix(hass, prefix): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) # disconnect cleanly hass.data[DOMAIN][entry.entry_id]["elk"].disconnect() diff --git a/homeassistant/components/emonitor/__init__.py b/homeassistant/components/emonitor/__init__.py index 74630a193a406..516f38d64c236 100644 --- a/homeassistant/components/emonitor/__init__.py +++ b/homeassistant/components/emonitor/__init__.py @@ -1,5 +1,4 @@ """The SiteSage Emonitor integration.""" -import asyncio from datetime import timedelta import logging @@ -38,27 +37,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok diff --git a/homeassistant/components/enphase_envoy/__init__.py b/homeassistant/components/enphase_envoy/__init__.py index 26318faa7f9b5..dfd6b782408d6 100644 --- a/homeassistant/components/enphase_envoy/__init__.py +++ b/homeassistant/components/enphase_envoy/__init__.py @@ -1,7 +1,6 @@ """The Enphase Envoy integration.""" from __future__ import annotations -import asyncio from datetime import timedelta import logging @@ -79,25 +78,14 @@ async def async_update_data(): NAME: name, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok diff --git a/homeassistant/components/epson/__init__.py b/homeassistant/components/epson/__init__.py index 94254f64f88a5..b560151e0585d 100644 --- a/homeassistant/components/epson/__init__.py +++ b/homeassistant/components/epson/__init__.py @@ -1,5 +1,4 @@ """The epson integration.""" -import asyncio import logging from epson_projector import Projector @@ -43,23 +42,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): return False hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = projector - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 8edb6d79bcd68..66b16cf3fe360 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -594,12 +594,9 @@ async def _cleanup_instance( async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload an esphome config entry.""" entry_data = await _cleanup_instance(hass, entry) - tasks = [] - for platform in entry_data.loaded_platforms: - tasks.append(hass.config_entries.async_forward_entry_unload(entry, platform)) - if tasks: - await asyncio.wait(tasks) - return True + return await hass.config_entries.async_unload_platforms( + entry, entry_data.loaded_platforms + ) async def platform_async_setup_entry( diff --git a/homeassistant/components/ezviz/__init__.py b/homeassistant/components/ezviz/__init__.py index 7619d83e27bc8..670e07a07dc1e 100644 --- a/homeassistant/components/ezviz/__init__.py +++ b/homeassistant/components/ezviz/__init__.py @@ -1,5 +1,4 @@ """Support for Ezviz camera.""" -import asyncio from datetime import timedelta import logging @@ -82,10 +81,7 @@ async def async_setup_entry(hass, entry): DATA_COORDINATOR: coordinator, DATA_UNDO_UPDATE_LISTENER: undo_listener, } - for component in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -96,19 +92,10 @@ async def async_unload_entry(hass, entry): if entry.data.get(CONF_TYPE) == ATTR_TYPE_CAMERA: return True - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS - ] - ) - ) - + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN][entry.entry_id][DATA_UNDO_UPDATE_LISTENER]() hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok diff --git a/homeassistant/components/faa_delays/__init__.py b/homeassistant/components/faa_delays/__init__.py index 6db9b6675264e..56cf9ad13bcd2 100644 --- a/homeassistant/components/faa_delays/__init__.py +++ b/homeassistant/components/faa_delays/__init__.py @@ -1,5 +1,4 @@ """The FAA Delays integration.""" -import asyncio from datetime import timedelta import logging @@ -30,27 +29,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok diff --git a/homeassistant/components/fireservicerota/__init__.py b/homeassistant/components/fireservicerota/__init__.py index 0a4936b6ed6df..aa10a16f0886f 100644 --- a/homeassistant/components/fireservicerota/__init__.py +++ b/homeassistant/components/fireservicerota/__init__.py @@ -1,5 +1,4 @@ """The FireServiceRota integration.""" -import asyncio from datetime import timedelta import logging @@ -59,10 +58,7 @@ async def async_update_data(): DATA_COORDINATOR: coordinator, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -73,19 +69,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.async_add_executor_job( hass.data[DOMAIN][entry.entry_id].websocket.stop_listener ) - - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) - + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: del hass.data[DOMAIN][entry.entry_id] - return unload_ok diff --git a/homeassistant/components/flick_electric/__init__.py b/homeassistant/components/flick_electric/__init__.py index 04d7b88f52b59..54167b6a55fde 100644 --- a/homeassistant/components/flick_electric/__init__.py +++ b/homeassistant/components/flick_electric/__init__.py @@ -21,6 +21,8 @@ CONF_ID_TOKEN = "id_token" +PLATFORMS = ["sensor"] + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Flick Electric from a config entry.""" @@ -29,20 +31,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = FlickAPI(auth) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "sensor") - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - if await hass.config_entries.async_forward_entry_unload(entry, "sensor"): + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) - return True - - return False + return unload_ok class HassFlickAuth(AbstractFlickAuth): diff --git a/homeassistant/components/flo/__init__.py b/homeassistant/components/flo/__init__.py index 4ea6d1dec9342..890f18ee3b751 100644 --- a/homeassistant/components/flo/__init__.py +++ b/homeassistant/components/flo/__init__.py @@ -44,25 +44,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): tasks = [device.async_refresh() for device in devices] await asyncio.gather(*tasks) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok diff --git a/homeassistant/components/flume/__init__.py b/homeassistant/components/flume/__init__.py index 9acc575602365..c8e652fefd6bd 100644 --- a/homeassistant/components/flume/__init__.py +++ b/homeassistant/components/flume/__init__.py @@ -1,5 +1,4 @@ """The flume integration.""" -import asyncio from functools import partial import logging @@ -74,24 +73,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): FLUME_HTTP_SESSION: http_session, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) hass.data[DOMAIN][entry.entry_id][FLUME_HTTP_SESSION].close() diff --git a/homeassistant/components/flunearyou/__init__.py b/homeassistant/components/flunearyou/__init__.py index 8e5e3762f32c8..6eb4d54fe4fb3 100644 --- a/homeassistant/components/flunearyou/__init__.py +++ b/homeassistant/components/flunearyou/__init__.py @@ -31,15 +31,15 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass, config_entry): +async def async_setup_entry(hass, entry): """Set up Flu Near You as config entry.""" - hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id] = {} + hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] = {} websession = aiohttp_client.async_get_clientsession(hass) client = Client(websession) - latitude = config_entry.data.get(CONF_LATITUDE, hass.config.latitude) - longitude = config_entry.data.get(CONF_LONGITUDE, hass.config.longitude) + latitude = entry.data.get(CONF_LATITUDE, hass.config.latitude) + longitude = entry.data.get(CONF_LONGITUDE, hass.config.longitude) async def async_update(api_category): """Get updated date from the API based on category.""" @@ -54,7 +54,7 @@ async def async_update(api_category): data_init_tasks = [] for api_category in [CATEGORY_CDC_REPORT, CATEGORY_USER_REPORT]: - coordinator = hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id][ + coordinator = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id][ api_category ] = DataUpdateCoordinator( hass, @@ -67,25 +67,15 @@ async def async_update(api_category): await asyncio.gather(*data_init_tasks) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True -async def async_unload_entry(hass, config_entry): +async def async_unload_entry(hass, entry): """Unload an Flu Near You config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: - hass.data[DOMAIN][DATA_COORDINATOR].pop(config_entry.entry_id) + hass.data[DOMAIN][DATA_COORDINATOR].pop(entry.entry_id) return unload_ok diff --git a/homeassistant/components/forked_daapd/__init__.py b/homeassistant/components/forked_daapd/__init__.py index 0186b18ee74f9..fc67d78d5edd9 100644 --- a/homeassistant/components/forked_daapd/__init__.py +++ b/homeassistant/components/forked_daapd/__init__.py @@ -3,18 +3,18 @@ from .const import DOMAIN, HASS_DATA_REMOVE_LISTENERS_KEY, HASS_DATA_UPDATER_KEY +PLATFORMS = [MP_DOMAIN] + async def async_setup_entry(hass, entry): """Set up forked-daapd from a config entry by forwarding to platform.""" - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, MP_DOMAIN) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass, entry): """Remove forked-daapd component.""" - status = await hass.config_entries.async_forward_entry_unload(entry, MP_DOMAIN) + status = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if status and hass.data.get(DOMAIN) and hass.data[DOMAIN].get(entry.entry_id): hass.data[DOMAIN][entry.entry_id][ HASS_DATA_UPDATER_KEY diff --git a/homeassistant/components/foscam/__init__.py b/homeassistant/components/foscam/__init__.py index 1b3ae5e72163b..308b1a3cc9ff1 100644 --- a/homeassistant/components/foscam/__init__.py +++ b/homeassistant/components/foscam/__init__.py @@ -1,5 +1,4 @@ """The foscam component.""" -import asyncio from libpyfoscam import FoscamCamera @@ -14,19 +13,11 @@ PLATFORMS = ["camera"] -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the foscam component.""" - hass.data.setdefault(DOMAIN, {}) - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up foscam from a config entry.""" - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = entry.data return True @@ -34,15 +25,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) - + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/freebox/__init__.py b/homeassistant/components/freebox/__init__.py index 976041721c361..40e01db39d176 100644 --- a/homeassistant/components/freebox/__init__.py +++ b/homeassistant/components/freebox/__init__.py @@ -1,5 +1,4 @@ """Support for Freebox devices (Freebox v6 and Freebox mini 4K).""" -import asyncio import logging import voluptuous as vol @@ -45,10 +44,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.unique_id] = router - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) # Services async def async_reboot(call): @@ -70,14 +66,7 @@ async def async_close_connection(event): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: router = hass.data[DOMAIN].pop(entry.unique_id) await router.close() diff --git a/homeassistant/components/fritz/__init__.py b/homeassistant/components/fritz/__init__.py index 6c8f54ea92899..507804bb8579f 100644 --- a/homeassistant/components/fritz/__init__.py +++ b/homeassistant/components/fritz/__init__.py @@ -1,5 +1,4 @@ """Support for AVM Fritz!Box functions.""" -import asyncio import logging from fritzconnection.core.exceptions import FritzConnectionException, FritzSecurityError @@ -52,10 +51,7 @@ def _async_unload(event): hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_unload) ) # Load the other platforms like switch - for domain in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, domain) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -65,14 +61,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigType) -> bool: fritzbox: FritzBoxTools = hass.data[DOMAIN][entry.entry_id] fritzbox.async_unload() - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index b398a1ee775ad..16b005359e1ba 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -1,7 +1,6 @@ """Support for AVM Fritz!Box smarthome devices.""" from __future__ import annotations -import asyncio from datetime import timedelta from pyfritzhome import Fritzhome, FritzhomeDevice, LoginError @@ -80,10 +79,7 @@ async def async_update_coordinator(): await coordinator.async_config_entry_first_refresh() - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) def logout_fritzbox(event): """Close connections to this fritzbox.""" @@ -101,14 +97,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: fritz = hass.data[DOMAIN][entry.entry_id][CONF_CONNECTIONS] await hass.async_add_executor_job(fritz.logout) - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/fritzbox_callmonitor/__init__.py b/homeassistant/components/fritzbox_callmonitor/__init__.py index 0ba0de598497f..4c36ee3ddfb07 100644 --- a/homeassistant/components/fritzbox_callmonitor/__init__.py +++ b/homeassistant/components/fritzbox_callmonitor/__init__.py @@ -1,5 +1,4 @@ """The fritzbox_callmonitor integration.""" -from asyncio import gather import logging from fritzconnection.core.exceptions import FritzConnectionException, FritzSecurityError @@ -54,10 +53,7 @@ async def async_setup_entry(hass, config_entry): UNDO_UPDATE_LISTENER: undo_listener, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) return True @@ -65,13 +61,8 @@ async def async_setup_entry(hass, config_entry): async def async_unload_entry(hass, config_entry): """Unloading the fritzbox_callmonitor platforms.""" - unload_ok = all( - await gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS ) hass.data[DOMAIN][config_entry.entry_id][UNDO_UPDATE_LISTENER]() diff --git a/tests/components/foscam/test_config_flow.py b/tests/components/foscam/test_config_flow.py index 3b8910c4dbc94..2f72000aaed1e 100644 --- a/tests/components/foscam/test_config_flow.py +++ b/tests/components/foscam/test_config_flow.py @@ -87,8 +87,6 @@ async def test_user_valid(hass): with patch( "homeassistant.components.foscam.config_flow.FoscamCamera", ) as mock_foscam_camera, patch( - "homeassistant.components.foscam.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.foscam.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -105,7 +103,6 @@ async def test_user_valid(hass): assert result["title"] == CAMERA_NAME assert result["data"] == VALID_CONFIG - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -263,8 +260,6 @@ async def test_import_user_valid(hass): with patch( "homeassistant.components.foscam.config_flow.FoscamCamera", ) as mock_foscam_camera, patch( - "homeassistant.components.foscam.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.foscam.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -282,7 +277,6 @@ async def test_import_user_valid(hass): assert result["title"] == CAMERA_NAME assert result["data"] == VALID_CONFIG - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -293,8 +287,6 @@ async def test_import_user_valid_with_name(hass): with patch( "homeassistant.components.foscam.config_flow.FoscamCamera", ) as mock_foscam_camera, patch( - "homeassistant.components.foscam.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.foscam.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -316,7 +308,6 @@ async def test_import_user_valid_with_name(hass): assert result["title"] == name assert result["data"] == VALID_CONFIG - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 From 1b957a0ce06af7f16e624ee6692816a6df4411ec Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 27 Apr 2021 10:36:13 +0200 Subject: [PATCH 0557/1317] Use ' instead of " for build if workflows (#49739) --- .github/workflows/builder.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index c5eccbf9b2a43..ab08962c64646 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -50,7 +50,7 @@ jobs: name: Build PyPi package needs: init runs-on: ubuntu-latest - if: needs.init.outputs.publish == "true" + if: needs.init.outputs.publish == 'true' steps: - name: Checkout the repository uses: actions/checkout@v2 @@ -86,13 +86,13 @@ jobs: uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - if: needs.init.outputs.channel == "dev" + if: needs.init.outputs.channel == 'dev' uses: actions/setup-python@v2.2.1 with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Adjust nightly version - if: needs.init.outputs.channel == "dev" + if: needs.init.outputs.channel == 'dev' shell: bash run: | python3 -m pip install packaging @@ -199,7 +199,7 @@ jobs: channel: ${{ needs.init.outputs.channel }} - name: Update version file (stable -> beta) - if: needs.init.outputs.channel == "stable" + if: needs.init.outputs.channel == 'stable' uses: home-assistant/actions/helpers/version-push@master with: key: "homeassistant[]" From e5e215353da05e6d612b09b9b9599c78101f7b25 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Tue, 27 Apr 2021 10:49:41 +0200 Subject: [PATCH 0558/1317] Add swap byte/word/byteword option to modbus sensor (#49719) Co-authored-by: Martin Hjelmare --- homeassistant/components/modbus/__init__.py | 10 +- homeassistant/components/modbus/const.py | 5 + homeassistant/components/modbus/sensor.py | 100 +++++++++------- tests/components/modbus/test_modbus_sensor.py | 113 ++++++++++++++++++ 4 files changed, 185 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 2defb32393d0c..35abdca48fe15 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -78,6 +78,11 @@ CONF_STATUS_REGISTER_TYPE, CONF_STEP, CONF_STOPBITS, + CONF_SWAP, + CONF_SWAP_BYTE, + CONF_SWAP_NONE, + CONF_SWAP_WORD, + CONF_SWAP_WORD_BYTE, CONF_TARGET_TEMP, CONF_VERIFY_REGISTER, CONF_VERIFY_STATE, @@ -204,7 +209,10 @@ def number(value: Any) -> int | float: vol.Optional(CONF_INPUT_TYPE, default=CALL_TYPE_REGISTER_HOLDING): vol.In( [CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT] ), - vol.Optional(CONF_REVERSE_ORDER, default=False): cv.boolean, + vol.Optional(CONF_REVERSE_ORDER): cv.boolean, + vol.Optional(CONF_SWAP, default=CONF_SWAP_NONE): vol.In( + [CONF_SWAP_NONE, CONF_SWAP_BYTE, CONF_SWAP_WORD, CONF_SWAP_WORD_BYTE] + ), vol.Optional(CONF_SCALE, default=1): number, vol.Optional(CONF_STRUCTURE): cv.string, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, diff --git a/homeassistant/components/modbus/const.py b/homeassistant/components/modbus/const.py index ffe89757ef127..f5c7dced77d90 100644 --- a/homeassistant/components/modbus/const.py +++ b/homeassistant/components/modbus/const.py @@ -35,6 +35,11 @@ CONF_STATUS_REGISTER_TYPE = "status_register_type" CONF_STEP = "temp_step" CONF_STOPBITS = "stopbits" +CONF_SWAP = "swap" +CONF_SWAP_BYTE = "byte" +CONF_SWAP_NONE = "none" +CONF_SWAP_WORD = "word" +CONF_SWAP_WORD_BYTE = "word_byte" CONF_SWITCH = "switch" CONF_TARGET_TEMP = "target_temp_register" CONF_VERIFY_REGISTER = "verify_register" diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index c747d0a29d00f..81b54cb62e1d2 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -43,6 +43,11 @@ CONF_REGISTERS, CONF_REVERSE_ORDER, CONF_SCALE, + CONF_SWAP, + CONF_SWAP_BYTE, + CONF_SWAP_NONE, + CONF_SWAP_WORD, + CONF_SWAP_WORD_BYTE, DATA_TYPE_CUSTOM, DATA_TYPE_FLOAT, DATA_TYPE_INT, @@ -146,6 +151,28 @@ async def async_setup_platform( ) continue + if CONF_REVERSE_ORDER in entry: + if entry[CONF_REVERSE_ORDER]: + entry[CONF_SWAP] = CONF_SWAP_WORD + else: + entry[CONF_SWAP] = CONF_SWAP_NONE + del entry[CONF_REVERSE_ORDER] + if entry.get(CONF_SWAP) != CONF_SWAP_NONE: + if entry[CONF_SWAP] == CONF_SWAP_BYTE: + regs_needed = 1 + else: # CONF_SWAP_WORD_BYTE, CONF_SWAP_WORD + regs_needed = 2 + if ( + entry[CONF_COUNT] < regs_needed + or (entry[CONF_COUNT] % regs_needed) != 0 + ): + _LOGGER.error( + "Error in sensor %s swap(%s) not possible due to count: %d", + entry[CONF_NAME], + entry[CONF_SWAP], + entry[CONF_COUNT], + ) + continue if CONF_HUB in entry: # from old config! hub: ModbusHub = hass.data[MODBUS_DOMAIN][entry[CONF_HUB]] @@ -156,20 +183,8 @@ async def async_setup_platform( sensors.append( ModbusRegisterSensor( hub, - entry[CONF_NAME], - entry.get(CONF_SLAVE), - entry[CONF_ADDRESS], - entry[CONF_INPUT_TYPE], - entry.get(CONF_UNIT_OF_MEASUREMENT), - entry[CONF_COUNT], - entry[CONF_REVERSE_ORDER], - entry[CONF_SCALE], - entry[CONF_OFFSET], + entry, structure, - entry[CONF_PRECISION], - entry[CONF_DATA_TYPE], - entry.get(CONF_DEVICE_CLASS), - entry[CONF_SCAN_INTERVAL], ) ) @@ -184,39 +199,28 @@ class ModbusRegisterSensor(RestoreEntity, SensorEntity): def __init__( self, hub, - name, - slave, - register, - register_type, - unit_of_measurement, - count, - reverse_order, - scale, - offset, + entry, structure, - precision, - data_type, - device_class, - scan_interval, ): """Initialize the modbus register sensor.""" self._hub = hub - self._name = name + self._name = entry[CONF_NAME] + slave = entry.get(CONF_SLAVE) self._slave = int(slave) if slave else None - self._register = int(register) - self._register_type = register_type - self._unit_of_measurement = unit_of_measurement - self._count = int(count) - self._reverse_order = reverse_order - self._scale = scale - self._offset = offset - self._precision = precision + self._register = int(entry[CONF_ADDRESS]) + self._register_type = entry[CONF_INPUT_TYPE] + self._unit_of_measurement = entry.get(CONF_UNIT_OF_MEASUREMENT) + self._count = int(entry[CONF_COUNT]) + self._swap = entry[CONF_SWAP] + self._scale = entry[CONF_SCALE] + self._offset = entry[CONF_OFFSET] + self._precision = entry[CONF_PRECISION] self._structure = structure - self._data_type = data_type - self._device_class = device_class + self._data_type = entry[CONF_DATA_TYPE] + self._device_class = entry.get(CONF_DEVICE_CLASS) self._value = None self._available = True - self._scan_interval = timedelta(seconds=scan_interval) + self._scan_interval = timedelta(seconds=entry.get(CONF_SCAN_INTERVAL)) async def async_added_to_hass(self): """Handle entity which will be added.""" @@ -263,6 +267,21 @@ def available(self) -> bool: """Return True if entity is available.""" return self._available + def _swap_registers(self, registers): + """Do swap as needed.""" + if self._swap in [CONF_SWAP_BYTE, CONF_SWAP_WORD_BYTE]: + # convert [12][34] --> [21][43] + for i, register in enumerate(registers): + registers[i] = int.from_bytes( + register.to_bytes(2, byteorder="little"), + byteorder="big", + signed=False, + ) + if self._swap in [CONF_SWAP_WORD, CONF_SWAP_WORD_BYTE]: + # convert [12][34] ==> [34][12] + registers.reverse() + return registers + def _update(self): """Update the state of the sensor.""" if self._register_type == CALL_TYPE_REGISTER_INPUT: @@ -278,10 +297,7 @@ def _update(self): self.schedule_update_ha_state() return - registers = result.registers - if self._reverse_order: - registers.reverse() - + registers = self._swap_registers(result.registers) byte_string = b"".join([x.to_bytes(2, byteorder="big") for x in registers]) if self._data_type == DATA_TYPE_STRING: self._value = byte_string.decode() diff --git a/tests/components/modbus/test_modbus_sensor.py b/tests/components/modbus/test_modbus_sensor.py index fb8a00f8c07cb..b8ab10953c89d 100644 --- a/tests/components/modbus/test_modbus_sensor.py +++ b/tests/components/modbus/test_modbus_sensor.py @@ -1,4 +1,5 @@ """The tests for the Modbus sensor component.""" +import logging from unittest import mock import pytest @@ -14,6 +15,11 @@ CONF_REGISTERS, CONF_REVERSE_ORDER, CONF_SCALE, + CONF_SWAP, + CONF_SWAP_BYTE, + CONF_SWAP_NONE, + CONF_SWAP_WORD, + CONF_SWAP_WORD_BYTE, DATA_TYPE_CUSTOM, DATA_TYPE_FLOAT, DATA_TYPE_INT, @@ -112,6 +118,38 @@ CONF_DEVICE_CLASS: "battery", }, ), + ( + True, + { + CONF_ADDRESS: 51, + CONF_COUNT: 1, + CONF_SWAP: CONF_SWAP_NONE, + }, + ), + ( + True, + { + CONF_ADDRESS: 51, + CONF_COUNT: 1, + CONF_SWAP: CONF_SWAP_BYTE, + }, + ), + ( + True, + { + CONF_ADDRESS: 51, + CONF_COUNT: 2, + CONF_SWAP: CONF_SWAP_WORD, + }, + ), + ( + True, + { + CONF_ADDRESS: 51, + CONF_COUNT: 2, + CONF_SWAP: CONF_SWAP_WORD_BYTE, + }, + ), ], ) async def test_config_sensor(hass, do_discovery, do_config): @@ -408,6 +446,51 @@ async def test_config_wrong_struct_sensor(hass, do_config): None, STATE_UNAVAILABLE, ), + ( + { + CONF_COUNT: 1, + CONF_DATA_TYPE: DATA_TYPE_INT, + CONF_SWAP: CONF_SWAP_NONE, + }, + [0x0102], + str(int(0x0102)), + ), + ( + { + CONF_COUNT: 1, + CONF_DATA_TYPE: DATA_TYPE_INT, + CONF_SWAP: CONF_SWAP_BYTE, + }, + [0x0201], + str(int(0x0102)), + ), + ( + { + CONF_COUNT: 2, + CONF_DATA_TYPE: DATA_TYPE_INT, + CONF_SWAP: CONF_SWAP_BYTE, + }, + [0x0102, 0x0304], + str(int(0x02010403)), + ), + ( + { + CONF_COUNT: 2, + CONF_DATA_TYPE: DATA_TYPE_INT, + CONF_SWAP: CONF_SWAP_WORD, + }, + [0x0102, 0x0304], + str(int(0x03040102)), + ), + ( + { + CONF_COUNT: 2, + CONF_DATA_TYPE: DATA_TYPE_INT, + CONF_SWAP: CONF_SWAP_WORD_BYTE, + }, + [0x0102, 0x0304], + str(int(0x04030201)), + ), ], ) async def test_all_sensor(hass, cfg, regs, expected): @@ -508,3 +591,33 @@ async def test_restore_state_sensor(hass): ) entity_id = f"{SENSOR_DOMAIN}.{sensor_name}" assert hass.states.get(entity_id).state == test_value + + +@pytest.mark.parametrize( + "swap_type", + [CONF_SWAP_WORD, CONF_SWAP_WORD_BYTE], +) +async def test_swap_sensor_wrong_config(hass, caplog, swap_type): + """Run test for sensor swap.""" + sensor_name = "modbus_test_sensor" + config = { + CONF_NAME: sensor_name, + CONF_ADDRESS: 1234, + CONF_COUNT: 1, + CONF_SWAP: swap_type, + CONF_DATA_TYPE: DATA_TYPE_INT, + } + + caplog.set_level(logging.ERROR) + caplog.clear() + await base_config_test( + hass, + config, + sensor_name, + SENSOR_DOMAIN, + CONF_SENSORS, + None, + method_discovery=True, + expect_init_to_fail=True, + ) + assert caplog.messages[-1].startswith("Error in sensor " + sensor_name + " swap") From 0d410209d2af9f65df970bf9715067c1dc533934 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 27 Apr 2021 11:17:40 +0200 Subject: [PATCH 0559/1317] Add dispatch - odroid c2 (#49744) --- .github/workflows/builder.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index ab08962c64646..4727193509bfd 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -2,6 +2,7 @@ name: Build images # yamllint disable-line rule:truthy on: + workflow_dispatch: release: types: ["published"] schedule: @@ -133,6 +134,7 @@ jobs: machine: - generic-x86-64 - intel-nuc + - odroid-c2 - odroid-c4 - odroid-n2 - odroid-xu From b00ccf98f03803528393f6212cf8c6d2ee16f0ca Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 27 Apr 2021 11:19:21 +0200 Subject: [PATCH 0560/1317] TP Link: Don't report HS when in CT mode (#49704) * TP Link: Don't report HS when in CT mode * Update tests --- homeassistant/components/tplink/light.py | 2 +- tests/components/tplink/test_light.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index 0d9db7ba108ce..61123bd635315 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -354,7 +354,7 @@ def _light_state_from_params(self, light_state_params) -> LightState: ): color_temp = kelvin_to_mired(light_state_params[LIGHT_STATE_COLOR_TEMP]) - if light_features.supported_features & SUPPORT_COLOR: + if color_temp is None and light_features.supported_features & SUPPORT_COLOR: hue_saturation = ( light_state_params[LIGHT_STATE_HUE], light_state_params[LIGHT_STATE_SATURATION], diff --git a/tests/components/tplink/test_light.py b/tests/components/tplink/test_light.py index 5e81295468b6c..ea8809bc679d6 100644 --- a/tests/components/tplink/test_light.py +++ b/tests/components/tplink/test_light.py @@ -524,8 +524,8 @@ async def test_light(hass: HomeAssistant, light_mock_data: LightMockData) -> Non state = hass.states.get("light.light1") assert state.state == "on" assert state.attributes["brightness"] == 51 - assert state.attributes["hs_color"] == (110, 90) assert state.attributes["color_temp"] == 222 + assert "hs_color" not in state.attributes assert light_state["on_off"] == 1 await hass.services.async_call( @@ -541,6 +541,7 @@ async def test_light(hass: HomeAssistant, light_mock_data: LightMockData) -> Non assert state.state == "on" assert state.attributes["brightness"] == 56 assert state.attributes["hs_color"] == (23, 27) + assert "color_temp" not in state.attributes assert light_state["brightness"] == 22 assert light_state["hue"] == 23 assert light_state["saturation"] == 27 @@ -580,8 +581,8 @@ async def test_light(hass: HomeAssistant, light_mock_data: LightMockData) -> Non state = hass.states.get("light.light1") assert state.state == "on" assert state.attributes["brightness"] == 168 - assert state.attributes["hs_color"] == (77, 78) assert state.attributes["color_temp"] == 156 + assert "hs_color" not in state.attributes assert light_state["brightness"] == 66 assert light_state["hue"] == 77 assert light_state["saturation"] == 78 From 96e7ae94f81b49a156fac8e24d08006803c9c9f8 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 27 Apr 2021 11:20:10 +0200 Subject: [PATCH 0561/1317] Fix config entry reference for Home Assistant Cast user (#49729) * Fix config entry reference for Home Assistant Cast user * Simplify config_entry lookup --- homeassistant/components/cast/media_player.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index c5914e93cc7c6..0ca7f5f16821b 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -53,7 +53,7 @@ STATE_PLAYING, ) from homeassistant.core import HomeAssistant, callback -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.network import NoURLAvailableError, get_url import homeassistant.util.dt as dt_util @@ -463,8 +463,9 @@ async def async_play_media(self, media_type, media_id, **kwargs): # Create a signed path. if media_id[0] == "/": # Sign URL with Home Assistant Cast User - config_entries = self.hass.config_entries.async_entries(CAST_DOMAIN) - user_id = config_entries[0].data["user_id"] + config_entry_id = self.registry_entry.config_entry_id + config_entry = self.hass.config_entries.async_get_entry(config_entry_id) + user_id = config_entry.data["user_id"] user = await self.hass.auth.async_get_user(user_id) if user.refresh_tokens: refresh_token: RefreshToken = list(user.refresh_tokens.values())[0] From b28a868fd0dd4241f3bf3f144f34cc924053512d Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 27 Apr 2021 12:37:41 +0200 Subject: [PATCH 0562/1317] Fix arch command on build pipeline for machine (#49748) --- .github/workflows/builder.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 4727193509bfd..8b8719ef889c7 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -171,7 +171,6 @@ jobs: with: args: | $BUILD_ARGS \ - --${{ matrix.arch }} \ --target /data/machine \ --with-codenotary "${{ secrets.VCN_USER }}" "${{ secrets.VCN_PASSWORD }}" "${{ secrets.VCN_ORG }}" \ --validate-from "${{ secrets.VCN_ORG }}" \ From d2c989ed934f38c86f41125e36f07f232eb25d47 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 27 Apr 2021 12:41:31 +0200 Subject: [PATCH 0563/1317] Fix variable{1} on build pipeline (#49750) --- .github/workflows/builder.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 8b8719ef889c7..1362f3486d1bb 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -237,7 +237,7 @@ jobs: export DOCKER_CLI_EXPERIMENTAL=enabled function create_manifest() { - local docker_reg={1} + local docker_reg=${1} local tag_l=${2} local tag_r=${3} @@ -272,7 +272,7 @@ jobs: } function validate_image() { - local image={1} + local image=${1} state="$(vcn authenticate --org home-assistant.io --output json docker://${image} | jq '.verification.status // 2')" if [[ "${state}" != "0" ]]; then echo "Invalid signature!" From 238198e05ee5168461a6ce50deab926cc07e56b7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 27 Apr 2021 13:24:13 +0200 Subject: [PATCH 0564/1317] Update actions/setup-python requirement to v2.2.2 (#49742) Updates the requirements on [actions/setup-python](https://github.com/actions/setup-python) to permit the latest version. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/commits/dc73133d4da04e56a135ae2246682783cc7c7cb6) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/builder.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 1362f3486d1bb..904e70de1f858 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -28,7 +28,7 @@ jobs: fetch-depth: 0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.2.1 + uses: actions/setup-python@v2.2.2 with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -57,7 +57,7 @@ jobs: uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.2.1 + uses: actions/setup-python@v2.2.2 with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -88,7 +88,7 @@ jobs: - name: Set up Python ${{ env.DEFAULT_PYTHON }} if: needs.init.outputs.channel == 'dev' - uses: actions/setup-python@v2.2.1 + uses: actions/setup-python@v2.2.2 with: python-version: ${{ env.DEFAULT_PYTHON }} From 4b8e1335bc21589d741529bdbca043ec9584ff6b Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 27 Apr 2021 13:45:58 +0200 Subject: [PATCH 0565/1317] Fix " on build pipeline (#49756) --- .github/workflows/builder.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 904e70de1f858..f34b3a6467755 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -174,7 +174,7 @@ jobs: --target /data/machine \ --with-codenotary "${{ secrets.VCN_USER }}" "${{ secrets.VCN_PASSWORD }}" "${{ secrets.VCN_ORG }}" \ --validate-from "${{ secrets.VCN_ORG }}" \ - --machine "${{ needs.init.outputs.version }}=${{ matrix.machine }}"" + --machine "${{ needs.init.outputs.version }}=${{ matrix.machine }}" publish_ha: name: Publish version files From b5fdc05f5f0660b34f653fda4ea62c0e7606de09 Mon Sep 17 00:00:00 2001 From: Vincent Le Bourlot Date: Tue, 27 Apr 2021 13:47:20 +0200 Subject: [PATCH 0566/1317] Fix neato possible None state when creating entity (#49746) --- homeassistant/components/neato/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/neato/sensor.py b/homeassistant/components/neato/sensor.py index 83add4ff3f7de..98208698037e1 100644 --- a/homeassistant/components/neato/sensor.py +++ b/homeassistant/components/neato/sensor.py @@ -80,7 +80,7 @@ def available(self): @property def state(self): """Return the state.""" - return self._state["details"]["charge"] + return self._state["details"]["charge"] if self._state else None @property def unit_of_measurement(self): From ff57a5bd7dc70f5378b2a3122410225bffb490ae Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Tue, 27 Apr 2021 13:52:13 +0200 Subject: [PATCH 0567/1317] Manifest cleanup (#49745) * Remove empty homekit dict in guardian manifest * Clean up srp_energy manifest --- homeassistant/components/guardian/manifest.json | 1 - homeassistant/components/srp_energy/manifest.json | 4 ---- 2 files changed, 5 deletions(-) diff --git a/homeassistant/components/guardian/manifest.json b/homeassistant/components/guardian/manifest.json index 4bc889f4ab053..28f46a9bf1484 100644 --- a/homeassistant/components/guardian/manifest.json +++ b/homeassistant/components/guardian/manifest.json @@ -5,7 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/guardian", "requirements": ["aioguardian==1.0.4"], "zeroconf": ["_api._udp.local."], - "homekit": {}, "codeowners": ["@bachya"], "iot_class": "local_polling" } diff --git a/homeassistant/components/srp_energy/manifest.json b/homeassistant/components/srp_energy/manifest.json index eb9aa7d12c404..73aac879a0027 100644 --- a/homeassistant/components/srp_energy/manifest.json +++ b/homeassistant/components/srp_energy/manifest.json @@ -4,10 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/srp_energy", "requirements": ["srpenergy==1.3.2"], - "ssdp": [], - "zeroconf": [], - "homekit": {}, - "dependencies": [], "codeowners": ["@briglx"], "iot_class": "cloud_polling" } From f6be95eb4cf44e27e2a4738c7fb44c6a2cf657a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 27 Apr 2021 15:04:47 +0200 Subject: [PATCH 0568/1317] Use machine in name for machine build (#49761) --- .github/workflows/builder.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index f34b3a6467755..32ea439b830ba 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -126,7 +126,7 @@ jobs: --generic ${{ needs.init.outputs.version }} build_machine: - name: Build ${{ matrix.arch }} machine core image + name: Build ${{ matrix.machine }} machine core image needs: ["init", "build_base"] runs-on: ubuntu-latest strategy: From 6bc0fb2e4234dbd6ef7403a7b8ac83c6c57d9eae Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Tue, 27 Apr 2021 10:02:16 -0400 Subject: [PATCH 0569/1317] Bump ZHA quirks library (#49757) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 3e99f971e8885..e64dee8d0a256 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -7,7 +7,7 @@ "bellows==0.23.1", "pyserial==3.5", "pyserial-asyncio==0.5", - "zha-quirks==0.0.56", + "zha-quirks==0.0.57", "zigpy-cc==0.5.2", "zigpy-deconz==0.12.0", "zigpy==0.33.0", diff --git a/requirements_all.txt b/requirements_all.txt index f3e9a2c0e88d6..660a2c474cfea 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2391,7 +2391,7 @@ zengge==0.2 zeroconf==0.29.0 # homeassistant.components.zha -zha-quirks==0.0.56 +zha-quirks==0.0.57 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4c24079645a66..8fffd4c0176a0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1267,7 +1267,7 @@ zeep[async]==4.0.0 zeroconf==0.29.0 # homeassistant.components.zha -zha-quirks==0.0.56 +zha-quirks==0.0.57 # homeassistant.components.zha zigpy-cc==0.5.2 From b91d2be00bbaf29d888e5dce9e53ba5bb85e136b Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Tue, 27 Apr 2021 10:04:22 -0400 Subject: [PATCH 0570/1317] Better ZHA device reconfiguration (#49672) * initial take * cleanup * fix mock for configure_reporting --- homeassistant/components/zha/api.py | 18 +----- .../components/zha/core/channels/__init__.py | 7 +++ .../components/zha/core/channels/base.py | 56 +++++++++++++++++++ homeassistant/components/zha/core/const.py | 5 ++ tests/components/zha/common.py | 6 +- 5 files changed, 76 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index b5b29534ed903..aedc32ac94b4c 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -54,6 +54,7 @@ WARNING_DEVICE_SQUAWK_MODE_ARMED, WARNING_DEVICE_STROBE_HIGH, WARNING_DEVICE_STROBE_YES, + ZHA_CHANNEL_MSG, ZHA_CONFIG_SCHEMAS, ) from .core.group import GroupMember @@ -468,34 +469,21 @@ async def websocket_reconfigure_node(hass, connection, msg): zha_gateway: ZhaGatewayType = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] ieee = msg[ATTR_IEEE] device: ZhaDeviceType = zha_gateway.get_device(ieee) - ieee_str = str(device.ieee) - nwk_str = device.nwk.__repr__() - - class DeviceLogFilterer(logging.Filter): - """Log filterer that limits messages to the specified device.""" - - def filter(self, record): - message = record.getMessage() - return nwk_str in message or ieee_str in message - - filterer = DeviceLogFilterer() async def forward_messages(data): """Forward events to websocket.""" connection.send_message(websocket_api.event_message(msg["id"], data)) remove_dispatcher_function = async_dispatcher_connect( - hass, "zha_gateway_message", forward_messages + hass, ZHA_CHANNEL_MSG, forward_messages ) @callback def async_cleanup() -> None: - """Remove signal listener and turn off debug mode.""" - zha_gateway.async_disable_debug_mode(filterer=filterer) + """Remove signal listener.""" remove_dispatcher_function() connection.subscriptions[msg["id"]] = async_cleanup - zha_gateway.async_enable_debug_mode(filterer=filterer) _LOGGER.debug("Reconfiguring node with ieee_address: %s", ieee) hass.async_create_task(device.async_configure()) diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py index 289f1c36d4df1..e6d2d722f615f 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -130,6 +130,13 @@ async def async_configure(self) -> None: await self.zdo_channel.async_configure() self.zdo_channel.debug("'async_configure' stage succeeded") await asyncio.gather(*(pool.async_configure() for pool in self.pools)) + async_dispatcher_send( + self.zha_device.hass, + const.ZHA_CHANNEL_MSG, + { + const.ATTR_TYPE: const.ZHA_CHANNEL_CFG_DONE, + }, + ) @callback def async_new_entity( diff --git a/homeassistant/components/zha/core/channels/base.py b/homeassistant/components/zha/core/channels/base.py index 4238707656df4..4d1e71e884ea2 100644 --- a/homeassistant/components/zha/core/channels/base.py +++ b/homeassistant/components/zha/core/channels/base.py @@ -11,6 +11,7 @@ from homeassistant.const import ATTR_COMMAND from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_send from .. import typing as zha_typing from ..const import ( @@ -18,10 +19,15 @@ ATTR_ATTRIBUTE_ID, ATTR_ATTRIBUTE_NAME, ATTR_CLUSTER_ID, + ATTR_TYPE, ATTR_UNIQUE_ID, ATTR_VALUE, CHANNEL_ZDO, SIGNAL_ATTR_UPDATED, + ZHA_CHANNEL_MSG, + ZHA_CHANNEL_MSG_BIND, + ZHA_CHANNEL_MSG_CFG_RPT, + ZHA_CHANNEL_MSG_DATA, ) from ..helpers import LogMixin, safe_read @@ -148,10 +154,34 @@ async def bind(self): try: res = await self.cluster.bind() self.debug("bound '%s' cluster: %s", self.cluster.ep_attribute, res[0]) + async_dispatcher_send( + self._ch_pool.hass, + ZHA_CHANNEL_MSG, + { + ATTR_TYPE: ZHA_CHANNEL_MSG_BIND, + ZHA_CHANNEL_MSG_DATA: { + "cluster_name": self.cluster.name, + "cluster_id": self.cluster.cluster_id, + "success": res[0] == 0, + }, + }, + ) except (zigpy.exceptions.ZigbeeException, asyncio.TimeoutError) as ex: self.debug( "Failed to bind '%s' cluster: %s", self.cluster.ep_attribute, str(ex) ) + async_dispatcher_send( + self._ch_pool.hass, + ZHA_CHANNEL_MSG, + { + ATTR_TYPE: ZHA_CHANNEL_MSG_BIND, + ZHA_CHANNEL_MSG_DATA: { + "cluster_name": self.cluster.name, + "cluster_id": self.cluster.cluster_id, + "success": False, + }, + }, + ) async def configure_reporting(self) -> None: """Configure attribute reporting for a cluster. @@ -159,6 +189,7 @@ async def configure_reporting(self) -> None: This also swallows ZigbeeException exceptions that are thrown when devices are unreachable. """ + event_data = {} kwargs = {} if self.cluster.cluster_id >= 0xFC00 and self._ch_pool.manufacturer_code: kwargs["manufacturer"] = self._ch_pool.manufacturer_code @@ -167,6 +198,14 @@ async def configure_reporting(self) -> None: attr = report["attr"] attr_name = self.cluster.attributes.get(attr, [attr])[0] min_report_int, max_report_int, reportable_change = report["config"] + event_data[attr_name] = { + "min": min_report_int, + "max": max_report_int, + "id": attr, + "name": attr_name, + "change": reportable_change, + } + try: res = await self.cluster.configure_reporting( attr, min_report_int, max_report_int, reportable_change, **kwargs @@ -180,6 +219,9 @@ async def configure_reporting(self) -> None: reportable_change, res, ) + event_data[attr_name]["success"] = ( + res[0][0].status == 0 or res[0][0].status == 134 + ) except (zigpy.exceptions.ZigbeeException, asyncio.TimeoutError) as ex: self.debug( "failed to set reporting for '%s' attr on '%s' cluster: %s", @@ -187,6 +229,20 @@ async def configure_reporting(self) -> None: self.cluster.ep_attribute, str(ex), ) + event_data[attr_name]["success"] = False + + async_dispatcher_send( + self._ch_pool.hass, + ZHA_CHANNEL_MSG, + { + ATTR_TYPE: ZHA_CHANNEL_MSG_CFG_RPT, + ZHA_CHANNEL_MSG_DATA: { + "cluster_name": self.cluster.name, + "cluster_id": self.cluster.cluster_id, + "attributes": event_data, + }, + }, + ) async def async_configure(self) -> None: """Set cluster binding and attribute reporting.""" diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index 2576aa9f463ee..7df850909f48d 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -339,6 +339,11 @@ def description(self) -> str: WARNING_DEVICE_SQUAWK_MODE_DISARMED = 1 ZHA_DISCOVERY_NEW = "zha_discovery_new_{}" +ZHA_CHANNEL_MSG = "zha_channel_message" +ZHA_CHANNEL_MSG_BIND = "zha_channel_bind" +ZHA_CHANNEL_MSG_CFG_RPT = "zha_channel_configure_reporting" +ZHA_CHANNEL_MSG_DATA = "zha_channel_msg_data" +ZHA_CHANNEL_CFG_DONE = "zha_channel_cfg_done" ZHA_GW_MSG = "zha_gateway_message" ZHA_GW_MSG_DEVICE_FULL_INIT = "device_fully_initialized" ZHA_GW_MSG_DEVICE_INFO = "device_info" diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index eeffa3fb91122..45caed95ae6a0 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -93,7 +93,11 @@ async def _read_attribute_raw(attributes, *args, **kwargs): return (result,) cluster.bind = AsyncMock(return_value=[0]) - cluster.configure_reporting = AsyncMock(return_value=[0]) + cluster.configure_reporting = AsyncMock( + return_value=[ + [zcl_f.ConfigureReportingResponseRecord(zcl_f.Status.SUCCESS, 0x00, 0xAABB)] + ] + ) cluster.deserialize = Mock() cluster.handle_cluster_request = Mock() cluster.read_attributes = AsyncMock(wraps=cluster.read_attributes) From 664075962f6e4efe0e8bcfff3571ada0f5f4051d Mon Sep 17 00:00:00 2001 From: tkdrob Date: Tue, 27 Apr 2021 10:04:45 -0400 Subject: [PATCH 0571/1317] Clean up profiler constants (#49752) --- homeassistant/components/profiler/__init__.py | 4 +--- tests/components/profiler/test_init.py | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/profiler/__init__.py b/homeassistant/components/profiler/__init__.py index e6aa2ce557da2..e6bc68ba91895 100644 --- a/homeassistant/components/profiler/__init__.py +++ b/homeassistant/components/profiler/__init__.py @@ -15,6 +15,7 @@ import voluptuous as vol from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_SCAN_INTERVAL, CONF_TYPE from homeassistant.core import HomeAssistant, ServiceCall import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_time_interval @@ -44,8 +45,6 @@ DEFAULT_SCAN_INTERVAL = timedelta(seconds=30) CONF_SECONDS = "seconds" -CONF_SCAN_INTERVAL = "scan_interval" -CONF_TYPE = "type" LOG_INTERVAL_SUB = "log_interval_subscription" @@ -54,7 +53,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Profiler from a config entry.""" - lock = asyncio.Lock() domain_data = hass.data[DOMAIN] = {} diff --git a/tests/components/profiler/test_init.py b/tests/components/profiler/test_init.py index be376ea8aed98..809a6164ce286 100644 --- a/tests/components/profiler/test_init.py +++ b/tests/components/profiler/test_init.py @@ -5,9 +5,7 @@ from homeassistant import setup from homeassistant.components.profiler import ( - CONF_SCAN_INTERVAL, CONF_SECONDS, - CONF_TYPE, SERVICE_DUMP_LOG_OBJECTS, SERVICE_LOG_EVENT_LOOP_SCHEDULED, SERVICE_LOG_THREAD_FRAMES, @@ -17,6 +15,7 @@ SERVICE_STOP_LOG_OBJECTS, ) from homeassistant.components.profiler.const import DOMAIN +from homeassistant.const import CONF_SCAN_INTERVAL, CONF_TYPE import homeassistant.util.dt as dt_util from tests.common import MockConfigEntry, async_fire_time_changed From 978d706b089d38443099337d37c3fc33fa8eb9e0 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Tue, 27 Apr 2021 10:05:03 -0400 Subject: [PATCH 0572/1317] Clean up deconz constants (#49754) --- homeassistant/components/deconz/const.py | 4 +--- homeassistant/components/deconz/deconz_event.py | 3 ++- homeassistant/components/deconz/lock.py | 12 +++++++++--- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index fb4e497587d61..799fc221e2c54 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -64,8 +64,7 @@ FANS = ["Fan"] # Locks -LOCKS = ["Door Lock", "ZHADoorLock"] -LOCK_TYPES = LOCKS +LOCK_TYPES = ["Door Lock", "ZHADoorLock"] # Switches POWER_PLUGS = ["On/Off light", "On/Off plug-in unit", "Smart plug"] @@ -74,4 +73,3 @@ CONF_ANGLE = "angle" CONF_GESTURE = "gesture" -CONF_XY = "xy" diff --git a/homeassistant/components/deconz/deconz_event.py b/homeassistant/components/deconz/deconz_event.py index 11dbfa89c908c..afb9dd7fc79f4 100644 --- a/homeassistant/components/deconz/deconz_event.py +++ b/homeassistant/components/deconz/deconz_event.py @@ -15,6 +15,7 @@ CONF_EVENT, CONF_ID, CONF_UNIQUE_ID, + CONF_XY, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, @@ -25,7 +26,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util import slugify -from .const import CONF_ANGLE, CONF_GESTURE, CONF_XY, LOGGER, NEW_SENSOR +from .const import CONF_ANGLE, CONF_GESTURE, LOGGER, NEW_SENSOR from .deconz_device import DeconzBase CONF_DECONZ_EVENT = "deconz_event" diff --git a/homeassistant/components/deconz/lock.py b/homeassistant/components/deconz/lock.py index 6daa6cd153761..75f6bc872dbc6 100644 --- a/homeassistant/components/deconz/lock.py +++ b/homeassistant/components/deconz/lock.py @@ -3,7 +3,7 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import LOCKS, NEW_LIGHT, NEW_SENSOR +from .const import LOCK_TYPES, NEW_LIGHT, NEW_SENSOR from .deconz_device import DeconzDevice from .gateway import get_gateway_from_config_entry @@ -20,7 +20,10 @@ def async_add_lock_from_light(lights=gateway.api.lights.values()): for light in lights: - if light.type in LOCKS and light.uniqueid not in gateway.entities[DOMAIN]: + if ( + light.type in LOCK_TYPES + and light.uniqueid not in gateway.entities[DOMAIN] + ): entities.append(DeconzLock(light, gateway)) if entities: @@ -39,7 +42,10 @@ def async_add_lock_from_sensor(sensors=gateway.api.sensors.values()): for sensor in sensors: - if sensor.type in LOCKS and sensor.uniqueid not in gateway.entities[DOMAIN]: + if ( + sensor.type in LOCK_TYPES + and sensor.uniqueid not in gateway.entities[DOMAIN] + ): entities.append(DeconzLock(sensor, gateway)) if entities: From 157dd273dab7aaa0d0d7cc2f2476a8202593cc1b Mon Sep 17 00:00:00 2001 From: tkdrob Date: Tue, 27 Apr 2021 10:05:14 -0400 Subject: [PATCH 0573/1317] Use core constants for openalpr_cloud (#49755) --- homeassistant/components/openalpr_cloud/image_processing.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/openalpr_cloud/image_processing.py b/homeassistant/components/openalpr_cloud/image_processing.py index e8ae2d240290a..bc33832bba13f 100644 --- a/homeassistant/components/openalpr_cloud/image_processing.py +++ b/homeassistant/components/openalpr_cloud/image_processing.py @@ -17,7 +17,7 @@ from homeassistant.components.openalpr_local.image_processing import ( ImageProcessingAlprEntity, ) -from homeassistant.const import CONF_API_KEY, HTTP_OK +from homeassistant.const import CONF_API_KEY, CONF_REGION, HTTP_OK from homeassistant.core import split_entity_id from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -41,8 +41,6 @@ "vn2", ] -CONF_REGION = "region" - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, From a1fdf84dbac9db3e0c775affaaa65c87b3ec5757 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 27 Apr 2021 04:09:59 -1000 Subject: [PATCH 0574/1317] Reduce config entry setup/unload boilerplate G-J (#49737) --- .../components/garmin_connect/__init__.py | 16 ++-------- homeassistant/components/gdacs/__init__.py | 20 +++---------- homeassistant/components/geofency/__init__.py | 9 +++--- .../components/geonetnz_quakes/__init__.py | 20 +++---------- .../components/geonetnz_volcano/__init__.py | 18 ++++------- .../components/geonetnz_volcano/const.py | 2 ++ homeassistant/components/gios/__init__.py | 20 ++++++------- homeassistant/components/glances/__init__.py | 22 +++++++------- homeassistant/components/goalzero/__init__.py | 15 ++-------- .../components/gogogate2/__init__.py | 29 +++++------------- .../components/google_travel_time/__init__.py | 18 ++--------- .../components/gpslogger/__init__.py | 10 +++---- homeassistant/components/gree/__init__.py | 22 +++----------- homeassistant/components/guardian/__init__.py | 14 ++------- homeassistant/components/habitica/__init__.py | 25 +++++----------- homeassistant/components/harmony/__init__.py | 14 ++------- homeassistant/components/hassio/__init__.py | 21 ++++--------- homeassistant/components/heos/__init__.py | 11 ++++--- .../components/hisense_aehw4a1/__init__.py | 9 +++--- homeassistant/components/hive/__init__.py | 11 +------ homeassistant/components/hlk_sw16/__init__.py | 9 +++--- .../components/home_connect/__init__.py | 15 ++-------- .../components/home_plus_control/__init__.py | 9 ++---- .../homekit_controller/connection.py | 14 ++------- .../components/homematicip_cloud/hap.py | 15 ++++------ .../components/huawei_lte/__init__.py | 11 ++++--- homeassistant/components/hue/bridge.py | 30 ++++--------------- .../components/huisbaasje/__init__.py | 29 +++++++----------- .../hunterdouglas_powerview/__init__.py | 16 ++-------- .../components/hvv_departures/__init__.py | 16 ++-------- homeassistant/components/hyperion/__init__.py | 9 ++---- homeassistant/components/ialarm/__init__.py | 10 ++----- .../components/iaqualink/__init__.py | 27 ++++++++--------- homeassistant/components/icloud/__init__.py | 16 ++-------- homeassistant/components/ipma/__init__.py | 13 ++++---- homeassistant/components/ipp/__init__.py | 17 ++--------- homeassistant/components/iqvia/__init__.py | 16 ++-------- .../islamic_prayer_times/__init__.py | 11 ++----- homeassistant/components/isy994/__init__.py | 15 ++-------- homeassistant/components/izone/__init__.py | 10 +++---- homeassistant/components/juicenet/__init__.py | 16 ++-------- .../components/homematicip_cloud/test_hap.py | 10 +------ .../components/huisbaasje/test_config_flow.py | 5 ---- 43 files changed, 172 insertions(+), 493 deletions(-) diff --git a/homeassistant/components/garmin_connect/__init__.py b/homeassistant/components/garmin_connect/__init__.py index f816196aa290b..4ac157707fc45 100644 --- a/homeassistant/components/garmin_connect/__init__.py +++ b/homeassistant/components/garmin_connect/__init__.py @@ -1,5 +1,4 @@ """The Garmin Connect integration.""" -import asyncio from datetime import date, timedelta import logging @@ -52,27 +51,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = garmin_data - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok diff --git a/homeassistant/components/gdacs/__init__.py b/homeassistant/components/gdacs/__init__.py index 8144b7667ca6c..b637d59b66c95 100644 --- a/homeassistant/components/gdacs/__init__.py +++ b/homeassistant/components/gdacs/__init__.py @@ -1,5 +1,4 @@ """The Global Disaster Alert and Coordination System (GDACS) integration.""" -import asyncio from datetime import timedelta import logging @@ -97,17 +96,11 @@ async def async_setup_entry(hass, config_entry): return True -async def async_unload_entry(hass, config_entry): +async def async_unload_entry(hass, entry): """Unload an GDACS component config entry.""" - manager = hass.data[DOMAIN][FEED].pop(config_entry.entry_id) + manager = hass.data[DOMAIN][FEED].pop(entry.entry_id) await manager.async_stop() - await asyncio.wait( - [ - hass.config_entries.async_forward_entry_unload(config_entry, domain) - for domain in PLATFORMS - ] - ) - return True + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) class GdacsFeedEntityManager: @@ -142,12 +135,7 @@ def __init__(self, hass, config_entry, radius_in_km): async def async_init(self): """Schedule initial and regular updates based on configured time interval.""" - for domain in PLATFORMS: - self._hass.async_create_task( - self._hass.config_entries.async_forward_entry_setup( - self._config_entry, domain - ) - ) + self._hass.config_entries.async_setup_platforms(self._config_entry, PLATFORMS) async def update(event_time): """Update.""" diff --git a/homeassistant/components/geofency/__init__.py b/homeassistant/components/geofency/__init__.py index e0a3dc47818fb..1cbaea2373371 100644 --- a/homeassistant/components/geofency/__init__.py +++ b/homeassistant/components/geofency/__init__.py @@ -19,6 +19,8 @@ from .const import DOMAIN +PLATFORMS = [DEVICE_TRACKER] + CONF_MOBILE_BEACONS = "mobile_beacons" CONFIG_SCHEMA = vol.Schema( @@ -136,9 +138,7 @@ async def async_setup_entry(hass, entry): DOMAIN, "Geofency", entry.data[CONF_WEBHOOK_ID], handle_webhook ) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, DEVICE_TRACKER) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -146,8 +146,7 @@ async def async_unload_entry(hass, entry): """Unload a config entry.""" hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID]) hass.data[DOMAIN]["unsub_device_tracker"].pop(entry.entry_id)() - await hass.config_entries.async_forward_entry_unload(entry, DEVICE_TRACKER) - return True + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) async_remove_entry = config_entry_flow.webhook_async_remove_entry diff --git a/homeassistant/components/geonetnz_quakes/__init__.py b/homeassistant/components/geonetnz_quakes/__init__.py index a41fe350a11a0..23b08103a681b 100644 --- a/homeassistant/components/geonetnz_quakes/__init__.py +++ b/homeassistant/components/geonetnz_quakes/__init__.py @@ -1,5 +1,4 @@ """The GeoNet NZ Quakes integration.""" -import asyncio from datetime import timedelta import logging @@ -104,17 +103,11 @@ async def async_setup_entry(hass, config_entry): return True -async def async_unload_entry(hass, config_entry): +async def async_unload_entry(hass, entry): """Unload an GeoNet NZ Quakes component config entry.""" - manager = hass.data[DOMAIN][FEED].pop(config_entry.entry_id) + manager = hass.data[DOMAIN][FEED].pop(entry.entry_id) await manager.async_stop() - await asyncio.wait( - [ - hass.config_entries.async_forward_entry_unload(config_entry, domain) - for domain in PLATFORMS - ] - ) - return True + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) class GeonetnzQuakesFeedEntityManager: @@ -150,12 +143,7 @@ def __init__(self, hass, config_entry, radius_in_km): async def async_init(self): """Schedule initial and regular updates based on configured time interval.""" - for domain in PLATFORMS: - self._hass.async_create_task( - self._hass.config_entries.async_forward_entry_setup( - self._config_entry, domain - ) - ) + self._hass.config_entries.async_setup_platforms(self._config_entry, PLATFORMS) async def update(event_time): """Update.""" diff --git a/homeassistant/components/geonetnz_volcano/__init__.py b/homeassistant/components/geonetnz_volcano/__init__.py index c3db7770499f9..dee87e54437c4 100644 --- a/homeassistant/components/geonetnz_volcano/__init__.py +++ b/homeassistant/components/geonetnz_volcano/__init__.py @@ -1,7 +1,6 @@ """The GeoNet NZ Volcano integration.""" from __future__ import annotations -import asyncio from datetime import datetime, timedelta import logging @@ -25,7 +24,7 @@ from homeassistant.util.unit_system import METRIC_SYSTEM from .config_flow import configured_instances -from .const import DEFAULT_RADIUS, DEFAULT_SCAN_INTERVAL, DOMAIN, FEED +from .const import DEFAULT_RADIUS, DEFAULT_SCAN_INTERVAL, DOMAIN, FEED, PLATFORMS _LOGGER = logging.getLogger(__name__) @@ -94,14 +93,11 @@ async def async_setup_entry(hass, config_entry): return True -async def async_unload_entry(hass, config_entry): +async def async_unload_entry(hass, entry): """Unload an GeoNet NZ Volcano component config entry.""" - manager = hass.data[DOMAIN][FEED].pop(config_entry.entry_id) + manager = hass.data[DOMAIN][FEED].pop(entry.entry_id) await manager.async_stop() - await asyncio.wait( - [hass.config_entries.async_forward_entry_unload(config_entry, "sensor")] - ) - return True + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) class GeonetnzVolcanoFeedEntityManager: @@ -133,11 +129,7 @@ def __init__(self, hass, config_entry, radius_in_km, unit_system): async def async_init(self): """Schedule initial and regular updates based on configured time interval.""" - self._hass.async_create_task( - self._hass.config_entries.async_forward_entry_setup( - self._config_entry, "sensor" - ) - ) + self._hass.config_entries.async_setup_platforms(self._config_entry, PLATFORMS) async def update(event_time): """Update.""" diff --git a/homeassistant/components/geonetnz_volcano/const.py b/homeassistant/components/geonetnz_volcano/const.py index d48e9775f1959..b70d224a685b5 100644 --- a/homeassistant/components/geonetnz_volcano/const.py +++ b/homeassistant/components/geonetnz_volcano/const.py @@ -14,3 +14,5 @@ DEFAULT_ICON = "mdi:image-filter-hdr" DEFAULT_RADIUS = 50.0 DEFAULT_SCAN_INTERVAL = timedelta(minutes=5) + +PLATFORMS = ["sensor"] diff --git a/homeassistant/components/gios/__init__.py b/homeassistant/components/gios/__init__.py index f25f7e76f59ef..90e12061da323 100644 --- a/homeassistant/components/gios/__init__.py +++ b/homeassistant/components/gios/__init__.py @@ -12,10 +12,12 @@ _LOGGER = logging.getLogger(__name__) +PLATFORMS = ["air_quality"] -async def async_setup_entry(hass, config_entry): + +async def async_setup_entry(hass, entry): """Set up GIOS as config entry.""" - station_id = config_entry.data[CONF_STATION_ID] + station_id = entry.data[CONF_STATION_ID] _LOGGER.debug("Using station_id: %s", station_id) websession = async_get_clientsession(hass) @@ -24,19 +26,17 @@ async def async_setup_entry(hass, config_entry): await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][config_entry.entry_id] = coordinator + hass.data[DOMAIN][entry.entry_id] = coordinator + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, "air_quality") - ) return True -async def async_unload_entry(hass, config_entry): +async def async_unload_entry(hass, entry): """Unload a config entry.""" - hass.data[DOMAIN].pop(config_entry.entry_id) - await hass.config_entries.async_forward_entry_unload(config_entry, "air_quality") - return True + hass.data[DOMAIN].pop(entry.entry_id) + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) class GiosDataUpdateCoordinator(DataUpdateCoordinator): diff --git a/homeassistant/components/glances/__init__.py b/homeassistant/components/glances/__init__.py index 5a0a1f3339456..0ccf8509cdd2d 100644 --- a/homeassistant/components/glances/__init__.py +++ b/homeassistant/components/glances/__init__.py @@ -36,6 +36,8 @@ _LOGGER = logging.getLogger(__name__) +PLATFORMS = ["sensor"] + GLANCES_SCHEMA = vol.All( vol.Schema( { @@ -79,11 +81,12 @@ async def async_setup_entry(hass, config_entry): return True -async def async_unload_entry(hass, config_entry): +async def async_unload_entry(hass, entry): """Unload a config entry.""" - await hass.config_entries.async_forward_entry_unload(config_entry, "sensor") - hass.data[DOMAIN].pop(config_entry.entry_id) - return True + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + return unload_ok class GlancesData: @@ -127,13 +130,12 @@ async def async_setup(self): self.add_options() self.set_scan_interval(self.config_entry.options[CONF_SCAN_INTERVAL]) - self.config_entry.add_update_listener(self.async_options_updated) - - self.hass.async_create_task( - self.hass.config_entries.async_forward_entry_setup( - self.config_entry, "sensor" - ) + self.config_entry.async_on_unload( + self.config_entry.add_update_listener(self.async_options_updated) ) + + self.hass.config_entries.async_setup_platforms(self.config_entry, PLATFORMS) + return True def add_options(self): diff --git a/homeassistant/components/goalzero/__init__.py b/homeassistant/components/goalzero/__init__.py index e2e8bd5981cc7..34e57eeeac95c 100644 --- a/homeassistant/components/goalzero/__init__.py +++ b/homeassistant/components/goalzero/__init__.py @@ -1,5 +1,4 @@ """The Goal Zero Yeti integration.""" -import asyncio import logging from goalzero import Yeti, exceptions @@ -56,24 +55,14 @@ async def async_update_data(): DATA_KEY_COORDINATOR: coordinator, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok diff --git a/homeassistant/components/gogogate2/__init__.py b/homeassistant/components/gogogate2/__init__.py index 4c9e646c54d25..d4271b3937a35 100644 --- a/homeassistant/components/gogogate2/__init__.py +++ b/homeassistant/components/gogogate2/__init__.py @@ -1,5 +1,4 @@ """The gogogate2 component.""" -import asyncio from homeassistant.components.cover import DOMAIN as COVER from homeassistant.components.sensor import DOMAIN as SENSOR @@ -13,40 +12,28 @@ PLATFORMS = [COVER, SENSOR] -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Do setup of Gogogate2.""" # Update the config entry. config_updates = {} - if CONF_DEVICE not in config_entry.data: + if CONF_DEVICE not in entry.data: config_updates["data"] = { - **config_entry.data, + **entry.data, **{CONF_DEVICE: DEVICE_TYPE_GOGOGATE2}, } if config_updates: - hass.config_entries.async_update_entry(config_entry, **config_updates) + hass.config_entries.async_update_entry(entry, **config_updates) - data_update_coordinator = get_data_update_coordinator(hass, config_entry) + data_update_coordinator = get_data_update_coordinator(hass, entry) await data_update_coordinator.async_config_entry_first_refresh() - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload Gogogate2 config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - ) - ) - - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/google_travel_time/__init__.py b/homeassistant/components/google_travel_time/__init__.py index ef53db9c815c8..5d4b3d1b74a24 100644 --- a/homeassistant/components/google_travel_time/__init__.py +++ b/homeassistant/components/google_travel_time/__init__.py @@ -1,5 +1,4 @@ """The google_travel_time component.""" -import asyncio from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -9,23 +8,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Google Maps Travel Time from a config entry.""" - for component in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) - ) - + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS - ] - ) - ) - - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/gpslogger/__init__.py b/homeassistant/components/gpslogger/__init__.py index d230d3dedc584..0ec8e6588673f 100644 --- a/homeassistant/components/gpslogger/__init__.py +++ b/homeassistant/components/gpslogger/__init__.py @@ -28,6 +28,8 @@ DOMAIN, ) +PLATFORMS = [DEVICE_TRACKER] + TRACKER_UPDATE = f"{DOMAIN}_tracker_update" @@ -98,9 +100,8 @@ async def async_setup_entry(hass, entry): DOMAIN, "GPSLogger", entry.data[CONF_WEBHOOK_ID], handle_webhook ) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, DEVICE_TRACKER) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True @@ -108,8 +109,7 @@ async def async_unload_entry(hass, entry): """Unload a config entry.""" hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID]) hass.data[DOMAIN]["unsub_device_tracker"].pop(entry.entry_id)() - await hass.config_entries.async_forward_entry_unload(entry, DEVICE_TRACKER) - return True + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) async_remove_entry = config_entry_flow.webhook_async_remove_entry diff --git a/homeassistant/components/gree/__init__.py b/homeassistant/components/gree/__init__.py index b215d4eb9119d..b873d5ba4d374 100644 --- a/homeassistant/components/gree/__init__.py +++ b/homeassistant/components/gree/__init__.py @@ -1,5 +1,4 @@ """The Gree Climate integration.""" -import asyncio from datetime import timedelta import logging @@ -21,26 +20,17 @@ _LOGGER = logging.getLogger(__name__) - -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Gree Climate component.""" - hass.data[DOMAIN] = {} - return True +PLATFORMS = [CLIMATE_DOMAIN, SWITCH_DOMAIN] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Gree Climate from a config entry.""" + hass.data.setdefault(DOMAIN, {}) gree_discovery = DiscoveryService(hass) hass.data[DATA_DISCOVERY_SERVICE] = gree_discovery hass.data[DOMAIN].setdefault(DISPATCHERS, []) - - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, CLIMATE_DOMAIN) - ) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, SWITCH_DOMAIN) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) async def _async_scan_update(_=None): await gree_discovery.discovery.scan() @@ -67,12 +57,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): if hass.data.get(DATA_DISCOVERY_SERVICE) is not None: hass.data.pop(DATA_DISCOVERY_SERVICE) - results = asyncio.gather( - hass.config_entries.async_forward_entry_unload(entry, CLIMATE_DOMAIN), - hass.config_entries.async_forward_entry_unload(entry, SWITCH_DOMAIN), - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - unload_ok = all(await results) if unload_ok: hass.data[DOMAIN].pop(COORDINATORS, None) hass.data[DOMAIN].pop(DISPATCHERS, None) diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index ebb5e71e1cbb6..6c76da3373d57 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -105,24 +105,14 @@ def async_process_paired_sensor_uids(): ].async_add_listener(async_process_paired_sensor_uids) # Set up all of the Guardian entity platforms: - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN][DATA_CLIENT].pop(entry.entry_id) hass.data[DOMAIN][DATA_COORDINATOR].pop(entry.entry_id) diff --git a/homeassistant/components/habitica/__init__.py b/homeassistant/components/habitica/__init__.py index 159e760e2235d..e8846d1f85a19 100644 --- a/homeassistant/components/habitica/__init__.py +++ b/homeassistant/components/habitica/__init__.py @@ -1,5 +1,4 @@ """The habitica integration.""" -import asyncio import logging from habitipy.aio import HabitipyAsync @@ -100,7 +99,7 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool: return True -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up habitica from a config entry.""" class HAHabitipyAsync(HabitipyAsync): @@ -131,7 +130,7 @@ async def handle_api_call(call): ) data = hass.data.setdefault(DOMAIN, {}) - config = config_entry.data + config = entry.data websession = async_get_clientsession(hass) url = config[CONF_URL] username = config[CONF_API_USER] @@ -143,15 +142,12 @@ async def handle_api_call(call): if name is None: name = user["profile"]["name"] hass.config_entries.async_update_entry( - config_entry, - data={**config_entry.data, CONF_NAME: name}, + entry, + data={**entry.data, CONF_NAME: name}, ) - data[config_entry.entry_id] = api + data[entry.entry_id] = api - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) if not hass.services.has_service(DOMAIN, SERVICE_API_CALL): hass.services.async_register( @@ -163,14 +159,7 @@ async def handle_api_call(call): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/harmony/__init__.py b/homeassistant/components/harmony/__init__.py index cd69bd8017cb7..d0172bf737895 100644 --- a/homeassistant/components/harmony/__init__.py +++ b/homeassistant/components/harmony/__init__.py @@ -58,10 +58,7 @@ async def _async_on_stop(event): CANCEL_STOP: cancel_stop, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -115,14 +112,7 @@ async def _update_listener(hass: HomeAssistant, entry: ConfigEntry): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) # Shutdown a harmony remote for removal entry_data = hass.data[DOMAIN][entry.entry_id] diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index a2e7960972d97..eabd9bc7cd9d9 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -1,7 +1,6 @@ """Support for Hass.io.""" from __future__ import annotations -import asyncio from datetime import timedelta import logging import os @@ -518,31 +517,21 @@ async def async_handle_core_service(call): return True -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" dev_reg = await async_get_registry(hass) - coordinator = HassioDataUpdateCoordinator(hass, config_entry, dev_reg) + coordinator = HassioDataUpdateCoordinator(hass, entry, dev_reg) hass.data[ADDONS_COORDINATOR] = coordinator await coordinator.async_refresh() - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) # Pop add-on data hass.data.pop(ADDONS_COORDINATOR, None) diff --git a/homeassistant/components/heos/__init__.py b/homeassistant/components/heos/__init__.py index 652aa84483265..56155cb21a2a4 100644 --- a/homeassistant/components/heos/__init__.py +++ b/homeassistant/components/heos/__init__.py @@ -28,6 +28,8 @@ SIGNAL_HEOS_UPDATED, ) +PLATFORMS = [MEDIA_PLAYER_DOMAIN] + CONFIG_SCHEMA = vol.Schema( {DOMAIN: vol.Schema({vol.Required(CONF_HOST): cv.string})}, extra=vol.ALLOW_EXTRA ) @@ -119,9 +121,8 @@ async def disconnect_controller(event): services.register(hass, controller) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, MEDIA_PLAYER_DOMAIN) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True @@ -133,9 +134,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): services.remove(hass) - return await hass.config_entries.async_forward_entry_unload( - entry, MEDIA_PLAYER_DOMAIN - ) + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) class ControllerManager: diff --git a/homeassistant/components/hisense_aehw4a1/__init__.py b/homeassistant/components/hisense_aehw4a1/__init__.py index 725b294c00f03..1134ac4181da4 100644 --- a/homeassistant/components/hisense_aehw4a1/__init__.py +++ b/homeassistant/components/hisense_aehw4a1/__init__.py @@ -15,6 +15,8 @@ _LOGGER = logging.getLogger(__name__) +PLATFORMS = [CLIMATE_DOMAIN] + def coerce_ip(value): """Validate that provided value is a valid IP address.""" @@ -70,13 +72,10 @@ async def async_setup(hass, config): async def async_setup_entry(hass, entry): """Set up a config entry for Hisense AEH-W4A1.""" - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, CLIMATE_DOMAIN) - ) - + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass, entry): """Unload a config entry.""" - return await hass.config_entries.async_forward_entry_unload(entry, CLIMATE_DOMAIN) + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/hive/__init__.py b/homeassistant/components/hive/__init__.py index cc20b49b67abb..19ed6beedf9b9 100644 --- a/homeassistant/components/hive/__init__.py +++ b/homeassistant/components/hive/__init__.py @@ -1,5 +1,4 @@ """Support for the Hive devices and services.""" -import asyncio from functools import wraps import logging @@ -92,15 +91,7 @@ async def async_setup_entry(hass, entry): async def async_unload_entry(hass, entry): """Unload a config entry.""" - - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/hlk_sw16/__init__.py b/homeassistant/components/hlk_sw16/__init__.py index 91b269cc52037..e36af7676ed06 100644 --- a/homeassistant/components/hlk_sw16/__init__.py +++ b/homeassistant/components/hlk_sw16/__init__.py @@ -24,6 +24,8 @@ _LOGGER = logging.getLogger(__name__) +PLATFORMS = ["switch"] + DATA_DEVICE_REGISTER = "hlk_sw16_device_register" DATA_DEVICE_LISTENER = "hlk_sw16_device_listener" @@ -111,9 +113,7 @@ async def connect(): hass.data[DOMAIN][entry.entry_id][DATA_DEVICE_REGISTER] = client # Load entities - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "switch") - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) _LOGGER.info("Connected to HLK-SW16 device: %s", address) @@ -126,8 +126,7 @@ async def async_unload_entry(hass, entry): """Unload a config entry.""" client = hass.data[DOMAIN][entry.entry_id].pop(DATA_DEVICE_REGISTER) client.stop() - unload_ok = await hass.config_entries.async_forward_entry_unload(entry, "switch") - + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: if hass.data[DOMAIN][entry.entry_id]: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/home_connect/__init__.py b/homeassistant/components/home_connect/__init__.py index baf4fd17f85b7..f8a9157dca267 100644 --- a/homeassistant/components/home_connect/__init__.py +++ b/homeassistant/components/home_connect/__init__.py @@ -1,6 +1,5 @@ """Support for BSH Home Connect appliances.""" -import asyncio from datetime import timedelta import logging @@ -71,24 +70,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await update_all_devices(hass, entry) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/home_plus_control/__init__.py b/homeassistant/components/home_plus_control/__init__.py index e559cd030b355..176dc2fbd02e8 100644 --- a/homeassistant/components/home_plus_control/__init__.py +++ b/homeassistant/components/home_plus_control/__init__.py @@ -157,13 +157,8 @@ async def start_platforms(): async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload the Legrand Home+ Control config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, component) - for component in PLATFORMS - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS ) if unload_ok: # Unsubscribe the config_entry signal dispatcher connections diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index 677b8dab5f6f3..cc9ba7b620e1f 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -269,17 +269,9 @@ async def async_unload(self): await self.pairing.unsubscribe(self.watchable_characteristics) - unloads = [] - for platform in self.platforms: - unloads.append( - self.hass.config_entries.async_forward_entry_unload( - self.config_entry, platform - ) - ) - - results = await asyncio.gather(*unloads) - - return False not in results + return await self.hass.config_entries.async_unload_platforms( + self.config_entry, self.platforms + ) async def async_refresh_entity_map(self, config_num): """Handle setup of a HomeKit accessory.""" diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py index e731da2262e29..ad641c0f46de2 100644 --- a/homeassistant/components/homematicip_cloud/hap.py +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -101,12 +101,8 @@ async def async_setup(self, tries: int = 0) -> bool: "Connected to HomematicIP with HAP %s", self.config_entry.unique_id ) - for platform in PLATFORMS: - self.hass.async_create_task( - self.hass.config_entries.async_forward_entry_setup( - self.config_entry, platform - ) - ) + self.hass.config_entries.async_setup_platforms(self.config_entry, PLATFORMS) + return True @callback @@ -214,10 +210,9 @@ async def async_reset(self) -> bool: self._retry_task.cancel() await self.home.disable_events() _LOGGER.info("Closed connection to HomematicIP cloud server") - for platform in PLATFORMS: - await self.hass.config_entries.async_forward_entry_unload( - self.config_entry, platform - ) + await self.hass.config_entries.async_unload_platforms( + self.config_entry, PLATFORMS + ) self.hmip_device_by_entity_id = {} return True diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index ece967aa72b66..f0e8b0150e37a 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -420,10 +420,8 @@ def signal_update() -> None: ) # Forward config entry setup to platforms - for domain in CONFIG_ENTRY_PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, domain) - ) + hass.config_entries.async_setup_platforms(config_entry, CONFIG_ENTRY_PLATFORMS) + # Notify doesn't support config entry setup yet, load with discovery for now await discovery.async_load_platform( hass, @@ -462,8 +460,9 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> """Unload config entry.""" # Forward config entry unload to platforms - for domain in CONFIG_ENTRY_PLATFORMS: - await hass.config_entries.async_forward_entry_unload(config_entry, domain) + await hass.config_entries.async_unload_platforms( + config_entry, CONFIG_ENTRY_PLATFORMS + ) # Forget about the router and invoke its cleanup router = hass.data[DOMAIN].routers.pop(config_entry.data[CONF_URL]) diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index 801f2a33b7089..698ad9e18e3f4 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -29,6 +29,9 @@ # How long should we sleep if the hub is busy HUB_BUSY_SLEEP = 0.5 + +PLATFORMS = ["light", "binary_sensor", "sensor"] + _LOGGER = logging.getLogger(__name__) @@ -101,17 +104,7 @@ async def async_setup(self, tries=0): self.api = bridge self.sensor_manager = SensorManager(self) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(self.config_entry, "light") - ) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup( - self.config_entry, "binary_sensor" - ) - ) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(self.config_entry, "sensor") - ) + hass.config_entries.async_setup_platforms(self.config_entry, PLATFORMS) self.parallel_updates_semaphore = asyncio.Semaphore( 3 if self.api.config.modelid == "BSB001" else 10 @@ -179,21 +172,10 @@ async def async_reset(self): # If setup was successful, we set api variable, forwarded entry and # register service - results = await asyncio.gather( - self.hass.config_entries.async_forward_entry_unload( - self.config_entry, "light" - ), - self.hass.config_entries.async_forward_entry_unload( - self.config_entry, "binary_sensor" - ), - self.hass.config_entries.async_forward_entry_unload( - self.config_entry, "sensor" - ), + return await self.hass.config_entries.async_unload_platforms( + self.config_entry, PLATFORMS ) - # None and True are OK - return False not in results - async def hue_activate_scene(self, data, skip_reload=False, hide_warnings=False): """Service to call directly into bridge to set scenes.""" group_name = data[ATTR_GROUP_NAME] diff --git a/homeassistant/components/huisbaasje/__init__.py b/homeassistant/components/huisbaasje/__init__.py index 3af6db3efb5f2..f89c9f076258b 100644 --- a/homeassistant/components/huisbaasje/__init__.py +++ b/homeassistant/components/huisbaasje/__init__.py @@ -23,20 +23,17 @@ SOURCE_TYPES, ) -_LOGGER = logging.getLogger(__name__) - +PLATFORMS = ["sensor"] -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Huisbaasje component.""" - return True +_LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Huisbaasje from a config entry.""" # Create the Huisbaasje client huisbaasje = Huisbaasje( - username=config_entry.data[CONF_USERNAME], - password=config_entry.data[CONF_PASSWORD], + username=entry.data[CONF_USERNAME], + password=entry.data[CONF_PASSWORD], source_types=SOURCE_TYPES, request_timeout=FETCH_TIMEOUT, ) @@ -63,28 +60,22 @@ async def async_update_data(): await coordinator.async_config_entry_first_refresh() # Load the client in the data of home assistant - hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = { - DATA_COORDINATOR: coordinator - } + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {DATA_COORDINATOR: coordinator} # Offload the loading of entities to the platform - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, "sensor") - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" # Forward the unloading of the entry to the platform - unload_ok = await hass.config_entries.async_forward_entry_unload( - config_entry, "sensor" - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) # If successful, unload the Huisbaasje client if unload_ok: - hass.data[DOMAIN].pop(config_entry.entry_id) + hass.data[DOMAIN].pop(entry.entry_id) return unload_ok diff --git a/homeassistant/components/hunterdouglas_powerview/__init__.py b/homeassistant/components/hunterdouglas_powerview/__init__.py index 2c606dda9f28c..a25d24fef8102 100644 --- a/homeassistant/components/hunterdouglas_powerview/__init__.py +++ b/homeassistant/components/hunterdouglas_powerview/__init__.py @@ -1,5 +1,4 @@ """The Hunter Douglas PowerView integration.""" -import asyncio from datetime import timedelta import logging @@ -127,10 +126,7 @@ async def async_update_data(): DEVICE_INFO: device_info, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -172,15 +168,7 @@ def _async_map_data_by_id(data): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok diff --git a/homeassistant/components/hvv_departures/__init__.py b/homeassistant/components/hvv_departures/__init__.py index b3eb53bff7a3c..acdb3dcfb64cc 100644 --- a/homeassistant/components/hvv_departures/__init__.py +++ b/homeassistant/components/hvv_departures/__init__.py @@ -1,5 +1,4 @@ """The HVV integration.""" -import asyncio from homeassistant.components.binary_sensor import DOMAIN as DOMAIN_BINARY_SENSOR from homeassistant.components.sensor import DOMAIN as DOMAIN_SENSOR @@ -27,22 +26,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = hub - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/hyperion/__init__.py b/homeassistant/components/hyperion/__init__.py index 74c6998dc0196..ddadb4feea540 100644 --- a/homeassistant/components/hyperion/__init__.py +++ b/homeassistant/components/hyperion/__init__.py @@ -297,13 +297,8 @@ async def _async_entry_updated(hass: HomeAssistant, config_entry: ConfigEntry) - async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS ) if unload_ok and config_entry.entry_id in hass.data[DOMAIN]: config_data = hass.data[DOMAIN].pop(config_entry.entry_id) diff --git a/homeassistant/components/ialarm/__init__.py b/homeassistant/components/ialarm/__init__.py index 03d07a15394e6..a74eea7ba0755 100644 --- a/homeassistant/components/ialarm/__init__.py +++ b/homeassistant/components/ialarm/__init__.py @@ -14,7 +14,7 @@ from .const import DATA_COORDINATOR, DOMAIN, IALARM_TO_HASS -PLATFORM = "alarm_control_panel" +PLATFORMS = ["alarm_control_panel"] _LOGGER = logging.getLogger(__name__) @@ -39,20 +39,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): DATA_COORDINATOR: coordinator, } - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, PLATFORM) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload iAlarm config.""" - unload_ok = await hass.config_entries.async_forward_entry_unload(entry, PLATFORM) - + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok diff --git a/homeassistant/components/iaqualink/__init__.py b/homeassistant/components/iaqualink/__init__.py index 86dd6cb29329e..37dc0e39f3de0 100644 --- a/homeassistant/components/iaqualink/__init__.py +++ b/homeassistant/components/iaqualink/__init__.py @@ -46,6 +46,14 @@ ATTR_CONFIG = "config" PARALLEL_UPDATES = 0 +PLATFORMS = [ + BINARY_SENSOR_DOMAIN, + CLIMATE_DOMAIN, + LIGHT_DOMAIN, + SENSOR_DOMAIN, + SWITCH_DOMAIN, +] + CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( @@ -160,24 +168,13 @@ async def _async_systems_update(now): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - forward_unload = hass.config_entries.async_forward_entry_unload - - tasks = [] - - if hass.data[DOMAIN][BINARY_SENSOR_DOMAIN]: - tasks += [forward_unload(entry, BINARY_SENSOR_DOMAIN)] - if hass.data[DOMAIN][CLIMATE_DOMAIN]: - tasks += [forward_unload(entry, CLIMATE_DOMAIN)] - if hass.data[DOMAIN][LIGHT_DOMAIN]: - tasks += [forward_unload(entry, LIGHT_DOMAIN)] - if hass.data[DOMAIN][SENSOR_DOMAIN]: - tasks += [forward_unload(entry, SENSOR_DOMAIN)] - if hass.data[DOMAIN][SWITCH_DOMAIN]: - tasks += [forward_unload(entry, SWITCH_DOMAIN)] + platforms_to_unload = [ + platform for platform in PLATFORMS if platform in hass.data[DOMAIN] + ] hass.data[DOMAIN].clear() - return all(await asyncio.gather(*tasks)) + return await hass.config_entries.async_unload_platforms(entry, platforms_to_unload) def refresh_system(func): diff --git a/homeassistant/components/icloud/__init__.py b/homeassistant/components/icloud/__init__.py index 4bedb89ee0b3b..9267170391de6 100644 --- a/homeassistant/components/icloud/__init__.py +++ b/homeassistant/components/icloud/__init__.py @@ -1,5 +1,4 @@ """The iCloud component.""" -import asyncio import voluptuous as vol @@ -135,10 +134,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.unique_id] = account - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) def play_sound(service: ServiceDataType) -> None: """Play sound on the device.""" @@ -224,15 +220,7 @@ def _get_account(account_identifier: str) -> any: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.data[CONF_USERNAME]) - return unload_ok diff --git a/homeassistant/components/ipma/__init__.py b/homeassistant/components/ipma/__init__.py index 9a4d7f932e166..1a26d3756531e 100644 --- a/homeassistant/components/ipma/__init__.py +++ b/homeassistant/components/ipma/__init__.py @@ -4,16 +4,15 @@ DEFAULT_NAME = "ipma" +PLATFORMS = ["weather"] -async def async_setup_entry(hass, config_entry): + +async def async_setup_entry(hass, entry): """Set up IPMA station as config entry.""" - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, "weather") - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True -async def async_unload_entry(hass, config_entry): +async def async_unload_entry(hass, entry): """Unload a config entry.""" - await hass.config_entries.async_forward_entry_unload(config_entry, "weather") - return True + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/ipp/__init__.py b/homeassistant/components/ipp/__init__.py index 95a222ecfe4aa..d4ae0e0e1cb63 100644 --- a/homeassistant/components/ipp/__init__.py +++ b/homeassistant/components/ipp/__init__.py @@ -1,7 +1,6 @@ """The Internet Printing Protocol (IPP) integration.""" from __future__ import annotations -import asyncio from datetime import timedelta import logging from typing import Any @@ -58,28 +57,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) - + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok diff --git a/homeassistant/components/iqvia/__init__.py b/homeassistant/components/iqvia/__init__.py index c548a115e04eb..14e6353a06421 100644 --- a/homeassistant/components/iqvia/__init__.py +++ b/homeassistant/components/iqvia/__init__.py @@ -85,28 +85,16 @@ async def async_get_data_from_api(api_coro): await asyncio.gather(*init_data_update_tasks) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass, entry): """Unload an OpenUV config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) - + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN][DATA_COORDINATOR].pop(entry.entry_id) - return unload_ok diff --git a/homeassistant/components/islamic_prayer_times/__init__.py b/homeassistant/components/islamic_prayer_times/__init__.py index d7ded256f733f..8fa2d1b04cb92 100644 --- a/homeassistant/components/islamic_prayer_times/__init__.py +++ b/homeassistant/components/islamic_prayer_times/__init__.py @@ -22,6 +22,7 @@ _LOGGER = logging.getLogger(__name__) +PLATFORMS = ["sensor"] CONFIG_SCHEMA = vol.Schema( { @@ -63,9 +64,7 @@ async def async_unload_entry(hass, config_entry): if hass.data[DOMAIN].event_unsub: hass.data[DOMAIN].event_unsub() hass.data.pop(DOMAIN) - await hass.config_entries.async_forward_entry_unload(config_entry, "sensor") - - return True + return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) class IslamicPrayerClient: @@ -180,11 +179,7 @@ async def async_setup(self): await self.async_update() self.config_entry.add_update_listener(self.async_options_updated) - self.hass.async_create_task( - self.hass.config_entries.async_forward_entry_setup( - self.config_entry, "sensor" - ) - ) + self.hass.config_entries.async_setup_platforms(self.config_entry, PLATFORMS) return True diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index de43407c371ce..90e114e702335 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -1,7 +1,6 @@ """Support the ISY-994 controllers.""" from __future__ import annotations -import asyncio from functools import partial from urllib.parse import urlparse @@ -177,10 +176,7 @@ async def async_setup_entry( await _async_get_or_create_isy_device_in_registry(hass, entry, isy) # Load platforms for the devices in the ISY controller that we support. - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) def _start_auto_update() -> None: """Start isy auto update.""" @@ -245,14 +241,7 @@ async def async_unload_entry( hass: HomeAssistant, entry: config_entries.ConfigEntry ) -> bool: """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) hass_isy_data = hass.data[DOMAIN][entry.entry_id] diff --git a/homeassistant/components/izone/__init__.py b/homeassistant/components/izone/__init__.py index 3d708ceea17c7..76744550649be 100644 --- a/homeassistant/components/izone/__init__.py +++ b/homeassistant/components/izone/__init__.py @@ -10,6 +10,8 @@ from .const import DATA_CONFIG, IZONE from .discovery import async_start_discovery_service, async_stop_discovery_service +PLATFORMS = ["climate"] + CONFIG_SCHEMA = vol.Schema( { IZONE: vol.Schema( @@ -45,15 +47,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType): async def async_setup_entry(hass, entry): """Set up from a config entry.""" await async_start_discovery_service(hass) - - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "climate") - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass, entry): """Unload the config entry and stop discovery process.""" await async_stop_discovery_service(hass) - await hass.config_entries.async_forward_entry_unload(entry, "climate") - return True + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/juicenet/__init__.py b/homeassistant/components/juicenet/__init__.py index a7fb5e6b9b5e0..f892babd9cf2a 100644 --- a/homeassistant/components/juicenet/__init__.py +++ b/homeassistant/components/juicenet/__init__.py @@ -1,5 +1,4 @@ """The JuiceNet integration.""" -import asyncio from datetime import timedelta import logging @@ -91,25 +90,14 @@ async def async_update_data(): await coordinator.async_config_entry_first_refresh() - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok diff --git a/tests/components/homematicip_cloud/test_hap.py b/tests/components/homematicip_cloud/test_hap.py index 6c95017a63516..1f85626980ce3 100644 --- a/tests/components/homematicip_cloud/test_hap.py +++ b/tests/components/homematicip_cloud/test_hap.py @@ -82,15 +82,7 @@ async def test_hap_setup_works(): assert await hap.async_setup() assert hap.home is home - assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 8 - assert hass.config_entries.async_forward_entry_setup.mock_calls[0][1] == ( - entry, - "alarm_control_panel", - ) - assert hass.config_entries.async_forward_entry_setup.mock_calls[1][1] == ( - entry, - "binary_sensor", - ) + assert len(hass.config_entries.async_setup_platforms.mock_calls) == 1 async def test_hap_setup_connection_error(): diff --git a/tests/components/huisbaasje/test_config_flow.py b/tests/components/huisbaasje/test_config_flow.py index 35e28b645ebee..5da159b282c76 100644 --- a/tests/components/huisbaasje/test_config_flow.py +++ b/tests/components/huisbaasje/test_config_flow.py @@ -26,8 +26,6 @@ async def test_form(hass): "huisbaasje.Huisbaasje.get_user_id", return_value="test-id", ) as mock_get_user_id, patch( - "homeassistant.components.huisbaasje.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.huisbaasje.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -49,7 +47,6 @@ async def test_form(hass): } assert len(mock_authenticate.mock_calls) == 1 assert len(mock_get_user_id.mock_calls) == 1 - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -139,8 +136,6 @@ async def test_form_entry_exists(hass): with patch("huisbaasje.Huisbaasje.authenticate", return_value=None), patch( "huisbaasje.Huisbaasje.get_user_id", return_value="test-id", - ), patch( - "homeassistant.components.huisbaasje.async_setup", return_value=True ), patch( "homeassistant.components.huisbaasje.async_setup_entry", return_value=True, From 9742bfdf460b3b5a5b7d2a821975e3e0a5ab9a41 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Tue, 27 Apr 2021 10:41:37 -0400 Subject: [PATCH 0575/1317] Add selectors to wake_on_lan services (#49767) --- .../components/wake_on_lan/services.yaml | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/wake_on_lan/services.yaml b/homeassistant/components/wake_on_lan/services.yaml index 54ce72c94322a..7540451d0610f 100644 --- a/homeassistant/components/wake_on_lan/services.yaml +++ b/homeassistant/components/wake_on_lan/services.yaml @@ -1,12 +1,25 @@ send_magic_packet: + name: Send magic packet description: Send a 'magic packet' to wake up a device with 'Wake-On-LAN' capabilities. fields: mac: + name: MAC address description: MAC address of the device to wake up. + required: true example: "aa:bb:cc:dd:ee:ff" + selector: + text: broadcast_address: - description: Optional broadcast IP where to send the magic packet. + name: Broadcast address + description: Broadcast IP where to send the magic packet. example: 192.168.255.255 + selector: + text: broadcast_port: - description: Optional port where to send the magic packet. + name: Broadcast port + description: Port where to send the magic packet. example: 9 + selector: + number: + min: 1 + max: 65535 From d4ed65e0f53fcab991b13061b1101470f24287a6 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Tue, 27 Apr 2021 09:52:05 -0500 Subject: [PATCH 0576/1317] Add power binary_sensor support to Sonos (#49730) * Add power binary_sensor support to Sonos * Prepare for future unloading of config entries * Remove unnecessary calls to super() inits * Add binary_sensor to tests, remove invalid test for empty battery payload * Move sensor added_to_hass to common sensor class * Avoid dispatching sensors if no battery * Use proper attributes property * Remove power source fallback * Update homeassistant/components/sonos/speaker.py Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- .../components/sonos/binary_sensor.py | 67 +++++++ homeassistant/components/sonos/const.py | 7 +- homeassistant/components/sonos/entity.py | 23 ++- .../components/sonos/media_player.py | 9 +- homeassistant/components/sonos/sensor.py | 172 ++---------------- homeassistant/components/sonos/speaker.py | 99 +++++++++- tests/components/sonos/test_sensor.py | 26 ++- 7 files changed, 218 insertions(+), 185 deletions(-) create mode 100644 homeassistant/components/sonos/binary_sensor.py diff --git a/homeassistant/components/sonos/binary_sensor.py b/homeassistant/components/sonos/binary_sensor.py new file mode 100644 index 0000000000000..b7d515a8f11de --- /dev/null +++ b/homeassistant/components/sonos/binary_sensor.py @@ -0,0 +1,67 @@ +"""Entity representing a Sonos power sensor.""" +from __future__ import annotations + +import datetime +import logging +from typing import Any + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_BATTERY_CHARGING, + BinarySensorEntity, +) +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import DATA_SONOS, SONOS_CREATE_BATTERY +from .entity import SonosSensorEntity +from .speaker import SonosSpeaker + +_LOGGER = logging.getLogger(__name__) + +ATTR_BATTERY_POWER_SOURCE = "power_source" + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Sonos from a config entry.""" + + async def _async_create_entity(speaker: SonosSpeaker) -> None: + entity = SonosPowerEntity(speaker, hass.data[DATA_SONOS]) + async_add_entities([entity]) + + config_entry.async_on_unload( + async_dispatcher_connect(hass, SONOS_CREATE_BATTERY, _async_create_entity) + ) + + +class SonosPowerEntity(SonosSensorEntity, BinarySensorEntity): + """Representation of a Sonos power entity.""" + + @property + def unique_id(self) -> str: + """Return the unique ID of the sensor.""" + return f"{self.soco.uid}-power" + + @property + def name(self) -> str: + """Return the name of the sensor.""" + return f"{self.speaker.zone_name} Power" + + @property + def device_class(self) -> str: + """Return the entity's device class.""" + return DEVICE_CLASS_BATTERY_CHARGING + + async def async_update(self, now: datetime.datetime | None = None) -> None: + """Poll the device for the current state.""" + await self.speaker.async_poll_battery() + + @property + def is_on(self) -> bool | None: + """Return the state of the binary sensor.""" + return self.speaker.charging + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return entity specific state attributes.""" + return { + ATTR_BATTERY_POWER_SOURCE: self.speaker.power_source, + } diff --git a/homeassistant/components/sonos/const.py b/homeassistant/components/sonos/const.py index b841347ce27b9..133bf77399183 100644 --- a/homeassistant/components/sonos/const.py +++ b/homeassistant/components/sonos/const.py @@ -1,6 +1,7 @@ """Const for Sonos.""" import datetime +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.media_player.const import ( MEDIA_CLASS_ALBUM, @@ -22,7 +23,7 @@ DOMAIN = "sonos" DATA_SONOS = "sonos_media_player" -PLATFORMS = {MP_DOMAIN, SENSOR_DOMAIN} +PLATFORMS = {BINARY_SENSOR_DOMAIN, MP_DOMAIN, SENSOR_DOMAIN} SONOS_ARTIST = "artists" SONOS_ALBUM = "albums" @@ -128,12 +129,12 @@ ] SONOS_CONTENT_UPDATE = "sonos_content_update" -SONOS_DISCOVERY_UPDATE = "sonos_discovery_update" +SONOS_CREATE_BATTERY = "sonos_create_battery" +SONOS_CREATE_MEDIA_PLAYER = "sonos_create_media_player" SONOS_ENTITY_CREATED = "sonos_entity_created" SONOS_ENTITY_UPDATE = "sonos_entity_update" SONOS_GROUP_UPDATE = "sonos_group_update" SONOS_MEDIA_UPDATE = "sonos_media_update" -SONOS_PROPERTIES_UPDATE = "sonos_properties_update" SONOS_PLAYER_RECONNECTED = "sonos_player_reconnected" SONOS_STATE_UPDATED = "sonos_state_updated" SONOS_VOLUME_UPDATE = "sonos_properties_update" diff --git a/homeassistant/components/sonos/entity.py b/homeassistant/components/sonos/entity.py index 159b3fb348adf..a6cbadae0148a 100644 --- a/homeassistant/components/sonos/entity.py +++ b/homeassistant/components/sonos/entity.py @@ -7,11 +7,19 @@ from pysonos.core import SoCo import homeassistant.helpers.device_registry as dr -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.entity import Entity from . import SonosData -from .const import DOMAIN, SONOS_ENTITY_UPDATE, SONOS_STATE_UPDATED +from .const import ( + DOMAIN, + SONOS_ENTITY_CREATED, + SONOS_ENTITY_UPDATE, + SONOS_STATE_UPDATED, +) from .speaker import SonosSpeaker _LOGGER = logging.getLogger(__name__) @@ -71,3 +79,14 @@ def available(self) -> bool: def should_poll(self) -> bool: """Return that we should not be polled (we handle that internally).""" return False + + +class SonosSensorEntity(SonosEntity): + """Representation of a Sonos sensor entity.""" + + async def async_added_to_hass(self) -> None: + """Handle common setup when added to hass.""" + await super().async_added_to_hass() + async_dispatcher_send( + self.hass, f"{SONOS_ENTITY_CREATED}-{self.soco.uid}", self.platform.domain + ) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 57ce1f8a8ae03..73d144f6b0c9c 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -30,7 +30,6 @@ from homeassistant.components.media_player import MediaPlayerEntity from homeassistant.components.media_player.const import ( ATTR_MEDIA_ENQUEUE, - DOMAIN as MP_DOMAIN, MEDIA_TYPE_ALBUM, MEDIA_TYPE_ARTIST, MEDIA_TYPE_MUSIC, @@ -75,7 +74,7 @@ MEDIA_TYPES_TO_SONOS, PLAYABLE_MEDIA_TYPES, SONOS_CONTENT_UPDATE, - SONOS_DISCOVERY_UPDATE, + SONOS_CREATE_MEDIA_PLAYER, SONOS_ENTITY_CREATED, SONOS_GROUP_UPDATE, SONOS_MEDIA_UPDATE, @@ -189,7 +188,9 @@ async def async_service_handle(service_call: ServiceCall) -> None: hass, entities, service_call.data[ATTR_WITH_GROUP] # type: ignore[arg-type] ) - async_dispatcher_connect(hass, SONOS_DISCOVERY_UPDATE, async_create_entities) + config_entry.async_on_unload( + async_dispatcher_connect(hass, SONOS_CREATE_MEDIA_PLAYER, async_create_entities) + ) hass.services.async_register( SONOS_DOMAIN, @@ -390,7 +391,7 @@ async def async_added_to_hass(self) -> None: async_dispatcher_send(self.hass, SONOS_GROUP_UPDATE) async_dispatcher_send( - self.hass, f"{SONOS_ENTITY_CREATED}-{self.soco.uid}", MP_DOMAIN + self.hass, f"{SONOS_ENTITY_CREATED}-{self.soco.uid}", self.platform.domain ) @property diff --git a/homeassistant/components/sonos/sensor.py b/homeassistant/components/sonos/sensor.py index 2ca5e0979dcaa..a18c143fe6119 100644 --- a/homeassistant/components/sonos/sensor.py +++ b/homeassistant/components/sonos/sensor.py @@ -1,133 +1,35 @@ """Entity representing a Sonos battery level.""" from __future__ import annotations -import contextlib import datetime import logging -from typing import Any - -from pysonos.core import SoCo -from pysonos.events_base import Event as SonosEvent -from pysonos.exceptions import SoCoException - -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorEntity -from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE, STATE_UNKNOWN -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) -from homeassistant.util import dt as dt_util - -from . import SonosData -from .const import ( - BATTERY_SCAN_INTERVAL, - DATA_SONOS, - SONOS_DISCOVERY_UPDATE, - SONOS_ENTITY_CREATED, - SONOS_PROPERTIES_UPDATE, -) -from .entity import SonosEntity -from .speaker import SonosSpeaker - -_LOGGER = logging.getLogger(__name__) - -ATTR_BATTERY_LEVEL = "battery_level" -ATTR_BATTERY_CHARGING = "charging" -ATTR_BATTERY_POWERSOURCE = "power_source" - -EVENT_CHARGING = { - "CHARGING": True, - "NOT_CHARGING": False, -} +from homeassistant.components.sensor import SensorEntity +from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE +from homeassistant.helpers.dispatcher import async_dispatcher_connect -def fetch_battery_info_or_none(soco: SoCo) -> dict[str, Any] | None: - """Fetch battery_info from the given SoCo object. +from .const import DATA_SONOS, SONOS_CREATE_BATTERY +from .entity import SonosSensorEntity +from .speaker import SonosSpeaker - Returns None if the device doesn't support battery info - or if the device is offline. - """ - with contextlib.suppress(ConnectionError, TimeoutError, SoCoException): - return soco.get_battery_info() - return None +_LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Sonos from a config entry.""" - sonos_data = hass.data[DATA_SONOS] - - async def _async_create_entity(speaker: SonosSpeaker) -> SonosBatteryEntity | None: - if battery_info := await hass.async_add_executor_job( - fetch_battery_info_or_none, speaker.soco - ): - return SonosBatteryEntity(speaker, sonos_data, battery_info) - return None + async def _async_create_entity(speaker: SonosSpeaker) -> None: + entity = SonosBatteryEntity(speaker, hass.data[DATA_SONOS]) + async_add_entities([entity]) - async def _async_create_entities(speaker: SonosSpeaker): - if entity := await _async_create_entity(speaker): - async_add_entities([entity]) - else: - async_dispatcher_send( - hass, f"{SONOS_ENTITY_CREATED}-{speaker.soco.uid}", SENSOR_DOMAIN - ) + config_entry.async_on_unload( + async_dispatcher_connect(hass, SONOS_CREATE_BATTERY, _async_create_entity) + ) - async_dispatcher_connect(hass, SONOS_DISCOVERY_UPDATE, _async_create_entities) - -class SonosBatteryEntity(SonosEntity, SensorEntity): +class SonosBatteryEntity(SonosSensorEntity, SensorEntity): """Representation of a Sonos Battery entity.""" - def __init__( - self, speaker: SonosSpeaker, sonos_data: SonosData, battery_info: dict[str, Any] - ) -> None: - """Initialize a SonosBatteryEntity.""" - super().__init__(speaker, sonos_data) - self._battery_info: dict[str, Any] = battery_info - self._last_event: datetime.datetime | None = None - - async def async_added_to_hass(self) -> None: - """Register polling callback when added to hass.""" - await super().async_added_to_hass() - - self.async_on_remove( - self.hass.helpers.event.async_track_time_interval( - self.async_update, BATTERY_SCAN_INTERVAL - ) - ) - self.async_on_remove( - async_dispatcher_connect( - self.hass, - f"{SONOS_PROPERTIES_UPDATE}-{self.soco.uid}", - self.async_update_battery_info, - ) - ) - async_dispatcher_send( - self.hass, f"{SONOS_ENTITY_CREATED}-{self.soco.uid}", SENSOR_DOMAIN - ) - - async def async_update_battery_info(self, event: SonosEvent = None) -> None: - """Update battery info using the provided SonosEvent.""" - if event is None: - return - - if (more_info := event.variables.get("more_info")) is None: - return - - more_info_dict = dict(x.split(":") for x in more_info.split(",")) - self._last_event = dt_util.utcnow() - - is_charging = EVENT_CHARGING[more_info_dict["BattChg"]] - if is_charging == self.charging: - self._battery_info.update({"Level": int(more_info_dict["BattPct"])}) - else: - if battery_info := await self.hass.async_add_executor_job( - fetch_battery_info_or_none, self.soco - ): - self._battery_info = battery_info - - self.async_write_ha_state() - @property def unique_id(self) -> str: """Return the unique ID of the sensor.""" @@ -148,51 +50,11 @@ def unit_of_measurement(self) -> str: """Get the unit of measurement.""" return PERCENTAGE - async def async_update(self, event=None) -> None: + async def async_update(self, now: datetime.datetime | None = None) -> None: """Poll the device for the current state.""" - if not self.available: - # wait for the Sonos device to come back online - return - - if ( - self._last_event - and dt_util.utcnow() - self._last_event < BATTERY_SCAN_INTERVAL - ): - return - - if battery_info := await self.hass.async_add_executor_job( - fetch_battery_info_or_none, self.soco - ): - self._battery_info = battery_info - self.async_write_ha_state() - - @property - def battery_level(self) -> int: - """Return the battery level.""" - return self._battery_info.get("Level", 0) - - @property - def power_source(self) -> str: - """Return the name of the power source. - - Observed to be either BATTERY or SONOS_CHARGING_RING or USB_POWER. - """ - return self._battery_info.get("PowerSource", STATE_UNKNOWN) - - @property - def charging(self) -> bool: - """Return the charging status of this battery.""" - return self.power_source not in ("BATTERY", STATE_UNKNOWN) + await self.speaker.async_poll_battery() @property def state(self) -> int | None: """Return the state of the sensor.""" - return self._battery_info.get("Level") - - @property - def device_state_attributes(self) -> dict[str, Any]: - """Return entity specific state attributes.""" - return { - ATTR_BATTERY_CHARGING: self.charging, - ATTR_BATTERY_POWERSOURCE: self.power_source, - } + return self.speaker.battery_info.get("Level") diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 2d67cf8041fe5..73704c6136485 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -2,6 +2,7 @@ from __future__ import annotations from asyncio import gather +import contextlib import datetime import logging from typing import Any, Callable @@ -10,33 +11,52 @@ from pysonos.events_base import Event as SonosEvent, SubscriptionBase from pysonos.exceptions import SoCoException +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import ( async_dispatcher_send, dispatcher_connect, dispatcher_send, ) +from homeassistant.util import dt as dt_util from .const import ( + BATTERY_SCAN_INTERVAL, PLATFORMS, SCAN_INTERVAL, SEEN_EXPIRE_TIME, SONOS_CONTENT_UPDATE, - SONOS_DISCOVERY_UPDATE, + SONOS_CREATE_BATTERY, + SONOS_CREATE_MEDIA_PLAYER, SONOS_ENTITY_CREATED, SONOS_ENTITY_UPDATE, SONOS_GROUP_UPDATE, SONOS_MEDIA_UPDATE, SONOS_PLAYER_RECONNECTED, - SONOS_PROPERTIES_UPDATE, SONOS_SEEN, SONOS_STATE_UPDATED, SONOS_VOLUME_UPDATE, ) +EVENT_CHARGING = { + "CHARGING": True, + "NOT_CHARGING": False, +} + _LOGGER = logging.getLogger(__name__) +def fetch_battery_info_or_none(soco: SoCo) -> dict[str, Any] | None: + """Fetch battery_info from the given SoCo object. + + Returns None if the device doesn't support battery info + or if the device is offline. + """ + with contextlib.suppress(ConnectionError, TimeoutError, SoCoException): + return soco.get_battery_info() + + class SonosSpeaker: """Representation of a Sonos speaker.""" @@ -60,6 +80,10 @@ def __init__( self.version = speaker_info["software_version"] self.zone_name = speaker_info["zone_name"] + self.battery_info: dict[str, Any] | None = None + self._last_battery_event: datetime.datetime | None = None + self._battery_poll_timer: Callable | None = None + def setup(self) -> None: """Run initial setup of the speaker.""" self._entity_creation_dispatcher = dispatcher_connect( @@ -70,7 +94,18 @@ def setup(self) -> None: self._seen_dispatcher = dispatcher_connect( self.hass, f"{SONOS_SEEN}-{self.soco.uid}", self.async_seen ) - dispatcher_send(self.hass, SONOS_DISCOVERY_UPDATE, self) + + if (battery_info := fetch_battery_info_or_none(self.soco)) is not None: + # Battery events can be infrequent, polling is still necessary + self.battery_info = battery_info + self._battery_poll_timer = self.hass.helpers.event.track_time_interval( + self.async_poll_battery, BATTERY_SCAN_INTERVAL + ) + dispatcher_send(self.hass, SONOS_CREATE_BATTERY, self) + else: + self._platforms_ready.update({BINARY_SENSOR_DOMAIN, SENSOR_DOMAIN}) + + dispatcher_send(self.hass, SONOS_CREATE_MEDIA_PLAYER, self) async def async_handle_new_entity(self, entity_type: str) -> None: """Listen to new entities to trigger first subscription.""" @@ -149,9 +184,7 @@ def async_dispatch_volume(self, event: SonosEvent | None = None) -> None: @callback def async_dispatch_properties(self, event: SonosEvent | None = None) -> None: """Update properties from event.""" - async_dispatcher_send( - self.hass, f"{SONOS_PROPERTIES_UPDATE}-{self.soco.uid}", event - ) + self.hass.async_create_task(self.async_update_device_properties(event)) @callback def async_dispatch_groups(self, event: SonosEvent | None = None) -> None: @@ -217,3 +250,57 @@ async def async_unseen(self, now: datetime.datetime | None = None) -> None: await subscription.unsubscribe() self._subscriptions = [] + + async def async_update_device_properties(self, event: SonosEvent = None) -> None: + """Update device properties using the provided SonosEvent.""" + if event is None: + return + + if (more_info := event.variables.get("more_info")) is not None: + battery_dict = dict(x.split(":") for x in more_info.split(",")) + await self.async_update_battery_info(battery_dict) + + self.async_write_entity_states() + + async def async_update_battery_info(self, battery_dict: dict[str, Any]) -> None: + """Update battery info using the decoded SonosEvent.""" + self._last_battery_event = dt_util.utcnow() + + is_charging = EVENT_CHARGING[battery_dict["BattChg"]] + if is_charging == self.charging: + self.battery_info.update({"Level": int(battery_dict["BattPct"])}) + else: + if battery_info := await self.hass.async_add_executor_job( + fetch_battery_info_or_none, self.soco + ): + self.battery_info = battery_info + + @property + def power_source(self) -> str: + """Return the name of the current power source. + + Observed to be either BATTERY or SONOS_CHARGING_RING or USB_POWER. + """ + return self.battery_info["PowerSource"] + + @property + def charging(self) -> bool: + """Return the charging status of the speaker.""" + return self.power_source != "BATTERY" + + async def async_poll_battery(self, now: datetime.datetime | None = None) -> None: + """Poll the device for the current battery state.""" + if not self.available: + return + + if ( + self._last_battery_event + and dt_util.utcnow() - self._last_battery_event < BATTERY_SCAN_INTERVAL + ): + return + + if battery_info := await self.hass.async_add_executor_job( + fetch_battery_info_or_none, self.soco + ): + self.battery_info = battery_info + self.async_write_entity_states() diff --git a/tests/components/sonos/test_sensor.py b/tests/components/sonos/test_sensor.py index a1fc1d7efd8db..42bf6eedb9c20 100644 --- a/tests/components/sonos/test_sensor.py +++ b/tests/components/sonos/test_sensor.py @@ -2,6 +2,8 @@ from pysonos.exceptions import NotSupportedException from homeassistant.components.sonos import DOMAIN +from homeassistant.components.sonos.binary_sensor import ATTR_BATTERY_POWER_SOURCE +from homeassistant.const import STATE_ON from homeassistant.setup import async_setup_component @@ -22,6 +24,7 @@ async def test_entity_registry_unsupported(hass, config_entry, config, soco): assert "media_player.zone_a" in entity_registry.entities assert "sensor.zone_a_battery" not in entity_registry.entities + assert "binary_sensor.zone_a_power" not in entity_registry.entities async def test_entity_registry_supported(hass, config_entry, config, soco): @@ -32,17 +35,7 @@ async def test_entity_registry_supported(hass, config_entry, config, soco): assert "media_player.zone_a" in entity_registry.entities assert "sensor.zone_a_battery" in entity_registry.entities - - -async def test_battery_missing_attributes(hass, config_entry, config, soco): - """Test sonos device with unknown battery state.""" - soco.get_battery_info.return_value = {} - - await setup_platform(hass, config_entry, config) - - entity_registry = await hass.helpers.entity_registry.async_get_registry() - - assert entity_registry.entities.get("sensor.zone_a_battery") is None + assert "binary_sensor.zone_a_power" in entity_registry.entities async def test_battery_attributes(hass, config_entry, config, soco): @@ -53,9 +46,12 @@ async def test_battery_attributes(hass, config_entry, config, soco): battery = entity_registry.entities["sensor.zone_a_battery"] battery_state = hass.states.get(battery.entity_id) - - # confirm initial state from conftest assert battery_state.state == "100" assert battery_state.attributes.get("unit_of_measurement") == "%" - assert battery_state.attributes.get("charging") - assert battery_state.attributes.get("power_source") == "SONOS_CHARGING_RING" + + power = entity_registry.entities["binary_sensor.zone_a_power"] + power_state = hass.states.get(power.entity_id) + assert power_state.state == STATE_ON + assert ( + power_state.attributes.get(ATTR_BATTERY_POWER_SOURCE) == "SONOS_CHARGING_RING" + ) From a644c2e8baf749e2fe27b171721136edde95360d Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Tue, 27 Apr 2021 10:58:59 -0400 Subject: [PATCH 0577/1317] Add alarm control panel support to ZHA (#49080) * start implementation of IAS ACE * starting alarm control panel * enums * use new enums from zigpy * fix import * write state * fix registries after rebase * remove extra line * cleanup * fix deprecation warning * updates to catch up with codebase evolution * minor updates * cleanup * implement more ias ace functionality * cleanup * make config helper work for supplied section * connect to configuration * use ha async_create_task * add some tests * remove unused restore method * update tests * add tests from panel POV * dynamically include alarm control panel config * fix import Co-authored-by: Alexei Chetroi --- .../components/zha/alarm_control_panel.py | 174 +++++++++++++ homeassistant/components/zha/api.py | 7 + .../components/zha/core/channels/security.py | 235 ++++++++++++++++- homeassistant/components/zha/core/const.py | 22 +- homeassistant/components/zha/core/device.py | 6 +- .../components/zha/core/discovery.py | 1 + homeassistant/components/zha/core/helpers.py | 17 +- .../components/zha/core/registries.py | 2 + homeassistant/components/zha/light.py | 11 +- tests/components/zha/conftest.py | 9 + .../zha/test_alarm_control_panel.py | 245 ++++++++++++++++++ 11 files changed, 719 insertions(+), 10 deletions(-) create mode 100644 homeassistant/components/zha/alarm_control_panel.py create mode 100644 tests/components/zha/test_alarm_control_panel.py diff --git a/homeassistant/components/zha/alarm_control_panel.py b/homeassistant/components/zha/alarm_control_panel.py new file mode 100644 index 0000000000000..bd11ce0774142 --- /dev/null +++ b/homeassistant/components/zha/alarm_control_panel.py @@ -0,0 +1,174 @@ +"""Alarm control panels on Zigbee Home Automation networks.""" +import functools +import logging + +from zigpy.zcl.clusters.security import IasAce + +from homeassistant.components.alarm_control_panel import ( + ATTR_CHANGED_BY, + ATTR_CODE_ARM_REQUIRED, + ATTR_CODE_FORMAT, + DOMAIN, + FORMAT_TEXT, + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, + SUPPORT_ALARM_TRIGGER, + AlarmControlPanelEntity, +) +from homeassistant.components.zha.core.typing import ZhaDeviceType +from homeassistant.const import ( + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED, +) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .core import discovery +from .core.channels.security import ( + SIGNAL_ALARM_TRIGGERED, + SIGNAL_ARMED_STATE_CHANGED, + IasAce as AceChannel, +) +from .core.const import ( + CHANNEL_IAS_ACE, + CONF_ALARM_ARM_REQUIRES_CODE, + CONF_ALARM_FAILED_TRIES, + CONF_ALARM_MASTER_CODE, + DATA_ZHA, + DATA_ZHA_DISPATCHERS, + SIGNAL_ADD_ENTITIES, + ZHA_ALARM_OPTIONS, +) +from .core.helpers import async_get_zha_config_value +from .core.registries import ZHA_ENTITIES +from .entity import ZhaEntity + +_LOGGER = logging.getLogger(__name__) + + +STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) + +IAS_ACE_STATE_MAP = { + IasAce.PanelStatus.Panel_Disarmed: STATE_ALARM_DISARMED, + IasAce.PanelStatus.Armed_Stay: STATE_ALARM_ARMED_HOME, + IasAce.PanelStatus.Armed_Night: STATE_ALARM_ARMED_NIGHT, + IasAce.PanelStatus.Armed_Away: STATE_ALARM_ARMED_AWAY, + IasAce.PanelStatus.In_Alarm: STATE_ALARM_TRIGGERED, +} + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Zigbee Home Automation alarm control panel from config entry.""" + entities_to_create = hass.data[DATA_ZHA][DOMAIN] + + unsub = async_dispatcher_connect( + hass, + SIGNAL_ADD_ENTITIES, + functools.partial( + discovery.async_add_entities, async_add_entities, entities_to_create + ), + ) + hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS].append(unsub) + + +@STRICT_MATCH(channel_names=CHANNEL_IAS_ACE) +class ZHAAlarmControlPanel(ZhaEntity, AlarmControlPanelEntity): + """Entity for ZHA alarm control devices.""" + + def __init__(self, unique_id, zha_device: ZhaDeviceType, channels, **kwargs): + """Initialize the ZHA alarm control device.""" + super().__init__(unique_id, zha_device, channels, **kwargs) + cfg_entry = zha_device.gateway.config_entry + self._channel: AceChannel = channels[0] + self._channel.panel_code = async_get_zha_config_value( + cfg_entry, ZHA_ALARM_OPTIONS, CONF_ALARM_MASTER_CODE, "1234" + ) + self._channel.code_required_arm_actions = async_get_zha_config_value( + cfg_entry, ZHA_ALARM_OPTIONS, CONF_ALARM_ARM_REQUIRES_CODE, False + ) + self._channel.max_invalid_tries = async_get_zha_config_value( + cfg_entry, ZHA_ALARM_OPTIONS, CONF_ALARM_FAILED_TRIES, 3 + ) + + async def async_added_to_hass(self): + """Run when about to be added to hass.""" + await super().async_added_to_hass() + self.async_accept_signal( + self._channel, SIGNAL_ARMED_STATE_CHANGED, self.async_set_armed_mode + ) + self.async_accept_signal( + self._channel, SIGNAL_ALARM_TRIGGERED, self.async_alarm_trigger + ) + + @callback + def async_set_armed_mode(self) -> None: + """Set the entity state.""" + self.async_write_ha_state() + + @property + def code_format(self): + """Regex for code format or None if no code is required.""" + return FORMAT_TEXT + + @property + def changed_by(self): + """Last change triggered by.""" + return None + + @property + def code_arm_required(self): + """Whether the code is required for arm actions.""" + return self._channel.code_required_arm_actions + + async def async_alarm_disarm(self, code=None): + """Send disarm command.""" + self._channel.arm(IasAce.ArmMode.Disarm, code, 0) + self.async_write_ha_state() + + async def async_alarm_arm_home(self, code=None): + """Send arm home command.""" + self._channel.arm(IasAce.ArmMode.Arm_Day_Home_Only, code, 0) + self.async_write_ha_state() + + async def async_alarm_arm_away(self, code=None): + """Send arm away command.""" + self._channel.arm(IasAce.ArmMode.Arm_All_Zones, code, 0) + self.async_write_ha_state() + + async def async_alarm_arm_night(self, code=None): + """Send arm night command.""" + self._channel.arm(IasAce.ArmMode.Arm_Night_Sleep_Only, code, 0) + self.async_write_ha_state() + + async def async_alarm_trigger(self, code=None): + """Send alarm trigger command.""" + self.async_write_ha_state() + + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return ( + SUPPORT_ALARM_ARM_HOME + | SUPPORT_ALARM_ARM_AWAY + | SUPPORT_ALARM_ARM_NIGHT + | SUPPORT_ALARM_TRIGGER + ) + + @property + def state(self): + """Return the state of the entity.""" + return IAS_ACE_STATE_MAP.get(self._channel.armed_state) + + @property + def state_attributes(self): + """Return the state attributes.""" + state_attr = { + ATTR_CODE_FORMAT: self.code_format, + ATTR_CHANGED_BY: self.changed_by, + ATTR_CODE_ARM_REQUIRED: self.code_arm_required, + } + return state_attr diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index aedc32ac94b4c..2b41deaab6bb4 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -9,6 +9,7 @@ import voluptuous as vol from zigpy.config.validators import cv_boolean from zigpy.types.named import EUI64 +from zigpy.zcl.clusters.security import IasAce import zigpy.zdo.types as zdo_types from homeassistant.components import websocket_api @@ -54,11 +55,13 @@ WARNING_DEVICE_SQUAWK_MODE_ARMED, WARNING_DEVICE_STROBE_HIGH, WARNING_DEVICE_STROBE_YES, + ZHA_ALARM_OPTIONS, ZHA_CHANNEL_MSG, ZHA_CONFIG_SCHEMAS, ) from .core.group import GroupMember from .core.helpers import ( + async_input_cluster_exists, async_is_bindable_target, convert_install_code, get_matched_clusters, @@ -894,6 +897,10 @@ def custom_serializer(schema: Any) -> Any: data = {"schemas": {}, "data": {}} for section, schema in ZHA_CONFIG_SCHEMAS.items(): + if section == ZHA_ALARM_OPTIONS and not async_input_cluster_exists( + hass, IasAce.cluster_id + ): + continue data["schemas"][section] = voluptuous_serialize.convert( schema, custom_serializer=custom_serializer ) diff --git a/homeassistant/components/zha/core/channels/security.py b/homeassistant/components/zha/core/channels/security.py index 313d016935e9a..2af44bdf4e1b9 100644 --- a/homeassistant/components/zha/core/channels/security.py +++ b/homeassistant/components/zha/core/channels/security.py @@ -8,13 +8,15 @@ import asyncio from collections.abc import Coroutine +import logging from zigpy.exceptions import ZigbeeException import zigpy.zcl.clusters.security as security +from zigpy.zcl.clusters.security import IasAce as AceCluster -from homeassistant.core import callback +from homeassistant.core import CALLABLE_T, callback -from .. import registries +from .. import registries, typing as zha_typing from ..const import ( SIGNAL_ATTR_UPDATED, WARNING_DEVICE_MODE_EMERGENCY, @@ -25,11 +27,238 @@ ) from .base import ChannelStatus, ZigbeeChannel +IAS_ACE_ARM = 0x0000 # ("arm", (t.enum8, t.CharacterString, t.uint8_t), False), +IAS_ACE_BYPASS = 0x0001 # ("bypass", (t.LVList(t.uint8_t), t.CharacterString), False), +IAS_ACE_EMERGENCY = 0x0002 # ("emergency", (), False), +IAS_ACE_FIRE = 0x0003 # ("fire", (), False), +IAS_ACE_PANIC = 0x0004 # ("panic", (), False), +IAS_ACE_GET_ZONE_ID_MAP = 0x0005 # ("get_zone_id_map", (), False), +IAS_ACE_GET_ZONE_INFO = 0x0006 # ("get_zone_info", (t.uint8_t,), False), +IAS_ACE_GET_PANEL_STATUS = 0x0007 # ("get_panel_status", (), False), +IAS_ACE_GET_BYPASSED_ZONE_LIST = 0x0008 # ("get_bypassed_zone_list", (), False), +IAS_ACE_GET_ZONE_STATUS = ( + 0x0009 # ("get_zone_status", (t.uint8_t, t.uint8_t, t.Bool, t.bitmap16), False) +) +NAME = 0 +SIGNAL_ARMED_STATE_CHANGED = "zha_armed_state_changed" +SIGNAL_ALARM_TRIGGERED = "zha_armed_triggered" + +_LOGGER = logging.getLogger(__name__) -@registries.ZIGBEE_CHANNEL_REGISTRY.register(security.IasAce.cluster_id) + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(AceCluster.cluster_id) class IasAce(ZigbeeChannel): """IAS Ancillary Control Equipment channel.""" + def __init__( + self, cluster: zha_typing.ZigpyClusterType, ch_pool: zha_typing.ChannelPoolType + ) -> None: + """Initialize IAS Ancillary Control Equipment channel.""" + super().__init__(cluster, ch_pool) + self.command_map: dict[int, CALLABLE_T] = { + IAS_ACE_ARM: self.arm, + IAS_ACE_BYPASS: self._bypass, + IAS_ACE_EMERGENCY: self._emergency, + IAS_ACE_FIRE: self._fire, + IAS_ACE_PANIC: self._panic, + IAS_ACE_GET_ZONE_ID_MAP: self._get_zone_id_map, + IAS_ACE_GET_ZONE_INFO: self._get_zone_info, + IAS_ACE_GET_PANEL_STATUS: self._send_panel_status_response, + IAS_ACE_GET_BYPASSED_ZONE_LIST: self._get_bypassed_zone_list, + IAS_ACE_GET_ZONE_STATUS: self._get_zone_status, + } + self.arm_map: dict[AceCluster.ArmMode, CALLABLE_T] = { + AceCluster.ArmMode.Disarm: self._disarm, + AceCluster.ArmMode.Arm_All_Zones: self._arm_away, + AceCluster.ArmMode.Arm_Day_Home_Only: self._arm_day, + AceCluster.ArmMode.Arm_Night_Sleep_Only: self._arm_night, + } + self.armed_state: AceCluster.PanelStatus = AceCluster.PanelStatus.Panel_Disarmed + self.invalid_tries: int = 0 + + # These will all be setup by the entity from zha configuration + self.panel_code: str = "1234" + self.code_required_arm_actions = False + self.max_invalid_tries: int = 3 + + # where do we store this to handle restarts + self.alarm_status: AceCluster.AlarmStatus = AceCluster.AlarmStatus.No_Alarm + + @callback + def cluster_command(self, tsn, command_id, args) -> None: + """Handle commands received to this cluster.""" + self.warning( + "received command %s", self._cluster.server_commands.get(command_id)[NAME] + ) + self.command_map[command_id](*args) + + def arm(self, arm_mode: int, code: str, zone_id: int): + """Handle the IAS ACE arm command.""" + mode = AceCluster.ArmMode(arm_mode) + + self.zha_send_event( + self._cluster.server_commands.get(IAS_ACE_ARM)[NAME], + { + "arm_mode": mode.value, + "arm_mode_description": mode.name, + "code": code, + "zone_id": zone_id, + }, + ) + + zigbee_reply = self.arm_map[mode](code) + self._ch_pool.hass.async_create_task(zigbee_reply) + + if self.invalid_tries >= self.max_invalid_tries: + self.alarm_status = AceCluster.AlarmStatus.Emergency + self.armed_state = AceCluster.PanelStatus.In_Alarm + self.async_send_signal(f"{self.unique_id}_{SIGNAL_ALARM_TRIGGERED}") + else: + self.async_send_signal(f"{self.unique_id}_{SIGNAL_ARMED_STATE_CHANGED}") + self._send_panel_status_changed() + + def _disarm(self, code: str): + """Test the code and disarm the panel if the code is correct.""" + if ( + code != self.panel_code + and self.armed_state != AceCluster.PanelStatus.Panel_Disarmed + ): + self.warning("Invalid code supplied to IAS ACE") + self.invalid_tries += 1 + zigbee_reply = self.arm_response( + AceCluster.ArmNotification.Invalid_Arm_Disarm_Code + ) + else: + self.invalid_tries = 0 + if ( + self.armed_state == AceCluster.PanelStatus.Panel_Disarmed + and self.alarm_status == AceCluster.AlarmStatus.No_Alarm + ): + self.warning("IAS ACE already disarmed") + zigbee_reply = self.arm_response( + AceCluster.ArmNotification.Already_Disarmed + ) + else: + self.warning("Disarming all IAS ACE zones") + zigbee_reply = self.arm_response( + AceCluster.ArmNotification.All_Zones_Disarmed + ) + + self.armed_state = AceCluster.PanelStatus.Panel_Disarmed + self.alarm_status = AceCluster.AlarmStatus.No_Alarm + return zigbee_reply + + def _arm_day(self, code: str) -> None: + """Arm the panel for day / home zones.""" + return self._handle_arm( + code, + AceCluster.PanelStatus.Armed_Stay, + AceCluster.ArmNotification.Only_Day_Home_Zones_Armed, + ) + + def _arm_night(self, code: str) -> None: + """Arm the panel for night / sleep zones.""" + return self._handle_arm( + code, + AceCluster.PanelStatus.Armed_Night, + AceCluster.ArmNotification.Only_Night_Sleep_Zones_Armed, + ) + + def _arm_away(self, code: str) -> None: + """Arm the panel for away mode.""" + return self._handle_arm( + code, + AceCluster.PanelStatus.Armed_Away, + AceCluster.ArmNotification.All_Zones_Armed, + ) + + def _handle_arm( + self, + code: str, + panel_status: AceCluster.PanelStatus, + armed_type: AceCluster.ArmNotification, + ) -> None: + """Arm the panel with the specified statuses.""" + if self.code_required_arm_actions and code != self.panel_code: + self.warning("Invalid code supplied to IAS ACE") + zigbee_reply = self.arm_response( + AceCluster.ArmNotification.Invalid_Arm_Disarm_Code + ) + else: + self.warning("Arming all IAS ACE zones") + self.armed_state = panel_status + zigbee_reply = self.arm_response(armed_type) + return zigbee_reply + + def _bypass(self, zone_list, code) -> None: + """Handle the IAS ACE bypass command.""" + self.zha_send_event( + self._cluster.server_commands.get(IAS_ACE_BYPASS)[NAME], + {"zone_list": zone_list, "code": code}, + ) + + def _emergency(self) -> None: + """Handle the IAS ACE emergency command.""" + self._set_alarm( + AceCluster.AlarmStatus.Emergency, + IAS_ACE_EMERGENCY, + ) + + def _fire(self) -> None: + """Handle the IAS ACE fire command.""" + self._set_alarm( + AceCluster.AlarmStatus.Fire, + IAS_ACE_FIRE, + ) + + def _panic(self) -> None: + """Handle the IAS ACE panic command.""" + self._set_alarm( + AceCluster.AlarmStatus.Emergency_Panic, + IAS_ACE_PANIC, + ) + + def _set_alarm(self, status: AceCluster.PanelStatus, event: str) -> None: + """Set the specified alarm status.""" + self.alarm_status = status + self.armed_state = AceCluster.PanelStatus.In_Alarm + self.async_send_signal(f"{self.unique_id}_{SIGNAL_ALARM_TRIGGERED}") + self._send_panel_status_changed() + + def _get_zone_id_map(self): + """Handle the IAS ACE zone id map command.""" + + def _get_zone_info(self, zone_id): + """Handle the IAS ACE zone info command.""" + + def _send_panel_status_response(self) -> None: + """Handle the IAS ACE panel status response command.""" + response = self.panel_status_response( + self.armed_state, + 0x00, + AceCluster.AudibleNotification.Default_Sound, + self.alarm_status, + ) + self._ch_pool.hass.async_create_task(response) + + def _send_panel_status_changed(self) -> None: + """Handle the IAS ACE panel status changed command.""" + response = self.panel_status_changed( + self.armed_state, + 0x00, + AceCluster.AudibleNotification.Default_Sound, + self.alarm_status, + ) + self._ch_pool.hass.async_create_task(response) + + def _get_bypassed_zone_list(self): + """Handle the IAS ACE bypassed zone list command.""" + + def _get_zone_status( + self, starting_zone_id, max_zone_ids, zone_status_mask_flag, zone_status_mask + ): + """Handle the IAS ACE zone status command.""" + @registries.CHANNEL_ONLY_CLUSTERS.register(security.IasWd.cluster_id) @registries.ZIGBEE_CHANNEL_REGISTRY.register(security.IasWd.cluster_id) diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index 7df850909f48d..c4c18c4304bad 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -13,6 +13,7 @@ import zigpy_zigate.zigbee.application import zigpy_znp.zigbee.application +from homeassistant.components.alarm_control_panel import DOMAIN as ALARM from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR from homeassistant.components.climate import DOMAIN as CLIMATE from homeassistant.components.cover import DOMAIN as COVER @@ -83,6 +84,7 @@ CHANNEL_EVENT_RELAY = "event_relay" CHANNEL_FAN = "fan" CHANNEL_HUMIDITY = "humidity" +CHANNEL_IAS_ACE = "ias_ace" CHANNEL_IAS_WD = "ias_wd" CHANNEL_IDENTIFY = "identify" CHANNEL_ILLUMINANCE = "illuminance" @@ -106,6 +108,7 @@ CLUSTER_TYPE_OUT = "out" PLATFORMS = ( + ALARM, BINARY_SENSOR, CLIMATE, COVER, @@ -118,6 +121,10 @@ SWITCH, ) +CONF_ALARM_MASTER_CODE = "alarm_master_code" +CONF_ALARM_FAILED_TRIES = "alarm_failed_tries" +CONF_ALARM_ARM_REQUIRES_CODE = "alarm_arm_requires_code" + CONF_BAUDRATE = "baudrate" CONF_CUSTOM_QUIRKS_PATH = "custom_quirks_path" CONF_DATABASE = "database_path" @@ -137,6 +144,14 @@ } ) +CONF_ZHA_ALARM_SCHEMA = vol.Schema( + { + vol.Required(CONF_ALARM_MASTER_CODE, default="1234"): cv.string, + vol.Required(CONF_ALARM_FAILED_TRIES, default=3): cv.positive_int, + vol.Required(CONF_ALARM_ARM_REQUIRES_CODE, default=False): cv.boolean, + } +) + CUSTOM_CONFIGURATION = "custom_configuration" DATA_DEVICE_CONFIG = "zha_device_config" @@ -191,8 +206,13 @@ PRESET_SCHEDULE = "schedule" PRESET_COMPLEX = "complex" +ZHA_ALARM_OPTIONS = "zha_alarm_options" ZHA_OPTIONS = "zha_options" -ZHA_CONFIG_SCHEMAS = {ZHA_OPTIONS: CONF_ZHA_OPTIONS_SCHEMA} + +ZHA_CONFIG_SCHEMAS = { + ZHA_OPTIONS: CONF_ZHA_OPTIONS_SCHEMA, + ZHA_ALARM_OPTIONS: CONF_ZHA_ALARM_SCHEMA, +} class RadioType(enum.Enum): diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index c8866990cd924..6497a85b8f952 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -65,6 +65,7 @@ UNKNOWN, UNKNOWN_MANUFACTURER, UNKNOWN_MODEL, + ZHA_OPTIONS, ) from .helpers import LogMixin, async_get_zha_config_value @@ -396,7 +397,10 @@ def device_info(self): async def async_configure(self): """Configure the device.""" should_identify = async_get_zha_config_value( - self._zha_gateway.config_entry, CONF_ENABLE_IDENTIFY_ON_JOIN, True + self._zha_gateway.config_entry, + ZHA_OPTIONS, + CONF_ENABLE_IDENTIFY_ON_JOIN, + True, ) self.debug("started configuration") await self._channels.async_configure() diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py index b12d6efbcf821..6545f14668f4a 100644 --- a/homeassistant/components/zha/core/discovery.py +++ b/homeassistant/components/zha/core/discovery.py @@ -15,6 +15,7 @@ from . import const as zha_const, registries as zha_regs, typing as zha_typing from .. import ( # noqa: F401 pylint: disable=unused-import, + alarm_control_panel, binary_sensor, climate, cover, diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index f38e4c2c69577..84088148a8e12 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -31,7 +31,6 @@ CUSTOM_CONFIGURATION, DATA_ZHA, DATA_ZHA_GATEWAY, - ZHA_OPTIONS, ) from .registries import BINDABLE_CLUSTERS from .typing import ZhaDeviceType, ZigpyClusterType @@ -131,15 +130,27 @@ def async_is_bindable_target(source_zha_device, target_zha_device): @callback -def async_get_zha_config_value(config_entry, config_key, default): +def async_get_zha_config_value(config_entry, section, config_key, default): """Get the value for the specified configuration from the zha config entry.""" return ( config_entry.options.get(CUSTOM_CONFIGURATION, {}) - .get(ZHA_OPTIONS, {}) + .get(section, {}) .get(config_key, default) ) +def async_input_cluster_exists(hass, cluster_id): + """Determine if a device containing the specified in cluster is paired.""" + zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + zha_devices = zha_gateway.devices.values() + for zha_device in zha_devices: + clusters_by_endpoint = zha_device.async_get_clusters() + for clusters in clusters_by_endpoint.values(): + if cluster_id in clusters[CLUSTER_TYPE_IN]: + return True + return False + + async def async_get_zha_device(hass, device_id): """Get a ZHA device for the given device registry id.""" device_registry = await hass.helpers.device_registry.async_get_registry() diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index 2f9ed57745a26..42f09d5323fc6 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -9,6 +9,7 @@ import zigpy.profiles.zll import zigpy.zcl as zcl +from homeassistant.components.alarm_control_panel import DOMAIN as ALARM from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR from homeassistant.components.climate import DOMAIN as CLIMATE from homeassistant.components.cover import DOMAIN as COVER @@ -104,6 +105,7 @@ zigpy.profiles.zha.DeviceType.ON_OFF_PLUG_IN_UNIT: SWITCH, zigpy.profiles.zha.DeviceType.SHADE: COVER, zigpy.profiles.zha.DeviceType.SMART_PLUG: SWITCH, + zigpy.profiles.zha.DeviceType.IAS_ANCILLARY_CONTROL: ALARM, }, zigpy.profiles.zll.PROFILE_ID: { zigpy.profiles.zll.DeviceType.COLOR_LIGHT: LIGHT, diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 2aadb1199a2f9..c7001611aa080 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -54,6 +54,7 @@ SIGNAL_ADD_ENTITIES, SIGNAL_ATTR_UPDATED, SIGNAL_SET_LEVEL, + ZHA_OPTIONS, ) from .core.helpers import LogMixin, async_get_zha_config_value from .core.registries import ZHA_ENTITIES @@ -394,7 +395,10 @@ def __init__(self, unique_id, zha_device: ZhaDeviceType, channels, **kwargs): self._effect_list = effect_list self._default_transition = async_get_zha_config_value( - zha_device.gateway.config_entry, CONF_DEFAULT_LIGHT_TRANSITION, 0 + zha_device.gateway.config_entry, + ZHA_OPTIONS, + CONF_DEFAULT_LIGHT_TRANSITION, + 0, ) @callback @@ -553,7 +557,10 @@ def __init__( self._identify_channel = group.endpoint[Identify.cluster_id] self._debounced_member_refresh = None self._default_transition = async_get_zha_config_value( - zha_device.gateway.config_entry, CONF_DEFAULT_LIGHT_TRANSITION, 0 + zha_device.gateway.config_entry, + ZHA_OPTIONS, + CONF_DEFAULT_LIGHT_TRANSITION, + 0, ) async def async_added_to_hass(self): diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index b3ac4aff16e56..df90256b3a8e9 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -48,6 +48,15 @@ async def config_entry_fixture(hass): zigpy.config.CONF_DEVICE: {zigpy.config.CONF_DEVICE_PATH: "/dev/ttyUSB0"}, zha_const.CONF_RADIO_TYPE: "ezsp", }, + options={ + zha_const.CUSTOM_CONFIGURATION: { + zha_const.ZHA_ALARM_OPTIONS: { + zha_const.CONF_ALARM_ARM_REQUIRES_CODE: False, + zha_const.CONF_ALARM_MASTER_CODE: "4321", + zha_const.CONF_ALARM_FAILED_TRIES: 2, + } + } + }, ) entry.add_to_hass(hass) return entry diff --git a/tests/components/zha/test_alarm_control_panel.py b/tests/components/zha/test_alarm_control_panel.py new file mode 100644 index 0000000000000..c3428a044a4f8 --- /dev/null +++ b/tests/components/zha/test_alarm_control_panel.py @@ -0,0 +1,245 @@ +"""Test zha alarm control panel.""" +from unittest.mock import AsyncMock, call, patch, sentinel + +import pytest +import zigpy.profiles.zha as zha +import zigpy.zcl.clusters.security as security +import zigpy.zcl.foundation as zcl_f + +from homeassistant.components.alarm_control_panel import DOMAIN as ALARM_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED, + STATE_UNAVAILABLE, +) + +from .common import async_enable_traffic, find_entity_id + + +@pytest.fixture +def zigpy_device(zigpy_device_mock): + """Device tracker zigpy device.""" + endpoints = { + 1: { + "in_clusters": [security.IasAce.cluster_id], + "out_clusters": [], + "device_type": zha.DeviceType.IAS_ANCILLARY_CONTROL, + } + } + return zigpy_device_mock( + endpoints, node_descriptor=b"\x02@\x8c\x02\x10RR\x00\x00\x00R\x00\x00" + ) + + +@patch( + "zigpy.zcl.clusters.security.IasAce.client_command", + new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), +) +async def test_alarm_control_panel(hass, zha_device_joined_restored, zigpy_device): + """Test zha alarm control panel platform.""" + + zha_device = await zha_device_joined_restored(zigpy_device) + cluster = zigpy_device.endpoints.get(1).ias_ace + entity_id = await find_entity_id(ALARM_DOMAIN, zha_device, hass) + assert entity_id is not None + assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED + await async_enable_traffic(hass, [zha_device], enabled=False) + # test that the panel was created and that it is unavailable + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + + # allow traffic to flow through the gateway and device + await async_enable_traffic(hass, [zha_device]) + + # test that the state has changed from unavailable to STATE_ALARM_DISARMED + assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED + + # arm_away from HA + cluster.client_command.reset_mock() + await hass.services.async_call( + ALARM_DOMAIN, "alarm_arm_away", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_AWAY + assert cluster.client_command.call_count == 2 + assert cluster.client_command.await_count == 2 + assert cluster.client_command.call_args == call( + 4, + security.IasAce.PanelStatus.Armed_Away, + 0, + security.IasAce.AudibleNotification.Default_Sound, + security.IasAce.AlarmStatus.No_Alarm, + ) + + # disarm from HA + await reset_alarm_panel(hass, cluster, entity_id) + + # trip alarm from faulty code entry + cluster.client_command.reset_mock() + await hass.services.async_call( + ALARM_DOMAIN, "alarm_arm_away", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_AWAY + cluster.client_command.reset_mock() + await hass.services.async_call( + ALARM_DOMAIN, + "alarm_disarm", + {ATTR_ENTITY_ID: entity_id, "code": "1111"}, + blocking=True, + ) + await hass.services.async_call( + ALARM_DOMAIN, + "alarm_disarm", + {ATTR_ENTITY_ID: entity_id, "code": "1111"}, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_ALARM_TRIGGERED + assert cluster.client_command.call_count == 4 + assert cluster.client_command.await_count == 4 + assert cluster.client_command.call_args == call( + 4, + security.IasAce.PanelStatus.In_Alarm, + 0, + security.IasAce.AudibleNotification.Default_Sound, + security.IasAce.AlarmStatus.Emergency, + ) + + # reset the panel + await reset_alarm_panel(hass, cluster, entity_id) + + # arm_home from HA + cluster.client_command.reset_mock() + await hass.services.async_call( + ALARM_DOMAIN, "alarm_arm_home", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_HOME + assert cluster.client_command.call_count == 2 + assert cluster.client_command.await_count == 2 + assert cluster.client_command.call_args == call( + 4, + security.IasAce.PanelStatus.Armed_Stay, + 0, + security.IasAce.AudibleNotification.Default_Sound, + security.IasAce.AlarmStatus.No_Alarm, + ) + + # arm_night from HA + cluster.client_command.reset_mock() + await hass.services.async_call( + ALARM_DOMAIN, "alarm_arm_night", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_NIGHT + assert cluster.client_command.call_count == 2 + assert cluster.client_command.await_count == 2 + assert cluster.client_command.call_args == call( + 4, + security.IasAce.PanelStatus.Armed_Night, + 0, + security.IasAce.AudibleNotification.Default_Sound, + security.IasAce.AlarmStatus.No_Alarm, + ) + + # reset the panel + await reset_alarm_panel(hass, cluster, entity_id) + + # arm from panel + cluster.listener_event( + "cluster_command", 1, 0, [security.IasAce.ArmMode.Arm_All_Zones, "", 0] + ) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_AWAY + + # reset the panel + await reset_alarm_panel(hass, cluster, entity_id) + + # arm day home only from panel + cluster.listener_event( + "cluster_command", 1, 0, [security.IasAce.ArmMode.Arm_Day_Home_Only, "", 0] + ) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_HOME + + # reset the panel + await reset_alarm_panel(hass, cluster, entity_id) + + # arm night sleep only from panel + cluster.listener_event( + "cluster_command", 1, 0, [security.IasAce.ArmMode.Arm_Night_Sleep_Only, "", 0] + ) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_NIGHT + + # disarm from panel with bad code + cluster.listener_event( + "cluster_command", 1, 0, [security.IasAce.ArmMode.Disarm, "", 0] + ) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_NIGHT + + # disarm from panel with bad code for 2nd time trips alarm + cluster.listener_event( + "cluster_command", 1, 0, [security.IasAce.ArmMode.Disarm, "", 0] + ) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_ALARM_TRIGGERED + + # disarm from panel with good code + cluster.listener_event( + "cluster_command", 1, 0, [security.IasAce.ArmMode.Disarm, "4321", 0] + ) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED + + # panic from panel + cluster.listener_event("cluster_command", 1, 4, []) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_ALARM_TRIGGERED + + # reset the panel + await reset_alarm_panel(hass, cluster, entity_id) + + # fire from panel + cluster.listener_event("cluster_command", 1, 3, []) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_ALARM_TRIGGERED + + # reset the panel + await reset_alarm_panel(hass, cluster, entity_id) + + # emergency from panel + cluster.listener_event("cluster_command", 1, 2, []) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_ALARM_TRIGGERED + + # reset the panel + await reset_alarm_panel(hass, cluster, entity_id) + + +async def reset_alarm_panel(hass, cluster, entity_id): + """Reset the state of the alarm panel.""" + cluster.client_command.reset_mock() + await hass.services.async_call( + ALARM_DOMAIN, + "alarm_disarm", + {ATTR_ENTITY_ID: entity_id, "code": "4321"}, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED + assert cluster.client_command.call_count == 2 + assert cluster.client_command.await_count == 2 + assert cluster.client_command.call_args == call( + 4, + security.IasAce.PanelStatus.Panel_Disarmed, + 0, + security.IasAce.AudibleNotification.Default_Sound, + security.IasAce.AlarmStatus.No_Alarm, + ) + cluster.client_command.reset_mock() From 2adc6d62e5ccc106f68423d05eb12d38cb291137 Mon Sep 17 00:00:00 2001 From: Ruslan Sayfutdinov Date: Tue, 27 Apr 2021 17:13:11 +0100 Subject: [PATCH 0578/1317] Replace .no-strict-typing with .strict-typing (#49762) --- .no-strict-typing | 950 --------------------------- .strict-typing | 47 ++ homeassistant/components/__init__.py | 8 +- mypy.ini | 15 +- script/hassfest/mypy_config.py | 36 +- 5 files changed, 86 insertions(+), 970 deletions(-) delete mode 100644 .no-strict-typing create mode 100644 .strict-typing diff --git a/.no-strict-typing b/.no-strict-typing deleted file mode 100644 index a24f7bcf8e376..0000000000000 --- a/.no-strict-typing +++ /dev/null @@ -1,950 +0,0 @@ -# Used by hassfest for generating mypy.ini. -# Components listed here will be excluded from strict mypy checks. -# But basic checks for existing type annotations will still be applied. - -homeassistant.components.abode.* -homeassistant.components.accuweather.* -homeassistant.components.acer_projector.* -homeassistant.components.acmeda.* -homeassistant.components.actiontec.* -homeassistant.components.adguard.* -homeassistant.components.ads.* -homeassistant.components.advantage_air.* -homeassistant.components.aemet.* -homeassistant.components.aftership.* -homeassistant.components.agent_dvr.* -homeassistant.components.air_quality.* -homeassistant.components.airly.* -homeassistant.components.airnow.* -homeassistant.components.airvisual.* -homeassistant.components.aladdin_connect.* -homeassistant.components.alarm_control_panel.* -homeassistant.components.alarmdecoder.* -homeassistant.components.alert.* -homeassistant.components.alexa.* -homeassistant.components.almond.* -homeassistant.components.alpha_vantage.* -homeassistant.components.amazon_polly.* -homeassistant.components.ambiclimate.* -homeassistant.components.ambient_station.* -homeassistant.components.amcrest.* -homeassistant.components.ampio.* -homeassistant.components.analytics.* -homeassistant.components.android_ip_webcam.* -homeassistant.components.androidtv.* -homeassistant.components.anel_pwrctrl.* -homeassistant.components.anthemav.* -homeassistant.components.apache_kafka.* -homeassistant.components.apcupsd.* -homeassistant.components.api.* -homeassistant.components.apns.* -homeassistant.components.apple_tv.* -homeassistant.components.apprise.* -homeassistant.components.aprs.* -homeassistant.components.aqualogic.* -homeassistant.components.aquostv.* -homeassistant.components.arcam_fmj.* -homeassistant.components.arduino.* -homeassistant.components.arest.* -homeassistant.components.arlo.* -homeassistant.components.arris_tg2492lg.* -homeassistant.components.aruba.* -homeassistant.components.arwn.* -homeassistant.components.asterisk_cdr.* -homeassistant.components.asterisk_mbox.* -homeassistant.components.asuswrt.* -homeassistant.components.atag.* -homeassistant.components.aten_pe.* -homeassistant.components.atome.* -homeassistant.components.august.* -homeassistant.components.aurora.* -homeassistant.components.aurora_abb_powerone.* -homeassistant.components.auth.* -homeassistant.components.avea.* -homeassistant.components.avion.* -homeassistant.components.awair.* -homeassistant.components.aws.* -homeassistant.components.axis.* -homeassistant.components.azure_devops.* -homeassistant.components.azure_event_hub.* -homeassistant.components.azure_service_bus.* -homeassistant.components.baidu.* -homeassistant.components.bayesian.* -homeassistant.components.bbb_gpio.* -homeassistant.components.bbox.* -homeassistant.components.beewi_smartclim.* -homeassistant.components.bh1750.* -homeassistant.components.bitcoin.* -homeassistant.components.bizkaibus.* -homeassistant.components.blackbird.* -homeassistant.components.blebox.* -homeassistant.components.blink.* -homeassistant.components.blinksticklight.* -homeassistant.components.blinkt.* -homeassistant.components.blockchain.* -homeassistant.components.bloomsky.* -homeassistant.components.blueprint.* -homeassistant.components.bluesound.* -homeassistant.components.bluetooth_le_tracker.* -homeassistant.components.bluetooth_tracker.* -homeassistant.components.bme280.* -homeassistant.components.bme680.* -homeassistant.components.bmp280.* -homeassistant.components.bmw_connected_drive.* -homeassistant.components.braviatv.* -homeassistant.components.broadlink.* -homeassistant.components.brother.* -homeassistant.components.brottsplatskartan.* -homeassistant.components.browser.* -homeassistant.components.brunt.* -homeassistant.components.bsblan.* -homeassistant.components.bt_home_hub_5.* -homeassistant.components.bt_smarthub.* -homeassistant.components.buienradar.* -homeassistant.components.caldav.* -homeassistant.components.camera.* -homeassistant.components.canary.* -homeassistant.components.cast.* -homeassistant.components.cert_expiry.* -homeassistant.components.channels.* -homeassistant.components.circuit.* -homeassistant.components.cisco_ios.* -homeassistant.components.cisco_mobility_express.* -homeassistant.components.cisco_webex_teams.* -homeassistant.components.citybikes.* -homeassistant.components.clementine.* -homeassistant.components.clickatell.* -homeassistant.components.clicksend.* -homeassistant.components.clicksend_tts.* -homeassistant.components.climacell.* -homeassistant.components.climate.* -homeassistant.components.cloud.* -homeassistant.components.cloudflare.* -homeassistant.components.cmus.* -homeassistant.components.co2signal.* -homeassistant.components.coinbase.* -homeassistant.components.color_extractor.* -homeassistant.components.comed_hourly_pricing.* -homeassistant.components.comfoconnect.* -homeassistant.components.command_line.* -homeassistant.components.compensation.* -homeassistant.components.concord232.* -homeassistant.components.config.* -homeassistant.components.configurator.* -homeassistant.components.control4.* -homeassistant.components.conversation.* -homeassistant.components.coolmaster.* -homeassistant.components.coronavirus.* -homeassistant.components.counter.* -homeassistant.components.cppm_tracker.* -homeassistant.components.cpuspeed.* -homeassistant.components.cups.* -homeassistant.components.currencylayer.* -homeassistant.components.daikin.* -homeassistant.components.danfoss_air.* -homeassistant.components.darksky.* -homeassistant.components.datadog.* -homeassistant.components.ddwrt.* -homeassistant.components.debugpy.* -homeassistant.components.deconz.* -homeassistant.components.decora.* -homeassistant.components.decora_wifi.* -homeassistant.components.default_config.* -homeassistant.components.delijn.* -homeassistant.components.deluge.* -homeassistant.components.demo.* -homeassistant.components.denon.* -homeassistant.components.denonavr.* -homeassistant.components.deutsche_bahn.* -homeassistant.components.device_sun_light_trigger.* -homeassistant.components.device_tracker.* -homeassistant.components.devolo_home_control.* -homeassistant.components.dexcom.* -homeassistant.components.dhcp.* -homeassistant.components.dht.* -homeassistant.components.dialogflow.* -homeassistant.components.digital_ocean.* -homeassistant.components.digitalloggers.* -homeassistant.components.directv.* -homeassistant.components.discogs.* -homeassistant.components.discord.* -homeassistant.components.discovery.* -homeassistant.components.dlib_face_detect.* -homeassistant.components.dlib_face_identify.* -homeassistant.components.dlink.* -homeassistant.components.dlna_dmr.* -homeassistant.components.dnsip.* -homeassistant.components.dominos.* -homeassistant.components.doods.* -homeassistant.components.doorbird.* -homeassistant.components.dovado.* -homeassistant.components.downloader.* -homeassistant.components.dsmr.* -homeassistant.components.dsmr_reader.* -homeassistant.components.dte_energy_bridge.* -homeassistant.components.dublin_bus_transport.* -homeassistant.components.duckdns.* -homeassistant.components.dunehd.* -homeassistant.components.dwd_weather_warnings.* -homeassistant.components.dweet.* -homeassistant.components.dynalite.* -homeassistant.components.dyson.* -homeassistant.components.eafm.* -homeassistant.components.ebox.* -homeassistant.components.ebusd.* -homeassistant.components.ecoal_boiler.* -homeassistant.components.ecobee.* -homeassistant.components.econet.* -homeassistant.components.ecovacs.* -homeassistant.components.eddystone_temperature.* -homeassistant.components.edimax.* -homeassistant.components.edl21.* -homeassistant.components.ee_brightbox.* -homeassistant.components.efergy.* -homeassistant.components.egardia.* -homeassistant.components.eight_sleep.* -homeassistant.components.elgato.* -homeassistant.components.eliqonline.* -homeassistant.components.elkm1.* -homeassistant.components.elv.* -homeassistant.components.emby.* -homeassistant.components.emoncms.* -homeassistant.components.emoncms_history.* -homeassistant.components.emonitor.* -homeassistant.components.emulated_hue.* -homeassistant.components.emulated_kasa.* -homeassistant.components.emulated_roku.* -homeassistant.components.enigma2.* -homeassistant.components.enocean.* -homeassistant.components.enphase_envoy.* -homeassistant.components.entur_public_transport.* -homeassistant.components.environment_canada.* -homeassistant.components.envirophat.* -homeassistant.components.envisalink.* -homeassistant.components.ephember.* -homeassistant.components.epson.* -homeassistant.components.epsonworkforce.* -homeassistant.components.eq3btsmart.* -homeassistant.components.esphome.* -homeassistant.components.essent.* -homeassistant.components.etherscan.* -homeassistant.components.eufy.* -homeassistant.components.everlights.* -homeassistant.components.evohome.* -homeassistant.components.ezviz.* -homeassistant.components.faa_delays.* -homeassistant.components.facebook.* -homeassistant.components.facebox.* -homeassistant.components.fail2ban.* -homeassistant.components.familyhub.* -homeassistant.components.fan.* -homeassistant.components.fastdotcom.* -homeassistant.components.feedreader.* -homeassistant.components.ffmpeg.* -homeassistant.components.ffmpeg_motion.* -homeassistant.components.ffmpeg_noise.* -homeassistant.components.fibaro.* -homeassistant.components.fido.* -homeassistant.components.file.* -homeassistant.components.filesize.* -homeassistant.components.filter.* -homeassistant.components.fints.* -homeassistant.components.fireservicerota.* -homeassistant.components.firmata.* -homeassistant.components.fitbit.* -homeassistant.components.fixer.* -homeassistant.components.fleetgo.* -homeassistant.components.flexit.* -homeassistant.components.flic.* -homeassistant.components.flick_electric.* -homeassistant.components.flo.* -homeassistant.components.flock.* -homeassistant.components.flume.* -homeassistant.components.flunearyou.* -homeassistant.components.flux.* -homeassistant.components.flux_led.* -homeassistant.components.folder.* -homeassistant.components.folder_watcher.* -homeassistant.components.foobot.* -homeassistant.components.forked_daapd.* -homeassistant.components.fortios.* -homeassistant.components.foscam.* -homeassistant.components.foursquare.* -homeassistant.components.free_mobile.* -homeassistant.components.freebox.* -homeassistant.components.freedns.* -homeassistant.components.fritz.* -homeassistant.components.fritzbox.* -homeassistant.components.fritzbox_callmonitor.* -homeassistant.components.fritzbox_netmonitor.* -homeassistant.components.fronius.* -homeassistant.components.frontier_silicon.* -homeassistant.components.futurenow.* -homeassistant.components.garadget.* -homeassistant.components.garmin_connect.* -homeassistant.components.gc100.* -homeassistant.components.gdacs.* -homeassistant.components.generic.* -homeassistant.components.generic_thermostat.* -homeassistant.components.geniushub.* -homeassistant.components.geo_json_events.* -homeassistant.components.geo_rss_events.* -homeassistant.components.geofency.* -homeassistant.components.geonetnz_quakes.* -homeassistant.components.geonetnz_volcano.* -homeassistant.components.gios.* -homeassistant.components.github.* -homeassistant.components.gitlab_ci.* -homeassistant.components.gitter.* -homeassistant.components.glances.* -homeassistant.components.gntp.* -homeassistant.components.goalfeed.* -homeassistant.components.goalzero.* -homeassistant.components.gogogate2.* -homeassistant.components.google.* -homeassistant.components.google_assistant.* -homeassistant.components.google_cloud.* -homeassistant.components.google_domains.* -homeassistant.components.google_maps.* -homeassistant.components.google_pubsub.* -homeassistant.components.google_translate.* -homeassistant.components.google_travel_time.* -homeassistant.components.google_wifi.* -homeassistant.components.gpmdp.* -homeassistant.components.gpsd.* -homeassistant.components.gpslogger.* -homeassistant.components.graphite.* -homeassistant.components.gree.* -homeassistant.components.greeneye_monitor.* -homeassistant.components.greenwave.* -homeassistant.components.growatt_server.* -homeassistant.components.gstreamer.* -homeassistant.components.gtfs.* -homeassistant.components.guardian.* -homeassistant.components.habitica.* -homeassistant.components.hangouts.* -homeassistant.components.harman_kardon_avr.* -homeassistant.components.harmony.* -homeassistant.components.hassio.* -homeassistant.components.haveibeenpwned.* -homeassistant.components.hddtemp.* -homeassistant.components.hdmi_cec.* -homeassistant.components.heatmiser.* -homeassistant.components.heos.* -homeassistant.components.here_travel_time.* -homeassistant.components.hikvision.* -homeassistant.components.hikvisioncam.* -homeassistant.components.hisense_aehw4a1.* -homeassistant.components.history_stats.* -homeassistant.components.hitron_coda.* -homeassistant.components.hive.* -homeassistant.components.hlk_sw16.* -homeassistant.components.home_connect.* -homeassistant.components.home_plus_control.* -homeassistant.components.homeassistant.* -homeassistant.components.homekit.* -homeassistant.components.homekit_controller.* -homeassistant.components.homematic.* -homeassistant.components.homematicip_cloud.* -homeassistant.components.homeworks.* -homeassistant.components.honeywell.* -homeassistant.components.horizon.* -homeassistant.components.hp_ilo.* -homeassistant.components.html5.* -homeassistant.components.htu21d.* -homeassistant.components.huawei_router.* -homeassistant.components.hue.* -homeassistant.components.huisbaasje.* -homeassistant.components.humidifier.* -homeassistant.components.hunterdouglas_powerview.* -homeassistant.components.hvv_departures.* -homeassistant.components.hydrawise.* -homeassistant.components.ialarm.* -homeassistant.components.iammeter.* -homeassistant.components.iaqualink.* -homeassistant.components.icloud.* -homeassistant.components.idteck_prox.* -homeassistant.components.ifttt.* -homeassistant.components.iglo.* -homeassistant.components.ign_sismologia.* -homeassistant.components.ihc.* -homeassistant.components.image.* -homeassistant.components.imap.* -homeassistant.components.imap_email_content.* -homeassistant.components.incomfort.* -homeassistant.components.influxdb.* -homeassistant.components.input_boolean.* -homeassistant.components.input_datetime.* -homeassistant.components.input_number.* -homeassistant.components.input_select.* -homeassistant.components.input_text.* -homeassistant.components.insteon.* -homeassistant.components.intent.* -homeassistant.components.intent_script.* -homeassistant.components.intesishome.* -homeassistant.components.ios.* -homeassistant.components.iota.* -homeassistant.components.iperf3.* -homeassistant.components.ipma.* -homeassistant.components.ipp.* -homeassistant.components.iqvia.* -homeassistant.components.irish_rail_transport.* -homeassistant.components.islamic_prayer_times.* -homeassistant.components.iss.* -homeassistant.components.isy994.* -homeassistant.components.itach.* -homeassistant.components.itunes.* -homeassistant.components.izone.* -homeassistant.components.jewish_calendar.* -homeassistant.components.joaoapps_join.* -homeassistant.components.juicenet.* -homeassistant.components.kaiterra.* -homeassistant.components.kankun.* -homeassistant.components.keba.* -homeassistant.components.keenetic_ndms2.* -homeassistant.components.kef.* -homeassistant.components.keyboard.* -homeassistant.components.keyboard_remote.* -homeassistant.components.kira.* -homeassistant.components.kiwi.* -homeassistant.components.kmtronic.* -homeassistant.components.kodi.* -homeassistant.components.konnected.* -homeassistant.components.kostal_plenticore.* -homeassistant.components.kulersky.* -homeassistant.components.kwb.* -homeassistant.components.lacrosse.* -homeassistant.components.lametric.* -homeassistant.components.lannouncer.* -homeassistant.components.lastfm.* -homeassistant.components.launch_library.* -homeassistant.components.lcn.* -homeassistant.components.lg_netcast.* -homeassistant.components.lg_soundbar.* -homeassistant.components.life360.* -homeassistant.components.lifx.* -homeassistant.components.lifx_cloud.* -homeassistant.components.lifx_legacy.* -homeassistant.components.lightwave.* -homeassistant.components.limitlessled.* -homeassistant.components.linksys_smart.* -homeassistant.components.linode.* -homeassistant.components.linux_battery.* -homeassistant.components.lirc.* -homeassistant.components.litejet.* -homeassistant.components.litterrobot.* -homeassistant.components.llamalab_automate.* -homeassistant.components.local_file.* -homeassistant.components.local_ip.* -homeassistant.components.locative.* -homeassistant.components.logbook.* -homeassistant.components.logentries.* -homeassistant.components.logger.* -homeassistant.components.logi_circle.* -homeassistant.components.london_air.* -homeassistant.components.london_underground.* -homeassistant.components.loopenergy.* -homeassistant.components.lovelace.* -homeassistant.components.luci.* -homeassistant.components.luftdaten.* -homeassistant.components.lupusec.* -homeassistant.components.lutron.* -homeassistant.components.lutron_caseta.* -homeassistant.components.lw12wifi.* -homeassistant.components.lyft.* -homeassistant.components.lyric.* -homeassistant.components.magicseaweed.* -homeassistant.components.mailgun.* -homeassistant.components.manual.* -homeassistant.components.manual_mqtt.* -homeassistant.components.map.* -homeassistant.components.marytts.* -homeassistant.components.mastodon.* -homeassistant.components.matrix.* -homeassistant.components.maxcube.* -homeassistant.components.mazda.* -homeassistant.components.mcp23017.* -homeassistant.components.media_extractor.* -homeassistant.components.media_source.* -homeassistant.components.mediaroom.* -homeassistant.components.melcloud.* -homeassistant.components.melissa.* -homeassistant.components.meraki.* -homeassistant.components.message_bird.* -homeassistant.components.met.* -homeassistant.components.met_eireann.* -homeassistant.components.meteo_france.* -homeassistant.components.meteoalarm.* -homeassistant.components.metoffice.* -homeassistant.components.mfi.* -homeassistant.components.mhz19.* -homeassistant.components.microsoft.* -homeassistant.components.microsoft_face.* -homeassistant.components.microsoft_face_detect.* -homeassistant.components.microsoft_face_identify.* -homeassistant.components.miflora.* -homeassistant.components.mikrotik.* -homeassistant.components.mill.* -homeassistant.components.min_max.* -homeassistant.components.minecraft_server.* -homeassistant.components.minio.* -homeassistant.components.mitemp_bt.* -homeassistant.components.mjpeg.* -homeassistant.components.mobile_app.* -homeassistant.components.mochad.* -homeassistant.components.modbus.* -homeassistant.components.modem_callerid.* -homeassistant.components.mold_indicator.* -homeassistant.components.monoprice.* -homeassistant.components.moon.* -homeassistant.components.motion_blinds.* -homeassistant.components.motioneye.* -homeassistant.components.mpchc.* -homeassistant.components.mpd.* -homeassistant.components.mqtt.* -homeassistant.components.mqtt_eventstream.* -homeassistant.components.mqtt_json.* -homeassistant.components.mqtt_room.* -homeassistant.components.mqtt_statestream.* -homeassistant.components.msteams.* -homeassistant.components.mullvad.* -homeassistant.components.mvglive.* -homeassistant.components.my.* -homeassistant.components.mychevy.* -homeassistant.components.mycroft.* -homeassistant.components.myq.* -homeassistant.components.mysensors.* -homeassistant.components.mystrom.* -homeassistant.components.mythicbeastsdns.* -homeassistant.components.n26.* -homeassistant.components.nad.* -homeassistant.components.namecheapdns.* -homeassistant.components.nanoleaf.* -homeassistant.components.neato.* -homeassistant.components.nederlandse_spoorwegen.* -homeassistant.components.nello.* -homeassistant.components.ness_alarm.* -homeassistant.components.nest.* -homeassistant.components.netatmo.* -homeassistant.components.netdata.* -homeassistant.components.netgear.* -homeassistant.components.netgear_lte.* -homeassistant.components.netio.* -homeassistant.components.neurio_energy.* -homeassistant.components.nexia.* -homeassistant.components.nextbus.* -homeassistant.components.nextcloud.* -homeassistant.components.nfandroidtv.* -homeassistant.components.nightscout.* -homeassistant.components.niko_home_control.* -homeassistant.components.nilu.* -homeassistant.components.nissan_leaf.* -homeassistant.components.nmap_tracker.* -homeassistant.components.nmbs.* -homeassistant.components.no_ip.* -homeassistant.components.noaa_tides.* -homeassistant.components.norway_air.* -homeassistant.components.notify_events.* -homeassistant.components.notion.* -homeassistant.components.nsw_fuel_station.* -homeassistant.components.nsw_rural_fire_service_feed.* -homeassistant.components.nuheat.* -homeassistant.components.nuki.* -homeassistant.components.numato.* -homeassistant.components.nut.* -homeassistant.components.nws.* -homeassistant.components.nx584.* -homeassistant.components.nzbget.* -homeassistant.components.oasa_telematics.* -homeassistant.components.obihai.* -homeassistant.components.octoprint.* -homeassistant.components.oem.* -homeassistant.components.ohmconnect.* -homeassistant.components.ombi.* -homeassistant.components.omnilogic.* -homeassistant.components.onboarding.* -homeassistant.components.ondilo_ico.* -homeassistant.components.onewire.* -homeassistant.components.onkyo.* -homeassistant.components.onvif.* -homeassistant.components.openalpr_cloud.* -homeassistant.components.openalpr_local.* -homeassistant.components.opencv.* -homeassistant.components.openerz.* -homeassistant.components.openevse.* -homeassistant.components.openexchangerates.* -homeassistant.components.opengarage.* -homeassistant.components.openhardwaremonitor.* -homeassistant.components.openhome.* -homeassistant.components.opensensemap.* -homeassistant.components.opensky.* -homeassistant.components.opentherm_gw.* -homeassistant.components.openuv.* -homeassistant.components.openweathermap.* -homeassistant.components.opnsense.* -homeassistant.components.opple.* -homeassistant.components.orangepi_gpio.* -homeassistant.components.oru.* -homeassistant.components.orvibo.* -homeassistant.components.osramlightify.* -homeassistant.components.otp.* -homeassistant.components.ovo_energy.* -homeassistant.components.owntracks.* -homeassistant.components.ozw.* -homeassistant.components.panasonic_bluray.* -homeassistant.components.panasonic_viera.* -homeassistant.components.pandora.* -homeassistant.components.panel_custom.* -homeassistant.components.panel_iframe.* -homeassistant.components.pcal9535a.* -homeassistant.components.pencom.* -homeassistant.components.person.* -homeassistant.components.philips_js.* -homeassistant.components.pi4ioe5v9xxxx.* -homeassistant.components.pi_hole.* -homeassistant.components.picnic.* -homeassistant.components.picotts.* -homeassistant.components.piglow.* -homeassistant.components.pilight.* -homeassistant.components.ping.* -homeassistant.components.pioneer.* -homeassistant.components.pjlink.* -homeassistant.components.plaato.* -homeassistant.components.plant.* -homeassistant.components.plex.* -homeassistant.components.plugwise.* -homeassistant.components.plum_lightpad.* -homeassistant.components.pocketcasts.* -homeassistant.components.point.* -homeassistant.components.poolsense.* -homeassistant.components.powerwall.* -homeassistant.components.profiler.* -homeassistant.components.progettihwsw.* -homeassistant.components.proliphix.* -homeassistant.components.prometheus.* -homeassistant.components.prowl.* -homeassistant.components.proxmoxve.* -homeassistant.components.proxy.* -homeassistant.components.ps4.* -homeassistant.components.pulseaudio_loopback.* -homeassistant.components.push.* -homeassistant.components.pushbullet.* -homeassistant.components.pushover.* -homeassistant.components.pushsafer.* -homeassistant.components.pvoutput.* -homeassistant.components.pvpc_hourly_pricing.* -homeassistant.components.pyload.* -homeassistant.components.python_script.* -homeassistant.components.qbittorrent.* -homeassistant.components.qld_bushfire.* -homeassistant.components.qnap.* -homeassistant.components.qrcode.* -homeassistant.components.quantum_gateway.* -homeassistant.components.qvr_pro.* -homeassistant.components.qwikswitch.* -homeassistant.components.rachio.* -homeassistant.components.radarr.* -homeassistant.components.radiotherm.* -homeassistant.components.rainbird.* -homeassistant.components.raincloud.* -homeassistant.components.rainforest_eagle.* -homeassistant.components.rainmachine.* -homeassistant.components.random.* -homeassistant.components.raspihats.* -homeassistant.components.raspyrfm.* -homeassistant.components.recollect_waste.* -homeassistant.components.recorder.* -homeassistant.components.recswitch.* -homeassistant.components.reddit.* -homeassistant.components.rejseplanen.* -homeassistant.components.remember_the_milk.* -homeassistant.components.remote_rpi_gpio.* -homeassistant.components.repetier.* -homeassistant.components.rest.* -homeassistant.components.rest_command.* -homeassistant.components.rflink.* -homeassistant.components.rfxtrx.* -homeassistant.components.ring.* -homeassistant.components.ripple.* -homeassistant.components.risco.* -homeassistant.components.rituals_perfume_genie.* -homeassistant.components.rmvtransport.* -homeassistant.components.rocketchat.* -homeassistant.components.roku.* -homeassistant.components.roomba.* -homeassistant.components.roon.* -homeassistant.components.route53.* -homeassistant.components.rova.* -homeassistant.components.rpi_camera.* -homeassistant.components.rpi_gpio.* -homeassistant.components.rpi_gpio_pwm.* -homeassistant.components.rpi_pfio.* -homeassistant.components.rpi_power.* -homeassistant.components.rpi_rf.* -homeassistant.components.rss_feed_template.* -homeassistant.components.rtorrent.* -homeassistant.components.ruckus_unleashed.* -homeassistant.components.russound_rio.* -homeassistant.components.russound_rnet.* -homeassistant.components.sabnzbd.* -homeassistant.components.safe_mode.* -homeassistant.components.saj.* -homeassistant.components.samsungtv.* -homeassistant.components.satel_integra.* -homeassistant.components.schluter.* -homeassistant.components.scrape.* -homeassistant.components.screenlogic.* -homeassistant.components.script.* -homeassistant.components.scsgate.* -homeassistant.components.search.* -homeassistant.components.season.* -homeassistant.components.sendgrid.* -homeassistant.components.sense.* -homeassistant.components.sensehat.* -homeassistant.components.sensibo.* -homeassistant.components.sentry.* -homeassistant.components.serial.* -homeassistant.components.serial_pm.* -homeassistant.components.sesame.* -homeassistant.components.seven_segments.* -homeassistant.components.seventeentrack.* -homeassistant.components.sharkiq.* -homeassistant.components.shell_command.* -homeassistant.components.shelly.* -homeassistant.components.shiftr.* -homeassistant.components.shodan.* -homeassistant.components.shopping_list.* -homeassistant.components.sht31.* -homeassistant.components.sigfox.* -homeassistant.components.sighthound.* -homeassistant.components.signal_messenger.* -homeassistant.components.simplepush.* -homeassistant.components.simplisafe.* -homeassistant.components.simulated.* -homeassistant.components.sinch.* -homeassistant.components.sisyphus.* -homeassistant.components.sky_hub.* -homeassistant.components.skybeacon.* -homeassistant.components.skybell.* -homeassistant.components.sleepiq.* -homeassistant.components.slide.* -homeassistant.components.sma.* -homeassistant.components.smappee.* -homeassistant.components.smart_meter_texas.* -homeassistant.components.smarthab.* -homeassistant.components.smartthings.* -homeassistant.components.smarttub.* -homeassistant.components.smarty.* -homeassistant.components.smhi.* -homeassistant.components.sms.* -homeassistant.components.smtp.* -homeassistant.components.snapcast.* -homeassistant.components.snips.* -homeassistant.components.snmp.* -homeassistant.components.sochain.* -homeassistant.components.solaredge.* -homeassistant.components.solaredge_local.* -homeassistant.components.solarlog.* -homeassistant.components.solax.* -homeassistant.components.soma.* -homeassistant.components.somfy.* -homeassistant.components.somfy_mylink.* -homeassistant.components.sonarr.* -homeassistant.components.songpal.* -homeassistant.components.sonos.* -homeassistant.components.sony_projector.* -homeassistant.components.soundtouch.* -homeassistant.components.spaceapi.* -homeassistant.components.spc.* -homeassistant.components.speedtestdotnet.* -homeassistant.components.spider.* -homeassistant.components.splunk.* -homeassistant.components.spotcrime.* -homeassistant.components.spotify.* -homeassistant.components.sql.* -homeassistant.components.squeezebox.* -homeassistant.components.srp_energy.* -homeassistant.components.ssdp.* -homeassistant.components.starline.* -homeassistant.components.starlingbank.* -homeassistant.components.startca.* -homeassistant.components.statistics.* -homeassistant.components.statsd.* -homeassistant.components.steam_online.* -homeassistant.components.stiebel_eltron.* -homeassistant.components.stookalert.* -homeassistant.components.stream.* -homeassistant.components.streamlabswater.* -homeassistant.components.stt.* -homeassistant.components.subaru.* -homeassistant.components.suez_water.* -homeassistant.components.supervisord.* -homeassistant.components.supla.* -homeassistant.components.surepetcare.* -homeassistant.components.swiss_hydrological_data.* -homeassistant.components.swiss_public_transport.* -homeassistant.components.swisscom.* -homeassistant.components.switchbot.* -homeassistant.components.switcher_kis.* -homeassistant.components.switchmate.* -homeassistant.components.syncthru.* -homeassistant.components.synology_chat.* -homeassistant.components.synology_dsm.* -homeassistant.components.synology_srm.* -homeassistant.components.syslog.* -homeassistant.components.system_health.* -homeassistant.components.system_log.* -homeassistant.components.tado.* -homeassistant.components.tag.* -homeassistant.components.tahoma.* -homeassistant.components.tank_utility.* -homeassistant.components.tankerkoenig.* -homeassistant.components.tapsaff.* -homeassistant.components.tasmota.* -homeassistant.components.tautulli.* -homeassistant.components.tcp.* -homeassistant.components.ted5000.* -homeassistant.components.telegram.* -homeassistant.components.telegram_bot.* -homeassistant.components.tellduslive.* -homeassistant.components.tellstick.* -homeassistant.components.telnet.* -homeassistant.components.temper.* -homeassistant.components.template.* -homeassistant.components.tensorflow.* -homeassistant.components.tesla.* -homeassistant.components.tfiac.* -homeassistant.components.thermoworks_smoke.* -homeassistant.components.thethingsnetwork.* -homeassistant.components.thingspeak.* -homeassistant.components.thinkingcleaner.* -homeassistant.components.thomson.* -homeassistant.components.threshold.* -homeassistant.components.tibber.* -homeassistant.components.tikteck.* -homeassistant.components.tile.* -homeassistant.components.time_date.* -homeassistant.components.timer.* -homeassistant.components.tmb.* -homeassistant.components.tod.* -homeassistant.components.todoist.* -homeassistant.components.tof.* -homeassistant.components.tomato.* -homeassistant.components.toon.* -homeassistant.components.torque.* -homeassistant.components.totalconnect.* -homeassistant.components.touchline.* -homeassistant.components.tplink.* -homeassistant.components.tplink_lte.* -homeassistant.components.traccar.* -homeassistant.components.trace.* -homeassistant.components.trackr.* -homeassistant.components.tradfri.* -homeassistant.components.trafikverket_train.* -homeassistant.components.trafikverket_weatherstation.* -homeassistant.components.transmission.* -homeassistant.components.transport_nsw.* -homeassistant.components.travisci.* -homeassistant.components.trend.* -homeassistant.components.tuya.* -homeassistant.components.twentemilieu.* -homeassistant.components.twilio.* -homeassistant.components.twilio_call.* -homeassistant.components.twilio_sms.* -homeassistant.components.twinkly.* -homeassistant.components.twitch.* -homeassistant.components.twitter.* -homeassistant.components.ubus.* -homeassistant.components.ue_smart_radio.* -homeassistant.components.uk_transport.* -homeassistant.components.unifi.* -homeassistant.components.unifi_direct.* -homeassistant.components.unifiled.* -homeassistant.components.universal.* -homeassistant.components.upb.* -homeassistant.components.upc_connect.* -homeassistant.components.upcloud.* -homeassistant.components.updater.* -homeassistant.components.upnp.* -homeassistant.components.uptime.* -homeassistant.components.uptimerobot.* -homeassistant.components.uscis.* -homeassistant.components.usgs_earthquakes_feed.* -homeassistant.components.utility_meter.* -homeassistant.components.uvc.* -homeassistant.components.vallox.* -homeassistant.components.vasttrafik.* -homeassistant.components.velbus.* -homeassistant.components.velux.* -homeassistant.components.venstar.* -homeassistant.components.vera.* -homeassistant.components.verisure.* -homeassistant.components.versasense.* -homeassistant.components.version.* -homeassistant.components.vesync.* -homeassistant.components.viaggiatreno.* -homeassistant.components.vicare.* -homeassistant.components.vilfo.* -homeassistant.components.vivotek.* -homeassistant.components.vizio.* -homeassistant.components.vlc.* -homeassistant.components.vlc_telnet.* -homeassistant.components.voicerss.* -homeassistant.components.volkszaehler.* -homeassistant.components.volumio.* -homeassistant.components.volvooncall.* -homeassistant.components.vultr.* -homeassistant.components.w800rf32.* -homeassistant.components.wake_on_lan.* -homeassistant.components.waqi.* -homeassistant.components.waterfurnace.* -homeassistant.components.watson_iot.* -homeassistant.components.watson_tts.* -homeassistant.components.waze_travel_time.* -homeassistant.components.webhook.* -homeassistant.components.webostv.* -homeassistant.components.wemo.* -homeassistant.components.whois.* -homeassistant.components.wiffi.* -homeassistant.components.wilight.* -homeassistant.components.wink.* -homeassistant.components.wirelesstag.* -homeassistant.components.withings.* -homeassistant.components.wled.* -homeassistant.components.wolflink.* -homeassistant.components.workday.* -homeassistant.components.worldclock.* -homeassistant.components.worldtidesinfo.* -homeassistant.components.worxlandroid.* -homeassistant.components.wsdot.* -homeassistant.components.wunderground.* -homeassistant.components.x10.* -homeassistant.components.xbee.* -homeassistant.components.xbox.* -homeassistant.components.xbox_live.* -homeassistant.components.xeoma.* -homeassistant.components.xiaomi.* -homeassistant.components.xiaomi_aqara.* -homeassistant.components.xiaomi_miio.* -homeassistant.components.xiaomi_tv.* -homeassistant.components.xmpp.* -homeassistant.components.xs1.* -homeassistant.components.yale_smart_alarm.* -homeassistant.components.yamaha.* -homeassistant.components.yamaha_musiccast.* -homeassistant.components.yandex_transport.* -homeassistant.components.yandextts.* -homeassistant.components.yeelight.* -homeassistant.components.yeelightsunflower.* -homeassistant.components.yi.* -homeassistant.components.zabbix.* -homeassistant.components.zamg.* -homeassistant.components.zengge.* -homeassistant.components.zerproc.* -homeassistant.components.zestimate.* -homeassistant.components.zha.* -homeassistant.components.zhong_hong.* -homeassistant.components.ziggo_mediabox_xl.* -homeassistant.components.zodiac.* -homeassistant.components.zoneminder.* -homeassistant.components.zwave.* diff --git a/.strict-typing b/.strict-typing new file mode 100644 index 0000000000000..ab150056a85e8 --- /dev/null +++ b/.strict-typing @@ -0,0 +1,47 @@ +# Used by hassfest for generating mypy.ini. +# If component is fully covered with type annotations, please add it here +# to enable strict mypy checks. + +homeassistant.components +homeassistant.components.automation.* +homeassistant.components.binary_sensor.* +homeassistant.components.bond.* +homeassistant.components.calendar.* +homeassistant.components.cover.* +homeassistant.components.device_automation.* +homeassistant.components.frontend.* +homeassistant.components.geo_location.* +homeassistant.components.group.* +homeassistant.components.history.* +homeassistant.components.http.* +homeassistant.components.huawei_lte.* +homeassistant.components.hyperion.* +homeassistant.components.image_processing.* +homeassistant.components.integration.* +homeassistant.components.knx.* +homeassistant.components.light.* +homeassistant.components.lock.* +homeassistant.components.mailbox.* +homeassistant.components.media_player.* +homeassistant.components.notify.* +homeassistant.components.number.* +homeassistant.components.persistent_notification.* +homeassistant.components.proximity.* +homeassistant.components.recorder.purge +homeassistant.components.recorder.repack +homeassistant.components.remote.* +homeassistant.components.scene.* +homeassistant.components.sensor.* +homeassistant.components.slack.* +homeassistant.components.sonos.media_player +homeassistant.components.sun.* +homeassistant.components.switch.* +homeassistant.components.systemmonitor.* +homeassistant.components.tts.* +homeassistant.components.vacuum.* +homeassistant.components.water_heater.* +homeassistant.components.weather.* +homeassistant.components.websocket_api.* +homeassistant.components.zeroconf.* +homeassistant.components.zone.* +homeassistant.components.zwave_js.* diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py index 31e937a0fe7bc..2a062109eaf2e 100644 --- a/homeassistant/components/__init__.py +++ b/homeassistant/components/__init__.py @@ -7,16 +7,16 @@ format ".". - Each component should publish services only under its own domain. """ -import logging +from __future__ import annotations -from homeassistant.core import split_entity_id +import logging -# mypy: allow-untyped-defs +from homeassistant.core import HomeAssistant, split_entity_id _LOGGER = logging.getLogger(__name__) -def is_on(hass, entity_id=None): +def is_on(hass: HomeAssistant, entity_id: str | None = None) -> bool: """Load up the module to call the is_on method. If there is no entity id given we will check all. diff --git a/mypy.ini b/mypy.ini index f80dbf0b75e34..d07714ae3ed54 100644 --- a/mypy.ini +++ b/mypy.ini @@ -22,7 +22,7 @@ warn_return_any = true warn_unreachable = true warn_unused_ignores = true -[mypy-homeassistant.components.abode.*,homeassistant.components.accuweather.*,homeassistant.components.acer_projector.*,homeassistant.components.acmeda.*,homeassistant.components.actiontec.*,homeassistant.components.adguard.*,homeassistant.components.ads.*,homeassistant.components.advantage_air.*,homeassistant.components.aemet.*,homeassistant.components.aftership.*,homeassistant.components.agent_dvr.*,homeassistant.components.air_quality.*,homeassistant.components.airly.*,homeassistant.components.airnow.*,homeassistant.components.airvisual.*,homeassistant.components.aladdin_connect.*,homeassistant.components.alarm_control_panel.*,homeassistant.components.alarmdecoder.*,homeassistant.components.alert.*,homeassistant.components.alexa.*,homeassistant.components.almond.*,homeassistant.components.alpha_vantage.*,homeassistant.components.amazon_polly.*,homeassistant.components.ambiclimate.*,homeassistant.components.ambient_station.*,homeassistant.components.amcrest.*,homeassistant.components.ampio.*,homeassistant.components.analytics.*,homeassistant.components.android_ip_webcam.*,homeassistant.components.androidtv.*,homeassistant.components.anel_pwrctrl.*,homeassistant.components.anthemav.*,homeassistant.components.apache_kafka.*,homeassistant.components.apcupsd.*,homeassistant.components.api.*,homeassistant.components.apns.*,homeassistant.components.apple_tv.*,homeassistant.components.apprise.*,homeassistant.components.aprs.*,homeassistant.components.aqualogic.*,homeassistant.components.aquostv.*,homeassistant.components.arcam_fmj.*,homeassistant.components.arduino.*,homeassistant.components.arest.*,homeassistant.components.arlo.*,homeassistant.components.arris_tg2492lg.*,homeassistant.components.aruba.*,homeassistant.components.arwn.*,homeassistant.components.asterisk_cdr.*,homeassistant.components.asterisk_mbox.*,homeassistant.components.asuswrt.*,homeassistant.components.atag.*,homeassistant.components.aten_pe.*,homeassistant.components.atome.*,homeassistant.components.august.*,homeassistant.components.aurora.*,homeassistant.components.aurora_abb_powerone.*,homeassistant.components.auth.*,homeassistant.components.avea.*,homeassistant.components.avion.*,homeassistant.components.awair.*,homeassistant.components.aws.*,homeassistant.components.axis.*,homeassistant.components.azure_devops.*,homeassistant.components.azure_event_hub.*,homeassistant.components.azure_service_bus.*,homeassistant.components.baidu.*,homeassistant.components.bayesian.*,homeassistant.components.bbb_gpio.*,homeassistant.components.bbox.*,homeassistant.components.beewi_smartclim.*,homeassistant.components.bh1750.*,homeassistant.components.bitcoin.*,homeassistant.components.bizkaibus.*,homeassistant.components.blackbird.*,homeassistant.components.blebox.*,homeassistant.components.blink.*,homeassistant.components.blinksticklight.*,homeassistant.components.blinkt.*,homeassistant.components.blockchain.*,homeassistant.components.bloomsky.*,homeassistant.components.blueprint.*,homeassistant.components.bluesound.*,homeassistant.components.bluetooth_le_tracker.*,homeassistant.components.bluetooth_tracker.*,homeassistant.components.bme280.*,homeassistant.components.bme680.*,homeassistant.components.bmp280.*,homeassistant.components.bmw_connected_drive.*,homeassistant.components.braviatv.*,homeassistant.components.broadlink.*,homeassistant.components.brother.*,homeassistant.components.brottsplatskartan.*,homeassistant.components.browser.*,homeassistant.components.brunt.*,homeassistant.components.bsblan.*,homeassistant.components.bt_home_hub_5.*,homeassistant.components.bt_smarthub.*,homeassistant.components.buienradar.*,homeassistant.components.caldav.*,homeassistant.components.camera.*,homeassistant.components.canary.*,homeassistant.components.cast.*,homeassistant.components.cert_expiry.*,homeassistant.components.channels.*,homeassistant.components.circuit.*,homeassistant.components.cisco_ios.*,homeassistant.components.cisco_mobility_express.*,homeassistant.components.cisco_webex_teams.*,homeassistant.components.citybikes.*,homeassistant.components.clementine.*,homeassistant.components.clickatell.*,homeassistant.components.clicksend.*,homeassistant.components.clicksend_tts.*,homeassistant.components.climacell.*,homeassistant.components.climate.*,homeassistant.components.cloud.*,homeassistant.components.cloudflare.*,homeassistant.components.cmus.*,homeassistant.components.co2signal.*,homeassistant.components.coinbase.*,homeassistant.components.color_extractor.*,homeassistant.components.comed_hourly_pricing.*,homeassistant.components.comfoconnect.*,homeassistant.components.command_line.*,homeassistant.components.compensation.*,homeassistant.components.concord232.*,homeassistant.components.config.*,homeassistant.components.configurator.*,homeassistant.components.control4.*,homeassistant.components.conversation.*,homeassistant.components.coolmaster.*,homeassistant.components.coronavirus.*,homeassistant.components.counter.*,homeassistant.components.cppm_tracker.*,homeassistant.components.cpuspeed.*,homeassistant.components.cups.*,homeassistant.components.currencylayer.*,homeassistant.components.daikin.*,homeassistant.components.danfoss_air.*,homeassistant.components.darksky.*,homeassistant.components.datadog.*,homeassistant.components.ddwrt.*,homeassistant.components.debugpy.*,homeassistant.components.deconz.*,homeassistant.components.decora.*,homeassistant.components.decora_wifi.*,homeassistant.components.default_config.*,homeassistant.components.delijn.*,homeassistant.components.deluge.*,homeassistant.components.demo.*,homeassistant.components.denon.*,homeassistant.components.denonavr.*,homeassistant.components.deutsche_bahn.*,homeassistant.components.device_sun_light_trigger.*,homeassistant.components.device_tracker.*,homeassistant.components.devolo_home_control.*,homeassistant.components.dexcom.*,homeassistant.components.dhcp.*,homeassistant.components.dht.*,homeassistant.components.dialogflow.*,homeassistant.components.digital_ocean.*,homeassistant.components.digitalloggers.*,homeassistant.components.directv.*,homeassistant.components.discogs.*,homeassistant.components.discord.*,homeassistant.components.discovery.*,homeassistant.components.dlib_face_detect.*,homeassistant.components.dlib_face_identify.*,homeassistant.components.dlink.*,homeassistant.components.dlna_dmr.*,homeassistant.components.dnsip.*,homeassistant.components.dominos.*,homeassistant.components.doods.*,homeassistant.components.doorbird.*,homeassistant.components.dovado.*,homeassistant.components.downloader.*,homeassistant.components.dsmr.*,homeassistant.components.dsmr_reader.*,homeassistant.components.dte_energy_bridge.*,homeassistant.components.dublin_bus_transport.*,homeassistant.components.duckdns.*,homeassistant.components.dunehd.*,homeassistant.components.dwd_weather_warnings.*,homeassistant.components.dweet.*,homeassistant.components.dynalite.*,homeassistant.components.dyson.*,homeassistant.components.eafm.*,homeassistant.components.ebox.*,homeassistant.components.ebusd.*,homeassistant.components.ecoal_boiler.*,homeassistant.components.ecobee.*,homeassistant.components.econet.*,homeassistant.components.ecovacs.*,homeassistant.components.eddystone_temperature.*,homeassistant.components.edimax.*,homeassistant.components.edl21.*,homeassistant.components.ee_brightbox.*,homeassistant.components.efergy.*,homeassistant.components.egardia.*,homeassistant.components.eight_sleep.*,homeassistant.components.elgato.*,homeassistant.components.eliqonline.*,homeassistant.components.elkm1.*,homeassistant.components.elv.*,homeassistant.components.emby.*,homeassistant.components.emoncms.*,homeassistant.components.emoncms_history.*,homeassistant.components.emonitor.*,homeassistant.components.emulated_hue.*,homeassistant.components.emulated_kasa.*,homeassistant.components.emulated_roku.*,homeassistant.components.enigma2.*,homeassistant.components.enocean.*,homeassistant.components.enphase_envoy.*,homeassistant.components.entur_public_transport.*,homeassistant.components.environment_canada.*,homeassistant.components.envirophat.*,homeassistant.components.envisalink.*,homeassistant.components.ephember.*,homeassistant.components.epson.*,homeassistant.components.epsonworkforce.*,homeassistant.components.eq3btsmart.*,homeassistant.components.esphome.*,homeassistant.components.essent.*,homeassistant.components.etherscan.*,homeassistant.components.eufy.*,homeassistant.components.everlights.*,homeassistant.components.evohome.*,homeassistant.components.ezviz.*,homeassistant.components.faa_delays.*,homeassistant.components.facebook.*,homeassistant.components.facebox.*,homeassistant.components.fail2ban.*,homeassistant.components.familyhub.*,homeassistant.components.fan.*,homeassistant.components.fastdotcom.*,homeassistant.components.feedreader.*,homeassistant.components.ffmpeg.*,homeassistant.components.ffmpeg_motion.*,homeassistant.components.ffmpeg_noise.*,homeassistant.components.fibaro.*,homeassistant.components.fido.*,homeassistant.components.file.*,homeassistant.components.filesize.*,homeassistant.components.filter.*,homeassistant.components.fints.*,homeassistant.components.fireservicerota.*,homeassistant.components.firmata.*,homeassistant.components.fitbit.*,homeassistant.components.fixer.*,homeassistant.components.fleetgo.*,homeassistant.components.flexit.*,homeassistant.components.flic.*,homeassistant.components.flick_electric.*,homeassistant.components.flo.*,homeassistant.components.flock.*,homeassistant.components.flume.*,homeassistant.components.flunearyou.*,homeassistant.components.flux.*,homeassistant.components.flux_led.*,homeassistant.components.folder.*,homeassistant.components.folder_watcher.*,homeassistant.components.foobot.*,homeassistant.components.forked_daapd.*,homeassistant.components.fortios.*,homeassistant.components.foscam.*,homeassistant.components.foursquare.*,homeassistant.components.free_mobile.*,homeassistant.components.freebox.*,homeassistant.components.freedns.*,homeassistant.components.fritz.*,homeassistant.components.fritzbox.*,homeassistant.components.fritzbox_callmonitor.*,homeassistant.components.fritzbox_netmonitor.*,homeassistant.components.fronius.*,homeassistant.components.frontier_silicon.*,homeassistant.components.futurenow.*,homeassistant.components.garadget.*,homeassistant.components.garmin_connect.*,homeassistant.components.gc100.*,homeassistant.components.gdacs.*,homeassistant.components.generic.*,homeassistant.components.generic_thermostat.*,homeassistant.components.geniushub.*,homeassistant.components.geo_json_events.*,homeassistant.components.geo_rss_events.*,homeassistant.components.geofency.*,homeassistant.components.geonetnz_quakes.*,homeassistant.components.geonetnz_volcano.*,homeassistant.components.gios.*,homeassistant.components.github.*,homeassistant.components.gitlab_ci.*,homeassistant.components.gitter.*,homeassistant.components.glances.*,homeassistant.components.gntp.*,homeassistant.components.goalfeed.*,homeassistant.components.goalzero.*,homeassistant.components.gogogate2.*,homeassistant.components.google.*,homeassistant.components.google_assistant.*,homeassistant.components.google_cloud.*,homeassistant.components.google_domains.*,homeassistant.components.google_maps.*,homeassistant.components.google_pubsub.*,homeassistant.components.google_translate.*,homeassistant.components.google_travel_time.*,homeassistant.components.google_wifi.*,homeassistant.components.gpmdp.*,homeassistant.components.gpsd.*,homeassistant.components.gpslogger.*,homeassistant.components.graphite.*,homeassistant.components.gree.*,homeassistant.components.greeneye_monitor.*,homeassistant.components.greenwave.*,homeassistant.components.growatt_server.*,homeassistant.components.gstreamer.*,homeassistant.components.gtfs.*,homeassistant.components.guardian.*,homeassistant.components.habitica.*,homeassistant.components.hangouts.*,homeassistant.components.harman_kardon_avr.*,homeassistant.components.harmony.*,homeassistant.components.hassio.*,homeassistant.components.haveibeenpwned.*,homeassistant.components.hddtemp.*,homeassistant.components.hdmi_cec.*,homeassistant.components.heatmiser.*,homeassistant.components.heos.*,homeassistant.components.here_travel_time.*,homeassistant.components.hikvision.*,homeassistant.components.hikvisioncam.*,homeassistant.components.hisense_aehw4a1.*,homeassistant.components.history_stats.*,homeassistant.components.hitron_coda.*,homeassistant.components.hive.*,homeassistant.components.hlk_sw16.*,homeassistant.components.home_connect.*,homeassistant.components.home_plus_control.*,homeassistant.components.homeassistant.*,homeassistant.components.homekit.*,homeassistant.components.homekit_controller.*,homeassistant.components.homematic.*,homeassistant.components.homematicip_cloud.*,homeassistant.components.homeworks.*,homeassistant.components.honeywell.*,homeassistant.components.horizon.*,homeassistant.components.hp_ilo.*,homeassistant.components.html5.*,homeassistant.components.htu21d.*,homeassistant.components.huawei_router.*,homeassistant.components.hue.*,homeassistant.components.huisbaasje.*,homeassistant.components.humidifier.*,homeassistant.components.hunterdouglas_powerview.*,homeassistant.components.hvv_departures.*,homeassistant.components.hydrawise.*,homeassistant.components.ialarm.*,homeassistant.components.iammeter.*,homeassistant.components.iaqualink.*,homeassistant.components.icloud.*,homeassistant.components.idteck_prox.*,homeassistant.components.ifttt.*,homeassistant.components.iglo.*,homeassistant.components.ign_sismologia.*,homeassistant.components.ihc.*,homeassistant.components.image.*,homeassistant.components.imap.*,homeassistant.components.imap_email_content.*,homeassistant.components.incomfort.*,homeassistant.components.influxdb.*,homeassistant.components.input_boolean.*,homeassistant.components.input_datetime.*,homeassistant.components.input_number.*,homeassistant.components.input_select.*,homeassistant.components.input_text.*,homeassistant.components.insteon.*,homeassistant.components.intent.*,homeassistant.components.intent_script.*,homeassistant.components.intesishome.*,homeassistant.components.ios.*,homeassistant.components.iota.*,homeassistant.components.iperf3.*,homeassistant.components.ipma.*,homeassistant.components.ipp.*,homeassistant.components.iqvia.*,homeassistant.components.irish_rail_transport.*,homeassistant.components.islamic_prayer_times.*,homeassistant.components.iss.*,homeassistant.components.isy994.*,homeassistant.components.itach.*,homeassistant.components.itunes.*,homeassistant.components.izone.*,homeassistant.components.jewish_calendar.*,homeassistant.components.joaoapps_join.*,homeassistant.components.juicenet.*,homeassistant.components.kaiterra.*,homeassistant.components.kankun.*,homeassistant.components.keba.*,homeassistant.components.keenetic_ndms2.*,homeassistant.components.kef.*,homeassistant.components.keyboard.*,homeassistant.components.keyboard_remote.*,homeassistant.components.kira.*,homeassistant.components.kiwi.*,homeassistant.components.kmtronic.*,homeassistant.components.kodi.*,homeassistant.components.konnected.*,homeassistant.components.kostal_plenticore.*,homeassistant.components.kulersky.*,homeassistant.components.kwb.*,homeassistant.components.lacrosse.*,homeassistant.components.lametric.*,homeassistant.components.lannouncer.*,homeassistant.components.lastfm.*,homeassistant.components.launch_library.*,homeassistant.components.lcn.*,homeassistant.components.lg_netcast.*,homeassistant.components.lg_soundbar.*,homeassistant.components.life360.*,homeassistant.components.lifx.*,homeassistant.components.lifx_cloud.*,homeassistant.components.lifx_legacy.*,homeassistant.components.lightwave.*,homeassistant.components.limitlessled.*,homeassistant.components.linksys_smart.*,homeassistant.components.linode.*,homeassistant.components.linux_battery.*,homeassistant.components.lirc.*,homeassistant.components.litejet.*,homeassistant.components.litterrobot.*,homeassistant.components.llamalab_automate.*,homeassistant.components.local_file.*,homeassistant.components.local_ip.*,homeassistant.components.locative.*,homeassistant.components.logbook.*,homeassistant.components.logentries.*,homeassistant.components.logger.*,homeassistant.components.logi_circle.*,homeassistant.components.london_air.*,homeassistant.components.london_underground.*,homeassistant.components.loopenergy.*,homeassistant.components.lovelace.*,homeassistant.components.luci.*,homeassistant.components.luftdaten.*,homeassistant.components.lupusec.*,homeassistant.components.lutron.*,homeassistant.components.lutron_caseta.*,homeassistant.components.lw12wifi.*,homeassistant.components.lyft.*,homeassistant.components.lyric.*,homeassistant.components.magicseaweed.*,homeassistant.components.mailgun.*,homeassistant.components.manual.*,homeassistant.components.manual_mqtt.*,homeassistant.components.map.*,homeassistant.components.marytts.*,homeassistant.components.mastodon.*,homeassistant.components.matrix.*,homeassistant.components.maxcube.*,homeassistant.components.mazda.*,homeassistant.components.mcp23017.*,homeassistant.components.media_extractor.*,homeassistant.components.media_source.*,homeassistant.components.mediaroom.*,homeassistant.components.melcloud.*,homeassistant.components.melissa.*,homeassistant.components.meraki.*,homeassistant.components.message_bird.*,homeassistant.components.met.*,homeassistant.components.met_eireann.*,homeassistant.components.meteo_france.*,homeassistant.components.meteoalarm.*,homeassistant.components.metoffice.*,homeassistant.components.mfi.*,homeassistant.components.mhz19.*,homeassistant.components.microsoft.*,homeassistant.components.microsoft_face.*,homeassistant.components.microsoft_face_detect.*,homeassistant.components.microsoft_face_identify.*,homeassistant.components.miflora.*,homeassistant.components.mikrotik.*,homeassistant.components.mill.*,homeassistant.components.min_max.*,homeassistant.components.minecraft_server.*,homeassistant.components.minio.*,homeassistant.components.mitemp_bt.*,homeassistant.components.mjpeg.*,homeassistant.components.mobile_app.*,homeassistant.components.mochad.*,homeassistant.components.modbus.*,homeassistant.components.modem_callerid.*,homeassistant.components.mold_indicator.*,homeassistant.components.monoprice.*,homeassistant.components.moon.*,homeassistant.components.motion_blinds.*,homeassistant.components.motioneye.*,homeassistant.components.mpchc.*,homeassistant.components.mpd.*,homeassistant.components.mqtt.*,homeassistant.components.mqtt_eventstream.*,homeassistant.components.mqtt_json.*,homeassistant.components.mqtt_room.*,homeassistant.components.mqtt_statestream.*,homeassistant.components.msteams.*,homeassistant.components.mullvad.*,homeassistant.components.mvglive.*,homeassistant.components.my.*,homeassistant.components.mychevy.*,homeassistant.components.mycroft.*,homeassistant.components.myq.*,homeassistant.components.mysensors.*,homeassistant.components.mystrom.*,homeassistant.components.mythicbeastsdns.*,homeassistant.components.n26.*,homeassistant.components.nad.*,homeassistant.components.namecheapdns.*,homeassistant.components.nanoleaf.*,homeassistant.components.neato.*,homeassistant.components.nederlandse_spoorwegen.*,homeassistant.components.nello.*,homeassistant.components.ness_alarm.*,homeassistant.components.nest.*,homeassistant.components.netatmo.*,homeassistant.components.netdata.*,homeassistant.components.netgear.*,homeassistant.components.netgear_lte.*,homeassistant.components.netio.*,homeassistant.components.neurio_energy.*,homeassistant.components.nexia.*,homeassistant.components.nextbus.*,homeassistant.components.nextcloud.*,homeassistant.components.nfandroidtv.*,homeassistant.components.nightscout.*,homeassistant.components.niko_home_control.*,homeassistant.components.nilu.*,homeassistant.components.nissan_leaf.*,homeassistant.components.nmap_tracker.*,homeassistant.components.nmbs.*,homeassistant.components.no_ip.*,homeassistant.components.noaa_tides.*,homeassistant.components.norway_air.*,homeassistant.components.notify_events.*,homeassistant.components.notion.*,homeassistant.components.nsw_fuel_station.*,homeassistant.components.nsw_rural_fire_service_feed.*,homeassistant.components.nuheat.*,homeassistant.components.nuki.*,homeassistant.components.numato.*,homeassistant.components.nut.*,homeassistant.components.nws.*,homeassistant.components.nx584.*,homeassistant.components.nzbget.*,homeassistant.components.oasa_telematics.*,homeassistant.components.obihai.*,homeassistant.components.octoprint.*,homeassistant.components.oem.*,homeassistant.components.ohmconnect.*,homeassistant.components.ombi.*,homeassistant.components.omnilogic.*,homeassistant.components.onboarding.*,homeassistant.components.ondilo_ico.*,homeassistant.components.onewire.*,homeassistant.components.onkyo.*,homeassistant.components.onvif.*,homeassistant.components.openalpr_cloud.*,homeassistant.components.openalpr_local.*,homeassistant.components.opencv.*,homeassistant.components.openerz.*,homeassistant.components.openevse.*,homeassistant.components.openexchangerates.*,homeassistant.components.opengarage.*,homeassistant.components.openhardwaremonitor.*,homeassistant.components.openhome.*,homeassistant.components.opensensemap.*,homeassistant.components.opensky.*,homeassistant.components.opentherm_gw.*,homeassistant.components.openuv.*,homeassistant.components.openweathermap.*,homeassistant.components.opnsense.*,homeassistant.components.opple.*,homeassistant.components.orangepi_gpio.*,homeassistant.components.oru.*,homeassistant.components.orvibo.*,homeassistant.components.osramlightify.*,homeassistant.components.otp.*,homeassistant.components.ovo_energy.*,homeassistant.components.owntracks.*,homeassistant.components.ozw.*,homeassistant.components.panasonic_bluray.*,homeassistant.components.panasonic_viera.*,homeassistant.components.pandora.*,homeassistant.components.panel_custom.*,homeassistant.components.panel_iframe.*,homeassistant.components.pcal9535a.*,homeassistant.components.pencom.*,homeassistant.components.person.*,homeassistant.components.philips_js.*,homeassistant.components.pi4ioe5v9xxxx.*,homeassistant.components.pi_hole.*,homeassistant.components.picnic.*,homeassistant.components.picotts.*,homeassistant.components.piglow.*,homeassistant.components.pilight.*,homeassistant.components.ping.*,homeassistant.components.pioneer.*,homeassistant.components.pjlink.*,homeassistant.components.plaato.*,homeassistant.components.plant.*,homeassistant.components.plex.*,homeassistant.components.plugwise.*,homeassistant.components.plum_lightpad.*,homeassistant.components.pocketcasts.*,homeassistant.components.point.*,homeassistant.components.poolsense.*,homeassistant.components.powerwall.*,homeassistant.components.profiler.*,homeassistant.components.progettihwsw.*,homeassistant.components.proliphix.*,homeassistant.components.prometheus.*,homeassistant.components.prowl.*,homeassistant.components.proxmoxve.*,homeassistant.components.proxy.*,homeassistant.components.ps4.*,homeassistant.components.pulseaudio_loopback.*,homeassistant.components.push.*,homeassistant.components.pushbullet.*,homeassistant.components.pushover.*,homeassistant.components.pushsafer.*,homeassistant.components.pvoutput.*,homeassistant.components.pvpc_hourly_pricing.*,homeassistant.components.pyload.*,homeassistant.components.python_script.*,homeassistant.components.qbittorrent.*,homeassistant.components.qld_bushfire.*,homeassistant.components.qnap.*,homeassistant.components.qrcode.*,homeassistant.components.quantum_gateway.*,homeassistant.components.qvr_pro.*,homeassistant.components.qwikswitch.*,homeassistant.components.rachio.*,homeassistant.components.radarr.*,homeassistant.components.radiotherm.*,homeassistant.components.rainbird.*,homeassistant.components.raincloud.*,homeassistant.components.rainforest_eagle.*,homeassistant.components.rainmachine.*,homeassistant.components.random.*,homeassistant.components.raspihats.*,homeassistant.components.raspyrfm.*,homeassistant.components.recollect_waste.*,homeassistant.components.recorder.*,homeassistant.components.recswitch.*,homeassistant.components.reddit.*,homeassistant.components.rejseplanen.*,homeassistant.components.remember_the_milk.*,homeassistant.components.remote_rpi_gpio.*,homeassistant.components.repetier.*,homeassistant.components.rest.*,homeassistant.components.rest_command.*,homeassistant.components.rflink.*,homeassistant.components.rfxtrx.*,homeassistant.components.ring.*,homeassistant.components.ripple.*,homeassistant.components.risco.*,homeassistant.components.rituals_perfume_genie.*,homeassistant.components.rmvtransport.*,homeassistant.components.rocketchat.*,homeassistant.components.roku.*,homeassistant.components.roomba.*,homeassistant.components.roon.*,homeassistant.components.route53.*,homeassistant.components.rova.*,homeassistant.components.rpi_camera.*,homeassistant.components.rpi_gpio.*,homeassistant.components.rpi_gpio_pwm.*,homeassistant.components.rpi_pfio.*,homeassistant.components.rpi_power.*,homeassistant.components.rpi_rf.*,homeassistant.components.rss_feed_template.*,homeassistant.components.rtorrent.*,homeassistant.components.ruckus_unleashed.*,homeassistant.components.russound_rio.*,homeassistant.components.russound_rnet.*,homeassistant.components.sabnzbd.*,homeassistant.components.safe_mode.*,homeassistant.components.saj.*,homeassistant.components.samsungtv.*,homeassistant.components.satel_integra.*,homeassistant.components.schluter.*,homeassistant.components.scrape.*,homeassistant.components.screenlogic.*,homeassistant.components.script.*,homeassistant.components.scsgate.*,homeassistant.components.search.*,homeassistant.components.season.*,homeassistant.components.sendgrid.*,homeassistant.components.sense.*,homeassistant.components.sensehat.*,homeassistant.components.sensibo.*,homeassistant.components.sentry.*,homeassistant.components.serial.*,homeassistant.components.serial_pm.*,homeassistant.components.sesame.*,homeassistant.components.seven_segments.*,homeassistant.components.seventeentrack.*,homeassistant.components.sharkiq.*,homeassistant.components.shell_command.*,homeassistant.components.shelly.*,homeassistant.components.shiftr.*,homeassistant.components.shodan.*,homeassistant.components.shopping_list.*,homeassistant.components.sht31.*,homeassistant.components.sigfox.*,homeassistant.components.sighthound.*,homeassistant.components.signal_messenger.*,homeassistant.components.simplepush.*,homeassistant.components.simplisafe.*,homeassistant.components.simulated.*,homeassistant.components.sinch.*,homeassistant.components.sisyphus.*,homeassistant.components.sky_hub.*,homeassistant.components.skybeacon.*,homeassistant.components.skybell.*,homeassistant.components.sleepiq.*,homeassistant.components.slide.*,homeassistant.components.sma.*,homeassistant.components.smappee.*,homeassistant.components.smart_meter_texas.*,homeassistant.components.smarthab.*,homeassistant.components.smartthings.*,homeassistant.components.smarttub.*,homeassistant.components.smarty.*,homeassistant.components.smhi.*,homeassistant.components.sms.*,homeassistant.components.smtp.*,homeassistant.components.snapcast.*,homeassistant.components.snips.*,homeassistant.components.snmp.*,homeassistant.components.sochain.*,homeassistant.components.solaredge.*,homeassistant.components.solaredge_local.*,homeassistant.components.solarlog.*,homeassistant.components.solax.*,homeassistant.components.soma.*,homeassistant.components.somfy.*,homeassistant.components.somfy_mylink.*,homeassistant.components.sonarr.*,homeassistant.components.songpal.*,homeassistant.components.sonos.*,homeassistant.components.sony_projector.*,homeassistant.components.soundtouch.*,homeassistant.components.spaceapi.*,homeassistant.components.spc.*,homeassistant.components.speedtestdotnet.*,homeassistant.components.spider.*,homeassistant.components.splunk.*,homeassistant.components.spotcrime.*,homeassistant.components.spotify.*,homeassistant.components.sql.*,homeassistant.components.squeezebox.*,homeassistant.components.srp_energy.*,homeassistant.components.ssdp.*,homeassistant.components.starline.*,homeassistant.components.starlingbank.*,homeassistant.components.startca.*,homeassistant.components.statistics.*,homeassistant.components.statsd.*,homeassistant.components.steam_online.*,homeassistant.components.stiebel_eltron.*,homeassistant.components.stookalert.*,homeassistant.components.stream.*,homeassistant.components.streamlabswater.*,homeassistant.components.stt.*,homeassistant.components.subaru.*,homeassistant.components.suez_water.*,homeassistant.components.supervisord.*,homeassistant.components.supla.*,homeassistant.components.surepetcare.*,homeassistant.components.swiss_hydrological_data.*,homeassistant.components.swiss_public_transport.*,homeassistant.components.swisscom.*,homeassistant.components.switchbot.*,homeassistant.components.switcher_kis.*,homeassistant.components.switchmate.*,homeassistant.components.syncthru.*,homeassistant.components.synology_chat.*,homeassistant.components.synology_dsm.*,homeassistant.components.synology_srm.*,homeassistant.components.syslog.*,homeassistant.components.system_health.*,homeassistant.components.system_log.*,homeassistant.components.tado.*,homeassistant.components.tag.*,homeassistant.components.tahoma.*,homeassistant.components.tank_utility.*,homeassistant.components.tankerkoenig.*,homeassistant.components.tapsaff.*,homeassistant.components.tasmota.*,homeassistant.components.tautulli.*,homeassistant.components.tcp.*,homeassistant.components.ted5000.*,homeassistant.components.telegram.*,homeassistant.components.telegram_bot.*,homeassistant.components.tellduslive.*,homeassistant.components.tellstick.*,homeassistant.components.telnet.*,homeassistant.components.temper.*,homeassistant.components.template.*,homeassistant.components.tensorflow.*,homeassistant.components.tesla.*,homeassistant.components.tfiac.*,homeassistant.components.thermoworks_smoke.*,homeassistant.components.thethingsnetwork.*,homeassistant.components.thingspeak.*,homeassistant.components.thinkingcleaner.*,homeassistant.components.thomson.*,homeassistant.components.threshold.*,homeassistant.components.tibber.*,homeassistant.components.tikteck.*,homeassistant.components.tile.*,homeassistant.components.time_date.*,homeassistant.components.timer.*,homeassistant.components.tmb.*,homeassistant.components.tod.*,homeassistant.components.todoist.*,homeassistant.components.tof.*,homeassistant.components.tomato.*,homeassistant.components.toon.*,homeassistant.components.torque.*,homeassistant.components.totalconnect.*,homeassistant.components.touchline.*,homeassistant.components.tplink.*,homeassistant.components.tplink_lte.*,homeassistant.components.traccar.*,homeassistant.components.trace.*,homeassistant.components.trackr.*,homeassistant.components.tradfri.*,homeassistant.components.trafikverket_train.*,homeassistant.components.trafikverket_weatherstation.*,homeassistant.components.transmission.*,homeassistant.components.transport_nsw.*,homeassistant.components.travisci.*,homeassistant.components.trend.*,homeassistant.components.tuya.*,homeassistant.components.twentemilieu.*,homeassistant.components.twilio.*,homeassistant.components.twilio_call.*,homeassistant.components.twilio_sms.*,homeassistant.components.twinkly.*,homeassistant.components.twitch.*,homeassistant.components.twitter.*,homeassistant.components.ubus.*,homeassistant.components.ue_smart_radio.*,homeassistant.components.uk_transport.*,homeassistant.components.unifi.*,homeassistant.components.unifi_direct.*,homeassistant.components.unifiled.*,homeassistant.components.universal.*,homeassistant.components.upb.*,homeassistant.components.upc_connect.*,homeassistant.components.upcloud.*,homeassistant.components.updater.*,homeassistant.components.upnp.*,homeassistant.components.uptime.*,homeassistant.components.uptimerobot.*,homeassistant.components.uscis.*,homeassistant.components.usgs_earthquakes_feed.*,homeassistant.components.utility_meter.*,homeassistant.components.uvc.*,homeassistant.components.vallox.*,homeassistant.components.vasttrafik.*,homeassistant.components.velbus.*,homeassistant.components.velux.*,homeassistant.components.venstar.*,homeassistant.components.vera.*,homeassistant.components.verisure.*,homeassistant.components.versasense.*,homeassistant.components.version.*,homeassistant.components.vesync.*,homeassistant.components.viaggiatreno.*,homeassistant.components.vicare.*,homeassistant.components.vilfo.*,homeassistant.components.vivotek.*,homeassistant.components.vizio.*,homeassistant.components.vlc.*,homeassistant.components.vlc_telnet.*,homeassistant.components.voicerss.*,homeassistant.components.volkszaehler.*,homeassistant.components.volumio.*,homeassistant.components.volvooncall.*,homeassistant.components.vultr.*,homeassistant.components.w800rf32.*,homeassistant.components.wake_on_lan.*,homeassistant.components.waqi.*,homeassistant.components.waterfurnace.*,homeassistant.components.watson_iot.*,homeassistant.components.watson_tts.*,homeassistant.components.waze_travel_time.*,homeassistant.components.webhook.*,homeassistant.components.webostv.*,homeassistant.components.wemo.*,homeassistant.components.whois.*,homeassistant.components.wiffi.*,homeassistant.components.wilight.*,homeassistant.components.wink.*,homeassistant.components.wirelesstag.*,homeassistant.components.withings.*,homeassistant.components.wled.*,homeassistant.components.wolflink.*,homeassistant.components.workday.*,homeassistant.components.worldclock.*,homeassistant.components.worldtidesinfo.*,homeassistant.components.worxlandroid.*,homeassistant.components.wsdot.*,homeassistant.components.wunderground.*,homeassistant.components.x10.*,homeassistant.components.xbee.*,homeassistant.components.xbox.*,homeassistant.components.xbox_live.*,homeassistant.components.xeoma.*,homeassistant.components.xiaomi.*,homeassistant.components.xiaomi_aqara.*,homeassistant.components.xiaomi_miio.*,homeassistant.components.xiaomi_tv.*,homeassistant.components.xmpp.*,homeassistant.components.xs1.*,homeassistant.components.yale_smart_alarm.*,homeassistant.components.yamaha.*,homeassistant.components.yamaha_musiccast.*,homeassistant.components.yandex_transport.*,homeassistant.components.yandextts.*,homeassistant.components.yeelight.*,homeassistant.components.yeelightsunflower.*,homeassistant.components.yi.*,homeassistant.components.zabbix.*,homeassistant.components.zamg.*,homeassistant.components.zengge.*,homeassistant.components.zerproc.*,homeassistant.components.zestimate.*,homeassistant.components.zha.*,homeassistant.components.zhong_hong.*,homeassistant.components.ziggo_mediabox_xl.*,homeassistant.components.zodiac.*,homeassistant.components.zoneminder.*,homeassistant.components.zwave.*] +[mypy-homeassistant.components.*] check_untyped_defs = false disallow_incomplete_defs = false disallow_subclassing_any = false @@ -35,5 +35,18 @@ warn_return_any = false warn_unreachable = false warn_unused_ignores = false +[mypy-homeassistant.components,homeassistant.components.automation.*,homeassistant.components.binary_sensor.*,homeassistant.components.bond.*,homeassistant.components.calendar.*,homeassistant.components.cover.*,homeassistant.components.device_automation.*,homeassistant.components.frontend.*,homeassistant.components.geo_location.*,homeassistant.components.group.*,homeassistant.components.history.*,homeassistant.components.http.*,homeassistant.components.huawei_lte.*,homeassistant.components.hyperion.*,homeassistant.components.image_processing.*,homeassistant.components.integration.*,homeassistant.components.knx.*,homeassistant.components.light.*,homeassistant.components.lock.*,homeassistant.components.mailbox.*,homeassistant.components.media_player.*,homeassistant.components.notify.*,homeassistant.components.number.*,homeassistant.components.persistent_notification.*,homeassistant.components.proximity.*,homeassistant.components.recorder.purge,homeassistant.components.recorder.repack,homeassistant.components.remote.*,homeassistant.components.scene.*,homeassistant.components.sensor.*,homeassistant.components.slack.*,homeassistant.components.sonos.media_player,homeassistant.components.sun.*,homeassistant.components.switch.*,homeassistant.components.systemmonitor.*,homeassistant.components.tts.*,homeassistant.components.vacuum.*,homeassistant.components.water_heater.*,homeassistant.components.weather.*,homeassistant.components.websocket_api.*,homeassistant.components.zeroconf.*,homeassistant.components.zone.*,homeassistant.components.zwave_js.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +strict_equality = true +warn_return_any = true +warn_unreachable = true +warn_unused_ignores = true + [mypy-homeassistant.components.adguard.*,homeassistant.components.aemet.*,homeassistant.components.airly.*,homeassistant.components.alarmdecoder.*,homeassistant.components.alexa.*,homeassistant.components.almond.*,homeassistant.components.amcrest.*,homeassistant.components.analytics.*,homeassistant.components.asuswrt.*,homeassistant.components.atag.*,homeassistant.components.aurora.*,homeassistant.components.awair.*,homeassistant.components.axis.*,homeassistant.components.azure_devops.*,homeassistant.components.azure_event_hub.*,homeassistant.components.blueprint.*,homeassistant.components.bluetooth_tracker.*,homeassistant.components.bmw_connected_drive.*,homeassistant.components.bsblan.*,homeassistant.components.camera.*,homeassistant.components.canary.*,homeassistant.components.cast.*,homeassistant.components.cert_expiry.*,homeassistant.components.climacell.*,homeassistant.components.climate.*,homeassistant.components.cloud.*,homeassistant.components.cloudflare.*,homeassistant.components.config.*,homeassistant.components.control4.*,homeassistant.components.conversation.*,homeassistant.components.deconz.*,homeassistant.components.demo.*,homeassistant.components.denonavr.*,homeassistant.components.device_tracker.*,homeassistant.components.devolo_home_control.*,homeassistant.components.dhcp.*,homeassistant.components.directv.*,homeassistant.components.doorbird.*,homeassistant.components.dsmr.*,homeassistant.components.dynalite.*,homeassistant.components.eafm.*,homeassistant.components.edl21.*,homeassistant.components.elgato.*,homeassistant.components.elkm1.*,homeassistant.components.emonitor.*,homeassistant.components.enphase_envoy.*,homeassistant.components.entur_public_transport.*,homeassistant.components.esphome.*,homeassistant.components.evohome.*,homeassistant.components.fan.*,homeassistant.components.filter.*,homeassistant.components.fints.*,homeassistant.components.fireservicerota.*,homeassistant.components.firmata.*,homeassistant.components.fitbit.*,homeassistant.components.flo.*,homeassistant.components.fortios.*,homeassistant.components.foscam.*,homeassistant.components.freebox.*,homeassistant.components.fritz.*,homeassistant.components.fritzbox.*,homeassistant.components.garmin_connect.*,homeassistant.components.geniushub.*,homeassistant.components.gios.*,homeassistant.components.glances.*,homeassistant.components.gogogate2.*,homeassistant.components.google_assistant.*,homeassistant.components.google_maps.*,homeassistant.components.google_pubsub.*,homeassistant.components.gpmdp.*,homeassistant.components.gree.*,homeassistant.components.growatt_server.*,homeassistant.components.gtfs.*,homeassistant.components.guardian.*,homeassistant.components.habitica.*,homeassistant.components.harmony.*,homeassistant.components.hassio.*,homeassistant.components.hdmi_cec.*,homeassistant.components.here_travel_time.*,homeassistant.components.hisense_aehw4a1.*,homeassistant.components.home_connect.*,homeassistant.components.home_plus_control.*,homeassistant.components.homeassistant.*,homeassistant.components.homekit.*,homeassistant.components.homekit_controller.*,homeassistant.components.homematicip_cloud.*,homeassistant.components.honeywell.*,homeassistant.components.hue.*,homeassistant.components.huisbaasje.*,homeassistant.components.humidifier.*,homeassistant.components.iaqualink.*,homeassistant.components.icloud.*,homeassistant.components.ihc.*,homeassistant.components.image.*,homeassistant.components.incomfort.*,homeassistant.components.influxdb.*,homeassistant.components.input_boolean.*,homeassistant.components.input_datetime.*,homeassistant.components.input_number.*,homeassistant.components.insteon.*,homeassistant.components.ipp.*,homeassistant.components.isy994.*,homeassistant.components.izone.*,homeassistant.components.kaiterra.*,homeassistant.components.keenetic_ndms2.*,homeassistant.components.kodi.*,homeassistant.components.konnected.*,homeassistant.components.kostal_plenticore.*,homeassistant.components.kulersky.*,homeassistant.components.lifx.*,homeassistant.components.litejet.*,homeassistant.components.litterrobot.*,homeassistant.components.lovelace.*,homeassistant.components.luftdaten.*,homeassistant.components.lutron_caseta.*,homeassistant.components.lyric.*,homeassistant.components.marytts.*,homeassistant.components.media_source.*,homeassistant.components.melcloud.*,homeassistant.components.meteo_france.*,homeassistant.components.metoffice.*,homeassistant.components.minecraft_server.*,homeassistant.components.mobile_app.*,homeassistant.components.modbus.*,homeassistant.components.motion_blinds.*,homeassistant.components.motioneye.*,homeassistant.components.mqtt.*,homeassistant.components.mullvad.*,homeassistant.components.mysensors.*,homeassistant.components.n26.*,homeassistant.components.neato.*,homeassistant.components.ness_alarm.*,homeassistant.components.nest.*,homeassistant.components.netatmo.*,homeassistant.components.netio.*,homeassistant.components.nightscout.*,homeassistant.components.nilu.*,homeassistant.components.nmap_tracker.*,homeassistant.components.norway_air.*,homeassistant.components.notion.*,homeassistant.components.nsw_fuel_station.*,homeassistant.components.nuki.*,homeassistant.components.nws.*,homeassistant.components.nzbget.*,homeassistant.components.omnilogic.*,homeassistant.components.onboarding.*,homeassistant.components.ondilo_ico.*,homeassistant.components.onewire.*,homeassistant.components.onvif.*,homeassistant.components.ovo_energy.*,homeassistant.components.ozw.*,homeassistant.components.panasonic_viera.*,homeassistant.components.philips_js.*,homeassistant.components.pilight.*,homeassistant.components.ping.*,homeassistant.components.pioneer.*,homeassistant.components.plaato.*,homeassistant.components.plex.*,homeassistant.components.plugwise.*,homeassistant.components.plum_lightpad.*,homeassistant.components.point.*,homeassistant.components.profiler.*,homeassistant.components.proxmoxve.*,homeassistant.components.rachio.*,homeassistant.components.rainmachine.*,homeassistant.components.recollect_waste.*,homeassistant.components.recorder.*,homeassistant.components.reddit.*,homeassistant.components.ring.*,homeassistant.components.rituals_perfume_genie.*,homeassistant.components.roku.*,homeassistant.components.rpi_power.*,homeassistant.components.ruckus_unleashed.*,homeassistant.components.sabnzbd.*,homeassistant.components.screenlogic.*,homeassistant.components.script.*,homeassistant.components.search.*,homeassistant.components.sense.*,homeassistant.components.sentry.*,homeassistant.components.sesame.*,homeassistant.components.sharkiq.*,homeassistant.components.shell_command.*,homeassistant.components.shelly.*,homeassistant.components.sma.*,homeassistant.components.smart_meter_texas.*,homeassistant.components.smartthings.*,homeassistant.components.smarttub.*,homeassistant.components.smarty.*,homeassistant.components.smhi.*,homeassistant.components.solaredge.*,homeassistant.components.solarlog.*,homeassistant.components.somfy.*,homeassistant.components.somfy_mylink.*,homeassistant.components.sonarr.*,homeassistant.components.songpal.*,homeassistant.components.sonos.*,homeassistant.components.spotify.*,homeassistant.components.stream.*,homeassistant.components.stt.*,homeassistant.components.surepetcare.*,homeassistant.components.switchbot.*,homeassistant.components.switcher_kis.*,homeassistant.components.synology_dsm.*,homeassistant.components.synology_srm.*,homeassistant.components.system_health.*,homeassistant.components.system_log.*,homeassistant.components.tado.*,homeassistant.components.tasmota.*,homeassistant.components.tcp.*,homeassistant.components.telegram_bot.*,homeassistant.components.template.*,homeassistant.components.tesla.*,homeassistant.components.timer.*,homeassistant.components.todoist.*,homeassistant.components.toon.*,homeassistant.components.tplink.*,homeassistant.components.trace.*,homeassistant.components.tradfri.*,homeassistant.components.tuya.*,homeassistant.components.twentemilieu.*,homeassistant.components.unifi.*,homeassistant.components.upcloud.*,homeassistant.components.updater.*,homeassistant.components.upnp.*,homeassistant.components.velbus.*,homeassistant.components.vera.*,homeassistant.components.verisure.*,homeassistant.components.vizio.*,homeassistant.components.volumio.*,homeassistant.components.webostv.*,homeassistant.components.wemo.*,homeassistant.components.wink.*,homeassistant.components.withings.*,homeassistant.components.wled.*,homeassistant.components.wunderground.*,homeassistant.components.xbox.*,homeassistant.components.xiaomi_aqara.*,homeassistant.components.xiaomi_miio.*,homeassistant.components.yamaha.*,homeassistant.components.yeelight.*,homeassistant.components.zerproc.*,homeassistant.components.zha.*,homeassistant.components.zwave.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index a5ca0fbfc3b0e..45fa1eb653981 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -297,29 +297,29 @@ def generate_and_validate(config: Config) -> str: """Validate and generate mypy config.""" - strict_disabled_path = config.root / ".no-strict-typing" + config_path = config.root / ".strict-typing" - with strict_disabled_path.open() as fp: + with config_path.open() as fp: lines = fp.readlines() # Filter empty and commented lines. - not_strict_modules: list[str] = [ + strict_modules: list[str] = [ line.strip() for line in lines if line.strip() != "" and not line.startswith("#") ] - for module in not_strict_modules: - if not module.startswith("homeassistant.components."): + + ignored_modules_set: set[str] = set(IGNORED_MODULES) + for module in strict_modules: + if ( + not module.startswith("homeassistant.components.") + and module != "homeassistant.components" + ): config.add_error( "mypy_config", f"Only components should be added: {module}" ) - not_strict_modules_set: set[str] = set(not_strict_modules) - for module in IGNORED_MODULES: - if module not in not_strict_modules_set: - config.add_error( - "mypy_config", - f"Ignored module '{module} must be excluded from strict typing", - ) + if module in ignored_modules_set: + config.add_error("mypy_config", f"Module '{module}' is in ignored list") mypy_config = configparser.ConfigParser() @@ -330,10 +330,16 @@ def generate_and_validate(config: Config) -> str: for key in STRICT_SETTINGS: mypy_config.set(general_section, key, "true") - strict_disabled_section = "mypy-" + ",".join(not_strict_modules) - mypy_config.add_section(strict_disabled_section) + # By default strict checks are disabled for components. + components_section = "mypy-homeassistant.components.*" + mypy_config.add_section(components_section) + for key in STRICT_SETTINGS: + mypy_config.set(components_section, key, "false") + + strict_section = "mypy-" + ",".join(strict_modules) + mypy_config.add_section(strict_section) for key in STRICT_SETTINGS: - mypy_config.set(strict_disabled_section, key, "false") + mypy_config.set(strict_section, key, "true") ignored_section = "mypy-" + ",".join(IGNORED_MODULES) mypy_config.add_section(ignored_section) From d2d80093a15bcca11a8207e25e5a13a37b3f2a85 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Tue, 27 Apr 2021 12:33:52 -0400 Subject: [PATCH 0579/1317] Add selector to google assistant services (#49769) --- homeassistant/components/google_assistant/services.yaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/google_assistant/services.yaml b/homeassistant/components/google_assistant/services.yaml index 33a52c8ef6050..fe5ef51c2ce9c 100644 --- a/homeassistant/components/google_assistant/services.yaml +++ b/homeassistant/components/google_assistant/services.yaml @@ -1,5 +1,9 @@ request_sync: + name: Request sync description: Send a request_sync command to Google. fields: agent_user_id: - description: "Optional. Only needed for automations. Specific Home Assistant user id (not username, ID in configuration > users > under username) to sync with Google Assistant. Do not need when you call this service through Home Assistant front end or API. Used in automation script or other place where context.user_id is missing." + name: Agent user ID + description: "Only needed for automations. Specific Home Assistant user id (not username, ID in configuration > users > under username) to sync with Google Assistant. Do not need when you call this service through Home Assistant front end or API. Used in automation script or other place where context.user_id is missing." + selector: + text: From b5cb9e4ade7a4b756876068a8aec1d1f148de8df Mon Sep 17 00:00:00 2001 From: tkdrob Date: Tue, 27 Apr 2021 12:36:41 -0400 Subject: [PATCH 0580/1317] Clean up tellduslive constants (#49765) --- homeassistant/components/tellduslive/__init__.py | 4 +--- homeassistant/components/tellduslive/const.py | 7 ------- homeassistant/components/tellduslive/entry.py | 1 - 3 files changed, 1 insertion(+), 11 deletions(-) diff --git a/homeassistant/components/tellduslive/__init__.py b/homeassistant/components/tellduslive/__init__.py index 70cc884881457..0473c52ed92d6 100644 --- a/homeassistant/components/tellduslive/__init__.py +++ b/homeassistant/components/tellduslive/__init__.py @@ -7,13 +7,12 @@ import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_SCAN_INTERVAL +from homeassistant.const import CONF_HOST, CONF_SCAN_INTERVAL import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_call_later from .const import ( - CONF_HOST, DOMAIN, KEY_SCAN_INTERVAL, KEY_SESSION, @@ -52,7 +51,6 @@ async def async_setup_entry(hass, entry): """Create a tellduslive session.""" - conf = entry.data[KEY_SESSION] if CONF_HOST in conf: diff --git a/homeassistant/components/tellduslive/const.py b/homeassistant/components/tellduslive/const.py index 8d9f28cc5cf9f..6b3bb1c643710 100644 --- a/homeassistant/components/tellduslive/const.py +++ b/homeassistant/components/tellduslive/const.py @@ -1,13 +1,6 @@ """Consts used by TelldusLive.""" from datetime import timedelta -from homeassistant.const import ( # noqa: F401 pylint: disable=unused-import - ATTR_BATTERY_LEVEL, - CONF_HOST, - CONF_TOKEN, - DEVICE_DEFAULT_NAME, -) - APPLICATION_NAME = "Home Assistant" DOMAIN = "tellduslive" diff --git a/homeassistant/components/tellduslive/entry.py b/homeassistant/components/tellduslive/entry.py index 4453622b21e12..67a59fc8dab6c 100644 --- a/homeassistant/components/tellduslive/entry.py +++ b/homeassistant/components/tellduslive/entry.py @@ -93,7 +93,6 @@ def extra_state_attributes(self): @property def _battery_level(self): """Return the battery level of a device.""" - if self.device.battery == BATTERY_LOW: return 1 if self.device.battery == BATTERY_UNKNOWN: From b10534359be835a39fca382d6d0c500e30083b21 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 27 Apr 2021 06:49:13 -1000 Subject: [PATCH 0581/1317] Reduce config entry setup/unload boilerplate K-M (#49775) --- .../components/keenetic_ndms2/__init__.py | 12 ++++------ homeassistant/components/kmtronic/__init__.py | 15 ++---------- homeassistant/components/kodi/__init__.py | 15 ++---------- .../components/konnected/__init__.py | 15 ++---------- .../components/kostal_plenticore/__init__.py | 22 +++-------------- homeassistant/components/kulersky/__init__.py | 15 ++---------- homeassistant/components/lcn/__init__.py | 15 +++--------- homeassistant/components/lifx/__init__.py | 12 ++++------ homeassistant/components/litejet/__init__.py | 16 ++----------- .../components/litterrobot/__init__.py | 15 ++---------- homeassistant/components/local_ip/__init__.py | 9 +++---- homeassistant/components/local_ip/const.py | 2 +- homeassistant/components/locative/__init__.py | 7 +++--- .../components/logi_circle/__init__.py | 10 +++----- .../components/luftdaten/__init__.py | 8 +++---- .../components/lutron_caseta/__init__.py | 15 +++--------- homeassistant/components/lyric/__init__.py | 15 ++---------- homeassistant/components/mazda/__init__.py | 15 ++---------- homeassistant/components/melcloud/__init__.py | 14 ++++------- homeassistant/components/met/__init__.py | 12 ++++++---- .../components/met_eireann/__init__.py | 12 ++++++---- .../components/meteo_france/__init__.py | 15 ++---------- .../components/metoffice/__init__.py | 15 ++---------- homeassistant/components/mikrotik/__init__.py | 8 +++++-- homeassistant/components/mikrotik/const.py | 2 ++ homeassistant/components/mikrotik/hub.py | 7 ++---- homeassistant/components/mill/__init__.py | 13 ++++------ .../components/minecraft_server/__init__.py | 15 ++++-------- .../components/mobile_app/__init__.py | 15 ++---------- .../components/monoprice/__init__.py | 16 ++----------- .../components/motion_blinds/__init__.py | 15 +++--------- .../components/motioneye/__init__.py | 9 +------ homeassistant/components/mullvad/__init__.py | 16 ++----------- homeassistant/components/myq/__init__.py | 24 +++---------------- .../components/mysensors/__init__.py | 9 ++----- .../kostal_plenticore/test_config_flow.py | 3 --- tests/components/myq/test_config_flow.py | 3 --- 37 files changed, 104 insertions(+), 352 deletions(-) diff --git a/homeassistant/components/keenetic_ndms2/__init__.py b/homeassistant/components/keenetic_ndms2/__init__.py index 6156fb00d0263..787e6a5f5f1c9 100644 --- a/homeassistant/components/keenetic_ndms2/__init__.py +++ b/homeassistant/components/keenetic_ndms2/__init__.py @@ -38,10 +38,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b UNDO_UPDATE_LISTENER: undo_listener, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) return True @@ -50,8 +47,9 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> """Unload a config entry.""" hass.data[DOMAIN][config_entry.entry_id][UNDO_UPDATE_LISTENER]() - for platform in PLATFORMS: - await hass.config_entries.async_forward_entry_unload(config_entry, platform) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS + ) router: KeeneticRouter = hass.data[DOMAIN][config_entry.entry_id][ROUTER] @@ -59,7 +57,7 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> hass.data[DOMAIN].pop(config_entry.entry_id) - return True + return unload_ok async def update_listener(hass, config_entry): diff --git a/homeassistant/components/kmtronic/__init__.py b/homeassistant/components/kmtronic/__init__.py index a028a62cbc54e..3b8da77faab46 100644 --- a/homeassistant/components/kmtronic/__init__.py +++ b/homeassistant/components/kmtronic/__init__.py @@ -1,5 +1,4 @@ """The kmtronic integration.""" -import asyncio from datetime import timedelta import logging @@ -59,10 +58,7 @@ async def async_update_data(): DATA_COORDINATOR: coordinator, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) update_listener = entry.add_update_listener(async_update_options) hass.data[DOMAIN][entry.entry_id][UPDATE_LISTENER] = update_listener @@ -77,14 +73,7 @@ async def async_update_options(hass: HomeAssistant, config_entry: ConfigEntry) - async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: update_listener = hass.data[DOMAIN][entry.entry_id][UPDATE_LISTENER] update_listener() diff --git a/homeassistant/components/kodi/__init__.py b/homeassistant/components/kodi/__init__.py index d42b4aa2ec4b6..fe318b103d10e 100644 --- a/homeassistant/components/kodi/__init__.py +++ b/homeassistant/components/kodi/__init__.py @@ -1,6 +1,5 @@ """The kodi component.""" -import asyncio import logging from pykodi import CannotConnectError, InvalidAuthError, Kodi, get_kodi_connection @@ -67,24 +66,14 @@ async def _close(event): DATA_REMOVE_LISTENER: remove_stop_listener, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: data = hass.data[DOMAIN].pop(entry.entry_id) await data[DATA_CONNECTION].close() diff --git a/homeassistant/components/konnected/__init__.py b/homeassistant/components/konnected/__init__.py index db1e20204cd7c..857521b9fad7e 100644 --- a/homeassistant/components/konnected/__init__.py +++ b/homeassistant/components/konnected/__init__.py @@ -1,5 +1,4 @@ """Support for Konnected devices.""" -import asyncio import copy import hmac import json @@ -261,10 +260,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): # async_connect will handle retries until it establishes a connection await client.async_connect() - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) # config entry specific data to enable unload hass.data[DOMAIN][entry.entry_id] = { @@ -275,14 +271,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() diff --git a/homeassistant/components/kostal_plenticore/__init__.py b/homeassistant/components/kostal_plenticore/__init__.py index f06657fdaa162..f00e6ee1327d5 100644 --- a/homeassistant/components/kostal_plenticore/__init__.py +++ b/homeassistant/components/kostal_plenticore/__init__.py @@ -1,5 +1,4 @@ """The Kostal Plenticore Solar Inverter integration.""" -import asyncio import logging from kostal.plenticore import PlenticoreApiException @@ -15,14 +14,9 @@ PLATFORMS = ["sensor"] -async def async_setup(hass: HomeAssistant, config: dict) -> bool: - """Set up the Kostal Plenticore Solar Inverter component.""" - hass.data.setdefault(DOMAIN, {}) - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Kostal Plenticore Solar Inverter from a config entry.""" + hass.data.setdefault(DOMAIN, {}) plenticore = Plenticore(hass, entry) @@ -31,24 +25,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = plenticore - for component in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: # remove API object plenticore = hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/kulersky/__init__.py b/homeassistant/components/kulersky/__init__.py index 358d13dee564f..6409d435bf352 100644 --- a/homeassistant/components/kulersky/__init__.py +++ b/homeassistant/components/kulersky/__init__.py @@ -1,5 +1,4 @@ """Kuler Sky lights integration.""" -import asyncio from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -16,10 +15,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): if DATA_ADDRESSES not in hass.data[DOMAIN]: hass.data[DOMAIN][DATA_ADDRESSES] = set() - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -33,11 +29,4 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data.pop(DOMAIN, None) - return all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/lcn/__init__.py b/homeassistant/components/lcn/__init__.py index 9384fbed29d3e..faf524f658558 100644 --- a/homeassistant/components/lcn/__init__.py +++ b/homeassistant/components/lcn/__init__.py @@ -1,5 +1,4 @@ """Support for LCN devices.""" -import asyncio import logging import pypck @@ -95,10 +94,7 @@ async def async_setup_entry(hass, config_entry): entity_registry.async_clear_config_entry(config_entry.entry_id) # forward config_entry to components - for component in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, component) - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) # register service calls for service_name, service in SERVICES: @@ -113,13 +109,8 @@ async def async_setup_entry(hass, config_entry): async def async_unload_entry(hass, config_entry): """Close connection to PCHK host represented by config_entry.""" # forward unloading to platforms - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, component) - for component in PLATFORMS - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS ) if unload_ok and config_entry.entry_id in hass.data[DOMAIN]: diff --git a/homeassistant/components/lifx/__init__.py b/homeassistant/components/lifx/__init__.py index 6e921a59afedd..b0b67450b5e9c 100644 --- a/homeassistant/components/lifx/__init__.py +++ b/homeassistant/components/lifx/__init__.py @@ -26,6 +26,8 @@ DATA_LIFX_MANAGER = "lifx_manager" +PLATFORMS = [LIGHT_DOMAIN] + async def async_setup(hass, config): """Set up the LIFX component.""" @@ -45,17 +47,11 @@ async def async_setup(hass, config): async def async_setup_entry(hass, entry): """Set up LIFX from a config entry.""" - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, LIGHT_DOMAIN) - ) - + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass, entry): """Unload a config entry.""" hass.data.pop(DATA_LIFX_MANAGER).cleanup() - - await hass.config_entries.async_forward_entry_unload(entry, LIGHT_DOMAIN) - - return True + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/litejet/__init__.py b/homeassistant/components/litejet/__init__.py index f00853af52483..b69df5ffd31f1 100644 --- a/homeassistant/components/litejet/__init__.py +++ b/homeassistant/components/litejet/__init__.py @@ -1,5 +1,4 @@ """Support for the LiteJet lighting system.""" -import asyncio import logging import pylitejet @@ -59,25 +58,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN] = system - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a LiteJet config entry.""" - - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].close() diff --git a/homeassistant/components/litterrobot/__init__.py b/homeassistant/components/litterrobot/__init__.py index 83bf9f785a26c..424a6a92abadb 100644 --- a/homeassistant/components/litterrobot/__init__.py +++ b/homeassistant/components/litterrobot/__init__.py @@ -1,5 +1,4 @@ """The Litter-Robot integration.""" -import asyncio from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException @@ -25,24 +24,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): raise ConfigEntryNotReady from ex if hub.account.robots: - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/local_ip/__init__.py b/homeassistant/components/local_ip/__init__.py index 637520aa30c1d..1e8376b6b6fc7 100644 --- a/homeassistant/components/local_ip/__init__.py +++ b/homeassistant/components/local_ip/__init__.py @@ -6,7 +6,7 @@ from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -from .const import DOMAIN, PLATFORM +from .const import DOMAIN, PLATFORMS CONFIG_SCHEMA = vol.Schema( { @@ -34,13 +34,10 @@ async def async_setup(hass: HomeAssistant, config: dict): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up local_ip from a config entry.""" - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, PLATFORM) - ) - + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - return await hass.config_entries.async_forward_entry_unload(entry, PLATFORM) + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/local_ip/const.py b/homeassistant/components/local_ip/const.py index e18246a97309e..0bac6d874d11a 100644 --- a/homeassistant/components/local_ip/const.py +++ b/homeassistant/components/local_ip/const.py @@ -1,6 +1,6 @@ """Local IP constants.""" DOMAIN = "local_ip" -PLATFORM = "sensor" +PLATFORMS = ["sensor"] SENSOR = "address" diff --git a/homeassistant/components/locative/__init__.py b/homeassistant/components/locative/__init__.py index bb2a19c638019..97df92a9f8985 100644 --- a/homeassistant/components/locative/__init__.py +++ b/homeassistant/components/locative/__init__.py @@ -25,6 +25,7 @@ DOMAIN = "locative" TRACKER_UPDATE = f"{DOMAIN}_tracker_update" +PLATFORMS = [DEVICE_TRACKER] ATTR_DEVICE_ID = "device" ATTR_TRIGGER = "trigger" @@ -116,9 +117,7 @@ async def async_setup_entry(hass, entry): DOMAIN, "Locative", entry.data[CONF_WEBHOOK_ID], handle_webhook ) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, DEVICE_TRACKER) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -126,7 +125,7 @@ async def async_unload_entry(hass, entry): """Unload a config entry.""" hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID]) hass.data[DOMAIN]["unsub_device_tracker"].pop(entry.entry_id)() - return await hass.config_entries.async_forward_entry_unload(entry, DEVICE_TRACKER) + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) async_remove_entry = config_entry_flow.webhook_async_remove_entry diff --git a/homeassistant/components/logi_circle/__init__.py b/homeassistant/components/logi_circle/__init__.py index 1311e50f29347..9e1a4803e110c 100644 --- a/homeassistant/components/logi_circle/__init__.py +++ b/homeassistant/components/logi_circle/__init__.py @@ -179,10 +179,7 @@ async def async_setup_entry(hass, entry): hass.data[DATA_LOGI] = logi_circle - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) async def service_handler(service): """Dispatch service calls to target entities.""" @@ -229,8 +226,7 @@ async def shut_down(event=None): async def async_unload_entry(hass, entry): """Unload a config entry.""" - for platform in PLATFORMS: - await hass.config_entries.async_forward_entry_unload(entry, platform) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) logi_circle = hass.data.pop(DATA_LOGI) @@ -238,4 +234,4 @@ async def async_unload_entry(hass, entry): # and clear all locally cached tokens await logi_circle.auth_provider.clear_authorization() - return True + return unload_ok diff --git a/homeassistant/components/luftdaten/__init__.py b/homeassistant/components/luftdaten/__init__.py index ca1b9aed4ff0a..6db0ad96f643b 100644 --- a/homeassistant/components/luftdaten/__init__.py +++ b/homeassistant/components/luftdaten/__init__.py @@ -33,6 +33,8 @@ DATA_LUFTDATEN_LISTENER = "data_luftdaten_listener" DEFAULT_ATTRIBUTION = "Data provided by luftdaten.info" +PLATFORMS = ["sensor"] + SENSOR_HUMIDITY = "humidity" SENSOR_PM10 = "P1" SENSOR_PM2_5 = "P2" @@ -152,9 +154,7 @@ async def async_setup_entry(hass, config_entry): except LuftdatenError as err: raise ConfigEntryNotReady from err - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, "sensor") - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) async def refresh_sensors(event_time): """Refresh Luftdaten data.""" @@ -181,7 +181,7 @@ async def async_unload_entry(hass, config_entry): hass.data[DOMAIN][DATA_LUFTDATEN_CLIENT].pop(config_entry.entry_id) - return await hass.config_entries.async_forward_entry_unload(config_entry, "sensor") + return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) class LuftDatenData: diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index 89eef781c256c..144a9a74c554a 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -137,10 +137,7 @@ async def async_setup_entry(hass, config_entry): # pico remotes to control other devices. await async_setup_lip(hass, config_entry, bridge.lip_devices) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) return True @@ -283,15 +280,9 @@ async def async_unload_entry(hass, config_entry): if data[BRIDGE_LIP]: await data[BRIDGE_LIP].async_stop() - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS ) - if unload_ok: hass.data[DOMAIN].pop(config_entry.entry_id) diff --git a/homeassistant/components/lyric/__init__.py b/homeassistant/components/lyric/__init__.py index 7a6e00da7d2d8..9f6d38ad4e781 100644 --- a/homeassistant/components/lyric/__init__.py +++ b/homeassistant/components/lyric/__init__.py @@ -1,7 +1,6 @@ """The Honeywell Lyric integration.""" from __future__ import annotations -import asyncio from datetime import timedelta import logging from typing import Any @@ -117,24 +116,14 @@ async def async_update_data() -> Lyric: # Fetch initial data so we have data when entities subscribe await coordinator.async_config_entry_first_refresh() - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/mazda/__init__.py b/homeassistant/components/mazda/__init__.py index c640dd2528ff0..f6e31fa4357fc 100644 --- a/homeassistant/components/mazda/__init__.py +++ b/homeassistant/components/mazda/__init__.py @@ -1,5 +1,4 @@ """The Mazda Connected Services integration.""" -import asyncio from datetime import timedelta import logging @@ -101,24 +100,14 @@ async def async_update_data(): await coordinator.async_config_entry_first_refresh() # Setup components - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/melcloud/__init__.py b/homeassistant/components/melcloud/__init__.py index 528854308d6ff..fcff9ab330437 100644 --- a/homeassistant/components/melcloud/__init__.py +++ b/homeassistant/components/melcloud/__init__.py @@ -63,25 +63,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): conf = entry.data mel_devices = await mel_devices_setup(hass, conf[CONF_TOKEN]) hass.data.setdefault(DOMAIN, {}).update({entry.entry_id: mel_devices}) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass, config_entry): """Unload a config entry.""" - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS ) hass.data[DOMAIN].pop(config_entry.entry_id) if not hass.data[DOMAIN]: hass.data.pop(DOMAIN) - return True + return unload_ok class MelCloudDevice: diff --git a/homeassistant/components/met/__init__.py b/homeassistant/components/met/__init__.py index 1e1a203342ebe..dd932a7595730 100644 --- a/homeassistant/components/met/__init__.py +++ b/homeassistant/components/met/__init__.py @@ -27,6 +27,7 @@ URL = "https://aa015h6buqvih86i1.api.met.no/weatherapi/locationforecast/2.0/complete" +PLATFORMS = ["weather"] _LOGGER = logging.getLogger(__name__) @@ -56,20 +57,21 @@ async def async_setup_entry(hass, config_entry): hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][config_entry.entry_id] = coordinator - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, "weather") - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) return True async def async_unload_entry(hass, config_entry): """Unload a config entry.""" - await hass.config_entries.async_forward_entry_unload(config_entry, "weather") + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS + ) + hass.data[DOMAIN][config_entry.entry_id].untrack_home() hass.data[DOMAIN].pop(config_entry.entry_id) - return True + return unload_ok class MetDataUpdateCoordinator(DataUpdateCoordinator): diff --git a/homeassistant/components/met_eireann/__init__.py b/homeassistant/components/met_eireann/__init__.py index 365e4dbafb364..c70f436009d5a 100644 --- a/homeassistant/components/met_eireann/__init__.py +++ b/homeassistant/components/met_eireann/__init__.py @@ -15,6 +15,8 @@ UPDATE_INTERVAL = timedelta(minutes=60) +PLATFORMS = ["weather"] + async def async_setup_entry(hass, config_entry): """Set up Met Éireann as config entry.""" @@ -47,19 +49,19 @@ async def _async_update_data(): hass.data[DOMAIN][config_entry.entry_id] = coordinator - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, "weather") - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) return True async def async_unload_entry(hass, config_entry): """Unload a config entry.""" - await hass.config_entries.async_forward_entry_unload(config_entry, "weather") + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS + ) hass.data[DOMAIN].pop(config_entry.entry_id) - return True + return unload_ok class MetEireannWeatherData: diff --git a/homeassistant/components/meteo_france/__init__.py b/homeassistant/components/meteo_france/__init__.py index cdd55c06db75c..4ec03e4f5a59d 100644 --- a/homeassistant/components/meteo_france/__init__.py +++ b/homeassistant/components/meteo_france/__init__.py @@ -1,5 +1,4 @@ """Support for Meteo-France weather data.""" -import asyncio from datetime import timedelta import logging @@ -173,10 +172,7 @@ async def _async_update_data_alert(): UNDO_UPDATE_LISTENER: undo_listener, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -194,14 +190,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): department, ) - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/metoffice/__init__.py b/homeassistant/components/metoffice/__init__.py index 5dfeceb79f890..9bf9e44b72af8 100644 --- a/homeassistant/components/metoffice/__init__.py +++ b/homeassistant/components/metoffice/__init__.py @@ -1,6 +1,5 @@ """The Met Office integration.""" -import asyncio import logging from homeassistant.config_entries import ConfigEntry @@ -56,24 +55,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): if metoffice_data.now is None: raise ConfigEntryNotReady() - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) if not hass.data[DOMAIN]: diff --git a/homeassistant/components/mikrotik/__init__.py b/homeassistant/components/mikrotik/__init__.py index 9a8ee7bdb45c2..cd96cba327cba 100644 --- a/homeassistant/components/mikrotik/__init__.py +++ b/homeassistant/components/mikrotik/__init__.py @@ -21,6 +21,7 @@ DEFAULT_DETECTION_TIME, DEFAULT_NAME, DOMAIN, + PLATFORMS, ) from .hub import MikrotikHub @@ -42,6 +43,7 @@ ) ) + CONFIG_SCHEMA = vol.Schema( {DOMAIN: vol.All(cv.ensure_list, [MIKROTIK_SCHEMA])}, extra=vol.ALLOW_EXTRA ) @@ -84,8 +86,10 @@ async def async_setup_entry(hass, config_entry): async def async_unload_entry(hass, config_entry): """Unload a config entry.""" - await hass.config_entries.async_forward_entry_unload(config_entry, "device_tracker") + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS + ) hass.data[DOMAIN].pop(config_entry.entry_id) - return True + return unload_ok diff --git a/homeassistant/components/mikrotik/const.py b/homeassistant/components/mikrotik/const.py index d81e8878d1cf4..1fbe0af5c1b23 100644 --- a/homeassistant/components/mikrotik/const.py +++ b/homeassistant/components/mikrotik/const.py @@ -37,6 +37,8 @@ IS_CAPSMAN: "/caps-man/interface/print", } +PLATFORMS = ["device_tracker"] + ATTR_DEVICE_TRACKER = [ "comment", "mac-address", diff --git a/homeassistant/components/mikrotik/hub.py b/homeassistant/components/mikrotik/hub.py index 2f1f89ba60da3..63be0a4a358ad 100644 --- a/homeassistant/components/mikrotik/hub.py +++ b/homeassistant/components/mikrotik/hub.py @@ -31,6 +31,7 @@ IS_WIRELESS, MIKROTIK_SERVICES, NAME, + PLATFORMS, WIRELESS, ) from .errors import CannotConnect, LoginError @@ -385,11 +386,7 @@ async def async_setup(self): await self.hass.async_add_executor_job(self._mk_data.get_hub_details) await self.hass.async_add_executor_job(self._mk_data.update) - self.hass.async_create_task( - self.hass.config_entries.async_forward_entry_setup( - self.config_entry, "device_tracker" - ) - ) + self.hass.config_entries.async_setup_platforms(self.config_entry, PLATFORMS) return True diff --git a/homeassistant/components/mill/__init__.py b/homeassistant/components/mill/__init__.py index e58a7865e2857..115bb5eb33c19 100644 --- a/homeassistant/components/mill/__init__.py +++ b/homeassistant/components/mill/__init__.py @@ -1,17 +1,14 @@ """The mill component.""" +PLATFORMS = ["climate"] + async def async_setup_entry(hass, entry): """Set up the Mill heater.""" - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "climate") - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True -async def async_unload_entry(hass, config_entry): +async def async_unload_entry(hass, entry): """Unload a config entry.""" - unload_ok = await hass.config_entries.async_forward_entry_unload( - config_entry, "climate" - ) - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/minecraft_server/__init__.py b/homeassistant/components/minecraft_server/__init__.py index e887f31ae0f9b..5d507006b05a1 100644 --- a/homeassistant/components/minecraft_server/__init__.py +++ b/homeassistant/components/minecraft_server/__init__.py @@ -1,7 +1,6 @@ """The Minecraft Server integration.""" from __future__ import annotations -import asyncio from datetime import datetime, timedelta import logging from typing import Any @@ -44,10 +43,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b server.start_periodic_update() # Set up platforms. - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) return True @@ -58,18 +54,15 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> server = hass.data[DOMAIN][unique_id] # Unload platforms. - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS ) # Clean up. server.stop_periodic_update() hass.data[DOMAIN].pop(unique_id) - return True + return unload_ok class MinecraftServer: diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py index 1321818b91fec..0fe1386d7ce1f 100644 --- a/homeassistant/components/mobile_app/__init__.py +++ b/homeassistant/components/mobile_app/__init__.py @@ -1,5 +1,4 @@ """Integrates Native Apps to Home Assistant.""" -import asyncio from contextlib import suppress from homeassistant.components import cloud, notify as hass_notify @@ -89,10 +88,7 @@ async def async_setup_entry(hass, entry): registration_name = f"Mobile App: {registration[ATTR_DEVICE_NAME]}" webhook_register(hass, DOMAIN, registration_name, webhook_id, handle_webhook) - for domain in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, domain) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) await hass_notify.async_reload(hass, DOMAIN) @@ -101,14 +97,7 @@ async def async_setup_entry(hass, entry): async def async_unload_entry(hass, entry): """Unload a mobile app entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if not unload_ok: return False diff --git a/homeassistant/components/monoprice/__init__.py b/homeassistant/components/monoprice/__init__.py index 61aa8b408cf79..f543220b5b9eb 100644 --- a/homeassistant/components/monoprice/__init__.py +++ b/homeassistant/components/monoprice/__init__.py @@ -1,5 +1,4 @@ """The Monoprice 6-Zone Amplifier integration.""" -import asyncio import logging from pymonoprice import get_monoprice @@ -49,25 +48,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): FIRST_RUN: first_run, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) - + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/motion_blinds/__init__.py b/homeassistant/components/motion_blinds/__init__.py index 73a27c90140b5..d2400beb4f563 100644 --- a/homeassistant/components/motion_blinds/__init__.py +++ b/homeassistant/components/motion_blinds/__init__.py @@ -1,5 +1,4 @@ """The motion_blinds component.""" -import asyncio from datetime import timedelta import logging from socket import timeout @@ -159,10 +158,7 @@ def stop_motion_multicast(event): sw_version=motion_gateway.protocol, ) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -171,13 +167,8 @@ async def async_unload_entry( hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry ): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS ) if unload_ok: diff --git a/homeassistant/components/motioneye/__init__.py b/homeassistant/components/motioneye/__init__.py index 5387de8225c1a..cb5e80b9c989c 100644 --- a/homeassistant/components/motioneye/__init__.py +++ b/homeassistant/components/motioneye/__init__.py @@ -225,14 +225,7 @@ async def setup_then_listen() -> None: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: config_data = hass.data[DOMAIN].pop(entry.entry_id) await config_data[CONF_CLIENT].async_client_close() diff --git a/homeassistant/components/mullvad/__init__.py b/homeassistant/components/mullvad/__init__.py index 325c0603f32bd..d89c947a4f383 100644 --- a/homeassistant/components/mullvad/__init__.py +++ b/homeassistant/components/mullvad/__init__.py @@ -1,5 +1,4 @@ """The Mullvad VPN integration.""" -import asyncio from datetime import timedelta import logging @@ -34,25 +33,14 @@ async def async_get_mullvad_api_data(): hass.data[DOMAIN] = coordinator - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) - + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: del hass.data[DOMAIN] diff --git a/homeassistant/components/myq/__init__.py b/homeassistant/components/myq/__init__.py index b25751d7270db..fd3a46bbb5a78 100644 --- a/homeassistant/components/myq/__init__.py +++ b/homeassistant/components/myq/__init__.py @@ -1,5 +1,4 @@ """The MyQ integration.""" -import asyncio from datetime import timedelta import logging @@ -18,17 +17,10 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the MyQ component.""" - - hass.data.setdefault(DOMAIN, {}) - - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up MyQ from a config entry.""" + hass.data.setdefault(DOMAIN, {}) websession = aiohttp_client.async_get_clientsession(hass) conf = entry.data @@ -58,24 +50,14 @@ async def async_update_data(): hass.data[DOMAIN][entry.entry_id] = {MYQ_GATEWAY: myq, MYQ_COORDINATOR: coordinator} - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/mysensors/__init__.py b/homeassistant/components/mysensors/__init__.py index c5ed31326a353..812e6bf167095 100644 --- a/homeassistant/components/mysensors/__init__.py +++ b/homeassistant/components/mysensors/__init__.py @@ -239,13 +239,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: gateway = get_mysensors_gateway(hass, entry.entry_id) - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS_WITH_ENTRY_SUPPORT - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + entry, PLATFORMS_WITH_ENTRY_SUPPORT ) if not unload_ok: return False diff --git a/tests/components/kostal_plenticore/test_config_flow.py b/tests/components/kostal_plenticore/test_config_flow.py index 04a69892b4381..7ce95f71e8e99 100644 --- a/tests/components/kostal_plenticore/test_config_flow.py +++ b/tests/components/kostal_plenticore/test_config_flow.py @@ -23,8 +23,6 @@ async def test_formx(hass): with patch( "homeassistant.components.kostal_plenticore.config_flow.PlenticoreApiClient" ) as mock_api_class, patch( - "homeassistant.components.kostal_plenticore.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.kostal_plenticore.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -63,7 +61,6 @@ async def test_formx(hass): "password": "test-password", } await hass.async_block_till_done() - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/myq/test_config_flow.py b/tests/components/myq/test_config_flow.py index 683b6beab8aa8..3ae2da82f4650 100644 --- a/tests/components/myq/test_config_flow.py +++ b/tests/components/myq/test_config_flow.py @@ -23,8 +23,6 @@ async def test_form_user(hass): "homeassistant.components.myq.config_flow.pymyq.login", return_value=True, ), patch( - "homeassistant.components.myq.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.myq.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -40,7 +38,6 @@ async def test_form_user(hass): "username": "test-username", "password": "test-password", } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 From ce6469081729f8c060da9a7043a513545b648082 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 27 Apr 2021 19:27:12 +0200 Subject: [PATCH 0582/1317] Make number of stored traces configurable (#49728) --- .../components/automation/__init__.py | 5 ++ homeassistant/components/automation/config.py | 3 + homeassistant/components/automation/const.py | 1 + homeassistant/components/automation/trace.py | 7 +- homeassistant/components/script/__init__.py | 6 +- homeassistant/components/script/trace.py | 5 +- homeassistant/components/trace/__init__.py | 17 ++++- homeassistant/components/trace/const.py | 3 +- .../components/trace/websocket_api.py | 4 +- tests/components/trace/test_websocket_api.py | 76 ++++++++++++++++--- 10 files changed, 104 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 4493dc23e0da0..a338f6cf161f5 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -74,6 +74,7 @@ from .const import ( CONF_ACTION, CONF_INITIAL_STATE, + CONF_TRACE, CONF_TRIGGER, CONF_TRIGGER_VARIABLES, DEFAULT_INITIAL_STATE, @@ -274,6 +275,7 @@ def __init__( trigger_variables, raw_config, blueprint_inputs, + trace_config, ): """Initialize an automation entity.""" self._id = automation_id @@ -292,6 +294,7 @@ def __init__( self._trigger_variables: ScriptVariables = trigger_variables self._raw_config = raw_config self._blueprint_inputs = blueprint_inputs + self._trace_config = trace_config @property def name(self): @@ -444,6 +447,7 @@ async def async_trigger(self, run_variables, context=None, skip_condition=False) self._raw_config, self._blueprint_inputs, trigger_context, + self._trace_config, ) as automation_trace: if self._variables: try: @@ -682,6 +686,7 @@ async def _async_process_config( config_block.get(CONF_TRIGGER_VARIABLES), raw_config, raw_blueprint_inputs, + config_block[CONF_TRACE], ) entities.append(entity) diff --git a/homeassistant/components/automation/config.py b/homeassistant/components/automation/config.py index b4b8b49fa3e5b..e28fa5c477f68 100644 --- a/homeassistant/components/automation/config.py +++ b/homeassistant/components/automation/config.py @@ -8,6 +8,7 @@ from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) +from homeassistant.components.trace import TRACE_CONFIG_SCHEMA from homeassistant.config import async_log_exception, config_without_domain from homeassistant.const import ( CONF_ALIAS, @@ -26,6 +27,7 @@ CONF_ACTION, CONF_HIDE_ENTITY, CONF_INITIAL_STATE, + CONF_TRACE, CONF_TRIGGER, CONF_TRIGGER_VARIABLES, DOMAIN, @@ -45,6 +47,7 @@ CONF_ID: str, CONF_ALIAS: cv.string, vol.Optional(CONF_DESCRIPTION): cv.string, + vol.Optional(CONF_TRACE, default={}): TRACE_CONFIG_SCHEMA, vol.Optional(CONF_INITIAL_STATE): cv.boolean, vol.Optional(CONF_HIDE_ENTITY): cv.boolean, vol.Required(CONF_TRIGGER): cv.TRIGGER_SCHEMA, diff --git a/homeassistant/components/automation/const.py b/homeassistant/components/automation/const.py index d6f34ddfeb642..a82c78ded7783 100644 --- a/homeassistant/components/automation/const.py +++ b/homeassistant/components/automation/const.py @@ -12,6 +12,7 @@ CONF_INITIAL_STATE = "initial_state" CONF_BLUEPRINT = "blueprint" CONF_INPUT = "input" +CONF_TRACE = "trace" DEFAULT_INITIAL_STATE = True diff --git a/homeassistant/components/automation/trace.py b/homeassistant/components/automation/trace.py index cfdbe02056b92..102aeda5a651f 100644 --- a/homeassistant/components/automation/trace.py +++ b/homeassistant/components/automation/trace.py @@ -5,6 +5,7 @@ from typing import Any from homeassistant.components.trace import ActionTrace, async_store_trace +from homeassistant.components.trace.const import CONF_STORED_TRACES from homeassistant.core import Context # mypy: allow-untyped-calls, allow-untyped-defs @@ -38,10 +39,12 @@ def as_short_dict(self) -> dict[str, Any]: @contextmanager -def trace_automation(hass, automation_id, config, blueprint_inputs, context): +def trace_automation( + hass, automation_id, config, blueprint_inputs, context, trace_config +): """Trace action execution of automation with automation_id.""" trace = AutomationTrace(automation_id, config, blueprint_inputs, context) - async_store_trace(hass, trace) + async_store_trace(hass, trace, trace_config[CONF_STORED_TRACES]) try: yield trace diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index 8f2e0743f77d2..e851850a924b9 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -6,6 +6,7 @@ import voluptuous as vol +from homeassistant.components.trace import TRACE_CONFIG_SCHEMA from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_MODE, @@ -58,6 +59,7 @@ CONF_EXAMPLE = "example" CONF_FIELDS = "fields" CONF_REQUIRED = "required" +CONF_TRACE = "trace" ENTITY_ID_FORMAT = DOMAIN + ".{}" @@ -67,6 +69,7 @@ SCRIPT_ENTRY_SCHEMA = make_script_schema( { vol.Optional(CONF_ALIAS): cv.string, + vol.Optional(CONF_TRACE, default={}): TRACE_CONFIG_SCHEMA, vol.Optional(CONF_ICON): cv.icon, vol.Required(CONF_SEQUENCE): cv.SCRIPT_SCHEMA, vol.Optional(CONF_DESCRIPTION, default=""): cv.string, @@ -319,6 +322,7 @@ def __init__(self, hass, object_id, cfg, raw_config): ) self._changed = asyncio.Event() self._raw_config = raw_config + self._trace_config = cfg[CONF_TRACE] @property def should_poll(self): @@ -384,7 +388,7 @@ async def async_turn_on(self, **kwargs): async def _async_run(self, variables, context): with trace_script( - self.hass, self.object_id, self._raw_config, context + self.hass, self.object_id, self._raw_config, context, self._trace_config ) as script_trace: # Prepare tracing the execution of the script's sequence script_trace.set_trace(trace_get()) diff --git a/homeassistant/components/script/trace.py b/homeassistant/components/script/trace.py index a8053feaa1e4a..9e9812e51be8d 100644 --- a/homeassistant/components/script/trace.py +++ b/homeassistant/components/script/trace.py @@ -5,6 +5,7 @@ from typing import Any from homeassistant.components.trace import ActionTrace, async_store_trace +from homeassistant.components.trace.const import CONF_STORED_TRACES from homeassistant.core import Context @@ -23,10 +24,10 @@ def __init__( @contextmanager -def trace_script(hass, item_id, config, context): +def trace_script(hass, item_id, config, context, trace_config): """Trace execution of a script.""" trace = ScriptTrace(item_id, config, context) - async_store_trace(hass, trace) + async_store_trace(hass, trace, trace_config[CONF_STORED_TRACES]) try: yield trace diff --git a/homeassistant/components/trace/__init__.py b/homeassistant/components/trace/__init__.py index e845f92806849..bf78f754b86ab 100644 --- a/homeassistant/components/trace/__init__.py +++ b/homeassistant/components/trace/__init__.py @@ -6,7 +6,10 @@ from itertools import count from typing import Any +import voluptuous as vol + from homeassistant.core import Context +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.trace import ( TraceElement, script_execution_get, @@ -17,11 +20,15 @@ import homeassistant.util.dt as dt_util from . import websocket_api -from .const import DATA_TRACE, STORED_TRACES +from .const import CONF_STORED_TRACES, DATA_TRACE, DEFAULT_STORED_TRACES from .utils import LimitedSizeDict DOMAIN = "trace" +TRACE_CONFIG_SCHEMA = { + vol.Optional(CONF_STORED_TRACES, default=DEFAULT_STORED_TRACES): cv.positive_int +} + async def async_setup(hass, config): """Initialize the trace integration.""" @@ -30,18 +37,20 @@ async def async_setup(hass, config): return True -def async_store_trace(hass, trace): +def async_store_trace(hass, trace, stored_traces): """Store a trace if its item_id is valid.""" key = trace.key if key[1]: traces = hass.data[DATA_TRACE] if key not in traces: - traces[key] = LimitedSizeDict(size_limit=STORED_TRACES) + traces[key] = LimitedSizeDict(size_limit=stored_traces) + else: + traces[key].size_limit = stored_traces traces[key][trace.run_id] = trace class ActionTrace: - """Base container for an script or automation trace.""" + """Base container for a script or automation trace.""" _run_ids = count(0) diff --git a/homeassistant/components/trace/const.py b/homeassistant/components/trace/const.py index 05942d7ee4dc6..f64bf4e3f383d 100644 --- a/homeassistant/components/trace/const.py +++ b/homeassistant/components/trace/const.py @@ -1,4 +1,5 @@ """Shared constants for script and automation tracing and debugging.""" +CONF_STORED_TRACES = "stored_traces" DATA_TRACE = "trace" -STORED_TRACES = 5 # Stored traces per script or automation +DEFAULT_STORED_TRACES = 5 # Stored traces per script or automation diff --git a/homeassistant/components/trace/websocket_api.py b/homeassistant/components/trace/websocket_api.py index 8f59660e74dab..59d8c58635e6a 100644 --- a/homeassistant/components/trace/websocket_api.py +++ b/homeassistant/components/trace/websocket_api.py @@ -57,7 +57,7 @@ def async_setup(hass: HomeAssistant) -> None: } ) def websocket_trace_get(hass, connection, msg): - """Get an script or automation trace.""" + """Get a script or automation trace.""" key = (msg["domain"], msg["item_id"]) run_id = msg["run_id"] @@ -77,7 +77,7 @@ def websocket_trace_get(hass, connection, msg): def get_debug_traces(hass, key): - """Return a serializable list of debug traces for an script or automation.""" + """Return a serializable list of debug traces for a script or automation.""" traces = [] for trace in hass.data[DATA_TRACE].get(key, {}).values(): diff --git a/tests/components/trace/test_websocket_api.py b/tests/components/trace/test_websocket_api.py index 8f8428dd51730..4c6ff88fa1b5f 100644 --- a/tests/components/trace/test_websocket_api.py +++ b/tests/components/trace/test_websocket_api.py @@ -4,7 +4,7 @@ import pytest from homeassistant.bootstrap import async_setup_component -from homeassistant.components.trace.const import STORED_TRACES +from homeassistant.components.trace.const import DEFAULT_STORED_TRACES from homeassistant.core import Context, callback from homeassistant.helpers.typing import UNDEFINED @@ -12,7 +12,7 @@ def _find_run_id(traces, trace_type, item_id): - """Find newest run_id for an script or automation.""" + """Find newest run_id for a script or automation.""" for trace in reversed(traces): if trace["domain"] == trace_type and trace["item_id"] == item_id: return trace["run_id"] @@ -21,7 +21,7 @@ def _find_run_id(traces, trace_type, item_id): def _find_traces(traces, trace_type, item_id): - """Find traces for an script or automation.""" + """Find traces for a script or automation.""" return [ trace for trace in traces @@ -29,7 +29,9 @@ def _find_traces(traces, trace_type, item_id): ] -async def _setup_automation_or_script(hass, domain, configs, script_config=None): +async def _setup_automation_or_script( + hass, domain, configs, script_config=None, stored_traces=None +): """Set up automations or scripts from automation config.""" if domain == "script": configs = {config["id"]: {"sequence": config["action"]} for config in configs} @@ -42,6 +44,16 @@ async def _setup_automation_or_script(hass, domain, configs, script_config=None) else: configs = {**configs, **script_config} + if stored_traces is not None: + if domain == "script": + for config in configs.values(): + config["trace"] = {} + config["trace"]["stored_traces"] = stored_traces + else: + for config in configs: + config["trace"] = {} + config["trace"]["stored_traces"] = stored_traces + assert await async_setup_component(hass, domain, {domain: configs}) @@ -97,7 +109,7 @@ async def test_get_trace( context_key, condition_results, ): - """Test tracing an script or automation.""" + """Test tracing a script or automation.""" id = 1 def next_id(): @@ -347,8 +359,11 @@ async def test_get_invalid_trace(hass, hass_ws_client, domain): assert response["error"]["code"] == "not_found" -@pytest.mark.parametrize("domain", ["automation", "script"]) -async def test_trace_overflow(hass, hass_ws_client, domain): +@pytest.mark.parametrize( + "domain,stored_traces", + [("automation", None), ("automation", 10), ("script", None), ("script", 10)], +) +async def test_trace_overflow(hass, hass_ws_client, domain, stored_traces): """Test the number of stored traces per script or automation is limited.""" id = 1 @@ -367,7 +382,9 @@ def next_id(): "trigger": {"platform": "event", "event_type": "test_event2"}, "action": {"event": "another_event"}, } - await _setup_automation_or_script(hass, domain, [sun_config, moon_config]) + await _setup_automation_or_script( + hass, domain, [sun_config, moon_config], stored_traces=stored_traces + ) client = await hass_ws_client() @@ -390,7 +407,7 @@ def next_id(): assert len(_find_traces(response["result"], domain, "sun")) == 1 # Trigger "moon" enough times to overflow the max number of stored traces - for _ in range(STORED_TRACES): + for _ in range(stored_traces or DEFAULT_STORED_TRACES): await _run_automation_or_script(hass, domain, moon_config, "test_event2") await hass.async_block_till_done() @@ -398,13 +415,50 @@ def next_id(): response = await client.receive_json() assert response["success"] moon_traces = _find_traces(response["result"], domain, "moon") - assert len(moon_traces) == STORED_TRACES + assert len(moon_traces) == stored_traces or DEFAULT_STORED_TRACES assert moon_traces[0] assert int(moon_traces[0]["run_id"]) == int(moon_run_id) + 1 - assert int(moon_traces[-1]["run_id"]) == int(moon_run_id) + STORED_TRACES + assert int(moon_traces[-1]["run_id"]) == int(moon_run_id) + ( + stored_traces or DEFAULT_STORED_TRACES + ) assert len(_find_traces(response["result"], domain, "sun")) == 1 +@pytest.mark.parametrize("domain", ["automation", "script"]) +async def test_trace_no_traces(hass, hass_ws_client, domain): + """Test the storing traces for a script or automation can be disabled.""" + id = 1 + + def next_id(): + nonlocal id + id += 1 + return id + + sun_config = { + "id": "sun", + "trigger": {"platform": "event", "event_type": "test_event"}, + "action": {"event": "some_event"}, + } + await _setup_automation_or_script(hass, domain, [sun_config], stored_traces=0) + + client = await hass_ws_client() + + await client.send_json({"id": next_id(), "type": "trace/list", "domain": domain}) + response = await client.receive_json() + assert response["success"] + assert response["result"] == [] + + # Trigger "sun" automation / script once + await _run_automation_or_script(hass, domain, sun_config, "test_event") + await hass.async_block_till_done() + + # List traces + await client.send_json({"id": next_id(), "type": "trace/list", "domain": domain}) + response = await client.receive_json() + assert response["success"] + assert len(_find_traces(response["result"], domain, "sun")) == 0 + + @pytest.mark.parametrize( "domain, prefix, trigger, last_step, script_execution", [ From ba76d9f97723f78026f540794e54ccded7b7d266 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Tue, 27 Apr 2021 13:39:41 -0400 Subject: [PATCH 0583/1317] Add selectors to zha services (#49773) * Add selectors to zha services * Use IEEE --- homeassistant/components/zha/services.yaml | 216 ++++++++++++++++++++- 1 file changed, 213 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/services.yaml b/homeassistant/components/zha/services.yaml index e756edbc48b4f..63f30c2e3f11f 100644 --- a/homeassistant/components/zha/services.yaml +++ b/homeassistant/components/zha/services.yaml @@ -1,168 +1,378 @@ # Describes the format for available zha services permit: + name: Permit description: Allow nodes to join the Zigbee network. fields: duration: + name: Duration description: Time to permit joins, in seconds example: 60 - ieee_address: + default: 60 + selector: + number: + min: 0 + max: 254 + unit_of_measurement: seconds + ieee: + name: IEEE description: IEEE address of the node permitting new joins example: "00:0d:6f:00:05:7d:2d:34" + selector: + text: source_ieee: + name: Source IEEE description: IEEE address of the joining device (must be used with install code) example: "00:0a:bf:00:01:10:23:35" + selector: + text: install_code: + name: Install Code description: Install code of the joining device (must be used with source_ieee) example: "1234-5678-1234-5678-AABB-CCDD-AABB-CCDD-EEFF" + selector: + text: qr_code: + name: QR Code description: value of the QR install code (different between vendors) example: "Z:000D6FFFFED4163B$I:52797BF4A5084DAA8E1712B61741CA024051" + selector: + text: remove: + name: Remove description: Remove a node from the Zigbee network. fields: - ieee_address: + ieee: + name: IEEE description: IEEE address of the node to remove + required: true example: "00:0d:6f:00:05:7d:2d:34" + selector: + text: reconfigure_device: + name: Reconfigure device description: >- Reconfigure ZHA device (heal device). Use this if you are having issues with the device. If the device in question is a battery powered device please ensure it is awake and accepting commands when you use this service. fields: - ieee_address: + ieee: + name: IEEE description: IEEE address of the device to reconfigure + required: true example: "00:0d:6f:00:05:7d:2d:34" + selector: + text: set_zigbee_cluster_attribute: + name: Set zigbee cluster attribute description: >- Set attribute value for the specified cluster on the specified entity. fields: ieee: + name: IEEE description: IEEE address for the device + required: true example: "00:0d:6f:00:05:7d:2d:34" endpoint_id: + name: Endpoint ID description: Endpoint id for the cluster + required: true example: 1 cluster_id: + name: Cluster ID description: ZCL cluster to retrieve attributes for + required: true example: 6 + selector: + number: + min: 1 + max: 65535 cluster_type: + name: Cluster Type description: type of the cluster (in or out) example: "out" + default: "in" + selector: + select: + options: + - "in" + - "out" attribute: + name: Attribute description: id of the attribute to set + required: true example: 0 + selector: + number: + min: 1 + max: 65535 value: + name: Value description: value to write to the attribute + required: true example: 0x0001 + selector: + text: manufacturer: + name: Manufacturer description: manufacturer code example: 0x00FC + selector: + text: issue_zigbee_cluster_command: + name: Issue zigbee cluster command description: >- Issue command on the specified cluster on the specified entity. fields: ieee: + name: IEEE description: IEEE address for the device + required: true example: "00:0d:6f:00:05:7d:2d:34" + selector: + text: endpoint_id: + name: Endpoint ID description: Endpoint id for the cluster + required: true example: 1 + selector: + number: + min: 1 + max: 65535 cluster_id: + name: Cluster ID description: ZCL cluster to retrieve attributes for + required: true example: 6 + selector: + number: + min: 1 + max: 65535 cluster_type: + name: Cluster Type description: type of the cluster (in or out) example: "out" + default: "in" + selector: + select: + options: + - "in" + - "out" command: + name: Command description: id of the command to execute + required: true example: 0 + selector: + number: + min: 1 + max: 65535 command_type: + name: Command Type description: type of the command to execute (client or server) + required: true example: "server" + selector: + select: + options: + - "client" + - "server" args: + name: Args description: args to pass to the command example: "[arg1, arg2, argN]" + selector: + object: manufacturer: + name: Manufacturer description: manufacturer code example: 0x00FC + selector: + text: issue_zigbee_group_command: + name: Issue zigbee group command description: >- Issue command on the specified cluster on the specified group. fields: group: + name: Group description: Hexadecimal address of the group + required: true example: 0x0222 + selector: + text: cluster_id: + name: Cluster ID description: ZCL cluster to send command to + required: true example: 6 + selector: + number: + min: 1 + max: 65535 + cluster_type: + name: Cluster Type + description: type of the cluster (in or out) + example: "out" + default: "in" + selector: + select: + options: + - "in" + - "out" command: + name: Command description: id of the command to execute + required: true example: 0 + selector: + number: + min: 1 + max: 65535 args: + name: Args description: args to pass to the command example: "[arg1, arg2, argN]" + selector: + object: manufacturer: + name: Manufacturer description: manufacturer code example: 0x00FC + selector: + text: warning_device_squawk: + name: Warning device squawk description: >- This service uses the WD capabilities to emit a quick audible/visible pulse called a "squawk". The squawk command has no effect if the WD is currently active (warning in progress). fields: ieee: + name: IEEE description: IEEE address for the device + required: true example: "00:0d:6f:00:05:7d:2d:34" + selector: + text: mode: + name: Mode description: >- The Squawk Mode field is used as a 4-bit enumeration, and can have one of the values shown in Table 8-24 of the ZCL spec - Squawk Mode Field. The exact operation of each mode (how the WD “squawks”) is implementation specific. example: 1 + default: 0 + selector: + number: + min: 0 + max: 1 + mode: box strobe: + name: Strobe description: >- The strobe field is used as a Boolean, and determines if the visual indication is also required in addition to the audible squawk, as shown in Table 8-25 of the ZCL spec - Strobe Bit. example: 1 + default: 1 + selector: + number: + min: 0 + max: 1 + mode: box level: + name: Level description: >- The squawk level field is used as a 2-bit enumeration, and determines the intensity of audible squawk sound as shown in Table 8-26 of the ZCL spec - Squawk Level Field Values. example: 2 + default: 2 + selector: + number: + min: 0 + max: 3 + mode: box warning_device_warn: + name: Warning device warn description: >- This service starts the WD operation. The WD alerts the surrounding area by audible (siren) and visual (strobe) signals. fields: ieee: + name: IEEE description: IEEE address for the device + required: true example: "00:0d:6f:00:05:7d:2d:34" + selector: + text: mode: + name: Mode description: >- The Warning Mode field is used as an 4-bit enumeration, can have one of the values 0-6 defined below in table 8-20 of the ZCL spec. The exact behavior of the WD device in each mode is according to the relevant security standards. example: 1 + default: 3 + selector: + number: + min: 0 + max: 6 + mode: box strobe: + name: Strobe description: >- The Strobe field is used as a 2-bit enumeration, and determines if the visual indication is required in addition to the audible siren, as indicated in Table 8-21 of the ZCL spec. "0" means no strobe, "1" means strobe. If the strobe field is “1” and the Warning Mode is “0” (“Stop”) then only the strobe is activated. example: 1 + default: 1 + selector: + number: + min: 0 + max: 1 + mode: box level: + name: Level description: >- The Siren Level field is used as a 2-bit enumeration, and indicates the intensity of audible squawk sound as shown in Table 8-22 of the ZCL spec. example: 2 + default: 2 + selector: + number: + min: 0 + max: 3 + mode: box duration: + name: Duration description: >- Requested duration of warning, in seconds (16 bit). If both Strobe and Warning Mode are "0" this field SHALL be ignored. example: 2 + default: 5 + selector: + number: + min: 0 + max: 65535 + unit_of_measurement: seconds duty_cycle: + name: Duty cycle description: >- Indicates the length of the flash cycle. This provides a means of varying the flash duration for different alarm types (e.g., fire, police, burglar). Valid range is 0-100 in increments of 10. All other values SHALL be rounded to the nearest valid value. Strobe SHALL calculate duty cycle over a duration of one second. The ON state SHALL precede the OFF state. For example, if Strobe Duty Cycle Field specifies “40,” then the strobe SHALL flash ON for 4/10ths of a second and then turn OFF for 6/10ths of a second. example: 50 + default: 0 + selector: + number: + min: 0 + max: 100 + step: 10 intensity: + name: Intensity description: >- Indicates the intensity of the strobe as shown in Table 8-23 of the ZCL spec. This attribute is designed to vary the output of the strobe (i.e., brightness) and not its frequency, which is detailed in section 8.4.2.3.1.6 of the ZCL spec. example: 2 + default: 2 + selector: + number: + min: 0 + max: 3 + mode: box clear_lock_user_code: name: Clear lock user From fdadacd158a817154adb68cd9098b3d2accf709b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 27 Apr 2021 20:07:55 +0200 Subject: [PATCH 0584/1317] Improve color conversion for RGBW lights (#49764) --- homeassistant/components/light/__init__.py | 15 ++++++--- tests/components/light/test_init.py | 36 ++++++++++++++++++++++ tests/components/mqtt/test_light_json.py | 6 ++-- 3 files changed, 49 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index dba75b805ade6..0328da7c1bc54 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -71,7 +71,7 @@ COLOR_MODE_RGBWW, } COLOR_MODES_BRIGHTNESS = VALID_COLOR_MODES - {COLOR_MODE_ONOFF} -COLOR_MODES_COLOR = {COLOR_MODE_HS, COLOR_MODE_RGB, COLOR_MODE_XY} +COLOR_MODES_COLOR = {COLOR_MODE_HS, COLOR_MODE_RGB, COLOR_MODE_RGBW, COLOR_MODE_XY} def valid_supported_color_modes(color_modes): @@ -318,7 +318,8 @@ async def async_handle_light_on_service(light, call): if COLOR_MODE_RGB in supported_color_modes: params[ATTR_RGB_COLOR] = color_util.color_hs_to_RGB(*hs_color) elif COLOR_MODE_RGBW in supported_color_modes: - params[ATTR_RGBW_COLOR] = (*color_util.color_hs_to_RGB(*hs_color), 0) + rgb_color = color_util.color_hs_to_RGB(*hs_color) + params[ATTR_RGBW_COLOR] = color_util.color_rgb_to_rgbw(*rgb_color) elif COLOR_MODE_RGBWW in supported_color_modes: params[ATTR_RGBWW_COLOR] = ( *color_util.color_hs_to_RGB(*hs_color), @@ -685,6 +686,13 @@ def _light_internal_convert_color(self, color_mode: str) -> dict: data[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color) data[ATTR_RGB_COLOR] = tuple(int(x) for x in rgb_color[0:3]) data[ATTR_XY_COLOR] = color_util.color_RGB_to_xy(*rgb_color) + elif color_mode == COLOR_MODE_RGBW and self._light_internal_rgbw_color: + rgbw_color = self._light_internal_rgbw_color + rgb_color = color_util.color_rgbw_to_rgb(*rgbw_color) + data[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color) + data[ATTR_RGB_COLOR] = tuple(int(x) for x in rgb_color[0:3]) + data[ATTR_RGBW_COLOR] = tuple(int(x) for x in rgbw_color[0:4]) + data[ATTR_XY_COLOR] = color_util.color_RGB_to_xy(*rgb_color) return data @final @@ -722,9 +730,6 @@ def state_attributes(self): if color_mode in COLOR_MODES_COLOR: data.update(self._light_internal_convert_color(color_mode)) - if color_mode == COLOR_MODE_RGBW: - data[ATTR_RGBW_COLOR] = self._light_internal_rgbw_color - if color_mode == COLOR_MODE_RGBWW: data[ATTR_RGBWW_COLOR] = self.rgbww_color diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index f0cca89892c58..e52192c62ee27 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -1195,7 +1195,10 @@ async def test_light_state_rgbw(hass): "friendly_name": "Test_rgbw", "supported_color_modes": [light.COLOR_MODE_RGBW], "supported_features": 0, + "hs_color": (240.0, 25.0), + "rgb_color": (3, 3, 4), "rgbw_color": (1, 2, 3, 4), + "xy_color": (0.301, 0.295), } @@ -1298,6 +1301,39 @@ async def test_light_service_call_color_conversion(hass): _, data = entity6.last_call("turn_on") assert data == {"brightness": 255, "rgbww_color": (0, 0, 255, 0, 0)} + await hass.services.async_call( + "light", + "turn_on", + { + "entity_id": [ + entity0.entity_id, + entity1.entity_id, + entity2.entity_id, + entity3.entity_id, + entity4.entity_id, + entity5.entity_id, + entity6.entity_id, + ], + "brightness_pct": 100, + "hs_color": (240, 0), + }, + blocking=True, + ) + _, data = entity0.last_call("turn_on") + assert data == {"brightness": 255, "hs_color": (240.0, 0.0)} + _, data = entity1.last_call("turn_on") + assert data == {"brightness": 255, "rgb_color": (255, 255, 255)} + _, data = entity2.last_call("turn_on") + assert data == {"brightness": 255, "xy_color": (0.323, 0.329)} + _, data = entity3.last_call("turn_on") + assert data == {"brightness": 255, "hs_color": (240.0, 0.0)} + _, data = entity4.last_call("turn_on") + assert data == {"brightness": 255, "hs_color": (240.0, 0.0)} + _, data = entity5.last_call("turn_on") + assert data == {"brightness": 255, "rgbw_color": (0, 0, 0, 255)} + _, data = entity6.last_call("turn_on") + assert data == {"brightness": 255, "rgbww_color": (255, 255, 255, 0, 0)} + await hass.services.async_call( "light", "turn_on", diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index 77e5936c7b49c..432c17cda2542 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -870,11 +870,11 @@ async def test_sending_mqtt_commands_and_optimistic2(hass, mqtt_mock): assert state.attributes["brightness"] == 75 assert state.attributes["color_mode"] == "rgbw" assert state.attributes["rgbw_color"] == (255, 128, 0, 123) - assert "hs_color" not in state.attributes - assert "rgb_color" not in state.attributes + assert state.attributes["hs_color"] == (30.0, 67.451) + assert state.attributes["rgb_color"] == (255, 169, 83) assert "rgbww_color" not in state.attributes assert "white_value" not in state.attributes - assert "xy_color" not in state.attributes + assert state.attributes["xy_color"] == (0.526, 0.393) mqtt_mock.async_publish.assert_called_once_with( "test_light_rgb/set", JsonValidator( From 046f02b7b8bf770ff8182e5ef86fb81083a7e3cd Mon Sep 17 00:00:00 2001 From: tkdrob Date: Tue, 27 Apr 2021 14:31:53 -0400 Subject: [PATCH 0585/1317] Add selectors to device_tracker services (#49780) --- .../components/device_tracker/services.yaml | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/homeassistant/components/device_tracker/services.yaml b/homeassistant/components/device_tracker/services.yaml index 63435d0ac9d65..9e27a04fabf17 100644 --- a/homeassistant/components/device_tracker/services.yaml +++ b/homeassistant/components/device_tracker/services.yaml @@ -1,26 +1,53 @@ # Describes the format for available device tracker services see: + name: See description: Control tracked device. fields: mac: + name: MAC address description: MAC address of device example: "FF:FF:FF:FF:FF:FF" + selector: + text: dev_id: + name: Device ID description: Id of device (find id in known_devices.yaml). example: "phonedave" + selector: + text: host_name: + name: Host name description: Hostname of device example: "Dave" + selector: + text: location_name: + name: Location name description: Name of location where device is located (not_home is away). example: "home" + selector: + text: gps: + name: GPS coordinates description: GPS coordinates where device is located (latitude, longitude). example: "[51.509802, -0.086692]" + selector: + object: gps_accuracy: + name: GPS accuracy description: Accuracy of GPS coordinates. example: "80" + selector: + number: + min: 1 + max: 100 battery: + name: Battery level description: Battery level of device. example: "100" + selector: + number: + min: 0 + max: 100 + unit_of_measurement: "%" From 81264ff759f6ede4eb45e891554551c0a91cbc85 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Tue, 27 Apr 2021 14:34:21 -0400 Subject: [PATCH 0586/1317] Add selectors to synology_dsm services (#49772) --- homeassistant/components/synology_dsm/services.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/homeassistant/components/synology_dsm/services.yaml b/homeassistant/components/synology_dsm/services.yaml index f75b2f0ec8a98..3e25d4bef9de5 100644 --- a/homeassistant/components/synology_dsm/services.yaml +++ b/homeassistant/components/synology_dsm/services.yaml @@ -1,15 +1,23 @@ # synology-dsm service entries description. reboot: + name: Reboot description: Reboot the NAS. fields: serial: + name: Serial description: serial of the NAS to reboot; required when multiple NAS are configured. example: 1NDVC86409 + selector: + text: shutdown: + name: Shutdown description: Shutdown the NAS. fields: serial: + name: Serial description: serial of the NAS to shutdown; required when multiple NAS are configured. example: 1NDVC86409 + selector: + text: From 6df19205da7abde2748c678f6b9d11fa8996c931 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Tue, 27 Apr 2021 14:37:59 -0400 Subject: [PATCH 0587/1317] Add selectors to group services (#49779) --- homeassistant/components/group/services.yaml | 27 ++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/homeassistant/components/group/services.yaml b/homeassistant/components/group/services.yaml index 57e11d672dc0c..aac3e9aad5953 100644 --- a/homeassistant/components/group/services.yaml +++ b/homeassistant/components/group/services.yaml @@ -1,32 +1,59 @@ # Describes the format for available group services reload: + name: Reload description: Reload group configuration, entities, and notify services. set: + name: Set description: Create/Update a user group. fields: object_id: + name: Object ID description: Group id and part of entity id. + required: true example: "test_group" + selector: + text: name: + name: Name description: Name of group example: "My test group" + selector: + text: icon: + name: Icon description: Name of icon for the group. example: "mdi:camera" + selector: + text: entities: + name: Entities description: List of all members in the group. Not compatible with 'delta'. example: domain.entity_id1, domain.entity_id2 + selector: + object: add_entities: + name: Add Entities description: List of members they will change on group listening. example: domain.entity_id1, domain.entity_id2 + selector: + object: all: + name: All description: Enable this option if the group should only turn on when all entities are on. example: true + selector: + boolean: remove: + name: Remove description: Remove a user group. fields: object_id: + name: Object ID description: Group id and part of entity id. + required: true example: "test_group" + selector: + entity: + domain: group From 3f3f77c6e63f98b5f3ed935cefb01121ce6e5764 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 27 Apr 2021 08:42:21 -1000 Subject: [PATCH 0588/1317] Reduce config entry setup/unload boilerplate N-P (#49777) --- homeassistant/components/neato/__init__.py | 13 ++---------- homeassistant/components/nest/__init__.py | 15 ++----------- homeassistant/components/netatmo/__init__.py | 15 ++----------- homeassistant/components/nexia/__init__.py | 15 ++----------- .../components/nightscout/__init__.py | 16 ++------------ homeassistant/components/notion/__init__.py | 14 ++----------- homeassistant/components/nuheat/__init__.py | 15 ++----------- homeassistant/components/nuki/__init__.py | 15 ++----------- homeassistant/components/nut/__init__.py | 15 ++----------- homeassistant/components/nws/__init__.py | 16 +++----------- homeassistant/components/nzbget/__init__.py | 16 ++------------ .../components/omnilogic/__init__.py | 15 ++----------- .../components/ondilo_ico/__init__.py | 15 ++----------- homeassistant/components/onewire/__init__.py | 9 ++------ homeassistant/components/onvif/__init__.py | 16 ++------------ .../components/opentherm_gw/__init__.py | 16 +++++--------- homeassistant/components/openuv/__init__.py | 14 +++---------- .../components/openweathermap/__init__.py | 21 +++---------------- .../components/ovo_energy/__init__.py | 10 ++++----- .../components/owntracks/__init__.py | 9 ++++---- homeassistant/components/ozw/__init__.py | 9 +------- .../components/panasonic_viera/__init__.py | 16 +++----------- .../components/philips_js/__init__.py | 14 ++----------- homeassistant/components/pi_hole/__init__.py | 15 +++---------- homeassistant/components/picnic/__init__.py | 15 ++----------- homeassistant/components/plaato/__init__.py | 18 ++++------------ homeassistant/components/plex/__init__.py | 9 ++------ homeassistant/components/plugwise/gateway.py | 14 +++---------- homeassistant/components/point/__init__.py | 6 ++---- .../components/poolsense/__init__.py | 17 ++------------- .../components/powerwall/__init__.py | 15 ++----------- .../components/progettihwsw/__init__.py | 15 ++----------- homeassistant/components/ps4/__init__.py | 11 +++++----- .../pvpc_hourly_pricing/__init__.py | 9 +++----- .../components/pvpc_hourly_pricing/const.py | 2 +- .../components/owntracks/test_config_flow.py | 4 ++-- 36 files changed, 90 insertions(+), 389 deletions(-) diff --git a/homeassistant/components/neato/__init__.py b/homeassistant/components/neato/__init__.py index 036d91534f404..b009e876a7b97 100644 --- a/homeassistant/components/neato/__init__.py +++ b/homeassistant/components/neato/__init__.py @@ -1,5 +1,4 @@ """Support for Neato botvac connected vacuum cleaners.""" -import asyncio from datetime import timedelta import logging @@ -92,22 +91,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[NEATO_LOGIN] = hub - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigType) -> bool: """Unload config entry.""" - unload_functions = ( - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ) - - unload_ok = all(await asyncio.gather(*unload_functions)) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[NEATO_DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 42b167ee851bc..d58ad4863edcd 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -1,6 +1,5 @@ """Support for Nest devices.""" -import asyncio import logging from google_nest_sdm.event import EventMessage @@ -191,10 +190,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data[DOMAIN].pop(DATA_NEST_UNAVAILABLE, None) hass.data[DOMAIN][DATA_SUBSCRIBER] = subscriber - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -207,14 +203,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): _LOGGER.debug("Stopping nest subscriber") subscriber = hass.data[DOMAIN][DATA_SUBSCRIBER] subscriber.stop_async() - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(DATA_SUBSCRIBER) hass.data[DOMAIN].pop(DATA_NEST_UNAVAILABLE, None) diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index 131542acb0e34..1f452f1ccd471 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -1,5 +1,4 @@ """The Netatmo integration.""" -import asyncio import logging import secrets @@ -111,10 +110,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): await data_handler.async_setup() hass.data[DOMAIN][entry.entry_id][DATA_HANDLER] = data_handler - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) async def unregister_webhook(_): if CONF_WEBHOOK_ID not in entry.data: @@ -213,14 +209,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): await hass.data[DOMAIN][entry.entry_id][DATA_HANDLER].async_cleanup() - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/nexia/__init__.py b/homeassistant/components/nexia/__init__.py index 07f6230eb0dab..da3e00b2d6ac2 100644 --- a/homeassistant/components/nexia/__init__.py +++ b/homeassistant/components/nexia/__init__.py @@ -1,5 +1,4 @@ """Support for Nexia / Trane XL Thermostats.""" -import asyncio from datetime import timedelta from functools import partial import logging @@ -73,24 +72,14 @@ async def _async_update_data(): UPDATE_COORDINATOR: coordinator, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/nightscout/__init__.py b/homeassistant/components/nightscout/__init__.py index dd9405317350a..8608386c483c4 100644 --- a/homeassistant/components/nightscout/__init__.py +++ b/homeassistant/components/nightscout/__init__.py @@ -1,5 +1,4 @@ """The Nightscout integration.""" -import asyncio from asyncio import TimeoutError as AsyncIOTimeoutError from aiohttp import ClientError @@ -43,25 +42,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): entry_type="service", ) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) - + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index ca0ccf08c8974..edadca64ec48d 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -99,24 +99,14 @@ async def async_update(): await coordinator.async_config_entry_first_refresh() - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a Notion config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN][DATA_COORDINATOR].pop(entry.entry_id) diff --git a/homeassistant/components/nuheat/__init__.py b/homeassistant/components/nuheat/__init__.py index c04bfe647206d..db50a9a70d9e5 100644 --- a/homeassistant/components/nuheat/__init__.py +++ b/homeassistant/components/nuheat/__init__.py @@ -1,5 +1,4 @@ """Support for NuHeat thermostats.""" -import asyncio from datetime import timedelta import logging @@ -75,24 +74,14 @@ async def _async_update_data(): hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = (thermostat, coordinator) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/nuki/__init__.py b/homeassistant/components/nuki/__init__.py index 173beca0c4a6d..f937bddf6232e 100644 --- a/homeassistant/components/nuki/__init__.py +++ b/homeassistant/components/nuki/__init__.py @@ -1,6 +1,5 @@ """The nuki component.""" -import asyncio from datetime import timedelta import logging @@ -122,24 +121,14 @@ async def async_update_data(): # Fetch initial data so we have data when entities subscribe await coordinator.async_refresh() - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass, entry): """Unload the Nuki entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/nut/__init__.py b/homeassistant/components/nut/__init__.py index f526e49c6b832..77458b2cfb739 100644 --- a/homeassistant/components/nut/__init__.py +++ b/homeassistant/components/nut/__init__.py @@ -1,5 +1,4 @@ """The nut component.""" -import asyncio from datetime import timedelta import logging @@ -95,10 +94,7 @@ async def async_update_data(): UNDO_UPDATE_LISTENER: undo_listener, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -169,14 +165,7 @@ def find_resources_in_config_entry(config_entry): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() diff --git a/homeassistant/components/nws/__init__.py b/homeassistant/components/nws/__init__.py index 021b996c9453d..386a426c1d1c9 100644 --- a/homeassistant/components/nws/__init__.py +++ b/homeassistant/components/nws/__init__.py @@ -1,7 +1,6 @@ """The National Weather Service integration.""" from __future__ import annotations -import asyncio from collections.abc import Awaitable import datetime import logging @@ -155,23 +154,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): await coordinator_forecast.async_refresh() await coordinator_forecast_hourly.async_refresh() - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) if len(hass.data[DOMAIN]) == 0: diff --git a/homeassistant/components/nzbget/__init__.py b/homeassistant/components/nzbget/__init__.py index 3e85839e5d7ba..7b250d393eabd 100644 --- a/homeassistant/components/nzbget/__init__.py +++ b/homeassistant/components/nzbget/__init__.py @@ -1,6 +1,4 @@ """The NZBGet integration.""" -import asyncio - import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry @@ -103,10 +101,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_UNDO_UPDATE_LISTENER: undo_listener, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) _async_register_services(hass, coordinator) @@ -115,14 +110,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN][entry.entry_id][DATA_UNDO_UPDATE_LISTENER]() diff --git a/homeassistant/components/omnilogic/__init__.py b/homeassistant/components/omnilogic/__init__.py index 8c5d460e549cc..f50efb7eafb84 100644 --- a/homeassistant/components/omnilogic/__init__.py +++ b/homeassistant/components/omnilogic/__init__.py @@ -1,5 +1,4 @@ """The Omnilogic integration.""" -import asyncio import logging from omnilogic import LoginException, OmniLogic, OmniLogicException @@ -57,24 +56,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): OMNI_API: api, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/ondilo_ico/__init__.py b/homeassistant/components/ondilo_ico/__init__.py index 0975802b9b251..2b8b2cc22b7e7 100644 --- a/homeassistant/components/ondilo_ico/__init__.py +++ b/homeassistant/components/ondilo_ico/__init__.py @@ -1,5 +1,4 @@ """The Ondilo ICO integration.""" -import asyncio from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -29,24 +28,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = api.OndiloClient(hass, entry, implementation) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/onewire/__init__.py b/homeassistant/components/onewire/__init__.py index cd6d594fafb49..4bf0382a92c55 100644 --- a/homeassistant/components/onewire/__init__.py +++ b/homeassistant/components/onewire/__init__.py @@ -67,13 +67,8 @@ async def start_platforms() -> None: async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS ) if unload_ok: hass.data[DOMAIN].pop(config_entry.unique_id) diff --git a/homeassistant/components/onvif/__init__.py b/homeassistant/components/onvif/__init__.py index 46303781673b5..f90ccb1676099 100644 --- a/homeassistant/components/onvif/__init__.py +++ b/homeassistant/components/onvif/__init__.py @@ -1,6 +1,4 @@ """The ONVIF integration.""" -import asyncio - from onvif.exceptions import ONVIFAuthError, ONVIFError, ONVIFTimeoutError from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS @@ -88,10 +86,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): if device.capabilities.events: platforms += ["binary_sensor", "sensor"] - for platform in platforms: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, platforms) entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, device.async_stop) @@ -110,14 +105,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): platforms += ["binary_sensor", "sensor"] await device.events.async_stop() - return all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in platforms - ] - ) - ) + return await hass.config_entries.async_unload_platforms(entry, platforms) async def _get_snapshot_auth(device): diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py index 8686997e7481a..e3ec9ddef130f 100644 --- a/homeassistant/components/opentherm_gw/__init__.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -1,5 +1,4 @@ """Support for OpenTherm Gateway devices.""" -import asyncio from datetime import date, datetime import logging @@ -81,6 +80,8 @@ extra=vol.ALLOW_EXTRA, ) +PLATFORMS = [COMP_BINARY_SENSOR, COMP_CLIMATE, COMP_SENSOR] + async def options_updated(hass, entry): """Handle options update.""" @@ -112,10 +113,7 @@ async def async_setup_entry(hass, config_entry): # Schedule directly on the loop to avoid blocking HA startup. hass.loop.create_task(gateway.connect_and_subscribe()) - for comp in [COMP_BINARY_SENSOR, COMP_CLIMATE, COMP_SENSOR]: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, comp) - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) register_services(hass) return True @@ -400,14 +398,10 @@ async def set_setback_temp(call): async def async_unload_entry(hass, entry): """Cleanup and disconnect from gateway.""" - await asyncio.gather( - hass.config_entries.async_forward_entry_unload(entry, COMP_BINARY_SENSOR), - hass.config_entries.async_forward_entry_unload(entry, COMP_CLIMATE), - hass.config_entries.async_forward_entry_unload(entry, COMP_SENSOR), - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) gateway = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][entry.data[CONF_ID]] await gateway.cleanup() - return True + return unload_ok class OpenThermGatewayDevice: diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py index aeefe435845d9..e1af166a3c296 100644 --- a/homeassistant/components/openuv/__init__.py +++ b/homeassistant/components/openuv/__init__.py @@ -69,10 +69,7 @@ async def async_setup_entry(hass, config_entry): LOGGER.error("Config entry failed: %s", err) raise ConfigEntryNotReady from err - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) @_verify_domain_control async def update_data(service): @@ -107,13 +104,8 @@ async def update_protection_data(service): async def async_unload_entry(hass, config_entry): """Unload an OpenUV config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS ) if unload_ok: hass.data[DOMAIN][DATA_CLIENT].pop(config_entry.entry_id) diff --git a/homeassistant/components/openweathermap/__init__.py b/homeassistant/components/openweathermap/__init__.py index f6d47d1dcae01..49846a0ad0ae5 100644 --- a/homeassistant/components/openweathermap/__init__.py +++ b/homeassistant/components/openweathermap/__init__.py @@ -1,5 +1,4 @@ """The openweathermap component.""" -import asyncio import logging from pyowm import OWM @@ -31,12 +30,6 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: dict) -> bool: - """Set up the OpenWeatherMap component.""" - hass.data.setdefault(DOMAIN, {}) - return True - - async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): """Set up OpenWeatherMap as config entry.""" name = config_entry.data[CONF_NAME] @@ -61,10 +54,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): ENTRY_WEATHER_COORDINATOR: weather_coordinator, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) update_listener = config_entry.add_update_listener(async_update_options) hass.data[DOMAIN][config_entry.entry_id][UPDATE_LISTENER] = update_listener @@ -101,13 +91,8 @@ async def async_update_options(hass: HomeAssistant, config_entry: ConfigEntry): async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS ) if unload_ok: update_listener = hass.data[DOMAIN][config_entry.entry_id][UPDATE_LISTENER] diff --git a/homeassistant/components/ovo_energy/__init__.py b/homeassistant/components/ovo_energy/__init__.py index 749f7b7e249f3..d94e337e3d3fa 100644 --- a/homeassistant/components/ovo_energy/__init__.py +++ b/homeassistant/components/ovo_energy/__init__.py @@ -25,6 +25,8 @@ _LOGGER = logging.getLogger(__name__) +PLATFORMS = ["sensor"] + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up OVO Energy from a config entry.""" @@ -75,9 +77,7 @@ async def async_update_data() -> OVODailyUsage: await coordinator.async_config_entry_first_refresh() # Setup components - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "sensor") - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -85,11 +85,11 @@ async def async_update_data() -> OVODailyUsage: async def async_unload_entry(hass: HomeAssistant, entry: ConfigType) -> bool: """Unload OVO Energy config entry.""" # Unload sensors - await hass.config_entries.async_forward_entry_unload(entry, "sensor") + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) del hass.data[DOMAIN][entry.entry_id] - return True + return unload_ok class OVOEnergyEntity(CoordinatorEntity): diff --git a/homeassistant/components/owntracks/__init__.py b/homeassistant/components/owntracks/__init__.py index d3091d7d02713..d51566718d60e 100644 --- a/homeassistant/components/owntracks/__init__.py +++ b/homeassistant/components/owntracks/__init__.py @@ -32,6 +32,7 @@ CONF_REGION_MAPPING = "region_mapping" CONF_EVENTS_ONLY = "events_only" BEACON_DEV_ID = "beacon" +PLATFORMS = ["device_tracker"] DEFAULT_OWNTRACKS_TOPIC = "owntracks/#" @@ -101,9 +102,7 @@ async def async_setup_entry(hass, entry): DOMAIN, "OwnTracks", webhook_id, handle_webhook ) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "device_tracker") - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) hass.data[DOMAIN]["unsub"] = hass.helpers.dispatcher.async_dispatcher_connect( DOMAIN, async_handle_message @@ -115,10 +114,10 @@ async def async_setup_entry(hass, entry): async def async_unload_entry(hass, entry): """Unload an OwnTracks config entry.""" hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID]) - await hass.config_entries.async_forward_entry_unload(entry, "device_tracker") + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) hass.data[DOMAIN]["unsub"]() - return True + return unload_ok async def async_remove_entry(hass, entry): diff --git a/homeassistant/components/ozw/__init__.py b/homeassistant/components/ozw/__init__.py index f3d827a57ff88..17ab4ca7eb8ad 100644 --- a/homeassistant/components/ozw/__init__.py +++ b/homeassistant/components/ozw/__init__.py @@ -301,14 +301,7 @@ async def async_stop_mqtt_client(event=None): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" # cleanup platforms - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if not unload_ok: return False diff --git a/homeassistant/components/panasonic_viera/__init__.py b/homeassistant/components/panasonic_viera/__init__.py index 67cf07dc43338..8f0a0e89d4581 100644 --- a/homeassistant/components/panasonic_viera/__init__.py +++ b/homeassistant/components/panasonic_viera/__init__.py @@ -1,5 +1,4 @@ """The Panasonic Viera integration.""" -import asyncio from functools import partial import logging from urllib.request import URLError @@ -104,25 +103,16 @@ async def async_setup_entry(hass, config_entry): data={**config, ATTR_DEVICE_INFO: device_info}, ) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) return True async def async_unload_entry(hass, config_entry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS ) - if unload_ok: hass.data[DOMAIN].pop(config_entry.entry_id) diff --git a/homeassistant/components/philips_js/__init__.py b/homeassistant/components/philips_js/__init__.py index bf17284d77705..cc78402dda90b 100644 --- a/homeassistant/components/philips_js/__init__.py +++ b/homeassistant/components/philips_js/__init__.py @@ -43,24 +43,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/pi_hole/__init__.py b/homeassistant/components/pi_hole/__init__.py index bc486a0c9014e..7e897887d8dfe 100644 --- a/homeassistant/components/pi_hole/__init__.py +++ b/homeassistant/components/pi_hole/__init__.py @@ -1,5 +1,4 @@ """The pi_hole component.""" -import asyncio import logging from hole import Hole @@ -126,23 +125,15 @@ async def async_update_data(): DATA_KEY_COORDINATOR: coordinator, } - for platform in _async_platforms(entry): - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, _async_platforms(entry)) return True async def async_unload_entry(hass, entry): """Unload Pi-hole entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in _async_platforms(entry) - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + entry, _async_platforms(entry) ) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/picnic/__init__.py b/homeassistant/components/picnic/__init__.py index 003111088e101..055faadb78452 100644 --- a/homeassistant/components/picnic/__init__.py +++ b/homeassistant/components/picnic/__init__.py @@ -1,5 +1,4 @@ """The Picnic integration.""" -import asyncio from python_picnic_api import PicnicAPI @@ -35,24 +34,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): CONF_COORDINATOR: picnic_coordinator, } - for component in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/plaato/__init__.py b/homeassistant/components/plaato/__init__.py index 9ed8d85f2324a..d73b997398a09 100644 --- a/homeassistant/components/plaato/__init__.py +++ b/homeassistant/components/plaato/__init__.py @@ -1,6 +1,5 @@ """Support for Plaato devices.""" -import asyncio from datetime import timedelta import logging @@ -94,11 +93,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): else: await async_setup_coordinator(hass, entry) - for platform in PLATFORMS: - if entry.options.get(platform, True): - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms( + entry, [platform for platform in PLATFORMS if entry.options.get(platform, True)] + ) return True @@ -177,14 +174,7 @@ async def async_unload_coordinator(hass: HomeAssistant, entry: ConfigEntry): async def async_unload_platforms(hass: HomeAssistant, entry: ConfigEntry, platforms): """Unload platforms.""" - unloaded = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in platforms - ] - ) - ) + unloaded = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unloaded: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index ec2c6480776c9..c534384a7eb7b 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -1,5 +1,4 @@ """Support to embed Plex.""" -import asyncio from functools import partial import logging @@ -232,15 +231,11 @@ async def async_unload_entry(hass, entry): for unsub in dispatchers: unsub() - tasks = [ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - await asyncio.gather(*tasks) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) hass.data[PLEX_DOMAIN][SERVERS].pop(server_id) - return True + return unload_ok async def async_options_updated(hass, entry): diff --git a/homeassistant/components/plugwise/gateway.py b/homeassistant/components/plugwise/gateway.py index 3f805f1475d0f..a6d8960edf2f1 100644 --- a/homeassistant/components/plugwise/gateway.py +++ b/homeassistant/components/plugwise/gateway.py @@ -136,10 +136,7 @@ async def async_update_data(): if single_master_thermostat is None: platforms = SENSOR_PLATFORMS - for platform in platforms: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, platforms) return True @@ -154,13 +151,8 @@ async def _update_listener(hass: HomeAssistant, entry: ConfigEntry): async def async_unload_entry_gw(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS_GATEWAY - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + entry, PLATFORMS_GATEWAY ) hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() diff --git a/homeassistant/components/point/__init__.py b/homeassistant/components/point/__init__.py index 38561d42abc3f..6128b6ae16200 100644 --- a/homeassistant/components/point/__init__.py +++ b/homeassistant/components/point/__init__.py @@ -139,13 +139,11 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): session = hass.data[DOMAIN].pop(entry.entry_id) await session.remove_webhook() - for platform in PLATFORMS: - await hass.config_entries.async_forward_entry_unload(entry, platform) - + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if not hass.data[DOMAIN]: hass.data.pop(DOMAIN) - return True + return unload_ok async def handle_webhook(hass, webhook_id, request): diff --git a/homeassistant/components/poolsense/__init__.py b/homeassistant/components/poolsense/__init__.py index 4fee2d01a73d2..89e340ee95ed5 100644 --- a/homeassistant/components/poolsense/__init__.py +++ b/homeassistant/components/poolsense/__init__.py @@ -1,5 +1,4 @@ """The PoolSense integration.""" -import asyncio from datetime import timedelta import logging @@ -46,28 +45,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) - + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok diff --git a/homeassistant/components/powerwall/__init__.py b/homeassistant/components/powerwall/__init__.py index e3c08e747701f..1792ca19fc818 100644 --- a/homeassistant/components/powerwall/__init__.py +++ b/homeassistant/components/powerwall/__init__.py @@ -1,5 +1,4 @@ """The Tesla Powerwall integration.""" -import asyncio from datetime import timedelta import logging @@ -154,10 +153,7 @@ async def async_update_data(): await coordinator.async_config_entry_first_refresh() - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -210,14 +206,7 @@ def _fetch_powerwall_data(power_wall): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) hass.data[DOMAIN][entry.entry_id][POWERWALL_HTTP_SESSION].close() diff --git a/homeassistant/components/progettihwsw/__init__.py b/homeassistant/components/progettihwsw/__init__.py index bb8757e096268..78ea16bb26c21 100644 --- a/homeassistant/components/progettihwsw/__init__.py +++ b/homeassistant/components/progettihwsw/__init__.py @@ -1,5 +1,4 @@ """Automation manager for boards manufactured by ProgettiHWSW Italy.""" -import asyncio from ProgettiHWSW.ProgettiHWSWAPI import ProgettiHWSWAPI from ProgettiHWSW.input import Input @@ -23,24 +22,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): # Check board validation again to load new values to API. await hass.data[DOMAIN][entry.entry_id].check_board() - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/ps4/__init__.py b/homeassistant/components/ps4/__init__.py index 51583b5f4bc64..65940b9dc4879 100644 --- a/homeassistant/components/ps4/__init__.py +++ b/homeassistant/components/ps4/__init__.py @@ -38,6 +38,8 @@ } ) +PLATFORMS = ["media_player"] + class PS4Data: """Init Data Class.""" @@ -59,18 +61,15 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass, config_entry): +async def async_setup_entry(hass, entry): """Set up PS4 from a config entry.""" - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, "media_player") - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass, entry): """Unload a PS4 config entry.""" - await hass.config_entries.async_forward_entry_unload(entry, "media_player") - return True + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) async def async_migrate_entry(hass, entry): diff --git a/homeassistant/components/pvpc_hourly_pricing/__init__.py b/homeassistant/components/pvpc_hourly_pricing/__init__.py index 5930da52313a2..dfb7282aae9ea 100644 --- a/homeassistant/components/pvpc_hourly_pricing/__init__.py +++ b/homeassistant/components/pvpc_hourly_pricing/__init__.py @@ -6,7 +6,7 @@ from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -from .const import ATTR_TARIFF, DEFAULT_NAME, DEFAULT_TARIFF, DOMAIN, PLATFORM, TARIFFS +from .const import ATTR_TARIFF, DEFAULT_NAME, DEFAULT_TARIFF, DOMAIN, PLATFORMS, TARIFFS UI_CONFIG_SCHEMA = vol.Schema( { @@ -44,13 +44,10 @@ async def async_setup(hass: HomeAssistant, config: dict): async def async_setup_entry(hass: HomeAssistant, entry: config_entries.ConfigEntry): """Set up pvpc hourly pricing from a config entry.""" - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, PLATFORM) - ) - + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: config_entries.ConfigEntry): """Unload a config entry.""" - return await hass.config_entries.async_forward_entry_unload(entry, PLATFORM) + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/pvpc_hourly_pricing/const.py b/homeassistant/components/pvpc_hourly_pricing/const.py index d75ad9fe35c0f..9e11bc57d6dbe 100644 --- a/homeassistant/components/pvpc_hourly_pricing/const.py +++ b/homeassistant/components/pvpc_hourly_pricing/const.py @@ -2,7 +2,7 @@ from aiopvpc import TARIFFS DOMAIN = "pvpc_hourly_pricing" -PLATFORM = "sensor" +PLATFORMS = ["sensor"] ATTR_TARIFF = "tariff" DEFAULT_NAME = "PVPC" DEFAULT_TARIFF = TARIFFS[1] diff --git a/tests/components/owntracks/test_config_flow.py b/tests/components/owntracks/test_config_flow.py index 93d4bf5385b46..c56770099efd8 100644 --- a/tests/components/owntracks/test_config_flow.py +++ b/tests/components/owntracks/test_config_flow.py @@ -154,8 +154,8 @@ async def test_unload(hass): assert entry.data["webhook_id"] in hass.data["webhook"] with patch( - "homeassistant.config_entries.ConfigEntries.async_forward_entry_unload", - return_value=None, + "homeassistant.config_entries.ConfigEntries.async_unload_platforms", + return_value=True, ) as mock_unload: assert await hass.config_entries.async_unload(entry.entry_id) From ebbcfb1bc74967c7c9eb8ca58230edfb315652b3 Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 27 Apr 2021 20:58:52 +0200 Subject: [PATCH 0589/1317] Fix and upgrade surepetcare (#49223) Co-authored-by: Martin Hjelmare --- .../components/surepetcare/__init__.py | 182 ++++++------------ .../components/surepetcare/binary_sensor.py | 123 +++++------- homeassistant/components/surepetcare/const.py | 14 -- .../components/surepetcare/manifest.json | 2 +- .../components/surepetcare/sensor.py | 103 +++------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/surepetcare/__init__.py | 13 +- tests/components/surepetcare/conftest.py | 28 ++- .../surepetcare/test_binary_sensor.py | 24 ++- tests/components/surepetcare/test_sensor.py | 29 +++ 11 files changed, 192 insertions(+), 330 deletions(-) create mode 100644 tests/components/surepetcare/test_sensor.py diff --git a/homeassistant/components/surepetcare/__init__.py b/homeassistant/components/surepetcare/__init__.py index 4a65931d3f04a..3873d17343d54 100644 --- a/homeassistant/components/surepetcare/__init__.py +++ b/homeassistant/components/surepetcare/__init__.py @@ -1,26 +1,17 @@ -"""Support for Sure Petcare cat/pet flaps.""" +"""The surepetcare integration.""" from __future__ import annotations +from datetime import timedelta import logging from typing import Any -from surepy import ( - MESTART_RESOURCE, - SureLockStateID, - SurePetcare, - SurePetcareAuthenticationError, - SurePetcareError, - SurepyProduct, -) +from surepy import Surepy +from surepy.enums import LockState +from surepy.exceptions import SurePetcareAuthenticationError, SurePetcareError import voluptuous as vol -from homeassistant.const import ( - CONF_ID, - CONF_PASSWORD, - CONF_SCAN_INTERVAL, - CONF_TYPE, - CONF_USERNAME, -) +from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -31,11 +22,7 @@ ATTR_LOCK_STATE, CONF_FEEDERS, CONF_FLAPS, - CONF_PARENT, CONF_PETS, - CONF_PRODUCT_ID, - DATA_SURE_PETCARE, - DEFAULT_SCAN_INTERVAL, DOMAIN, SERVICE_SET_LOCK_STATE, SPC, @@ -45,50 +32,49 @@ _LOGGER = logging.getLogger(__name__) +PLATFORMS = ["binary_sensor", "sensor"] +SCAN_INTERVAL = timedelta(minutes=3) CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_FEEDERS, default=[]): vol.All( - cv.ensure_list, [cv.positive_int] - ), - vol.Optional(CONF_FLAPS, default=[]): vol.All( - cv.ensure_list, [cv.positive_int] - ), - vol.Optional(CONF_PETS): vol.All(cv.ensure_list, [cv.positive_int]), - vol.Optional( - CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL - ): cv.time_period, - } + vol.All( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_FEEDERS): vol.All( + cv.ensure_list, [cv.positive_int] + ), + vol.Optional(CONF_FLAPS): vol.All( + cv.ensure_list, [cv.positive_int] + ), + vol.Optional(CONF_PETS): vol.All(cv.ensure_list, [cv.positive_int]), + vol.Optional(CONF_SCAN_INTERVAL): cv.time_period, + }, + cv.deprecated(CONF_FEEDERS), + cv.deprecated(CONF_FLAPS), + cv.deprecated(CONF_PETS), + cv.deprecated(CONF_SCAN_INTERVAL), + ) ) }, extra=vol.ALLOW_EXTRA, ) -async def async_setup(hass, config) -> bool: - """Initialize the Sure Petcare component.""" +async def async_setup(hass: HomeAssistant, config: dict) -> bool: + """Set up the Sure Petcare integration.""" conf = config[DOMAIN] + hass.data.setdefault(DOMAIN, {}) - # update interval - scan_interval = conf[CONF_SCAN_INTERVAL] - - # shared data - hass.data[DOMAIN] = hass.data[DATA_SURE_PETCARE] = {} - - # sure petcare api connection try: - surepy = SurePetcare( + surepy = Surepy( conf[CONF_USERNAME], conf[CONF_PASSWORD], - hass.loop, - async_get_clientsession(hass), + auth_token=None, api_timeout=SURE_API_TIMEOUT, + session=async_get_clientsession(hass), ) - except SurePetcareAuthenticationError: _LOGGER.error("Unable to connect to surepetcare.io: Wrong credentials!") return False @@ -96,50 +82,12 @@ async def async_setup(hass, config) -> bool: _LOGGER.error("Unable to connect to surepetcare.io: Wrong %s!", error) return False - # add feeders - things = [ - {CONF_ID: feeder, CONF_TYPE: SurepyProduct.FEEDER} - for feeder in conf[CONF_FEEDERS] - ] - - # add flaps (don't differentiate between CAT and PET for now) - things.extend( - [ - {CONF_ID: flap, CONF_TYPE: SurepyProduct.PET_FLAP} - for flap in conf[CONF_FLAPS] - ] - ) + spc = SurePetcareAPI(hass, surepy) + hass.data[DOMAIN][SPC] = spc - # discover hubs the flaps/feeders are connected to - hub_ids = set() - for device in things.copy(): - device_data = await surepy.device(device[CONF_ID]) - if ( - CONF_PARENT in device_data - and device_data[CONF_PARENT][CONF_PRODUCT_ID] == SurepyProduct.HUB - and device_data[CONF_PARENT][CONF_ID] not in hub_ids - ): - things.append( - { - CONF_ID: device_data[CONF_PARENT][CONF_ID], - CONF_TYPE: SurepyProduct.HUB, - } - ) - hub_ids.add(device_data[CONF_PARENT][CONF_ID]) - - # add pets - things.extend( - [{CONF_ID: pet, CONF_TYPE: SurepyProduct.PET} for pet in conf[CONF_PETS]] - ) - - _LOGGER.debug("Devices and Pets to setup: %s", things) - - spc = hass.data[DATA_SURE_PETCARE][SPC] = SurePetcareAPI(hass, surepy, things) - - # initial update await spc.async_update() - async_track_time_interval(hass, spc.async_update, scan_interval) + async_track_time_interval(hass, spc.async_update, SCAN_INTERVAL) # load platforms hass.async_create_task( @@ -164,10 +112,12 @@ async def handle_set_lock_state(call): vol.Lower, vol.In( [ - SureLockStateID.UNLOCKED.name.lower(), - SureLockStateID.LOCKED_IN.name.lower(), - SureLockStateID.LOCKED_OUT.name.lower(), - SureLockStateID.LOCKED_ALL.name.lower(), + # https://github.com/PyCQA/pylint/issues/2062 + # pylint: disable=no-member + LockState.UNLOCKED.name.lower(), + LockState.LOCKED_IN.name.lower(), + LockState.LOCKED_OUT.name.lower(), + LockState.LOCKED_ALL.name.lower(), ] ), ), @@ -187,50 +137,32 @@ async def handle_set_lock_state(call): class SurePetcareAPI: """Define a generic Sure Petcare object.""" - def __init__(self, hass, surepy: SurePetcare, ids: list[dict[str, Any]]) -> None: + def __init__(self, hass: HomeAssistant, surepy: Surepy) -> None: """Initialize the Sure Petcare object.""" self.hass = hass self.surepy = surepy - self.ids = ids - self.states: dict[str, Any] = {} + self.states = {} - async def async_update(self, arg: Any = None) -> None: - """Refresh Sure Petcare data.""" + async def async_update(self, _: Any = None) -> None: + """Get the latest data from Sure Petcare.""" - # Fetch all data from SurePet API, refreshing the surepy cache - # TODO: get surepy upstream to add a method to clear the cache explicitly pylint: disable=fixme - await self.surepy._get_resource( # pylint: disable=protected-access - resource=MESTART_RESOURCE - ) - for thing in self.ids: - sure_id = thing[CONF_ID] - sure_type = thing[CONF_TYPE] - - try: - type_state = self.states.setdefault(sure_type, {}) - - if sure_type in [ - SurepyProduct.CAT_FLAP, - SurepyProduct.PET_FLAP, - SurepyProduct.FEEDER, - SurepyProduct.HUB, - ]: - type_state[sure_id] = await self.surepy.device(sure_id) - elif sure_type == SurepyProduct.PET: - type_state[sure_id] = await self.surepy.pet(sure_id) - - except SurePetcareError as error: - _LOGGER.error("Unable to retrieve data from surepetcare.io: %s", error) + try: + self.states = await self.surepy.get_entities() + except SurePetcareError as error: + _LOGGER.error("Unable to fetch data: %s", error) async_dispatcher_send(self.hass, TOPIC_UPDATE) async def set_lock_state(self, flap_id: int, state: str) -> None: """Update the lock state of a flap.""" - if state == SureLockStateID.UNLOCKED.name.lower(): + + # https://github.com/PyCQA/pylint/issues/2062 + # pylint: disable=no-member + if state == LockState.UNLOCKED.name.lower(): await self.surepy.unlock(flap_id) - elif state == SureLockStateID.LOCKED_IN.name.lower(): + elif state == LockState.LOCKED_IN.name.lower(): await self.surepy.lock_in(flap_id) - elif state == SureLockStateID.LOCKED_OUT.name.lower(): + elif state == LockState.LOCKED_OUT.name.lower(): await self.surepy.lock_out(flap_id) - elif state == SureLockStateID.LOCKED_ALL.name.lower(): + elif state == LockState.LOCKED_ALL.name.lower(): await self.surepy.lock(flap_id) diff --git a/homeassistant/components/surepetcare/binary_sensor.py b/homeassistant/components/surepetcare/binary_sensor.py index e96a5eaf35e59..5f6a82839e14f 100644 --- a/homeassistant/components/surepetcare/binary_sensor.py +++ b/homeassistant/components/surepetcare/binary_sensor.py @@ -1,23 +1,22 @@ """Support for Sure PetCare Flaps/Pets binary sensors.""" from __future__ import annotations -from datetime import datetime import logging from typing import Any -from surepy import SureLocationID, SurepyProduct +from surepy.entities import SurepyEntity +from surepy.enums import EntityType, Location, SureEnum from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, DEVICE_CLASS_PRESENCE, BinarySensorEntity, ) -from homeassistant.const import CONF_ID, CONF_TYPE from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import SurePetcareAPI -from .const import DATA_SURE_PETCARE, SPC, TOPIC_UPDATE +from .const import DOMAIN, SPC, TOPIC_UPDATE _LOGGER = logging.getLogger(__name__) @@ -29,30 +28,27 @@ async def async_setup_platform( if discovery_info is None: return - entities = [] + entities: list[SurepyEntity] = [] - spc = hass.data[DATA_SURE_PETCARE][SPC] + spc: SurePetcareAPI = hass.data[DOMAIN][SPC] - for thing in spc.ids: - sure_id = thing[CONF_ID] - sure_type = thing[CONF_TYPE] + for surepy_entity in spc.states.values(): # connectivity - if sure_type in [ - SurepyProduct.CAT_FLAP, - SurepyProduct.PET_FLAP, - SurepyProduct.FEEDER, + if surepy_entity.type in [ + EntityType.CAT_FLAP, + EntityType.PET_FLAP, + EntityType.FEEDER, + EntityType.FELAQUA, ]: - entities.append(DeviceConnectivity(sure_id, sure_type, spc)) + entities.append( + DeviceConnectivity(surepy_entity.id, surepy_entity.type, spc) + ) - if sure_type == SurepyProduct.PET: - entity = Pet(sure_id, spc) - elif sure_type == SurepyProduct.HUB: - entity = Hub(sure_id, spc) - else: - continue - - entities.append(entity) + if surepy_entity.type == EntityType.PET: + entities.append(Pet(surepy_entity.id, spc)) + elif surepy_entity.type == EntityType.HUB: + entities.append(Hub(surepy_entity.id, spc)) async_add_entities(entities, True) @@ -65,35 +61,29 @@ def __init__( _id: int, spc: SurePetcareAPI, device_class: str, - sure_type: SurepyProduct, + sure_type: EntityType, ): """Initialize a Sure Petcare binary sensor.""" + self._id = _id - self._sure_type = sure_type self._device_class = device_class self._spc: SurePetcareAPI = spc - self._spc_data: dict[str, Any] = self._spc.states[self._sure_type].get(self._id) - self._state: dict[str, Any] = {} + + self._surepy_entity: SurepyEntity = self._spc.states[self._id] + self._state: SureEnum | dict[str, Any] = None # cover special case where a device has no name set - if "name" in self._spc_data: - name = self._spc_data["name"] + if self._surepy_entity.name: + name = self._surepy_entity.name else: - name = f"Unnamed {self._sure_type.name.capitalize()}" + name = f"Unnamed {self._surepy_entity.type.name.capitalize()}" - self._name = f"{self._sure_type.name.capitalize()} {name.capitalize()}" - - self._async_unsub_dispatcher_connect = None - - @property - def is_on(self) -> bool | None: - """Return true if entity is on/unlocked.""" - return bool(self._state) + self._name = f"{self._surepy_entity.type.name.capitalize()} {name.capitalize()}" @property def should_poll(self) -> bool: - """Return true.""" + """Return if the entity should use default polling.""" return False @property @@ -109,30 +99,21 @@ def device_class(self) -> str: @property def unique_id(self) -> str: """Return an unique ID.""" - return f"{self._spc_data['household_id']}-{self._id}" + return f"{self._surepy_entity.household_id}-{self._id}" - async def async_update(self) -> None: + @callback + def _async_update(self) -> None: """Get the latest data and update the state.""" - self._spc_data = self._spc.states[self._sure_type].get(self._id) - self._state = self._spc_data.get("status") + self._surepy_entity = self._spc.states[self._id] + self._state = self._surepy_entity.raw_data()["status"] _LOGGER.debug("%s -> self._state: %s", self._name, self._state) async def async_added_to_hass(self) -> None: """Register callbacks.""" - - @callback - def update() -> None: - """Update the state.""" - self.async_schedule_update_ha_state(True) - - self._async_unsub_dispatcher_connect = async_dispatcher_connect( - self.hass, TOPIC_UPDATE, update + self.async_on_remove( + async_dispatcher_connect(self.hass, TOPIC_UPDATE, self._async_update) ) - - async def async_will_remove_from_hass(self) -> None: - """Disconnect dispatcher listener when removed.""" - if self._async_unsub_dispatcher_connect: - self._async_unsub_dispatcher_connect() + self._async_update() class Hub(SurePetcareBinarySensor): @@ -140,7 +121,7 @@ class Hub(SurePetcareBinarySensor): def __init__(self, _id: int, spc: SurePetcareAPI) -> None: """Initialize a Sure Petcare Hub.""" - super().__init__(_id, spc, DEVICE_CLASS_CONNECTIVITY, SurepyProduct.HUB) + super().__init__(_id, spc, DEVICE_CLASS_CONNECTIVITY, EntityType.HUB) @property def available(self) -> bool: @@ -156,10 +137,12 @@ def is_on(self) -> bool: def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes of the device.""" attributes = None - if self._state: + if self._surepy_entity.raw_data(): attributes = { - "led_mode": int(self._state["led_mode"]), - "pairing_mode": bool(self._state["pairing_mode"]), + "led_mode": int(self._surepy_entity.raw_data()["status"]["led_mode"]), + "pairing_mode": bool( + self._surepy_entity.raw_data()["status"]["pairing_mode"] + ), } return attributes @@ -170,13 +153,13 @@ class Pet(SurePetcareBinarySensor): def __init__(self, _id: int, spc: SurePetcareAPI) -> None: """Initialize a Sure Petcare Pet.""" - super().__init__(_id, spc, DEVICE_CLASS_PRESENCE, SurepyProduct.PET) + super().__init__(_id, spc, DEVICE_CLASS_PRESENCE, EntityType.PET) @property def is_on(self) -> bool: """Return true if entity is at home.""" try: - return bool(SureLocationID(self._state["where"]) == SureLocationID.INSIDE) + return bool(Location(self._state.where) == Location.INSIDE) except (KeyError, TypeError): return False @@ -185,19 +168,15 @@ def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes of the device.""" attributes = None if self._state: - attributes = { - "since": str( - datetime.fromisoformat(self._state["since"]).replace(tzinfo=None) - ), - "where": SureLocationID(self._state["where"]).name.capitalize(), - } + attributes = {"since": self._state.since, "where": self._state.where} return attributes - async def async_update(self) -> None: + @callback + def _async_update(self) -> None: """Get the latest data and update the state.""" - self._spc_data = self._spc.states[self._sure_type].get(self._id) - self._state = self._spc_data.get("position") + self._surepy_entity = self._spc.states[self._id] + self._state = self._surepy_entity.location _LOGGER.debug("%s -> self._state: %s", self._name, self._state) @@ -207,7 +186,7 @@ class DeviceConnectivity(SurePetcareBinarySensor): def __init__( self, _id: int, - sure_type: SurepyProduct, + sure_type: EntityType, spc: SurePetcareAPI, ) -> None: """Initialize a Sure Petcare Device.""" @@ -221,7 +200,7 @@ def name(self) -> str: @property def unique_id(self) -> str: """Return an unique ID.""" - return f"{self._spc_data['household_id']}-{self._id}-connectivity" + return f"{self._surepy_entity.household_id}-{self._id}-connectivity" @property def available(self) -> bool: diff --git a/homeassistant/components/surepetcare/const.py b/homeassistant/components/surepetcare/const.py index 86215c12ade66..cb5a78a3c1ef2 100644 --- a/homeassistant/components/surepetcare/const.py +++ b/homeassistant/components/surepetcare/const.py @@ -1,24 +1,11 @@ """Constants for the Sure Petcare component.""" -from datetime import timedelta - DOMAIN = "surepetcare" -DEFAULT_DEVICE_CLASS = "lock" -DEFAULT_ICON = "mdi:cat" -DEFAULT_SCAN_INTERVAL = timedelta(minutes=3) -DATA_SURE_PETCARE = f"data_{DOMAIN}" SPC = "spc" -SUREPY = "surepy" -CONF_HOUSEHOLD_ID = "household_id" CONF_FEEDERS = "feeders" CONF_FLAPS = "flaps" -CONF_PARENT = "parent" CONF_PETS = "pets" -CONF_PRODUCT_ID = "product_id" -CONF_DATA = "data" - -SURE_IDS = "sure_ids" # platforms TOPIC_UPDATE = f"{DOMAIN}_data_update" @@ -27,7 +14,6 @@ SURE_API_TIMEOUT = 60 # flap -BATTERY_ICON = "mdi:battery" SURE_BATT_VOLTAGE_FULL = 1.6 # voltage SURE_BATT_VOLTAGE_LOW = 1.25 # voltage SURE_BATT_VOLTAGE_DIFF = SURE_BATT_VOLTAGE_FULL - SURE_BATT_VOLTAGE_LOW diff --git a/homeassistant/components/surepetcare/manifest.json b/homeassistant/components/surepetcare/manifest.json index 6c5b0616be755..231ede6474f04 100644 --- a/homeassistant/components/surepetcare/manifest.json +++ b/homeassistant/components/surepetcare/manifest.json @@ -3,6 +3,6 @@ "name": "Sure Petcare", "documentation": "https://www.home-assistant.io/integrations/surepetcare", "codeowners": ["@benleb"], - "requirements": ["surepy==0.4.0"], + "requirements": ["surepy==0.6.0"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/surepetcare/sensor.py b/homeassistant/components/surepetcare/sensor.py index 0a49781767b20..33396e252676e 100644 --- a/homeassistant/components/surepetcare/sensor.py +++ b/homeassistant/components/surepetcare/sensor.py @@ -4,22 +4,17 @@ import logging from typing import Any -from surepy import SureLockStateID, SurepyProduct +from surepy.entities import SurepyEntity +from surepy.enums import EntityType from homeassistant.components.sensor import SensorEntity -from homeassistant.const import ( - ATTR_VOLTAGE, - CONF_ID, - CONF_TYPE, - DEVICE_CLASS_BATTERY, - PERCENTAGE, -) +from homeassistant.const import ATTR_VOLTAGE, DEVICE_CLASS_BATTERY, PERCENTAGE from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import SurePetcareAPI from .const import ( - DATA_SURE_PETCARE, + DOMAIN, SPC, SURE_BATT_VOLTAGE_DIFF, SURE_BATT_VOLTAGE_LOW, @@ -34,56 +29,39 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if discovery_info is None: return - entities = [] + entities: list[SurepyEntity] = [] - spc = hass.data[DATA_SURE_PETCARE][SPC] + spc: SurePetcareAPI = hass.data[DOMAIN][SPC] - for entity in spc.ids: - sure_type = entity[CONF_TYPE] + for surepy_entity in spc.states.values(): - if sure_type in [ - SurepyProduct.CAT_FLAP, - SurepyProduct.PET_FLAP, - SurepyProduct.FEEDER, + if surepy_entity.type in [ + EntityType.CAT_FLAP, + EntityType.PET_FLAP, + EntityType.FEEDER, + EntityType.FELAQUA, ]: - entities.append(SureBattery(entity[CONF_ID], sure_type, spc)) - - if sure_type in [SurepyProduct.CAT_FLAP, SurepyProduct.PET_FLAP]: - entities.append(Flap(entity[CONF_ID], sure_type, spc)) + entities.append(SureBattery(surepy_entity.id, spc)) - async_add_entities(entities, True) + async_add_entities(entities) class SurePetcareSensor(SensorEntity): """A binary sensor implementation for Sure Petcare Entities.""" - def __init__(self, _id: int, sure_type: SurepyProduct, spc: SurePetcareAPI): + def __init__(self, _id: int, spc: SurePetcareAPI): """Initialize a Sure Petcare sensor.""" self._id = _id - self._sure_type = sure_type + self._spc: SurePetcareAPI = spc - self._spc = spc - self._spc_data: dict[str, Any] = self._spc.states[self._sure_type].get(self._id) + self._surepy_entity: SurepyEntity = self._spc.states[_id] self._state: dict[str, Any] = {} - self._name = ( - f"{self._sure_type.name.capitalize()} " - f"{self._spc_data['name'].capitalize()}" + f"{self._surepy_entity.type.name.capitalize()} " + f"{self._surepy_entity.name.capitalize()}" ) - self._async_unsub_dispatcher_connect = None - - @property - def name(self) -> str: - """Return the name of the device if any.""" - return self._name - - @property - def unique_id(self) -> str: - """Return an unique ID.""" - return f"{self._spc_data['household_id']}-{self._id}" - @property def available(self) -> bool: """Return true if entity is available.""" @@ -94,46 +72,19 @@ def should_poll(self) -> bool: """Return true.""" return False - async def async_update(self) -> None: + @callback + def _async_update(self) -> None: """Get the latest data and update the state.""" - self._spc_data = self._spc.states[self._sure_type].get(self._id) - self._state = self._spc_data.get("status") + self._surepy_entity = self._spc.states[self._id] + self._state = self._surepy_entity.raw_data()["status"] _LOGGER.debug("%s -> self._state: %s", self._name, self._state) async def async_added_to_hass(self) -> None: """Register callbacks.""" - - @callback - def update() -> None: - """Update the state.""" - self.async_schedule_update_ha_state(True) - - self._async_unsub_dispatcher_connect = async_dispatcher_connect( - self.hass, TOPIC_UPDATE, update + self.async_on_remove( + async_dispatcher_connect(self.hass, TOPIC_UPDATE, self._async_update) ) - - async def async_will_remove_from_hass(self) -> None: - """Disconnect dispatcher listener when removed.""" - if self._async_unsub_dispatcher_connect: - self._async_unsub_dispatcher_connect() - - -class Flap(SurePetcareSensor): - """Sure Petcare Flap.""" - - @property - def state(self) -> int | None: - """Return battery level in percent.""" - return SureLockStateID(self._state["locking"]["mode"]).name.capitalize() - - @property - def extra_state_attributes(self) -> dict[str, Any] | None: - """Return the state attributes of the device.""" - attributes = None - if self._state: - attributes = {"learn_mode": bool(self._state["learn_mode"])} - - return attributes + self._async_update() class SureBattery(SurePetcareSensor): @@ -160,7 +111,7 @@ def state(self) -> int | None: @property def unique_id(self) -> str: """Return an unique ID.""" - return f"{self._spc_data['household_id']}-{self._id}-battery" + return f"{self._surepy_entity.household_id}-{self._surepy_entity.id}-battery" @property def device_class(self) -> str: diff --git a/requirements_all.txt b/requirements_all.txt index 660a2c474cfea..d2fd9fb61551f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2178,7 +2178,7 @@ sucks==0.9.4 sunwatcher==0.2.1 # homeassistant.components.surepetcare -surepy==0.4.0 +surepy==0.6.0 # homeassistant.components.swiss_hydrological_data swisshydrodata==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8fffd4c0176a0..499b9f1b36467 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1165,7 +1165,7 @@ subarulink==0.3.12 sunwatcher==0.2.1 # homeassistant.components.surepetcare -surepy==0.4.0 +surepy==0.6.0 # homeassistant.components.synology_dsm synologydsm-api==1.0.2 diff --git a/tests/components/surepetcare/__init__.py b/tests/components/surepetcare/__init__.py index d4af323d0630b..7dda9e23d90f0 100644 --- a/tests/components/surepetcare/__init__.py +++ b/tests/components/surepetcare/__init__.py @@ -1,11 +1,9 @@ """Tests for Sure Petcare integration.""" -from unittest.mock import patch - from homeassistant.components.surepetcare.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -HOUSEHOLD_ID = "household-id" -HUB_ID = "hub-id" +HOUSEHOLD_ID = 987654321 +HUB_ID = 123456789 MOCK_HUB = { "id": HUB_ID, @@ -79,10 +77,3 @@ "pets": [24680], }, } - - -def _patch_sensor_setup(): - return patch( - "homeassistant.components.surepetcare.sensor.async_setup_platform", - return_value=True, - ) diff --git a/tests/components/surepetcare/conftest.py b/tests/components/surepetcare/conftest.py index 44e2a72240687..43738f2258706 100644 --- a/tests/components/surepetcare/conftest.py +++ b/tests/components/surepetcare/conftest.py @@ -1,22 +1,18 @@ """Define fixtures available for all tests.""" -from unittest.mock import AsyncMock, patch +from unittest.mock import patch -from pytest import fixture -from surepy import SurePetcare +import pytest +from surepy import MESTART_RESOURCE -from homeassistant.helpers.aiohttp_client import async_get_clientsession +from . import MOCK_API_DATA -@fixture -async def surepetcare(hass): +@pytest.fixture +async def surepetcare(): """Mock the SurePetcare for easier testing.""" - with patch("homeassistant.components.surepetcare.SurePetcare") as mock_surepetcare: - instance = mock_surepetcare.return_value = SurePetcare( - "test-username", - "test-password", - hass.loop, - async_get_clientsession(hass), - api_timeout=1, - ) - instance._get_resource = AsyncMock(return_value=None) - yield mock_surepetcare + with patch("surepy.SureAPIClient", autospec=True) as mock_client_class, patch( + "surepy.find_token" + ): + client = mock_client_class.return_value + client.resources = {MESTART_RESOURCE: {"data": MOCK_API_DATA}} + yield client diff --git a/tests/components/surepetcare/test_binary_sensor.py b/tests/components/surepetcare/test_binary_sensor.py index 67755dbd6450d..cd0445dd6d543 100644 --- a/tests/components/surepetcare/test_binary_sensor.py +++ b/tests/components/surepetcare/test_binary_sensor.py @@ -1,34 +1,32 @@ """The tests for the Sure Petcare binary sensor platform.""" -from surepy import MESTART_RESOURCE from homeassistant.components.surepetcare.const import DOMAIN from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component -from . import MOCK_API_DATA, MOCK_CONFIG, _patch_sensor_setup +from . import HOUSEHOLD_ID, HUB_ID, MOCK_CONFIG EXPECTED_ENTITY_IDS = { - "binary_sensor.pet_flap_pet_flap_connectivity": "household-id-13576-connectivity", - "binary_sensor.pet_flap_cat_flap_connectivity": "household-id-13579-connectivity", - "binary_sensor.feeder_feeder_connectivity": "household-id-12345-connectivity", - "binary_sensor.pet_pet": "household-id-24680", - "binary_sensor.hub_hub": "household-id-hub-id", + "binary_sensor.pet_flap_pet_flap_connectivity": f"{HOUSEHOLD_ID}-13576-connectivity", + "binary_sensor.cat_flap_cat_flap_connectivity": f"{HOUSEHOLD_ID}-13579-connectivity", + "binary_sensor.feeder_feeder_connectivity": f"{HOUSEHOLD_ID}-12345-connectivity", + "binary_sensor.pet_pet": f"{HOUSEHOLD_ID}-24680", + "binary_sensor.hub_hub": f"{HOUSEHOLD_ID}-{HUB_ID}", } async def test_binary_sensors(hass, surepetcare) -> None: """Test the generation of unique ids.""" - instance = surepetcare.return_value - instance._resource[MESTART_RESOURCE] = {"data": MOCK_API_DATA} - - with _patch_sensor_setup(): - assert await async_setup_component(hass, DOMAIN, MOCK_CONFIG) - await hass.async_block_till_done() + assert await async_setup_component(hass, DOMAIN, MOCK_CONFIG) + await hass.async_block_till_done() entity_registry = er.async_get(hass) state_entity_ids = hass.states.async_entity_ids() for entity_id, unique_id in EXPECTED_ENTITY_IDS.items(): assert entity_id in state_entity_ids + state = hass.states.get(entity_id) + assert state + assert state.state == "on" entity = entity_registry.async_get(entity_id) assert entity.unique_id == unique_id diff --git a/tests/components/surepetcare/test_sensor.py b/tests/components/surepetcare/test_sensor.py new file mode 100644 index 0000000000000..8e7160364ea50 --- /dev/null +++ b/tests/components/surepetcare/test_sensor.py @@ -0,0 +1,29 @@ +"""Test the surepetcare sensor platform.""" +from homeassistant.components.surepetcare.const import DOMAIN +from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component + +from . import HOUSEHOLD_ID, MOCK_CONFIG + +EXPECTED_ENTITY_IDS = { + "sensor.pet_flap_pet_flap_battery_level": f"{HOUSEHOLD_ID}-13576-battery", + "sensor.cat_flap_cat_flap_battery_level": f"{HOUSEHOLD_ID}-13579-battery", + "sensor.feeder_feeder_battery_level": f"{HOUSEHOLD_ID}-12345-battery", +} + + +async def test_binary_sensors(hass, surepetcare) -> None: + """Test the generation of unique ids.""" + assert await async_setup_component(hass, DOMAIN, MOCK_CONFIG) + await hass.async_block_till_done() + + entity_registry = er.async_get(hass) + state_entity_ids = hass.states.async_entity_ids() + + for entity_id, unique_id in EXPECTED_ENTITY_IDS.items(): + assert entity_id in state_entity_ids + state = hass.states.get(entity_id) + assert state + assert state.state == "100" + entity = entity_registry.async_get(entity_id) + assert entity.unique_id == unique_id From 458ca970c9e8d0160f34190bc925bdb53614172a Mon Sep 17 00:00:00 2001 From: tkdrob Date: Tue, 27 Apr 2021 15:02:48 -0400 Subject: [PATCH 0590/1317] Add selectors to profiler services (#49781) --- .../components/profiler/services.yaml | 40 +++++++++++++++++-- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/profiler/services.yaml b/homeassistant/components/profiler/services.yaml index 2b59c7a405418..ff634e02ac54c 100644 --- a/homeassistant/components/profiler/services.yaml +++ b/homeassistant/components/profiler/services.yaml @@ -1,30 +1,62 @@ start: + name: Start description: Start the Profiler fields: seconds: + name: Seconds description: The number of seconds to run the profiler. example: 60.0 + default: 60.0 + selector: + number: + min: 1 + max: 3600 + unit_of_measurement: seconds memory: + name: Memory description: Start the Memory Profiler fields: seconds: + name: Seconds description: The number of seconds to run the memory profiler. example: 60.0 + default: 60.0 + selector: + number: + min: 1 + max: 3600 + unit_of_measurement: seconds start_log_objects: + name: Start log objects description: Start logging growth of objects in memory fields: scan_interval: + name: Scan interval description: The number of seconds between logging objects. example: 60.0 + default: 30.0 + selector: + number: + min: 1 + max: 3600 + unit_of_measurement: seconds stop_log_objects: - description: Stop logging growth of objects in memory + name: Stop log objects + description: Stop logging growth of objects in memory. dump_log_objects: + name: Dump log objects description: Dump the repr of all matching objects to the log. fields: type: - description: The type of objects to dump to the log + name: Type + description: The type of objects to dump to the log. + required: true example: State + selector: + text: log_thread_frames: - description: Log the current frames for all threads + name: Log thread frames + description: Log the current frames for all threads. log_event_loop_scheduled: - description: Log what is scheduled in the event loop + name: Log event loop scheduled + description: Log what is scheduled in the event loop. From 5e00fdccfdf7eb335d69fb240fb7a850d3758ceb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Tue, 27 Apr 2021 22:41:03 +0300 Subject: [PATCH 0591/1317] Use ConfigEntry.async_on_unload in UpCloud (#49784) --- homeassistant/components/upcloud/__init__.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/upcloud/__init__.py b/homeassistant/components/upcloud/__init__.py index 4f13aaa546062..d3835f30bd907 100644 --- a/homeassistant/components/upcloud/__init__.py +++ b/homeassistant/components/upcloud/__init__.py @@ -21,7 +21,7 @@ STATE_ON, STATE_PROBLEM, ) -from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( @@ -91,7 +91,6 @@ def __init__( hass, _LOGGER, name=f"{username}@UpCloud", update_interval=update_interval ) self.cloud_manager = cloud_manager - self.unsub_handlers: list[CALLBACK_TYPE] = [] async def async_update_config(self, config_entry: ConfigEntry) -> None: """Handle config update.""" @@ -210,10 +209,10 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b await coordinator.async_config_entry_first_refresh() # Listen to config entry updates - coordinator.unsub_handlers.append( + config_entry.async_on_unload( config_entry.add_update_listener(_async_signal_options_update) ) - coordinator.unsub_handlers.append( + config_entry.async_on_unload( async_dispatcher_connect( hass, _config_entry_update_signal_name(config_entry), @@ -237,11 +236,7 @@ async def async_unload_entry(hass, config_entry): for domain in CONFIG_ENTRY_DOMAINS: await hass.config_entries.async_forward_entry_unload(config_entry, domain) - coordinator: UpCloudDataUpdateCoordinator = hass.data[ - DATA_UPCLOUD - ].coordinators.pop(config_entry.data[CONF_USERNAME]) - while coordinator.unsub_handlers: - coordinator.unsub_handlers.pop()() + hass.data[DATA_UPCLOUD].coordinators.pop(config_entry.data[CONF_USERNAME]) return True From d2fd50444298b88be9558ab4a149f238a536e7a0 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 27 Apr 2021 21:48:24 +0200 Subject: [PATCH 0592/1317] Limit precision when stringifying float states (#48822) * Limit precision when stringifying float states * Add test * Fix typing * Move StateType * Update * Move conversion to entity helper * Address review comments * Tweak precision * Tweak * Make _stringify_state an instance method --- homeassistant/helpers/entity.py | 26 +++++++++++++++++----- tests/components/input_number/test_init.py | 20 +++++++++++++++++ tests/helpers/test_entity.py | 14 ++++++++++++ 3 files changed, 55 insertions(+), 5 deletions(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index e6af7751c88f8..1706d9b309db4 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -7,6 +7,8 @@ from datetime import datetime, timedelta import functools as ft import logging +import math +import sys from timeit import default_timer as timer from typing import Any @@ -43,6 +45,10 @@ SOURCE_CONFIG_ENTRY = "config_entry" SOURCE_PLATFORM_CONFIG = "platform_config" +# Used when converting float states to string: limit precision according to machine +# epsilon to make the string representation readable +FLOAT_PRECISION = abs(int(math.floor(math.log10(abs(sys.float_info.epsilon))))) - 1 + @callback @bind_hass @@ -327,6 +333,19 @@ def async_write_ha_state(self) -> None: self._async_write_ha_state() + def _stringify_state(self) -> str: + """Convert state to string.""" + if not self.available: + return STATE_UNAVAILABLE + state = self.state + if state is None: + return STATE_UNKNOWN + if isinstance(state, float): + # If the entity's state is a float, limit precision according to machine + # epsilon to make the string representation readable + return f"{state:.{FLOAT_PRECISION}}" + return str(state) + @callback def _async_write_ha_state(self) -> None: """Write the state to the state machine.""" @@ -346,11 +365,8 @@ def _async_write_ha_state(self) -> None: attr = self.capability_attributes attr = dict(attr) if attr else {} - if not self.available: - state = STATE_UNAVAILABLE - else: - sstate = self.state - state = STATE_UNKNOWN if sstate is None else str(sstate) + state = self._stringify_state() + if self.available: attr.update(self.state_attributes or {}) extra_state_attributes = self.extra_state_attributes # Backwards compatibility for "device_state_attributes" deprecated in 2021.4 diff --git a/tests/components/input_number/test_init.py b/tests/components/input_number/test_init.py index d6d80a9ad8704..ca496723d99c1 100644 --- a/tests/components/input_number/test_init.py +++ b/tests/components/input_number/test_init.py @@ -162,6 +162,26 @@ async def test_increment(hass): assert float(state.state) == 51 +async def test_rounding(hass): + """Test increment introducing floating point error is rounded.""" + assert await async_setup_component( + hass, + DOMAIN, + {DOMAIN: {"test_2": {"initial": 2.4, "min": 0, "max": 51, "step": 1.2}}}, + ) + entity_id = "input_number.test_2" + assert 2.4 + 1.2 != 3.6 + + state = hass.states.get(entity_id) + assert float(state.state) == 2.4 + + await increment(hass, entity_id) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert float(state.state) == 3.6 + + async def test_decrement(hass): """Test decrement method.""" assert await async_setup_component( diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 8d587301fb849..8142f563f0172 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -774,3 +774,17 @@ async def test_get_supported_features_raises_on_unknown(hass): """Test get_supported_features raises on unknown entity_id.""" with pytest.raises(HomeAssistantError): entity.get_supported_features(hass, "hello.world") + + +async def test_float_conversion(hass): + """Test conversion of float state to string rounds.""" + assert 2.4 + 1.2 != 3.6 + with patch.object(entity.Entity, "state", PropertyMock(return_value=2.4 + 1.2)): + ent = entity.Entity() + ent.hass = hass + ent.entity_id = "hello.world" + ent.async_write_ha_state() + + state = hass.states.get("hello.world") + assert state is not None + assert state.state == "3.6" From 87420627a8e13e829240e83f25536b74d7d1b5bd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 27 Apr 2021 10:10:04 -1000 Subject: [PATCH 0593/1317] Reduce config entry setup/unload boilerplate Q-S (#49778) --- homeassistant/components/rachio/__init__.py | 17 ++------------ .../components/rainmachine/__init__.py | 14 ++--------- .../components/recollect_waste/__init__.py | 15 ++---------- homeassistant/components/rfxtrx/__init__.py | 14 ++--------- homeassistant/components/ring/__init__.py | 15 ++---------- homeassistant/components/risco/__init__.py | 10 +------- .../rituals_perfume_genie/__init__.py | 15 ++---------- homeassistant/components/roku/__init__.py | 17 ++------------ homeassistant/components/roomba/__init__.py | 14 +++-------- .../components/rpi_power/__init__.py | 8 +++---- .../components/ruckus_unleashed/__init__.py | 15 ++---------- .../components/screenlogic/__init__.py | 14 ++--------- homeassistant/components/sense/__init__.py | 15 ++---------- homeassistant/components/sharkiq/__init__.py | 14 +++-------- homeassistant/components/shelly/__init__.py | 14 ++--------- .../components/simplisafe/__init__.py | 14 ++--------- homeassistant/components/sma/__init__.py | 15 ++---------- homeassistant/components/smappee/__init__.py | 17 ++------------ .../components/smart_meter_texas/__init__.py | 14 ++--------- homeassistant/components/smarthab/__init__.py | 22 ++++-------------- .../components/smartthings/__init__.py | 11 ++------- homeassistant/components/smarttub/__init__.py | 23 ++++--------------- homeassistant/components/smhi/__init__.py | 13 +++++------ homeassistant/components/sms/__init__.py | 16 ++----------- homeassistant/components/solarlog/__init__.py | 8 +++---- homeassistant/components/soma/__init__.py | 17 ++------------ homeassistant/components/somfy/__init__.py | 14 ++--------- .../components/somfy_mylink/__init__.py | 14 ++--------- homeassistant/components/sonarr/__init__.py | 15 ++---------- homeassistant/components/songpal/__init__.py | 8 +++---- .../components/speedtestdotnet/__init__.py | 17 +++++++------- homeassistant/components/spider/__init__.py | 16 ++----------- homeassistant/components/spotify/__init__.py | 10 ++++---- .../components/squeezebox/__init__.py | 8 +++---- .../components/srp_energy/__init__.py | 8 ++----- homeassistant/components/starline/__init__.py | 16 ++++++------- homeassistant/components/subaru/__init__.py | 15 ++---------- homeassistant/components/syncthru/__init__.py | 10 ++++---- .../components/synology_dsm/__init__.py | 18 +++------------ tests/components/smhi/test_init.py | 5 ++-- 40 files changed, 119 insertions(+), 436 deletions(-) diff --git a/homeassistant/components/rachio/__init__.py b/homeassistant/components/rachio/__init__.py index 0335bd9928c05..3f75537cc8dbf 100644 --- a/homeassistant/components/rachio/__init__.py +++ b/homeassistant/components/rachio/__init__.py @@ -1,5 +1,4 @@ """Integration with the Rachio Iro sprinkler system controller.""" -import asyncio import logging import secrets @@ -28,18 +27,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) - + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok @@ -96,9 +86,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data[DOMAIN][entry.entry_id] = person async_register_webhook(hass, webhook_id, entry.entry_id) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index e71e8a1f6d2c1..4e709e319f619 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -155,10 +155,7 @@ async def async_update(api_category: str) -> dict: await asyncio.gather(*controller_init_tasks) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) hass.data[DOMAIN][DATA_LISTENER] = entry.add_update_listener(async_reload_entry) @@ -167,14 +164,7 @@ async def async_update(api_category: str) -> dict: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload an RainMachine config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN][DATA_COORDINATOR].pop(entry.entry_id) cancel_listener = hass.data[DOMAIN][DATA_LISTENER].pop(entry.entry_id) diff --git a/homeassistant/components/recollect_waste/__init__.py b/homeassistant/components/recollect_waste/__init__.py index 2e6f780c74928..f061532c3d1b8 100644 --- a/homeassistant/components/recollect_waste/__init__.py +++ b/homeassistant/components/recollect_waste/__init__.py @@ -1,7 +1,6 @@ """The ReCollect Waste integration.""" from __future__ import annotations -import asyncio from datetime import date, timedelta from aiorecollect.client import Client, PickupEvent @@ -58,10 +57,7 @@ async def async_get_pickup_events() -> list[PickupEvent]: hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] = coordinator - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) hass.data[DOMAIN][DATA_LISTENER][entry.entry_id] = entry.add_update_listener( async_reload_entry @@ -77,14 +73,7 @@ async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload an RainMachine config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN][DATA_COORDINATOR].pop(entry.entry_id) cancel_listener = hass.data[DOMAIN][DATA_LISTENER].pop(entry.entry_id) diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index d23a3e4e6ffb0..a4be36df998b1 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -202,24 +202,14 @@ async def async_setup_entry(hass, entry: config_entries.ConfigEntry): ) return False - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass, entry: config_entries.ConfigEntry): """Unload RFXtrx component.""" - if not all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ): + if not await hass.config_entries.async_unload_platforms(entry, PLATFORMS): return False hass.services.async_remove(DOMAIN, SERVICE_SEND) diff --git a/homeassistant/components/ring/__init__.py b/homeassistant/components/ring/__init__.py index f5211ac54c03c..a0d07d0a878a1 100644 --- a/homeassistant/components/ring/__init__.py +++ b/homeassistant/components/ring/__init__.py @@ -100,10 +100,7 @@ def token_updater(token): ), } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) if hass.services.has_service(DOMAIN, "update"): return True @@ -124,15 +121,7 @@ async def async_refresh_all(_): async def async_unload_entry(hass, entry): """Unload Ring entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) - if not unload_ok: + if not await hass.config_entries.async_unload_platforms(entry, PLATFORMS): return False hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/risco/__init__.py b/homeassistant/components/risco/__init__.py index 3a39bbb00f3f9..48c50f9cc462b 100644 --- a/homeassistant/components/risco/__init__.py +++ b/homeassistant/components/risco/__init__.py @@ -71,15 +71,7 @@ async def start_platforms(): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) - + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/rituals_perfume_genie/__init__.py b/homeassistant/components/rituals_perfume_genie/__init__.py index 3cc5c29d36909..7b1a4ae7d1cd1 100644 --- a/homeassistant/components/rituals_perfume_genie/__init__.py +++ b/homeassistant/components/rituals_perfume_genie/__init__.py @@ -1,5 +1,4 @@ """The Rituals Perfume Genie integration.""" -import asyncio from datetime import timedelta import logging @@ -48,24 +47,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data[DOMAIN][entry.entry_id][DEVICES][hublot] = device hass.data[DOMAIN][entry.entry_id][COORDINATORS][hublot] = coordinator - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/roku/__init__.py b/homeassistant/components/roku/__init__.py index d7bf30593742e..72ecd0a8d056d 100644 --- a/homeassistant/components/roku/__init__.py +++ b/homeassistant/components/roku/__init__.py @@ -1,7 +1,6 @@ """Support for Roku.""" from __future__ import annotations -import asyncio from datetime import timedelta import logging from typing import Any @@ -49,28 +48,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) - + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok diff --git a/homeassistant/components/roomba/__init__.py b/homeassistant/components/roomba/__init__.py index aa7c06d23a088..3936d3f6d1dfe 100644 --- a/homeassistant/components/roomba/__init__.py +++ b/homeassistant/components/roomba/__init__.py @@ -68,10 +68,7 @@ async def _async_disconnect_roomba(event): CANCEL_STOP: cancel_stop, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) if not config_entry.update_listeners: config_entry.add_update_listener(async_update_options) @@ -119,13 +116,8 @@ async def async_update_options(hass, config_entry): async def async_unload_entry(hass, config_entry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS ) if unload_ok: domain_data = hass.data[DOMAIN][config_entry.entry_id] diff --git a/homeassistant/components/rpi_power/__init__.py b/homeassistant/components/rpi_power/__init__.py index 3f9a9d6e74cc1..305ad7d1f6283 100644 --- a/homeassistant/components/rpi_power/__init__.py +++ b/homeassistant/components/rpi_power/__init__.py @@ -2,15 +2,15 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +PLATFORMS = ["binary_sensor"] + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Raspberry Pi Power Supply Checker from a config entry.""" - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "binary_sensor") - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - return await hass.config_entries.async_forward_entry_unload(entry, "binary_sensor") + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/ruckus_unleashed/__init__.py b/homeassistant/components/ruckus_unleashed/__init__.py index 78d15f24a63f3..6ea3b736dcd40 100644 --- a/homeassistant/components/ruckus_unleashed/__init__.py +++ b/homeassistant/components/ruckus_unleashed/__init__.py @@ -1,5 +1,4 @@ """The Ruckus Unleashed integration.""" -import asyncio from pyruckus import Ruckus @@ -64,24 +63,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: UNDO_UPDATE_LISTENERS: [], } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: for listener in hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENERS]: listener() diff --git a/homeassistant/components/screenlogic/__init__.py b/homeassistant/components/screenlogic/__init__.py index 6fa19582a46d4..30f544303cd2e 100644 --- a/homeassistant/components/screenlogic/__init__.py +++ b/homeassistant/components/screenlogic/__init__.py @@ -77,24 +77,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): "listener": entry.add_update_listener(async_update_listener), } - for component in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) hass.data[DOMAIN][entry.entry_id]["listener"]() if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/sense/__init__.py b/homeassistant/components/sense/__init__.py index ee466c813f56a..162b7cd75cf2c 100644 --- a/homeassistant/components/sense/__init__.py +++ b/homeassistant/components/sense/__init__.py @@ -1,5 +1,4 @@ """Support for monitoring a Sense energy sensor.""" -import asyncio from datetime import timedelta import logging @@ -146,10 +145,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): SENSE_DISCOVERED_DEVICES_DATA: sense_discovered_devices, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) async def async_sense_update(_): """Retrieve latest state.""" @@ -181,14 +177,7 @@ def _remove_update_callback_at_stop(event): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) data = hass.data[DOMAIN][entry.entry_id] data[EVENT_STOP_REMOVE]() data[TRACK_TIME_REMOVE]() diff --git a/homeassistant/components/sharkiq/__init__.py b/homeassistant/components/sharkiq/__init__.py index 02e1bba85111d..ed5c7ae1b5421 100644 --- a/homeassistant/components/sharkiq/__init__.py +++ b/homeassistant/components/sharkiq/__init__.py @@ -63,10 +63,7 @@ async def async_setup_entry(hass, config_entry): hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][config_entry.entry_id] = coordinator - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) return True @@ -87,13 +84,8 @@ async def async_update_options(hass, config_entry): async def async_unload_entry(hass, config_entry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS ) if unload_ok: domain_data = hass.data[DOMAIN][config_entry.entry_id] diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 1e68ca784097e..29eb07b3a906e 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -137,10 +137,7 @@ async def async_device_setup( ] = ShellyDeviceRestWrapper(hass, device) platforms = PLATFORMS - for platform in platforms: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, platforms) class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator): @@ -334,14 +331,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][REST] = None platforms = PLATFORMS - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in platforms - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, platforms) if unload_ok: hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][COAP].shutdown() hass.data[DOMAIN][DATA_CONFIG_ENTRY].pop(entry.entry_id) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 12c27c3c63cf9..9997b86c288e6 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -224,10 +224,7 @@ async def async_setup_entry(hass, config_entry): # noqa: C901 ) await simplisafe.async_init() - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) @callback def verify_system_exists(coro): @@ -329,14 +326,7 @@ async def set_system_properties(call): async def async_unload_entry(hass, entry): """Unload a SimpliSafe config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN][DATA_CLIENT].pop(entry.entry_id) for remove_listener in hass.data[DOMAIN][DATA_LISTENER].pop(entry.entry_id): diff --git a/homeassistant/components/sma/__init__.py b/homeassistant/components/sma/__init__.py index 6ca5fe712b78e..ef948440a1714 100644 --- a/homeassistant/components/sma/__init__.py +++ b/homeassistant/components/sma/__init__.py @@ -1,7 +1,6 @@ """The sma integration.""" from __future__ import annotations -import asyncio from datetime import timedelta import logging @@ -187,24 +186,14 @@ async def async_close_session(event): PYSMA_REMOVE_LISTENER: remove_stop_listener, } - for component in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: data = hass.data[DOMAIN].pop(entry.entry_id) await data[PYSMA_OBJECT].close_session() diff --git a/homeassistant/components/smappee/__init__.py b/homeassistant/components/smappee/__init__.py index 9c867b7d17f2b..3386f7340eb9f 100644 --- a/homeassistant/components/smappee/__init__.py +++ b/homeassistant/components/smappee/__init__.py @@ -1,5 +1,4 @@ """The Smappee integration.""" -import asyncio from pysmappee import Smappee, helper, mqtt import voluptuous as vol @@ -105,28 +104,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data[DOMAIN][entry.entry_id] = SmappeeBase(hass, smappee) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) - + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id, None) - return unload_ok diff --git a/homeassistant/components/smart_meter_texas/__init__.py b/homeassistant/components/smart_meter_texas/__init__.py index 71504fb52aa13..1fc3eb218a7c3 100644 --- a/homeassistant/components/smart_meter_texas/__init__.py +++ b/homeassistant/components/smart_meter_texas/__init__.py @@ -78,10 +78,7 @@ async def async_update_data(): asyncio.create_task(coordinator.async_refresh()) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -114,14 +111,7 @@ async def read_meters(self): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/smarthab/__init__.py b/homeassistant/components/smarthab/__init__.py index ba6e7a5e5e734..7759d0382249e 100644 --- a/homeassistant/components/smarthab/__init__.py +++ b/homeassistant/components/smarthab/__init__.py @@ -1,5 +1,4 @@ """Support for SmartHab device integration.""" -import asyncio import logging import pysmarthab @@ -69,27 +68,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): # Pass hub object to child platforms hass.data[DOMAIN][entry.entry_id] = {DATA_HUB: hub} - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload config entry from SmartHab integration.""" - - result = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) - - if result: + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) - - return result + return unload_ok diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index d9a96301e6678..00ea0eb681e9e 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -189,10 +189,7 @@ async def retrieve_device_status(device): ) return False - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -217,11 +214,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): if broker: broker.disconnect() - tasks = [ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - return all(await asyncio.gather(*tasks)) + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: diff --git a/homeassistant/components/smarttub/__init__.py b/homeassistant/components/smarttub/__init__.py index c907bfdeae388..a396b50840d3c 100644 --- a/homeassistant/components/smarttub/__init__.py +++ b/homeassistant/components/smarttub/__init__.py @@ -1,5 +1,4 @@ """SmartTub integration.""" -import asyncio import logging from .const import DOMAIN, SMARTTUB_CONTROLLER @@ -22,26 +21,14 @@ async def async_setup_entry(hass, entry): if not await controller.async_setup_entry(entry): return False - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass, entry): """Remove a smarttub config entry.""" - if not all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ): - return False - - hass.data[DOMAIN].pop(entry.entry_id) - - return True + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + return unload_ok diff --git a/homeassistant/components/smhi/__init__.py b/homeassistant/components/smhi/__init__.py index 84151bd35eea5..70ee0aaa38646 100644 --- a/homeassistant/components/smhi/__init__.py +++ b/homeassistant/components/smhi/__init__.py @@ -8,16 +8,15 @@ DEFAULT_NAME = "smhi" +PLATFORMS = ["weather"] -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up SMHI forecast as config entry.""" - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, "weather") - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - await hass.config_entries.async_forward_entry_unload(config_entry, "weather") - return True + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/sms/__init__.py b/homeassistant/components/sms/__init__.py index c4fdb38ebaa63..55238c5cf39ed 100644 --- a/homeassistant/components/sms/__init__.py +++ b/homeassistant/components/sms/__init__.py @@ -1,5 +1,4 @@ """The sms component.""" -import asyncio import voluptuous as vol @@ -46,25 +45,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): if not gateway: return False hass.data[DOMAIN][SMS_GATEWAY] = gateway - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) - + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: gateway = hass.data[DOMAIN].pop(SMS_GATEWAY) await gateway.terminate_async() diff --git a/homeassistant/components/solarlog/__init__.py b/homeassistant/components/solarlog/__init__.py index 5db2e15f12160..f48dcfc6267b5 100644 --- a/homeassistant/components/solarlog/__init__.py +++ b/homeassistant/components/solarlog/__init__.py @@ -2,15 +2,15 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +PLATFORMS = ["sensor"] + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up a config entry for solarlog.""" - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "sensor") - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass, entry): """Unload a config entry.""" - return await hass.config_entries.async_forward_entry_unload(entry, "sensor") + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/soma/__init__.py b/homeassistant/components/soma/__init__.py index 7c4d252208abf..62cdeb11f8b87 100644 --- a/homeassistant/components/soma/__init__.py +++ b/homeassistant/components/soma/__init__.py @@ -1,5 +1,4 @@ """Support for Soma Smartshades.""" -import asyncio from api.soma_api import SomaApi import voluptuous as vol @@ -50,26 +49,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): devices = await hass.async_add_executor_job(hass.data[DOMAIN][API].list_devices) hass.data[DOMAIN][DEVICES] = devices["shades"] - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) - - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) class SomaEntity(Entity): diff --git a/homeassistant/components/somfy/__init__.py b/homeassistant/components/somfy/__init__.py index 80cf20a95c40a..e5c3015d2fa02 100644 --- a/homeassistant/components/somfy/__init__.py +++ b/homeassistant/components/somfy/__init__.py @@ -1,6 +1,5 @@ """Support for Somfy hubs.""" from abc import abstractmethod -import asyncio from datetime import timedelta import logging @@ -135,10 +134,7 @@ async def _update_all_devices(): model=hub.type, ) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -146,13 +142,7 @@ async def _update_all_devices(): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" hass.data[DOMAIN].pop(API, None) - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - return True + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) class SomfyEntity(CoordinatorEntity, Entity): diff --git a/homeassistant/components/somfy_mylink/__init__.py b/homeassistant/components/somfy_mylink/__init__.py index 40240306dc473..dfb0220a53139 100644 --- a/homeassistant/components/somfy_mylink/__init__.py +++ b/homeassistant/components/somfy_mylink/__init__.py @@ -121,10 +121,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): UNDO_UPDATE_LISTENER: undo_listener, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -179,14 +176,7 @@ def _async_migrate_entity_config( async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() diff --git a/homeassistant/components/sonarr/__init__.py b/homeassistant/components/sonarr/__init__.py index 12fe47f80c714..3299842c48ce2 100644 --- a/homeassistant/components/sonarr/__init__.py +++ b/homeassistant/components/sonarr/__init__.py @@ -1,7 +1,6 @@ """The Sonarr component.""" from __future__ import annotations -import asyncio from datetime import timedelta import logging from typing import Any @@ -81,24 +80,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_UNDO_UPDATE_LISTENER: undo_listener, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) hass.data[DOMAIN][entry.entry_id][DATA_UNDO_UPDATE_LISTENER]() diff --git a/homeassistant/components/songpal/__init__.py b/homeassistant/components/songpal/__init__.py index b5d87e29c4539..b542591b29445 100644 --- a/homeassistant/components/songpal/__init__.py +++ b/homeassistant/components/songpal/__init__.py @@ -19,6 +19,8 @@ extra=vol.ALLOW_EXTRA, ) +PLATFORMS = ["media_player"] + async def async_setup(hass: HomeAssistant, config: OrderedDict) -> bool: """Set up songpal environment.""" @@ -38,12 +40,10 @@ async def async_setup(hass: HomeAssistant, config: OrderedDict) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up songpal media player.""" - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "media_player") - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload songpal media player.""" - return await hass.config_entries.async_forward_entry_unload(entry, "media_player") + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/speedtestdotnet/__init__.py b/homeassistant/components/speedtestdotnet/__init__.py index cb3144e266e88..9c76b351f33dc 100644 --- a/homeassistant/components/speedtestdotnet/__init__.py +++ b/homeassistant/components/speedtestdotnet/__init__.py @@ -46,6 +46,8 @@ extra=vol.ALLOW_EXTRA, ) +PLATFORMS = ["sensor"] + def server_id_valid(server_id): """Check if server_id is valid.""" @@ -96,9 +98,7 @@ async def _enable_scheduled_speedtests(*_): hass.data[DOMAIN] = coordinator - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, "sensor") - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) return True @@ -109,11 +109,12 @@ async def async_unload_entry(hass, config_entry): hass.data[DOMAIN].async_unload() - await hass.config_entries.async_forward_entry_unload(config_entry, "sensor") - - hass.data.pop(DOMAIN) - - return True + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS + ) + if unload_ok: + hass.data.pop(DOMAIN) + return unload_ok class SpeedTestDataCoordinator(DataUpdateCoordinator): diff --git a/homeassistant/components/spider/__init__.py b/homeassistant/components/spider/__init__.py index d9ccdfd248a13..887f6471ccab9 100644 --- a/homeassistant/components/spider/__init__.py +++ b/homeassistant/components/spider/__init__.py @@ -1,5 +1,4 @@ """Support for Spider Smart devices.""" -import asyncio import logging from spiderpy.spiderapi import SpiderApi, SpiderApiException, UnauthorizedException @@ -66,25 +65,14 @@ async def async_setup_entry(hass, entry): hass.data[DOMAIN][entry.entry_id] = api - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass, entry): """Unload Spider entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) - + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if not unload_ok: return False diff --git a/homeassistant/components/spotify/__init__.py b/homeassistant/components/spotify/__init__.py index c4b8e30a8ba61..3aab37c9392c8 100644 --- a/homeassistant/components/spotify/__init__.py +++ b/homeassistant/components/spotify/__init__.py @@ -37,6 +37,8 @@ extra=vol.ALLOW_EXTRA, ) +PLATFORMS = [MEDIA_PLAYER_DOMAIN] + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Spotify integration.""" @@ -86,20 +88,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not set(session.token["scope"].split(" ")).issuperset(SPOTIFY_SCOPES): raise ConfigEntryAuthFailed - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, MEDIA_PLAYER_DOMAIN) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload Spotify config entry.""" # Unload entities for this entry/device. - await hass.config_entries.async_forward_entry_unload(entry, MEDIA_PLAYER_DOMAIN) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) # Cleanup del hass.data[DOMAIN][entry.entry_id] if not hass.data[DOMAIN]: del hass.data[DOMAIN] - return True + return unload_ok diff --git a/homeassistant/components/squeezebox/__init__.py b/homeassistant/components/squeezebox/__init__.py index f276daac56ac1..f680c4f5f2fe6 100644 --- a/homeassistant/components/squeezebox/__init__.py +++ b/homeassistant/components/squeezebox/__init__.py @@ -10,12 +10,12 @@ _LOGGER = logging.getLogger(__name__) +PLATFORMS = [MP_DOMAIN] + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Logitech Squeezebox from a config entry.""" - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, MP_DOMAIN) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -34,4 +34,4 @@ async def async_unload_entry(hass, entry): hass.data[DOMAIN][DISCOVERY_TASK].cancel() hass.data[DOMAIN].pop(DISCOVERY_TASK) - return await hass.config_entries.async_forward_entry_unload(entry, MP_DOMAIN) + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/srp_energy/__init__.py b/homeassistant/components/srp_energy/__init__.py index b8a93ee44b07c..785558ba34e06 100644 --- a/homeassistant/components/srp_energy/__init__.py +++ b/homeassistant/components/srp_energy/__init__.py @@ -30,9 +30,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): _LOGGER.error("Unable to connect to Srp Energy: %s", str(ex)) raise ConfigEntryNotReady from ex - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "sensor") - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -42,6 +40,4 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry): # unload srp client hass.data[SRP_ENERGY_DOMAIN] = None # Remove config entry - await hass.config_entries.async_forward_entry_unload(config_entry, "sensor") - - return True + return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) diff --git a/homeassistant/components/starline/__init__.py b/homeassistant/components/starline/__init__.py index 2eb729721d3fc..91edc7badeb30 100644 --- a/homeassistant/components/starline/__init__.py +++ b/homeassistant/components/starline/__init__.py @@ -37,10 +37,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b config_entry_id=config_entry.entry_id, **account.device_info(device) ) - for domain in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, domain) - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) async def async_set_scan_interval(call): """Set scan interval.""" @@ -85,7 +82,9 @@ async def async_update(call=None): ), ) - config_entry.add_update_listener(async_options_updated) + config_entry.async_on_unload( + config_entry.add_update_listener(async_options_updated) + ) await async_options_updated(hass, config_entry) return True @@ -93,12 +92,13 @@ async def async_update(call=None): async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload a config entry.""" - for domain in PLATFORMS: - await hass.config_entries.async_forward_entry_unload(config_entry, domain) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS + ) account: StarlineAccount = hass.data[DOMAIN][config_entry.entry_id] account.unload() - return True + return unload_ok async def async_options_updated(hass: HomeAssistant, config_entry: ConfigEntry) -> None: diff --git a/homeassistant/components/subaru/__init__.py b/homeassistant/components/subaru/__init__.py index 4807ca259101d..94c1243b710e5 100644 --- a/homeassistant/components/subaru/__init__.py +++ b/homeassistant/components/subaru/__init__.py @@ -1,5 +1,4 @@ """The Subaru integration.""" -import asyncio from datetime import timedelta import logging import time @@ -89,24 +88,14 @@ async def async_update_data(): ENTRY_VEHICLES: vehicle_info, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok diff --git a/homeassistant/components/syncthru/__init__.py b/homeassistant/components/syncthru/__init__.py index b09f799df366c..120796d935f81 100644 --- a/homeassistant/components/syncthru/__init__.py +++ b/homeassistant/components/syncthru/__init__.py @@ -16,6 +16,8 @@ _LOGGER = logging.getLogger(__name__) +PLATFORMS = [SENSOR_DOMAIN] + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up config entry.""" @@ -47,17 +49,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: name=printer.hostname(), ) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, SENSOR_DOMAIN) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload the config entry.""" - await hass.config_entries.async_forward_entry_unload(entry, SENSOR_DOMAIN) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) hass.data[DOMAIN].pop(entry.entry_id, None) - return True + return unload_ok def device_identifiers(printer: SyncThru) -> set[tuple[str, str]]: diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index cdfad25e97237..058c810b15711 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -1,7 +1,6 @@ """The Synology DSM component.""" from __future__ import annotations -import asyncio from datetime import timedelta import logging from typing import Any @@ -119,7 +118,7 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): # noqa: C901 +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Synology DSM sensors.""" # Migrate old unique_id @@ -286,25 +285,14 @@ async def async_coordinator_update_data_switches(): update_interval=timedelta(seconds=30), ) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload Synology DSM sensors.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) - + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: entry_data = hass.data[DOMAIN][entry.unique_id] entry_data[UNDO_UPDATE_LISTENER]() diff --git a/tests/components/smhi/test_init.py b/tests/components/smhi/test_init.py index 297a6f587d84d..ac4177dca7d54 100644 --- a/tests/components/smhi/test_init.py +++ b/tests/components/smhi/test_init.py @@ -19,11 +19,12 @@ async def test_forward_async_setup_entry() -> None: hass = Mock() assert await smhi.async_setup_entry(hass, {}) is True - assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 1 + assert len(hass.config_entries.async_setup_platforms.mock_calls) == 1 async def test_forward_async_unload_entry() -> None: """Test that it will forward unload entry.""" hass = AsyncMock() + hass.config_entries.async_unload_platforms = AsyncMock(return_value=True) assert await smhi.async_unload_entry(hass, {}) is True - assert len(hass.config_entries.async_forward_entry_unload.mock_calls) == 1 + assert len(hass.config_entries.async_unload_platforms.mock_calls) == 1 From 4b74c57285d24f424ed6e9b1dc06658b0283e66d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 27 Apr 2021 10:19:57 -1000 Subject: [PATCH 0594/1317] Reduce config entry setup/unload boilerplate T-U (#49786) --- homeassistant/components/tado/__init__.py | 15 ++------------- homeassistant/components/tasmota/__init__.py | 9 +-------- .../components/tellduslive/__init__.py | 10 ++++------ homeassistant/components/tesla/__init__.py | 17 ++++------------- homeassistant/components/tibber/__init__.py | 16 +++------------- homeassistant/components/tile/__init__.py | 16 ++-------------- homeassistant/components/toon/__init__.py | 15 ++------------- .../components/totalconnect/__init__.py | 15 ++------------- homeassistant/components/tplink/__init__.py | 18 ++++++------------ homeassistant/components/traccar/__init__.py | 10 +++++----- homeassistant/components/tradfri/__init__.py | 15 ++------------- .../components/transmission/__init__.py | 14 +++++--------- homeassistant/components/tuya/__init__.py | 14 ++------------ .../components/twentemilieu/__init__.py | 10 +++++----- homeassistant/components/twinkly/__init__.py | 11 +++-------- homeassistant/components/unifi/controller.py | 19 ++++--------------- homeassistant/components/upb/__init__.py | 18 +++--------------- homeassistant/components/upcloud/__init__.py | 12 +++++------- homeassistant/components/upnp/__init__.py | 8 ++++---- 19 files changed, 64 insertions(+), 198 deletions(-) diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index 5a396bedcf2ab..37ee3b47b9b3e 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -1,5 +1,4 @@ """Support for the (unofficial) Tado API.""" -import asyncio from datetime import timedelta import logging @@ -85,10 +84,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): UPDATE_LISTENER: update_listener, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -108,14 +104,7 @@ async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) hass.data[DOMAIN][entry.entry_id][UPDATE_TRACK]() hass.data[DOMAIN][entry.entry_id][UPDATE_LISTENER]() diff --git a/homeassistant/components/tasmota/__init__.py b/homeassistant/components/tasmota/__init__.py index 83baae9c19c56..af7f9222c5018 100644 --- a/homeassistant/components/tasmota/__init__.py +++ b/homeassistant/components/tasmota/__init__.py @@ -104,14 +104,7 @@ async def async_unload_entry(hass, entry): """Unload a config entry.""" # cleanup platforms - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if not unload_ok: return False diff --git a/homeassistant/components/tellduslive/__init__.py b/homeassistant/components/tellduslive/__init__.py index 0473c52ed92d6..716dd8fb1d37a 100644 --- a/homeassistant/components/tellduslive/__init__.py +++ b/homeassistant/components/tellduslive/__init__.py @@ -119,15 +119,13 @@ async def async_unload_entry(hass, config_entry): hass.data[NEW_CLIENT_TASK].cancel() interval_tracker = hass.data.pop(INTERVAL_TRACKER) interval_tracker() - await asyncio.wait( - [ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in hass.data.pop(CONFIG_ENTRY_IS_SETUP) - ] + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, CONFIG_ENTRY_IS_SETUP ) del hass.data[DOMAIN] del hass.data[DATA_CONFIG_ENTRY_LOCK] - return True + del hass.data[CONFIG_ENTRY_IS_SETUP] + return unload_ok class TelldusLiveClient: diff --git a/homeassistant/components/tesla/__init__.py b/homeassistant/components/tesla/__init__.py index 11b96144ed60e..80cefaa9c563f 100644 --- a/homeassistant/components/tesla/__init__.py +++ b/homeassistant/components/tesla/__init__.py @@ -1,5 +1,4 @@ """Support for Tesla cars.""" -import asyncio from collections import defaultdict from datetime import timedelta import logging @@ -188,23 +187,15 @@ async def async_setup_entry(hass, config_entry): for device in all_devices: entry_data["devices"][device.hass_type].append(device) - for platform in PLATFORMS: - _LOGGER.debug("Loading %s", platform) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + return True async def async_unload_entry(hass, config_entry) -> bool: """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS ) for listener in hass.data[DOMAIN][config_entry.entry_id][DATA_LISTENER]: listener() diff --git a/homeassistant/components/tibber/__init__.py b/homeassistant/components/tibber/__init__.py index ed5b0c4ce6068..81c3fd406a2fa 100644 --- a/homeassistant/components/tibber/__init__.py +++ b/homeassistant/components/tibber/__init__.py @@ -73,10 +73,7 @@ async def _close(event): _LOGGER.error("Failed to login. %s", exp) return False - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) # set up notify platform, no entry support for notify component yet, # have to use discovery to load platform. @@ -90,17 +87,10 @@ async def _close(event): async def async_unload_entry(hass, config_entry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS ) - if unload_ok: tibber_connection = hass.data.get(DOMAIN) await tibber_connection.rt_disconnect() - return unload_ok diff --git a/homeassistant/components/tile/__init__.py b/homeassistant/components/tile/__init__.py index 48bf8177c6364..91e1567cd6561 100644 --- a/homeassistant/components/tile/__init__.py +++ b/homeassistant/components/tile/__init__.py @@ -1,5 +1,4 @@ """The Tile component.""" -import asyncio from datetime import timedelta from functools import partial @@ -74,25 +73,14 @@ async def async_update_tile(tile): await gather_with_concurrency(DEFAULT_INIT_TASK_LIMIT, *coordinator_init_tasks) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass, entry): """Unload a Tile config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN][DATA_COORDINATOR].pop(entry.entry_id) - return unload_ok diff --git a/homeassistant/components/toon/__init__.py b/homeassistant/components/toon/__init__.py index 87c68b5addbe5..f05c480aede04 100644 --- a/homeassistant/components/toon/__init__.py +++ b/homeassistant/components/toon/__init__.py @@ -1,5 +1,4 @@ """Support for Toon van Eneco devices.""" -import asyncio import voluptuous as vol @@ -115,10 +114,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) # Spin up the platforms - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) # If Home Assistant is already in a running state, register the webhook # immediately, else trigger it after Home Assistant has finished starting. @@ -139,14 +135,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.data[DOMAIN][entry.entry_id].unregister_webhook() # Unload entities for this entry/device. - unload_ok = all( - await asyncio.gather( - *( - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ) - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) # Cleanup if unload_ok: diff --git a/homeassistant/components/totalconnect/__init__.py b/homeassistant/components/totalconnect/__init__.py index db0fa1e5755b0..c122de310ddfa 100644 --- a/homeassistant/components/totalconnect/__init__.py +++ b/homeassistant/components/totalconnect/__init__.py @@ -1,5 +1,4 @@ """The totalconnect component.""" -import asyncio import logging from total_connect_client import TotalConnectClient @@ -58,24 +57,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = client - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index 17b58569c7eb4..e68c30f48b5b9 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -25,6 +25,8 @@ DOMAIN = "tplink" +PLATFORMS = [CONF_LIGHT, CONF_SWITCH] + TPLINK_HOST_SCHEMA = vol.Schema({vol.Required(CONF_HOST): cv.string}) @@ -109,17 +111,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigType): async def async_unload_entry(hass, entry): """Unload a config entry.""" - forward_unload = hass.config_entries.async_forward_entry_unload - remove_lights = remove_switches = False - if hass.data[DOMAIN][CONF_LIGHT]: - remove_lights = await forward_unload(entry, "light") - if hass.data[DOMAIN][CONF_SWITCH]: - remove_switches = await forward_unload(entry, "switch") - - if remove_lights or remove_switches: + platforms = [platform for platform in PLATFORMS if platform in hass.data[DOMAIN]] + unload_ok = await hass.config_entries.async_unload_platforms(entry, platforms) + if unload_ok: hass.data[DOMAIN].clear() - return True - # We were not able to unload the platforms, either because there - # were none or one of the forward_unloads failed. - return False + return unload_ok diff --git a/homeassistant/components/traccar/__init__.py b/homeassistant/components/traccar/__init__.py index cc598a9851b13..439bdc6f09e32 100644 --- a/homeassistant/components/traccar/__init__.py +++ b/homeassistant/components/traccar/__init__.py @@ -25,6 +25,9 @@ DOMAIN, ) +PLATFORMS = [DEVICE_TRACKER] + + TRACKER_UPDATE = f"{DOMAIN}_tracker_update" @@ -93,9 +96,7 @@ async def async_setup_entry(hass, entry): DOMAIN, "Traccar", entry.data[CONF_WEBHOOK_ID], handle_webhook ) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, DEVICE_TRACKER) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -103,8 +104,7 @@ async def async_unload_entry(hass, entry): """Unload a config entry.""" hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID]) hass.data[DOMAIN]["unsub_device_tracker"].pop(entry.entry_id)() - await hass.config_entries.async_forward_entry_unload(entry, DEVICE_TRACKER) - return True + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) async_remove_entry = config_entry_flow.webhook_async_remove_entry diff --git a/homeassistant/components/tradfri/__init__.py b/homeassistant/components/tradfri/__init__.py index 13d6d5713008a..bf8fa00bbc84e 100644 --- a/homeassistant/components/tradfri/__init__.py +++ b/homeassistant/components/tradfri/__init__.py @@ -1,5 +1,4 @@ """Support for IKEA Tradfri.""" -import asyncio from datetime import timedelta import logging @@ -149,10 +148,7 @@ async def on_hass_stop(event): sw_version=gateway_info.firmware_version, ) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) async def async_keep_alive(now): if hass.is_stopping: @@ -172,14 +168,7 @@ async def async_keep_alive(now): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: tradfri_data = hass.data[DOMAIN].pop(entry.entry_id) factory = tradfri_data[FACTORY] diff --git a/homeassistant/components/transmission/__init__.py b/homeassistant/components/transmission/__init__.py index cb4bcceeeeaac..b50f228ddad04 100644 --- a/homeassistant/components/transmission/__init__.py +++ b/homeassistant/components/transmission/__init__.py @@ -127,8 +127,9 @@ async def async_unload_entry(hass, config_entry): if client.unsub_timer: client.unsub_timer() - for platform in PLATFORMS: - await hass.config_entries.async_forward_entry_unload(config_entry, platform) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS + ) if not hass.data[DOMAIN]: hass.services.async_remove(DOMAIN, SERVICE_ADD_TORRENT) @@ -136,7 +137,7 @@ async def async_unload_entry(hass, config_entry): hass.services.async_remove(DOMAIN, SERVICE_START_TORRENT) hass.services.async_remove(DOMAIN, SERVICE_STOP_TORRENT) - return True + return unload_ok async def get_api(hass, entry): @@ -198,12 +199,7 @@ async def async_setup(self): self.add_options() self.set_scan_interval(self.config_entry.options[CONF_SCAN_INTERVAL]) - for platform in PLATFORMS: - self.hass.async_create_task( - self.hass.config_entries.async_forward_entry_setup( - self.config_entry, platform - ) - ) + self.hass.config_entries.async_setup_platforms(self.config_entry, PLATFORMS) def add_torrent(service): """Add new torrent to download.""" diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index 6dacc2e27497a..443042d8aff8b 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -1,5 +1,4 @@ """Support for Tuya Smart devices.""" -import asyncio from datetime import timedelta import logging @@ -250,17 +249,8 @@ async def async_force_update(call): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unloading the Tuya platforms.""" domain_data = hass.data[DOMAIN] - - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload( - entry, platform.split(".", 1)[0] - ) - for platform in domain_data[ENTRY_IS_SETUP] - ] - ) - ) + platforms = [platform.split(".", 1)[0] for platform in domain_data[ENTRY_IS_SETUP]] + unload_ok = await hass.config_entries.async_unload_platforms(entry, platforms) if unload_ok: domain_data["listener"]() domain_data[STOP_CANCEL]() diff --git a/homeassistant/components/twentemilieu/__init__.py b/homeassistant/components/twentemilieu/__init__.py index f53e4463146f4..94495cb83ceae 100644 --- a/homeassistant/components/twentemilieu/__init__.py +++ b/homeassistant/components/twentemilieu/__init__.py @@ -28,6 +28,8 @@ SERVICE_UPDATE = "update" SERVICE_SCHEMA = vol.Schema({vol.Optional(CONF_ID): cv.string}) +PLATFORMS = ["sensor"] + async def _update_twentemilieu(hass: HomeAssistant, unique_id: str | None) -> None: """Update Twente Milieu.""" @@ -71,9 +73,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: unique_id = entry.data[CONF_ID] hass.data.setdefault(DOMAIN, {})[unique_id] = twentemilieu - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "sensor") - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) async def _interval_update(now=None) -> None: """Update Twente Milieu data.""" @@ -86,8 +86,8 @@ async def _interval_update(now=None) -> None: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload Twente Milieu config entry.""" - await hass.config_entries.async_forward_entry_unload(entry, "sensor") + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) del hass.data[DOMAIN][entry.data[CONF_ID]] - return True + return unload_ok diff --git a/homeassistant/components/twinkly/__init__.py b/homeassistant/components/twinkly/__init__.py index 876d02bd698c8..24c714dc43779 100644 --- a/homeassistant/components/twinkly/__init__.py +++ b/homeassistant/components/twinkly/__init__.py @@ -8,11 +8,7 @@ from .const import CONF_ENTRY_HOST, CONF_ENTRY_ID, DOMAIN - -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the twinkly integration.""" - - return True +PLATFORMS = ["light"] async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): @@ -27,9 +23,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): host, async_get_clientsession(hass) ) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, "light") - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + return True diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index 0d8848e29205b..cea17e4e54c1b 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -362,12 +362,7 @@ async def async_setup(self): self.wireless_clients = wireless_clients.get_data(self.config_entry) self.update_wireless_clients() - for platform in PLATFORMS: - self.hass.async_create_task( - self.hass.config_entries.async_forward_entry_setup( - self.config_entry, platform - ) - ) + self.hass.config_entries.async_setup_platforms(self.config_entry, PLATFORMS) self.api.start_websocket() @@ -452,16 +447,10 @@ async def async_reset(self): """ self.api.stop_websocket() - unload_ok = all( - await asyncio.gather( - *[ - self.hass.config_entries.async_forward_entry_unload( - self.config_entry, platform - ) - for platform in PLATFORMS - ] - ) + unload_ok = await self.hass.config_entries.async_unload_platforms( + self.config_entry, PLATFORMS ) + if not unload_ok: return False diff --git a/homeassistant/components/upb/__init__.py b/homeassistant/components/upb/__init__.py index ba9faeb1797c8..7b3b30fdb29cb 100644 --- a/homeassistant/components/upb/__init__.py +++ b/homeassistant/components/upb/__init__.py @@ -1,5 +1,4 @@ """Support the UPB PIM.""" -import asyncio import upb_lib @@ -29,10 +28,7 @@ async def async_setup_entry(hass, config_entry): hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][config_entry.entry_id] = {"upb": upb} - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) def _element_changed(element, changeset): change = changeset.get("last_change") @@ -60,21 +56,13 @@ def _element_changed(element, changeset): async def async_unload_entry(hass, config_entry): """Unload the config_entry.""" - - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS ) - if unload_ok: upb = hass.data[DOMAIN][config_entry.entry_id]["upb"] upb.disconnect() hass.data[DOMAIN].pop(config_entry.entry_id) - return unload_ok diff --git a/homeassistant/components/upcloud/__init__.py b/homeassistant/components/upcloud/__init__.py index d3835f30bd907..21c9941667386 100644 --- a/homeassistant/components/upcloud/__init__.py +++ b/homeassistant/components/upcloud/__init__.py @@ -223,22 +223,20 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b upcloud_data.coordinators[config_entry.data[CONF_USERNAME]] = coordinator # Forward entry setup - for domain in CONFIG_ENTRY_DOMAINS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, domain) - ) + hass.config_entries.async_setup_platforms(config_entry, CONFIG_ENTRY_DOMAINS) return True async def async_unload_entry(hass, config_entry): """Unload the config entry.""" - for domain in CONFIG_ENTRY_DOMAINS: - await hass.config_entries.async_forward_entry_unload(config_entry, domain) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, CONFIG_ENTRY_DOMAINS + ) hass.data[DATA_UPCLOUD].coordinators.pop(config_entry.data[CONF_USERNAME]) - return True + return unload_ok class UpCloudServerEntity(CoordinatorEntity): diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index 3b4672a8fe50f..7edf7b99d36b0 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -31,6 +31,8 @@ NOTIFICATION_ID = "upnp_notification" NOTIFICATION_TITLE = "UPnP/IGD Setup" +PLATFORMS = ["sensor"] + CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( @@ -144,9 +146,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b # Create sensors. _LOGGER.debug("Enabling sensors") - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, "sensor") - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) # Start device updater. await device.async_start() @@ -166,4 +166,4 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> del hass.data[DOMAIN][DOMAIN_DEVICES][udn] _LOGGER.debug("Deleting sensors") - return await hass.config_entries.async_forward_entry_unload(config_entry, "sensor") + return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) From 41c6474249e43bd72f990f26dc01c09219555883 Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Tue, 27 Apr 2021 13:43:48 -0700 Subject: [PATCH 0595/1317] Add Screenlogic IntelliChem and SCG data (#49689) --- .../components/screenlogic/__init__.py | 8 +- .../components/screenlogic/binary_sensor.py | 68 ++++++++++- .../components/screenlogic/sensor.py | 114 +++++++++++++++--- .../components/screenlogic/switch.py | 7 +- 4 files changed, 175 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/screenlogic/__init__.py b/homeassistant/components/screenlogic/__init__.py index 30f544303cd2e..2225ef3d9dd13 100644 --- a/homeassistant/components/screenlogic/__init__.py +++ b/homeassistant/components/screenlogic/__init__.py @@ -132,10 +132,16 @@ async def _async_update_data(self): class ScreenlogicEntity(CoordinatorEntity): """Base class for all ScreenLogic entities.""" - def __init__(self, coordinator, data_key): + def __init__(self, coordinator, data_key, enabled=True): """Initialize of the entity.""" super().__init__(coordinator) self._data_key = data_key + self._enabled_default = enabled + + @property + def entity_registry_enabled_default(self): + """Entity enabled by default.""" + return self._enabled_default @property def mac(self): diff --git a/homeassistant/components/screenlogic/binary_sensor.py b/homeassistant/components/screenlogic/binary_sensor.py index bcff3e18bb208..649e692540843 100644 --- a/homeassistant/components/screenlogic/binary_sensor.py +++ b/homeassistant/components/screenlogic/binary_sensor.py @@ -1,7 +1,7 @@ """Support for a ScreenLogic Binary Sensor.""" import logging -from screenlogicpy.const import DATA as SL_DATA, DEVICE_TYPE, ON_OFF +from screenlogicpy.const import DATA as SL_DATA, DEVICE_TYPE, EQUIPMENT, ON_OFF from homeassistant.components.binary_sensor import ( DEVICE_CLASS_PROBLEM, @@ -24,6 +24,37 @@ async def async_setup_entry(hass, config_entry, async_add_entities): # Generic binary sensor entities.append(ScreenLogicBinarySensor(coordinator, "chem_alarm")) + if ( + coordinator.data[SL_DATA.KEY_CONFIG]["equipment_flags"] + & EQUIPMENT.FLAG_INTELLICHEM + ): + # IntelliChem alarm sensors + entities.extend( + [ + ScreenlogicChemistryAlarmBinarySensor(coordinator, chem_alarm) + for chem_alarm in coordinator.data[SL_DATA.KEY_CHEMISTRY][ + SL_DATA.KEY_ALERTS + ] + ] + ) + + # Intellichem notification sensors + entities.extend( + [ + ScreenlogicChemistryNotificationBinarySensor(coordinator, chem_notif) + for chem_notif in coordinator.data[SL_DATA.KEY_CHEMISTRY][ + SL_DATA.KEY_NOTIFICATIONS + ] + ] + ) + + if ( + coordinator.data[SL_DATA.KEY_CONFIG]["equipment_flags"] + & EQUIPMENT.FLAG_CHLORINATOR + ): + # SCG binary sensor + entities.append(ScreenlogicSCGBinarySensor(coordinator, "scg_status")) + async_add_entities(entities) @@ -38,8 +69,8 @@ def name(self): @property def device_class(self): """Return the device class.""" - device_class = self.sensor.get("device_type") - return SL_DEVICE_TYPE_TO_HA_DEVICE_CLASS.get(device_class) + device_type = self.sensor.get("device_type") + return SL_DEVICE_TYPE_TO_HA_DEVICE_CLASS.get(device_type) @property def is_on(self) -> bool: @@ -50,3 +81,34 @@ def is_on(self) -> bool: def sensor(self): """Shortcut to access the sensor data.""" return self.coordinator.data[SL_DATA.KEY_SENSORS][self._data_key] + + +class ScreenlogicChemistryAlarmBinarySensor(ScreenLogicBinarySensor): + """Representation of a ScreenLogic IntelliChem alarm binary sensor entity.""" + + @property + def sensor(self): + """Shortcut to access the sensor data.""" + return self.coordinator.data[SL_DATA.KEY_CHEMISTRY][SL_DATA.KEY_ALERTS][ + self._data_key + ] + + +class ScreenlogicChemistryNotificationBinarySensor(ScreenLogicBinarySensor): + """Representation of a ScreenLogic IntelliChem notification binary sensor entity.""" + + @property + def sensor(self): + """Shortcut to access the sensor data.""" + return self.coordinator.data[SL_DATA.KEY_CHEMISTRY][SL_DATA.KEY_NOTIFICATIONS][ + self._data_key + ] + + +class ScreenlogicSCGBinarySensor(ScreenLogicBinarySensor): + """Representation of a ScreenLogic SCG binary sensor entity.""" + + @property + def sensor(self): + """Shortcut to access the sensor data.""" + return self.coordinator.data[SL_DATA.KEY_SCG][self._data_key] diff --git a/homeassistant/components/screenlogic/sensor.py b/homeassistant/components/screenlogic/sensor.py index acb30b08f97a2..2419ee46eed62 100644 --- a/homeassistant/components/screenlogic/sensor.py +++ b/homeassistant/components/screenlogic/sensor.py @@ -1,7 +1,12 @@ """Support for a ScreenLogic Sensor.""" import logging -from screenlogicpy.const import DATA as SL_DATA, DEVICE_TYPE +from screenlogicpy.const import ( + CHEM_DOSING_STATE, + DATA as SL_DATA, + DEVICE_TYPE, + EQUIPMENT, +) from homeassistant.components.sensor import ( DEVICE_CLASS_POWER, @@ -14,7 +19,32 @@ _LOGGER = logging.getLogger(__name__) -PUMP_SENSORS = ("currentWatts", "currentRPM", "currentGPM") +SUPPORTED_CHEM_SENSORS = ( + "calcium_harness", + "current_orp", + "current_ph", + "cya", + "orp_dosing_state", + "orp_last_dose_time", + "orp_last_dose_volume", + "orp_setpoint", + "ph_dosing_state", + "ph_last_dose_time", + "ph_last_dose_volume", + "ph_probe_water_temp", + "ph_setpoint", + "salt_tds_ppm", + "total_alkalinity", +) + +SUPPORTED_SCG_SENSORS = ( + "scg_level1", + "scg_level2", + "scg_salt_ppm", + "scg_super_chlor_timer", +) + +SUPPORTED_PUMP_SENSORS = ("currentWatts", "currentRPM", "currentGPM") SL_DEVICE_TYPE_TO_HA_DEVICE_CLASS = { DEVICE_TYPE.TEMPERATURE: DEVICE_CLASS_TEMPERATURE, @@ -26,22 +56,45 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up entry.""" entities = [] coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"] + equipment_flags = coordinator.data[SL_DATA.KEY_CONFIG]["equipment_flags"] # Generic sensors - for sensor in coordinator.data[SL_DATA.KEY_SENSORS]: - if sensor == "chem_alarm": + for sensor_name, sensor_data in coordinator.data[SL_DATA.KEY_SENSORS].items(): + if sensor_name in ("chem_alarm", "salt_ppm"): continue - if coordinator.data[SL_DATA.KEY_SENSORS][sensor]["value"] != 0: - entities.append(ScreenLogicSensor(coordinator, sensor)) + if sensor_data["value"] != 0: + entities.append(ScreenLogicSensor(coordinator, sensor_name)) # Pump sensors - for pump in coordinator.data[SL_DATA.KEY_PUMPS]: - if ( - coordinator.data[SL_DATA.KEY_PUMPS][pump]["data"] != 0 - and "currentWatts" in coordinator.data[SL_DATA.KEY_PUMPS][pump] - ): - for pump_key in PUMP_SENSORS: - entities.append(ScreenLogicPumpSensor(coordinator, pump, pump_key)) + for pump_num, pump_data in coordinator.data[SL_DATA.KEY_PUMPS].items(): + if pump_data["data"] != 0 and "currentWatts" in pump_data: + entities.extend( + ScreenLogicPumpSensor(coordinator, pump_num, pump_key) + for pump_key in pump_data + if pump_key in SUPPORTED_PUMP_SENSORS + ) + + # IntelliChem sensors + if equipment_flags & EQUIPMENT.FLAG_INTELLICHEM: + for chem_sensor_name in coordinator.data[SL_DATA.KEY_CHEMISTRY]: + enabled = True + if equipment_flags & EQUIPMENT.FLAG_CHLORINATOR: + if chem_sensor_name in ("salt_tds_ppm"): + enabled = False + if chem_sensor_name in SUPPORTED_CHEM_SENSORS: + entities.append( + ScreenLogicChemistrySensor(coordinator, chem_sensor_name, enabled) + ) + + # SCG sensors + if equipment_flags & EQUIPMENT.FLAG_CHLORINATOR: + entities.extend( + [ + ScreenLogicSCGSensor(coordinator, scg_sensor) + for scg_sensor in coordinator.data[SL_DATA.KEY_SCG] + if scg_sensor in SUPPORTED_SCG_SENSORS + ] + ) async_add_entities(entities) @@ -80,9 +133,9 @@ def sensor(self): class ScreenLogicPumpSensor(ScreenLogicSensor): """Representation of a ScreenLogic pump sensor entity.""" - def __init__(self, coordinator, pump, key): + def __init__(self, coordinator, pump, key, enabled=True): """Initialize of the pump sensor.""" - super().__init__(coordinator, f"{key}_{pump}") + super().__init__(coordinator, f"{key}_{pump}", enabled) self._pump_id = pump self._key = key @@ -90,3 +143,34 @@ def __init__(self, coordinator, pump, key): def sensor(self): """Shortcut to access the pump sensor data.""" return self.coordinator.data[SL_DATA.KEY_PUMPS][self._pump_id][self._key] + + +class ScreenLogicChemistrySensor(ScreenLogicSensor): + """Representation of a ScreenLogic IntelliChem sensor entity.""" + + def __init__(self, coordinator, key, enabled=True): + """Initialize of the pump sensor.""" + super().__init__(coordinator, f"chem_{key}", enabled) + self._key = key + + @property + def state(self): + """State of the sensor.""" + value = self.sensor["value"] + if "dosing_state" in self._key: + return CHEM_DOSING_STATE.NAME_FOR_NUM[value] + return value + + @property + def sensor(self): + """Shortcut to access the pump sensor data.""" + return self.coordinator.data[SL_DATA.KEY_CHEMISTRY][self._key] + + +class ScreenLogicSCGSensor(ScreenLogicSensor): + """Representation of ScreenLogic SCG sensor entity.""" + + @property + def sensor(self): + """Shortcut to access the pump sensor data.""" + return self.coordinator.data[SL_DATA.KEY_SCG][self._data_key] diff --git a/homeassistant/components/screenlogic/switch.py b/homeassistant/components/screenlogic/switch.py index e8824b8bd92cd..ff73afebb575d 100644 --- a/homeassistant/components/screenlogic/switch.py +++ b/homeassistant/components/screenlogic/switch.py @@ -1,7 +1,7 @@ """Support for a ScreenLogic 'circuit' switch.""" import logging -from screenlogicpy.const import DATA as SL_DATA, ON_OFF +from screenlogicpy.const import DATA as SL_DATA, GENERIC_CIRCUIT_NAMES, ON_OFF from homeassistant.components.switch import SwitchEntity @@ -16,8 +16,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entities = [] coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"] - for circuit in coordinator.data[SL_DATA.KEY_CIRCUITS]: - entities.append(ScreenLogicSwitch(coordinator, circuit)) + for circuit_num, circuit in coordinator.data[SL_DATA.KEY_CIRCUITS].items(): + enabled = circuit["name"] not in GENERIC_CIRCUIT_NAMES + entities.append(ScreenLogicSwitch(coordinator, circuit_num, enabled)) async_add_entities(entities) From a57761103c695b7289f3c917deb8e218794ba083 Mon Sep 17 00:00:00 2001 From: Tom Toor Date: Tue, 27 Apr 2021 13:44:59 -0700 Subject: [PATCH 0596/1317] Mutesync integration (#49679) Co-authored-by: Paulus Schoutsen Co-authored-by: Franck Nijhof Co-authored-by: Franck Nijhof --- .coveragerc | 2 + CODEOWNERS | 1 + homeassistant/components/mutesync/__init__.py | 54 ++++++++++++ .../components/mutesync/binary_sensor.py | 53 ++++++++++++ .../components/mutesync/config_flow.py | 82 +++++++++++++++++++ homeassistant/components/mutesync/const.py | 3 + .../components/mutesync/manifest.json | 11 +++ .../components/mutesync/strings.json | 16 ++++ .../components/mutesync/translations/en.json | 16 ++++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/mutesync/__init__.py | 1 + tests/components/mutesync/test_config_flow.py | 72 ++++++++++++++++ 14 files changed, 318 insertions(+) create mode 100644 homeassistant/components/mutesync/__init__.py create mode 100644 homeassistant/components/mutesync/binary_sensor.py create mode 100644 homeassistant/components/mutesync/config_flow.py create mode 100644 homeassistant/components/mutesync/const.py create mode 100644 homeassistant/components/mutesync/manifest.json create mode 100644 homeassistant/components/mutesync/strings.json create mode 100644 homeassistant/components/mutesync/translations/en.json create mode 100644 tests/components/mutesync/__init__.py create mode 100644 tests/components/mutesync/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index a9342397123a1..05a752764c37c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -631,6 +631,8 @@ omit = homeassistant/components/msteams/notify.py homeassistant/components/mullvad/__init__.py homeassistant/components/mullvad/binary_sensor.py + homeassistant/components/mutesync/__init__.py + homeassistant/components/mutesync/binary_sensor.py homeassistant/components/nest/const.py homeassistant/components/mvglive/sensor.py homeassistant/components/mychevy/* diff --git a/CODEOWNERS b/CODEOWNERS index 4bd020ffb1225..f23dda7aaaff2 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -301,6 +301,7 @@ homeassistant/components/mpd/* @fabaff homeassistant/components/mqtt/* @emontnemery homeassistant/components/msteams/* @peroyvind homeassistant/components/mullvad/* @meichthys +homeassistant/components/mutesync/* @currentoor homeassistant/components/my/* @home-assistant/core homeassistant/components/myq/* @bdraco homeassistant/components/mysensors/* @MartinHjelmare @functionpointer diff --git a/homeassistant/components/mutesync/__init__.py b/homeassistant/components/mutesync/__init__.py new file mode 100644 index 0000000000000..9ed00f84febfb --- /dev/null +++ b/homeassistant/components/mutesync/__init__.py @@ -0,0 +1,54 @@ +"""The mütesync integration.""" +from __future__ import annotations + +from datetime import timedelta +import logging + +import async_timeout +import mutesync + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers import update_coordinator + +from .const import DOMAIN + +PLATFORMS = ["binary_sensor"] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up mütesync from a config entry.""" + client = mutesync.PyMutesync( + entry.data["token"], + entry.data["host"], + hass.helpers.aiohttp_client.async_get_clientsession(), + ) + + async def update_data(): + """Update the data.""" + async with async_timeout.timeout(5): + return await client.get_state() + + coordinator = hass.data.setdefault(DOMAIN, {})[ + entry.entry_id + ] = update_coordinator.DataUpdateCoordinator( + hass, + logging.getLogger(__name__), + name=DOMAIN, + update_interval=timedelta(seconds=10), + update_method=update_data, + ) + await coordinator.async_config_entry_first_refresh() + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/mutesync/binary_sensor.py b/homeassistant/components/mutesync/binary_sensor.py new file mode 100644 index 0000000000000..a2f87bf9017b0 --- /dev/null +++ b/homeassistant/components/mutesync/binary_sensor.py @@ -0,0 +1,53 @@ +"""mütesync binary sensor entities.""" +from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.helpers import update_coordinator + +from .const import DOMAIN + +SENSORS = { + "in_meeting": "In Meeting", + "muted": "Muted", +} + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the mütesync button.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id] + async_add_entities( + [MuteStatus(coordinator, sensor_type) for sensor_type in SENSORS], True + ) + + +class MuteStatus(update_coordinator.CoordinatorEntity, BinarySensorEntity): + """Mütesync binary sensors.""" + + def __init__(self, coordinator, sensor_type): + """Initialize our sensor.""" + super().__init__(coordinator) + self._sensor_type = sensor_type + + @property + def name(self): + """Return the name of the sensor.""" + return SENSORS[self._sensor_type] + + @property + def unique_id(self): + """Return the unique ID of the sensor.""" + return f"{self.coordinator.data['user-id']}-{self._sensor_type}" + + @property + def is_on(self): + """Return the state of the sensor.""" + return self.coordinator.data[self._sensor_type] + + @property + def device_info(self): + """Return the device info of the sensor.""" + return { + "identifiers": {(DOMAIN, self.coordinator.data["user-id"])}, + "name": "mutesync", + "manufacturer": "mütesync", + "model": "mutesync app", + "entry_type": "service", + } diff --git a/homeassistant/components/mutesync/config_flow.py b/homeassistant/components/mutesync/config_flow.py new file mode 100644 index 0000000000000..94d9b53a9d614 --- /dev/null +++ b/homeassistant/components/mutesync/config_flow.py @@ -0,0 +1,82 @@ +"""Config flow for mütesync integration.""" +from __future__ import annotations + +import asyncio +from typing import Any + +import aiohttp +import async_timeout +import mutesync +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultDict +from homeassistant.exceptions import HomeAssistantError + +from .const import DOMAIN + +STEP_USER_DATA_SCHEMA = vol.Schema({"host": str}) + + +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: + """Validate the user input allows us to connect. + + Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. + """ + session = hass.helpers.aiohttp_client.async_get_clientsession() + try: + async with async_timeout.timeout(10): + token = await mutesync.authenticate(session, data["host"]) + except aiohttp.ClientResponseError as error: + if error.status == 403: + raise InvalidAuth from error + raise CannotConnect from error + except (aiohttp.ClientError, asyncio.TimeoutError) as error: + raise CannotConnect from error + + return token + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for mütesync.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResultDict: + """Handle the initial step.""" + if user_input is None: + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA + ) + + errors = {} + + try: + token = await validate_input(self.hass, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + errors["base"] = "unknown" + else: + return self.async_create_entry( + title=user_input["host"], + data={"token": token, "host": user_input["host"]}, + ) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) + + +class CannotConnect(HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/mutesync/const.py b/homeassistant/components/mutesync/const.py new file mode 100644 index 0000000000000..fcf05584f4285 --- /dev/null +++ b/homeassistant/components/mutesync/const.py @@ -0,0 +1,3 @@ +"""Constants for the mütesync integration.""" + +DOMAIN = "mutesync" diff --git a/homeassistant/components/mutesync/manifest.json b/homeassistant/components/mutesync/manifest.json new file mode 100644 index 0000000000000..74e6d89d9f854 --- /dev/null +++ b/homeassistant/components/mutesync/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "mutesync", + "name": "mutesync", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/mutesync", + "requirements": ["mutesync==0.0.1"], + "iot_class": "local_polling", + "codeowners": [ + "@currentoor" + ] +} diff --git a/homeassistant/components/mutesync/strings.json b/homeassistant/components/mutesync/strings.json new file mode 100644 index 0000000000000..9b18620acf88f --- /dev/null +++ b/homeassistant/components/mutesync/strings.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "Enable authentication in mütesync Preferences > Authentication", + "unknown": "[%key:common::config_flow::error::unknown%]" + } + } +} diff --git a/homeassistant/components/mutesync/translations/en.json b/homeassistant/components/mutesync/translations/en.json new file mode 100644 index 0000000000000..0152f03bc2ad5 --- /dev/null +++ b/homeassistant/components/mutesync/translations/en.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Enable authentication in m\u00fctesync Preferences > Authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index bbf27893dc3bd..3b408860d59a2 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -155,6 +155,7 @@ "motioneye", "mqtt", "mullvad", + "mutesync", "myq", "mysensors", "neato", diff --git a/requirements_all.txt b/requirements_all.txt index d2fd9fb61551f..dac6f3c3549d2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -962,6 +962,9 @@ mullvad-api==1.0.0 # homeassistant.components.tts mutagen==1.45.1 +# homeassistant.components.mutesync +mutesync==0.0.1 + # homeassistant.components.mychevy mychevy==2.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 499b9f1b36467..bf695a0eeb39a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -522,6 +522,9 @@ mullvad-api==1.0.0 # homeassistant.components.tts mutagen==1.45.1 +# homeassistant.components.mutesync +mutesync==0.0.1 + # homeassistant.components.keenetic_ndms2 ndms2_client==0.1.1 diff --git a/tests/components/mutesync/__init__.py b/tests/components/mutesync/__init__.py new file mode 100644 index 0000000000000..5213265a7b0f6 --- /dev/null +++ b/tests/components/mutesync/__init__.py @@ -0,0 +1 @@ +"""Tests for the mütesync integration.""" diff --git a/tests/components/mutesync/test_config_flow.py b/tests/components/mutesync/test_config_flow.py new file mode 100644 index 0000000000000..39a8feb24724d --- /dev/null +++ b/tests/components/mutesync/test_config_flow.py @@ -0,0 +1,72 @@ +"""Test the mütesync config flow.""" +import asyncio +from unittest.mock import patch + +import aiohttp +import pytest + +from homeassistant import config_entries, setup +from homeassistant.components.mutesync.const import DOMAIN +from homeassistant.core import HomeAssistant + + +async def test_form(hass: HomeAssistant) -> None: + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] is None + + with patch("mutesync.authenticate", return_value="bla",), patch( + "homeassistant.components.mutesync.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "1.1.1.1" + assert result2["data"] == { + "host": "1.1.1.1", + "token": "bla", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + "side_effect,error", + [ + (Exception, "unknown"), + (aiohttp.ClientResponseError(None, None, status=403), "invalid_auth"), + (aiohttp.ClientResponseError(None, None, status=500), "cannot_connect"), + (asyncio.TimeoutError, "cannot_connect"), + ], +) +async def test_form_error( + side_effect: Exception, error: str, hass: HomeAssistant +) -> None: + """Test we handle error situations.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "mutesync.authenticate", + side_effect=side_effect, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": error} From f9a2c1cfd54033b0abc9ad6be02ba26f58d838c2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 27 Apr 2021 10:51:11 -1000 Subject: [PATCH 0597/1317] Reduce config entry setup/unload boilerplate V-Z (#49789) --- homeassistant/components/velbus/__init__.py | 10 ++-------- homeassistant/components/verisure/__init__.py | 16 ++-------------- homeassistant/components/vesync/__init__.py | 10 +--------- homeassistant/components/vilfo/__init__.py | 15 ++------------- homeassistant/components/vizio/__init__.py | 16 +++------------- homeassistant/components/volumio/__init__.py | 15 ++------------- .../components/waze_travel_time/__init__.py | 16 ++-------------- homeassistant/components/wiffi/__init__.py | 15 +++------------ homeassistant/components/wilight/__init__.py | 15 +++------------ homeassistant/components/wled/__init__.py | 16 ++-------------- homeassistant/components/wolflink/__init__.py | 13 ++++--------- homeassistant/components/xbox/__init__.py | 15 ++------------- .../components/xiaomi_aqara/__init__.py | 15 ++------------- homeassistant/components/yeelight/__init__.py | 16 ++-------------- homeassistant/components/zerproc/__init__.py | 15 ++------------- 15 files changed, 34 insertions(+), 184 deletions(-) diff --git a/homeassistant/components/velbus/__init__.py b/homeassistant/components/velbus/__init__.py index 6d5e741a3ce34..47f51d8b26ee4 100644 --- a/homeassistant/components/velbus/__init__.py +++ b/homeassistant/components/velbus/__init__.py @@ -1,5 +1,4 @@ """Support for Velbus devices.""" -import asyncio import logging import velbus @@ -111,17 +110,12 @@ def set_memo_text(service): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Remove the velbus connection.""" - await asyncio.wait( - [ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) hass.data[DOMAIN][entry.entry_id]["cntrl"].stop() hass.data[DOMAIN].pop(entry.entry_id) if not hass.data[DOMAIN]: hass.data.pop(DOMAIN) - return True + return unload_ok class VelbusEntity(Entity): diff --git a/homeassistant/components/verisure/__init__.py b/homeassistant/components/verisure/__init__.py index 622f2aecc1441..f61208309fc78 100644 --- a/homeassistant/components/verisure/__init__.py +++ b/homeassistant/components/verisure/__init__.py @@ -1,7 +1,6 @@ """Support for Verisure devices.""" from __future__ import annotations -import asyncio from contextlib import suppress import os from typing import Any @@ -137,25 +136,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = coordinator # Set up all platforms for this device/entry. - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload Verisure config entry.""" - unload_ok = all( - await asyncio.gather( - *( - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ) - ) - ) - + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if not unload_ok: return False diff --git a/homeassistant/components/vesync/__init__.py b/homeassistant/components/vesync/__init__.py index 686a71427c33e..6ae978eb4b8b4 100644 --- a/homeassistant/components/vesync/__init__.py +++ b/homeassistant/components/vesync/__init__.py @@ -1,5 +1,4 @@ """VeSync integration.""" -import asyncio import logging from pyvesync import VeSync @@ -153,14 +152,7 @@ async def async_new_device_discovery(service): async def async_unload_entry(hass, entry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/vilfo/__init__.py b/homeassistant/components/vilfo/__init__.py index 16488269da68c..59387fa81c8ba 100644 --- a/homeassistant/components/vilfo/__init__.py +++ b/homeassistant/components/vilfo/__init__.py @@ -1,5 +1,4 @@ """The Vilfo Router integration.""" -import asyncio from datetime import timedelta import logging @@ -36,24 +35,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = vilfo_router - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/vizio/__init__.py b/homeassistant/components/vizio/__init__.py index b8afba7d69e7d..bec6b80302374 100644 --- a/homeassistant/components/vizio/__init__.py +++ b/homeassistant/components/vizio/__init__.py @@ -1,7 +1,6 @@ """The vizio component.""" from __future__ import annotations -import asyncio from datetime import timedelta import logging from typing import Any @@ -69,25 +68,16 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b await coordinator.async_refresh() hass.data[DOMAIN][CONF_APPS] = coordinator - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS ) - # Exclude this config entry because its not unloaded yet if not any( entry.state == ENTRY_STATE_LOADED diff --git a/homeassistant/components/volumio/__init__.py b/homeassistant/components/volumio/__init__.py index a9c6fb746aaec..f9b9432d75504 100644 --- a/homeassistant/components/volumio/__init__.py +++ b/homeassistant/components/volumio/__init__.py @@ -1,5 +1,4 @@ """The Volumio integration.""" -import asyncio from pyvolumio import CannotConnectError, Volumio @@ -30,24 +29,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): DATA_INFO: info, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/waze_travel_time/__init__.py b/homeassistant/components/waze_travel_time/__init__.py index 20a0c01c64217..5800cfe94ab4c 100644 --- a/homeassistant/components/waze_travel_time/__init__.py +++ b/homeassistant/components/waze_travel_time/__init__.py @@ -1,5 +1,4 @@ """The waze_travel_time component.""" -import asyncio from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -9,21 +8,10 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Load the saved entities.""" - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) - + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload a config entry.""" - return all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - ) - ) + return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) diff --git a/homeassistant/components/wiffi/__init__.py b/homeassistant/components/wiffi/__init__.py index 36f6e641508fb..f36e4b0df3224 100644 --- a/homeassistant/components/wiffi/__init__.py +++ b/homeassistant/components/wiffi/__init__.py @@ -1,5 +1,4 @@ """Component for wiffi support.""" -import asyncio from datetime import timedelta import errno import logging @@ -54,10 +53,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): _LOGGER.error("Port %s already in use", config_entry.data[CONF_PORT]) raise ConfigEntryNotReady from exc - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) return True @@ -72,13 +68,8 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry): api: WiffiIntegrationApi = hass.data[DOMAIN][config_entry.entry_id] await api.server.close_server() - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS ) if unload_ok: api = hass.data[DOMAIN].pop(config_entry.entry_id) diff --git a/homeassistant/components/wilight/__init__.py b/homeassistant/components/wilight/__init__.py index 88589f1ed706f..0ac2713994b40 100644 --- a/homeassistant/components/wilight/__init__.py +++ b/homeassistant/components/wilight/__init__.py @@ -1,5 +1,4 @@ """The WiLight integration.""" -import asyncio from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -26,10 +25,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data[DOMAIN][entry.entry_id] = parent # Set up all platforms for this device/entry. - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -38,19 +34,14 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload WiLight config entry.""" # Unload entities for this entry/device. - await asyncio.gather( - *( - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) # Cleanup parent = hass.data[DOMAIN][entry.entry_id] await parent.async_reset() del hass.data[DOMAIN][entry.entry_id] - return True + return unload_ok class WiLightDevice(Entity): diff --git a/homeassistant/components/wled/__init__.py b/homeassistant/components/wled/__init__.py index a54635f26b842..8c8c6d887e79d 100644 --- a/homeassistant/components/wled/__init__.py +++ b/homeassistant/components/wled/__init__.py @@ -1,7 +1,6 @@ """Support for WLED.""" from __future__ import annotations -import asyncio from datetime import timedelta import logging from typing import Any @@ -52,10 +51,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) # Set up all platforms for this device/entry. - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -64,15 +60,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload WLED config entry.""" # Unload entities for this entry/device. - unload_ok = all( - await asyncio.gather( - *( - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ) - ) - ) - + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: del hass.data[DOMAIN][entry.entry_id] diff --git a/homeassistant/components/wolflink/__init__.py b/homeassistant/components/wolflink/__init__.py index 39cd7127402b9..06f3408c6a573 100644 --- a/homeassistant/components/wolflink/__init__.py +++ b/homeassistant/components/wolflink/__init__.py @@ -23,11 +23,7 @@ _LOGGER = logging.getLogger(__name__) - -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Wolf SmartSet Service component.""" - hass.data[DOMAIN] = {} - return True +PLATFORMS = ["sensor"] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): @@ -78,21 +74,20 @@ async def async_update_data(): await coordinator.async_refresh() + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = {} hass.data[DOMAIN][entry.entry_id][PARAMETERS] = parameters hass.data[DOMAIN][entry.entry_id][COORDINATOR] = coordinator hass.data[DOMAIN][entry.entry_id][DEVICE_ID] = device_id - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "sensor") - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = await hass.config_entries.async_forward_entry_unload(entry, "sensor") + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/xbox/__init__.py b/homeassistant/components/xbox/__init__.py index 2484c99b63821..db278d0da43fd 100644 --- a/homeassistant/components/xbox/__init__.py +++ b/homeassistant/components/xbox/__init__.py @@ -1,7 +1,6 @@ """The xbox integration.""" from __future__ import annotations -import asyncio from contextlib import suppress from dataclasses import dataclass from datetime import timedelta @@ -102,24 +101,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): "coordinator": coordinator, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: # Unsub from coordinator updates hass.data[DOMAIN][entry.entry_id]["sensor_unsub"]() diff --git a/homeassistant/components/xiaomi_aqara/__init__.py b/homeassistant/components/xiaomi_aqara/__init__.py index ba7f717f42196..d78398fb46fcf 100644 --- a/homeassistant/components/xiaomi_aqara/__init__.py +++ b/homeassistant/components/xiaomi_aqara/__init__.py @@ -1,5 +1,4 @@ """Support for Xiaomi Gateways.""" -import asyncio from datetime import timedelta import logging @@ -188,10 +187,7 @@ def stop_xiaomi(event): else: platforms = GATEWAY_PLATFORMS_NO_KEY - for platform in platforms: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, platforms) return True @@ -205,14 +201,7 @@ async def async_unload_entry( else: platforms = GATEWAY_PLATFORMS_NO_KEY - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in platforms - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, platforms) if unload_ok: hass.data[DOMAIN][GATEWAYS_KEY].pop(entry.entry_id) diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index 944e6e6bec250..a51323b516e9f 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -198,11 +198,7 @@ async def _initialize(host: str, capabilities: dict | None = None) -> None: await device.async_setup() async def _load_platforms(): - - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) # Move options from data for imported entries # Initialize options with default values for other entries @@ -244,15 +240,7 @@ async def _load_platforms(): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) - + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: data = hass.data[DOMAIN][DATA_CONFIG_ENTRIES].pop(entry.entry_id) remove_init_dispatcher = data.get(DATA_REMOVE_INIT_DISPATCHER) diff --git a/homeassistant/components/zerproc/__init__.py b/homeassistant/components/zerproc/__init__.py index 12953afeb2dbf..8d42c81162f0c 100644 --- a/homeassistant/components/zerproc/__init__.py +++ b/homeassistant/components/zerproc/__init__.py @@ -1,5 +1,4 @@ """Zerproc lights integration.""" -import asyncio from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.core import HomeAssistant @@ -25,10 +24,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): if DATA_ADDRESSES not in hass.data[DOMAIN]: hass.data[DOMAIN][DATA_ADDRESSES] = set() - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -42,11 +38,4 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data.pop(DOMAIN, None) - return all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) From c193f8fd183a453f0d6f008c446b9e3d6b389e47 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Tue, 27 Apr 2021 16:55:26 -0400 Subject: [PATCH 0598/1317] Clean up intent_script (#49770) --- homeassistant/components/intent_script/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/intent_script/__init__.py b/homeassistant/components/intent_script/__init__.py index 892ea83982cd4..ffa622307fd95 100644 --- a/homeassistant/components/intent_script/__init__.py +++ b/homeassistant/components/intent_script/__init__.py @@ -3,6 +3,7 @@ import voluptuous as vol +from homeassistant.const import CONF_TYPE from homeassistant.helpers import config_validation as cv, intent, script, template DOMAIN = "intent_script" @@ -12,7 +13,6 @@ CONF_ACTION = "action" CONF_CARD = "card" -CONF_TYPE = "type" CONF_TITLE = "title" CONF_CONTENT = "content" CONF_TEXT = "text" From 9db6d0cee4d481319738fc2fd12d7d3c3510864d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 28 Apr 2021 00:08:14 +0300 Subject: [PATCH 0599/1317] Huawei LTE unload cleanups (#49788) --- homeassistant/components/huawei_lte/__init__.py | 13 +------------ .../components/huawei_lte/device_tracker.py | 2 +- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index f0e8b0150e37a..c256fc2e7f2f5 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -39,7 +39,6 @@ CONF_RECIPIENT, CONF_URL, CONF_USERNAME, - EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant, ServiceCall from homeassistant.exceptions import ConfigEntryNotReady @@ -143,7 +142,6 @@ class Router: factory=lambda: defaultdict(set, ((x, {"initial_scan"}) for x in ALL_KEYS)), ) inflight_gets: set[str] = attr.ib(init=False, factory=set) - unload_handlers: list[CALLBACK_TYPE] = attr.ib(init=False, factory=list) client: Client suspended = attr.ib(init=False, default=False) notify_last_attempt: float = attr.ib(init=False, default=-1) @@ -292,10 +290,6 @@ def cleanup(self, *_: Any) -> None: self.subscriptions.clear() - for handler in self.unload_handlers: - handler() - self.unload_handlers.clear() - self.logout() @@ -444,13 +438,8 @@ def _update_router(*_: Any) -> None: router.update() # Set up periodic update - router.unload_handlers.append( - async_track_time_interval(hass, _update_router, SCAN_INTERVAL) - ) - - # Clean up at end config_entry.async_on_unload( - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, router.cleanup) + async_track_time_interval(hass, _update_router, SCAN_INTERVAL) ) return True diff --git a/homeassistant/components/huawei_lte/device_tracker.py b/homeassistant/components/huawei_lte/device_tracker.py index 25b1094c638b2..3a1dcfe83af19 100644 --- a/homeassistant/components/huawei_lte/device_tracker.py +++ b/homeassistant/components/huawei_lte/device_tracker.py @@ -102,7 +102,7 @@ async def _async_maybe_add_new_entities(url: str) -> None: disconnect_dispatcher = async_dispatcher_connect( hass, UPDATE_SIGNAL, _async_maybe_add_new_entities ) - router.unload_handlers.append(disconnect_dispatcher) + config_entry.async_on_unload(disconnect_dispatcher) # Add new entities from initial scan async_add_new_entities(hass, router.url, async_add_entities, tracked) From 513685bbeacca2c758d3ca33b337da3b7e72dd1d Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 27 Apr 2021 23:34:53 +0200 Subject: [PATCH 0600/1317] Add dynamic update interval to Airly integration (#47505) * Add dynamic update interval * Update tests * Improve tests * Improve comments * Add MAX_UPDATE_INTERVAL * Suggested change Co-authored-by: Martin Hjelmare * Use async_fire_time_changed to test update interval * Fix test_update_interval * Patch dt_util in airly integration * Cleaning * Use total_seconds instead of seconds * Fix update interval test * Refactor update interval test * Don't create new context manager Co-authored-by: Martin Hjelmare --- homeassistant/components/airly/__init__.py | 52 +++++++++----- homeassistant/components/airly/const.py | 3 +- tests/components/airly/test_init.py | 82 +++++++++++++++++----- 3 files changed, 103 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/airly/__init__.py b/homeassistant/components/airly/__init__.py index b0aa617995267..f855b30db48a8 100644 --- a/homeassistant/components/airly/__init__.py +++ b/homeassistant/components/airly/__init__.py @@ -11,6 +11,7 @@ from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.util import dt as dt_util from .const import ( ATTR_API_ADVICE, @@ -19,7 +20,8 @@ ATTR_API_CAQI_LEVEL, CONF_USE_NEAREST, DOMAIN, - MAX_REQUESTS_PER_DAY, + MAX_UPDATE_INTERVAL, + MIN_UPDATE_INTERVAL, NO_AIRLY_SENSORS, ) @@ -28,15 +30,30 @@ _LOGGER = logging.getLogger(__name__) -def set_update_interval(hass, instances): - """Set update_interval to another configured Airly instances.""" - # We check how many Airly configured instances are and calculate interval to not - # exceed allowed numbers of requests. - interval = timedelta(minutes=ceil(24 * 60 / MAX_REQUESTS_PER_DAY) * instances) +def set_update_interval(instances, requests_remaining): + """ + Return data update interval. - if hass.data.get(DOMAIN): - for instance in hass.data[DOMAIN].values(): - instance.update_interval = interval + The number of requests is reset at midnight UTC so we calculate the update + interval based on number of minutes until midnight, the number of Airly instances + and the number of remaining requests. + """ + now = dt_util.utcnow() + midnight = dt_util.find_next_time_expression_time( + now, seconds=[0], minutes=[0], hours=[0] + ) + minutes_to_midnight = (midnight - now).total_seconds() / 60 + interval = timedelta( + minutes=min( + max( + ceil(minutes_to_midnight / requests_remaining * instances), + MIN_UPDATE_INTERVAL, + ), + MAX_UPDATE_INTERVAL, + ) + ) + + _LOGGER.debug("Data will be update every %s", interval) return interval @@ -55,10 +72,8 @@ async def async_setup_entry(hass, config_entry): ) websession = async_get_clientsession(hass) - # Change update_interval for other Airly instances - update_interval = set_update_interval( - hass, len(hass.config_entries.async_entries(DOMAIN)) - ) + + update_interval = timedelta(minutes=MIN_UPDATE_INTERVAL) coordinator = AirlyDataUpdateCoordinator( hass, websession, api_key, latitude, longitude, update_interval, use_nearest @@ -82,9 +97,6 @@ async def async_unload_entry(hass, config_entry): if unload_ok: hass.data[DOMAIN].pop(config_entry.entry_id) - # Change update_interval for other Airly instances - set_update_interval(hass, len(hass.data[DOMAIN])) - return unload_ok @@ -132,6 +144,14 @@ async def _async_update_data(self): self.airly.requests_per_day, ) + # Airly API sometimes returns None for requests remaining so we update + # update_interval only if we have valid value. + if self.airly.requests_remaining: + self.update_interval = set_update_interval( + len(self.hass.config_entries.async_entries(DOMAIN)), + self.airly.requests_remaining, + ) + values = measurements.current["values"] index = measurements.current["indexes"][0] standards = measurements.current["standards"] diff --git a/homeassistant/components/airly/const.py b/homeassistant/components/airly/const.py index b8d2270c3c426..df4818ef94983 100644 --- a/homeassistant/components/airly/const.py +++ b/homeassistant/components/airly/const.py @@ -24,5 +24,6 @@ DOMAIN = "airly" LABEL_ADVICE = "advice" MANUFACTURER = "Airly sp. z o.o." -MAX_REQUESTS_PER_DAY = 100 +MAX_UPDATE_INTERVAL = 90 +MIN_UPDATE_INTERVAL = 5 NO_AIRLY_SENSORS = "There are no Airly sensors in this area yet." diff --git a/tests/components/airly/test_init.py b/tests/components/airly/test_init.py index 2898bd5c6f6a5..c2785d6f3e702 100644 --- a/tests/components/airly/test_init.py +++ b/tests/components/airly/test_init.py @@ -1,6 +1,7 @@ """Test init of Airly integration.""" -from datetime import timedelta +from unittest.mock import patch +from homeassistant.components.airly import set_update_interval from homeassistant.components.airly.const import DOMAIN from homeassistant.config_entries import ( ENTRY_STATE_LOADED, @@ -8,10 +9,11 @@ ENTRY_STATE_SETUP_RETRY, ) from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.util.dt import utcnow from . import API_POINT_URL -from tests.common import MockConfigEntry, load_fixture +from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture from tests.components.airly import init_integration @@ -88,37 +90,83 @@ async def test_config_with_turned_off_station(hass, aioclient_mock): async def test_update_interval(hass, aioclient_mock): """Test correct update interval when the number of configured instances changes.""" - entry = await init_integration(hass, aioclient_mock) - - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_LOADED - for instance in hass.data[DOMAIN].values(): - assert instance.update_interval == timedelta(minutes=15) + REMAINING_RQUESTS = 15 + HEADERS = { + "X-RateLimit-Limit-day": "100", + "X-RateLimit-Remaining-day": str(REMAINING_RQUESTS), + } entry = MockConfigEntry( domain=DOMAIN, - title="Work", - unique_id="66.66-111.11", + title="Home", + unique_id="123-456", data={ "api_key": "foo", - "latitude": 66.66, - "longitude": 111.11, - "name": "Work", + "latitude": 123, + "longitude": 456, + "name": "Home", }, ) aioclient_mock.get( - "https://airapi.airly.eu/v2/measurements/point?lat=66.660000&lng=111.110000", + API_POINT_URL, text=load_fixture("airly_valid_station.json"), + headers=HEADERS, ) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() + instances = 1 - assert len(hass.config_entries.async_entries(DOMAIN)) == 2 + assert aioclient_mock.call_count == 1 + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert entry.state == ENTRY_STATE_LOADED - for instance in hass.data[DOMAIN].values(): - assert instance.update_interval == timedelta(minutes=30) + + update_interval = set_update_interval(instances, REMAINING_RQUESTS) + future = utcnow() + update_interval + with patch("homeassistant.util.dt.utcnow") as mock_utcnow: + mock_utcnow.return_value = future + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + # call_count should increase by one because we have one instance configured + assert aioclient_mock.call_count == 2 + + # Now we add the second Airly instance + entry = MockConfigEntry( + domain=DOMAIN, + title="Work", + unique_id="66.66-111.11", + data={ + "api_key": "foo", + "latitude": 66.66, + "longitude": 111.11, + "name": "Work", + }, + ) + + aioclient_mock.get( + "https://airapi.airly.eu/v2/measurements/point?lat=66.660000&lng=111.110000", + text=load_fixture("airly_valid_station.json"), + headers=HEADERS, + ) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + instances = 2 + + assert aioclient_mock.call_count == 3 + assert len(hass.config_entries.async_entries(DOMAIN)) == 2 + assert entry.state == ENTRY_STATE_LOADED + + update_interval = set_update_interval(instances, REMAINING_RQUESTS) + future = utcnow() + update_interval + mock_utcnow.return_value = future + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + # call_count should increase by two because we have two instances configured + assert aioclient_mock.call_count == 5 async def test_unload_entry(hass, aioclient_mock): From 3fda66d9e2cc9dc1347be178b0f5747d7b62a32c Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Tue, 27 Apr 2021 14:48:27 -0700 Subject: [PATCH 0601/1317] Change motionEye to use a two item device identifier tuple (#49774) * Change to a two item device identifier tuple. * Don't use join. --- homeassistant/components/motioneye/__init__.py | 4 ++-- tests/components/motioneye/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/motioneye/__init__.py b/homeassistant/components/motioneye/__init__.py index cb5e80b9c989c..3d8c775f14063 100644 --- a/homeassistant/components/motioneye/__init__.py +++ b/homeassistant/components/motioneye/__init__.py @@ -52,9 +52,9 @@ def create_motioneye_client( def get_motioneye_device_identifier( config_entry_id: str, camera_id: int -) -> tuple[str, str, int]: +) -> tuple[str, str]: """Get the identifiers for a motionEye device.""" - return (DOMAIN, config_entry_id, camera_id) + return (DOMAIN, f"{config_entry_id}_{camera_id}") def get_motioneye_entity_unique_id( diff --git a/tests/components/motioneye/__init__.py b/tests/components/motioneye/__init__.py index a462d08303847..ed91d7c40a373 100644 --- a/tests/components/motioneye/__init__.py +++ b/tests/components/motioneye/__init__.py @@ -18,7 +18,7 @@ TEST_CAMERA_ID = 100 TEST_CAMERA_NAME = "Test Camera" TEST_CAMERA_ENTITY_ID = "camera.test_camera" -TEST_CAMERA_DEVICE_IDENTIFIER = (DOMAIN, TEST_CONFIG_ENTRY_ID, TEST_CAMERA_ID) +TEST_CAMERA_DEVICE_IDENTIFIER = (DOMAIN, f"{TEST_CONFIG_ENTRY_ID}_{TEST_CAMERA_ID}") TEST_CAMERA = { "show_frame_changes": False, "framerate": 25, From cd845954291a9539613c660ee273ecf0cc5e94a3 Mon Sep 17 00:00:00 2001 From: Greg Dowling Date: Tue, 27 Apr 2021 22:55:29 +0100 Subject: [PATCH 0602/1317] Rework roon media player grouping to use media_player base services (#49667) * Add group/join status attributes to roon player. * Rework join/unjoin code to use base media_player services. * Switch join and unjoin to be sync. --- homeassistant/components/roon/manifest.json | 2 +- homeassistant/components/roon/media_player.py | 71 ++++++------------- homeassistant/components/roon/server.py | 6 ++ homeassistant/components/roon/services.yaml | 20 ------ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 29 insertions(+), 74 deletions(-) diff --git a/homeassistant/components/roon/manifest.json b/homeassistant/components/roon/manifest.json index 875294310d9c0..09fcaad5f1fa2 100644 --- a/homeassistant/components/roon/manifest.json +++ b/homeassistant/components/roon/manifest.json @@ -3,7 +3,7 @@ "name": "RoonLabs music player", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/roon", - "requirements": ["roonapi==0.0.32"], + "requirements": ["roonapi==0.0.36"], "codeowners": ["@pavoni"], "iot_class": "local_push" } diff --git a/homeassistant/components/roon/media_player.py b/homeassistant/components/roon/media_player.py index 773028da2d307..ff55c0fb1fb74 100644 --- a/homeassistant/components/roon/media_player.py +++ b/homeassistant/components/roon/media_player.py @@ -7,6 +7,7 @@ from homeassistant.components.media_player import MediaPlayerEntity from homeassistant.components.media_player.const import ( SUPPORT_BROWSE_MEDIA, + SUPPORT_GROUPING, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, @@ -42,6 +43,7 @@ SUPPORT_ROON = ( SUPPORT_BROWSE_MEDIA + | SUPPORT_GROUPING | SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_STOP @@ -59,12 +61,8 @@ _LOGGER = logging.getLogger(__name__) -SERVICE_JOIN = "join" -SERVICE_UNJOIN = "unjoin" SERVICE_TRANSFER = "transfer" -ATTR_JOIN = "join_ids" -ATTR_UNJOIN = "unjoin_ids" ATTR_TRANSFER = "transfer_id" @@ -75,16 +73,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): # Register entity services platform = entity_platform.current_platform.get() - platform.async_register_entity_service( - SERVICE_JOIN, - {vol.Required(ATTR_JOIN): vol.All(cv.ensure_list, [cv.entity_id])}, - "join", - ) - platform.async_register_entity_service( - SERVICE_UNJOIN, - {vol.Optional(ATTR_UNJOIN): vol.All(cv.ensure_list, [cv.entity_id])}, - "unjoin", - ) platform.async_register_entity_service( SERVICE_TRANSFER, {vol.Required(ATTR_TRANSFER): cv.entity_id}, @@ -164,6 +152,13 @@ def supported_features(self): """Flag media player features that are supported.""" return SUPPORT_ROON + @property + def group_members(self): + """Return the grouped players.""" + + roon_names = self._server.roonapi.grouped_zone_names(self._output_id) + return [self._server.entity_id(roon_name) for roon_name in roon_names] + @property def device_info(self): """Return the device info.""" @@ -491,8 +486,8 @@ def play_media(self, media_type, media_id, **kwargs): path_list, ) - def join(self, join_ids): - """Add another Roon player to this player's join group.""" + def join_players(self, group_members): + """Join `group_members` as a player group with the current player.""" zone_data = self._server.roonapi.zone_by_output_id(self._output_id) if zone_data is None: @@ -511,7 +506,7 @@ def join(self, join_ids): sync_available[zone["display_name"]] = output["output_id"] names = [] - for entity_id in join_ids: + for entity_id in group_members: name = self._server.roon_name(entity_id) if name is None: _LOGGER.error("No roon player found for %s", entity_id) @@ -531,43 +526,17 @@ def join(self, join_ids): [self._output_id] + [sync_available[name] for name in names] ) - def unjoin(self, unjoin_ids=None): - """Remove a Roon player to this player's join group.""" + def unjoin_player(self): + """Remove this player from any group.""" - zone_data = self._server.roonapi.zone_by_output_id(self._output_id) - if zone_data is None: - _LOGGER.error("No zone data for %s", self.name) + if not self._server.roonapi.is_grouped(self._output_id): + _LOGGER.error( + "Can't unjoin player %s because it's not in a group", + self.name, + ) return - join_group = { - output["display_name"]: output["output_id"] - for output in zone_data["outputs"] - if output["display_name"] != self.name - } - - if unjoin_ids is None: - # unjoin everything - names = list(join_group) - else: - names = [] - for entity_id in unjoin_ids: - name = self._server.roon_name(entity_id) - if name is None: - _LOGGER.error("No roon player found for %s", entity_id) - return - - if name not in join_group: - _LOGGER.error( - "Can't unjoin player %s from %s because it's not in the joined group %s", - name, - self.name, - list(join_group), - ) - return - names.append(name) - - _LOGGER.debug("Unjoining %s from %s", names, self.name) - self._server.roonapi.ungroup_outputs([join_group[name] for name in names]) + self._server.roonapi.ungroup_outputs([self._output_id]) async def async_transfer(self, transfer_id): """Transfer playback from this roon player to another.""" diff --git a/homeassistant/components/roon/server.py b/homeassistant/components/roon/server.py index 83b620e176ea8..d216dca419dd4 100644 --- a/homeassistant/components/roon/server.py +++ b/homeassistant/components/roon/server.py @@ -28,6 +28,7 @@ def __init__(self, hass, config_entry): self.offline_devices = set() self._exit = False self._roon_name_by_id = {} + self._id_by_roon_name = {} async def async_setup(self, tries=0): """Set up a roon server based on config parameters.""" @@ -78,11 +79,16 @@ def zones(self): def add_player_id(self, entity_id, roon_name): """Register a roon player.""" self._roon_name_by_id[entity_id] = roon_name + self._id_by_roon_name[roon_name] = entity_id def roon_name(self, entity_id): """Get the name of the roon player from entity_id.""" return self._roon_name_by_id.get(entity_id) + def entity_id(self, roon_name): + """Get the id of the roon player from the roon name.""" + return self._id_by_roon_name.get(roon_name) + def stop_roon(self): """Stop background worker.""" self.roonapi.stop() diff --git a/homeassistant/components/roon/services.yaml b/homeassistant/components/roon/services.yaml index ec096effe5baa..6622d9b4c3134 100644 --- a/homeassistant/components/roon/services.yaml +++ b/homeassistant/components/roon/services.yaml @@ -1,23 +1,3 @@ -join: - description: Group players together. - fields: - entity_id: - description: id of the player that will be the master of the group. - example: "media_player.study" - join_ids: - description: id(s) of the players that will join the master. - example: "['media_player.bedroom', 'media_player.kitchen']" - -unjoin: - description: Remove players from a group. - fields: - entity_id: - description: id of the player that is the master of the group.. - example: "media_player.study" - unjoin_ids: - description: Optional id(s) of the players that will be unjoined from the group. If not specified, all players will be unjoined from the master. - example: "['media_player.bedroom', 'media_player.kitchen']" - transfer: description: Transfer playback from one player to another. fields: diff --git a/requirements_all.txt b/requirements_all.txt index dac6f3c3549d2..2e5d75817494b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2000,7 +2000,7 @@ rokuecp==0.8.1 roombapy==1.6.3 # homeassistant.components.roon -roonapi==0.0.32 +roonapi==0.0.36 # homeassistant.components.rova rova==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bf695a0eeb39a..3bddefb76e995 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1067,7 +1067,7 @@ rokuecp==0.8.1 roombapy==1.6.3 # homeassistant.components.roon -roonapi==0.0.32 +roonapi==0.0.36 # homeassistant.components.rpi_power rpi-bad-power==0.1.0 From 89e7983ee0980b60a65bb2c15c92a0f9cf6ee128 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 28 Apr 2021 00:15:38 +0200 Subject: [PATCH 0603/1317] Add Blueprint foundation to Scripts (#48621) Co-authored-by: Paulus Schoutsen --- homeassistant/components/config/script.py | 9 +- homeassistant/components/script/__init__.py | 164 +++++++++--------- .../blueprints/confirmable_notification.yaml | 74 ++++++++ homeassistant/components/script/config.py | 91 ++++++++-- homeassistant/components/script/const.py | 20 +++ homeassistant/components/script/helpers.py | 15 ++ homeassistant/components/script/manifest.json | 2 +- homeassistant/components/script/trace.py | 19 +- tests/components/demo/conftest.py | 1 + tests/components/emulated_hue/conftest.py | 3 + tests/components/logbook/conftest.py | 3 + tests/components/mqtt/conftest.py | 1 + tests/components/script/conftest.py | 3 + tests/components/script/test_blueprint.py | 114 ++++++++++++ tests/components/script/test_init.py | 30 ++++ tests/components/trace/conftest.py | 3 + tests/test_config.py | 1 - 17 files changed, 449 insertions(+), 104 deletions(-) create mode 100644 homeassistant/components/script/blueprints/confirmable_notification.yaml create mode 100644 homeassistant/components/script/const.py create mode 100644 homeassistant/components/script/helpers.py create mode 100644 tests/components/emulated_hue/conftest.py create mode 100644 tests/components/logbook/conftest.py create mode 100644 tests/components/script/conftest.py create mode 100644 tests/components/script/test_blueprint.py create mode 100644 tests/components/trace/conftest.py diff --git a/homeassistant/components/config/script.py b/homeassistant/components/config/script.py index 73b1ee0be5c97..7adc766a1abb9 100644 --- a/homeassistant/components/config/script.py +++ b/homeassistant/components/config/script.py @@ -1,6 +1,9 @@ """Provide configuration end points for scripts.""" -from homeassistant.components.script import DOMAIN, SCRIPT_ENTRY_SCHEMA -from homeassistant.components.script.config import async_validate_config_item +from homeassistant.components.script import DOMAIN +from homeassistant.components.script.config import ( + SCRIPT_ENTITY_SCHEMA, + async_validate_config_item, +) from homeassistant.config import SCRIPT_CONFIG_PATH from homeassistant.const import SERVICE_RELOAD import homeassistant.helpers.config_validation as cv @@ -21,7 +24,7 @@ async def hook(action, config_key): "config", SCRIPT_CONFIG_PATH, cv.slug, - SCRIPT_ENTRY_SCHEMA, + SCRIPT_ENTITY_SCHEMA, post_write_hook=hook, data_validator=async_validate_config_item, ) diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index e851850a924b9..41d5e697cf19f 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -3,21 +3,21 @@ import asyncio import logging +from typing import Any, Dict, cast import voluptuous as vol +from voluptuous.humanize import humanize_error -from homeassistant.components.trace import TRACE_CONFIG_SCHEMA +from homeassistant.components.blueprint import BlueprintInputs from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_MODE, ATTR_NAME, CONF_ALIAS, - CONF_DEFAULT, CONF_DESCRIPTION, CONF_ICON, CONF_MODE, CONF_NAME, - CONF_SELECTOR, CONF_SEQUENCE, CONF_VARIABLES, SERVICE_RELOAD, @@ -27,6 +27,7 @@ STATE_ON, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import extract_domain_configs import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import make_entity_service_schema from homeassistant.helpers.entity import ToggleEntity @@ -36,62 +37,26 @@ ATTR_MAX, CONF_MAX, CONF_MAX_EXCEEDED, - SCRIPT_MODE_SINGLE, Script, - make_script_schema, ) -from homeassistant.helpers.selector import validate_selector from homeassistant.helpers.service import async_set_service_schema from homeassistant.helpers.trace import trace_get, trace_path from homeassistant.loader import bind_hass -from .trace import trace_script - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = "script" - -ATTR_LAST_ACTION = "last_action" -ATTR_LAST_TRIGGERED = "last_triggered" -ATTR_VARIABLES = "variables" - -CONF_ADVANCED = "advanced" -CONF_EXAMPLE = "example" -CONF_FIELDS = "fields" -CONF_REQUIRED = "required" -CONF_TRACE = "trace" - -ENTITY_ID_FORMAT = DOMAIN + ".{}" - -EVENT_SCRIPT_STARTED = "script_started" - - -SCRIPT_ENTRY_SCHEMA = make_script_schema( - { - vol.Optional(CONF_ALIAS): cv.string, - vol.Optional(CONF_TRACE, default={}): TRACE_CONFIG_SCHEMA, - vol.Optional(CONF_ICON): cv.icon, - vol.Required(CONF_SEQUENCE): cv.SCRIPT_SCHEMA, - vol.Optional(CONF_DESCRIPTION, default=""): cv.string, - vol.Optional(CONF_VARIABLES): cv.SCRIPT_VARIABLES_SCHEMA, - vol.Optional(CONF_FIELDS, default={}): { - cv.string: { - vol.Optional(CONF_ADVANCED, default=False): cv.boolean, - vol.Optional(CONF_DEFAULT): cv.match_all, - vol.Optional(CONF_DESCRIPTION): cv.string, - vol.Optional(CONF_EXAMPLE): cv.string, - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_REQUIRED, default=False): cv.boolean, - vol.Optional(CONF_SELECTOR): validate_selector, - } - }, - }, - SCRIPT_MODE_SINGLE, -) - -CONFIG_SCHEMA = vol.Schema( - {DOMAIN: cv.schema_with_slug_keys(SCRIPT_ENTRY_SCHEMA)}, extra=vol.ALLOW_EXTRA +from .config import ScriptConfig, async_validate_config_item +from .const import ( + ATTR_LAST_ACTION, + ATTR_LAST_TRIGGERED, + ATTR_VARIABLES, + CONF_FIELDS, + CONF_TRACE, + DOMAIN, + ENTITY_ID_FORMAT, + EVENT_SCRIPT_STARTED, + LOGGER, ) +from .helpers import async_get_blueprints +from .trace import trace_script SCRIPT_SERVICE_SCHEMA = vol.Schema(dict) SCRIPT_TURN_ONOFF_SCHEMA = make_entity_service_schema( @@ -201,9 +166,13 @@ def areas_in_script(hass: HomeAssistant, entity_id: str) -> list[str]: async def async_setup(hass, config): """Load the scripts from the configuration.""" - hass.data[DOMAIN] = component = EntityComponent(_LOGGER, DOMAIN, hass) + hass.data[DOMAIN] = component = EntityComponent(LOGGER, DOMAIN, hass) + + # To register scripts as valid domain for Blueprint + async_get_blueprints(hass) - await _async_process_config(hass, config, component) + if not await _async_process_config(hass, config, component): + await async_get_blueprints(hass).async_populate() async def reload_service(service): """Call a service to reload scripts.""" @@ -257,8 +226,50 @@ async def toggle_service(service): return True -async def _async_process_config(hass, config, component): - """Process script configuration.""" +async def _async_process_config(hass, config, component) -> bool: + """Process script configuration. + + Return true, if Blueprints were used. + """ + entities = [] + blueprints_used = False + + for config_key in extract_domain_configs(config, DOMAIN): + conf: dict[str, dict[str, Any] | BlueprintInputs] = config[config_key] + + for object_id, config_block in conf.items(): + raw_blueprint_inputs = None + raw_config = None + + if isinstance(config_block, BlueprintInputs): + blueprints_used = True + blueprint_inputs = config_block + raw_blueprint_inputs = blueprint_inputs.config_with_inputs + + try: + raw_config = blueprint_inputs.async_substitute() + config_block = cast( + Dict[str, Any], + await async_validate_config_item(hass, raw_config), + ) + except vol.Invalid as err: + LOGGER.error( + "Blueprint %s generated invalid script with input %s: %s", + blueprint_inputs.blueprint.name, + blueprint_inputs.inputs, + humanize_error(config_block, err), + ) + continue + else: + raw_config = cast(ScriptConfig, config_block).raw_config + + entities.append( + ScriptEntity( + hass, object_id, config_block, raw_config, raw_blueprint_inputs + ) + ) + + await component.async_add_entities(entities) async def service_handler(service): """Execute a service call to script.