Merge pull request #50627 from home-assistant/rc

This commit is contained in:
Paulus Schoutsen 2021-05-14 14:56:50 -07:00 committed by GitHub
commit 7a0f245f92
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 133 additions and 123 deletions

View File

@ -3,7 +3,7 @@
"name": "ASUSWRT",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/asuswrt",
"requirements": ["aioasuswrt==1.3.1"],
"requirements": ["aioasuswrt==1.3.4"],
"codeowners": ["@kennedyshead", "@ollo69"],
"iot_class": "local_polling"
}

View File

@ -9,6 +9,7 @@ from pyiqvia.errors import IQVIAError
from homeassistant.components.sensor import SensorEntity
from homeassistant.const import ATTR_ATTRIBUTION
from homeassistant.core import callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
@ -74,9 +75,14 @@ async def async_setup_entry(hass, entry):
update_interval=DEFAULT_SCAN_INTERVAL,
update_method=partial(async_get_data_from_api, api_coro),
)
init_data_update_tasks.append(coordinator.async_config_entry_first_refresh())
init_data_update_tasks.append(coordinator.async_refresh())
await asyncio.gather(*init_data_update_tasks)
results = await asyncio.gather(*init_data_update_tasks, return_exceptions=True)
if all(isinstance(result, Exception) for result in results):
# The IQVIA API can be selectively flaky, meaning that any number of the setup
# API calls could fail. We only retry integration setup if *all* of the initial
# API calls fail:
raise ConfigEntryNotReady()
hass.data[DOMAIN].setdefault(DATA_COORDINATOR, {})[entry.entry_id] = coordinators
hass.config_entries.async_setup_platforms(entry, PLATFORMS)

View File

@ -104,9 +104,12 @@ class ForecastSensor(IQVIAEntity):
@callback
def update_from_latest_data(self):
"""Update the sensor."""
data = self.coordinator.data.get("Location")
if not self.coordinator.data:
return
if not data or not data.get("periods"):
data = self.coordinator.data.get("Location", {})
if not data.get("periods"):
return
indices = [p["Index"] for p in data["periods"]]

View File

@ -95,21 +95,21 @@ def valid_supported_color_modes(color_modes: Iterable[str]) -> set[str]:
return color_modes
def brightness_supported(color_modes: Iterable[str]) -> bool:
def brightness_supported(color_modes: Iterable[str] | None) -> bool:
"""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: Iterable[str]) -> bool:
def color_supported(color_modes: Iterable[str] | None) -> bool:
"""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: Iterable[str]) -> bool:
def color_temp_supported(color_modes: Iterable[str] | None) -> bool:
"""Test if color temperature is supported."""
if not color_modes:
return False

View File

