Merge pull request #38894 from home-assistant/rc

This commit is contained in:
Paulus Schoutsen 2020-08-15 07:35:34 +02:00 committed by GitHub
commit e8b7ddc966
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 186 additions and 86 deletions

View File

@ -20,6 +20,7 @@ from .const import (
COORDINATOR_RAIN, COORDINATOR_RAIN,
DOMAIN, DOMAIN,
PLATFORMS, PLATFORMS,
UNDO_UPDATE_LISTENER,
) )
_LOGGER = logging.getLogger(__name__) _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(): async def _async_update_data_forecast_forecast():
"""Fetch data from API endpoint.""" """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(): async def _async_update_data_rain():
"""Fetch data from API endpoint.""" """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(): async def _async_update_data_alert():
"""Fetch data from API endpoint.""" """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 client.get_warning_current_phenomenoms, department, 0, True
) )
@ -156,10 +159,13 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
entry.title, entry.title,
) )
undo_listener = entry.add_update_listener(_async_update_listener)
hass.data[DOMAIN][entry.entry_id] = { hass.data[DOMAIN][entry.entry_id] = {
COORDINATOR_FORECAST: coordinator_forecast, COORDINATOR_FORECAST: coordinator_forecast,
COORDINATOR_RAIN: coordinator_rain, COORDINATOR_RAIN: coordinator_rain,
COORDINATOR_ALERT: coordinator_alert, COORDINATOR_ALERT: coordinator_alert,
UNDO_UPDATE_LISTENER: undo_listener,
} }
for platform in PLATFORMS: for platform in PLATFORMS:
@ -192,8 +198,14 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry):
) )
) )
if unload_ok: if unload_ok:
hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]()
hass.data[DOMAIN].pop(entry.entry_id) hass.data[DOMAIN].pop(entry.entry_id)
if len(hass.data[DOMAIN]) == 0: if not hass.data[DOMAIN]:
hass.data.pop(DOMAIN) hass.data.pop(DOMAIN)
return unload_ok return unload_ok
async def _async_update_listener(hass: HomeAssistantType, entry: ConfigEntry):
"""Handle options update."""
await hass.config_entries.async_reload(entry.entry_id)

View File

@ -21,13 +21,18 @@ class MeteoFranceFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1 VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
def __init__(self):
"""Init MeteoFranceFlowHandler."""
self.places = []
@staticmethod @staticmethod
@callback @callback
def async_get_options_flow(config_entry): def async_get_options_flow(config_entry):
"""Get the options flow for this handler.""" """Get the options flow for this handler."""
return MeteoFranceOptionsFlowHandler(config_entry) 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.""" """Show the setup form to the user."""
if user_input is None: if user_input is None:
@ -46,7 +51,7 @@ class MeteoFranceFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
errors = {} errors = {}
if user_input is None: 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 city = user_input[CONF_CITY] # Might be a city name or a postal code
latitude = user_input.get(CONF_LATITUDE) latitude = user_input.get(CONF_LATITUDE)
@ -54,13 +59,15 @@ class MeteoFranceFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
if not latitude: if not latitude:
client = MeteoFranceClient() client = MeteoFranceClient()
places = await self.hass.async_add_executor_job(client.search_places, city) self.places = await self.hass.async_add_executor_job(
_LOGGER.debug("places search result: %s", places) client.search_places, city
if not places: )
_LOGGER.debug("Places search result: %s", self.places)
if not self.places:
errors[CONF_CITY] = "empty" 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 # Check if already configured
await self.async_set_unique_id(f"{latitude}, {longitude}") await self.async_set_unique_id(f"{latitude}, {longitude}")
@ -74,19 +81,27 @@ class MeteoFranceFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Import a config entry.""" """Import a config entry."""
return await self.async_step_user(user_input) 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.""" """Step where the user choose the city from the API search results."""
if places and len(places) > 1 and self.source != SOURCE_IMPORT: if not user_input:
places_for_form = {} if len(self.places) > 1 and self.source != SOURCE_IMPORT:
for place in places: places_for_form = {}
places_for_form[_build_place_key(place)] = f"{place}" for place in self.places:
places_for_form[_build_place_key(place)] = f"{place}"
return await self._show_cities_form(places_for_form) return self.async_show_form(
# for import and only 1 city in the search result step_id="cities",
if places and not user_input: data_schema=vol.Schema(
user_input = {CONF_CITY: _build_place_key(places[0])} {
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( return await self.async_step_user(
{ {
CONF_CITY: city_infos[0], 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): class MeteoFranceOptionsFlowHandler(config_entries.OptionsFlow):
"""Handle a option flow.""" """Handle a option flow."""

View File

@ -1,6 +1,9 @@
"""Meteo-France component constants.""" """Meteo-France component constants."""
from homeassistant.const import ( from homeassistant.const import (
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_TIMESTAMP,
PRESSURE_HPA, PRESSURE_HPA,
SPEED_KILOMETERS_PER_HOUR, SPEED_KILOMETERS_PER_HOUR,
TEMP_CELSIUS, TEMP_CELSIUS,
@ -12,6 +15,7 @@ PLATFORMS = ["sensor", "weather"]
COORDINATOR_FORECAST = "coordinator_forecast" COORDINATOR_FORECAST = "coordinator_forecast"
COORDINATOR_RAIN = "coordinator_rain" COORDINATOR_RAIN = "coordinator_rain"
COORDINATOR_ALERT = "coordinator_alert" COORDINATOR_ALERT = "coordinator_alert"
UNDO_UPDATE_LISTENER = "undo_update_listener"
ATTRIBUTION = "Data provided by Météo-France" ATTRIBUTION = "Data provided by Météo-France"
CONF_CITY = "city" CONF_CITY = "city"
@ -24,7 +28,7 @@ ATTR_NEXT_RAIN_1_HOUR_FORECAST = "1_hour_forecast"
ENTITY_NAME = "name" ENTITY_NAME = "name"
ENTITY_UNIT = "unit" ENTITY_UNIT = "unit"
ENTITY_ICON = "icon" ENTITY_ICON = "icon"
ENTITY_CLASS = "device_class" ENTITY_DEVICE_CLASS = "device_class"
ENTITY_ENABLE = "enable" ENTITY_ENABLE = "enable"
ENTITY_API_DATA_PATH = "data_path" ENTITY_API_DATA_PATH = "data_path"
@ -32,8 +36,8 @@ SENSOR_TYPES = {
"pressure": { "pressure": {
ENTITY_NAME: "Pressure", ENTITY_NAME: "Pressure",
ENTITY_UNIT: PRESSURE_HPA, ENTITY_UNIT: PRESSURE_HPA,
ENTITY_ICON: "mdi:gauge", ENTITY_ICON: None,
ENTITY_CLASS: "pressure", ENTITY_DEVICE_CLASS: DEVICE_CLASS_PRESSURE,
ENTITY_ENABLE: False, ENTITY_ENABLE: False,
ENTITY_API_DATA_PATH: "current_forecast:sea_level", ENTITY_API_DATA_PATH: "current_forecast:sea_level",
}, },
@ -41,7 +45,7 @@ SENSOR_TYPES = {
ENTITY_NAME: "Rain chance", ENTITY_NAME: "Rain chance",
ENTITY_UNIT: UNIT_PERCENTAGE, ENTITY_UNIT: UNIT_PERCENTAGE,
ENTITY_ICON: "mdi:weather-rainy", ENTITY_ICON: "mdi:weather-rainy",
ENTITY_CLASS: None, ENTITY_DEVICE_CLASS: None,
ENTITY_ENABLE: True, ENTITY_ENABLE: True,
ENTITY_API_DATA_PATH: "probability_forecast:rain:3h", ENTITY_API_DATA_PATH: "probability_forecast:rain:3h",
}, },
@ -49,7 +53,7 @@ SENSOR_TYPES = {
ENTITY_NAME: "Snow chance", ENTITY_NAME: "Snow chance",
ENTITY_UNIT: UNIT_PERCENTAGE, ENTITY_UNIT: UNIT_PERCENTAGE,
ENTITY_ICON: "mdi:weather-snowy", ENTITY_ICON: "mdi:weather-snowy",
ENTITY_CLASS: None, ENTITY_DEVICE_CLASS: None,
ENTITY_ENABLE: True, ENTITY_ENABLE: True,
ENTITY_API_DATA_PATH: "probability_forecast:snow:3h", ENTITY_API_DATA_PATH: "probability_forecast:snow:3h",
}, },
@ -57,7 +61,7 @@ SENSOR_TYPES = {
ENTITY_NAME: "Freeze chance", ENTITY_NAME: "Freeze chance",
ENTITY_UNIT: UNIT_PERCENTAGE, ENTITY_UNIT: UNIT_PERCENTAGE,
ENTITY_ICON: "mdi:snowflake", ENTITY_ICON: "mdi:snowflake",
ENTITY_CLASS: None, ENTITY_DEVICE_CLASS: None,
ENTITY_ENABLE: True, ENTITY_ENABLE: True,
ENTITY_API_DATA_PATH: "probability_forecast:freezing", ENTITY_API_DATA_PATH: "probability_forecast:freezing",
}, },
@ -65,23 +69,23 @@ SENSOR_TYPES = {
ENTITY_NAME: "Wind speed", ENTITY_NAME: "Wind speed",
ENTITY_UNIT: SPEED_KILOMETERS_PER_HOUR, ENTITY_UNIT: SPEED_KILOMETERS_PER_HOUR,
ENTITY_ICON: "mdi:weather-windy", ENTITY_ICON: "mdi:weather-windy",
ENTITY_CLASS: None, ENTITY_DEVICE_CLASS: None,
ENTITY_ENABLE: False, ENTITY_ENABLE: False,
ENTITY_API_DATA_PATH: "current_forecast:wind:speed", ENTITY_API_DATA_PATH: "current_forecast:wind:speed",
}, },
"next_rain": { "next_rain": {
ENTITY_NAME: "Next rain", ENTITY_NAME: "Next rain",
ENTITY_UNIT: None, ENTITY_UNIT: None,
ENTITY_ICON: "mdi:weather-pouring", ENTITY_ICON: None,
ENTITY_CLASS: "timestamp", ENTITY_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP,
ENTITY_ENABLE: True, ENTITY_ENABLE: True,
ENTITY_API_DATA_PATH: None, ENTITY_API_DATA_PATH: None,
}, },
"temperature": { "temperature": {
ENTITY_NAME: "Temperature", ENTITY_NAME: "Temperature",
ENTITY_UNIT: TEMP_CELSIUS, ENTITY_UNIT: TEMP_CELSIUS,
ENTITY_ICON: "mdi:thermometer", ENTITY_ICON: None,
ENTITY_CLASS: "temperature", ENTITY_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
ENTITY_ENABLE: False, ENTITY_ENABLE: False,
ENTITY_API_DATA_PATH: "current_forecast:T:value", ENTITY_API_DATA_PATH: "current_forecast:T:value",
}, },
@ -89,7 +93,7 @@ SENSOR_TYPES = {
ENTITY_NAME: "UV", ENTITY_NAME: "UV",
ENTITY_UNIT: None, ENTITY_UNIT: None,
ENTITY_ICON: "mdi:sunglasses", ENTITY_ICON: "mdi:sunglasses",
ENTITY_CLASS: None, ENTITY_DEVICE_CLASS: None,
ENTITY_ENABLE: True, ENTITY_ENABLE: True,
ENTITY_API_DATA_PATH: "today_forecast:uv", ENTITY_API_DATA_PATH: "today_forecast:uv",
}, },
@ -97,7 +101,7 @@ SENSOR_TYPES = {
ENTITY_NAME: "Weather alert", ENTITY_NAME: "Weather alert",
ENTITY_UNIT: None, ENTITY_UNIT: None,
ENTITY_ICON: "mdi:weather-cloudy-alert", ENTITY_ICON: "mdi:weather-cloudy-alert",
ENTITY_CLASS: None, ENTITY_DEVICE_CLASS: None,
ENTITY_ENABLE: True, ENTITY_ENABLE: True,
ENTITY_API_DATA_PATH: None, ENTITY_API_DATA_PATH: None,
}, },
@ -105,7 +109,7 @@ SENSOR_TYPES = {
ENTITY_NAME: "Daily precipitation", ENTITY_NAME: "Daily precipitation",
ENTITY_UNIT: "mm", ENTITY_UNIT: "mm",
ENTITY_ICON: "mdi:cup-water", ENTITY_ICON: "mdi:cup-water",
ENTITY_CLASS: None, ENTITY_DEVICE_CLASS: None,
ENTITY_ENABLE: True, ENTITY_ENABLE: True,
ENTITY_API_DATA_PATH: "today_forecast:precipitation:24h", ENTITY_API_DATA_PATH: "today_forecast:precipitation:24h",
}, },
@ -113,7 +117,7 @@ SENSOR_TYPES = {
ENTITY_NAME: "Cloud cover", ENTITY_NAME: "Cloud cover",
ENTITY_UNIT: UNIT_PERCENTAGE, ENTITY_UNIT: UNIT_PERCENTAGE,
ENTITY_ICON: "mdi:weather-partly-cloudy", ENTITY_ICON: "mdi:weather-partly-cloudy",
ENTITY_CLASS: None, ENTITY_DEVICE_CLASS: None,
ENTITY_ENABLE: True, ENTITY_ENABLE: True,
ENTITY_API_DATA_PATH: "current_forecast:clouds", ENTITY_API_DATA_PATH: "current_forecast:clouds",
}, },
@ -128,7 +132,7 @@ CONDITION_CLASSES = {
"Brouillard", "Brouillard",
"Brouillard givrant", "Brouillard givrant",
], ],
"hail": ["Risque de grêle"], "hail": ["Risque de grêle", "Risque de grèle"],
"lightning": ["Risque d'orages", "Orages"], "lightning": ["Risque d'orages", "Orages"],
"lightning-rainy": ["Pluie orageuses", "Pluies orageuses", "Averses orageuses"], "lightning-rainy": ["Pluie orageuses", "Pluies orageuses", "Averses orageuses"],
"partlycloudy": [ "partlycloudy": [

View File

@ -21,7 +21,7 @@ from .const import (
COORDINATOR_RAIN, COORDINATOR_RAIN,
DOMAIN, DOMAIN,
ENTITY_API_DATA_PATH, ENTITY_API_DATA_PATH,
ENTITY_CLASS, ENTITY_DEVICE_CLASS,
ENTITY_ENABLE, ENTITY_ENABLE,
ENTITY_ICON, ENTITY_ICON,
ENTITY_NAME, ENTITY_NAME,
@ -128,7 +128,7 @@ class MeteoFranceSensor(Entity):
@property @property
def device_class(self): def device_class(self):
"""Return the device class.""" """Return the device class."""
return SENSOR_TYPES[self._type][ENTITY_CLASS] return SENSOR_TYPES[self._type][ENTITY_DEVICE_CLASS]
@property @property
def entity_registry_enabled_default(self) -> bool: def entity_registry_enabled_default(self) -> bool:
@ -170,9 +170,15 @@ class MeteoFranceRainSensor(MeteoFranceSensor):
@property @property
def state(self): def state(self):
"""Return the state.""" """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 ( 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 @property
@ -180,11 +186,7 @@ class MeteoFranceRainSensor(MeteoFranceSensor):
"""Return the state attributes.""" """Return the state attributes."""
return { return {
ATTR_NEXT_RAIN_1_HOUR_FORECAST: [ ATTR_NEXT_RAIN_1_HOUR_FORECAST: [
{ {dt_util.utc_from_timestamp(item["dt"]).isoformat(): item["desc"]}
dt_util.as_local(
self.coordinator.data.timestamp_to_locale_time(item["dt"])
).strftime("%H:%M"): item["desc"]
}
for item in self.coordinator.data.forecast for item in self.coordinator.data.forecast
], ],
ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_ATTRIBUTION: ATTRIBUTION,

View File

@ -16,6 +16,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_MODE, TEMP_CELSIUS from homeassistant.const import CONF_MODE, TEMP_CELSIUS
from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.util import dt as dt_util
from .const import ( from .const import (
ATTRIBUTION, ATTRIBUTION,
@ -134,9 +135,9 @@ class MeteoFranceWeather(WeatherEntity):
continue continue
forecast_data.append( forecast_data.append(
{ {
ATTR_FORECAST_TIME: self.coordinator.data.timestamp_to_locale_time( ATTR_FORECAST_TIME: dt_util.utc_from_timestamp(
forecast["dt"] forecast["dt"]
), ).isoformat(),
ATTR_FORECAST_CONDITION: format_condition( ATTR_FORECAST_CONDITION: format_condition(
forecast["weather"]["desc"] forecast["weather"]["desc"]
), ),

View File

@ -40,7 +40,14 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
"""Fetch data from OVO Energy.""" """Fetch data from OVO Energy."""
now = datetime.utcnow() now = datetime.utcnow()
async with async_timeout.timeout(10): 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( coordinator = DataUpdateCoordinator(
hass, hass,

View File

@ -3,6 +3,6 @@
"name": "OVO Energy", "name": "OVO Energy",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/ovo_energy", "documentation": "https://www.home-assistant.io/integrations/ovo_energy",
"requirements": ["ovoenergy==1.1.6"], "requirements": ["ovoenergy==1.1.7"],
"codeowners": ["@timmo001"] "codeowners": ["@timmo001"]
} }

View File

@ -27,18 +27,34 @@ async def async_setup_entry(
] ]
client: OVOEnergy = hass.data[DOMAIN][entry.entry_id][DATA_CLIENT] client: OVOEnergy = hass.data[DOMAIN][entry.entry_id][DATA_CLIENT]
currency = coordinator.data.electricity[ entities = []
len(coordinator.data.electricity) - 1
].cost.currency_unit 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( async_add_entities(
[ entities, True,
OVOEnergyLastElectricityReading(coordinator, client),
OVOEnergyLastGasReading(coordinator, client),
OVOEnergyLastElectricityCost(coordinator, client, currency),
OVOEnergyLastGasCost(coordinator, client, currency),
],
True,
) )

View File

@ -172,7 +172,7 @@ class ZwaveLight(ZWaveDeviceEntity, LightEntity):
else: else:
# transition specified by user # 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): if ozw_version < (1, 6, 1205):
transition = kwargs[ATTR_TRANSITION] transition = kwargs[ATTR_TRANSITION]
if transition <= 127: if transition <= 127:

View File

@ -3,7 +3,7 @@ import asyncio
from uuid import UUID from uuid import UUID
from simplipy import API from simplipy import API
from simplipy.errors import InvalidCredentialsError, SimplipyError from simplipy.errors import EndpointUnavailable, InvalidCredentialsError, SimplipyError
from simplipy.websocket import ( from simplipy.websocket import (
EVENT_CAMERA_MOTION_DETECTED, EVENT_CAMERA_MOTION_DETECTED,
EVENT_CONNECTION_LOST, EVENT_CONNECTION_LOST,
@ -555,6 +555,13 @@ class SimpliSafe:
LOGGER.error("Error while using stored refresh token: %s", err) LOGGER.error("Error while using stored refresh token: %s", err)
return 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): if isinstance(result, SimplipyError):
LOGGER.error("SimpliSafe error while updating: %s", result) LOGGER.error("SimpliSafe error while updating: %s", result)
return return

View File

@ -3,6 +3,6 @@
"name": "SimpliSafe", "name": "SimpliSafe",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/simplisafe", "documentation": "https://www.home-assistant.io/integrations/simplisafe",
"requirements": ["simplisafe-python==9.2.2"], "requirements": ["simplisafe-python==9.3.0"],
"codeowners": ["@bachya"] "codeowners": ["@bachya"]
} }

View File

@ -1,4 +1,5 @@
"""Open ports in your router for Home Assistant and provide statistics.""" """Open ports in your router for Home Assistant and provide statistics."""
import asyncio
from ipaddress import ip_address from ipaddress import ip_address
from operator import itemgetter from operator import itemgetter
@ -106,7 +107,11 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry)
# discover and construct # discover and construct
udn = config_entry.data.get(CONFIG_ENTRY_UDN) udn = config_entry.data.get(CONFIG_ENTRY_UDN)
st = config_entry.data.get(CONFIG_ENTRY_ST) # pylint: disable=invalid-name 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: if not device:
_LOGGER.info("Unable to create UPnP/IGD, aborting") _LOGGER.info("Unable to create UPnP/IGD, aborting")
raise ConfigEntryNotReady raise ConfigEntryNotReady

View File

@ -3,7 +3,7 @@
"name": "Belkin WeMo", "name": "Belkin WeMo",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/wemo", "documentation": "https://www.home-assistant.io/integrations/wemo",
"requirements": ["pywemo==0.4.45"], "requirements": ["pywemo==0.4.46"],
"ssdp": [ "ssdp": [
{ {
"manufacturer": "Belkin International Inc." "manufacturer": "Belkin International Inc."

View File

@ -8,6 +8,7 @@ import voluptuous as vol
from zeroconf import ( from zeroconf import (
DNSPointer, DNSPointer,
DNSRecord, DNSRecord,
Error as ZeroconfError,
InterfaceChoice, InterfaceChoice,
IPVersion, IPVersion,
NonUniqueNameException, NonUniqueNameException,
@ -208,7 +209,12 @@ def setup(hass, config):
if state_change != ServiceStateChange.Added: if state_change != ServiceStateChange.Added:
return 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: if not service_info:
# Prevent the browser thread from collapsing as # Prevent the browser thread from collapsing as
# service_info can be None # service_info can be None

View File

@ -1,7 +1,7 @@
"""Constants used by Home Assistant components.""" """Constants used by Home Assistant components."""
MAJOR_VERSION = 0 MAJOR_VERSION = 0
MINOR_VERSION = 114 MINOR_VERSION = 114
PATCH_VERSION = "0" PATCH_VERSION = "1"
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__ = f"{__short_version__}.{PATCH_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER = (3, 7, 1) REQUIRED_PYTHON_VER = (3, 7, 1)

View File

@ -23,8 +23,8 @@ if TYPE_CHECKING:
SLOW_SETUP_WARNING = 10 SLOW_SETUP_WARNING = 10
SLOW_SETUP_MAX_WAIT = 60 SLOW_SETUP_MAX_WAIT = 60
SLOW_ADD_ENTITY_MAX_WAIT = 10 # Per Entity SLOW_ADD_ENTITY_MAX_WAIT = 15 # Per Entity
SLOW_ADD_MIN_TIMEOUT = 60 SLOW_ADD_MIN_TIMEOUT = 500
PLATFORM_NOT_READY_RETRIES = 10 PLATFORM_NOT_READY_RETRIES = 10
DATA_ENTITY_PLATFORM = "entity_platform" DATA_ENTITY_PLATFORM = "entity_platform"

View File

@ -11,6 +11,18 @@ from homeassistant import bootstrap
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.frame import warn_use 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 @dataclasses.dataclass
class RuntimeConfig: class RuntimeConfig:
@ -57,7 +69,9 @@ class HassEventLoopPolicy(PolicyBase): # type: ignore
if self.debug: if self.debug:
loop.set_debug(True) 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(executor)
loop.set_default_executor = warn_use( # type: ignore loop.set_default_executor = warn_use( # type: ignore
loop.set_default_executor, "sets default executor on the event loop" loop.set_default_executor, "sets default executor on the event loop"

View File

@ -1029,7 +1029,7 @@ oru==0.1.11
orvibo==1.1.1 orvibo==1.1.1
# homeassistant.components.ovo_energy # homeassistant.components.ovo_energy
ovoenergy==1.1.6 ovoenergy==1.1.7
# homeassistant.components.mqtt # homeassistant.components.mqtt
# homeassistant.components.shiftr # homeassistant.components.shiftr
@ -1837,7 +1837,7 @@ pyvolumio==0.1.1
pywebpush==1.9.2 pywebpush==1.9.2
# homeassistant.components.wemo # homeassistant.components.wemo
pywemo==0.4.45 pywemo==0.4.46
# homeassistant.components.xeoma # homeassistant.components.xeoma
pyxeoma==1.4.1 pyxeoma==1.4.1
@ -1963,7 +1963,7 @@ simplehound==0.3
simplepush==1.1.4 simplepush==1.1.4
# homeassistant.components.simplisafe # homeassistant.components.simplisafe
simplisafe-python==9.2.2 simplisafe-python==9.3.0
# homeassistant.components.sisyphus # homeassistant.components.sisyphus
sisyphus-control==2.2.1 sisyphus-control==2.2.1

View File

@ -473,7 +473,7 @@ onvif-zeep-async==0.4.0
openerz-api==0.1.0 openerz-api==0.1.0
# homeassistant.components.ovo_energy # homeassistant.components.ovo_energy
ovoenergy==1.1.6 ovoenergy==1.1.7
# homeassistant.components.mqtt # homeassistant.components.mqtt
# homeassistant.components.shiftr # homeassistant.components.shiftr
@ -878,7 +878,7 @@ sentry-sdk==0.13.5
simplehound==0.3 simplehound==0.3
# homeassistant.components.simplisafe # homeassistant.components.simplisafe
simplisafe-python==9.2.2 simplisafe-python==9.3.0
# homeassistant.components.sleepiq # homeassistant.components.sleepiq
sleepyq==0.7 sleepyq==0.7

View File

@ -85,7 +85,7 @@ async def test_light(hass, light_data, light_msg, light_rgb_msg, sent_messages):
assert state.state == "off" assert state.state == "off"
# Test turn on without brightness # Test turn on without brightness
new_transition = 127 new_transition = 127.0
await hass.services.async_call( await hass.services.async_call(
"light", "light",
"turn_on", "turn_on",

View File

@ -1,6 +1,12 @@
"""Test Zeroconf component setup process.""" """Test Zeroconf component setup process."""
import pytest 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 import zeroconf
from homeassistant.components.zeroconf import CONF_DEFAULT_INTERFACE, CONF_IPV6 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() 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): async def test_homekit_match_partial_space(hass, mock_zeroconf):
"""Test configured options for a device are loaded via config entry.""" """Test configured options for a device are loaded via config entry."""
with patch.dict( with patch.dict(