From d2b98fa2854830360a34ece457530c8cf725d4b8 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 3 Aug 2022 22:33:05 +0200 Subject: [PATCH 01/19] Fix zwave_js addon info (#76044) * Add add-on store info command * Use add-on store info command in zwave_js * Fix init tests * Update tests * Fix method for addon store info * Fix response parsing * Fix store addon installed response parsing * Remove addon info log that can contain network keys * Add supervisor store addon info test * Default to version None if add-on not installed Co-authored-by: Mike Degatano Co-authored-by: Mike Degatano --- homeassistant/components/hassio/__init__.py | 12 ++++ homeassistant/components/zwave_js/addon.py | 22 +++++-- tests/components/hassio/test_init.py | 20 ++++++- tests/components/zwave_js/conftest.py | 60 +++++++++++++++++-- tests/components/zwave_js/test_config_flow.py | 18 +++--- tests/components/zwave_js/test_init.py | 22 ++++--- 6 files changed, 122 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 46592cbc20c..8535a0c3cc6 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -223,12 +223,24 @@ HARDWARE_INTEGRATIONS = { async def async_get_addon_info(hass: HomeAssistant, slug: str) -> dict: """Return add-on info. + The add-on must be installed. The caller of the function should handle HassioAPIError. """ hassio = hass.data[DOMAIN] return await hassio.get_addon_info(slug) +@api_data +async def async_get_addon_store_info(hass: HomeAssistant, slug: str) -> dict: + """Return add-on store info. + + The caller of the function should handle HassioAPIError. + """ + hassio: HassIO = hass.data[DOMAIN] + command = f"/store/addons/{slug}" + return await hassio.send_command(command, method="get") + + @bind_hass async def async_update_diagnostics(hass: HomeAssistant, diagnostics: bool) -> dict: """Update Supervisor diagnostics toggle. diff --git a/homeassistant/components/zwave_js/addon.py b/homeassistant/components/zwave_js/addon.py index 7552ee117cc..610fc850e90 100644 --- a/homeassistant/components/zwave_js/addon.py +++ b/homeassistant/components/zwave_js/addon.py @@ -14,6 +14,7 @@ from homeassistant.components.hassio import ( async_create_backup, async_get_addon_discovery_info, async_get_addon_info, + async_get_addon_store_info, async_install_addon, async_restart_addon, async_set_addon_options, @@ -136,7 +137,17 @@ class AddonManager: @api_error("Failed to get the Z-Wave JS add-on info") async def async_get_addon_info(self) -> AddonInfo: """Return and cache Z-Wave JS add-on info.""" - addon_info: dict = await async_get_addon_info(self._hass, ADDON_SLUG) + addon_store_info = await async_get_addon_store_info(self._hass, ADDON_SLUG) + LOGGER.debug("Add-on store info: %s", addon_store_info) + if not addon_store_info["installed"]: + return AddonInfo( + options={}, + state=AddonState.NOT_INSTALLED, + update_available=False, + version=None, + ) + + addon_info = await async_get_addon_info(self._hass, ADDON_SLUG) addon_state = self.async_get_addon_state(addon_info) return AddonInfo( options=addon_info["options"], @@ -148,10 +159,8 @@ class AddonManager: @callback def async_get_addon_state(self, addon_info: dict[str, Any]) -> AddonState: """Return the current state of the Z-Wave JS add-on.""" - addon_state = AddonState.NOT_INSTALLED + addon_state = AddonState.NOT_RUNNING - if addon_info["version"] is not None: - addon_state = AddonState.NOT_RUNNING if addon_info["state"] == "started": addon_state = AddonState.RUNNING if self._install_task and not self._install_task.done(): @@ -226,7 +235,7 @@ class AddonManager: """Update the Z-Wave JS add-on if needed.""" addon_info = await self.async_get_addon_info() - if addon_info.version is None: + if addon_info.state is AddonState.NOT_INSTALLED: raise AddonError("Z-Wave JS add-on is not installed") if not addon_info.update_available: @@ -301,6 +310,9 @@ class AddonManager: """Configure and start Z-Wave JS add-on.""" addon_info = await self.async_get_addon_info() + if addon_info.state is AddonState.NOT_INSTALLED: + raise AddonError("Z-Wave JS add-on is not installed") + new_addon_options = { CONF_ADDON_DEVICE: usb_path, CONF_ADDON_S0_LEGACY_KEY: s0_legacy_key, diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index 60fec517aa9..41b679e448a 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -8,7 +8,12 @@ import pytest from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.components import frontend from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.components.hassio import ADDONS_COORDINATOR, DOMAIN, STORAGE_KEY +from homeassistant.components.hassio import ( + ADDONS_COORDINATOR, + DOMAIN, + STORAGE_KEY, + async_get_addon_store_info, +) from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.helpers.device_registry import async_get @@ -748,3 +753,16 @@ async def test_setup_hardware_integration(hass, aioclient_mock, integration): assert aioclient_mock.call_count == 15 assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_get_store_addon_info(hass, hassio_stubs, aioclient_mock): + """Test get store add-on info from Supervisor API.""" + aioclient_mock.clear_requests() + aioclient_mock.get( + "http://127.0.0.1/store/addons/test", + json={"result": "ok", "data": {"name": "bla"}}, + ) + + data = await async_get_addon_store_info(hass, "test") + assert data["name"] == "bla" + assert aioclient_mock.call_count == 1 diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 7cf7ebd7ea2..1524aca719e 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -38,18 +38,56 @@ def mock_addon_info(addon_info_side_effect): yield addon_info +@pytest.fixture(name="addon_store_info_side_effect") +def addon_store_info_side_effect_fixture(): + """Return the add-on store info side effect.""" + return None + + +@pytest.fixture(name="addon_store_info") +def mock_addon_store_info(addon_store_info_side_effect): + """Mock Supervisor add-on info.""" + with patch( + "homeassistant.components.zwave_js.addon.async_get_addon_store_info", + side_effect=addon_store_info_side_effect, + ) as addon_store_info: + addon_store_info.return_value = { + "installed": None, + "state": None, + "version": "1.0.0", + } + yield addon_store_info + + @pytest.fixture(name="addon_running") -def mock_addon_running(addon_info): +def mock_addon_running(addon_store_info, addon_info): """Mock add-on already running.""" + addon_store_info.return_value = { + "installed": "1.0.0", + "state": "started", + "version": "1.0.0", + } addon_info.return_value["state"] = "started" + addon_info.return_value["version"] = "1.0.0" return addon_info @pytest.fixture(name="addon_installed") -def mock_addon_installed(addon_info): +def mock_addon_installed(addon_store_info, addon_info): """Mock add-on already installed but not running.""" + addon_store_info.return_value = { + "installed": "1.0.0", + "state": "stopped", + "version": "1.0.0", + } addon_info.return_value["state"] = "stopped" - addon_info.return_value["version"] = "1.0" + addon_info.return_value["version"] = "1.0.0" + return addon_info + + +@pytest.fixture(name="addon_not_installed") +def mock_addon_not_installed(addon_store_info, addon_info): + """Mock add-on not installed.""" return addon_info @@ -81,13 +119,18 @@ def mock_set_addon_options(set_addon_options_side_effect): @pytest.fixture(name="install_addon_side_effect") -def install_addon_side_effect_fixture(addon_info): +def install_addon_side_effect_fixture(addon_store_info, addon_info): """Return the install add-on side effect.""" async def install_addon(hass, slug): """Mock install add-on.""" + addon_store_info.return_value = { + "installed": "1.0.0", + "state": "stopped", + "version": "1.0.0", + } addon_info.return_value["state"] = "stopped" - addon_info.return_value["version"] = "1.0" + addon_info.return_value["version"] = "1.0.0" return install_addon @@ -112,11 +155,16 @@ def mock_update_addon(): @pytest.fixture(name="start_addon_side_effect") -def start_addon_side_effect_fixture(addon_info): +def start_addon_side_effect_fixture(addon_store_info, addon_info): """Return the start add-on options side effect.""" async def start_addon(hass, slug): """Mock start add-on.""" + addon_store_info.return_value = { + "installed": "1.0.0", + "state": "started", + "version": "1.0.0", + } addon_info.return_value["state"] = "started" return start_addon diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index f107a5fd8e2..a8a2c6c7191 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -422,7 +422,7 @@ async def test_abort_discovery_with_existing_entry( async def test_abort_hassio_discovery_with_existing_flow( - hass, supervisor, addon_options + hass, supervisor, addon_installed, addon_options ): """Test hassio discovery flow is aborted when another discovery has happened.""" result = await hass.config_entries.flow.async_init( @@ -701,15 +701,13 @@ async def test_discovery_addon_not_running( async def test_discovery_addon_not_installed( hass, supervisor, - addon_installed, + addon_not_installed, install_addon, addon_options, set_addon_options, start_addon, ): """Test discovery with add-on not installed.""" - addon_installed.return_value["version"] = None - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_HASSIO}, @@ -1443,7 +1441,7 @@ async def test_addon_installed_already_configured( async def test_addon_not_installed( hass, supervisor, - addon_installed, + addon_not_installed, install_addon, addon_options, set_addon_options, @@ -1451,8 +1449,6 @@ async def test_addon_not_installed( get_addon_discovery_info, ): """Test add-on not installed.""" - addon_installed.return_value["version"] = None - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -1533,9 +1529,10 @@ async def test_addon_not_installed( assert len(mock_setup_entry.mock_calls) == 1 -async def test_install_addon_failure(hass, supervisor, addon_installed, install_addon): +async def test_install_addon_failure( + hass, supervisor, addon_not_installed, install_addon +): """Test add-on install failure.""" - addon_installed.return_value["version"] = None install_addon.side_effect = HassioAPIError() result = await hass.config_entries.flow.async_init( @@ -2292,7 +2289,7 @@ async def test_options_addon_not_installed( hass, client, supervisor, - addon_installed, + addon_not_installed, install_addon, integration, addon_options, @@ -2306,7 +2303,6 @@ async def test_options_addon_not_installed( disconnect_calls, ): """Test options flow and add-on not installed on Supervisor.""" - addon_installed.return_value["version"] = None addon_options.update(old_addon_options) entry = integration entry.unique_id = "1234" diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index a2962261ac3..202088bb481 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -432,10 +432,14 @@ async def test_start_addon( async def test_install_addon( - hass, addon_installed, install_addon, addon_options, set_addon_options, start_addon + hass, + addon_not_installed, + install_addon, + addon_options, + set_addon_options, + start_addon, ): """Test install and start the Z-Wave JS add-on during entry setup.""" - addon_installed.return_value["version"] = None device = "/test" s0_legacy_key = "s0_legacy" s2_access_control_key = "s2_access_control" @@ -583,10 +587,10 @@ async def test_addon_options_changed( "addon_version, update_available, update_calls, backup_calls, " "update_addon_side_effect, create_backup_side_effect", [ - ("1.0", True, 1, 1, None, None), - ("1.0", False, 0, 0, None, None), - ("1.0", True, 1, 1, HassioAPIError("Boom"), None), - ("1.0", True, 0, 1, None, HassioAPIError("Boom")), + ("1.0.0", True, 1, 1, None, None), + ("1.0.0", False, 0, 0, None, None), + ("1.0.0", True, 1, 1, HassioAPIError("Boom"), None), + ("1.0.0", True, 0, 1, None, HassioAPIError("Boom")), ], ) async def test_update_addon( @@ -720,7 +724,7 @@ async def test_remove_entry( assert create_backup.call_count == 1 assert create_backup.call_args == call( hass, - {"name": "addon_core_zwave_js_1.0", "addons": ["core_zwave_js"]}, + {"name": "addon_core_zwave_js_1.0.0", "addons": ["core_zwave_js"]}, partial=True, ) assert uninstall_addon.call_count == 1 @@ -762,7 +766,7 @@ async def test_remove_entry( assert create_backup.call_count == 1 assert create_backup.call_args == call( hass, - {"name": "addon_core_zwave_js_1.0", "addons": ["core_zwave_js"]}, + {"name": "addon_core_zwave_js_1.0.0", "addons": ["core_zwave_js"]}, partial=True, ) assert uninstall_addon.call_count == 0 @@ -786,7 +790,7 @@ async def test_remove_entry( assert create_backup.call_count == 1 assert create_backup.call_args == call( hass, - {"name": "addon_core_zwave_js_1.0", "addons": ["core_zwave_js"]}, + {"name": "addon_core_zwave_js_1.0.0", "addons": ["core_zwave_js"]}, partial=True, ) assert uninstall_addon.call_count == 1 From d2955a48b089f7f77caf9fdc15e2add20c9158a7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 3 Aug 2022 07:46:54 -1000 Subject: [PATCH 02/19] Bump bleak to 0.15.1 (#76136) --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 40e63ec7180..f3828db5d10 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/bluetooth", "dependencies": ["websocket_api"], "quality_scale": "internal", - "requirements": ["bleak==0.15.0", "bluetooth-adapters==0.1.3"], + "requirements": ["bleak==0.15.1", "bluetooth-adapters==0.1.3"], "codeowners": ["@bdraco"], "config_flow": true, "iot_class": "local_push" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index f2140a9eca7..eed64c6bd5b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ atomicwrites-homeassistant==1.4.1 attrs==21.2.0 awesomeversion==22.6.0 bcrypt==3.1.7 -bleak==0.15.0 +bleak==0.15.1 bluetooth-adapters==0.1.3 certifi>=2021.5.30 ciso8601==2.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 6e1850d33a7..bb06b8fa82d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -405,7 +405,7 @@ bimmer_connected==0.10.1 bizkaibus==0.1.1 # homeassistant.components.bluetooth -bleak==0.15.0 +bleak==0.15.1 # homeassistant.components.blebox blebox_uniapi==2.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6f75b540b28..e9afa3f745c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -326,7 +326,7 @@ bellows==0.31.2 bimmer_connected==0.10.1 # homeassistant.components.bluetooth -bleak==0.15.0 +bleak==0.15.1 # homeassistant.components.blebox blebox_uniapi==2.0.2 From 5c9d557b10f8af86161eaccb6d7f9ab1c58d1628 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 4 Aug 2022 09:13:20 +0200 Subject: [PATCH 03/19] Allow climate operation mode fan_only as custom mode in Alexa (#76148) * Add support for FAN_ONLY mode * Tests for fan_only as custom mode --- homeassistant/components/alexa/const.py | 19 +++++++++++-------- tests/components/alexa/test_capabilities.py | 19 ++++++++++++++++++- tests/components/alexa/test_smart_home.py | 16 ++++++++++++++-- 3 files changed, 43 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/alexa/const.py b/homeassistant/components/alexa/const.py index 6b509d9b3c6..d51409a5a1c 100644 --- a/homeassistant/components/alexa/const.py +++ b/homeassistant/components/alexa/const.py @@ -68,16 +68,19 @@ API_TEMP_UNITS = {TEMP_FAHRENHEIT: "FAHRENHEIT", TEMP_CELSIUS: "CELSIUS"} # back to HA state. API_THERMOSTAT_MODES = OrderedDict( [ - (climate.HVAC_MODE_HEAT, "HEAT"), - (climate.HVAC_MODE_COOL, "COOL"), - (climate.HVAC_MODE_HEAT_COOL, "AUTO"), - (climate.HVAC_MODE_AUTO, "AUTO"), - (climate.HVAC_MODE_OFF, "OFF"), - (climate.HVAC_MODE_FAN_ONLY, "OFF"), - (climate.HVAC_MODE_DRY, "CUSTOM"), + (climate.HVACMode.HEAT, "HEAT"), + (climate.HVACMode.COOL, "COOL"), + (climate.HVACMode.HEAT_COOL, "AUTO"), + (climate.HVACMode.AUTO, "AUTO"), + (climate.HVACMode.OFF, "OFF"), + (climate.HVACMode.FAN_ONLY, "CUSTOM"), + (climate.HVACMode.DRY, "CUSTOM"), ] ) -API_THERMOSTAT_MODES_CUSTOM = {climate.HVAC_MODE_DRY: "DEHUMIDIFY"} +API_THERMOSTAT_MODES_CUSTOM = { + climate.HVACMode.DRY: "DEHUMIDIFY", + climate.HVACMode.FAN_ONLY: "FAN", +} API_THERMOSTAT_PRESETS = {climate.PRESET_ECO: "ECO"} # AlexaModeController does not like a single mode for the fan preset, we add PRESET_MODE_NA if a fan has only one preset_mode diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py index ea6c96bbaef..10ad5f7ebd2 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -590,7 +590,7 @@ async def test_report_climate_state(hass): {"value": 34.0, "scale": "CELSIUS"}, ) - for off_modes in (climate.HVAC_MODE_OFF, climate.HVAC_MODE_FAN_ONLY): + for off_modes in [climate.HVAC_MODE_OFF]: hass.states.async_set( "climate.downstairs", off_modes, @@ -626,6 +626,23 @@ async def test_report_climate_state(hass): "Alexa.TemperatureSensor", "temperature", {"value": 34.0, "scale": "CELSIUS"} ) + # assert fan_only is reported as CUSTOM + hass.states.async_set( + "climate.downstairs", + "fan_only", + { + "friendly_name": "Climate Downstairs", + "supported_features": 91, + climate.ATTR_CURRENT_TEMPERATURE: 31, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + }, + ) + properties = await reported_properties(hass, "climate.downstairs") + properties.assert_equal("Alexa.ThermostatController", "thermostatMode", "CUSTOM") + properties.assert_equal( + "Alexa.TemperatureSensor", "temperature", {"value": 31.0, "scale": "CELSIUS"} + ) + hass.states.async_set( "climate.heat", "heat", diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 0169eeff9d5..df45d90358b 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -2030,7 +2030,7 @@ async def test_thermostat(hass): "current_temperature": 75.0, "friendly_name": "Test Thermostat", "supported_features": 1 | 2 | 4 | 128, - "hvac_modes": ["off", "heat", "cool", "auto", "dry"], + "hvac_modes": ["off", "heat", "cool", "auto", "dry", "fan_only"], "preset_mode": None, "preset_modes": ["eco"], "min_temp": 50, @@ -2220,7 +2220,7 @@ async def test_thermostat(hass): properties = ReportedProperties(msg["context"]["properties"]) properties.assert_equal("Alexa.ThermostatController", "thermostatMode", "HEAT") - # Assert we can call custom modes + # Assert we can call custom modes for dry and fan_only call, msg = await assert_request_calls_service( "Alexa.ThermostatController", "SetThermostatMode", @@ -2233,6 +2233,18 @@ async def test_thermostat(hass): properties = ReportedProperties(msg["context"]["properties"]) properties.assert_equal("Alexa.ThermostatController", "thermostatMode", "CUSTOM") + call, msg = await assert_request_calls_service( + "Alexa.ThermostatController", + "SetThermostatMode", + "climate#test_thermostat", + "climate.set_hvac_mode", + hass, + payload={"thermostatMode": {"value": "CUSTOM", "customName": "FAN"}}, + ) + assert call.data["hvac_mode"] == "fan_only" + properties = ReportedProperties(msg["context"]["properties"]) + properties.assert_equal("Alexa.ThermostatController", "thermostatMode", "CUSTOM") + # assert unsupported custom mode msg = await assert_request_fails( "Alexa.ThermostatController", From 6340da72a5b1f166cbecd89f33354fa07ea0af8f Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Thu, 4 Aug 2022 18:36:37 +0100 Subject: [PATCH 04/19] Remove icon attribute if device class is set (#76161) --- homeassistant/components/integration/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index 5d0dde3e4de..b3b8a2a2b9d 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -223,6 +223,7 @@ class IntegrationSensor(RestoreEntity, SensorEntity): == SensorDeviceClass.POWER ): self._attr_device_class = SensorDeviceClass.ENERGY + self._attr_icon = None update_state = True if update_state: From 11319defaebe4d1f6eeeaba907d7bae1698af5dd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 4 Aug 2022 07:44:22 -1000 Subject: [PATCH 05/19] Fix flux_led ignored entries not being respected (#76173) --- .../components/flux_led/config_flow.py | 44 +++++++++++-------- tests/components/flux_led/test_config_flow.py | 27 ++++++++++++ 2 files changed, 52 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/flux_led/config_flow.py b/homeassistant/components/flux_led/config_flow.py index 61395d744b3..b245c0c2bc2 100644 --- a/homeassistant/components/flux_led/config_flow.py +++ b/homeassistant/components/flux_led/config_flow.py @@ -105,27 +105,33 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): assert mac_address is not None mac = dr.format_mac(mac_address) await self.async_set_unique_id(mac) - for entry in self._async_current_entries(include_ignore=False): - if entry.data[CONF_HOST] == device[ATTR_IPADDR] or ( - entry.unique_id - and ":" in entry.unique_id - and mac_matches_by_one(entry.unique_id, mac) + for entry in self._async_current_entries(include_ignore=True): + if not ( + entry.data.get(CONF_HOST) == device[ATTR_IPADDR] + or ( + entry.unique_id + and ":" in entry.unique_id + and mac_matches_by_one(entry.unique_id, mac) + ) ): - if ( - async_update_entry_from_discovery( - self.hass, entry, device, None, allow_update_mac - ) - or entry.state == config_entries.ConfigEntryState.SETUP_RETRY - ): - self.hass.async_create_task( - self.hass.config_entries.async_reload(entry.entry_id) - ) - else: - async_dispatcher_send( - self.hass, - FLUX_LED_DISCOVERY_SIGNAL.format(entry_id=entry.entry_id), - ) + continue + if entry.source == config_entries.SOURCE_IGNORE: raise AbortFlow("already_configured") + if ( + async_update_entry_from_discovery( + self.hass, entry, device, None, allow_update_mac + ) + or entry.state == config_entries.ConfigEntryState.SETUP_RETRY + ): + self.hass.async_create_task( + self.hass.config_entries.async_reload(entry.entry_id) + ) + else: + async_dispatcher_send( + self.hass, + FLUX_LED_DISCOVERY_SIGNAL.format(entry_id=entry.entry_id), + ) + raise AbortFlow("already_configured") async def _async_handle_discovery(self) -> FlowResult: """Handle any discovery.""" diff --git a/tests/components/flux_led/test_config_flow.py b/tests/components/flux_led/test_config_flow.py index 8abdb8e955b..3f1704f7e8c 100644 --- a/tests/components/flux_led/test_config_flow.py +++ b/tests/components/flux_led/test_config_flow.py @@ -695,3 +695,30 @@ async def test_options(hass: HomeAssistant): assert result2["data"] == user_input assert result2["data"] == config_entry.options assert hass.states.get("light.bulb_rgbcw_ddeeff") is not None + + +@pytest.mark.parametrize( + "source, data", + [ + (config_entries.SOURCE_DHCP, DHCP_DISCOVERY), + (config_entries.SOURCE_INTEGRATION_DISCOVERY, FLUX_DISCOVERY), + ], +) +async def test_discovered_can_be_ignored(hass, source, data): + """Test we abort if the mac was already ignored.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={}, + unique_id=MAC_ADDRESS, + source=config_entries.SOURCE_IGNORE, + ) + config_entry.add_to_hass(hass) + + with _patch_discovery(), _patch_wifibulb(): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": source}, data=data + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" From 60da54558eebed751bdf0fe0f46424804f36ff99 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 4 Aug 2022 05:58:15 -1000 Subject: [PATCH 06/19] Fix race in bluetooth async_process_advertisements (#76176) --- homeassistant/components/bluetooth/__init__.py | 2 +- tests/components/bluetooth/test_init.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index c91563d7729..0b81472f838 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -188,7 +188,7 @@ async def async_process_advertisements( def _async_discovered_device( service_info: BluetoothServiceInfoBleak, change: BluetoothChange ) -> None: - if callback(service_info): + if not done.done() and callback(service_info): done.set_result(service_info) unload = async_register_callback(hass, _async_discovered_device, match_dict, mode) diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index edc5eb024a6..ba315b1f380 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -856,6 +856,9 @@ async def test_process_advertisements_bail_on_good_advertisement( ) _get_underlying_scanner()._callback(device, adv) + _get_underlying_scanner()._callback(device, adv) + _get_underlying_scanner()._callback(device, adv) + await asyncio.sleep(0) result = await handle From 450af52bac303b7e9d142ddaa8727ade518f0a91 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 4 Aug 2022 11:45:28 -0600 Subject: [PATCH 07/19] Add repair item to remove no-longer-functioning Flu Near You integration (#76177) Co-authored-by: Franck Nijhof --- .coveragerc | 1 + .../components/flunearyou/__init__.py | 10 +++++ .../components/flunearyou/manifest.json | 1 + .../components/flunearyou/repairs.py | 42 +++++++++++++++++++ .../components/flunearyou/strings.json | 13 ++++++ .../flunearyou/translations/en.json | 13 ++++++ 6 files changed, 80 insertions(+) create mode 100644 homeassistant/components/flunearyou/repairs.py diff --git a/.coveragerc b/.coveragerc index d529cdbd9ca..97043eaa494 100644 --- a/.coveragerc +++ b/.coveragerc @@ -388,6 +388,7 @@ omit = homeassistant/components/flume/__init__.py homeassistant/components/flume/sensor.py homeassistant/components/flunearyou/__init__.py + homeassistant/components/flunearyou/repairs.py homeassistant/components/flunearyou/sensor.py homeassistant/components/folder/sensor.py homeassistant/components/folder_watcher/* diff --git a/homeassistant/components/flunearyou/__init__.py b/homeassistant/components/flunearyou/__init__.py index 5e48e1561b5..75349002ec0 100644 --- a/homeassistant/components/flunearyou/__init__.py +++ b/homeassistant/components/flunearyou/__init__.py @@ -9,6 +9,7 @@ from typing import Any from pyflunearyou import Client from pyflunearyou.errors import FluNearYouError +from homeassistant.components.repairs import IssueSeverity, async_create_issue from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, Platform from homeassistant.core import HomeAssistant @@ -26,6 +27,15 @@ PLATFORMS = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Flu Near You as config entry.""" + async_create_issue( + hass, + DOMAIN, + "integration_removal", + is_fixable=True, + severity=IssueSeverity.ERROR, + translation_key="integration_removal", + ) + websession = aiohttp_client.async_get_clientsession(hass) client = Client(session=websession) diff --git a/homeassistant/components/flunearyou/manifest.json b/homeassistant/components/flunearyou/manifest.json index ee69961d1b0..fa98bf2e01e 100644 --- a/homeassistant/components/flunearyou/manifest.json +++ b/homeassistant/components/flunearyou/manifest.json @@ -3,6 +3,7 @@ "name": "Flu Near You", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flunearyou", + "dependencies": ["repairs"], "requirements": ["pyflunearyou==2.0.2"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", diff --git a/homeassistant/components/flunearyou/repairs.py b/homeassistant/components/flunearyou/repairs.py new file mode 100644 index 00000000000..f48085ba623 --- /dev/null +++ b/homeassistant/components/flunearyou/repairs.py @@ -0,0 +1,42 @@ +"""Repairs platform for the Flu Near You integration.""" +from __future__ import annotations + +import asyncio + +import voluptuous as vol + +from homeassistant import data_entry_flow +from homeassistant.components.repairs import RepairsFlow +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + + +class FluNearYouFixFlow(RepairsFlow): + """Handler for an issue fixing flow.""" + + async def async_step_init( + self, user_input: dict[str, str] | None = None + ) -> data_entry_flow.FlowResult: + """Handle the first step of a fix flow.""" + return await self.async_step_confirm() + + async def async_step_confirm( + self, user_input: dict[str, str] | None = None + ) -> data_entry_flow.FlowResult: + """Handle the confirm step of a fix flow.""" + if user_input is not None: + removal_tasks = [ + self.hass.config_entries.async_remove(entry.entry_id) + for entry in self.hass.config_entries.async_entries(DOMAIN) + ] + await asyncio.gather(*removal_tasks) + return self.async_create_entry(title="Fixed issue", data={}) + return self.async_show_form(step_id="confirm", data_schema=vol.Schema({})) + + +async def async_create_fix_flow( + hass: HomeAssistant, issue_id: str +) -> FluNearYouFixFlow: + """Create flow.""" + return FluNearYouFixFlow() diff --git a/homeassistant/components/flunearyou/strings.json b/homeassistant/components/flunearyou/strings.json index 4df0326fc3b..59ec6125a34 100644 --- a/homeassistant/components/flunearyou/strings.json +++ b/homeassistant/components/flunearyou/strings.json @@ -16,5 +16,18 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_location%]" } + }, + "issues": { + "integration_removal": { + "title": "Flu Near You is no longer available", + "fix_flow": { + "step": { + "confirm": { + "title": "Remove Flu Near You", + "description": "The external data source powering the Flu Near You integration is no longer available; thus, the integration no longer works.\n\nPress SUBMIT to remove Flu Near You from your Home Assistant instance." + } + } + } + } } } diff --git a/homeassistant/components/flunearyou/translations/en.json b/homeassistant/components/flunearyou/translations/en.json index 29af5b2b288..3dcbfa2a628 100644 --- a/homeassistant/components/flunearyou/translations/en.json +++ b/homeassistant/components/flunearyou/translations/en.json @@ -16,5 +16,18 @@ "title": "Configure Flu Near You" } } + }, + "issues": { + "integration_removal": { + "fix_flow": { + "step": { + "confirm": { + "description": "The data source that powered the Flu Near You integration is no longer available. Press SUBMIT to remove all configured instances of the integration from Home Assistant.", + "title": "Remove Flu Near You" + } + } + }, + "title": "Flu Near You is no longer available" + } } } \ No newline at end of file From 2710e4b5ec6d022ac7be9d9972f52f5909539188 Mon Sep 17 00:00:00 2001 From: On Freund Date: Thu, 4 Aug 2022 21:57:53 +0300 Subject: [PATCH 08/19] Fix arm away in Risco (#76188) --- homeassistant/components/risco/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/risco/manifest.json b/homeassistant/components/risco/manifest.json index a7c07af3e18..fb4b8203aac 100644 --- a/homeassistant/components/risco/manifest.json +++ b/homeassistant/components/risco/manifest.json @@ -3,7 +3,7 @@ "name": "Risco", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/risco", - "requirements": ["pyrisco==0.5.0"], + "requirements": ["pyrisco==0.5.2"], "codeowners": ["@OnFreund"], "quality_scale": "platinum", "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index bb06b8fa82d..ff14c01a24b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1784,7 +1784,7 @@ pyrecswitch==1.0.2 pyrepetierng==0.1.0 # homeassistant.components.risco -pyrisco==0.5.0 +pyrisco==0.5.2 # homeassistant.components.rituals_perfume_genie pyrituals==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e9afa3f745c..f260265fd34 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1228,7 +1228,7 @@ pyps4-2ndscreen==1.3.1 pyqwikswitch==0.93 # homeassistant.components.risco -pyrisco==0.5.0 +pyrisco==0.5.2 # homeassistant.components.rituals_perfume_genie pyrituals==0.0.6 From 854ca853dc925013b7dab67312bfe1b7e6c3ac15 Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Thu, 4 Aug 2022 17:04:12 +0300 Subject: [PATCH 09/19] Fix nullable ip_address in mikrotik (#76197) --- homeassistant/components/mikrotik/device_tracker.py | 2 +- homeassistant/components/mikrotik/hub.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mikrotik/device_tracker.py b/homeassistant/components/mikrotik/device_tracker.py index 158d95dd683..6071c6e4f93 100644 --- a/homeassistant/components/mikrotik/device_tracker.py +++ b/homeassistant/components/mikrotik/device_tracker.py @@ -116,7 +116,7 @@ class MikrotikDataUpdateCoordinatorTracker( return self.device.mac @property - def ip_address(self) -> str: + def ip_address(self) -> str | None: """Return the mac address of the client.""" return self.device.ip_address diff --git a/homeassistant/components/mikrotik/hub.py b/homeassistant/components/mikrotik/hub.py index 66fe7226d9b..914911ee5cc 100644 --- a/homeassistant/components/mikrotik/hub.py +++ b/homeassistant/components/mikrotik/hub.py @@ -60,9 +60,9 @@ class Device: return self._params.get("host-name", self.mac) @property - def ip_address(self) -> str: + def ip_address(self) -> str | None: """Return device primary ip address.""" - return self._params["address"] + return self._params.get("address") @property def mac(self) -> str: From a4049e93d8fd5ff30e09581a471fe87630511dcd Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 4 Aug 2022 13:21:37 +0200 Subject: [PATCH 10/19] Mark RPI Power binary sensor as diagnostic (#76198) --- homeassistant/components/rpi_power/binary_sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/rpi_power/binary_sensor.py b/homeassistant/components/rpi_power/binary_sensor.py index f70581a8075..08535daf970 100644 --- a/homeassistant/components/rpi_power/binary_sensor.py +++ b/homeassistant/components/rpi_power/binary_sensor.py @@ -13,6 +13,7 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback _LOGGER = logging.getLogger(__name__) @@ -35,6 +36,7 @@ class RaspberryChargerBinarySensor(BinarySensorEntity): """Binary sensor representing the rpi power status.""" _attr_device_class = BinarySensorDeviceClass.PROBLEM + _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_icon = "mdi:raspberry-pi" _attr_name = "RPi Power status" _attr_unique_id = "rpi_power" # only one sensor possible From 1a030f118ac327a5a4ba9b07634fcc4ae1e71b72 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 4 Aug 2022 05:38:55 -1000 Subject: [PATCH 11/19] BLE pairing reliablity fixes for HomeKit Controller (#76199) - Remove the cached map from memory when unpairing so we do not reuse it again if they unpair/repair - Fixes for accessories that use a config number of 0 - General reliablity improvements to the pairing process under the hood of aiohomekit --- .../components/homekit_controller/__init__.py | 7 ++++--- .../homekit_controller/config_flow.py | 2 +- .../homekit_controller/manifest.json | 2 +- .../components/homekit_controller/storage.py | 19 ++++++++++++++----- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../homekit_controller/test_config_flow.py | 6 ++++++ 7 files changed, 28 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 37dd648dedb..b2ccad9a457 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -31,7 +31,7 @@ from homeassistant.helpers.typing import ConfigType from .config_flow import normalize_hkid from .connection import HKDevice, valid_serial_number from .const import ENTITY_MAP, KNOWN_DEVICES, TRIGGERS -from .storage import async_get_entity_storage +from .storage import EntityMapStorage, async_get_entity_storage from .utils import async_get_controller, folded_name _LOGGER = logging.getLogger(__name__) @@ -269,7 +269,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hkid = entry.data["AccessoryPairingID"] if hkid in hass.data[KNOWN_DEVICES]: - connection = hass.data[KNOWN_DEVICES][hkid] + connection: HKDevice = hass.data[KNOWN_DEVICES][hkid] await connection.async_unload() return True @@ -280,7 +280,8 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: hkid = entry.data["AccessoryPairingID"] # Remove cached type data from .storage/homekit_controller-entity-map - hass.data[ENTITY_MAP].async_delete_map(hkid) + entity_map_storage: EntityMapStorage = hass.data[ENTITY_MAP] + entity_map_storage.async_delete_map(hkid) controller = await async_get_controller(hass) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 31677e37b20..6920f048edb 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -569,7 +569,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): entity_storage = await async_get_entity_storage(self.hass) assert self.unique_id is not None entity_storage.async_create_or_update_map( - self.unique_id, + pairing.id, accessories_state.config_num, accessories_state.accessories.serialize(), ) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index ac1be576906..5aff66fe757 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==1.2.3"], + "requirements": ["aiohomekit==1.2.4"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/homeassistant/components/homekit_controller/storage.py b/homeassistant/components/homekit_controller/storage.py index 8c0628c97f6..51d8ce4ffd3 100644 --- a/homeassistant/components/homekit_controller/storage.py +++ b/homeassistant/components/homekit_controller/storage.py @@ -2,6 +2,7 @@ from __future__ import annotations +import logging from typing import Any, TypedDict from homeassistant.core import HomeAssistant, callback @@ -12,6 +13,7 @@ from .const import DOMAIN, ENTITY_MAP ENTITY_MAP_STORAGE_KEY = f"{DOMAIN}-entity-map" ENTITY_MAP_STORAGE_VERSION = 1 ENTITY_MAP_SAVE_DELAY = 10 +_LOGGER = logging.getLogger(__name__) class Pairing(TypedDict): @@ -68,6 +70,7 @@ class EntityMapStorage: self, homekit_id: str, config_num: int, accessories: list[Any] ) -> Pairing: """Create a new pairing cache.""" + _LOGGER.debug("Creating or updating entity map for %s", homekit_id) data = Pairing(config_num=config_num, accessories=accessories) self.storage_data[homekit_id] = data self._async_schedule_save() @@ -76,11 +79,17 @@ class EntityMapStorage: @callback def async_delete_map(self, homekit_id: str) -> None: """Delete pairing cache.""" - if homekit_id not in self.storage_data: - return - - self.storage_data.pop(homekit_id) - self._async_schedule_save() + removed_one = False + # Previously there was a bug where a lowercase homekit_id was stored + # in the storage. We need to account for that. + for hkid in (homekit_id, homekit_id.lower()): + if hkid not in self.storage_data: + continue + _LOGGER.debug("Deleting entity map for %s", hkid) + self.storage_data.pop(hkid) + removed_one = True + if removed_one: + self._async_schedule_save() @callback def _async_schedule_save(self) -> None: diff --git a/requirements_all.txt b/requirements_all.txt index ff14c01a24b..2785a3cdd1a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.2.3 +aiohomekit==1.2.4 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f260265fd34..283f9b19940 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.2.3 +aiohomekit==1.2.4 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index 78d3c609a9c..e72d9452e52 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -14,6 +14,7 @@ from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.components.homekit_controller import config_flow from homeassistant.components.homekit_controller.const import KNOWN_DEVICES +from homeassistant.components.homekit_controller.storage import async_get_entity_storage from homeassistant.data_entry_flow import ( RESULT_TYPE_ABORT, RESULT_TYPE_FORM, @@ -1071,6 +1072,8 @@ async def test_bluetooth_valid_device_discovery_paired(hass, controller): async def test_bluetooth_valid_device_discovery_unpaired(hass, controller): """Test bluetooth discovery with a homekit device and discovery works.""" setup_mock_accessory(controller) + storage = await async_get_entity_storage(hass) + with patch( "homeassistant.components.homekit_controller.config_flow.aiohomekit_const.BLE_TRANSPORT_SUPPORTED", True, @@ -1083,6 +1086,7 @@ async def test_bluetooth_valid_device_discovery_unpaired(hass, controller): assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "pair" + assert storage.get_map("00:00:00:00:00:00") is None assert get_flow_context(hass, result) == { "source": config_entries.SOURCE_BLUETOOTH, @@ -1098,3 +1102,5 @@ async def test_bluetooth_valid_device_discovery_unpaired(hass, controller): assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["title"] == "Koogeek-LS1-20833F" assert result3["data"] == {} + + assert storage.get_map("00:00:00:00:00:00") is not None From 31fed328ce3a942165d88a6b8711c4e79f119351 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 4 Aug 2022 14:01:26 +0200 Subject: [PATCH 12/19] Bump NextDNS library (#76207) --- homeassistant/components/nextdns/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/nextdns/test_diagnostics.py | 2 ++ 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nextdns/manifest.json b/homeassistant/components/nextdns/manifest.json index a427f930db8..3e2d3ebb3d0 100644 --- a/homeassistant/components/nextdns/manifest.json +++ b/homeassistant/components/nextdns/manifest.json @@ -3,7 +3,7 @@ "name": "NextDNS", "documentation": "https://www.home-assistant.io/integrations/nextdns", "codeowners": ["@bieniu"], - "requirements": ["nextdns==1.0.1"], + "requirements": ["nextdns==1.0.2"], "config_flow": true, "iot_class": "cloud_polling", "loggers": ["nextdns"] diff --git a/requirements_all.txt b/requirements_all.txt index 2785a3cdd1a..4dfca99bcb0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1103,7 +1103,7 @@ nextcloudmonitor==1.1.0 nextcord==2.0.0a8 # homeassistant.components.nextdns -nextdns==1.0.1 +nextdns==1.0.2 # homeassistant.components.niko_home_control niko-home-control==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 283f9b19940..2fbaa13c20f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -784,7 +784,7 @@ nexia==2.0.2 nextcord==2.0.0a8 # homeassistant.components.nextdns -nextdns==1.0.1 +nextdns==1.0.2 # homeassistant.components.nfandroidtv notifications-android-tv==0.1.5 diff --git a/tests/components/nextdns/test_diagnostics.py b/tests/components/nextdns/test_diagnostics.py index 85dbceafff9..0702a2fa1d8 100644 --- a/tests/components/nextdns/test_diagnostics.py +++ b/tests/components/nextdns/test_diagnostics.py @@ -48,11 +48,13 @@ async def test_entry_diagnostics( } assert result["protocols_coordinator_data"] == { "doh_queries": 20, + "doh3_queries": 0, "doq_queries": 10, "dot_queries": 30, "tcp_queries": 0, "udp_queries": 40, "doh_queries_ratio": 20.0, + "doh3_queries_ratio": 0.0, "doq_queries_ratio": 10.0, "dot_queries_ratio": 30.0, "tcp_queries_ratio": 0.0, From 1808dd3d84d493114d2401da2d045f8248fbb91e Mon Sep 17 00:00:00 2001 From: mkmer Date: Thu, 4 Aug 2022 12:01:58 -0400 Subject: [PATCH 13/19] Bump AIOAladdin Connect to 0.1.41 (#76217) --- homeassistant/components/aladdin_connect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index 5e55f391aa6..febba16170a 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "aladdin_connect", "name": "Aladdin Connect", "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", - "requirements": ["AIOAladdinConnect==0.1.39"], + "requirements": ["AIOAladdinConnect==0.1.41"], "codeowners": ["@mkmer"], "iot_class": "cloud_polling", "loggers": ["aladdin_connect"], diff --git a/requirements_all.txt b/requirements_all.txt index 4dfca99bcb0..3f29742133e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,7 +5,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.39 +AIOAladdinConnect==0.1.41 # homeassistant.components.adax Adax-local==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2fbaa13c20f..f95649de7f2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.39 +AIOAladdinConnect==0.1.41 # homeassistant.components.adax Adax-local==0.1.4 From db227a888d5927b77e54c696ef231bbb54421880 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 4 Aug 2022 15:57:00 +0200 Subject: [PATCH 14/19] Fix spelling of OpenWrt in luci integration manifest (#76219) --- homeassistant/components/luci/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/luci/manifest.json b/homeassistant/components/luci/manifest.json index 2d61852689a..b24c0234de9 100644 --- a/homeassistant/components/luci/manifest.json +++ b/homeassistant/components/luci/manifest.json @@ -1,6 +1,6 @@ { "domain": "luci", - "name": "OpenWRT (luci)", + "name": "OpenWrt (luci)", "documentation": "https://www.home-assistant.io/integrations/luci", "requirements": ["openwrt-luci-rpc==1.1.11"], "codeowners": ["@mzdrale"], From a17e99f714505de0073e0e29c186dd644992709c Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Thu, 4 Aug 2022 14:28:59 -0500 Subject: [PATCH 15/19] Fix Life360 recovery from server errors (#76231) --- .../components/life360/coordinator.py | 4 +- .../components/life360/device_tracker.py | 86 +++++++++++-------- 2 files changed, 53 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/life360/coordinator.py b/homeassistant/components/life360/coordinator.py index 05eecd43cdc..ed774bba8ca 100644 --- a/homeassistant/components/life360/coordinator.py +++ b/homeassistant/components/life360/coordinator.py @@ -91,9 +91,11 @@ class Life360Data: members: dict[str, Life360Member] = field(init=False, default_factory=dict) -class Life360DataUpdateCoordinator(DataUpdateCoordinator): +class Life360DataUpdateCoordinator(DataUpdateCoordinator[Life360Data]): """Life360 data update coordinator.""" + config_entry: ConfigEntry + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: """Initialize data update coordinator.""" super().__init__( diff --git a/homeassistant/components/life360/device_tracker.py b/homeassistant/components/life360/device_tracker.py index 5a18422487e..960bfb78cc2 100644 --- a/homeassistant/components/life360/device_tracker.py +++ b/homeassistant/components/life360/device_tracker.py @@ -11,10 +11,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_BATTERY_CHARGING from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( ATTR_ADDRESS, @@ -31,6 +28,7 @@ from .const import ( LOGGER, SHOW_DRIVING, ) +from .coordinator import Life360DataUpdateCoordinator, Life360Member _LOC_ATTRS = ( "address", @@ -95,23 +93,27 @@ async def async_setup_entry( entry.async_on_unload(coordinator.async_add_listener(process_data)) -class Life360DeviceTracker(CoordinatorEntity, TrackerEntity): +class Life360DeviceTracker( + CoordinatorEntity[Life360DataUpdateCoordinator], TrackerEntity +): """Life360 Device Tracker.""" _attr_attribution = ATTRIBUTION + _attr_unique_id: str - def __init__(self, coordinator: DataUpdateCoordinator, member_id: str) -> None: + def __init__( + self, coordinator: Life360DataUpdateCoordinator, member_id: str + ) -> None: """Initialize Life360 Entity.""" super().__init__(coordinator) self._attr_unique_id = member_id - self._data = coordinator.data.members[self.unique_id] + self._data: Life360Member | None = coordinator.data.members[member_id] + self._prev_data = self._data self._attr_name = self._data.name self._attr_entity_picture = self._data.entity_picture - self._prev_data = self._data - @property def _options(self) -> Mapping[str, Any]: """Shortcut to config entry options.""" @@ -120,16 +122,15 @@ class Life360DeviceTracker(CoordinatorEntity, TrackerEntity): @callback def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" - # Get a shortcut to this member's data. Can't guarantee it's the same dict every - # update, or that there is even data for this member every update, so need to - # update shortcut each time. - self._data = self.coordinator.data.members.get(self.unique_id) - + # Get a shortcut to this Member's data. This needs to be updated each time since + # coordinator provides a new Life360Member object each time, and it's possible + # that there is no data for this Member on some updates. if self.available: - # If nothing important has changed, then skip the update altogether. - if self._data == self._prev_data: - return + self._data = self.coordinator.data.members.get(self._attr_unique_id) + else: + self._data = None + if self._data: # Check if we should effectively throw out new location data. last_seen = self._data.last_seen prev_seen = self._prev_data.last_seen @@ -168,27 +169,21 @@ class Life360DeviceTracker(CoordinatorEntity, TrackerEntity): """Return True if state updates should be forced.""" return False - @property - def available(self) -> bool: - """Return if entity is available.""" - # Guard against member not being in last update for some reason. - return super().available and self._data is not None - @property def entity_picture(self) -> str | None: """Return the entity picture to use in the frontend, if any.""" - if self.available: + if self._data: self._attr_entity_picture = self._data.entity_picture return super().entity_picture - # All of the following will only be called if self.available is True. - @property def battery_level(self) -> int | None: """Return the battery level of the device. Percentage from 0-100. """ + if not self._data: + return None return self._data.battery_level @property @@ -202,11 +197,15 @@ class Life360DeviceTracker(CoordinatorEntity, TrackerEntity): Value in meters. """ + if not self._data: + return 0 return self._data.gps_accuracy @property def driving(self) -> bool: """Return if driving.""" + if not self._data: + return False if (driving_speed := self._options.get(CONF_DRIVING_SPEED)) is not None: if self._data.speed >= driving_speed: return True @@ -222,23 +221,38 @@ class Life360DeviceTracker(CoordinatorEntity, TrackerEntity): @property def latitude(self) -> float | None: """Return latitude value of the device.""" + if not self._data: + return None return self._data.latitude @property def longitude(self) -> float | None: """Return longitude value of the device.""" + if not self._data: + return None return self._data.longitude @property def extra_state_attributes(self) -> Mapping[str, Any] | None: """Return entity specific state attributes.""" - attrs = {} - attrs[ATTR_ADDRESS] = self._data.address - attrs[ATTR_AT_LOC_SINCE] = self._data.at_loc_since - attrs[ATTR_BATTERY_CHARGING] = self._data.battery_charging - attrs[ATTR_DRIVING] = self.driving - attrs[ATTR_LAST_SEEN] = self._data.last_seen - attrs[ATTR_PLACE] = self._data.place - attrs[ATTR_SPEED] = self._data.speed - attrs[ATTR_WIFI_ON] = self._data.wifi_on - return attrs + if not self._data: + return { + ATTR_ADDRESS: None, + ATTR_AT_LOC_SINCE: None, + ATTR_BATTERY_CHARGING: None, + ATTR_DRIVING: None, + ATTR_LAST_SEEN: None, + ATTR_PLACE: None, + ATTR_SPEED: None, + ATTR_WIFI_ON: None, + } + return { + ATTR_ADDRESS: self._data.address, + ATTR_AT_LOC_SINCE: self._data.at_loc_since, + ATTR_BATTERY_CHARGING: self._data.battery_charging, + ATTR_DRIVING: self.driving, + ATTR_LAST_SEEN: self._data.last_seen, + ATTR_PLACE: self._data.place, + ATTR_SPEED: self._data.speed, + ATTR_WIFI_ON: self._data.wifi_on, + } From a370e4f4b06e1e379f4d51fe5d3d02c9fbcdc39e Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 4 Aug 2022 11:43:02 -0600 Subject: [PATCH 16/19] More explicitly call out special cases with SimpliSafe authorization code (#76232) --- .../components/simplisafe/config_flow.py | 23 +++++++- .../components/simplisafe/strings.json | 3 +- .../simplisafe/translations/en.json | 24 +------- .../components/simplisafe/test_config_flow.py | 55 ++++++++++++++++--- 4 files changed, 71 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/simplisafe/config_flow.py b/homeassistant/components/simplisafe/config_flow.py index 0b92871ccb2..7ae363c3be3 100644 --- a/homeassistant/components/simplisafe/config_flow.py +++ b/homeassistant/components/simplisafe/config_flow.py @@ -81,17 +81,34 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): description_placeholders={CONF_URL: self._oauth_values.auth_url}, ) + auth_code = user_input[CONF_AUTH_CODE] + + if auth_code.startswith("="): + # Sometimes, users may include the "=" from the URL query param; in that + # case, strip it off and proceed: + LOGGER.debug('Stripping "=" from the start of the authorization code') + auth_code = auth_code[1:] + + if len(auth_code) != 45: + # SimpliSafe authorization codes are 45 characters in length; if the user + # provides something different, stop them here: + return self.async_show_form( + step_id="user", + data_schema=STEP_USER_SCHEMA, + errors={CONF_AUTH_CODE: "invalid_auth_code_length"}, + description_placeholders={CONF_URL: self._oauth_values.auth_url}, + ) + errors = {} session = aiohttp_client.async_get_clientsession(self.hass) - try: simplisafe = await API.async_from_auth( - user_input[CONF_AUTH_CODE], + auth_code, self._oauth_values.code_verifier, session=session, ) except InvalidCredentialsError: - errors = {"base": "invalid_auth"} + errors = {CONF_AUTH_CODE: "invalid_auth"} except SimplipyError as err: LOGGER.error("Unknown error while logging into SimpliSafe: %s", err) errors = {"base": "unknown"} diff --git a/homeassistant/components/simplisafe/strings.json b/homeassistant/components/simplisafe/strings.json index 16ae7111abf..618c21566f7 100644 --- a/homeassistant/components/simplisafe/strings.json +++ b/homeassistant/components/simplisafe/strings.json @@ -2,7 +2,7 @@ "config": { "step": { "user": { - "description": "SimpliSafe authenticates users via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) before starting.\n\nWhen you are ready, click [here]({url}) to open the SimpliSafe web app and input your credentials. When the process is complete, return here and input the authorization code from the SimpliSafe web app URL.", + "description": "SimpliSafe authenticates users via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) before starting.\n\nWhen you are ready, click [here]({url}) to open the SimpliSafe web app and input your credentials. If you've already logged into SimpliSafe in your browser, you may want to open a new tab, then copy/paste the above URL into that tab.\n\nWhen the process is complete, return here and input the authorization code from the `com.simplisafe.mobile` URL.", "data": { "auth_code": "Authorization Code" } @@ -11,6 +11,7 @@ "error": { "identifier_exists": "Account already registered", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "invalid_auth_code_length": "SimpliSafe authorization codes are 45 characters in length", "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { diff --git a/homeassistant/components/simplisafe/translations/en.json b/homeassistant/components/simplisafe/translations/en.json index 70b0cc15383..245bb18351e 100644 --- a/homeassistant/components/simplisafe/translations/en.json +++ b/homeassistant/components/simplisafe/translations/en.json @@ -2,39 +2,21 @@ "config": { "abort": { "already_configured": "This SimpliSafe account is already in use.", - "email_2fa_timed_out": "Timed out while waiting for email-based two-factor authentication.", "reauth_successful": "Re-authentication was successful", "wrong_account": "The user credentials provided do not match this SimpliSafe account." }, "error": { "identifier_exists": "Account already registered", "invalid_auth": "Invalid authentication", + "invalid_auth_code_length": "SimpliSafe authorization codes are 45 characters in length", "unknown": "Unexpected error" }, - "progress": { - "email_2fa": "Check your email for a verification link from Simplisafe." - }, "step": { - "reauth_confirm": { - "data": { - "password": "Password" - }, - "description": "Please re-enter the password for {username}.", - "title": "Reauthenticate Integration" - }, - "sms_2fa": { - "data": { - "code": "Code" - }, - "description": "Input the two-factor authentication code sent to you via SMS." - }, "user": { "data": { - "auth_code": "Authorization Code", - "password": "Password", - "username": "Username" + "auth_code": "Authorization Code" }, - "description": "SimpliSafe authenticates users via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) before starting.\n\nWhen you are ready, click [here]({url}) to open the SimpliSafe web app and input your credentials. When the process is complete, return here and input the authorization code from the SimpliSafe web app URL." + "description": "SimpliSafe authenticates users via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) before starting.\n\nWhen you are ready, click [here]({url}) to open the SimpliSafe web app and input your credentials. If you've already logged into SimpliSafe in your browser, you may want to open a new tab, then copy/paste the above URL into that tab.\n\nWhen the process is complete, return here and input the authorization code from the `com.simplisafe.mobile` URL." } } }, diff --git a/tests/components/simplisafe/test_config_flow.py b/tests/components/simplisafe/test_config_flow.py index 6e6f99ad4bb..cf92ed94d41 100644 --- a/tests/components/simplisafe/test_config_flow.py +++ b/tests/components/simplisafe/test_config_flow.py @@ -1,4 +1,5 @@ """Define tests for the SimpliSafe config flow.""" +import logging from unittest.mock import patch import pytest @@ -10,6 +11,8 @@ from homeassistant.components.simplisafe.config_flow import CONF_AUTH_CODE from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_CODE, CONF_TOKEN, CONF_USERNAME +VALID_AUTH_CODE = "code12345123451234512345123451234512345123451" + async def test_duplicate_error(config_entry, hass, setup_simplisafe): """Test that errors are shown when duplicates are added.""" @@ -23,12 +26,27 @@ async def test_duplicate_error(config_entry, hass, setup_simplisafe): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} + result["flow_id"], user_input={CONF_AUTH_CODE: VALID_AUTH_CODE} ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" +async def test_invalid_auth_code_length(hass): + """Test that an invalid auth code length show the correct error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["step_id"] == "user" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_AUTH_CODE: "too_short_code"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {CONF_AUTH_CODE: "invalid_auth_code_length"} + + async def test_invalid_credentials(hass): """Test that invalid credentials show the correct error.""" with patch( @@ -42,10 +60,11 @@ async def test_invalid_credentials(hass): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} + result["flow_id"], + user_input={CONF_AUTH_CODE: VALID_AUTH_CODE}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "invalid_auth"} + assert result["errors"] == {CONF_AUTH_CODE: "invalid_auth"} async def test_options_flow(config_entry, hass): @@ -80,7 +99,7 @@ async def test_step_reauth(config_entry, hass, setup_simplisafe): "homeassistant.components.simplisafe.async_setup_entry", return_value=True ), patch("homeassistant.config_entries.ConfigEntries.async_reload"): result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} + result["flow_id"], user_input={CONF_AUTH_CODE: VALID_AUTH_CODE} ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "reauth_successful" @@ -104,14 +123,29 @@ async def test_step_reauth_wrong_account(config_entry, hass, setup_simplisafe): "homeassistant.components.simplisafe.async_setup_entry", return_value=True ), patch("homeassistant.config_entries.ConfigEntries.async_reload"): result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} + result["flow_id"], user_input={CONF_AUTH_CODE: VALID_AUTH_CODE} ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "wrong_account" -async def test_step_user(hass, setup_simplisafe): - """Test the user step.""" +@pytest.mark.parametrize( + "auth_code,log_statement", + [ + ( + VALID_AUTH_CODE, + None, + ), + ( + f"={VALID_AUTH_CODE}", + 'Stripping "=" from the start of the authorization code', + ), + ], +) +async def test_step_user(auth_code, caplog, hass, log_statement, setup_simplisafe): + """Test successfully completion of the user step.""" + caplog.set_level = logging.DEBUG + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) @@ -121,10 +155,13 @@ async def test_step_user(hass, setup_simplisafe): "homeassistant.components.simplisafe.async_setup_entry", return_value=True ), patch("homeassistant.config_entries.ConfigEntries.async_reload"): result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} + result["flow_id"], user_input={CONF_AUTH_CODE: auth_code} ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + if log_statement: + assert any(m for m in caplog.messages if log_statement in m) + assert len(hass.config_entries.async_entries()) == 1 [config_entry] = hass.config_entries.async_entries(DOMAIN) assert config_entry.data == {CONF_USERNAME: "12345", CONF_TOKEN: "token123"} @@ -143,7 +180,7 @@ async def test_unknown_error(hass, setup_simplisafe): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} + result["flow_id"], user_input={CONF_AUTH_CODE: VALID_AUTH_CODE} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {"base": "unknown"} From 42509056bd9e7c7173c5c2dac651a813c2441a28 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Thu, 4 Aug 2022 17:41:47 +0100 Subject: [PATCH 17/19] Enable strict typing for HomeKit Controller config flow module (#76233) --- .strict-typing | 1 + .../components/homekit_controller/manifest.json | 2 +- mypy.ini | 11 +++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 15 insertions(+), 3 deletions(-) diff --git a/.strict-typing b/.strict-typing index c5b4e376414..4a1b405aa61 100644 --- a/.strict-typing +++ b/.strict-typing @@ -128,6 +128,7 @@ homeassistant.components.homekit.util homeassistant.components.homekit_controller homeassistant.components.homekit_controller.alarm_control_panel homeassistant.components.homekit_controller.button +homeassistant.components.homekit_controller.config_flow homeassistant.components.homekit_controller.const homeassistant.components.homekit_controller.lock homeassistant.components.homekit_controller.select diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 5aff66fe757..5f6b3f92220 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==1.2.4"], + "requirements": ["aiohomekit==1.2.5"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/mypy.ini b/mypy.ini index 37765023f74..3a4b51dce08 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1131,6 +1131,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.homekit_controller.config_flow] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.homekit_controller.const] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index 3f29742133e..c6c9337cd7b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.2.4 +aiohomekit==1.2.5 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f95649de7f2..210126d6f38 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.2.4 +aiohomekit==1.2.5 # homeassistant.components.emulated_hue # homeassistant.components.http From 6727dab3303c56609cd47824aae38e48220e0771 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 4 Aug 2022 21:51:02 +0200 Subject: [PATCH 18/19] Bumped version to 2022.8.1 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 18561a8bd2e..b08400c0cd5 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 = 8 -PATCH_VERSION: Final = "0" +PATCH_VERSION: Final = "1" __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/pyproject.toml b/pyproject.toml index c7e187e07f4..59ebe40572d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.8.0" +version = "2022.8.1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From d266b1ced69abd51f3253dbd8e19ad42fcd62b7a Mon Sep 17 00:00:00 2001 From: Jc2k Date: Thu, 4 Aug 2022 11:30:37 +0100 Subject: [PATCH 19/19] Fix some homekit_controller pylint warnings and (local only) test failures (#76122) --- .../homekit_controller/config_flow.py | 60 ++++++++++++++----- .../components/homekit_controller/light.py | 4 +- .../homekit_controller/test_sensor.py | 8 ++- 3 files changed, 51 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 6920f048edb..eba531b917c 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -1,14 +1,17 @@ """Config flow to configure homekit_controller.""" from __future__ import annotations -from collections.abc import Awaitable import logging import re from typing import TYPE_CHECKING, Any, cast import aiohomekit from aiohomekit import Controller, const as aiohomekit_const -from aiohomekit.controller.abstract import AbstractDiscovery, AbstractPairing +from aiohomekit.controller.abstract import ( + AbstractDiscovery, + AbstractPairing, + FinishPairing, +) from aiohomekit.exceptions import AuthenticationError from aiohomekit.model.categories import Categories from aiohomekit.model.status_flags import StatusFlags @@ -17,7 +20,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components import zeroconf -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.helpers import device_registry as dr @@ -78,7 +81,9 @@ def formatted_category(category: Categories) -> str: @callback -def find_existing_host(hass, serial: str) -> config_entries.ConfigEntry | None: +def find_existing_host( + hass: HomeAssistant, serial: str +) -> config_entries.ConfigEntry | None: """Return a set of the configured hosts.""" for entry in hass.config_entries.async_entries(DOMAIN): if entry.data.get("AccessoryPairingID") == serial: @@ -115,15 +120,17 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.category: Categories | None = None self.devices: dict[str, AbstractDiscovery] = {} self.controller: Controller | None = None - self.finish_pairing: Awaitable[AbstractPairing] | None = None + self.finish_pairing: FinishPairing | None = None - async def _async_setup_controller(self): + async def _async_setup_controller(self) -> None: """Create the controller.""" self.controller = await async_get_controller(self.hass) - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow start.""" - errors = {} + errors: dict[str, str] = {} if user_input is not None: key = user_input["device"] @@ -142,6 +149,8 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if self.controller is None: await self._async_setup_controller() + assert self.controller + self.devices = {} async for discovery in self.controller.async_discover(): @@ -167,7 +176,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ), ) - async def async_step_unignore(self, user_input): + async def async_step_unignore(self, user_input: dict[str, Any]) -> FlowResult: """Rediscover a previously ignored discover.""" unique_id = user_input["unique_id"] await self.async_set_unique_id(unique_id) @@ -175,19 +184,21 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if self.controller is None: await self._async_setup_controller() + assert self.controller + try: discovery = await self.controller.async_find(unique_id) except aiohomekit.AccessoryNotFoundError: return self.async_abort(reason="accessory_not_found_error") self.name = discovery.description.name - self.model = discovery.description.model + self.model = getattr(discovery.description, "model", BLE_DEFAULT_NAME) self.category = discovery.description.category self.hkid = discovery.description.id return self._async_step_pair_show_form() - async def _hkid_is_homekit(self, hkid): + async def _hkid_is_homekit(self, hkid: str) -> bool: """Determine if the device is a homekit bridge or accessory.""" dev_reg = dr.async_get(self.hass) device = dev_reg.async_get_device( @@ -410,7 +421,9 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self._async_step_pair_show_form() - async def async_step_pair(self, pair_info=None): + async def async_step_pair( + self, pair_info: dict[str, Any] | None = None + ) -> FlowResult: """Pair with a new HomeKit accessory.""" # If async_step_pair is called with no pairing code then we do the M1 # phase of pairing. If this is successful the device enters pairing @@ -428,11 +441,16 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # callable. We call the callable with the pin that the user has typed # in. + # Should never call this step without setting self.hkid + assert self.hkid + errors = {} if self.controller is None: await self._async_setup_controller() + assert self.controller + if pair_info and self.finish_pairing: self.context["pairing"] = True code = pair_info["pairing_code"] @@ -507,21 +525,27 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self._async_step_pair_show_form(errors) - async def async_step_busy_error(self, user_input=None): + async def async_step_busy_error( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Retry pairing after the accessory is busy.""" if user_input is not None: return await self.async_step_pair() return self.async_show_form(step_id="busy_error") - async def async_step_max_tries_error(self, user_input=None): + async def async_step_max_tries_error( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Retry pairing after the accessory has reached max tries.""" if user_input is not None: return await self.async_step_pair() return self.async_show_form(step_id="max_tries_error") - async def async_step_protocol_error(self, user_input=None): + async def async_step_protocol_error( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Retry pairing after the accessory has a protocol error.""" if user_input is not None: return await self.async_step_pair() @@ -529,7 +553,11 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form(step_id="protocol_error") @callback - def _async_step_pair_show_form(self, errors=None): + def _async_step_pair_show_form( + self, errors: dict[str, str] | None = None + ) -> FlowResult: + assert self.category + placeholders = self.context["title_placeholders"] = { "name": self.name, "category": formatted_category(self.category), diff --git a/homeassistant/components/homekit_controller/light.py b/homeassistant/components/homekit_controller/light.py index df691ac3f6f..d882f6790f7 100644 --- a/homeassistant/components/homekit_controller/light.py +++ b/homeassistant/components/homekit_controller/light.py @@ -107,9 +107,9 @@ class HomeKitLight(HomeKitEntity, LightEntity): return ColorMode.ONOFF @property - def supported_color_modes(self) -> set[ColorMode | str] | None: + def supported_color_modes(self) -> set[ColorMode]: """Flag supported color modes.""" - color_modes: set[ColorMode | str] = set() + color_modes: set[ColorMode] = set() if self.service.has(CharacteristicsTypes.HUE) or self.service.has( CharacteristicsTypes.SATURATION diff --git a/tests/components/homekit_controller/test_sensor.py b/tests/components/homekit_controller/test_sensor.py index 836da1e466f..b26e025c8a4 100644 --- a/tests/components/homekit_controller/test_sensor.py +++ b/tests/components/homekit_controller/test_sensor.py @@ -88,10 +88,12 @@ async def test_temperature_sensor_not_added_twice(hass, utcnow): hass, create_temperature_sensor_service, suffix="temperature" ) + created_sensors = set() for state in hass.states.async_all(): - if state.entity_id.startswith("button"): - continue - assert state.entity_id == helper.entity_id + if state.attributes.get("device_class") == SensorDeviceClass.TEMPERATURE: + created_sensors.add(state.entity_id) + + assert created_sensors == {helper.entity_id} async def test_humidity_sensor_read_state(hass, utcnow):