@ -13,11 +13,17 @@ from homeassistant.components.light import (
)
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 import config_validation as cv, entity_registry as er
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
from . import (
ATTR_BRIGHTNESS_PCT,
ATTR_BRIGHTNESS_STEP_PCT,
ATTR_SUPPORTED_COLOR_MODES,
DOMAIN,
brightness_supported,
)
TYPE_BRIGHTNESS_INCREASE = "brightness_increase"
TYPE_BRIGHTNESS_DECREASE = "brightness_decrease"
@ -37,6 +43,25 @@ ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend(
)
def get_supported_color_modes(hass: HomeAssistant, entity_id: str) -> set | None:
"""Get supported color modes for a light entity.
First try the statemachine, then entity registry.
"""
state = hass.states.get(entity_id)
if state:
return state.attributes.get(ATTR_SUPPORTED_COLOR_MODES)
entity_registry = er.async_get(hass)
entry = entity_registry.async_get(entity_id)
if not entry:
raise HomeAssistantError(f"Unknown entity {entity_id}")
if not entry.capabilities:
return None
return entry.capabilities.get(ATTR_SUPPORTED_COLOR_MODES)
async def async_call_action_from_config(
hass: HomeAssistant,
config: ConfigType,
@ -77,15 +102,16 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]:
"""List device actions."""
actions = await toggle_entity.async_get_actions(hass, device_id, DOMAIN)
registry = await entity_registry.async_get_registry(hass)
entity_registry = er.async_get(hass)
for entry in entity_registry.async_entries_for_device(registry, device_id):
for entry in er.async_entries_for_device(entity_registry, device_id):
if entry.domain != DOMAIN:
continue
supported_color_modes = get_supported_color_modes(hass, entry.entity_id)
supported_features = get_supported_features(hass, entry.entity_id)
if supported_features & SUPPORT_BRIGHTNESS:
if brightness_supported(supported_color_modes):
actions.extend(
(
{
@ -123,6 +149,11 @@ async def async_get_action_capabilities(hass: HomeAssistant, config: dict) -> di
if config[CONF_TYPE] != toggle_entity.CONF_TURN_ON:
return {}
try:
supported_color_modes = get_supported_color_modes(hass, config[ATTR_ENTITY_ID])
except HomeAssistantError:
supported_color_modes = None
try:
supported_features = get_supported_features(hass, config[ATTR_ENTITY_ID])
except HomeAssistantError:
@ -130,7 +161,7 @@ async def async_get_action_capabilities(hass: HomeAssistant, config: dict) -> di
extra_fields = {}
if supported_features & SUPPORT_BRIGHTNESS:
if brightness_supported(supported_color_modes):
extra_fields[vol.Optional(ATTR_BRIGHTNESS_PCT)] = VALID_BRIGHTNESS_PCT
if supported_features & SUPPORT_FLASH:

View File

@ -2,7 +2,7 @@
import voluptuous as vol
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, State
from homeassistant.helpers import intent
import homeassistant.helpers.config_validation as cv
import homeassistant.util.color as color_util
@ -10,10 +10,11 @@ import homeassistant.util.color as color_util
from . import (
ATTR_BRIGHTNESS_PCT,
ATTR_RGB_COLOR,
ATTR_SUPPORTED_COLOR_MODES,
DOMAIN,
SERVICE_TURN_ON,
SUPPORT_BRIGHTNESS,
SUPPORT_COLOR,
brightness_supported,
color_supported,
)
INTENT_SET = "HassLightSet"
@ -24,6 +25,24 @@ async def async_setup_intents(hass: HomeAssistant) -> None:
hass.helpers.intent.async_register(SetIntentHandler())
def _test_supports_color(state: State) -> None:
"""Test if state supports colors."""
supported_color_modes = state.attributes.get(ATTR_SUPPORTED_COLOR_MODES)
if not color_supported(supported_color_modes):
raise intent.IntentHandleError(
f"Entity {state.name} does not support changing colors"
)
def _test_supports_brightness(state: State) -> None:
"""Test if state supports brightness."""
supported_color_modes = state.attributes.get(ATTR_SUPPORTED_COLOR_MODES)
if not brightness_supported(supported_color_modes):
raise intent.IntentHandleError(
f"Entity {state.name} does not support changing brightness"
)
class SetIntentHandler(intent.IntentHandler):
"""Handle set color intents."""
@ -46,14 +65,14 @@ class SetIntentHandler(intent.IntentHandler):
speech_parts = []
if "color" in slots:
intent.async_test_feature(state, SUPPORT_COLOR, "changing colors")
_test_supports_color(state)
service_data[ATTR_RGB_COLOR] = slots["color"]["value"]
# Use original passed in value of the color because we don't have
# human readable names for that internally.
speech_parts.append(f"the color {intent_obj.slots['color']['value']}")
if "brightness" in slots:
intent.async_test_feature(state, SUPPORT_BRIGHTNESS, "changing brightness")
_test_supports_brightness(state)
service_data[ATTR_BRIGHTNESS_PCT] = slots["brightness"]["value"]
speech_parts.append(f"{slots['brightness']['value']}% brightness")

View File

@ -506,7 +506,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
self._target_temperature = roomstatus["target_temperature"]
self._preset = NETATMO_MAP_PRESET[roomstatus["setpoint_mode"]]
self._hvac_mode = HVAC_MAP_NETATMO[self._preset]
self._battery_level = roomstatus.get("battery_level")
self._battery_level = roomstatus.get("battery_state")
self._connected = True
self._away = self._hvac_mode == HVAC_MAP_NETATMO[STATE_NETATMO_AWAY]
@ -546,7 +546,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
roomstatus["heating_status"] = self._boilerstatus
batterylevel = self._home_status.thermostats[
roomstatus["module_id"]
].get("battery_level")
].get("battery_state")
elif roomstatus["module_type"] == NA_VALVE:
roomstatus["heating_power_request"] = self._room_status[
"heating_power_request"
@ -557,16 +557,11 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
self._boilerstatus and roomstatus["heating_status"]
)
batterylevel = self._home_status.valves[roomstatus["module_id"]].get(
"battery_level"
"battery_state"
)
if batterylevel:
batterypct = interpolate(batterylevel, roomstatus["module_type"])
if (
not roomstatus.get("battery_level")
or batterypct < roomstatus["battery_level"]
):
roomstatus["battery_level"] = batterypct
roomstatus["battery_state"] = batterylevel
return roomstatus
@ -602,48 +597,6 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
return {**super().device_info, "suggested_area": self._room_data["name"]}
def interpolate(batterylevel: int, module_type: str) -> int:
"""Interpolate battery level depending on device type."""
na_battery_levels = {
NA_THERM: {
"full": 4100,
"high": 3600,
"medium": 3300,
"low": 3000,
"empty": 2800,
},
NA_VALVE: {
"full": 3200,
"high": 2700,
"medium": 2400,
"low": 2200,
"empty": 2200,
},
}
levels = sorted(na_battery_levels[module_type].values())
steps = [20, 50, 80, 100]
na_battery_level = na_battery_levels[module_type]
if batterylevel >= na_battery_level["full"]:
return 100
if batterylevel >= na_battery_level["high"]:
i = 3
elif batterylevel >= na_battery_level["medium"]:
i = 2
elif batterylevel >= na_battery_level["low"]:
i = 1
else:
return 0
pct = steps[i - 1] + (
(steps[i] - steps[i - 1])
* (batterylevel - levels[i])
/ (levels[i + 1] - levels[i])
)
return int(pct)
def get_all_home_ids(home_data: pyatmo.HomeData) -> list[str]:
"""Get all the home ids returned by NetAtmo API."""
if home_data is None:

View File

@ -440,6 +440,11 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
self._media_position = None
self._media_position_updated_at = None
async def async_set_favorites(self) -> None:
"""Wrap favorites update method with an async lock."""
async with self.data.topology_condition:
await self.hass.async_add_executor_job(self._set_favorites)
def _set_favorites(self) -> None:
"""Set available favorites."""
self._favorites = []
@ -741,7 +746,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
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)
self.hass.async_add_job(self.async_set_favorites)
self.async_write_ha_state()
@property

View File

@ -96,7 +96,7 @@ class SonosSpeaker:
self.hass, f"{SONOS_SEEN}-{self.soco.uid}", self.async_seen
)
if (battery_info := fetch_battery_info_or_none(self.soco)) is not None:
if battery_info := fetch_battery_info_or_none(self.soco):
# 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(

