diff --git a/homeassistant/components/meteo_france/__init__.py b/homeassistant/components/meteo_france/__init__.py index 469c66ad79f..276aac188d9 100644 --- a/homeassistant/components/meteo_france/__init__.py +++ b/homeassistant/components/meteo_france/__init__.py @@ -20,6 +20,7 @@ from .const import ( COORDINATOR_RAIN, DOMAIN, PLATFORMS, + UNDO_UPDATE_LISTENER, ) _LOGGER = logging.getLogger(__name__) @@ -77,15 +78,17 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool async def _async_update_data_forecast_forecast(): """Fetch data from API endpoint.""" - return await hass.async_add_job(client.get_forecast, latitude, longitude) + return await hass.async_add_executor_job( + client.get_forecast, latitude, longitude + ) async def _async_update_data_rain(): """Fetch data from API endpoint.""" - return await hass.async_add_job(client.get_rain, latitude, longitude) + return await hass.async_add_executor_job(client.get_rain, latitude, longitude) async def _async_update_data_alert(): """Fetch data from API endpoint.""" - return await hass.async_add_job( + return await hass.async_add_executor_job( client.get_warning_current_phenomenoms, department, 0, True ) @@ -156,10 +159,13 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool entry.title, ) + undo_listener = entry.add_update_listener(_async_update_listener) + hass.data[DOMAIN][entry.entry_id] = { COORDINATOR_FORECAST: coordinator_forecast, COORDINATOR_RAIN: coordinator_rain, COORDINATOR_ALERT: coordinator_alert, + UNDO_UPDATE_LISTENER: undo_listener, } for platform in PLATFORMS: @@ -192,8 +198,14 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): ) ) if unload_ok: + hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() hass.data[DOMAIN].pop(entry.entry_id) - if len(hass.data[DOMAIN]) == 0: + if not hass.data[DOMAIN]: hass.data.pop(DOMAIN) return unload_ok + + +async def _async_update_listener(hass: HomeAssistantType, entry: ConfigEntry): + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/meteo_france/config_flow.py b/homeassistant/components/meteo_france/config_flow.py index 73b1ea41089..0854c280c16 100644 --- a/homeassistant/components/meteo_france/config_flow.py +++ b/homeassistant/components/meteo_france/config_flow.py @@ -21,13 +21,18 @@ class MeteoFranceFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + def __init__(self): + """Init MeteoFranceFlowHandler.""" + self.places = [] + @staticmethod @callback def async_get_options_flow(config_entry): """Get the options flow for this handler.""" return MeteoFranceOptionsFlowHandler(config_entry) - async def _show_setup_form(self, user_input=None, errors=None): + @callback + def _show_setup_form(self, user_input=None, errors=None): """Show the setup form to the user.""" if user_input is None: @@ -46,7 +51,7 @@ class MeteoFranceFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors = {} if user_input is None: - return await self._show_setup_form(user_input, errors) + return self._show_setup_form(user_input, errors) city = user_input[CONF_CITY] # Might be a city name or a postal code latitude = user_input.get(CONF_LATITUDE) @@ -54,13 +59,15 @@ class MeteoFranceFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if not latitude: client = MeteoFranceClient() - places = await self.hass.async_add_executor_job(client.search_places, city) - _LOGGER.debug("places search result: %s", places) - if not places: + self.places = await self.hass.async_add_executor_job( + client.search_places, city + ) + _LOGGER.debug("Places search result: %s", self.places) + if not self.places: errors[CONF_CITY] = "empty" - return await self._show_setup_form(user_input, errors) + return self._show_setup_form(user_input, errors) - return await self.async_step_cities(places=places) + return await self.async_step_cities() # Check if already configured await self.async_set_unique_id(f"{latitude}, {longitude}") @@ -74,19 +81,27 @@ class MeteoFranceFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Import a config entry.""" return await self.async_step_user(user_input) - async def async_step_cities(self, user_input=None, places=None): + async def async_step_cities(self, user_input=None): """Step where the user choose the city from the API search results.""" - if places and len(places) > 1 and self.source != SOURCE_IMPORT: - places_for_form = {} - for place in places: - places_for_form[_build_place_key(place)] = f"{place}" + if not user_input: + if len(self.places) > 1 and self.source != SOURCE_IMPORT: + places_for_form = {} + for place in self.places: + places_for_form[_build_place_key(place)] = f"{place}" - return await self._show_cities_form(places_for_form) - # for import and only 1 city in the search result - if places and not user_input: - user_input = {CONF_CITY: _build_place_key(places[0])} + return self.async_show_form( + step_id="cities", + data_schema=vol.Schema( + { + vol.Required(CONF_CITY): vol.All( + vol.Coerce(str), vol.In(places_for_form) + ) + } + ), + ) + user_input = {CONF_CITY: _build_place_key(self.places[0])} - city_infos = user_input.get(CONF_CITY).split(";") + city_infos = user_input[CONF_CITY].split(";") return await self.async_step_user( { CONF_CITY: city_infos[0], @@ -95,15 +110,6 @@ class MeteoFranceFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): } ) - async def _show_cities_form(self, cities): - """Show the form to choose the city.""" - return self.async_show_form( - step_id="cities", - data_schema=vol.Schema( - {vol.Required(CONF_CITY): vol.All(vol.Coerce(str), vol.In(cities))} - ), - ) - class MeteoFranceOptionsFlowHandler(config_entries.OptionsFlow): """Handle a option flow.""" diff --git a/homeassistant/components/meteo_france/const.py b/homeassistant/components/meteo_france/const.py index d1decb54078..8e6c625e331 100644 --- a/homeassistant/components/meteo_france/const.py +++ b/homeassistant/components/meteo_france/const.py @@ -1,6 +1,9 @@ """Meteo-France component constants.""" from homeassistant.const import ( + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_TIMESTAMP, PRESSURE_HPA, SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS, @@ -12,6 +15,7 @@ PLATFORMS = ["sensor", "weather"] COORDINATOR_FORECAST = "coordinator_forecast" COORDINATOR_RAIN = "coordinator_rain" COORDINATOR_ALERT = "coordinator_alert" +UNDO_UPDATE_LISTENER = "undo_update_listener" ATTRIBUTION = "Data provided by Météo-France" CONF_CITY = "city" @@ -24,7 +28,7 @@ ATTR_NEXT_RAIN_1_HOUR_FORECAST = "1_hour_forecast" ENTITY_NAME = "name" ENTITY_UNIT = "unit" ENTITY_ICON = "icon" -ENTITY_CLASS = "device_class" +ENTITY_DEVICE_CLASS = "device_class" ENTITY_ENABLE = "enable" ENTITY_API_DATA_PATH = "data_path" @@ -32,8 +36,8 @@ SENSOR_TYPES = { "pressure": { ENTITY_NAME: "Pressure", ENTITY_UNIT: PRESSURE_HPA, - ENTITY_ICON: "mdi:gauge", - ENTITY_CLASS: "pressure", + ENTITY_ICON: None, + ENTITY_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, ENTITY_ENABLE: False, ENTITY_API_DATA_PATH: "current_forecast:sea_level", }, @@ -41,7 +45,7 @@ SENSOR_TYPES = { ENTITY_NAME: "Rain chance", ENTITY_UNIT: UNIT_PERCENTAGE, ENTITY_ICON: "mdi:weather-rainy", - ENTITY_CLASS: None, + ENTITY_DEVICE_CLASS: None, ENTITY_ENABLE: True, ENTITY_API_DATA_PATH: "probability_forecast:rain:3h", }, @@ -49,7 +53,7 @@ SENSOR_TYPES = { ENTITY_NAME: "Snow chance", ENTITY_UNIT: UNIT_PERCENTAGE, ENTITY_ICON: "mdi:weather-snowy", - ENTITY_CLASS: None, + ENTITY_DEVICE_CLASS: None, ENTITY_ENABLE: True, ENTITY_API_DATA_PATH: "probability_forecast:snow:3h", }, @@ -57,7 +61,7 @@ SENSOR_TYPES = { ENTITY_NAME: "Freeze chance", ENTITY_UNIT: UNIT_PERCENTAGE, ENTITY_ICON: "mdi:snowflake", - ENTITY_CLASS: None, + ENTITY_DEVICE_CLASS: None, ENTITY_ENABLE: True, ENTITY_API_DATA_PATH: "probability_forecast:freezing", }, @@ -65,23 +69,23 @@ SENSOR_TYPES = { ENTITY_NAME: "Wind speed", ENTITY_UNIT: SPEED_KILOMETERS_PER_HOUR, ENTITY_ICON: "mdi:weather-windy", - ENTITY_CLASS: None, + ENTITY_DEVICE_CLASS: None, ENTITY_ENABLE: False, ENTITY_API_DATA_PATH: "current_forecast:wind:speed", }, "next_rain": { ENTITY_NAME: "Next rain", ENTITY_UNIT: None, - ENTITY_ICON: "mdi:weather-pouring", - ENTITY_CLASS: "timestamp", + ENTITY_ICON: None, + ENTITY_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP, ENTITY_ENABLE: True, ENTITY_API_DATA_PATH: None, }, "temperature": { ENTITY_NAME: "Temperature", ENTITY_UNIT: TEMP_CELSIUS, - ENTITY_ICON: "mdi:thermometer", - ENTITY_CLASS: "temperature", + ENTITY_ICON: None, + ENTITY_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, ENTITY_ENABLE: False, ENTITY_API_DATA_PATH: "current_forecast:T:value", }, @@ -89,7 +93,7 @@ SENSOR_TYPES = { ENTITY_NAME: "UV", ENTITY_UNIT: None, ENTITY_ICON: "mdi:sunglasses", - ENTITY_CLASS: None, + ENTITY_DEVICE_CLASS: None, ENTITY_ENABLE: True, ENTITY_API_DATA_PATH: "today_forecast:uv", }, @@ -97,7 +101,7 @@ SENSOR_TYPES = { ENTITY_NAME: "Weather alert", ENTITY_UNIT: None, ENTITY_ICON: "mdi:weather-cloudy-alert", - ENTITY_CLASS: None, + ENTITY_DEVICE_CLASS: None, ENTITY_ENABLE: True, ENTITY_API_DATA_PATH: None, }, @@ -105,7 +109,7 @@ SENSOR_TYPES = { ENTITY_NAME: "Daily precipitation", ENTITY_UNIT: "mm", ENTITY_ICON: "mdi:cup-water", - ENTITY_CLASS: None, + ENTITY_DEVICE_CLASS: None, ENTITY_ENABLE: True, ENTITY_API_DATA_PATH: "today_forecast:precipitation:24h", }, @@ -113,7 +117,7 @@ SENSOR_TYPES = { ENTITY_NAME: "Cloud cover", ENTITY_UNIT: UNIT_PERCENTAGE, ENTITY_ICON: "mdi:weather-partly-cloudy", - ENTITY_CLASS: None, + ENTITY_DEVICE_CLASS: None, ENTITY_ENABLE: True, ENTITY_API_DATA_PATH: "current_forecast:clouds", }, @@ -128,7 +132,7 @@ CONDITION_CLASSES = { "Brouillard", "Brouillard givrant", ], - "hail": ["Risque de grêle"], + "hail": ["Risque de grêle", "Risque de grèle"], "lightning": ["Risque d'orages", "Orages"], "lightning-rainy": ["Pluie orageuses", "Pluies orageuses", "Averses orageuses"], "partlycloudy": [ diff --git a/homeassistant/components/meteo_france/sensor.py b/homeassistant/components/meteo_france/sensor.py index 39e33dafd65..927250c6af6 100644 --- a/homeassistant/components/meteo_france/sensor.py +++ b/homeassistant/components/meteo_france/sensor.py @@ -21,7 +21,7 @@ from .const import ( COORDINATOR_RAIN, DOMAIN, ENTITY_API_DATA_PATH, - ENTITY_CLASS, + ENTITY_DEVICE_CLASS, ENTITY_ENABLE, ENTITY_ICON, ENTITY_NAME, @@ -128,7 +128,7 @@ class MeteoFranceSensor(Entity): @property def device_class(self): """Return the device class.""" - return SENSOR_TYPES[self._type][ENTITY_CLASS] + return SENSOR_TYPES[self._type][ENTITY_DEVICE_CLASS] @property def entity_registry_enabled_default(self) -> bool: @@ -170,9 +170,15 @@ class MeteoFranceRainSensor(MeteoFranceSensor): @property def state(self): """Return the state.""" - next_rain_date_locale = self.coordinator.data.next_rain_date_locale() + # search first cadran with rain + next_rain = next( + (cadran for cadran in self.coordinator.data.forecast if cadran["rain"] > 1), + None, + ) return ( - dt_util.as_local(next_rain_date_locale) if next_rain_date_locale else None + dt_util.utc_from_timestamp(next_rain["dt"]).isoformat() + if next_rain + else None ) @property @@ -180,11 +186,7 @@ class MeteoFranceRainSensor(MeteoFranceSensor): """Return the state attributes.""" return { ATTR_NEXT_RAIN_1_HOUR_FORECAST: [ - { - dt_util.as_local( - self.coordinator.data.timestamp_to_locale_time(item["dt"]) - ).strftime("%H:%M"): item["desc"] - } + {dt_util.utc_from_timestamp(item["dt"]).isoformat(): item["desc"]} for item in self.coordinator.data.forecast ], ATTR_ATTRIBUTION: ATTRIBUTION, diff --git a/homeassistant/components/meteo_france/weather.py b/homeassistant/components/meteo_france/weather.py index a9c4840901b..e8afcea9702 100644 --- a/homeassistant/components/meteo_france/weather.py +++ b/homeassistant/components/meteo_france/weather.py @@ -16,6 +16,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_MODE, TEMP_CELSIUS from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.util import dt as dt_util from .const import ( ATTRIBUTION, @@ -134,9 +135,9 @@ class MeteoFranceWeather(WeatherEntity): continue forecast_data.append( { - ATTR_FORECAST_TIME: self.coordinator.data.timestamp_to_locale_time( + ATTR_FORECAST_TIME: dt_util.utc_from_timestamp( forecast["dt"] - ), + ).isoformat(), ATTR_FORECAST_CONDITION: format_condition( forecast["weather"]["desc"] ), diff --git a/homeassistant/components/ovo_energy/__init__.py b/homeassistant/components/ovo_energy/__init__.py index 3aff51fa044..e98e81ba1c2 100644 --- a/homeassistant/components/ovo_energy/__init__.py +++ b/homeassistant/components/ovo_energy/__init__.py @@ -40,7 +40,14 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool """Fetch data from OVO Energy.""" now = datetime.utcnow() async with async_timeout.timeout(10): - return await client.get_daily_usage(now.strftime("%Y-%m")) + try: + await client.authenticate( + entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD] + ) + return await client.get_daily_usage(now.strftime("%Y-%m")) + except aiohttp.ClientError as exception: + _LOGGER.warning(exception) + return None coordinator = DataUpdateCoordinator( hass, diff --git a/homeassistant/components/ovo_energy/manifest.json b/homeassistant/components/ovo_energy/manifest.json index 2da08d3339b..ba9579279a9 100644 --- a/homeassistant/components/ovo_energy/manifest.json +++ b/homeassistant/components/ovo_energy/manifest.json @@ -3,6 +3,6 @@ "name": "OVO Energy", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ovo_energy", - "requirements": ["ovoenergy==1.1.6"], + "requirements": ["ovoenergy==1.1.7"], "codeowners": ["@timmo001"] } diff --git a/homeassistant/components/ovo_energy/sensor.py b/homeassistant/components/ovo_energy/sensor.py index 5fe1bb056e7..a0781836d6c 100644 --- a/homeassistant/components/ovo_energy/sensor.py +++ b/homeassistant/components/ovo_energy/sensor.py @@ -27,18 +27,34 @@ async def async_setup_entry( ] client: OVOEnergy = hass.data[DOMAIN][entry.entry_id][DATA_CLIENT] - currency = coordinator.data.electricity[ - len(coordinator.data.electricity) - 1 - ].cost.currency_unit + entities = [] + + if coordinator.data: + if coordinator.data.electricity: + entities.append(OVOEnergyLastElectricityReading(coordinator, client)) + entities.append( + OVOEnergyLastElectricityCost( + coordinator, + client, + coordinator.data.electricity[ + len(coordinator.data.electricity) - 1 + ].cost.currency_unit, + ) + ) + if coordinator.data.gas: + entities.append(OVOEnergyLastGasReading(coordinator, client)) + entities.append( + OVOEnergyLastGasCost( + coordinator, + client, + coordinator.data.gas[ + len(coordinator.data.gas) - 1 + ].cost.currency_unit, + ) + ) async_add_entities( - [ - OVOEnergyLastElectricityReading(coordinator, client), - OVOEnergyLastGasReading(coordinator, client), - OVOEnergyLastElectricityCost(coordinator, client, currency), - OVOEnergyLastGasCost(coordinator, client, currency), - ], - True, + entities, True, ) diff --git a/homeassistant/components/ozw/light.py b/homeassistant/components/ozw/light.py index 2c7461976c4..7464023868d 100644 --- a/homeassistant/components/ozw/light.py +++ b/homeassistant/components/ozw/light.py @@ -172,7 +172,7 @@ class ZwaveLight(ZWaveDeviceEntity, LightEntity): else: # transition specified by user - new_value = max(0, min(7620, kwargs[ATTR_TRANSITION])) + new_value = int(max(0, min(7620, kwargs[ATTR_TRANSITION]))) if ozw_version < (1, 6, 1205): transition = kwargs[ATTR_TRANSITION] if transition <= 127: diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 327549eeb62..07b4942ad34 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -3,7 +3,7 @@ import asyncio from uuid import UUID from simplipy import API -from simplipy.errors import InvalidCredentialsError, SimplipyError +from simplipy.errors import EndpointUnavailable, InvalidCredentialsError, SimplipyError from simplipy.websocket import ( EVENT_CAMERA_MOTION_DETECTED, EVENT_CONNECTION_LOST, @@ -555,6 +555,13 @@ class SimpliSafe: LOGGER.error("Error while using stored refresh token: %s", err) return + if isinstance(result, EndpointUnavailable): + # In case the user attempt an action not allowed in their current plan, + # we merely log that message at INFO level (so the user is aware, + # but not spammed with ERROR messages that they cannot change): + LOGGER.info(result) + return + if isinstance(result, SimplipyError): LOGGER.error("SimpliSafe error while updating: %s", result) return diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index 0ec77d13b9a..dd9ab53cb98 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -3,6 +3,6 @@ "name": "SimpliSafe", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/simplisafe", - "requirements": ["simplisafe-python==9.2.2"], + "requirements": ["simplisafe-python==9.3.0"], "codeowners": ["@bachya"] } diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index 98bf3e6f4dd..3a34cb26604 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -1,4 +1,5 @@ """Open ports in your router for Home Assistant and provide statistics.""" +import asyncio from ipaddress import ip_address from operator import itemgetter @@ -106,7 +107,11 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) # discover and construct udn = config_entry.data.get(CONFIG_ENTRY_UDN) st = config_entry.data.get(CONFIG_ENTRY_ST) # pylint: disable=invalid-name - device = await async_discover_and_construct(hass, udn, st) + try: + device = await async_discover_and_construct(hass, udn, st) + except asyncio.TimeoutError: + raise ConfigEntryNotReady + if not device: _LOGGER.info("Unable to create UPnP/IGD, aborting") raise ConfigEntryNotReady diff --git a/homeassistant/components/wemo/manifest.json b/homeassistant/components/wemo/manifest.json index 7bb3371c153..6200c3b46ed 100644 --- a/homeassistant/components/wemo/manifest.json +++ b/homeassistant/components/wemo/manifest.json @@ -3,7 +3,7 @@ "name": "Belkin WeMo", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wemo", - "requirements": ["pywemo==0.4.45"], + "requirements": ["pywemo==0.4.46"], "ssdp": [ { "manufacturer": "Belkin International Inc." diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 71e2f67bad7..f4f2494666b 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -8,6 +8,7 @@ import voluptuous as vol from zeroconf import ( DNSPointer, DNSRecord, + Error as ZeroconfError, InterfaceChoice, IPVersion, NonUniqueNameException, @@ -208,7 +209,12 @@ def setup(hass, config): if state_change != ServiceStateChange.Added: return - service_info = zeroconf.get_service_info(service_type, name) + try: + service_info = zeroconf.get_service_info(service_type, name) + except ZeroconfError: + _LOGGER.exception("Failed to get info for device %s", name) + return + if not service_info: # Prevent the browser thread from collapsing as # service_info can be None diff --git a/homeassistant/const.py b/homeassistant/const.py index 2726181612a..908f849d3aa 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 114 -PATCH_VERSION = "0" +PATCH_VERSION = "1" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 6d9a1275b06..7d6b6b52a9b 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -23,8 +23,8 @@ if TYPE_CHECKING: SLOW_SETUP_WARNING = 10 SLOW_SETUP_MAX_WAIT = 60 -SLOW_ADD_ENTITY_MAX_WAIT = 10 # Per Entity -SLOW_ADD_MIN_TIMEOUT = 60 +SLOW_ADD_ENTITY_MAX_WAIT = 15 # Per Entity +SLOW_ADD_MIN_TIMEOUT = 500 PLATFORM_NOT_READY_RETRIES = 10 DATA_ENTITY_PLATFORM = "entity_platform" diff --git a/homeassistant/runner.py b/homeassistant/runner.py index 26e7bab7616..b397f9438f2 100644 --- a/homeassistant/runner.py +++ b/homeassistant/runner.py @@ -11,6 +11,18 @@ from homeassistant import bootstrap from homeassistant.core import callback from homeassistant.helpers.frame import warn_use +# +# Python 3.8 has significantly less workers by default +# than Python 3.7. In order to be consistent between +# supported versions, we need to set max_workers. +# +# In most cases the workers are not I/O bound, as they +# are sleeping/blocking waiting for data from integrations +# updating so this number should be higher than the default +# use case. +# +MAX_EXECUTOR_WORKERS = 64 + @dataclasses.dataclass class RuntimeConfig: @@ -57,7 +69,9 @@ class HassEventLoopPolicy(PolicyBase): # type: ignore if self.debug: loop.set_debug(True) - executor = ThreadPoolExecutor(thread_name_prefix="SyncWorker") + executor = ThreadPoolExecutor( + thread_name_prefix="SyncWorker", max_workers=MAX_EXECUTOR_WORKERS + ) loop.set_default_executor(executor) loop.set_default_executor = warn_use( # type: ignore loop.set_default_executor, "sets default executor on the event loop" diff --git a/requirements_all.txt b/requirements_all.txt index 45acef0fd07..1b55cde4269 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1029,7 +1029,7 @@ oru==0.1.11 orvibo==1.1.1 # homeassistant.components.ovo_energy -ovoenergy==1.1.6 +ovoenergy==1.1.7 # homeassistant.components.mqtt # homeassistant.components.shiftr @@ -1837,7 +1837,7 @@ pyvolumio==0.1.1 pywebpush==1.9.2 # homeassistant.components.wemo -pywemo==0.4.45 +pywemo==0.4.46 # homeassistant.components.xeoma pyxeoma==1.4.1 @@ -1963,7 +1963,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==9.2.2 +simplisafe-python==9.3.0 # homeassistant.components.sisyphus sisyphus-control==2.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c1b4f560dc9..5a0a2c11485 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -473,7 +473,7 @@ onvif-zeep-async==0.4.0 openerz-api==0.1.0 # homeassistant.components.ovo_energy -ovoenergy==1.1.6 +ovoenergy==1.1.7 # homeassistant.components.mqtt # homeassistant.components.shiftr @@ -878,7 +878,7 @@ sentry-sdk==0.13.5 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==9.2.2 +simplisafe-python==9.3.0 # homeassistant.components.sleepiq sleepyq==0.7 diff --git a/tests/components/ozw/test_light.py b/tests/components/ozw/test_light.py index 8f9892e37cd..e35a7214858 100644 --- a/tests/components/ozw/test_light.py +++ b/tests/components/ozw/test_light.py @@ -85,7 +85,7 @@ async def test_light(hass, light_data, light_msg, light_rgb_msg, sent_messages): assert state.state == "off" # Test turn on without brightness - new_transition = 127 + new_transition = 127.0 await hass.services.async_call( "light", "turn_on", diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index e8315b5dc75..dee8ca97433 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -1,6 +1,12 @@ """Test Zeroconf component setup process.""" import pytest -from zeroconf import InterfaceChoice, IPVersion, ServiceInfo, ServiceStateChange +from zeroconf import ( + BadTypeInNameException, + InterfaceChoice, + IPVersion, + ServiceInfo, + ServiceStateChange, +) from homeassistant.components import zeroconf from homeassistant.components.zeroconf import CONF_DEFAULT_INTERFACE, CONF_IPV6 @@ -175,6 +181,20 @@ async def test_setup_with_ipv6_default(hass, mock_zeroconf): assert mock_zeroconf.called_with() +async def test_service_with_invalid_name(hass, mock_zeroconf, caplog): + """Test we do not crash on service with an invalid name.""" + with patch.object( + zeroconf, "HaServiceBrowser", side_effect=service_update_mock + ) as mock_service_browser: + mock_zeroconf.get_service_info.side_effect = BadTypeInNameException + 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 "Failed to get info for device name" in caplog.text + + async def test_homekit_match_partial_space(hass, mock_zeroconf): """Test configured options for a device are loaded via config entry.""" with patch.dict(