Compare commits

...

23 Commits

Author SHA1 Message Date
Paulus Schoutsen
740209a81d Merge pull request #38964 from home-assistant/rc 2020-08-17 12:43:35 +02:00
Paulus Schoutsen
3d66065fe6 Bumped version to 0.114.2 2020-08-17 09:29:00 +00:00
J. Nick Koston
68c83ea629 Accommodate systems with very large databases and slow disk/cpu (#38947)
On startup we run an sqlite3 quick_check to verify the database
integrity. In the majority of cases, the quick_check takes under
10 seconds.

On systems with very large databases and very slow disk/cpu,
this can take much longer so we freeze the timeout.
2020-08-17 09:28:20 +00:00
pbalogh77
7fb879f191 Fix HC3 compatibility further (#38931)
* Update __init__.py

Further fixes for HC3 compatibility.

* Update homeassistant/components/fibaro/__init__.py

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

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2020-08-17 09:28:19 +00:00
cgtobi
79924fcc7a Fix Netatmo climate boost/heat event handling (#38923)
* Fix boost event handling

* Replace strings with vars
2020-08-17 09:28:18 +00:00
Oncleben31
5fa14aae7d Fix error in meteo_france for overseas France cities (#38895) 2020-08-17 09:28:18 +00:00
automaton82
588fb283cc Fix the CONF_LOOP check to use the config (#38890) 2020-08-17 09:28:17 +00:00
Martin Hjelmare
48267c2705 Fix ozw pure rgb dimmer light (#38877)
* Fix ozw pure rgb light

* Add test
2020-08-17 09:28:16 +00:00
escoand
9ee6ae8f94 Better timeout handling in samsungtv integration (#38759)
* handle PlatformNotReady

* set timeout in bridge

* set timeout in test

* Revert "handle PlatformNotReady"

This reverts commit 118ee06ba0.
2020-08-17 09:28:15 +00:00
tizzen33
431fe5950c Fix 'Not Available' message for Onkyo integration (#38554) 2020-08-17 09:28:14 +00:00
Paulus Schoutsen
e8b7ddc966 Merge pull request #38894 from home-assistant/rc 2020-08-15 07:35:34 +02:00
Paulus Schoutsen
2485aa772c Bumped version to 0.114.1 2020-08-15 05:17:31 +00:00
J. Nick Koston
10525c7aa7 Adjust slow add entities timeouts to handle slowest known case (#38876)
With this change, we should still be able to startup
in under 10 minutes if something really goes wrong.

The testing done in #38661 was used to determine
these values.
2020-08-15 05:17:13 +00:00
Aidan Timson
0c83156ba4 Update ovoenergy package to v1.1.7 (#38875) 2020-08-15 05:17:12 +00:00
J. Nick Koston
0d4331829c Ensure service browser does not collapse on bad dns names (#38851)
If a device on the network presented a bad name, zeroconf
would throw zeroconf.BadTypeInNameException and the service
browser would die off.  We now trap the exception and continue.
2020-08-15 05:16:49 +00:00
Chris
276874c414 Fix ozw dimming transition (#38850)
* Handle float from light component

* Test with float

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2020-08-15 05:14:43 +00:00
Aidan Timson
ca5368f01f Fix OVO Energy Sensors (#38849) 2020-08-15 05:14:43 +00:00
Eric Severance
d4a7cefa8d Bump pywemo to 0.4.46 (#38845) 2020-08-15 05:14:42 +00:00
RogerSelwyn
473d0af85d Fix creation of unrequired sensors in OVO energy (#38835) 2020-08-15 05:14:41 +00:00
J. Nick Koston
eb16ca847a Make executor max_workers consistent between python versions (#38821)
The default on python 3.8 is for max_workers is significantly
lower than the default on python 3.7 which means we can get starved
for workers.

To determine a reasonable maximum, the maximum was increased to large
number on 5 production instances.

The number of worker threads created during startup that were
needed to avoid waiting for a thread:

  HOU 1 - 71
  HOU 2 - 48
  OGG 1 - 60
  OGG 2 - 68
  OGG 3 - 64

This lead to a selection of 64 as it was reliable in all cases
and did not have a significant memory impact
2020-08-15 05:14:40 +00:00
Aaron Bach
8feb382ae5 Handle unhandled exceptions related to unavailable SimpliSafe features (#38812) 2020-08-15 05:14:39 +00:00
Paulus Schoutsen
18eeda0e03 Catch upnp timeout error (#38794) 2020-08-15 05:14:39 +00:00
Quentame
caf5020bac Update meteo_france based on code review (#38789)
* Review: if not to pop

* Review: async_add_job --> async_add_executor_job

* Review: const

* Review: start logging messages with capital letter

* Review : UTC isoformated time --> fix "Invalid date""

* Fix hail forecast condition

* Review: _show_setup_form is a callback

* Fix update option

* Review: no icon for next_rain

* Review: inline cities form

* Review: store places as an instance attribute

* UNDO_UPDATE_LISTENER()
2020-08-15 05:14:38 +00:00
32 changed files with 476 additions and 123 deletions

View File

@@ -52,18 +52,21 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
coordinates=(lat, lon), precip_type=config.get(CONF_PRECIP_TYPE)
)
add_devices([ECCamera(radar_object, config.get(CONF_NAME))], True)
add_devices(
[ECCamera(radar_object, config.get(CONF_NAME), config[CONF_LOOP])], True
)
class ECCamera(Camera):
"""Implementation of an Environment Canada radar camera."""
def __init__(self, radar_object, camera_name):
def __init__(self, radar_object, camera_name, is_loop):
"""Initialize the camera."""
super().__init__()
self.radar_object = radar_object
self.camera_name = camera_name
self.is_loop = is_loop
self.content_type = "image/gif"
self.image = None
self.timestamp = None
@@ -90,7 +93,7 @@ class ECCamera(Camera):
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Update radar image."""
if CONF_LOOP:
if self.is_loop:
self.image = self.radar_object.get_loop()
else:
self.image = self.radar_object.get_latest_frame()

View File

@@ -235,10 +235,10 @@ class FibaroController:
scenes = self._client.scenes.list()
self._scene_map = {}
for device in scenes:
if "visible" in device and not device.visible:
if "name" not in device or "id" not in device:
continue
device.fibaro_controller = self
if device.roomID == 0:
if "roomID" not in device or device.roomID == 0:
room_name = "Unknown"
else:
room_name = self._room_map[device.roomID].name
@@ -250,6 +250,7 @@ class FibaroController:
device.unique_id_str = f"{self.hub_serial}.scene.{device.id}"
self._scene_map[device.id] = device
self.fibaro_devices["scene"].append(device)
_LOGGER.debug("%s scene -> %s", device.ha_id, device)
def _read_devices(self):
"""Read and process the device list."""
@@ -259,8 +260,10 @@ class FibaroController:
last_climate_parent = None
for device in devices:
try:
if "name" not in device or "id" not in device:
continue
device.fibaro_controller = self
if device.roomID == 0:
if "roomID" not in device or device.roomID == 0:
room_name = "Unknown"
else:
room_name = self._room_map[device.roomID].name

View File

@@ -4,6 +4,7 @@ from datetime import timedelta
import logging
from meteofrance.client import MeteoFranceClient
from meteofrance.helpers import is_valid_warning_department
import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
@@ -20,6 +21,7 @@ from .const import (
COORDINATOR_RAIN,
DOMAIN,
PLATFORMS,
UNDO_UPDATE_LISTENER,
)
_LOGGER = logging.getLogger(__name__)
@@ -77,15 +79,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
)
@@ -128,7 +132,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
_LOGGER.debug(
"Department corresponding to %s is %s", entry.title, department,
)
if department:
if is_valid_warning_department(department):
if not hass.data[DOMAIN].get(department):
coordinator_alert = DataUpdateCoordinator(
hass,
@@ -152,14 +156,17 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
)
else:
_LOGGER.warning(
"Weather alert not available: The city %s is not in France or Andorre.",
"Weather alert not available: The city %s is not in metropolitan France or Andorre.",
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 +199,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)

View File

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

View File

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

View File

@@ -3,6 +3,12 @@
"name": "Météo-France",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/meteo_france",
"requirements": ["meteofrance-api==0.1.0"],
"codeowners": ["@hacf-fr", "@oncleben31", "@Quentame"]
}
"requirements": [
"meteofrance-api==0.1.1"
],
"codeowners": [
"@hacf-fr",
"@oncleben31",
"@Quentame"
]
}

View File

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

View File

@@ -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"]
),

View File

@@ -262,8 +262,11 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
for room in home["rooms"]:
if data["event_type"] == "set_point":
if self._id == room["id"]:
if room["therm_setpoint_mode"] == "off":
if room["therm_setpoint_mode"] == STATE_NETATMO_OFF:
self._hvac_mode = HVAC_MODE_OFF
elif room["therm_setpoint_mode"] == STATE_NETATMO_MAX:
self._hvac_mode = HVAC_MODE_HEAT
self._target_temperature = DEFAULT_MAX_TEMP
else:
self._target_temperature = room["therm_setpoint_temperature"]
self.async_write_ha_state()

View File

@@ -239,6 +239,7 @@ class OnkyoDevice(MediaPlayerEntity):
self._source_mapping = sources
self._reverse_mapping = {value: key for key, value in sources.items()}
self._attributes = {}
self._hdmi_out_supported = True
def command(self, command):
"""Run an eiscp command and catch connection errors."""
@@ -251,6 +252,7 @@ class OnkyoDevice(MediaPlayerEntity):
else:
_LOGGER.info("%s is disconnected. Attempting to reconnect", self._name)
return False
_LOGGER.debug("Result for %s: %s", command, result)
return result
def update(self):
@@ -268,7 +270,13 @@ class OnkyoDevice(MediaPlayerEntity):
volume_raw = self.command("volume query")
mute_raw = self.command("audio-muting query")
current_source_raw = self.command("input-selector query")
hdmi_out_raw = self.command("hdmi-output-selector query")
# If the following command is sent to a device with only one HDMI out,
# the display shows 'Not Available'.
# We avoid this by checking if HDMI out is supported
if self._hdmi_out_supported:
hdmi_out_raw = self.command("hdmi-output-selector query")
else:
hdmi_out_raw = []
preset_raw = self.command("preset query")
if not (volume_raw and mute_raw and current_source_raw):
return
@@ -298,6 +306,8 @@ class OnkyoDevice(MediaPlayerEntity):
if not hdmi_out_raw:
return
self._attributes["video_out"] = ",".join(hdmi_out_raw[1])
if hdmi_out_raw[1] == "N/A":
self._hdmi_out_supported = False
@property
def name(self):

View File

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

View File

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

View File

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

View File

@@ -80,24 +80,24 @@ class ZwaveLight(ZWaveDeviceEntity, LightEntity):
if self.values.dimming_duration is not None:
self._supported_features |= SUPPORT_TRANSITION
if self.values.color is None or self.values.color_channels is None:
return
if self.values.color is not None:
self._supported_features |= SUPPORT_COLOR
self._supported_features |= SUPPORT_COLOR
if self.values.color_channels is not None:
# Support Color Temp if both white channels
if (self.values.color_channels.value & COLOR_CHANNEL_WARM_WHITE) and (
self.values.color_channels.value & COLOR_CHANNEL_COLD_WHITE
):
self._supported_features |= SUPPORT_COLOR_TEMP
# Support Color Temp if both white channels
if (self.values.color_channels.value & COLOR_CHANNEL_WARM_WHITE) and (
self.values.color_channels.value & COLOR_CHANNEL_COLD_WHITE
):
self._supported_features |= SUPPORT_COLOR_TEMP
# Support White value if only a single white channel
if ((self.values.color_channels.value & COLOR_CHANNEL_WARM_WHITE) != 0) ^ (
(self.values.color_channels.value & COLOR_CHANNEL_COLD_WHITE) != 0
):
self._supported_features |= SUPPORT_WHITE_VALUE
# Support White value if only a single white channel
if ((self.values.color_channels.value & COLOR_CHANNEL_WARM_WHITE) != 0) ^ (
(self.values.color_channels.value & COLOR_CHANNEL_COLD_WHITE) != 0
):
self._supported_features |= SUPPORT_WHITE_VALUE
self._calculate_rgb_values()
if self.values.color is not None:
self._calculate_color_values()
@property
def brightness(self):
@@ -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:
@@ -248,10 +248,8 @@ class ZwaveLight(ZWaveDeviceEntity, LightEntity):
self.values.primary.send_value(0)
def _calculate_rgb_values(self):
# Color Channels
self._color_channels = self.values.color_channels.data[ATTR_VALUE]
def _calculate_color_values(self):
"""Parse color rgb and color temperature data."""
# Color Data String
data = self.values.color.data[ATTR_VALUE]
@@ -259,6 +257,12 @@ class ZwaveLight(ZWaveDeviceEntity, LightEntity):
rgb = [int(data[1:3], 16), int(data[3:5], 16), int(data[5:7], 16)]
self._hs = color_util.color_RGB_to_hs(*rgb)
if self.values.color_channels is None:
return
# Color Channels
self._color_channels = self.values.color_channels.data[ATTR_VALUE]
# Parse remaining color channels. OpenZWave appends white channels
# that are present.
index = 7

View File

@@ -534,7 +534,15 @@ class Recorder(threading.Thread):
if self.db_url != SQLITE_URL_PREFIX and self.db_url.startswith(
SQLITE_URL_PREFIX
):
validate_or_move_away_sqlite_database(self.db_url)
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)
if self.engine is not None:
self.engine.dispose()

View File

@@ -250,7 +250,7 @@ class SamsungTVWSBridge(SamsungTVBridge):
host=self.host,
port=self.port,
token=self.token,
timeout=10,
timeout=8,
name=VALUE_CONF_NAME,
)
self._remote.open()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -902,7 +902,7 @@ messagebird==1.2.0
meteoalertapi==0.1.6
# homeassistant.components.meteo_france
meteofrance-api==0.1.0
meteofrance-api==0.1.1
# homeassistant.components.mfi
mficlient==0.3.0
@@ -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

View File

@@ -424,7 +424,7 @@ mbddns==0.1.2
mcstatus==2.3.0
# homeassistant.components.meteo_france
meteofrance-api==0.1.0
meteofrance-api==0.1.1
# homeassistant.components.mfi
mficlient==0.3.0
@@ -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

View File

@@ -33,6 +33,12 @@ def light_new_ozw_data_fixture():
return load_fixture("ozw/light_new_ozw_network_dump.csv")
@pytest.fixture(name="light_pure_rgb_dimmer_data", scope="session")
def light_pure_rgb_dimmer_data_fixture():
"""Load light rgb and dimmer MQTT data and return it."""
return load_fixture("ozw/light_pure_rgb_dimmer_dump.csv")
@pytest.fixture(name="light_no_rgb_data", scope="session")
def light_no_rgb_data_fixture():
"""Load light dimmer MQTT data and return it."""
@@ -139,6 +145,17 @@ async def light_rgb_msg_fixture(hass):
return message
@pytest.fixture(name="light_pure_rgb_msg")
async def light_pure_rgb_msg_fixture(hass):
"""Return a mock MQTT msg with a pure rgb light actuator message."""
light_json = json.loads(
await hass.async_add_executor_job(load_fixture, "ozw/light_pure_rgb.json")
)
message = MQTTMessage(topic=light_json["topic"], payload=light_json["payload"])
message.encode()
return message
@pytest.fixture(name="switch_msg")
async def switch_msg_fixture(hass):
"""Return a mock MQTT msg with a switch actuator message."""

View File

@@ -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",
@@ -350,6 +350,48 @@ async def test_light(hass, light_data, light_msg, light_rgb_msg, sent_messages):
assert state.attributes["color_temp"] == 153
async def test_pure_rgb_dimmer_light(
hass, light_pure_rgb_dimmer_data, light_msg, light_pure_rgb_msg, sent_messages
):
"""Test light with no color channels command class."""
receive_message = await setup_ozw(hass, fixture=light_pure_rgb_dimmer_data)
# Test loaded
state = hass.states.get("light.kitchen_rgb_strip_level")
assert state is not None
assert state.state == "on"
assert state.attributes["supported_features"] == 17
# Test setting hs_color
new_color = [300, 70]
await hass.services.async_call(
"light",
"turn_on",
{"entity_id": "light.kitchen_rgb_strip_level", "hs_color": new_color},
blocking=True,
)
assert len(sent_messages) == 2
msg = sent_messages[-1]
assert msg["topic"] == "OpenZWave/1/command/setvalue/"
assert msg["payload"] == {"Value": 255, "ValueIDKey": 122257425}
msg = sent_messages[-2]
assert msg["topic"] == "OpenZWave/1/command/setvalue/"
assert msg["payload"] == {"Value": "#ff4cff0000", "ValueIDKey": 122470423}
# Feedback on state
light_pure_rgb_msg.decode()
light_pure_rgb_msg.payload["Value"] = "#ff4cff0000"
light_pure_rgb_msg.encode()
receive_message(light_pure_rgb_msg)
await hass.async_block_till_done()
state = hass.states.get("light.kitchen_rgb_strip_level")
assert state is not None
assert state.state == "on"
assert state.attributes["hs_color"] == (300.0, 70.196)
async def test_no_rgb_light(hass, light_no_rgb_data, light_no_rgb_msg, sent_messages):
"""Test setting up config entry."""
receive_message = await setup_ozw(hass, fixture=light_no_rgb_data)

View File

@@ -97,7 +97,7 @@ MOCK_CALLS_ENTRY_WS = {
"host": "fake",
"name": "HomeAssistant",
"port": 8001,
"timeout": 10,
"timeout": 8,
"token": "abcde",
}

View File

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

25
tests/fixtures/ozw/light_pure_rgb.json vendored Normal file
View File

@@ -0,0 +1,25 @@
{
"topic": "OpenZWave/1/node/7/instance/1/commandclass/51/value/122470423/",
"payload": {
"Label": "Color",
"Value": "#ff00000000",
"Units": "#RRGGBBWW",
"ValueSet": false,
"ValuePolled": false,
"ChangeVerified": false,
"Min": 0,
"Max": 0,
"Type": "String",
"Instance": 1,
"CommandClass": "COMMAND_CLASS_COLOR",
"Index": 0,
"Node": 7,
"Genre": "User",
"Help": "Color (in RGB format)",
"ValueIDKey": 122470423,
"ReadOnly": false,
"WriteOnly": false,
"Event": "valueAdded",
"TimeStamp": 1597142799
}
}

File diff suppressed because one or more lines are too long