View File

@ -2,8 +2,13 @@
"domain": "version",
"name": "Version",
"documentation": "https://www.home-assistant.io/integrations/version",
"requirements": ["pyhaversion==21.3.0"],
"codeowners": ["@fabaff", "@ludeeus"],
"requirements": [
"pyhaversion==21.5.0"
],
"codeowners": [
"@fabaff",
"@ludeeus"
],
"quality_scale": "internal",
"iot_class": "local_push"
}
}

View File

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

View File

@ -119,7 +119,7 @@ def async_match_state(
@callback
def async_test_feature(state: State, feature: int, feature_name: str) -> None:
"""Test is state supports a feature."""
"""Test if state supports a feature."""
if state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) & feature == 0:
raise IntentHandleError(f"Entity {state.name} does not support {feature_name}")

View File

@ -138,7 +138,7 @@ aio_georss_gdacs==0.4
aioambient==1.2.4
# homeassistant.components.asuswrt
aioasuswrt==1.3.1
aioasuswrt==1.3.4
# homeassistant.components.azure_devops
aioazuredevops==1.3.5
@ -1437,7 +1437,7 @@ pygtfs==0.1.5
pygti==0.9.2
# homeassistant.components.version
pyhaversion==21.3.0
pyhaversion==21.5.0
# homeassistant.components.heos
pyheos==0.7.2

View File

@ -78,7 +78,7 @@ aio_georss_gdacs==0.4
aioambient==1.2.4
# homeassistant.components.asuswrt
aioasuswrt==1.3.1
aioasuswrt==1.3.4
# homeassistant.components.azure_devops
aioazuredevops==1.3.5
@ -778,7 +778,7 @@ pygatt[GATTTOOL]==4.0.5
pygti==0.9.2
# homeassistant.components.version
pyhaversion==21.3.0
pyhaversion==21.5.0
# homeassistant.components.heos
pyheos==0.7.2

View File

