From 5f6214ede7e0c5d5f06535ba067fed60758cb334 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 6 Feb 2022 23:17:10 +0100 Subject: [PATCH 01/24] check wan access type (#65389) --- homeassistant/components/fritz/common.py | 14 ++++++++------ homeassistant/components/fritz/sensor.py | 12 ++++++++---- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 2cd6616f134c..eab85ae40872 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -567,11 +567,11 @@ class AvmWrapper(FritzBoxTools): ) return {} - async def async_get_wan_dsl_interface_config(self) -> dict[str, Any]: - """Call WANDSLInterfaceConfig service.""" + async def async_get_wan_link_properties(self) -> dict[str, Any]: + """Call WANCommonInterfaceConfig service.""" return await self.hass.async_add_executor_job( - partial(self.get_wan_dsl_interface_config) + partial(self.get_wan_link_properties) ) async def async_get_wan_link_properties(self) -> dict[str, Any]: @@ -678,10 +678,12 @@ class AvmWrapper(FritzBoxTools): return self._service_call_action("WLANConfiguration", str(index), "GetInfo") - def get_wan_dsl_interface_config(self) -> dict[str, Any]: - """Call WANDSLInterfaceConfig service.""" + def get_wan_link_properties(self) -> dict[str, Any]: + """Call WANCommonInterfaceConfig service.""" - return self._service_call_action("WANDSLInterfaceConfig", "1", "GetInfo") + return self._service_call_action( + "WANCommonInterfaceConfig", "1", "GetCommonLinkProperties" + ) def get_wan_link_properties(self) -> dict[str, Any]: """Call WANCommonInterfaceConfig service.""" diff --git a/homeassistant/components/fritz/sensor.py b/homeassistant/components/fritz/sensor.py index 6155cdc59144..5e4b18eebcaf 100644 --- a/homeassistant/components/fritz/sensor.py +++ b/homeassistant/components/fritz/sensor.py @@ -277,10 +277,14 @@ async def async_setup_entry( _LOGGER.debug("Setting up FRITZ!Box sensors") avm_wrapper: AvmWrapper = hass.data[DOMAIN][entry.entry_id] - dsl: bool = False - dslinterface = await avm_wrapper.async_get_wan_dsl_interface_config() - if dslinterface: - dsl = dslinterface["NewEnable"] + link_properties = await avm_wrapper.async_get_wan_link_properties() + dsl: bool = link_properties.get("NewWANAccessType") == "DSL" + + _LOGGER.debug( + "WANAccessType of FritzBox %s is '%s'", + avm_wrapper.host, + link_properties.get("NewWANAccessType"), + ) entities = [ FritzBoxSensor(avm_wrapper, entry.title, description) From d754ea1645ffbc812b59c97fc0157ca8bf7f7e87 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sat, 5 Feb 2022 10:46:52 +0000 Subject: [PATCH 02/24] Fix OVO Energy NoneType error occurring for some users (#65714) --- homeassistant/components/ovo_energy/sensor.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ovo_energy/sensor.py b/homeassistant/components/ovo_energy/sensor.py index ba332a08a162..8f9a18d1f113 100644 --- a/homeassistant/components/ovo_energy/sensor.py +++ b/homeassistant/components/ovo_energy/sensor.py @@ -121,14 +121,22 @@ async def async_setup_entry( if coordinator.data: if coordinator.data.electricity: for description in SENSOR_TYPES_ELECTRICITY: - if description.key == KEY_LAST_ELECTRICITY_COST: + if ( + description.key == KEY_LAST_ELECTRICITY_COST + and coordinator.data.electricity[-1] is not None + and coordinator.data.electricity[-1].cost is not None + ): description.native_unit_of_measurement = ( coordinator.data.electricity[-1].cost.currency_unit ) entities.append(OVOEnergySensor(coordinator, description, client)) if coordinator.data.gas: for description in SENSOR_TYPES_GAS: - if description.key == KEY_LAST_GAS_COST: + if ( + description.key == KEY_LAST_GAS_COST + and coordinator.data.gas[-1] is not None + and coordinator.data.gas[-1].cost is not None + ): description.native_unit_of_measurement = coordinator.data.gas[ -1 ].cost.currency_unit From eff9690c8a108519ca274d4f975fa63ffb5df300 Mon Sep 17 00:00:00 2001 From: Sean Vig Date: Sun, 6 Feb 2022 17:14:44 -0500 Subject: [PATCH 03/24] Fix Amcrest service calls (#65717) Fixes #65522 Fixes #65647 --- homeassistant/components/amcrest/camera.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/amcrest/camera.py b/homeassistant/components/amcrest/camera.py index 6e729a8f1b57..3846f4945a98 100644 --- a/homeassistant/components/amcrest/camera.py +++ b/homeassistant/components/amcrest/camera.py @@ -515,8 +515,8 @@ class AmcrestCam(Camera): max_tries = 3 for tries in range(max_tries, 0, -1): try: - await getattr(self, f"_set_{func}")(value) - new_value = await getattr(self, f"_get_{func}")() + await getattr(self, f"_async_set_{func}")(value) + new_value = await getattr(self, f"_async_get_{func}")() if new_value != value: raise AmcrestCommandFailed except (AmcrestError, AmcrestCommandFailed) as error: From 57526bd21f968ab344f39b0aa8d4edc4ad16e985 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 5 Feb 2022 10:59:32 -0600 Subject: [PATCH 04/24] Add coverage for color_rgbww_to_rgb, fix divzero case (#65721) --- homeassistant/util/color.py | 5 ++++- tests/util/test_color.py | 43 +++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py index f055a5f32eb2..21c877f9377f 100644 --- a/homeassistant/util/color.py +++ b/homeassistant/util/color.py @@ -472,7 +472,10 @@ def color_rgbww_to_rgb( except ZeroDivisionError: ct_ratio = 0.5 color_temp_mired = min_mireds + ct_ratio * mired_range - color_temp_kelvin = color_temperature_mired_to_kelvin(color_temp_mired) + if color_temp_mired: + color_temp_kelvin = color_temperature_mired_to_kelvin(color_temp_mired) + else: + color_temp_kelvin = 0 w_r, w_g, w_b = color_temperature_to_rgb(color_temp_kelvin) white_level = max(cw, ww) / 255 diff --git a/tests/util/test_color.py b/tests/util/test_color.py index 0b1b8f7d17f4..eff71ddef4e3 100644 --- a/tests/util/test_color.py +++ b/tests/util/test_color.py @@ -405,6 +405,49 @@ def test_color_rgb_to_rgbww(): assert color_util.color_rgb_to_rgbww(255, 255, 255, 1, 5) == (103, 69, 0, 255, 255) +def test_color_rgbww_to_rgb(): + """Test color_rgbww_to_rgb conversions.""" + assert color_util.color_rgbww_to_rgb(0, 54, 98, 255, 255, 154, 370) == ( + 255, + 255, + 255, + ) + assert color_util.color_rgbww_to_rgb(255, 255, 255, 0, 0, 154, 370) == ( + 255, + 255, + 255, + ) + assert color_util.color_rgbww_to_rgb(0, 118, 241, 255, 255, 154, 370) == ( + 163, + 204, + 255, + ) + assert color_util.color_rgbww_to_rgb(0, 27, 49, 128, 128, 154, 370) == ( + 128, + 128, + 128, + ) + assert color_util.color_rgbww_to_rgb(0, 14, 25, 64, 64, 154, 370) == (64, 64, 64) + assert color_util.color_rgbww_to_rgb(9, 64, 0, 38, 38, 154, 370) == (32, 64, 16) + assert color_util.color_rgbww_to_rgb(0, 0, 0, 0, 0, 154, 370) == (0, 0, 0) + assert color_util.color_rgbww_to_rgb(103, 69, 0, 255, 255, 153, 370) == ( + 255, + 193, + 112, + ) + assert color_util.color_rgbww_to_rgb(255, 255, 255, 0, 0, 0, 0) == (255, 255, 255) + assert color_util.color_rgbww_to_rgb(255, 255, 255, 255, 255, 0, 0) == ( + 255, + 161, + 128, + ) + assert color_util.color_rgbww_to_rgb(255, 255, 255, 255, 255, 0, 370) == ( + 255, + 245, + 237, + ) + + def test_color_temperature_to_rgbww(): """Test color temp to warm, cold conversion. From 96952359207ee3e62ac0717c8d93878c3b3600a7 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 6 Feb 2022 23:13:05 +0100 Subject: [PATCH 05/24] Fix wind speed unit (#65723) --- homeassistant/components/accuweather/weather.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/accuweather/weather.py b/homeassistant/components/accuweather/weather.py index 00726f6db381..4ab9342de628 100644 --- a/homeassistant/components/accuweather/weather.py +++ b/homeassistant/components/accuweather/weather.py @@ -17,7 +17,12 @@ from homeassistant.components.weather import ( WeatherEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.const import ( + CONF_NAME, + SPEED_MILES_PER_HOUR, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo @@ -62,9 +67,13 @@ class AccuWeatherEntity(CoordinatorEntity, WeatherEntity): """Initialize.""" super().__init__(coordinator) self._unit_system = API_METRIC if coordinator.is_metric else API_IMPERIAL - self._attr_wind_speed_unit = self.coordinator.data["Wind"]["Speed"][ - self._unit_system - ]["Unit"] + wind_speed_unit = self.coordinator.data["Wind"]["Speed"][self._unit_system][ + "Unit" + ] + if wind_speed_unit == "mi/h": + self._attr_wind_speed_unit = SPEED_MILES_PER_HOUR + else: + self._attr_wind_speed_unit = wind_speed_unit self._attr_name = name self._attr_unique_id = coordinator.location_key self._attr_temperature_unit = ( From 058420bb2fd427a046f779bc0242d8d6d3c788b6 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 5 Feb 2022 00:39:01 -0700 Subject: [PATCH 06/24] Bump simplisafe-python to 2022.02.0 (#65748) --- homeassistant/components/simplisafe/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index fa343e7466af..62b5b9aa7b77 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -3,7 +3,7 @@ "name": "SimpliSafe", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/simplisafe", - "requirements": ["simplisafe-python==2022.01.0"], + "requirements": ["simplisafe-python==2022.02.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index e4ed0d863196..4fe479bcc648 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2190,7 +2190,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==2022.01.0 +simplisafe-python==2022.02.0 # homeassistant.components.sisyphus sisyphus-control==3.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cf75245b5dba..569162cd3578 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1337,7 +1337,7 @@ sharkiqpy==0.1.8 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==2022.01.0 +simplisafe-python==2022.02.0 # homeassistant.components.slack slackclient==2.5.0 From fc7ea6e1b365c81719620ae3218f66aa9110c46f Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Sun, 6 Feb 2022 23:15:50 +0100 Subject: [PATCH 07/24] Improve androidtv mac address handling and test coverage (#65749) * Better mac addr handling and improve test coverage * Apply suggested changes * Apply more suggested changes --- .../components/androidtv/__init__.py | 15 ++ .../components/androidtv/config_flow.py | 7 +- .../components/androidtv/media_player.py | 7 +- tests/components/androidtv/patchers.py | 26 ++-- .../components/androidtv/test_config_flow.py | 24 +++- .../components/androidtv/test_media_player.py | 133 ++++++------------ 6 files changed, 94 insertions(+), 118 deletions(-) diff --git a/homeassistant/components/androidtv/__init__.py b/homeassistant/components/androidtv/__init__.py index 81d4a3f06458..9b9683856028 100644 --- a/homeassistant/components/androidtv/__init__.py +++ b/homeassistant/components/androidtv/__init__.py @@ -18,6 +18,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.storage import STORAGE_DIR from homeassistant.helpers.typing import ConfigType @@ -33,16 +34,30 @@ from .const import ( DEVICE_ANDROIDTV, DEVICE_FIRETV, DOMAIN, + PROP_ETHMAC, PROP_SERIALNO, + PROP_WIFIMAC, SIGNAL_CONFIG_ENTITY, ) PLATFORMS = [Platform.MEDIA_PLAYER] RELOAD_OPTIONS = [CONF_STATE_DETECTION_RULES] +_INVALID_MACS = {"ff:ff:ff:ff:ff:ff"} + _LOGGER = logging.getLogger(__name__) +def get_androidtv_mac(dev_props): + """Return formatted mac from device properties.""" + for prop_mac in (PROP_ETHMAC, PROP_WIFIMAC): + if if_mac := dev_props.get(prop_mac): + mac = format_mac(if_mac) + if mac not in _INVALID_MACS: + return mac + return None + + def _setup_androidtv(hass, config): """Generate an ADB key (if needed) and load it.""" adbkey = config.get(CONF_ADBKEY, hass.config.path(STORAGE_DIR, "androidtv_adbkey")) diff --git a/homeassistant/components/androidtv/config_flow.py b/homeassistant/components/androidtv/config_flow.py index 0ec37fdeb6fa..8f0efc347992 100644 --- a/homeassistant/components/androidtv/config_flow.py +++ b/homeassistant/components/androidtv/config_flow.py @@ -11,9 +11,8 @@ from homeassistant import config_entries from homeassistant.const import CONF_DEVICE_CLASS, CONF_HOST, CONF_NAME, CONF_PORT from homeassistant.core import callback from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.device_registry import format_mac -from . import async_connect_androidtv +from . import async_connect_androidtv, get_androidtv_mac from .const import ( CONF_ADB_SERVER_IP, CONF_ADB_SERVER_PORT, @@ -132,9 +131,7 @@ class AndroidTVFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): PROP_WIFIMAC, dev_prop.get(PROP_WIFIMAC), ) - unique_id = format_mac( - dev_prop.get(PROP_ETHMAC) or dev_prop.get(PROP_WIFIMAC, "") - ) + unique_id = get_androidtv_mac(dev_prop) await aftv.adb_close() return None, unique_id diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index 03b1e6799618..1ab592143c62 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -51,12 +51,13 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from . import get_androidtv_mac from .const import ( ANDROID_DEV, ANDROID_DEV_OPT, @@ -80,8 +81,6 @@ from .const import ( DEVICE_ANDROIDTV, DEVICE_CLASSES, DOMAIN, - PROP_ETHMAC, - PROP_WIFIMAC, SIGNAL_CONFIG_ENTITY, ) @@ -343,7 +342,7 @@ class ADBDevice(MediaPlayerEntity): self._attr_device_info[ATTR_MANUFACTURER] = manufacturer if sw_version := info.get(ATTR_SW_VERSION): self._attr_device_info[ATTR_SW_VERSION] = sw_version - if mac := format_mac(info.get(PROP_ETHMAC) or info.get(PROP_WIFIMAC, "")): + if mac := get_androidtv_mac(info): self._attr_device_info[ATTR_CONNECTIONS] = {(CONNECTION_NETWORK_MAC, mac)} self._app_id_to_name = {} diff --git a/tests/components/androidtv/patchers.py b/tests/components/androidtv/patchers.py index 4411945c71b1..7cc14bbd7b59 100644 --- a/tests/components/androidtv/patchers.py +++ b/tests/components/androidtv/patchers.py @@ -1,13 +1,17 @@ """Define patches used for androidtv tests.""" - from unittest.mock import mock_open, patch +from androidtv.constants import CMD_DEVICE_PROPERTIES, CMD_MAC_ETH0, CMD_MAC_WLAN0 + KEY_PYTHON = "python" KEY_SERVER = "server" ADB_DEVICE_TCP_ASYNC_FAKE = "AdbDeviceTcpAsyncFake" DEVICE_ASYNC_FAKE = "DeviceAsyncFake" +PROPS_DEV_INFO = "fake\nfake\n0123456\nfake" +PROPS_DEV_MAC = "ether ab:cd:ef:gh:ij:kl brd" + class AdbDeviceTcpAsyncFake: """A fake of the `adb_shell.adb_device_async.AdbDeviceTcpAsync` class.""" @@ -100,12 +104,18 @@ def patch_connect(success): } -def patch_shell(response=None, error=False): +def patch_shell(response=None, error=False, mac_eth=False): """Mock the `AdbDeviceTcpAsyncFake.shell` and `DeviceAsyncFake.shell` methods.""" async def shell_success(self, cmd, *args, **kwargs): """Mock the `AdbDeviceTcpAsyncFake.shell` and `DeviceAsyncFake.shell` methods when they are successful.""" self.shell_cmd = cmd + if cmd == CMD_DEVICE_PROPERTIES: + return PROPS_DEV_INFO + if cmd == CMD_MAC_WLAN0: + return PROPS_DEV_MAC + if cmd == CMD_MAC_ETH0: + return PROPS_DEV_MAC if mac_eth else None return response async def shell_fail_python(self, cmd, *args, **kwargs): @@ -185,15 +195,3 @@ PATCH_ANDROIDTV_UPDATE_EXCEPTION = patch( "androidtv.androidtv.androidtv_async.AndroidTVAsync.update", side_effect=ZeroDivisionError, ) - -PATCH_DEVICE_PROPERTIES = patch( - "androidtv.basetv.basetv_async.BaseTVAsync.get_device_properties", - return_value={ - "manufacturer": "a", - "model": "b", - "serialno": "c", - "sw_version": "d", - "wifimac": "ab:cd:ef:gh:ij:kl", - "ethmac": None, - }, -) diff --git a/tests/components/androidtv/test_config_flow.py b/tests/components/androidtv/test_config_flow.py index 757be8f6d8d9..991d3757749f 100644 --- a/tests/components/androidtv/test_config_flow.py +++ b/tests/components/androidtv/test_config_flow.py @@ -31,6 +31,7 @@ from homeassistant.components.androidtv.const import ( DEFAULT_PORT, DOMAIN, PROP_ETHMAC, + PROP_WIFIMAC, ) from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER @@ -42,6 +43,7 @@ from tests.components.androidtv.patchers import isfile ADBKEY = "adbkey" ETH_MAC = "a1:b1:c1:d1:e1:f1" +WIFI_MAC = "a2:b2:c2:d2:e2:f2" HOST = "127.0.0.1" VALID_DETECT_RULE = [{"paused": {"media_session_state": 3}}] @@ -84,18 +86,28 @@ PATCH_SETUP_ENTRY = patch( class MockConfigDevice: """Mock class to emulate Android TV device.""" - def __init__(self, eth_mac=ETH_MAC): + def __init__(self, eth_mac=ETH_MAC, wifi_mac=None): """Initialize a fake device to test config flow.""" self.available = True - self.device_properties = {PROP_ETHMAC: eth_mac} + self.device_properties = {PROP_ETHMAC: eth_mac, PROP_WIFIMAC: wifi_mac} async def adb_close(self): """Fake method to close connection.""" self.available = False -@pytest.mark.parametrize("config", [CONFIG_PYTHON_ADB, CONFIG_ADB_SERVER]) -async def test_user(hass, config): +@pytest.mark.parametrize( + ["config", "eth_mac", "wifi_mac"], + [ + (CONFIG_PYTHON_ADB, ETH_MAC, None), + (CONFIG_ADB_SERVER, ETH_MAC, None), + (CONFIG_PYTHON_ADB, None, WIFI_MAC), + (CONFIG_ADB_SERVER, None, WIFI_MAC), + (CONFIG_PYTHON_ADB, ETH_MAC, WIFI_MAC), + (CONFIG_ADB_SERVER, ETH_MAC, WIFI_MAC), + ], +) +async def test_user(hass, config, eth_mac, wifi_mac): """Test user config.""" flow_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER, "show_advanced_options": True} @@ -106,7 +118,7 @@ async def test_user(hass, config): # test with all provided with patch( CONNECT_METHOD, - return_value=(MockConfigDevice(), None), + return_value=(MockConfigDevice(eth_mac, wifi_mac), None), ), PATCH_SETUP_ENTRY as mock_setup_entry, PATCH_GET_HOST_IP: result = await hass.config_entries.flow.async_configure( flow_result["flow_id"], user_input=config @@ -273,7 +285,7 @@ async def test_invalid_serial(hass): """Test for invalid serialno.""" with patch( CONNECT_METHOD, - return_value=(MockConfigDevice(eth_mac=""), None), + return_value=(MockConfigDevice(eth_mac=None), None), ), PATCH_GET_HOST_IP: result = await hass.config_entries.flow.async_init( DOMAIN, diff --git a/tests/components/androidtv/test_media_player.py b/tests/components/androidtv/test_media_player.py index e97de0fc928d..a0bab1736ff0 100644 --- a/tests/components/androidtv/test_media_player.py +++ b/tests/components/androidtv/test_media_player.py @@ -142,29 +142,6 @@ def _setup(config): return patch_key, entity_id, config_entry -async def test_setup_with_properties(hass): - """Test that setup succeeds with device properties. - - the response must be a string with the following info separated with line break: - "manufacturer, model, serialno, version, mac_wlan0_output, mac_eth0_output" - - """ - - patch_key, entity_id, config_entry = _setup(CONFIG_ANDROIDTV_ADB_SERVER) - config_entry.add_to_hass(hass) - response = "fake\nfake\n0123456\nfake\nether a1:b1:c1:d1:e1:f1 brd\nnone" - - with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ - patch_key - ], patchers.patch_shell(response)[patch_key]: - with patchers.PATCH_DEVICE_PROPERTIES: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - state = hass.states.get(entity_id) - assert state is not None - - @pytest.mark.parametrize( "config", [ @@ -190,9 +167,8 @@ async def test_reconnect(hass, caplog, config): ], patchers.patch_shell(SHELL_RESPONSE_OFF)[ patch_key ], patchers.PATCH_KEYGEN, patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: - with patchers.PATCH_DEVICE_PROPERTIES: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) @@ -259,9 +235,8 @@ async def test_adb_shell_returns_none(hass, config): ], patchers.patch_shell(SHELL_RESPONSE_OFF)[ patch_key ], patchers.PATCH_KEYGEN, patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: - with patchers.PATCH_DEVICE_PROPERTIES: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) @@ -289,9 +264,8 @@ async def test_setup_with_adbkey(hass): ], patchers.patch_shell(SHELL_RESPONSE_OFF)[ patch_key ], patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER, PATCH_ISFILE, PATCH_ACCESS: - with patchers.PATCH_DEVICE_PROPERTIES: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) @@ -324,9 +298,8 @@ async def test_sources(hass, config0): with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - with patchers.PATCH_DEVICE_PROPERTIES: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) @@ -404,9 +377,8 @@ async def _test_exclude_sources(hass, config0, expected_sources): with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - with patchers.PATCH_DEVICE_PROPERTIES: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) @@ -486,9 +458,8 @@ async def _test_select_source(hass, config0, source, expected_arg, method_patch) with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - with patchers.PATCH_DEVICE_PROPERTIES: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) @@ -714,9 +685,8 @@ async def test_setup_fail(hass, config): ], patchers.patch_shell(SHELL_RESPONSE_OFF)[ patch_key ], patchers.PATCH_KEYGEN, patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: - with patchers.PATCH_DEVICE_PROPERTIES: - assert await hass.config_entries.async_setup(config_entry.entry_id) is False - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(config_entry.entry_id) is False + await hass.async_block_till_done() await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) @@ -733,9 +703,8 @@ async def test_adb_command(hass): with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - with patchers.PATCH_DEVICE_PROPERTIES: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() with patch( "androidtv.basetv.basetv_async.BaseTVAsync.adb_shell", return_value=response @@ -763,9 +732,8 @@ async def test_adb_command_unicode_decode_error(hass): with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - with patchers.PATCH_DEVICE_PROPERTIES: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() with patch( "androidtv.basetv.basetv_async.BaseTVAsync.adb_shell", @@ -793,9 +761,8 @@ async def test_adb_command_key(hass): with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - with patchers.PATCH_DEVICE_PROPERTIES: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() with patch( "androidtv.basetv.basetv_async.BaseTVAsync.adb_shell", return_value=response @@ -823,9 +790,8 @@ async def test_adb_command_get_properties(hass): with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - with patchers.PATCH_DEVICE_PROPERTIES: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() with patch( "androidtv.androidtv.androidtv_async.AndroidTVAsync.get_properties_dict", @@ -853,9 +819,8 @@ async def test_learn_sendevent(hass): with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - with patchers.PATCH_DEVICE_PROPERTIES: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() with patch( "androidtv.basetv.basetv_async.BaseTVAsync.learn_sendevent", @@ -882,9 +847,8 @@ async def test_update_lock_not_acquired(hass): with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - with patchers.PATCH_DEVICE_PROPERTIES: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() with patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: await hass.helpers.entity_component.async_update_entity(entity_id) @@ -918,9 +882,8 @@ async def test_download(hass): with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - with patchers.PATCH_DEVICE_PROPERTIES: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() # Failed download because path is not whitelisted with patch("androidtv.basetv.basetv_async.BaseTVAsync.adb_pull") as patch_pull: @@ -965,9 +928,8 @@ async def test_upload(hass): with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - with patchers.PATCH_DEVICE_PROPERTIES: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() # Failed upload because path is not whitelisted with patch("androidtv.basetv.basetv_async.BaseTVAsync.adb_push") as patch_push: @@ -1010,9 +972,8 @@ async def test_androidtv_volume_set(hass): with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - with patchers.PATCH_DEVICE_PROPERTIES: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() with patch( "androidtv.basetv.basetv_async.BaseTVAsync.set_volume_level", return_value=0.5 @@ -1038,9 +999,8 @@ async def test_get_image(hass, hass_ws_client): with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - with patchers.PATCH_DEVICE_PROPERTIES: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() with patchers.patch_shell("11")[patch_key]: await hass.helpers.entity_component.async_update_entity(entity_id) @@ -1115,9 +1075,8 @@ async def test_services_androidtv(hass): with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[patch_key]: with patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - with patchers.PATCH_DEVICE_PROPERTIES: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() with patchers.patch_shell(SHELL_RESPONSE_STANDBY)[patch_key]: await _test_service( @@ -1162,9 +1121,8 @@ async def test_services_firetv(hass): with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[patch_key]: with patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - with patchers.PATCH_DEVICE_PROPERTIES: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() with patchers.patch_shell(SHELL_RESPONSE_STANDBY)[patch_key]: await _test_service(hass, entity_id, SERVICE_MEDIA_STOP, "back") @@ -1179,9 +1137,8 @@ async def test_volume_mute(hass): with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[patch_key]: with patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - with patchers.PATCH_DEVICE_PROPERTIES: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() with patchers.patch_shell(SHELL_RESPONSE_STANDBY)[patch_key]: service_data = {ATTR_ENTITY_ID: entity_id, ATTR_MEDIA_VOLUME_MUTED: True} @@ -1224,9 +1181,8 @@ async def test_connection_closed_on_ha_stop(hass): with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - with patchers.PATCH_DEVICE_PROPERTIES: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() with patch( "androidtv.androidtv.androidtv_async.AndroidTVAsync.adb_close" @@ -1249,9 +1205,8 @@ async def test_exception(hass): ], patchers.patch_shell(SHELL_RESPONSE_OFF)[ patch_key ], patchers.PATCH_KEYGEN, patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: - with patchers.PATCH_DEVICE_PROPERTIES: - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) From bccfe6646ee0aabbab166b44124bb7707971e98e Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 5 Feb 2022 00:41:12 -0700 Subject: [PATCH 08/24] Add redacted subscription data to SimpliSafe diagnostics (#65751) --- .../components/simplisafe/__init__.py | 1 + .../components/simplisafe/diagnostics.py | 15 +++ tests/components/simplisafe/conftest.py | 3 +- .../components/simplisafe/test_diagnostics.py | 93 ++++++++++++++++++- 4 files changed, 109 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index a133ec6c2dcd..2fd7e10a2f84 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -482,6 +482,7 @@ class SimpliSafe: self._websocket_reconnect_task: asyncio.Task | None = None self.entry = entry self.initial_event_to_use: dict[int, dict[str, Any]] = {} + self.subscription_data: dict[int, Any] = api.subscription_data self.systems: dict[int, SystemType] = {} # This will get filled in by async_init: diff --git a/homeassistant/components/simplisafe/diagnostics.py b/homeassistant/components/simplisafe/diagnostics.py index bc0dddef47c7..c7c03467c94b 100644 --- a/homeassistant/components/simplisafe/diagnostics.py +++ b/homeassistant/components/simplisafe/diagnostics.py @@ -11,14 +11,28 @@ from homeassistant.core import HomeAssistant from . import SimpliSafe from .const import DOMAIN +CONF_CREDIT_CARD = "creditCard" +CONF_EXPIRES = "expires" +CONF_LOCATION = "location" +CONF_LOCATION_NAME = "locationName" +CONF_PAYMENT_PROFILE_ID = "paymentProfileId" CONF_SERIAL = "serial" +CONF_SID = "sid" CONF_SYSTEM_ID = "system_id" +CONF_UID = "uid" CONF_WIFI_SSID = "wifi_ssid" TO_REDACT = { CONF_ADDRESS, + CONF_CREDIT_CARD, + CONF_EXPIRES, + CONF_LOCATION, + CONF_LOCATION_NAME, + CONF_PAYMENT_PROFILE_ID, CONF_SERIAL, + CONF_SID, CONF_SYSTEM_ID, + CONF_UID, CONF_WIFI_SSID, } @@ -34,6 +48,7 @@ async def async_get_config_entry_diagnostics( "entry": { "options": dict(entry.options), }, + "subscription_data": simplisafe.subscription_data, "systems": [system.as_dict() for system in simplisafe.systems.values()], }, TO_REDACT, diff --git a/tests/components/simplisafe/conftest.py b/tests/components/simplisafe/conftest.py index d9e6d46c2eb1..d4517717434c 100644 --- a/tests/components/simplisafe/conftest.py +++ b/tests/components/simplisafe/conftest.py @@ -18,11 +18,12 @@ USER_ID = "12345" @pytest.fixture(name="api") -def api_fixture(system_v3, websocket): +def api_fixture(data_subscription, system_v3, websocket): """Define a fixture for a simplisafe-python API object.""" return Mock( async_get_systems=AsyncMock(return_value={SYSTEM_ID: system_v3}), refresh_token=REFRESH_TOKEN, + subscription_data=data_subscription, user_id=USER_ID, websocket=websocket, ) diff --git a/tests/components/simplisafe/test_diagnostics.py b/tests/components/simplisafe/test_diagnostics.py index d2c2866bf5b7..13d5c778e89e 100644 --- a/tests/components/simplisafe/test_diagnostics.py +++ b/tests/components/simplisafe/test_diagnostics.py @@ -7,7 +7,96 @@ from tests.components.diagnostics import get_diagnostics_for_config_entry async def test_entry_diagnostics(hass, config_entry, hass_client, setup_simplisafe): """Test config entry diagnostics.""" assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { - "entry": {"options": {}}, + "entry": { + "options": {}, + }, + "subscription_data": { + "system_123": { + "uid": REDACTED, + "sid": REDACTED, + "sStatus": 20, + "activated": 1445034752, + "planSku": "SSEDSM2", + "planName": "Interactive Monitoring", + "price": 24.99, + "currency": "USD", + "country": "US", + "expires": REDACTED, + "canceled": 0, + "extraTime": 0, + "creditCard": REDACTED, + "time": 2628000, + "paymentProfileId": REDACTED, + "features": { + "monitoring": True, + "alerts": True, + "online": True, + "hazard": True, + "video": True, + "cameras": 10, + "dispatch": True, + "proInstall": False, + "discount": 0, + "vipCS": False, + "medical": True, + "careVisit": False, + "storageDays": 30, + }, + "status": { + "hasBaseStation": True, + "isActive": True, + "monitoring": "Active", + }, + "subscriptionFeatures": { + "monitoredSensorsTypes": [ + "Entry", + "Motion", + "GlassBreak", + "Smoke", + "CO", + "Freeze", + "Water", + ], + "monitoredPanicConditions": ["Fire", "Medical", "Duress"], + "dispatchTypes": ["Police", "Fire", "Medical", "Guard"], + "remoteControl": [ + "ArmDisarm", + "LockUnlock", + "ViewSettings", + "ConfigureSettings", + ], + "cameraFeatures": { + "liveView": True, + "maxRecordingCameras": 10, + "recordingStorageDays": 30, + "videoVerification": True, + }, + "support": { + "level": "Basic", + "annualVisit": False, + "professionalInstall": False, + }, + "cellCommunicationBackup": True, + "alertChannels": ["Push", "SMS", "Email"], + "alertTypes": ["Alarm", "Error", "Activity", "Camera"], + "alarmModes": ["Alarm", "SecretAlert", "Disabled"], + "supportedIntegrations": [ + "GoogleAssistant", + "AmazonAlexa", + "AugustLock", + ], + "timeline": {}, + }, + "dispatcher": "cops", + "dcid": 0, + "location": REDACTED, + "pinUnlocked": True, + "billDate": 1602887552, + "billInterval": 2628000, + "pinUnlockedBy": "pin", + "autoActivation": None, + } + }, "systems": [ { "address": REDACTED, @@ -183,7 +272,7 @@ async def test_entry_diagnostics(hass, config_entry, hass_client, setup_simplisa "shutter_open_when_off": False, "status": "online", "subscription_enabled": True, - }, + } ], "chime_volume": 2, "entry_delay_away": 30, From 5786f68bb71754356a37481ac178f83fdd2a00d8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 5 Feb 2022 10:59:22 -0600 Subject: [PATCH 09/24] Prevent multiple dhcp flows from being started for the same device/domain (#65753) --- homeassistant/components/dhcp/__init__.py | 6 +++++ tests/components/dhcp/test_init.py | 27 +++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/homeassistant/components/dhcp/__init__.py b/homeassistant/components/dhcp/__init__.py index 4310f8f2caf5..dd247c4cab9e 100644 --- a/homeassistant/components/dhcp/__init__.py +++ b/homeassistant/components/dhcp/__init__.py @@ -179,6 +179,7 @@ class WatcherBase: lowercase_hostname, ) + matched_domains = set() for entry in self._integration_matchers: if MAC_ADDRESS in entry and not fnmatch.fnmatch( uppercase_mac, entry[MAC_ADDRESS] @@ -191,6 +192,11 @@ class WatcherBase: continue _LOGGER.debug("Matched %s against %s", data, entry) + if entry["domain"] in matched_domains: + # Only match once per domain + continue + + matched_domains.add(entry["domain"]) discovery_flow.async_create_flow( self.hass, entry["domain"], diff --git a/tests/components/dhcp/test_init.py b/tests/components/dhcp/test_init.py index fb3387aeab66..0956230d7871 100644 --- a/tests/components/dhcp/test_init.py +++ b/tests/components/dhcp/test_init.py @@ -255,6 +255,33 @@ async def test_dhcp_match_macaddress(hass): ) +async def test_dhcp_multiple_match_only_one_flow(hass): + """Test matching the domain multiple times only generates one flow.""" + integration_matchers = [ + {"domain": "mock-domain", "macaddress": "B8B7F1*"}, + {"domain": "mock-domain", "hostname": "connect"}, + ] + + packet = Ether(RAW_DHCP_REQUEST) + + async_handle_dhcp_packet = await _async_get_handle_dhcp_packet( + hass, integration_matchers + ) + with patch.object(hass.config_entries.flow, "async_init") as mock_init: + await async_handle_dhcp_packet(packet) + + assert len(mock_init.mock_calls) == 1 + assert mock_init.mock_calls[0][1][0] == "mock-domain" + assert mock_init.mock_calls[0][2]["context"] == { + "source": config_entries.SOURCE_DHCP + } + assert mock_init.mock_calls[0][2]["data"] == dhcp.DhcpServiceInfo( + ip="192.168.210.56", + hostname="connect", + macaddress="b8b7f16db533", + ) + + async def test_dhcp_match_macaddress_without_hostname(hass): """Test matching based on macaddress only.""" integration_matchers = [{"domain": "mock-domain", "macaddress": "606BBD*"}] From 7a7f9deb89034f3ab475ddee4894ebded523236f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 5 Feb 2022 14:19:24 +0100 Subject: [PATCH 10/24] Update Pillow to 9.0.1 (#65779) --- homeassistant/components/doods/manifest.json | 2 +- homeassistant/components/image/manifest.json | 2 +- homeassistant/components/proxy/manifest.json | 2 +- homeassistant/components/qrcode/manifest.json | 2 +- homeassistant/components/seven_segments/manifest.json | 2 +- homeassistant/components/sighthound/manifest.json | 2 +- homeassistant/components/tensorflow/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/doods/manifest.json b/homeassistant/components/doods/manifest.json index 3957b2573649..86ad7ae4a90c 100644 --- a/homeassistant/components/doods/manifest.json +++ b/homeassistant/components/doods/manifest.json @@ -2,7 +2,7 @@ "domain": "doods", "name": "DOODS - Dedicated Open Object Detection Service", "documentation": "https://www.home-assistant.io/integrations/doods", - "requirements": ["pydoods==1.0.2", "pillow==9.0.0"], + "requirements": ["pydoods==1.0.2", "pillow==9.0.1"], "codeowners": [], "iot_class": "local_polling" } diff --git a/homeassistant/components/image/manifest.json b/homeassistant/components/image/manifest.json index 5f624ea8e1cb..2363b124e432 100644 --- a/homeassistant/components/image/manifest.json +++ b/homeassistant/components/image/manifest.json @@ -3,7 +3,7 @@ "name": "Image", "config_flow": false, "documentation": "https://www.home-assistant.io/integrations/image", - "requirements": ["pillow==9.0.0"], + "requirements": ["pillow==9.0.1"], "dependencies": ["http"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal" diff --git a/homeassistant/components/proxy/manifest.json b/homeassistant/components/proxy/manifest.json index 4f19e6afae2a..d1be59ebc870 100644 --- a/homeassistant/components/proxy/manifest.json +++ b/homeassistant/components/proxy/manifest.json @@ -2,6 +2,6 @@ "domain": "proxy", "name": "Camera Proxy", "documentation": "https://www.home-assistant.io/integrations/proxy", - "requirements": ["pillow==9.0.0"], + "requirements": ["pillow==9.0.1"], "codeowners": [] } diff --git a/homeassistant/components/qrcode/manifest.json b/homeassistant/components/qrcode/manifest.json index 63eca334d7b3..37697f2af830 100644 --- a/homeassistant/components/qrcode/manifest.json +++ b/homeassistant/components/qrcode/manifest.json @@ -2,7 +2,7 @@ "domain": "qrcode", "name": "QR Code", "documentation": "https://www.home-assistant.io/integrations/qrcode", - "requirements": ["pillow==9.0.0", "pyzbar==0.1.7"], + "requirements": ["pillow==9.0.1", "pyzbar==0.1.7"], "codeowners": [], "iot_class": "calculated" } diff --git a/homeassistant/components/seven_segments/manifest.json b/homeassistant/components/seven_segments/manifest.json index a49a471038ce..db8e57673b1a 100644 --- a/homeassistant/components/seven_segments/manifest.json +++ b/homeassistant/components/seven_segments/manifest.json @@ -2,7 +2,7 @@ "domain": "seven_segments", "name": "Seven Segments OCR", "documentation": "https://www.home-assistant.io/integrations/seven_segments", - "requirements": ["pillow==9.0.0"], + "requirements": ["pillow==9.0.1"], "codeowners": ["@fabaff"], "iot_class": "local_polling" } diff --git a/homeassistant/components/sighthound/manifest.json b/homeassistant/components/sighthound/manifest.json index def1359b1eed..8edef306d8d2 100644 --- a/homeassistant/components/sighthound/manifest.json +++ b/homeassistant/components/sighthound/manifest.json @@ -2,7 +2,7 @@ "domain": "sighthound", "name": "Sighthound", "documentation": "https://www.home-assistant.io/integrations/sighthound", - "requirements": ["pillow==9.0.0", "simplehound==0.3"], + "requirements": ["pillow==9.0.1", "simplehound==0.3"], "codeowners": ["@robmarkcole"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index 26b7421ef441..b9b0ff6b5c5b 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -7,7 +7,7 @@ "tf-models-official==2.5.0", "pycocotools==2.0.1", "numpy==1.21.4", - "pillow==9.0.0" + "pillow==9.0.1" ], "codeowners": [], "iot_class": "local_polling" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3f0b9516eade..5cded6a179d9 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -20,7 +20,7 @@ httpx==0.21.3 ifaddr==0.1.7 jinja2==3.0.3 paho-mqtt==1.6.1 -pillow==9.0.0 +pillow==9.0.1 pip>=8.0.3,<20.3 pyserial==3.5 python-slugify==4.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index 4fe479bcc648..84311dca3f1f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1249,7 +1249,7 @@ pilight==0.1.1 # homeassistant.components.seven_segments # homeassistant.components.sighthound # homeassistant.components.tensorflow -pillow==9.0.0 +pillow==9.0.1 # homeassistant.components.dominos pizzapi==0.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 569162cd3578..760dc13b2524 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -771,7 +771,7 @@ pilight==0.1.1 # homeassistant.components.seven_segments # homeassistant.components.sighthound # homeassistant.components.tensorflow -pillow==9.0.0 +pillow==9.0.1 # homeassistant.components.plex plexapi==4.9.1 From 4ba494f5cd5a8613d1a5e8168fad2864d4829001 Mon Sep 17 00:00:00 2001 From: Ferdinand <96470489+Smitplaza@users.noreply.github.com> Date: Sat, 5 Feb 2022 17:12:17 +0100 Subject: [PATCH 11/24] Fix the restart when the saj device is down (#65796) --- homeassistant/components/saj/sensor.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/saj/sensor.py b/homeassistant/components/saj/sensor.py index 670455d73549..2fb3729d0a8a 100644 --- a/homeassistant/components/saj/sensor.py +++ b/homeassistant/components/saj/sensor.py @@ -20,7 +20,6 @@ from homeassistant.const import ( CONF_TYPE, CONF_USERNAME, ENERGY_KILO_WATT_HOUR, - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, MASS_KILOGRAMS, POWER_WATT, @@ -33,6 +32,7 @@ from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.start import async_at_start from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType _LOGGER = logging.getLogger(__name__) @@ -131,17 +131,19 @@ async def async_setup_platform( return values + @callback def start_update_interval(event): """Start the update interval scheduling.""" nonlocal remove_interval_update remove_interval_update = async_track_time_interval_backoff(hass, async_saj) + @callback def stop_update_interval(event): """Properly cancel the scheduled update.""" remove_interval_update() # pylint: disable=not-callable - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_update_interval) hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, stop_update_interval) + async_at_start(hass, start_update_interval) @callback From a4d59aa599d44e9ec2fb1d597b2971c64b36268f Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sat, 5 Feb 2022 17:44:05 +0200 Subject: [PATCH 12/24] Bump aioshelly to 1.0.9 (#65803) --- homeassistant/components/shelly/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index 568f2b878aec..1d2697056527 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -3,7 +3,7 @@ "name": "Shelly", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/shelly", - "requirements": ["aioshelly==1.0.8"], + "requirements": ["aioshelly==1.0.9"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 84311dca3f1f..8ce4927f3afc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -254,7 +254,7 @@ aioridwell==2021.12.2 aiosenseme==0.6.1 # homeassistant.components.shelly -aioshelly==1.0.8 +aioshelly==1.0.9 # homeassistant.components.steamist aiosteamist==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 760dc13b2524..fb05ad28c595 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -189,7 +189,7 @@ aioridwell==2021.12.2 aiosenseme==0.6.1 # homeassistant.components.shelly -aioshelly==1.0.8 +aioshelly==1.0.9 # homeassistant.components.steamist aiosteamist==0.3.1 From 619a52a387210a67fa1f0d9136e681feffd05a84 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 6 Feb 2022 14:14:25 -0800 Subject: [PATCH 13/24] Fix legacy nest diagnostics to return empty rather than fail (#65824) Fix legacy nest diangostics to return gracefully, rather than a TypError by checking explicitiy for SDM in the config entry. Update diagnostics to use the common nest test fixture, and extend with support for the legacy nest config. Use the sdm test fixture in the existing legacy tests so they all share the same config files. --- homeassistant/components/nest/diagnostics.py | 5 +- tests/components/nest/common.py | 18 ++++ .../nest/test_config_flow_legacy.py | 6 +- tests/components/nest/test_diagnostics.py | 85 ++++++++++--------- tests/components/nest/test_init_legacy.py | 31 ++----- 5 files changed, 78 insertions(+), 67 deletions(-) diff --git a/homeassistant/components/nest/diagnostics.py b/homeassistant/components/nest/diagnostics.py index 0b6cfff6bae0..859aa8345814 100644 --- a/homeassistant/components/nest/diagnostics.py +++ b/homeassistant/components/nest/diagnostics.py @@ -12,7 +12,7 @@ from google_nest_sdm.exceptions import ApiException from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from .const import DATA_SUBSCRIBER, DOMAIN +from .const import DATA_SDM, DATA_SUBSCRIBER, DOMAIN REDACT_DEVICE_TRAITS = {InfoTrait.NAME} @@ -21,6 +21,9 @@ async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry ) -> dict: """Return diagnostics for a config entry.""" + if DATA_SDM not in config_entry.data: + return {} + if DATA_SUBSCRIBER not in hass.data[DOMAIN]: return {"error": "No subscriber configured"} diff --git a/tests/components/nest/common.py b/tests/components/nest/common.py index ca761e8987f1..e80ca84d58f0 100644 --- a/tests/components/nest/common.py +++ b/tests/components/nest/common.py @@ -102,6 +102,24 @@ TEST_CONFIG_HYBRID = NestTestConfig( }, ) +TEST_CONFIG_LEGACY = NestTestConfig( + config={ + "nest": { + "client_id": "some-client-id", + "client_secret": "some-client-secret", + }, + }, + config_entry_data={ + "auth_implementation": "local", + "tokens": { + "expires_at": time.time() + 86400, + "access_token": { + "token": "some-token", + }, + }, + }, +) + class FakeSubscriber(GoogleNestSubscriber): """Fake subscriber that supplies a FakeDeviceManager.""" diff --git a/tests/components/nest/test_config_flow_legacy.py b/tests/components/nest/test_config_flow_legacy.py index d21920b9e6f1..843c9b582ae1 100644 --- a/tests/components/nest/test_config_flow_legacy.py +++ b/tests/components/nest/test_config_flow_legacy.py @@ -6,9 +6,11 @@ from homeassistant import config_entries, data_entry_flow from homeassistant.components.nest import DOMAIN, config_flow from homeassistant.setup import async_setup_component +from .common import TEST_CONFIG_LEGACY + from tests.common import MockConfigEntry -CONFIG = {DOMAIN: {"client_id": "bla", "client_secret": "bla"}} +CONFIG = TEST_CONFIG_LEGACY.config async def test_abort_if_no_implementation_registered(hass): @@ -59,7 +61,7 @@ async def test_full_flow_implementation(hass): assert ( result["description_placeholders"] .get("url") - .startswith("https://home.nest.com/login/oauth2?client_id=bla") + .startswith("https://home.nest.com/login/oauth2?client_id=some-client-id") ) def mock_login(auth): diff --git a/tests/components/nest/test_diagnostics.py b/tests/components/nest/test_diagnostics.py index b603019da815..cf6c9c5b20fc 100644 --- a/tests/components/nest/test_diagnostics.py +++ b/tests/components/nest/test_diagnostics.py @@ -2,54 +2,45 @@ from unittest.mock import patch -from google_nest_sdm.device import Device from google_nest_sdm.exceptions import SubscriberException +import pytest -from homeassistant.components.nest import DOMAIN from homeassistant.config_entries import ConfigEntryState -from homeassistant.setup import async_setup_component -from .common import CONFIG, async_setup_sdm_platform, create_config_entry +from .common import TEST_CONFIG_LEGACY from tests.components.diagnostics import get_diagnostics_for_config_entry -THERMOSTAT_TYPE = "sdm.devices.types.THERMOSTAT" - -async def test_entry_diagnostics(hass, hass_client): +async def test_entry_diagnostics( + hass, hass_client, create_device, setup_platform, config_entry +): """Test config entry diagnostics.""" - devices = { - "some-device-id": Device.MakeDevice( - { - "name": "enterprises/project-id/devices/device-id", - "type": "sdm.devices.types.THERMOSTAT", - "assignee": "enterprises/project-id/structures/structure-id/rooms/room-id", - "traits": { - "sdm.devices.traits.Info": { - "customName": "My Sensor", - }, - "sdm.devices.traits.Temperature": { - "ambientTemperatureCelsius": 25.1, - }, - "sdm.devices.traits.Humidity": { - "ambientHumidityPercent": 35.0, - }, + create_device.create( + raw_data={ + "name": "enterprises/project-id/devices/device-id", + "type": "sdm.devices.types.THERMOSTAT", + "assignee": "enterprises/project-id/structures/structure-id/rooms/room-id", + "traits": { + "sdm.devices.traits.Info": { + "customName": "My Sensor", + }, + "sdm.devices.traits.Temperature": { + "ambientTemperatureCelsius": 25.1, + }, + "sdm.devices.traits.Humidity": { + "ambientHumidityPercent": 35.0, }, - "parentRelations": [ - { - "parent": "enterprises/project-id/structures/structure-id/rooms/room-id", - "displayName": "Lobby", - } - ], }, - auth=None, - ) - } - assert await async_setup_sdm_platform(hass, platform=None, devices=devices) - - entries = hass.config_entries.async_entries(DOMAIN) - assert len(entries) == 1 - config_entry = entries[0] + "parentRelations": [ + { + "parent": "enterprises/project-id/structures/structure-id/rooms/room-id", + "displayName": "Lobby", + } + ], + } + ) + await setup_platform() assert config_entry.state is ConfigEntryState.LOADED # Test that only non identifiable device information is returned @@ -76,20 +67,32 @@ async def test_entry_diagnostics(hass, hass_client): } -async def test_setup_susbcriber_failure(hass, hass_client): +async def test_setup_susbcriber_failure( + hass, hass_client, config_entry, setup_base_platform +): """Test configuration error.""" - config_entry = create_config_entry() - config_entry.add_to_hass(hass) with patch( "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation" ), patch( "homeassistant.components.nest.api.GoogleNestSubscriber.start_async", side_effect=SubscriberException(), ): - assert await async_setup_component(hass, DOMAIN, CONFIG) + await setup_base_platform() assert config_entry.state is ConfigEntryState.SETUP_RETRY assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { "error": "No subscriber configured" } + + +@pytest.mark.parametrize("nest_test_config", [TEST_CONFIG_LEGACY]) +async def test_legacy_config_entry_diagnostics( + hass, hass_client, config_entry, setup_base_platform +): + """Test config entry diagnostics for legacy integration doesn't fail.""" + + with patch("homeassistant.components.nest.legacy.Nest"): + await setup_base_platform() + + assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == {} diff --git a/tests/components/nest/test_init_legacy.py b/tests/components/nest/test_init_legacy.py index 3a78877a2351..cbf1bfe2d488 100644 --- a/tests/components/nest/test_init_legacy.py +++ b/tests/components/nest/test_init_legacy.py @@ -1,30 +1,18 @@ """Test basic initialization for the Legacy Nest API using mocks for the Nest python library.""" -import time from unittest.mock import MagicMock, PropertyMock, patch -from homeassistant.setup import async_setup_component +import pytest -from tests.common import MockConfigEntry +from .common import TEST_CONFIG_LEGACY DOMAIN = "nest" -CONFIG = { - "nest": { - "client_id": "some-client-id", - "client_secret": "some-client-secret", - }, -} -CONFIG_ENTRY_DATA = { - "auth_implementation": "local", - "tokens": { - "expires_at": time.time() + 86400, - "access_token": { - "token": "some-token", - }, - }, -} +@pytest.fixture +def nest_test_config(): + """Fixture to specify the overall test fixture configuration.""" + return TEST_CONFIG_LEGACY def make_thermostat(): @@ -45,7 +33,7 @@ def make_thermostat(): return device -async def test_thermostat(hass): +async def test_thermostat(hass, setup_base_platform): """Test simple initialization for thermostat entities.""" thermostat = make_thermostat() @@ -58,8 +46,6 @@ async def test_thermostat(hass): nest = MagicMock() type(nest).structures = PropertyMock(return_value=[structure]) - config_entry = MockConfigEntry(domain=DOMAIN, data=CONFIG_ENTRY_DATA) - config_entry.add_to_hass(hass) with patch("homeassistant.components.nest.legacy.Nest", return_value=nest), patch( "homeassistant.components.nest.legacy.sensor._VALID_SENSOR_TYPES", ["humidity", "temperature"], @@ -67,8 +53,7 @@ async def test_thermostat(hass): "homeassistant.components.nest.legacy.binary_sensor._VALID_BINARY_SENSOR_TYPES", {"fan": None}, ): - assert await async_setup_component(hass, DOMAIN, CONFIG) - await hass.async_block_till_done() + await setup_base_platform() climate = hass.states.get("climate.my_thermostat") assert climate is not None From 8e6bd840a4df00fd490bac846f4d3a0aed0f67ca Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 5 Feb 2022 22:49:37 -0600 Subject: [PATCH 14/24] Fix flash at turn on with newer 0x04 Magic Home models (#65836) --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index e10bf72b8c3e..4e6cf97fa31c 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["network"], "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.28.20"], + "requirements": ["flux_led==0.28.21"], "quality_scale": "platinum", "codeowners": ["@icemanch", "@bdraco"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 8ce4927f3afc..5f690e159b1b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -681,7 +681,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.28.20 +flux_led==0.28.21 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fb05ad28c595..827e27959992 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -427,7 +427,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.28.20 +flux_led==0.28.21 # homeassistant.components.homekit fnvhash==0.1.0 From fdfffcb73eb36b25934a97c43c194fed901f6600 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sun, 6 Feb 2022 11:37:23 -0600 Subject: [PATCH 15/24] Fix Spotify, Tidal, Apple Music playback on Sonos groups (#65838) --- homeassistant/components/sonos/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index d490120faf88..41453117c13e 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -558,7 +558,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): plex_plugin.play_now(media) return - share_link = self.speaker.share_link + share_link = self.coordinator.share_link if share_link.is_share_link(media_id): if kwargs.get(ATTR_MEDIA_ENQUEUE): share_link.add_share_link_to_queue(media_id) From 779171160390f4f3ef07a1bab1e68d39b6ff6e95 Mon Sep 17 00:00:00 2001 From: Jeef Date: Sun, 6 Feb 2022 01:32:04 -0700 Subject: [PATCH 16/24] feat: bumped version (#65863) --- homeassistant/components/intellifire/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/intellifire/manifest.json b/homeassistant/components/intellifire/manifest.json index 42edf00ad25a..965bb6f32dba 100644 --- a/homeassistant/components/intellifire/manifest.json +++ b/homeassistant/components/intellifire/manifest.json @@ -3,7 +3,7 @@ "name": "IntelliFire", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/intellifire", - "requirements": ["intellifire4py==0.5"], + "requirements": ["intellifire4py==0.6"], "dependencies": [], "codeowners": ["@jeeftor"], "iot_class": "local_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 5f690e159b1b..f51b7a08619d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -914,7 +914,7 @@ influxdb-client==1.24.0 influxdb==5.3.1 # homeassistant.components.intellifire -intellifire4py==0.5 +intellifire4py==0.6 # homeassistant.components.iotawatt iotawattpy==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 827e27959992..c28427ac595e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -586,7 +586,7 @@ influxdb-client==1.24.0 influxdb==5.3.1 # homeassistant.components.intellifire -intellifire4py==0.5 +intellifire4py==0.6 # homeassistant.components.iotawatt iotawattpy==0.1.0 From b9d346baed5ae87c2d10695930dbf81c5a9a2053 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 6 Feb 2022 16:12:30 -0600 Subject: [PATCH 17/24] Fix loss of ability to control white channel in HomeKit on RGB&W lights (#65864) * Fix loss of ability to control white channel in HomeKit on RGB&W lights - Fix white channel missing from RGB/W lights - Fix temp missing from RGB/CW lights - Fixes #65529 * cover the missing case * bright fix * force brightness notify on color mode change as well --- .../components/homekit/type_lights.py | 152 +++--- tests/components/homekit/test_type_lights.py | 462 ++++++++++++++++-- 2 files changed, 522 insertions(+), 92 deletions(-) diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index cdff3105ec3f..081f6f1bdd4a 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -1,4 +1,6 @@ """Class to hold all light accessories.""" +from __future__ import annotations + import logging import math @@ -12,12 +14,13 @@ from homeassistant.components.light import ( ATTR_HS_COLOR, ATTR_MAX_MIREDS, ATTR_MIN_MIREDS, - ATTR_RGB_COLOR, ATTR_RGBW_COLOR, ATTR_RGBWW_COLOR, ATTR_SUPPORTED_COLOR_MODES, + ATTR_WHITE, COLOR_MODE_RGBW, COLOR_MODE_RGBWW, + COLOR_MODE_WHITE, DOMAIN, brightness_supported, color_supported, @@ -32,9 +35,9 @@ from homeassistant.const import ( from homeassistant.core import callback from homeassistant.helpers.event import async_call_later from homeassistant.util.color import ( - color_hsv_to_RGB, color_temperature_mired_to_kelvin, color_temperature_to_hs, + color_temperature_to_rgbww, ) from .accessories import TYPES, HomeAccessory @@ -51,12 +54,13 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -RGB_COLOR = "rgb_color" CHANGE_COALESCE_TIME_WINDOW = 0.01 +DEFAULT_MIN_MIREDS = 153 +DEFAULT_MAX_MIREDS = 500 -COLOR_MODES_WITH_WHITES = {COLOR_MODE_RGBW, COLOR_MODE_RGBWW} +COLOR_MODES_WITH_WHITES = {COLOR_MODE_RGBW, COLOR_MODE_RGBWW, COLOR_MODE_WHITE} @TYPES.register("Light") @@ -79,8 +83,12 @@ class Light(HomeAccessory): self.color_modes = color_modes = ( attributes.get(ATTR_SUPPORTED_COLOR_MODES) or [] ) + self._previous_color_mode = attributes.get(ATTR_COLOR_MODE) self.color_supported = color_supported(color_modes) self.color_temp_supported = color_temp_supported(color_modes) + self.rgbw_supported = COLOR_MODE_RGBW in color_modes + self.rgbww_supported = COLOR_MODE_RGBWW in color_modes + self.white_supported = COLOR_MODE_WHITE in color_modes self.brightness_supported = brightness_supported(color_modes) if self.brightness_supported: @@ -89,7 +97,9 @@ class Light(HomeAccessory): if self.color_supported: self.chars.extend([CHAR_HUE, CHAR_SATURATION]) - if self.color_temp_supported: + if self.color_temp_supported or COLOR_MODES_WITH_WHITES.intersection( + self.color_modes + ): self.chars.append(CHAR_COLOR_TEMPERATURE) serv_light = self.add_preload_service(SERV_LIGHTBULB, self.chars) @@ -101,13 +111,22 @@ class Light(HomeAccessory): # to set to the correct initial value. self.char_brightness = serv_light.configure_char(CHAR_BRIGHTNESS, value=100) - if self.color_temp_supported: - min_mireds = math.floor(attributes.get(ATTR_MIN_MIREDS, 153)) - max_mireds = math.ceil(attributes.get(ATTR_MAX_MIREDS, 500)) + if CHAR_COLOR_TEMPERATURE in self.chars: + self.min_mireds = math.floor( + attributes.get(ATTR_MIN_MIREDS, DEFAULT_MIN_MIREDS) + ) + self.max_mireds = math.ceil( + attributes.get(ATTR_MAX_MIREDS, DEFAULT_MAX_MIREDS) + ) + if not self.color_temp_supported and not self.rgbww_supported: + self.max_mireds = self.min_mireds self.char_color_temp = serv_light.configure_char( CHAR_COLOR_TEMPERATURE, - value=min_mireds, - properties={PROP_MIN_VALUE: min_mireds, PROP_MAX_VALUE: max_mireds}, + value=self.min_mireds, + properties={ + PROP_MIN_VALUE: self.min_mireds, + PROP_MAX_VALUE: self.max_mireds, + }, ) if self.color_supported: @@ -165,33 +184,32 @@ class Light(HomeAccessory): ) return + # Handle white channels if CHAR_COLOR_TEMPERATURE in char_values: - params[ATTR_COLOR_TEMP] = char_values[CHAR_COLOR_TEMPERATURE] - events.append(f"color temperature at {params[ATTR_COLOR_TEMP]}") + temp = char_values[CHAR_COLOR_TEMPERATURE] + events.append(f"color temperature at {temp}") + bright_val = round( + ((brightness_pct or self.char_brightness.value) * 255) / 100 + ) + if self.color_temp_supported: + params[ATTR_COLOR_TEMP] = temp + elif self.rgbww_supported: + params[ATTR_RGBWW_COLOR] = color_temperature_to_rgbww( + temp, bright_val, self.min_mireds, self.max_mireds + ) + elif self.rgbw_supported: + params[ATTR_RGBW_COLOR] = (*(0,) * 3, bright_val) + elif self.white_supported: + params[ATTR_WHITE] = bright_val - elif ( - CHAR_HUE in char_values - or CHAR_SATURATION in char_values - # If we are adjusting brightness we need to send the full RGBW/RGBWW values - # since HomeKit does not support RGBW/RGBWW - or brightness_pct - and COLOR_MODES_WITH_WHITES.intersection(self.color_modes) - ): + elif CHAR_HUE in char_values or CHAR_SATURATION in char_values: hue_sat = ( char_values.get(CHAR_HUE, self.char_hue.value), char_values.get(CHAR_SATURATION, self.char_saturation.value), ) _LOGGER.debug("%s: Set hs_color to %s", self.entity_id, hue_sat) events.append(f"set color at {hue_sat}") - # HomeKit doesn't support RGBW/RGBWW so we need to remove any white values - if COLOR_MODE_RGBWW in self.color_modes: - val = brightness_pct or self.char_brightness.value - params[ATTR_RGBWW_COLOR] = (*color_hsv_to_RGB(*hue_sat, val), 0, 0) - elif COLOR_MODE_RGBW in self.color_modes: - val = brightness_pct or self.char_brightness.value - params[ATTR_RGBW_COLOR] = (*color_hsv_to_RGB(*hue_sat, val), 0) - else: - params[ATTR_HS_COLOR] = hue_sat + params[ATTR_HS_COLOR] = hue_sat if ( brightness_pct @@ -200,6 +218,9 @@ class Light(HomeAccessory): ): params[ATTR_BRIGHTNESS_PCT] = brightness_pct + _LOGGER.debug( + "Calling light service with params: %s -> %s", char_values, params + ) self.async_call_service(DOMAIN, service, params, ", ".join(events)) @callback @@ -210,52 +231,59 @@ class Light(HomeAccessory): attributes = new_state.attributes color_mode = attributes.get(ATTR_COLOR_MODE) self.char_on.set_value(int(state == STATE_ON)) + color_mode_changed = self._previous_color_mode != color_mode + self._previous_color_mode = color_mode # Handle Brightness - if self.brightness_supported: - if ( - color_mode - and COLOR_MODES_WITH_WHITES.intersection({color_mode}) - and (rgb_color := attributes.get(ATTR_RGB_COLOR)) - ): - # HomeKit doesn't support RGBW/RGBWW so we need to - # give it the color brightness only - brightness = max(rgb_color) - else: - brightness = attributes.get(ATTR_BRIGHTNESS) - if isinstance(brightness, (int, float)): - brightness = round(brightness / 255 * 100, 0) - # The homeassistant component might report its brightness as 0 but is - # not off. But 0 is a special value in homekit. When you turn on a - # homekit accessory it will try to restore the last brightness state - # which will be the last value saved by char_brightness.set_value. - # But if it is set to 0, HomeKit will update the brightness to 100 as - # it thinks 0 is off. - # - # Therefore, if the the brightness is 0 and the device is still on, - # the brightness is mapped to 1 otherwise the update is ignored in - # order to avoid this incorrect behavior. - if brightness == 0 and state == STATE_ON: - brightness = 1 - self.char_brightness.set_value(brightness) + if ( + self.brightness_supported + and (brightness := attributes.get(ATTR_BRIGHTNESS)) is not None + and isinstance(brightness, (int, float)) + ): + brightness = round(brightness / 255 * 100, 0) + # The homeassistant component might report its brightness as 0 but is + # not off. But 0 is a special value in homekit. When you turn on a + # homekit accessory it will try to restore the last brightness state + # which will be the last value saved by char_brightness.set_value. + # But if it is set to 0, HomeKit will update the brightness to 100 as + # it thinks 0 is off. + # + # Therefore, if the the brightness is 0 and the device is still on, + # the brightness is mapped to 1 otherwise the update is ignored in + # order to avoid this incorrect behavior. + if brightness == 0 and state == STATE_ON: + brightness = 1 + self.char_brightness.set_value(brightness) + if color_mode_changed: + self.char_brightness.notify() # Handle Color - color must always be set before color temperature # or the iOS UI will not display it correctly. if self.color_supported: - if ATTR_COLOR_TEMP in attributes: + if color_temp := attributes.get(ATTR_COLOR_TEMP): hue, saturation = color_temperature_to_hs( - color_temperature_mired_to_kelvin( - new_state.attributes[ATTR_COLOR_TEMP] - ) + color_temperature_mired_to_kelvin(color_temp) ) + elif color_mode == COLOR_MODE_WHITE: + hue, saturation = 0, 0 else: hue, saturation = attributes.get(ATTR_HS_COLOR, (None, None)) if isinstance(hue, (int, float)) and isinstance(saturation, (int, float)): self.char_hue.set_value(round(hue, 0)) self.char_saturation.set_value(round(saturation, 0)) + if color_mode_changed: + # If the color temp changed, be sure to force the color to update + self.char_hue.notify() + self.char_saturation.notify() - # Handle color temperature - if self.color_temp_supported: - color_temp = attributes.get(ATTR_COLOR_TEMP) + # Handle white channels + if CHAR_COLOR_TEMPERATURE in self.chars: + color_temp = None + if self.color_temp_supported: + color_temp = attributes.get(ATTR_COLOR_TEMP) + elif color_mode == COLOR_MODE_WHITE: + color_temp = self.min_mireds if isinstance(color_temp, (int, float)): self.char_color_temp.set_value(round(color_temp, 0)) + if color_mode_changed: + self.char_color_temp.notify() diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index 8e7b60b0a47f..6835629be374 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -5,7 +5,11 @@ from datetime import timedelta from pyhap.const import HAP_REPR_AID, HAP_REPR_CHARS, HAP_REPR_IID, HAP_REPR_VALUE import pytest -from homeassistant.components.homekit.const import ATTR_VALUE +from homeassistant.components.homekit.const import ( + ATTR_VALUE, + PROP_MAX_VALUE, + PROP_MIN_VALUE, +) from homeassistant.components.homekit.type_lights import ( CHANGE_COALESCE_TIME_WINDOW, Light, @@ -22,9 +26,12 @@ from homeassistant.components.light import ( ATTR_RGBW_COLOR, ATTR_RGBWW_COLOR, ATTR_SUPPORTED_COLOR_MODES, + ATTR_WHITE, COLOR_MODE_COLOR_TEMP, + COLOR_MODE_RGB, COLOR_MODE_RGBW, COLOR_MODE_RGBWW, + COLOR_MODE_WHITE, DOMAIN, ) from homeassistant.const import ( @@ -573,7 +580,7 @@ async def test_light_restore(hass, hk_driver, events): @pytest.mark.parametrize( - "supported_color_modes, state_props, turn_on_props, turn_on_props_with_brightness", + "supported_color_modes, state_props, turn_on_props_with_brightness", [ [ [COLOR_MODE_COLOR_TEMP, COLOR_MODE_RGBW], @@ -584,8 +591,7 @@ async def test_light_restore(hass, hk_driver, events): ATTR_BRIGHTNESS: 255, ATTR_COLOR_MODE: COLOR_MODE_RGBW, }, - {ATTR_RGBW_COLOR: (31, 127, 71, 0)}, - {ATTR_RGBW_COLOR: (15, 63, 35, 0)}, + {ATTR_HS_COLOR: (145, 75), ATTR_BRIGHTNESS_PCT: 25}, ], [ [COLOR_MODE_COLOR_TEMP, COLOR_MODE_RGBWW], @@ -596,21 +602,19 @@ async def test_light_restore(hass, hk_driver, events): ATTR_BRIGHTNESS: 255, ATTR_COLOR_MODE: COLOR_MODE_RGBWW, }, - {ATTR_RGBWW_COLOR: (31, 127, 71, 0, 0)}, - {ATTR_RGBWW_COLOR: (15, 63, 35, 0, 0)}, + {ATTR_HS_COLOR: (145, 75), ATTR_BRIGHTNESS_PCT: 25}, ], ], ) -async def test_light_rgb_with_white( +async def test_light_rgb_with_color_temp( hass, hk_driver, events, supported_color_modes, state_props, - turn_on_props, turn_on_props_with_brightness, ): - """Test lights with RGBW/RGBWW.""" + """Test lights with RGBW/RGBWW with color temp support.""" entity_id = "light.demo" hass.states.async_set( @@ -629,7 +633,7 @@ async def test_light_rgb_with_white( await hass.async_block_till_done() assert acc.char_hue.value == 23 assert acc.char_saturation.value == 100 - assert acc.char_brightness.value == 50 + assert acc.char_brightness.value == 100 # Set from HomeKit call_turn_on = async_mock_service(hass, DOMAIN, "turn_on") @@ -658,11 +662,10 @@ async def test_light_rgb_with_white( await _wait_for_light_coalesce(hass) assert call_turn_on assert call_turn_on[-1].data[ATTR_ENTITY_ID] == entity_id - for k, v in turn_on_props.items(): - assert call_turn_on[-1].data[k] == v + assert call_turn_on[-1].data[ATTR_HS_COLOR] == (145, 75) assert len(events) == 1 assert events[-1].data[ATTR_VALUE] == "set color at (145, 75)" - assert acc.char_brightness.value == 50 + assert acc.char_brightness.value == 100 hk_driver.set_characteristics( { @@ -697,7 +700,204 @@ async def test_light_rgb_with_white( @pytest.mark.parametrize( - "supported_color_modes, state_props, turn_on_props, turn_on_props_with_brightness", + "supported_color_modes, state_props, turn_on_props_with_brightness", + [ + [ + [COLOR_MODE_RGBW], + { + ATTR_RGBW_COLOR: (128, 50, 0, 255), + ATTR_RGB_COLOR: (128, 50, 0), + ATTR_HS_COLOR: (23.438, 100.0), + ATTR_BRIGHTNESS: 255, + ATTR_COLOR_MODE: COLOR_MODE_RGBW, + }, + {ATTR_RGBW_COLOR: (0, 0, 0, 191)}, + ], + [ + [COLOR_MODE_RGBWW], + { + ATTR_RGBWW_COLOR: (128, 50, 0, 255, 255), + ATTR_RGB_COLOR: (128, 50, 0), + ATTR_HS_COLOR: (23.438, 100.0), + ATTR_BRIGHTNESS: 255, + ATTR_COLOR_MODE: COLOR_MODE_RGBWW, + }, + {ATTR_RGBWW_COLOR: (0, 0, 0, 165, 26)}, + ], + ], +) +async def test_light_rgbwx_with_color_temp_and_brightness( + hass, + hk_driver, + events, + supported_color_modes, + state_props, + turn_on_props_with_brightness, +): + """Test lights with RGBW/RGBWW with color temp support and setting brightness.""" + entity_id = "light.demo" + + hass.states.async_set( + entity_id, + STATE_ON, + {ATTR_SUPPORTED_COLOR_MODES: supported_color_modes, **state_props}, + ) + await hass.async_block_till_done() + acc = Light(hass, hk_driver, "Light", entity_id, 1, None) + hk_driver.add_accessory(acc) + + assert acc.char_hue.value == 23 + assert acc.char_saturation.value == 100 + + await acc.run() + await hass.async_block_till_done() + assert acc.char_hue.value == 23 + assert acc.char_saturation.value == 100 + assert acc.char_brightness.value == 100 + + # Set from HomeKit + call_turn_on = async_mock_service(hass, DOMAIN, "turn_on") + + char_color_temp_iid = acc.char_color_temp.to_HAP()[HAP_REPR_IID] + char_brightness_iid = acc.char_brightness.to_HAP()[HAP_REPR_IID] + + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_color_temp_iid, + HAP_REPR_VALUE: 200, + }, + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_brightness_iid, + HAP_REPR_VALUE: 75, + }, + ] + }, + "mock_addr", + ) + await _wait_for_light_coalesce(hass) + assert call_turn_on + assert call_turn_on[-1].data[ATTR_ENTITY_ID] == entity_id + for k, v in turn_on_props_with_brightness.items(): + assert call_turn_on[-1].data[k] == v + assert len(events) == 1 + assert events[-1].data[ATTR_VALUE] == "brightness at 75%, color temperature at 200" + assert acc.char_brightness.value == 75 + + +async def test_light_rgb_or_w_lights( + hass, + hk_driver, + events, +): + """Test lights with RGB or W lights.""" + entity_id = "light.demo" + + hass.states.async_set( + entity_id, + STATE_ON, + { + ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_RGB, COLOR_MODE_WHITE], + ATTR_RGBW_COLOR: (128, 50, 0, 255), + ATTR_RGB_COLOR: (128, 50, 0), + ATTR_HS_COLOR: (23.438, 100.0), + ATTR_BRIGHTNESS: 255, + ATTR_COLOR_MODE: COLOR_MODE_RGB, + }, + ) + await hass.async_block_till_done() + acc = Light(hass, hk_driver, "Light", entity_id, 1, None) + hk_driver.add_accessory(acc) + + assert acc.char_hue.value == 23 + assert acc.char_saturation.value == 100 + + await acc.run() + await hass.async_block_till_done() + assert acc.char_hue.value == 23 + assert acc.char_saturation.value == 100 + assert acc.char_brightness.value == 100 + assert acc.char_color_temp.value == 153 + + # Set from HomeKit + call_turn_on = async_mock_service(hass, DOMAIN, "turn_on") + + char_hue_iid = acc.char_hue.to_HAP()[HAP_REPR_IID] + char_saturation_iid = acc.char_saturation.to_HAP()[HAP_REPR_IID] + char_brightness_iid = acc.char_brightness.to_HAP()[HAP_REPR_IID] + char_color_temp_iid = acc.char_color_temp.to_HAP()[HAP_REPR_IID] + + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_hue_iid, + HAP_REPR_VALUE: 145, + }, + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_saturation_iid, + HAP_REPR_VALUE: 75, + }, + ] + }, + "mock_addr", + ) + await _wait_for_light_coalesce(hass) + assert call_turn_on + assert call_turn_on[-1].data[ATTR_ENTITY_ID] == entity_id + assert call_turn_on[-1].data[ATTR_HS_COLOR] == (145, 75) + assert len(events) == 1 + assert events[-1].data[ATTR_VALUE] == "set color at (145, 75)" + assert acc.char_brightness.value == 100 + + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_color_temp_iid, + HAP_REPR_VALUE: acc.min_mireds, + }, + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_brightness_iid, + HAP_REPR_VALUE: 25, + }, + ] + }, + "mock_addr", + ) + await _wait_for_light_coalesce(hass) + assert call_turn_on + assert call_turn_on[-1].data[ATTR_ENTITY_ID] == entity_id + assert call_turn_on[-1].data[ATTR_WHITE] == round(25 * 255 / 100) + assert len(events) == 2 + assert events[-1].data[ATTR_VALUE] == "brightness at 25%, color temperature at 153" + assert acc.char_brightness.value == 25 + + hass.states.async_set( + entity_id, + STATE_ON, + { + ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_RGB, COLOR_MODE_WHITE], + ATTR_BRIGHTNESS: 255, + ATTR_COLOR_MODE: COLOR_MODE_WHITE, + }, + ) + await hass.async_block_till_done() + assert acc.char_hue.value == 0 + assert acc.char_saturation.value == 0 + assert acc.char_brightness.value == 100 + assert acc.char_color_temp.value == 153 + + +@pytest.mark.parametrize( + "supported_color_modes, state_props", [ [ [COLOR_MODE_COLOR_TEMP, COLOR_MODE_RGBW], @@ -708,8 +908,6 @@ async def test_light_rgb_with_white( ATTR_BRIGHTNESS: 255, ATTR_COLOR_MODE: COLOR_MODE_RGBW, }, - {ATTR_RGBW_COLOR: (31, 127, 71, 0)}, - {ATTR_COLOR_TEMP: 2700}, ], [ [COLOR_MODE_COLOR_TEMP, COLOR_MODE_RGBWW], @@ -720,8 +918,6 @@ async def test_light_rgb_with_white( ATTR_BRIGHTNESS: 255, ATTR_COLOR_MODE: COLOR_MODE_RGBWW, }, - {ATTR_RGBWW_COLOR: (31, 127, 71, 0, 0)}, - {ATTR_COLOR_TEMP: 2700}, ], ], ) @@ -731,8 +927,6 @@ async def test_light_rgb_with_white_switch_to_temp( events, supported_color_modes, state_props, - turn_on_props, - turn_on_props_with_brightness, ): """Test lights with RGBW/RGBWW that preserves brightness when switching to color temp.""" entity_id = "light.demo" @@ -753,7 +947,7 @@ async def test_light_rgb_with_white_switch_to_temp( await hass.async_block_till_done() assert acc.char_hue.value == 23 assert acc.char_saturation.value == 100 - assert acc.char_brightness.value == 50 + assert acc.char_brightness.value == 100 # Set from HomeKit call_turn_on = async_mock_service(hass, DOMAIN, "turn_on") @@ -782,19 +976,17 @@ async def test_light_rgb_with_white_switch_to_temp( await _wait_for_light_coalesce(hass) assert call_turn_on assert call_turn_on[-1].data[ATTR_ENTITY_ID] == entity_id - for k, v in turn_on_props.items(): - assert call_turn_on[-1].data[k] == v + assert call_turn_on[-1].data[ATTR_HS_COLOR] == (145, 75) assert len(events) == 1 assert events[-1].data[ATTR_VALUE] == "set color at (145, 75)" - assert acc.char_brightness.value == 50 - + assert acc.char_brightness.value == 100 hk_driver.set_characteristics( { HAP_REPR_CHARS: [ { HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_color_temp_iid, - HAP_REPR_VALUE: 2700, + HAP_REPR_VALUE: 500, }, ] }, @@ -803,11 +995,221 @@ async def test_light_rgb_with_white_switch_to_temp( await _wait_for_light_coalesce(hass) assert call_turn_on assert call_turn_on[-1].data[ATTR_ENTITY_ID] == entity_id - for k, v in turn_on_props_with_brightness.items(): - assert call_turn_on[-1].data[k] == v + assert call_turn_on[-1].data[ATTR_COLOR_TEMP] == 500 assert len(events) == 2 - assert events[-1].data[ATTR_VALUE] == "color temperature at 2700" - assert acc.char_brightness.value == 50 + assert events[-1].data[ATTR_VALUE] == "color temperature at 500" + assert acc.char_brightness.value == 100 + + +async def test_light_rgbww_with_color_temp_conversion( + hass, + hk_driver, + events, +): + """Test lights with RGBWW convert color temp as expected.""" + entity_id = "light.demo" + + hass.states.async_set( + entity_id, + STATE_ON, + { + ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_RGBWW], + ATTR_RGBWW_COLOR: (128, 50, 0, 255, 255), + ATTR_RGB_COLOR: (128, 50, 0), + ATTR_HS_COLOR: (23.438, 100.0), + ATTR_BRIGHTNESS: 255, + ATTR_COLOR_MODE: COLOR_MODE_RGBWW, + }, + ) + await hass.async_block_till_done() + acc = Light(hass, hk_driver, "Light", entity_id, 1, None) + hk_driver.add_accessory(acc) + + assert acc.char_hue.value == 23 + assert acc.char_saturation.value == 100 + + await acc.run() + await hass.async_block_till_done() + assert acc.char_hue.value == 23 + assert acc.char_saturation.value == 100 + assert acc.char_brightness.value == 100 + + # Set from HomeKit + call_turn_on = async_mock_service(hass, DOMAIN, "turn_on") + + char_hue_iid = acc.char_hue.to_HAP()[HAP_REPR_IID] + char_saturation_iid = acc.char_saturation.to_HAP()[HAP_REPR_IID] + char_color_temp_iid = acc.char_color_temp.to_HAP()[HAP_REPR_IID] + char_brightness_iid = acc.char_brightness.to_HAP()[HAP_REPR_IID] + + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_hue_iid, + HAP_REPR_VALUE: 145, + }, + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_saturation_iid, + HAP_REPR_VALUE: 75, + }, + ] + }, + "mock_addr", + ) + await _wait_for_light_coalesce(hass) + assert call_turn_on + assert call_turn_on[-1].data[ATTR_ENTITY_ID] == entity_id + assert call_turn_on[-1].data[ATTR_HS_COLOR] == (145, 75) + assert len(events) == 1 + assert events[-1].data[ATTR_VALUE] == "set color at (145, 75)" + assert acc.char_brightness.value == 100 + + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_color_temp_iid, + HAP_REPR_VALUE: 200, + }, + ] + }, + "mock_addr", + ) + await _wait_for_light_coalesce(hass) + assert call_turn_on + assert call_turn_on[-1].data[ATTR_ENTITY_ID] == entity_id + assert call_turn_on[-1].data[ATTR_RGBWW_COLOR] == (0, 0, 0, 220, 35) + assert len(events) == 2 + assert events[-1].data[ATTR_VALUE] == "color temperature at 200" + assert acc.char_brightness.value == 100 + + hass.states.async_set( + entity_id, + STATE_ON, + { + ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_RGBWW], + ATTR_RGBWW_COLOR: (0, 0, 0, 128, 255), + ATTR_RGB_COLOR: (255, 163, 79), + ATTR_HS_COLOR: (28.636, 69.02), + ATTR_BRIGHTNESS: 180, + ATTR_COLOR_MODE: COLOR_MODE_RGBWW, + }, + ) + await hass.async_block_till_done() + + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_brightness_iid, + HAP_REPR_VALUE: 100, + }, + ] + }, + "mock_addr", + ) + await _wait_for_light_coalesce(hass) + assert call_turn_on + assert call_turn_on[-1].data[ATTR_ENTITY_ID] == entity_id + assert call_turn_on[-1].data[ATTR_BRIGHTNESS_PCT] == 100 + assert len(events) == 3 + assert events[-1].data[ATTR_VALUE] == "brightness at 100%" + assert acc.char_brightness.value == 100 + + +async def test_light_rgbw_with_color_temp_conversion( + hass, + hk_driver, + events, +): + """Test lights with RGBW convert color temp as expected.""" + entity_id = "light.demo" + + hass.states.async_set( + entity_id, + STATE_ON, + { + ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_RGBW], + ATTR_RGBWW_COLOR: (128, 50, 0, 255, 255), + ATTR_RGB_COLOR: (128, 50, 0), + ATTR_HS_COLOR: (23.438, 100.0), + ATTR_BRIGHTNESS: 255, + ATTR_COLOR_MODE: COLOR_MODE_RGBW, + }, + ) + await hass.async_block_till_done() + acc = Light(hass, hk_driver, "Light", entity_id, 1, None) + hk_driver.add_accessory(acc) + + assert acc.char_hue.value == 23 + assert acc.char_saturation.value == 100 + + await acc.run() + await hass.async_block_till_done() + assert acc.char_hue.value == 23 + assert acc.char_saturation.value == 100 + assert acc.char_brightness.value == 100 + + # Set from HomeKit + call_turn_on = async_mock_service(hass, DOMAIN, "turn_on") + + char_hue_iid = acc.char_hue.to_HAP()[HAP_REPR_IID] + char_saturation_iid = acc.char_saturation.to_HAP()[HAP_REPR_IID] + char_color_temp_iid = acc.char_color_temp.to_HAP()[HAP_REPR_IID] + assert ( + acc.char_color_temp.properties[PROP_MIN_VALUE] + == acc.char_color_temp.properties[PROP_MAX_VALUE] + ) + + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_hue_iid, + HAP_REPR_VALUE: 145, + }, + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_saturation_iid, + HAP_REPR_VALUE: 75, + }, + ] + }, + "mock_addr", + ) + await _wait_for_light_coalesce(hass) + assert call_turn_on + assert call_turn_on[-1].data[ATTR_ENTITY_ID] == entity_id + assert call_turn_on[-1].data[ATTR_HS_COLOR] == (145, 75) + assert len(events) == 1 + assert events[-1].data[ATTR_VALUE] == "set color at (145, 75)" + assert acc.char_brightness.value == 100 + + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_color_temp_iid, + HAP_REPR_VALUE: 153, + }, + ] + }, + "mock_addr", + ) + await _wait_for_light_coalesce(hass) + assert call_turn_on + assert call_turn_on[-1].data[ATTR_ENTITY_ID] == entity_id + assert call_turn_on[-1].data[ATTR_RGBW_COLOR] == (0, 0, 0, 255) + assert len(events) == 2 + assert events[-1].data[ATTR_VALUE] == "color temperature at 153" + assert acc.char_brightness.value == 100 async def test_light_set_brightness_and_color(hass, hk_driver, events): From 0dbe9b7cf4afaa6a2044794842d61e52c26b4ea8 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sun, 6 Feb 2022 17:48:56 +0100 Subject: [PATCH 18/24] Update xknx to 0.19.2 - fix TCP tunnelling (#65920) --- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 9889f39ab35b..663c0e5839a7 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/knx", "requirements": [ - "xknx==0.19.1" + "xknx==0.19.2" ], "codeowners": [ "@Julius2342", diff --git a/requirements_all.txt b/requirements_all.txt index f51b7a08619d..d0b39f593c55 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2496,7 +2496,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.knx -xknx==0.19.1 +xknx==0.19.2 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c28427ac595e..a87796f665c0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1527,7 +1527,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.knx -xknx==0.19.1 +xknx==0.19.2 # homeassistant.components.bluesound # homeassistant.components.fritz From ad3b2f02b4580e6a1fbd6b803312927c5fba4999 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sun, 6 Feb 2022 20:23:31 +0100 Subject: [PATCH 19/24] disabled_by can be None when updating devices (#65934) --- homeassistant/components/config/device_registry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/config/device_registry.py b/homeassistant/components/config/device_registry.py index 5e7c2ef19385..686fffec2524 100644 --- a/homeassistant/components/config/device_registry.py +++ b/homeassistant/components/config/device_registry.py @@ -62,7 +62,7 @@ async def websocket_update_device(hass, connection, msg): msg.pop("type") msg_id = msg.pop("id") - if "disabled_by" in msg: + if msg.get("disabled_by") is not None: msg["disabled_by"] = DeviceEntryDisabler(msg["disabled_by"]) entry = registry.async_update_device(**msg) From aa9965675d222f5a0da8c3c45534669c1e57a7b8 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 6 Feb 2022 23:02:31 +0100 Subject: [PATCH 20/24] Improve device shutdown and unload of Synology DSM integration (#65936) * ignore errors during unload/logout * automatic host update is an info, nut debug --- homeassistant/components/synology_dsm/common.py | 7 ++++++- homeassistant/components/synology_dsm/config_flow.py | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/synology_dsm/common.py b/homeassistant/components/synology_dsm/common.py index 273c9cc6a421..54a0735186fc 100644 --- a/homeassistant/components/synology_dsm/common.py +++ b/homeassistant/components/synology_dsm/common.py @@ -16,6 +16,7 @@ from synology_dsm.api.storage.storage import SynoStorage from synology_dsm.api.surveillance_station import SynoSurveillanceStation from synology_dsm.exceptions import ( SynologyDSMAPIErrorException, + SynologyDSMException, SynologyDSMLoginFailedException, SynologyDSMRequestException, ) @@ -237,7 +238,11 @@ class SynoApi: async def async_unload(self) -> None: """Stop interacting with the NAS and prepare for removal from hass.""" - await self._syno_api_executer(self.dsm.logout) + try: + await self._syno_api_executer(self.dsm.logout) + except SynologyDSMException: + # ignore API errors during logout + pass async def async_update(self, now: timedelta | None = None) -> None: """Update function for updating API information.""" diff --git a/homeassistant/components/synology_dsm/config_flow.py b/homeassistant/components/synology_dsm/config_flow.py index 91ad49c5f846..256ad5eef8ed 100644 --- a/homeassistant/components/synology_dsm/config_flow.py +++ b/homeassistant/components/synology_dsm/config_flow.py @@ -267,7 +267,7 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN): and existing_entry.data[CONF_HOST] != parsed_url.hostname and not fqdn_with_ssl_verification ): - _LOGGER.debug( + _LOGGER.info( "Update host from '%s' to '%s' for NAS '%s' via SSDP discovery", existing_entry.data[CONF_HOST], parsed_url.hostname, From e90a6bbe1cc423543166614f43cbfebecb98634a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 6 Feb 2022 16:02:07 -0600 Subject: [PATCH 21/24] Add diagnostics support to HomeKit (#65942) * Add diagnostics support to HomeKit * remove debug --- .../components/homekit/diagnostics.py | 44 +++++++ tests/components/homekit/test_diagnostics.py | 119 ++++++++++++++++++ 2 files changed, 163 insertions(+) create mode 100644 homeassistant/components/homekit/diagnostics.py create mode 100644 tests/components/homekit/test_diagnostics.py diff --git a/homeassistant/components/homekit/diagnostics.py b/homeassistant/components/homekit/diagnostics.py new file mode 100644 index 000000000000..2a54c1ef543e --- /dev/null +++ b/homeassistant/components/homekit/diagnostics.py @@ -0,0 +1,44 @@ +"""Diagnostics support for HomeKit.""" +from __future__ import annotations + +from typing import Any + +from pyhap.accessory_driver import AccessoryDriver +from pyhap.state import State + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from . import HomeKit +from .const import DOMAIN, HOMEKIT + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + homekit: HomeKit = hass.data[DOMAIN][entry.entry_id][HOMEKIT] + driver: AccessoryDriver = homekit.driver + data: dict[str, Any] = { + "status": homekit.status, + "config-entry": { + "title": entry.title, + "version": entry.version, + "data": dict(entry.data), + "options": dict(entry.options), + }, + } + if not driver: + return data + data.update(driver.get_accessories()) + state: State = driver.state + data.update( + { + "client_properties": { + str(client): props for client, props in state.client_properties.items() + }, + "config_version": state.config_version, + "pairing_id": state.mac, + } + ) + return data diff --git a/tests/components/homekit/test_diagnostics.py b/tests/components/homekit/test_diagnostics.py new file mode 100644 index 000000000000..e3a85b85972f --- /dev/null +++ b/tests/components/homekit/test_diagnostics.py @@ -0,0 +1,119 @@ +"""Test homekit diagnostics.""" +from unittest.mock import ANY, patch + +from homeassistant.components.homekit.const import DOMAIN +from homeassistant.const import CONF_NAME, CONF_PORT, EVENT_HOMEASSISTANT_STARTED + +from .util import async_init_integration + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +async def test_config_entry_not_running( + hass, hass_client, hk_driver, mock_async_zeroconf +): + """Test generating diagnostics for a config entry.""" + entry = await async_init_integration(hass) + diag = await get_diagnostics_for_config_entry(hass, hass_client, entry) + assert diag == { + "config-entry": { + "data": {"name": "mock_name", "port": 12345}, + "options": {}, + "title": "Mock Title", + "version": 1, + }, + "status": 0, + } + + +async def test_config_entry_running(hass, hass_client, hk_driver, mock_async_zeroconf): + """Test generating diagnostics for a config entry.""" + entry = MockConfigEntry( + domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345} + ) + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + diag = await get_diagnostics_for_config_entry(hass, hass_client, entry) + assert diag == { + "accessories": [ + { + "aid": 1, + "services": [ + { + "characteristics": [ + {"format": "bool", "iid": 2, "perms": ["pw"], "type": "14"}, + { + "format": "string", + "iid": 3, + "perms": ["pr"], + "type": "20", + "value": "Home Assistant", + }, + { + "format": "string", + "iid": 4, + "perms": ["pr"], + "type": "21", + "value": "Bridge", + }, + { + "format": "string", + "iid": 5, + "perms": ["pr"], + "type": "23", + "value": "mock_name", + }, + { + "format": "string", + "iid": 6, + "perms": ["pr"], + "type": "30", + "value": "homekit.bridge", + }, + { + "format": "string", + "iid": 7, + "perms": ["pr"], + "type": "52", + "value": ANY, + }, + ], + "iid": 1, + "type": "3E", + }, + { + "characteristics": [ + { + "format": "string", + "iid": 9, + "perms": ["pr", "ev"], + "type": "37", + "value": "01.01.00", + } + ], + "iid": 8, + "type": "A2", + }, + ], + } + ], + "client_properties": {}, + "config-entry": { + "data": {"name": "mock_name", "port": 12345}, + "options": {}, + "title": "Mock Title", + "version": 1, + }, + "config_version": 2, + "pairing_id": ANY, + "status": 1, + } + + with patch("pyhap.accessory_driver.AccessoryDriver.async_start"), patch( + "homeassistant.components.homekit.HomeKit.async_stop" + ), patch("homeassistant.components.homekit.async_port_is_available"): + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() From 9b471ab653bfc6970b22077028b969678f8c55a8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 6 Feb 2022 14:23:08 -0800 Subject: [PATCH 22/24] Bumped version to 2022.2.3 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index d05b222fc55a..5c2939e0b1fd 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 2 -PATCH_VERSION: Final = "2" +PATCH_VERSION: Final = "3" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index c1de0930cbc7..8d9f8974eb71 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.2.2 +version = 2022.2.3 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 From 1338b347b5bbc418a1681f1b00004f0a0eab2f03 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 6 Feb 2022 14:33:07 -0800 Subject: [PATCH 23/24] Remove duplicate methods --- homeassistant/components/fritz/common.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index eab85ae40872..078bb4b42252 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -574,13 +574,6 @@ class AvmWrapper(FritzBoxTools): partial(self.get_wan_link_properties) ) - async def async_get_wan_link_properties(self) -> dict[str, Any]: - """Call WANCommonInterfaceConfig service.""" - - return await self.hass.async_add_executor_job( - partial(self.get_wan_link_properties) - ) - async def async_get_port_mapping(self, con_type: str, index: int) -> dict[str, Any]: """Call GetGenericPortMappingEntry action.""" @@ -685,13 +678,6 @@ class AvmWrapper(FritzBoxTools): "WANCommonInterfaceConfig", "1", "GetCommonLinkProperties" ) - def get_wan_link_properties(self) -> dict[str, Any]: - """Call WANCommonInterfaceConfig service.""" - - return self._service_call_action( - "WANCommonInterfaceConfig", "1", "GetCommonLinkProperties" - ) - def set_wlan_configuration(self, index: int, turn_on: bool) -> dict[str, Any]: """Call SetEnable action from WLANConfiguration service.""" From 66e076b57f1b9e6977c8f12e0ba5a15333f5837a Mon Sep 17 00:00:00 2001 From: "M. Frister" Date: Sun, 6 Feb 2022 18:17:41 +0100 Subject: [PATCH 24/24] Bump soco to 0.26.2 (#65919) --- homeassistant/components/sonos/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index b482556f2877..bd16701435cb 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -3,7 +3,7 @@ "name": "Sonos", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sonos", - "requirements": ["soco==0.26.0"], + "requirements": ["soco==0.26.2"], "dependencies": ["ssdp"], "after_dependencies": ["plex", "spotify", "zeroconf", "media_source"], "zeroconf": ["_sonos._tcp.local."], diff --git a/requirements_all.txt b/requirements_all.txt index d0b39f593c55..7f6e269746b5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2228,7 +2228,7 @@ smhi-pkg==1.0.15 snapcast==2.1.3 # homeassistant.components.sonos -soco==0.26.0 +soco==0.26.2 # homeassistant.components.solaredge_local solaredge-local==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a87796f665c0..901f753105d0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1355,7 +1355,7 @@ smarthab==0.21 smhi-pkg==1.0.15 # homeassistant.components.sonos -soco==0.26.0 +soco==0.26.2 # homeassistant.components.solaredge solaredge==0.0.2