@ -3,10 +3,11 @@ import pytest
import homeassistant.components.automation as automation
from homeassistant.components.light import (
ATTR_SUPPORTED_COLOR_MODES,
COLOR_MODE_BRIGHTNESS,
DOMAIN,
FLASH_LONG,
FLASH_SHORT,
SUPPORT_BRIGHTNESS,
SUPPORT_FLASH,
)
from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON
@ -55,7 +56,8 @@ async def test_get_actions(hass, device_reg, entity_reg):
"test",
"5678",
device_id=device_entry.id,
supported_features=SUPPORT_BRIGHTNESS | SUPPORT_FLASH,
supported_features=SUPPORT_FLASH,
capabilities={"supported_color_modes": ["brightness"]},
)
expected_actions = [
{
@ -132,13 +134,15 @@ async def test_get_action_capabilities(hass, device_reg, entity_reg):
@pytest.mark.parametrize(
"set_state,num_actions,supported_features_reg,supported_features_state,expected_capabilities",
"set_state,num_actions,supported_features_reg,supported_features_state,capabilities_reg,attributes_state,expected_capabilities",
[
(
False,
5,
SUPPORT_BRIGHTNESS,
0,
0,
{ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_BRIGHTNESS]},
{},
{
"turn_on": [
{
@ -155,7 +159,9 @@ async def test_get_action_capabilities(hass, device_reg, entity_reg):
True,
5,
0,
SUPPORT_BRIGHTNESS,
0,
None,
{ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_BRIGHTNESS]},
{
"turn_on": [
{
@ -173,6 +179,8 @@ async def test_get_action_capabilities(hass, device_reg, entity_reg):
4,
SUPPORT_FLASH,
0,
None,
{},
{
"turn_on": [
{
@ -189,6 +197,8 @@ async def test_get_action_capabilities(hass, device_reg, entity_reg):
4,
0,
SUPPORT_FLASH,
None,
{},
{
"turn_on": [
{
@ -210,6 +220,8 @@ async def test_get_action_capabilities_features(
num_actions,
supported_features_reg,
supported_features_state,
capabilities_reg,
attributes_state,
expected_capabilities,
):
"""Test we get the expected capabilities from a light action."""
@ -225,10 +237,13 @@ async def test_get_action_capabilities_features(
"5678",
device_id=device_entry.id,
supported_features=supported_features_reg,
capabilities=capabilities_reg,
).entity_id
if set_state:
hass.states.async_set(
entity_id, None, {"supported_features": supported_features_state}
entity_id,
None,
{"supported_features": supported_features_state, **attributes_state},
)
actions = await async_get_device_automations(hass, "action", device_entry.id)

View File

@ -1,7 +1,11 @@
"""Tests for the light intents."""
from homeassistant.components import light
from homeassistant.components.light import intent
from homeassistant.const import ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_TURN_ON
from homeassistant.components.light import (
ATTR_SUPPORTED_COLOR_MODES,
COLOR_MODE_HS,
intent,
)
from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON
from homeassistant.helpers.intent import IntentHandleError
from tests.common import async_mock_service
@ -10,7 +14,7 @@ from tests.common import async_mock_service
async def test_intent_set_color(hass):
"""Test the set color intent."""
hass.states.async_set(
"light.hello_2", "off", {ATTR_SUPPORTED_FEATURES: light.SUPPORT_COLOR}
"light.hello_2", "off", {ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_HS]}
)
hass.states.async_set("switch.hello", "off")
calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON)
@ -55,9 +59,7 @@ async def test_intent_set_color_tests_feature(hass):
async def test_intent_set_color_and_brightness(hass):
"""Test the set color intent."""
hass.states.async_set(
"light.hello_2",
"off",
{ATTR_SUPPORTED_FEATURES: (light.SUPPORT_COLOR | light.SUPPORT_BRIGHTNESS)},
"light.hello_2", "off", {ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_HS]}
)
hass.states.async_set("switch.hello", "off")
calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON)

View File

@ -1,8 +1,6 @@
"""The tests for the Netatmo climate platform."""
from unittest.mock import Mock, patch
import pytest
from homeassistant.components.climate import (
DOMAIN as CLIMATE_DOMAIN,
SERVICE_SET_HVAC_MODE,
@ -21,12 +19,7 @@ from homeassistant.components.climate.const import (
PRESET_BOOST,
)
from homeassistant.components.netatmo import climate
from homeassistant.components.netatmo.climate import (
NA_THERM,
NA_VALVE,
PRESET_FROST_GUARD,
PRESET_SCHEDULE,
)
from homeassistant.components.netatmo.climate import PRESET_FROST_GUARD, PRESET_SCHEDULE
from homeassistant.components.netatmo.const import (
ATTR_SCHEDULE_NAME,
SERVICE_SET_SCHEDULE,
@ -653,28 +646,6 @@ async def test_valves_service_turn_on(hass, climate_entry):
assert hass.states.get(climate_entity_entrada).state == "auto"
@pytest.mark.parametrize(
"batterylevel, module_type, expected",
[
(4101, NA_THERM, 100),
(3601, NA_THERM, 80),
(3450, NA_THERM, 65),
(3301, NA_THERM, 50),
(3001, NA_THERM, 20),
(2799, NA_THERM, 0),
(3201, NA_VALVE, 100),
(2701, NA_VALVE, 80),
(2550, NA_VALVE, 65),
(2401, NA_VALVE, 50),
(2201, NA_VALVE, 20),
(2001, NA_VALVE, 0),
],
)
async def test_interpolate(batterylevel, module_type, expected):
"""Test interpolation of battery levels depending on device type."""
assert climate.interpolate(batterylevel, module_type) == expected
async def test_get_all_home_ids():
"""Test extracting all home ids returned by NetAtmo API."""
# Test with backend returning no data