From d2b98fa2854830360a34ece457530c8cf725d4b8 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 3 Aug 2022 22:33:05 +0200 Subject: [PATCH 01/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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): From 0e7bf35e4a024b29339788509432770aa35a14fc Mon Sep 17 00:00:00 2001 From: Jc2k Date: Sat, 6 Aug 2022 21:45:44 +0100 Subject: [PATCH 20/81] Update gree to use the network component to set discovery interfaces (#75812) --- homeassistant/components/gree/__init__.py | 4 +++- homeassistant/components/gree/config_flow.py | 6 +++++- homeassistant/components/gree/manifest.json | 3 ++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/gree/common.py | 2 +- 6 files changed, 13 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/gree/__init__.py b/homeassistant/components/gree/__init__.py index d4a929f1642..ff3438ed53f 100644 --- a/homeassistant/components/gree/__init__.py +++ b/homeassistant/components/gree/__init__.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from homeassistant.components.network import async_get_ipv4_broadcast_addresses from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant @@ -32,7 +33,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) async def _async_scan_update(_=None): - await gree_discovery.discovery.scan() + bcast_addr = list(await async_get_ipv4_broadcast_addresses(hass)) + await gree_discovery.discovery.scan(0, bcast_ifaces=bcast_addr) _LOGGER.debug("Scanning network for Gree devices") await _async_scan_update() diff --git a/homeassistant/components/gree/config_flow.py b/homeassistant/components/gree/config_flow.py index d317fe6d873..58f83cd4486 100644 --- a/homeassistant/components/gree/config_flow.py +++ b/homeassistant/components/gree/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Gree.""" from greeclimate.discovery import Discovery +from homeassistant.components.network import async_get_ipv4_broadcast_addresses from homeassistant.core import HomeAssistant from homeassistant.helpers import config_entry_flow @@ -10,7 +11,10 @@ from .const import DISCOVERY_TIMEOUT, DOMAIN async def _async_has_devices(hass: HomeAssistant) -> bool: """Return if there are devices that can be discovered.""" gree_discovery = Discovery(DISCOVERY_TIMEOUT) - devices = await gree_discovery.scan(wait_for=DISCOVERY_TIMEOUT) + bcast_addr = list(await async_get_ipv4_broadcast_addresses(hass)) + devices = await gree_discovery.scan( + wait_for=DISCOVERY_TIMEOUT, bcast_ifaces=bcast_addr + ) return len(devices) > 0 diff --git a/homeassistant/components/gree/manifest.json b/homeassistant/components/gree/manifest.json index 1b2c8dd6a2a..97c0ec1780c 100644 --- a/homeassistant/components/gree/manifest.json +++ b/homeassistant/components/gree/manifest.json @@ -3,7 +3,8 @@ "name": "Gree Climate", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/gree", - "requirements": ["greeclimate==1.2.0"], + "requirements": ["greeclimate==1.3.0"], + "dependencies": ["network"], "codeowners": ["@cmroche"], "iot_class": "local_polling", "loggers": ["greeclimate"] diff --git a/requirements_all.txt b/requirements_all.txt index c6c9337cd7b..9e968a3cdb0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -769,7 +769,7 @@ gpiozero==1.6.2 gps3==0.33.3 # homeassistant.components.gree -greeclimate==1.2.0 +greeclimate==1.3.0 # homeassistant.components.greeneye_monitor greeneye_monitor==3.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 210126d6f38..3482f9496c8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -564,7 +564,7 @@ googlemaps==2.5.1 govee-ble==0.12.6 # homeassistant.components.gree -greeclimate==1.2.0 +greeclimate==1.3.0 # homeassistant.components.greeneye_monitor greeneye_monitor==3.0.3 diff --git a/tests/components/gree/common.py b/tests/components/gree/common.py index c7db03b118f..cd8a2d6ee28 100644 --- a/tests/components/gree/common.py +++ b/tests/components/gree/common.py @@ -28,7 +28,7 @@ class FakeDiscovery: """Add an event listener.""" self._listeners.append(listener) - async def scan(self, wait_for: int = 0): + async def scan(self, wait_for: int = 0, bcast_ifaces=None): """Search for devices, return mocked data.""" self.scan_count += 1 _LOGGER.info("CALLED SCAN %d TIMES", self.scan_count) From 8bfc3525241e6a799bcd1b0b29927d90e12c8868 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Fri, 5 Aug 2022 16:06:19 +0200 Subject: [PATCH 21/81] Use stored philips_js system data on start (#75981) Co-authored-by: Martin Hjelmare --- homeassistant/components/philips_js/__init__.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/philips_js/__init__.py b/homeassistant/components/philips_js/__init__.py index 9e574e69f90..24b3f9a91e0 100644 --- a/homeassistant/components/philips_js/__init__.py +++ b/homeassistant/components/philips_js/__init__.py @@ -38,15 +38,22 @@ LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Philips TV from a config entry.""" + system: SystemType | None = entry.data.get(CONF_SYSTEM) tvapi = PhilipsTV( entry.data[CONF_HOST], entry.data[CONF_API_VERSION], username=entry.data.get(CONF_USERNAME), password=entry.data.get(CONF_PASSWORD), + system=system, ) coordinator = PhilipsTVDataUpdateCoordinator(hass, tvapi, entry.options) await coordinator.async_refresh() + + if (actual_system := tvapi.system) and actual_system != system: + data = {**entry.data, CONF_SYSTEM: actual_system} + hass.config_entries.async_update_entry(entry, data=data) + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator From 9c21d565394cbc0d90b6dcab80c5f3ccae4f4592 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 7 Aug 2022 05:03:56 -1000 Subject: [PATCH 22/81] Ensure bluetooth recovers if Dbus gets restarted (#76249) --- .../components/bluetooth/__init__.py | 68 ++++++++++++++++--- tests/components/bluetooth/test_init.py | 56 +++++++++++++++ 2 files changed, 116 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 0b81472f838..2d7b47b7552 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -8,6 +8,7 @@ from dataclasses import dataclass from datetime import datetime, timedelta from enum import Enum import logging +import time from typing import TYPE_CHECKING, Final import async_timeout @@ -56,6 +57,10 @@ START_TIMEOUT = 9 SOURCE_LOCAL: Final = "local" +SCANNER_WATCHDOG_TIMEOUT: Final = 60 * 5 +SCANNER_WATCHDOG_INTERVAL: Final = timedelta(seconds=SCANNER_WATCHDOG_TIMEOUT) +MONOTONIC_TIME = time.monotonic + @dataclass class BluetoothServiceInfoBleak(BluetoothServiceInfo): @@ -252,9 +257,10 @@ async def async_setup_entry( ) -> bool: """Set up the bluetooth integration from a config entry.""" manager: BluetoothManager = hass.data[DOMAIN] - await manager.async_start( - BluetoothScanningMode.ACTIVE, entry.options.get(CONF_ADAPTER) - ) + async with manager.start_stop_lock: + await manager.async_start( + BluetoothScanningMode.ACTIVE, entry.options.get(CONF_ADAPTER) + ) entry.async_on_unload(entry.add_update_listener(_async_update_listener)) return True @@ -263,8 +269,6 @@ async def _async_update_listener( hass: HomeAssistant, entry: config_entries.ConfigEntry ) -> None: """Handle options update.""" - manager: BluetoothManager = hass.data[DOMAIN] - manager.async_start_reload() await hass.config_entries.async_reload(entry.entry_id) @@ -273,7 +277,9 @@ async def async_unload_entry( ) -> bool: """Unload a config entry.""" manager: BluetoothManager = hass.data[DOMAIN] - await manager.async_stop() + async with manager.start_stop_lock: + manager.async_start_reload() + await manager.async_stop() return True @@ -289,13 +295,19 @@ class BluetoothManager: self.hass = hass self._integration_matcher = integration_matcher self.scanner: HaBleakScanner | None = None + self.start_stop_lock = asyncio.Lock() self._cancel_device_detected: CALLBACK_TYPE | None = None self._cancel_unavailable_tracking: CALLBACK_TYPE | None = None + self._cancel_stop: CALLBACK_TYPE | None = None + self._cancel_watchdog: CALLBACK_TYPE | None = None self._unavailable_callbacks: dict[str, list[Callable[[str], None]]] = {} self._callbacks: list[ tuple[BluetoothCallback, BluetoothCallbackMatcher | None] ] = [] + self._last_detection = 0.0 self._reloading = False + self._adapter: str | None = None + self._scanning_mode = BluetoothScanningMode.ACTIVE @hass_callback def async_setup(self) -> None: @@ -317,6 +329,8 @@ class BluetoothManager: ) -> None: """Set up BT Discovery.""" assert self.scanner is not None + self._adapter = adapter + self._scanning_mode = scanning_mode if self._reloading: # On reload, we need to reset the scanner instance # since the devices in its history may not be reachable @@ -381,7 +395,32 @@ class BluetoothManager: _LOGGER.debug("BleakError while starting bluetooth: %s", ex, exc_info=True) raise ConfigEntryNotReady(f"Failed to start Bluetooth: {ex}") from ex self.async_setup_unavailable_tracking() - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop) + self._async_setup_scanner_watchdog() + self._cancel_stop = self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, self._async_hass_stopping + ) + + @hass_callback + def _async_setup_scanner_watchdog(self) -> None: + """If Dbus gets restarted or updated, we need to restart the scanner.""" + self._last_detection = MONOTONIC_TIME() + self._cancel_watchdog = async_track_time_interval( + self.hass, self._async_scanner_watchdog, SCANNER_WATCHDOG_INTERVAL + ) + + async def _async_scanner_watchdog(self, now: datetime) -> None: + """Check if the scanner is running.""" + time_since_last_detection = MONOTONIC_TIME() - self._last_detection + if time_since_last_detection < SCANNER_WATCHDOG_TIMEOUT: + return + _LOGGER.info( + "Bluetooth scanner has gone quiet for %s, restarting", + SCANNER_WATCHDOG_INTERVAL, + ) + async with self.start_stop_lock: + self.async_start_reload() + await self.async_stop() + await self.async_start(self._scanning_mode, self._adapter) @hass_callback def async_setup_unavailable_tracking(self) -> None: @@ -416,6 +455,7 @@ class BluetoothManager: self, device: BLEDevice, advertisement_data: AdvertisementData ) -> None: """Handle a detected device.""" + self._last_detection = MONOTONIC_TIME() matched_domains = self._integration_matcher.match_domains( device, advertisement_data ) @@ -528,14 +568,26 @@ class BluetoothManager: for device_adv in self.scanner.history.values() ] - async def async_stop(self, event: Event | None = None) -> None: + async def _async_hass_stopping(self, event: Event) -> None: + """Stop the Bluetooth integration at shutdown.""" + self._cancel_stop = None + await self.async_stop() + + async def async_stop(self) -> None: """Stop bluetooth discovery.""" + _LOGGER.debug("Stopping bluetooth discovery") + if self._cancel_watchdog: + self._cancel_watchdog() + self._cancel_watchdog = None if self._cancel_device_detected: self._cancel_device_detected() self._cancel_device_detected = None if self._cancel_unavailable_tracking: self._cancel_unavailable_tracking() self._cancel_unavailable_tracking = None + if self._cancel_stop: + self._cancel_stop() + self._cancel_stop = None if self.scanner: try: await self.scanner.stop() # type: ignore[no-untyped-call] diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index ba315b1f380..9432be9f2e4 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -10,6 +10,8 @@ import pytest from homeassistant.components import bluetooth from homeassistant.components.bluetooth import ( + SCANNER_WATCHDOG_INTERVAL, + SCANNER_WATCHDOG_TIMEOUT, SOURCE_LOCAL, UNAVAILABLE_TRACK_SECONDS, BluetoothChange, @@ -1522,3 +1524,57 @@ async def test_invalid_dbus_message(hass, caplog): hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() assert "dbus" in caplog.text + + +async def test_recovery_from_dbus_restart( + hass, mock_bleak_scanner_start, enable_bluetooth +): + """Test we can recover when DBus gets restarted out from under us.""" + assert await async_setup_component(hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}) + await hass.async_block_till_done() + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + start_time_monotonic = 1000 + scanner = _get_underlying_scanner() + mock_discovered = [MagicMock()] + type(scanner).discovered_devices = mock_discovered + + # Ensure we don't restart the scanner if we don't need to + with patch( + "homeassistant.components.bluetooth.MONOTONIC_TIME", + return_value=start_time_monotonic + 10, + ): + async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) + await hass.async_block_till_done() + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + # Fire a callback to reset the timer + with patch( + "homeassistant.components.bluetooth.MONOTONIC_TIME", + return_value=start_time_monotonic, + ): + scanner._callback( + BLEDevice("44:44:33:11:23:42", "any_name"), + AdvertisementData(local_name="any_name"), + ) + + # Ensure we don't restart the scanner if we don't need to + with patch( + "homeassistant.components.bluetooth.MONOTONIC_TIME", + return_value=start_time_monotonic + 20, + ): + async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) + await hass.async_block_till_done() + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + # We hit the timer, so we restart the scanner + with patch( + "homeassistant.components.bluetooth.MONOTONIC_TIME", + return_value=start_time_monotonic + SCANNER_WATCHDOG_TIMEOUT, + ): + async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) + await hass.async_block_till_done() + + assert len(mock_bleak_scanner_start.mock_calls) == 2 From bfa64d2e01521315e47aa2ae3b713cd9677d6a2d Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 7 Aug 2022 15:04:04 +0200 Subject: [PATCH 23/81] Fix default sensor names in NextDNS integration (#76264) --- homeassistant/components/nextdns/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nextdns/sensor.py b/homeassistant/components/nextdns/sensor.py index 168c0be8cd0..422bc2a237b 100644 --- a/homeassistant/components/nextdns/sensor.py +++ b/homeassistant/components/nextdns/sensor.py @@ -135,7 +135,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, icon="mdi:dns", - name="TCP Queries", + name="TCP queries", native_unit_of_measurement="queries", state_class=SensorStateClass.TOTAL, value=lambda data: data.tcp_queries, @@ -190,7 +190,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, icon="mdi:dns", - name="TCP Queries Ratio", + name="TCP queries ratio", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.tcp_queries_ratio, From 8971a2073ed603e7bccf655344220a21bc0e442e Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Fri, 5 Aug 2022 05:06:40 -0400 Subject: [PATCH 24/81] Bump ZHA dependencies (#76275) --- homeassistant/components/zha/manifest.json | 6 +++--- requirements_all.txt | 6 +++--- requirements_test_all.txt | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 179302af1cd..bad84054f1f 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,14 +4,14 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ - "bellows==0.31.2", + "bellows==0.31.3", "pyserial==3.5", "pyserial-asyncio==0.6", "zha-quirks==0.0.78", "zigpy-deconz==0.18.0", - "zigpy==0.48.0", + "zigpy==0.49.0", "zigpy-xbee==0.15.0", - "zigpy-zigate==0.9.0", + "zigpy-zigate==0.9.1", "zigpy-znp==0.8.1" ], "usb": [ diff --git a/requirements_all.txt b/requirements_all.txt index 9e968a3cdb0..df989f287b9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -396,7 +396,7 @@ beautifulsoup4==4.11.1 # beewi_smartclim==0.0.10 # homeassistant.components.zha -bellows==0.31.2 +bellows==0.31.3 # homeassistant.components.bmw_connected_drive bimmer_connected==0.10.1 @@ -2529,13 +2529,13 @@ zigpy-deconz==0.18.0 zigpy-xbee==0.15.0 # homeassistant.components.zha -zigpy-zigate==0.9.0 +zigpy-zigate==0.9.1 # homeassistant.components.zha zigpy-znp==0.8.1 # homeassistant.components.zha -zigpy==0.48.0 +zigpy==0.49.0 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3482f9496c8..344cd8d5675 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -320,7 +320,7 @@ base36==0.1.1 beautifulsoup4==4.11.1 # homeassistant.components.zha -bellows==0.31.2 +bellows==0.31.3 # homeassistant.components.bmw_connected_drive bimmer_connected==0.10.1 @@ -1703,13 +1703,13 @@ zigpy-deconz==0.18.0 zigpy-xbee==0.15.0 # homeassistant.components.zha -zigpy-zigate==0.9.0 +zigpy-zigate==0.9.1 # homeassistant.components.zha zigpy-znp==0.8.1 # homeassistant.components.zha -zigpy==0.48.0 +zigpy==0.49.0 # homeassistant.components.zwave_js zwave-js-server-python==0.39.0 From 9cf11cf6ed0e2645da0b82f843827944397030b4 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 5 Aug 2022 11:22:55 +0200 Subject: [PATCH 25/81] Bump pydeconz to v102 (#76287) --- homeassistant/components/deconz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index 6384ebfcd5f..e4e412056e6 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -3,7 +3,7 @@ "name": "deCONZ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", - "requirements": ["pydeconz==101"], + "requirements": ["pydeconz==102"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index df989f287b9..e0e7f26c4b1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1455,7 +1455,7 @@ pydaikin==2.7.0 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==101 +pydeconz==102 # homeassistant.components.delijn pydelijn==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 344cd8d5675..a58d5994737 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1001,7 +1001,7 @@ pycoolmasternet-async==0.1.2 pydaikin==2.7.0 # homeassistant.components.deconz -pydeconz==101 +pydeconz==102 # homeassistant.components.dexcom pydexcom==0.2.3 From 1c2dd78e4cfe1f0d17062768c9346e4feb4b9879 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Fri, 5 Aug 2022 09:34:21 -0400 Subject: [PATCH 26/81] Fix ZHA light color temp support (#76305) --- .../components/zha/core/channels/lighting.py | 39 +++++++------------ 1 file changed, 14 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/zha/core/channels/lighting.py b/homeassistant/components/zha/core/channels/lighting.py index 36bb0beb17d..1754b9aff68 100644 --- a/homeassistant/components/zha/core/channels/lighting.py +++ b/homeassistant/components/zha/core/channels/lighting.py @@ -1,7 +1,7 @@ """Lighting channels module for Zigbee Home Automation.""" from __future__ import annotations -from contextlib import suppress +from functools import cached_property from zigpy.zcl.clusters import lighting @@ -46,17 +46,8 @@ class ColorChannel(ZigbeeChannel): "color_loop_active": False, } - @property - def color_capabilities(self) -> int: - """Return color capabilities of the light.""" - with suppress(KeyError): - return self.cluster["color_capabilities"] - if self.cluster.get("color_temperature") is not None: - return self.CAPABILITIES_COLOR_XY | self.CAPABILITIES_COLOR_TEMP - return self.CAPABILITIES_COLOR_XY - - @property - def zcl_color_capabilities(self) -> lighting.Color.ColorCapabilities: + @cached_property + def color_capabilities(self) -> lighting.Color.ColorCapabilities: """Return ZCL color capabilities of the light.""" color_capabilities = self.cluster.get("color_capabilities") if color_capabilities is None: @@ -117,43 +108,41 @@ class ColorChannel(ZigbeeChannel): def hs_supported(self) -> bool: """Return True if the channel supports hue and saturation.""" return ( - self.zcl_color_capabilities is not None + self.color_capabilities is not None and lighting.Color.ColorCapabilities.Hue_and_saturation - in self.zcl_color_capabilities + in self.color_capabilities ) @property def enhanced_hue_supported(self) -> bool: """Return True if the channel supports enhanced hue and saturation.""" return ( - self.zcl_color_capabilities is not None - and lighting.Color.ColorCapabilities.Enhanced_hue - in self.zcl_color_capabilities + self.color_capabilities is not None + and lighting.Color.ColorCapabilities.Enhanced_hue in self.color_capabilities ) @property def xy_supported(self) -> bool: """Return True if the channel supports xy.""" return ( - self.zcl_color_capabilities is not None + self.color_capabilities is not None and lighting.Color.ColorCapabilities.XY_attributes - in self.zcl_color_capabilities + in self.color_capabilities ) @property def color_temp_supported(self) -> bool: """Return True if the channel supports color temperature.""" return ( - self.zcl_color_capabilities is not None + self.color_capabilities is not None and lighting.Color.ColorCapabilities.Color_temperature - in self.zcl_color_capabilities - ) + in self.color_capabilities + ) or self.color_temperature is not None @property def color_loop_supported(self) -> bool: """Return True if the channel supports color loop.""" return ( - self.zcl_color_capabilities is not None - and lighting.Color.ColorCapabilities.Color_loop - in self.zcl_color_capabilities + self.color_capabilities is not None + and lighting.Color.ColorCapabilities.Color_loop in self.color_capabilities ) From 4b63aa7f15b34ed8d03053f9b73ff0dd6f5cd0f5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 6 Aug 2022 00:46:27 -1000 Subject: [PATCH 27/81] Bump pySwitchbot to 0.18.4 (#76322) * Bump pySwitchbot to 0.18.3 Fixes #76321 Changelog: https://github.com/Danielhiversen/pySwitchbot/compare/0.17.3...0.18.3 * bump --- homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index f01eae4a938..b413b44d605 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,7 +2,7 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.17.3"], + "requirements": ["PySwitchbot==0.18.4"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index e0e7f26c4b1..106fb703461 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -37,7 +37,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.17.3 +PySwitchbot==0.18.4 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a58d5994737..c937ab13612 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -33,7 +33,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.17.3 +PySwitchbot==0.18.4 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 5026bff4264d82c8c50c7c7414779a5947e7d3ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Roy?= Date: Fri, 5 Aug 2022 17:30:45 -0700 Subject: [PATCH 28/81] Bump aiobafi6 to 0.7.2 to unblock #76328 (#76330) --- homeassistant/components/baf/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/baf/manifest.json b/homeassistant/components/baf/manifest.json index 15e0272b2b0..7462a64e770 100644 --- a/homeassistant/components/baf/manifest.json +++ b/homeassistant/components/baf/manifest.json @@ -3,7 +3,7 @@ "name": "Big Ass Fans", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/baf", - "requirements": ["aiobafi6==0.7.0"], + "requirements": ["aiobafi6==0.7.2"], "codeowners": ["@bdraco", "@jfroy"], "iot_class": "local_push", "zeroconf": [ diff --git a/requirements_all.txt b/requirements_all.txt index 106fb703461..5bd396ea124 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -128,7 +128,7 @@ aioasuswrt==1.4.0 aioazuredevops==1.3.5 # homeassistant.components.baf -aiobafi6==0.7.0 +aiobafi6==0.7.2 # homeassistant.components.aws aiobotocore==2.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c937ab13612..86b7dbb93e6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -115,7 +115,7 @@ aioasuswrt==1.4.0 aioazuredevops==1.3.5 # homeassistant.components.baf -aiobafi6==0.7.0 +aiobafi6==0.7.2 # homeassistant.components.aws aiobotocore==2.1.0 From e96903fddf03859676e38b0769bb2db68b3de057 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sun, 7 Aug 2022 15:42:47 +0200 Subject: [PATCH 29/81] Postpone broadlink platform switch until config entry is ready (#76371) --- homeassistant/components/broadlink/switch.py | 24 +++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/broadlink/switch.py b/homeassistant/components/broadlink/switch.py index 6a015748bd0..d38898a513f 100644 --- a/homeassistant/components/broadlink/switch.py +++ b/homeassistant/components/broadlink/switch.py @@ -26,11 +26,13 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from . import BroadlinkDevice from .const import DOMAIN from .entity import BroadlinkEntity from .helpers import data_packet, import_device, mac_address @@ -80,8 +82,18 @@ async def async_setup_platform( host = config.get(CONF_HOST) if switches := config.get(CONF_SWITCHES): - platform_data = hass.data[DOMAIN].platforms.setdefault(Platform.SWITCH, {}) - platform_data.setdefault(mac_addr, []).extend(switches) + platform_data = hass.data[DOMAIN].platforms.get(Platform.SWITCH, {}) + async_add_entities_config_entry: AddEntitiesCallback + device: BroadlinkDevice + async_add_entities_config_entry, device = platform_data.get( + mac_addr, (None, None) + ) + if not async_add_entities_config_entry: + raise PlatformNotReady + + async_add_entities_config_entry( + BroadlinkRMSwitch(device, config) for config in switches + ) else: _LOGGER.warning( @@ -104,12 +116,8 @@ async def async_setup_entry( switches: list[BroadlinkSwitch] = [] if device.api.type in {"RM4MINI", "RM4PRO", "RMMINI", "RMMINIB", "RMPRO"}: - platform_data = hass.data[DOMAIN].platforms.get(Platform.SWITCH, {}) - user_defined_switches = platform_data.get(device.api.mac, {}) - switches.extend( - BroadlinkRMSwitch(device, config) for config in user_defined_switches - ) - + platform_data = hass.data[DOMAIN].platforms.setdefault(Platform.SWITCH, {}) + platform_data[device.api.mac] = async_add_entities, device elif device.api.type == "SP1": switches.append(BroadlinkSP1Switch(device)) From c9581f6a2eb99416a2c4aeb7fb2a1d4e5c3837f6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 7 Aug 2022 12:13:08 -0400 Subject: [PATCH 30/81] Bumped version to 2022.8.2 --- 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 b08400c0cd5..dda81cbdc5c 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 = "1" +PATCH_VERSION: Final = "2" __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 59ebe40572d..568607d6d0d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.8.1" +version = "2022.8.2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 0f6b059e3e576523f0e1940820906b0b241ef5b8 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 7 Aug 2022 14:51:00 -0600 Subject: [PATCH 31/81] Add debug logging for unknown Notion errors (#76395) * Add debug logging for unknown Notion errors * Remove unused constant * Code review --- homeassistant/components/notion/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index 2a73d12d946..eaa3f55e56c 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -3,6 +3,8 @@ from __future__ import annotations import asyncio from datetime import timedelta +import logging +import traceback from typing import Any from aionotion import async_get_client @@ -31,7 +33,6 @@ PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] ATTR_SYSTEM_MODE = "system_mode" ATTR_SYSTEM_NAME = "system_name" -DEFAULT_ATTRIBUTION = "Data provided by Notion" DEFAULT_SCAN_INTERVAL = timedelta(minutes=1) CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) @@ -75,6 +76,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: f"There was a Notion error while updating {attr}: {result}" ) from result if isinstance(result, Exception): + if LOGGER.isEnabledFor(logging.DEBUG): + LOGGER.debug("".join(traceback.format_tb(result.__traceback__))) raise UpdateFailed( f"There was an unknown error while updating {attr}: {result}" ) from result From 79b371229dfe9bef26b389545d871a90b04dfd2a Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 7 Aug 2022 14:27:52 -0600 Subject: [PATCH 32/81] Automatically enable common RainMachine restriction entities (#76405) Automatically enable common delay-related RainMachine entities --- homeassistant/components/rainmachine/binary_sensor.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/homeassistant/components/rainmachine/binary_sensor.py b/homeassistant/components/rainmachine/binary_sensor.py index 6ba374a28ba..80e3b90e577 100644 --- a/homeassistant/components/rainmachine/binary_sensor.py +++ b/homeassistant/components/rainmachine/binary_sensor.py @@ -77,7 +77,6 @@ BINARY_SENSOR_DESCRIPTIONS = ( name="Hourly restrictions", icon="mdi:cancel", entity_category=EntityCategory.DIAGNOSTIC, - entity_registry_enabled_default=False, api_category=DATA_RESTRICTIONS_CURRENT, data_key="hourly", ), @@ -86,7 +85,6 @@ BINARY_SENSOR_DESCRIPTIONS = ( name="Month restrictions", icon="mdi:cancel", entity_category=EntityCategory.DIAGNOSTIC, - entity_registry_enabled_default=False, api_category=DATA_RESTRICTIONS_CURRENT, data_key="month", ), @@ -95,7 +93,6 @@ BINARY_SENSOR_DESCRIPTIONS = ( name="Rain delay restrictions", icon="mdi:cancel", entity_category=EntityCategory.DIAGNOSTIC, - entity_registry_enabled_default=False, api_category=DATA_RESTRICTIONS_CURRENT, data_key="rainDelay", ), @@ -113,7 +110,6 @@ BINARY_SENSOR_DESCRIPTIONS = ( name="Weekday restrictions", icon="mdi:cancel", entity_category=EntityCategory.DIAGNOSTIC, - entity_registry_enabled_default=False, api_category=DATA_RESTRICTIONS_CURRENT, data_key="weekDay", ), From ee2acabcbe3eb96b8b00a7903b8c3df59ea8c7c6 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 7 Aug 2022 13:44:50 -0600 Subject: [PATCH 33/81] Fix bug where RainMachine entity states don't populate on startup (#76412) --- homeassistant/components/rainmachine/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 3feeac7a827..07dbe63ca00 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -437,7 +437,7 @@ class RainMachineEntity(CoordinatorEntity): self.async_write_ha_state() async def async_added_to_hass(self) -> None: - """Handle entity which will be added.""" + """When entity is added to hass.""" await super().async_added_to_hass() self.update_from_latest_data() From f01b0a1a62eef8912cb7ba6dbb19533745cab885 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 7 Aug 2022 10:11:34 -1000 Subject: [PATCH 34/81] Fix Govee 5185 Meat Thermometers with older firmware not being discovered (#76414) --- homeassistant/components/govee_ble/manifest.json | 6 +++++- homeassistant/generated/bluetooth.py | 5 +++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/govee_ble/manifest.json b/homeassistant/components/govee_ble/manifest.json index 624a38ebe9d..eb33df867e1 100644 --- a/homeassistant/components/govee_ble/manifest.json +++ b/homeassistant/components/govee_ble/manifest.json @@ -15,6 +15,10 @@ "manufacturer_id": 18994, "service_uuid": "00008551-0000-1000-8000-00805f9b34fb" }, + { + "manufacturer_id": 818, + "service_uuid": "00008551-0000-1000-8000-00805f9b34fb" + }, { "manufacturer_id": 14474, "service_uuid": "00008151-0000-1000-8000-00805f9b34fb" @@ -24,7 +28,7 @@ "service_uuid": "00008251-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["govee-ble==0.12.6"], + "requirements": ["govee-ble==0.12.7"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index ef8193dad28..d704d00ab8a 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -41,6 +41,11 @@ BLUETOOTH: list[dict[str, str | int | list[int]]] = [ "manufacturer_id": 18994, "service_uuid": "00008551-0000-1000-8000-00805f9b34fb" }, + { + "domain": "govee_ble", + "manufacturer_id": 818, + "service_uuid": "00008551-0000-1000-8000-00805f9b34fb" + }, { "domain": "govee_ble", "manufacturer_id": 14474, diff --git a/requirements_all.txt b/requirements_all.txt index 5bd396ea124..f2bcb78f8c8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -760,7 +760,7 @@ googlemaps==2.5.1 goslide-api==0.5.1 # homeassistant.components.govee_ble -govee-ble==0.12.6 +govee-ble==0.12.7 # homeassistant.components.remote_rpi_gpio gpiozero==1.6.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 86b7dbb93e6..551896a46e9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -561,7 +561,7 @@ google-nest-sdm==2.0.0 googlemaps==2.5.1 # homeassistant.components.govee_ble -govee-ble==0.12.6 +govee-ble==0.12.7 # homeassistant.components.gree greeclimate==1.3.0 From af90159e7cd86bbdc06d1288ba06bebc64b5cef7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 8 Aug 2022 15:35:45 +0200 Subject: [PATCH 35/81] Fix iCloud listeners (#76437) --- homeassistant/components/icloud/account.py | 4 +++- homeassistant/components/icloud/device_tracker.py | 2 +- homeassistant/components/icloud/sensor.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/icloud/account.py b/homeassistant/components/icloud/account.py index 4dc3c07aba7..d0e3b8059a4 100644 --- a/homeassistant/components/icloud/account.py +++ b/homeassistant/components/icloud/account.py @@ -17,7 +17,7 @@ from pyicloud.services.findmyiphone import AppleDevice from homeassistant.components.zone import async_active_zone from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry from homeassistant.const import ATTR_ATTRIBUTION, CONF_USERNAME -from homeassistant.core import HomeAssistant +from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.event import track_point_in_utc_time @@ -104,6 +104,8 @@ class IcloudAccount: self._retried_fetch = False self._config_entry = config_entry + self.listeners: list[CALLBACK_TYPE] = [] + def setup(self) -> None: """Set up an iCloud account.""" try: diff --git a/homeassistant/components/icloud/device_tracker.py b/homeassistant/components/icloud/device_tracker.py index 9c2004f0edb..4a63486ae8d 100644 --- a/homeassistant/components/icloud/device_tracker.py +++ b/homeassistant/components/icloud/device_tracker.py @@ -35,7 +35,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up device tracker for iCloud component.""" - account = hass.data[DOMAIN][entry.unique_id] + account: IcloudAccount = hass.data[DOMAIN][entry.unique_id] tracked = set[str]() @callback diff --git a/homeassistant/components/icloud/sensor.py b/homeassistant/components/icloud/sensor.py index 6e415aa3350..3feb30f078f 100644 --- a/homeassistant/components/icloud/sensor.py +++ b/homeassistant/components/icloud/sensor.py @@ -20,7 +20,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up device tracker for iCloud component.""" - account = hass.data[DOMAIN][entry.unique_id] + account: IcloudAccount = hass.data[DOMAIN][entry.unique_id] tracked = set[str]() @callback From 8e3f5ec4709e990d80d19b920b9e3d5042e09a9a Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Mon, 8 Aug 2022 13:50:41 -0400 Subject: [PATCH 36/81] Bump version of pyunifiprotect to 4.0.12 (#76465) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 815b5250e1d..ad18be3dba9 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Protect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==4.0.11", "unifi-discovery==1.1.5"], + "requirements": ["pyunifiprotect==4.0.12", "unifi-discovery==1.1.5"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index f2bcb78f8c8..9c2c2f0eaae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2003,7 +2003,7 @@ pytrafikverket==0.2.0.1 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.0.11 +pyunifiprotect==4.0.12 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 551896a46e9..2cf8308fac1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1354,7 +1354,7 @@ pytrafikverket==0.2.0.1 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.0.11 +pyunifiprotect==4.0.12 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From 5213148fa84da1d6c89188c99bd95bb998440085 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 8 Aug 2022 13:06:49 -1000 Subject: [PATCH 37/81] Bump aiohomekit to 1.2.6 (#76488) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 5f6b3f92220..fa7bf39d385 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.5"], + "requirements": ["aiohomekit==1.2.6"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index 9c2c2f0eaae..21e2ffd6d68 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.5 +aiohomekit==1.2.6 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2cf8308fac1..7c37c01b24d 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.5 +aiohomekit==1.2.6 # homeassistant.components.emulated_hue # homeassistant.components.http From edac82487d950f3a507d6141a57f63ee942fa7a3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 9 Aug 2022 04:49:17 -1000 Subject: [PATCH 38/81] Fix inkbird ibbq2s that identify with xbbq (#76492) --- homeassistant/components/inkbird/manifest.json | 3 ++- homeassistant/generated/bluetooth.py | 4 ++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/inkbird/manifest.json b/homeassistant/components/inkbird/manifest.json index 686c9bada2d..b0ef08143c2 100644 --- a/homeassistant/components/inkbird/manifest.json +++ b/homeassistant/components/inkbird/manifest.json @@ -7,9 +7,10 @@ { "local_name": "sps" }, { "local_name": "Inkbird*" }, { "local_name": "iBBQ*" }, + { "local_name": "xBBQ*" }, { "local_name": "tps" } ], - "requirements": ["inkbird-ble==0.5.1"], + "requirements": ["inkbird-ble==0.5.2"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index d704d00ab8a..15a822599c6 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -75,6 +75,10 @@ BLUETOOTH: list[dict[str, str | int | list[int]]] = [ "domain": "inkbird", "local_name": "iBBQ*" }, + { + "domain": "inkbird", + "local_name": "xBBQ*" + }, { "domain": "inkbird", "local_name": "tps" diff --git a/requirements_all.txt b/requirements_all.txt index 21e2ffd6d68..be2ccf21bf8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -902,7 +902,7 @@ influxdb-client==1.24.0 influxdb==5.3.1 # homeassistant.components.inkbird -inkbird-ble==0.5.1 +inkbird-ble==0.5.2 # homeassistant.components.insteon insteon-frontend-home-assistant==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7c37c01b24d..e72cb275136 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -655,7 +655,7 @@ influxdb-client==1.24.0 influxdb==5.3.1 # homeassistant.components.inkbird -inkbird-ble==0.5.1 +inkbird-ble==0.5.2 # homeassistant.components.insteon insteon-frontend-home-assistant==0.2.0 From 6c1597ff9885a10f83a9d7ae74e6bf2e0d5a23f0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 9 Aug 2022 03:35:08 -1000 Subject: [PATCH 39/81] Bump govee-ble to 0.14.0 to fix H5052 sensors (#76497) --- homeassistant/components/govee_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/govee_ble/manifest.json b/homeassistant/components/govee_ble/manifest.json index eb33df867e1..2ba97b95d08 100644 --- a/homeassistant/components/govee_ble/manifest.json +++ b/homeassistant/components/govee_ble/manifest.json @@ -28,7 +28,7 @@ "service_uuid": "00008251-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["govee-ble==0.12.7"], + "requirements": ["govee-ble==0.14.0"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index be2ccf21bf8..29d5c25c679 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -760,7 +760,7 @@ googlemaps==2.5.1 goslide-api==0.5.1 # homeassistant.components.govee_ble -govee-ble==0.12.7 +govee-ble==0.14.0 # homeassistant.components.remote_rpi_gpio gpiozero==1.6.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e72cb275136..bfafbdefe81 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -561,7 +561,7 @@ google-nest-sdm==2.0.0 googlemaps==2.5.1 # homeassistant.components.govee_ble -govee-ble==0.12.7 +govee-ble==0.14.0 # homeassistant.components.gree greeclimate==1.3.0 From e5088d7e84f8b91e0e3b50e4c772694cb5e60d19 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 9 Aug 2022 09:48:48 -1000 Subject: [PATCH 40/81] Fix pairing with HK accessories that do not provide format for vendor chars (#76502) --- .../homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../fixtures/schlage_sense.json | 356 ++++++++++++++++++ .../specific_devices/test_schlage_sense.py | 40 ++ 5 files changed, 399 insertions(+), 3 deletions(-) create mode 100644 tests/components/homekit_controller/fixtures/schlage_sense.json create mode 100644 tests/components/homekit_controller/specific_devices/test_schlage_sense.py diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index fa7bf39d385..3107f5efbb2 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.6"], + "requirements": ["aiohomekit==1.2.7"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index 29d5c25c679..4b0ca28e2a4 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.6 +aiohomekit==1.2.7 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bfafbdefe81..6e79656b69c 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.6 +aiohomekit==1.2.7 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/tests/components/homekit_controller/fixtures/schlage_sense.json b/tests/components/homekit_controller/fixtures/schlage_sense.json new file mode 100644 index 00000000000..04e1923da55 --- /dev/null +++ b/tests/components/homekit_controller/fixtures/schlage_sense.json @@ -0,0 +1,356 @@ +[ + { + "aid": 1, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000020-0000-1000-8000-0026BB765291", + "iid": 4, + "perms": ["pr"], + "format": "string", + "value": "Schlage ", + "description": "Manufacturer", + "maxLen": 64 + }, + { + "type": "00000021-0000-1000-8000-0026BB765291", + "iid": 5, + "perms": ["pr"], + "format": "string", + "value": "BE479CAM619", + "description": "Model", + "maxLen": 64 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 6, + "perms": ["pr"], + "format": "string", + "value": "SENSE ", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000030-0000-1000-8000-0026BB765291", + "iid": 7, + "perms": ["pr"], + "format": "string", + "value": "AAAAAAA000", + "description": "Serial Number", + "maxLen": 64 + }, + { + "type": "00000014-0000-1000-8000-0026BB765291", + "iid": 3, + "perms": ["pw"], + "format": "bool", + "description": "Identify" + }, + { + "type": "00000052-0000-1000-8000-0026BB765291", + "iid": 8, + "perms": ["pr"], + "format": "string", + "value": "004.027.000", + "description": "Firmware Revision", + "maxLen": 64 + }, + { + "type": "00000053-0000-1000-8000-0026BB765291", + "iid": 51, + "perms": ["pr"], + "format": "string", + "value": "1.3.0", + "description": "Hardware Revision", + "maxLen": 64 + }, + { + "type": "00000054-0000-1000-8000-0026BB765291", + "iid": 50, + "perms": ["pr"], + "format": "string", + "value": "002.001.000", + "maxLen": 64 + } + ] + }, + { + "iid": 10, + "type": "7F0DEE73-4A3F-4103-98E6-A46CD301BDFB", + "characteristics": [ + { + "type": "44FF6853-58DB-4956-B298-5F6650DD61F6", + "iid": 25, + "perms": ["pw"], + "format": "data" + }, + { + "type": "CF68C40F-DC6F-4F7E-918C-4C536B643A2B", + "iid": 26, + "perms": ["pr", "pw"], + "format": "uint8", + "value": 0, + "minValue": 0, + "maxValue": 3, + "minStep": 1 + }, + { + "type": "4058C2B8-4545-4E77-B6B7-157C38F9718B", + "iid": 27, + "perms": ["pr", "pw"], + "format": "uint8", + "value": 0, + "minValue": 1, + "maxValue": 5, + "minStep": 1 + }, + { + "type": "B498F4B5-6364-4F79-B5CC-1563ADE070DF", + "iid": 28, + "perms": ["pr", "pw"], + "format": "uint8", + "value": 1, + "minValue": 0, + "maxValue": 1 + }, + { + "type": "AFAE7AD2-8DD3-4B20-BAE0-C0B18B79EDB5", + "iid": 29, + "perms": ["pw"], + "format": "data" + }, + { + "type": "87D91EC6-C508-4CAD-89F1-A21B0BF179A0", + "iid": 30, + "perms": ["pr"], + "format": "data", + "value": "000a00000000000000000000" + }, + { + "type": "4C3E2641-F57F-11E3-A3AC-0800200C9A66", + "iid": 31, + "perms": ["pr"], + "format": "uint64", + "value": 3468600224 + }, + { + "type": "EEC26990-F628-11E3-A3AC-0800200C9A66", + "iid": 32, + "perms": ["pr", "pw"], + "format": "uint8", + "value": 4, + "minValue": 4, + "maxValue": 8, + "minStep": 1 + }, + { + "type": "BCDE3B9E-3963-4123-B24D-42ECCBB3A9C4", + "iid": 33, + "perms": ["pr"], + "format": "data", + "value": "4e6f6e65" + }, + { + "type": "A9464D14-6806-4375-BA53-E14F7E0A6BEE", + "iid": 34, + "perms": ["pr", "pw"], + "format": null, + "value": "ff" + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 35, + "perms": ["pr"], + "format": "string", + "value": "Additional Settings", + "description": "Name", + "maxLen": 64 + }, + { + "type": "63D23C2F-2FBB-45E8-8540-47CC26C517D0", + "iid": 36, + "perms": ["pr"], + "format": "uint8", + "value": 100 + } + ] + }, + { + "iid": 23, + "type": "00000044-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000019-0000-1000-8000-0026BB765291", + "iid": 16, + "perms": ["pw"], + "format": "data", + "description": "Lock Control Point" + }, + { + "type": "00000037-0000-1000-8000-0026BB765291", + "iid": 17, + "perms": ["pr"], + "format": "string", + "value": "02.00.00", + "description": "Version", + "maxLen": 64 + }, + { + "type": "0000001F-0000-1000-8000-0026BB765291", + "iid": 18, + "perms": ["pr"], + "format": "data", + "value": "012431443133423434392d423941312d334135392d463042412d3245393030304233453430450208000000000000000003010404030001ff", + "description": "Logs" + }, + { + "type": "00000005-0000-1000-8000-0026BB765291", + "iid": 19, + "perms": ["pr", "pw"], + "format": "bool", + "value": true, + "description": "Audio Feedback" + }, + { + "type": "0000001A-0000-1000-8000-0026BB765291", + "iid": 20, + "perms": ["pr", "pw"], + "format": "uint32", + "value": 0, + "description": "Lock Management Auto Security Timeout", + "unit": "seconds" + }, + { + "type": "00000001-0000-1000-8000-0026BB765291", + "iid": 21, + "perms": ["pr", "pw"], + "format": "bool", + "value": false, + "description": "Administrator Only Access" + } + ] + }, + { + "iid": 30, + "type": "00000045-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "0000001D-0000-1000-8000-0026BB765291", + "iid": 11, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 3, + "description": "Lock Current State", + "minValue": 0, + "maxValue": 3, + "minStep": 1 + }, + { + "type": "0000001E-0000-1000-8000-0026BB765291", + "iid": 12, + "perms": ["pr", "pw", "ev"], + "format": "uint8", + "value": 1, + "description": "Lock Target State", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 13, + "perms": ["pr"], + "format": "string", + "value": "Lock Mechanism", + "description": "Name", + "maxLen": 64 + } + ] + }, + { + "iid": 34, + "type": "1F6B43AA-94DE-4BA9-981C-DA38823117BD", + "characteristics": [ + { + "type": "048D8799-695B-4A7F-A7F7-A4A1301587FE", + "iid": 39, + "perms": ["pw"], + "format": "data" + }, + { + "type": "66B7C7FD-95A7-4F89-B0AD-38073A67C46C", + "iid": 40, + "perms": ["pw"], + "format": "data" + }, + { + "type": "507EFC3F-9231-438C-976A-FA04427F1F8F", + "iid": 41, + "perms": ["pw"], + "format": "data" + }, + { + "type": "1DC15719-0882-4BAD-AB0F-9AEAB0600C90", + "iid": 42, + "perms": ["pr"], + "format": "data", + "value": "03" + } + ] + }, + { + "iid": 39, + "type": "00000055-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "0000004C-0000-1000-8000-0026BB765291", + "iid": 45, + "perms": [], + "format": "data", + "description": "Pair Setup" + }, + { + "type": "0000004E-0000-1000-8000-0026BB765291", + "iid": 46, + "perms": [], + "format": "data", + "description": "Pair Verify" + }, + { + "type": "0000004F-0000-1000-8000-0026BB765291", + "iid": 47, + "perms": [], + "format": "uint8", + "description": "Pairing Features" + }, + { + "type": "00000050-0000-1000-8000-0026BB765291", + "iid": 48, + "perms": ["pr", "pw"], + "format": "data", + "value": null, + "description": "Pairing Pairings" + } + ] + }, + { + "iid": 44, + "type": "000000A2-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000037-0000-1000-8000-0026BB765291", + "iid": 62, + "perms": ["pr"], + "format": "string", + "value": "02.00.00", + "description": "Version", + "maxLen": 64 + } + ] + } + ] + } +] diff --git a/tests/components/homekit_controller/specific_devices/test_schlage_sense.py b/tests/components/homekit_controller/specific_devices/test_schlage_sense.py new file mode 100644 index 00000000000..d572989e345 --- /dev/null +++ b/tests/components/homekit_controller/specific_devices/test_schlage_sense.py @@ -0,0 +1,40 @@ +"""Make sure that Schlage Sense is enumerated properly.""" + + +from tests.components.homekit_controller.common import ( + HUB_TEST_ACCESSORY_ID, + DeviceTestInfo, + EntityTestInfo, + assert_devices_and_entities_created, + setup_accessories_from_file, + setup_test_accessories, +) + + +async def test_schlage_sense_setup(hass): + """Test that the accessory can be correctly setup in HA.""" + accessories = await setup_accessories_from_file(hass, "schlage_sense.json") + await setup_test_accessories(hass, accessories) + + await assert_devices_and_entities_created( + hass, + DeviceTestInfo( + unique_id=HUB_TEST_ACCESSORY_ID, + name="SENSE ", + model="BE479CAM619", + manufacturer="Schlage ", + sw_version="004.027.000", + hw_version="1.3.0", + serial_number="AAAAAAA000", + devices=[], + entities=[ + EntityTestInfo( + entity_id="lock.sense_lock_mechanism", + friendly_name="SENSE Lock Mechanism", + unique_id="homekit-AAAAAAA000-30", + supported_features=0, + state="unknown", + ), + ], + ), + ) From 972c05eac8385a0b1abc0851f30029dce018ce46 Mon Sep 17 00:00:00 2001 From: Oscar Calvo <2091582+ocalvo@users.noreply.github.com> Date: Tue, 9 Aug 2022 14:46:22 -0600 Subject: [PATCH 41/81] Fix #76283 (#76531) --- homeassistant/components/sms/notify.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/sms/notify.py b/homeassistant/components/sms/notify.py index d076f3625ba..21b48946f55 100644 --- a/homeassistant/components/sms/notify.py +++ b/homeassistant/components/sms/notify.py @@ -20,31 +20,32 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def get_service(hass, config, discovery_info=None): """Get the SMS notification service.""" - if SMS_GATEWAY not in hass.data[DOMAIN]: - _LOGGER.error("SMS gateway not found, cannot initialize service") - return - - gateway = hass.data[DOMAIN][SMS_GATEWAY][GATEWAY] - if discovery_info is None: number = config[CONF_RECIPIENT] else: number = discovery_info[CONF_RECIPIENT] - return SMSNotificationService(gateway, number) + return SMSNotificationService(hass, number) class SMSNotificationService(BaseNotificationService): """Implement the notification service for SMS.""" - def __init__(self, gateway, number): + def __init__(self, hass, number): """Initialize the service.""" - self.gateway = gateway + + self.hass = hass self.number = number async def async_send_message(self, message="", **kwargs): """Send SMS message.""" + if SMS_GATEWAY not in self.hass.data[DOMAIN]: + _LOGGER.error("SMS gateway not found, cannot send message") + return + + gateway = self.hass.data[DOMAIN][SMS_GATEWAY][GATEWAY] + targets = kwargs.get(CONF_TARGET, [self.number]) smsinfo = { "Class": -1, @@ -67,6 +68,6 @@ class SMSNotificationService(BaseNotificationService): encoded_message["Number"] = target try: # Actually send the message - await self.gateway.send_sms_async(encoded_message) + await gateway.send_sms_async(encoded_message) except gammu.GSMError as exc: _LOGGER.error("Sending to %s failed: %s", target, exc) From 303d8b05d1b79421784e3c06bd8c35c60455d1e7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 9 Aug 2022 11:08:38 -1000 Subject: [PATCH 42/81] Bump aiohomekit to 1.2.8 (#76532) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 3107f5efbb2..cf3069e3b0d 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.7"], + "requirements": ["aiohomekit==1.2.8"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index 4b0ca28e2a4..c5254b61842 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.7 +aiohomekit==1.2.8 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6e79656b69c..c64f4506850 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.7 +aiohomekit==1.2.8 # homeassistant.components.emulated_hue # homeassistant.components.http From 56c80cd31accf41107fafb5269b0cde3f1ec87fd Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 9 Aug 2022 17:18:19 -0400 Subject: [PATCH 43/81] Bumped version to 2022.8.3 --- 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 dda81cbdc5c..f877b3f34f5 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 = "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/pyproject.toml b/pyproject.toml index 568607d6d0d..61f6b20af07 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.8.2" +version = "2022.8.3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 6e688b2b7f8f530c3d555af1b7b07936414646ec Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 10 Aug 2022 13:51:31 -0600 Subject: [PATCH 44/81] Bump ZHA dependencies (#76565) --- homeassistant/components/zha/manifest.json | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index bad84054f1f..1eb2536fed6 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,12 +4,12 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ - "bellows==0.31.3", + "bellows==0.32.0", "pyserial==3.5", "pyserial-asyncio==0.6", "zha-quirks==0.0.78", "zigpy-deconz==0.18.0", - "zigpy==0.49.0", + "zigpy==0.49.1", "zigpy-xbee==0.15.0", "zigpy-zigate==0.9.1", "zigpy-znp==0.8.1" diff --git a/requirements_all.txt b/requirements_all.txt index c5254b61842..1d078b34966 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -396,7 +396,7 @@ beautifulsoup4==4.11.1 # beewi_smartclim==0.0.10 # homeassistant.components.zha -bellows==0.31.3 +bellows==0.32.0 # homeassistant.components.bmw_connected_drive bimmer_connected==0.10.1 @@ -2535,7 +2535,7 @@ zigpy-zigate==0.9.1 zigpy-znp==0.8.1 # homeassistant.components.zha -zigpy==0.49.0 +zigpy==0.49.1 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c64f4506850..a3c3cf9ac48 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -320,7 +320,7 @@ base36==0.1.1 beautifulsoup4==4.11.1 # homeassistant.components.zha -bellows==0.31.3 +bellows==0.32.0 # homeassistant.components.bmw_connected_drive bimmer_connected==0.10.1 @@ -1709,7 +1709,7 @@ zigpy-zigate==0.9.1 zigpy-znp==0.8.1 # homeassistant.components.zha -zigpy==0.49.0 +zigpy==0.49.1 # homeassistant.components.zwave_js zwave-js-server-python==0.39.0 From 2dcc886b2f716148cb4270b107c5d08279badfb0 Mon Sep 17 00:00:00 2001 From: Dave Date: Wed, 10 Aug 2022 23:57:58 +0200 Subject: [PATCH 45/81] Replaces aiohttp.hdrs CONTENT_TYPE with plain string for the Swisscom integration (#76568) --- homeassistant/components/swisscom/device_tracker.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/swisscom/device_tracker.py b/homeassistant/components/swisscom/device_tracker.py index 3c8e7341be2..d95067e6b33 100644 --- a/homeassistant/components/swisscom/device_tracker.py +++ b/homeassistant/components/swisscom/device_tracker.py @@ -4,7 +4,6 @@ from __future__ import annotations from contextlib import suppress import logging -from aiohttp.hdrs import CONTENT_TYPE import requests import voluptuous as vol @@ -79,7 +78,7 @@ class SwisscomDeviceScanner(DeviceScanner): def get_swisscom_data(self): """Retrieve data from Swisscom and return parsed result.""" url = f"http://{self.host}/ws" - headers = {CONTENT_TYPE: "application/x-sah-ws-4-call+json"} + headers = {"Content-Type": "application/x-sah-ws-4-call+json"} data = """ {"service":"Devices", "method":"get", "parameters":{"expression":"lan and not self"}}""" From a3ea881a0161e8603e20f852d1666862c0591589 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Thu, 11 Aug 2022 02:35:05 +0100 Subject: [PATCH 46/81] Fix homekit_controller not noticing ip and port changes that zeroconf has found (#76570) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index cf3069e3b0d..4bd9a0b70f9 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.8"], + "requirements": ["aiohomekit==1.2.9"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index 1d078b34966..c0a8eddb705 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.8 +aiohomekit==1.2.9 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a3c3cf9ac48..92816e5efd4 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.8 +aiohomekit==1.2.9 # homeassistant.components.emulated_hue # homeassistant.components.http From 738423056e6af8f1c695badda7732f63ead2e118 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 11 Aug 2022 03:27:52 +0200 Subject: [PATCH 47/81] Fix Spotify deviding None value in current progress (#76581) --- homeassistant/components/spotify/media_player.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index 04f523c2d4b..5fcb5f28264 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -180,7 +180,10 @@ class SpotifyMediaPlayer(MediaPlayerEntity): @property def media_position(self) -> int | None: """Position of current playing media in seconds.""" - if not self._currently_playing: + if ( + not self._currently_playing + or self._currently_playing.get("progress_ms") is None + ): return None return self._currently_playing["progress_ms"] / 1000 From 451ab47caaaf69554193bc7caf3384522d1ede15 Mon Sep 17 00:00:00 2001 From: Antonino Piazza Date: Thu, 11 Aug 2022 11:03:12 +0200 Subject: [PATCH 48/81] Improve code quality in huawei_lte (#76583) Co-authored-by: Martin Hjelmare --- .../components/huawei_lte/__init__.py | 9 +- homeassistant/components/huawei_lte/switch.py | 4 +- tests/components/huawei_lte/test_switches.py | 182 ++++++++++-------- 3 files changed, 107 insertions(+), 88 deletions(-) diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index bace633f128..565286c4505 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -279,11 +279,12 @@ class Router: self._get_data( KEY_WLAN_WIFI_GUEST_NETWORK_SWITCH, lambda: next( - filter( - lambda ssid: ssid.get("wifiisguestnetwork") == "1", - self.client.wlan.multi_basic_settings() + ( + ssid + for ssid in self.client.wlan.multi_basic_settings() .get("Ssids", {}) - .get("Ssid", []), + .get("Ssid", []) + if isinstance(ssid, dict) and ssid.get("wifiisguestnetwork") == "1" ), {}, ), diff --git a/homeassistant/components/huawei_lte/switch.py b/homeassistant/components/huawei_lte/switch.py index 78579d62698..261b77987cf 100644 --- a/homeassistant/components/huawei_lte/switch.py +++ b/homeassistant/components/huawei_lte/switch.py @@ -37,7 +37,7 @@ async def async_setup_entry( if router.data.get(KEY_DIALUP_MOBILE_DATASWITCH): switches.append(HuaweiLteMobileDataSwitch(router)) - if router.data.get(KEY_WLAN_WIFI_GUEST_NETWORK_SWITCH).get("WifiEnable"): + if router.data.get(KEY_WLAN_WIFI_GUEST_NETWORK_SWITCH, {}).get("WifiEnable"): switches.append(HuaweiLteWifiGuestNetworkSwitch(router)) async_add_entities(switches, True) @@ -151,6 +151,6 @@ class HuaweiLteWifiGuestNetworkSwitch(HuaweiLteBaseSwitch): return "mdi:wifi" if self.is_on else "mdi:wifi-off" @property - def extra_state_attributes(self) -> dict[str, str]: + def extra_state_attributes(self) -> dict[str, str | None]: """Return the state attributes.""" return {"ssid": self.router.data[self.key].get("WifiSsid")} diff --git a/tests/components/huawei_lte/test_switches.py b/tests/components/huawei_lte/test_switches.py index 5bafed27e70..4b0b81a86cd 100644 --- a/tests/components/huawei_lte/test_switches.py +++ b/tests/components/huawei_lte/test_switches.py @@ -2,7 +2,6 @@ from unittest.mock import MagicMock, patch from huawei_lte_api.enums.cradle import ConnectionStatusEnum -from pytest import fixture from homeassistant.components.huawei_lte.const import DOMAIN from homeassistant.components.switch import ( @@ -20,94 +19,70 @@ from tests.common import MockConfigEntry SWITCH_WIFI_GUEST_NETWORK = "switch.lte_wifi_guest_network" -@fixture +def magic_client(multi_basic_settings_value: dict) -> MagicMock: + """Mock huawei_lte.Client.""" + information = MagicMock(return_value={"SerialNumber": "test-serial-number"}) + check_notifications = MagicMock(return_value={"SmsStorageFull": 0}) + status = MagicMock( + return_value={"ConnectionStatus": ConnectionStatusEnum.CONNECTED.value} + ) + multi_basic_settings = MagicMock(return_value=multi_basic_settings_value) + wifi_feature_switch = MagicMock(return_value={"wifi24g_switch_enable": 1}) + device = MagicMock(information=information) + monitoring = MagicMock(check_notifications=check_notifications, status=status) + wlan = MagicMock( + multi_basic_settings=multi_basic_settings, + wifi_feature_switch=wifi_feature_switch, + ) + return MagicMock(device=device, monitoring=monitoring, wlan=wlan) + + @patch("homeassistant.components.huawei_lte.Connection", MagicMock()) -@patch( - "homeassistant.components.huawei_lte.Client", - return_value=MagicMock( - device=MagicMock( - information=MagicMock(return_value={"SerialNumber": "test-serial-number"}) - ), - monitoring=MagicMock( - check_notifications=MagicMock(return_value={"SmsStorageFull": 0}), - status=MagicMock( - return_value={"ConnectionStatus": ConnectionStatusEnum.CONNECTED.value} - ), - ), - wlan=MagicMock( - multi_basic_settings=MagicMock( - return_value={ - "Ssids": {"Ssid": [{"wifiisguestnetwork": "1", "WifiEnable": "0"}]} - } - ), - wifi_feature_switch=MagicMock(return_value={"wifi24g_switch_enable": 1}), - ), - ), -) -async def setup_component_with_wifi_guest_network( - client: MagicMock, hass: HomeAssistant -) -> None: - """Initialize huawei_lte components.""" - assert client - huawei_lte = MockConfigEntry(domain=DOMAIN, data={CONF_URL: "http://huawei-lte"}) - huawei_lte.add_to_hass(hass) - assert await hass.config_entries.async_setup(huawei_lte.entry_id) - await hass.async_block_till_done() - - -@fixture -@patch("homeassistant.components.huawei_lte.Connection", MagicMock()) -@patch( - "homeassistant.components.huawei_lte.Client", - return_value=MagicMock( - device=MagicMock( - information=MagicMock(return_value={"SerialNumber": "test-serial-number"}) - ), - monitoring=MagicMock( - check_notifications=MagicMock(return_value={"SmsStorageFull": 0}), - status=MagicMock( - return_value={"ConnectionStatus": ConnectionStatusEnum.CONNECTED.value} - ), - ), - wlan=MagicMock( - multi_basic_settings=MagicMock(return_value={}), - wifi_feature_switch=MagicMock(return_value={"wifi24g_switch_enable": 1}), - ), - ), -) -async def setup_component_without_wifi_guest_network( - client: MagicMock, hass: HomeAssistant -) -> None: - """Initialize huawei_lte components.""" - assert client - huawei_lte = MockConfigEntry(domain=DOMAIN, data={CONF_URL: "http://huawei-lte"}) - huawei_lte.add_to_hass(hass) - assert await hass.config_entries.async_setup(huawei_lte.entry_id) - await hass.async_block_till_done() - - -def test_huawei_lte_wifi_guest_network_config_entry_when_network_is_not_present( +@patch("homeassistant.components.huawei_lte.Client", return_value=magic_client({})) +async def test_huawei_lte_wifi_guest_network_config_entry_when_network_is_not_present( + client, hass: HomeAssistant, - setup_component_without_wifi_guest_network, ) -> None: """Test switch wifi guest network config entry when network is not present.""" + huawei_lte = MockConfigEntry(domain=DOMAIN, data={CONF_URL: "http://huawei-lte"}) + huawei_lte.add_to_hass(hass) + await hass.config_entries.async_setup(huawei_lte.entry_id) + await hass.async_block_till_done() entity_registry: EntityRegistry = er.async_get(hass) assert not entity_registry.async_is_registered(SWITCH_WIFI_GUEST_NETWORK) -def test_huawei_lte_wifi_guest_network_config_entry_when_network_is_present( +@patch("homeassistant.components.huawei_lte.Connection", MagicMock()) +@patch( + "homeassistant.components.huawei_lte.Client", + return_value=magic_client( + {"Ssids": {"Ssid": [{"wifiisguestnetwork": "1", "WifiEnable": "0"}]}} + ), +) +async def test_huawei_lte_wifi_guest_network_config_entry_when_network_is_present( + client, hass: HomeAssistant, - setup_component_with_wifi_guest_network, ) -> None: """Test switch wifi guest network config entry when network is present.""" + huawei_lte = MockConfigEntry(domain=DOMAIN, data={CONF_URL: "http://huawei-lte"}) + huawei_lte.add_to_hass(hass) + await hass.config_entries.async_setup(huawei_lte.entry_id) + await hass.async_block_till_done() entity_registry: EntityRegistry = er.async_get(hass) assert entity_registry.async_is_registered(SWITCH_WIFI_GUEST_NETWORK) -async def test_turn_on_switch_wifi_guest_network( - hass: HomeAssistant, setup_component_with_wifi_guest_network -) -> None: +@patch("homeassistant.components.huawei_lte.Connection", MagicMock()) +@patch("homeassistant.components.huawei_lte.Client") +async def test_turn_on_switch_wifi_guest_network(client, hass: HomeAssistant) -> None: """Test switch wifi guest network turn on method.""" + client.return_value = magic_client( + {"Ssids": {"Ssid": [{"wifiisguestnetwork": "1", "WifiEnable": "0"}]}} + ) + huawei_lte = MockConfigEntry(domain=DOMAIN, data={CONF_URL: "http://huawei-lte"}) + huawei_lte.add_to_hass(hass) + await hass.config_entries.async_setup(huawei_lte.entry_id) + await hass.async_block_till_done() await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_ON, @@ -116,15 +91,20 @@ async def test_turn_on_switch_wifi_guest_network( ) await hass.async_block_till_done() assert hass.states.is_state(SWITCH_WIFI_GUEST_NETWORK, STATE_ON) - hass.data[DOMAIN].routers[ - "test-serial-number" - ].client.wlan.wifi_guest_network_switch.assert_called_once_with(True) + client.return_value.wlan.wifi_guest_network_switch.assert_called_once_with(True) -async def test_turn_off_switch_wifi_guest_network( - hass: HomeAssistant, setup_component_with_wifi_guest_network -) -> None: +@patch("homeassistant.components.huawei_lte.Connection", MagicMock()) +@patch("homeassistant.components.huawei_lte.Client") +async def test_turn_off_switch_wifi_guest_network(client, hass: HomeAssistant) -> None: """Test switch wifi guest network turn off method.""" + client.return_value = magic_client( + {"Ssids": {"Ssid": [{"wifiisguestnetwork": "1", "WifiEnable": "1"}]}} + ) + huawei_lte = MockConfigEntry(domain=DOMAIN, data={CONF_URL: "http://huawei-lte"}) + huawei_lte.add_to_hass(hass) + await hass.config_entries.async_setup(huawei_lte.entry_id) + await hass.async_block_till_done() await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_OFF, @@ -133,6 +113,44 @@ async def test_turn_off_switch_wifi_guest_network( ) await hass.async_block_till_done() assert hass.states.is_state(SWITCH_WIFI_GUEST_NETWORK, STATE_OFF) - hass.data[DOMAIN].routers[ - "test-serial-number" - ].client.wlan.wifi_guest_network_switch.assert_called_with(False) + client.return_value.wlan.wifi_guest_network_switch.assert_called_with(False) + + +@patch("homeassistant.components.huawei_lte.Connection", MagicMock()) +@patch( + "homeassistant.components.huawei_lte.Client", + return_value=magic_client({"Ssids": {"Ssid": "str"}}), +) +async def test_huawei_lte_wifi_guest_network_config_entry_when_ssid_is_str( + client, hass: HomeAssistant +): + """Test switch wifi guest network config entry when ssid is a str. + + Issue #76244. Huawai models: H312-371, E5372 and E8372. + """ + huawei_lte = MockConfigEntry(domain=DOMAIN, data={CONF_URL: "http://huawei-lte"}) + huawei_lte.add_to_hass(hass) + await hass.config_entries.async_setup(huawei_lte.entry_id) + await hass.async_block_till_done() + entity_registry: EntityRegistry = er.async_get(hass) + assert not entity_registry.async_is_registered(SWITCH_WIFI_GUEST_NETWORK) + + +@patch("homeassistant.components.huawei_lte.Connection", MagicMock()) +@patch( + "homeassistant.components.huawei_lte.Client", + return_value=magic_client({"Ssids": {"Ssid": None}}), +) +async def test_huawei_lte_wifi_guest_network_config_entry_when_ssid_is_none( + client, hass: HomeAssistant +): + """Test switch wifi guest network config entry when ssid is a None. + + Issue #76244. + """ + huawei_lte = MockConfigEntry(domain=DOMAIN, data={CONF_URL: "http://huawei-lte"}) + huawei_lte.add_to_hass(hass) + await hass.config_entries.async_setup(huawei_lte.entry_id) + await hass.async_block_till_done() + entity_registry: EntityRegistry = er.async_get(hass) + assert not entity_registry.async_is_registered(SWITCH_WIFI_GUEST_NETWORK) From 99c20223e5271244e27c4313ed1c6a953c648e62 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 10 Aug 2022 23:08:03 -1000 Subject: [PATCH 49/81] Fix Govee 5181 with old firmware (#76600) --- homeassistant/components/govee_ble/manifest.json | 6 +++++- homeassistant/generated/bluetooth.py | 5 +++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/govee_ble/manifest.json b/homeassistant/components/govee_ble/manifest.json index 2ba97b95d08..74917e3ca14 100644 --- a/homeassistant/components/govee_ble/manifest.json +++ b/homeassistant/components/govee_ble/manifest.json @@ -19,6 +19,10 @@ "manufacturer_id": 818, "service_uuid": "00008551-0000-1000-8000-00805f9b34fb" }, + { + "manufacturer_id": 59970, + "service_uuid": "00008151-0000-1000-8000-00805f9b34fb" + }, { "manufacturer_id": 14474, "service_uuid": "00008151-0000-1000-8000-00805f9b34fb" @@ -28,7 +32,7 @@ "service_uuid": "00008251-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["govee-ble==0.14.0"], + "requirements": ["govee-ble==0.14.1"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 15a822599c6..4f46ded6936 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -46,6 +46,11 @@ BLUETOOTH: list[dict[str, str | int | list[int]]] = [ "manufacturer_id": 818, "service_uuid": "00008551-0000-1000-8000-00805f9b34fb" }, + { + "domain": "govee_ble", + "manufacturer_id": 59970, + "service_uuid": "00008151-0000-1000-8000-00805f9b34fb" + }, { "domain": "govee_ble", "manufacturer_id": 14474, diff --git a/requirements_all.txt b/requirements_all.txt index c0a8eddb705..96be4dae1b1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -760,7 +760,7 @@ googlemaps==2.5.1 goslide-api==0.5.1 # homeassistant.components.govee_ble -govee-ble==0.14.0 +govee-ble==0.14.1 # homeassistant.components.remote_rpi_gpio gpiozero==1.6.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 92816e5efd4..59b922c4ef1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -561,7 +561,7 @@ google-nest-sdm==2.0.0 googlemaps==2.5.1 # homeassistant.components.govee_ble -govee-ble==0.14.0 +govee-ble==0.14.1 # homeassistant.components.gree greeclimate==1.3.0 From 294cc3ac6e3917b810b70c0d2b89793d0069c9b7 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 11 Aug 2022 14:53:40 +0200 Subject: [PATCH 50/81] Fix evohome preset modes (#76606) --- homeassistant/components/evohome/climate.py | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index c1a630d0d05..e9eab8c2ae3 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -128,26 +128,17 @@ class EvoClimateEntity(EvoDevice, ClimateEntity): _attr_temperature_unit = TEMP_CELSIUS - def __init__(self, evo_broker, evo_device) -> None: - """Initialize a Climate device.""" - super().__init__(evo_broker, evo_device) - - self._preset_modes = None - @property def hvac_modes(self) -> list[str]: """Return a list of available hvac operation modes.""" return list(HA_HVAC_TO_TCS) - @property - def preset_modes(self) -> list[str] | None: - """Return a list of available preset modes.""" - return self._preset_modes - class EvoZone(EvoChild, EvoClimateEntity): """Base for a Honeywell TCC Zone.""" + _attr_preset_modes = list(HA_PRESET_TO_EVO) + def __init__(self, evo_broker, evo_device) -> None: """Initialize a Honeywell TCC Zone.""" super().__init__(evo_broker, evo_device) @@ -233,7 +224,7 @@ class EvoZone(EvoChild, EvoClimateEntity): """ return self._evo_device.setpointCapabilities["maxHeatSetpoint"] - async def async_set_temperature(self, **kwargs) -> None: + async def async_set_temperature(self, **kwargs: Any) -> None: """Set a new target temperature.""" temperature = kwargs["temperature"] @@ -249,7 +240,7 @@ class EvoZone(EvoChild, EvoClimateEntity): self._evo_device.set_temperature(temperature, until=until) ) - async def async_set_hvac_mode(self, hvac_mode: str) -> None: + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set a Zone to one of its native EVO_* operating modes. Zones inherit their _effective_ operating mode from their Controller. @@ -387,7 +378,7 @@ class EvoController(EvoClimateEntity): """Return the current preset mode, e.g., home, away, temp.""" return TCS_PRESET_TO_HA.get(self._evo_tcs.systemModeStatus["mode"]) - async def async_set_temperature(self, **kwargs) -> None: + async def async_set_temperature(self, **kwargs: Any) -> None: """Raise exception as Controllers don't have a target temperature.""" raise NotImplementedError("Evohome Controllers don't have target temperatures.") From 84b8029c6a6572fbb4e64cf29c45cfe847999d82 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 11 Aug 2022 13:55:52 -1000 Subject: [PATCH 51/81] Add missing _abort_if_unique_id_configured to ble integrations (#76624) --- .../components/govee_ble/config_flow.py | 1 + .../components/inkbird/config_flow.py | 1 + homeassistant/components/moat/config_flow.py | 1 + .../components/sensorpush/config_flow.py | 1 + .../components/xiaomi_ble/config_flow.py | 1 + .../components/govee_ble/test_config_flow.py | 30 +++++++++++++++++++ tests/components/inkbird/test_config_flow.py | 28 +++++++++++++++++ tests/components/moat/test_config_flow.py | 28 +++++++++++++++++ .../components/sensorpush/test_config_flow.py | 30 +++++++++++++++++++ .../components/xiaomi_ble/test_config_flow.py | 30 +++++++++++++++++++ 10 files changed, 151 insertions(+) diff --git a/homeassistant/components/govee_ble/config_flow.py b/homeassistant/components/govee_ble/config_flow.py index 1e3a5566bfd..47d73f2779a 100644 --- a/homeassistant/components/govee_ble/config_flow.py +++ b/homeassistant/components/govee_ble/config_flow.py @@ -67,6 +67,7 @@ class GoveeConfigFlow(ConfigFlow, domain=DOMAIN): if user_input is not None: address = user_input[CONF_ADDRESS] await self.async_set_unique_id(address, raise_on_progress=False) + self._abort_if_unique_id_configured() return self.async_create_entry( title=self._discovered_devices[address], data={} ) diff --git a/homeassistant/components/inkbird/config_flow.py b/homeassistant/components/inkbird/config_flow.py index 21ed85e117e..524471bbcc7 100644 --- a/homeassistant/components/inkbird/config_flow.py +++ b/homeassistant/components/inkbird/config_flow.py @@ -67,6 +67,7 @@ class INKBIRDConfigFlow(ConfigFlow, domain=DOMAIN): if user_input is not None: address = user_input[CONF_ADDRESS] await self.async_set_unique_id(address, raise_on_progress=False) + self._abort_if_unique_id_configured() return self.async_create_entry( title=self._discovered_devices[address], data={} ) diff --git a/homeassistant/components/moat/config_flow.py b/homeassistant/components/moat/config_flow.py index 6f51b62d110..0e1b4f89568 100644 --- a/homeassistant/components/moat/config_flow.py +++ b/homeassistant/components/moat/config_flow.py @@ -67,6 +67,7 @@ class MoatConfigFlow(ConfigFlow, domain=DOMAIN): if user_input is not None: address = user_input[CONF_ADDRESS] await self.async_set_unique_id(address, raise_on_progress=False) + self._abort_if_unique_id_configured() return self.async_create_entry( title=self._discovered_devices[address], data={} ) diff --git a/homeassistant/components/sensorpush/config_flow.py b/homeassistant/components/sensorpush/config_flow.py index d10c2f481a6..63edd59a5b7 100644 --- a/homeassistant/components/sensorpush/config_flow.py +++ b/homeassistant/components/sensorpush/config_flow.py @@ -67,6 +67,7 @@ class SensorPushConfigFlow(ConfigFlow, domain=DOMAIN): if user_input is not None: address = user_input[CONF_ADDRESS] await self.async_set_unique_id(address, raise_on_progress=False) + self._abort_if_unique_id_configured() return self.async_create_entry( title=self._discovered_devices[address], data={} ) diff --git a/homeassistant/components/xiaomi_ble/config_flow.py b/homeassistant/components/xiaomi_ble/config_flow.py index a05e703db6a..4ec3b66d0f9 100644 --- a/homeassistant/components/xiaomi_ble/config_flow.py +++ b/homeassistant/components/xiaomi_ble/config_flow.py @@ -205,6 +205,7 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): if user_input is not None: address = user_input[CONF_ADDRESS] await self.async_set_unique_id(address, raise_on_progress=False) + self._abort_if_unique_id_configured() discovery = self._discovered_devices[address] self.context["title_placeholders"] = {"name": discovery.title} diff --git a/tests/components/govee_ble/test_config_flow.py b/tests/components/govee_ble/test_config_flow.py index a1b9fed3cd7..188672cdf18 100644 --- a/tests/components/govee_ble/test_config_flow.py +++ b/tests/components/govee_ble/test_config_flow.py @@ -78,6 +78,36 @@ async def test_async_step_user_with_found_devices(hass): assert result2["result"].unique_id == "4125DDBA-2774-4851-9889-6AADDD4CAC3D" +async def test_async_step_user_device_added_between_steps(hass): + """Test the device gets added via another flow between steps.""" + with patch( + "homeassistant.components.govee_ble.config_flow.async_discovered_service_info", + return_value=[GVH5177_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="4125DDBA-2774-4851-9889-6AADDD4CAC3D", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.govee_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "4125DDBA-2774-4851-9889-6AADDD4CAC3D"}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "already_configured" + + async def test_async_step_user_with_found_devices_already_setup(hass): """Test setup from service info cache with devices found.""" entry = MockConfigEntry( diff --git a/tests/components/inkbird/test_config_flow.py b/tests/components/inkbird/test_config_flow.py index c1f8b3ef545..fe210f75f4b 100644 --- a/tests/components/inkbird/test_config_flow.py +++ b/tests/components/inkbird/test_config_flow.py @@ -74,6 +74,34 @@ async def test_async_step_user_with_found_devices(hass): assert result2["result"].unique_id == "61DE521B-F0BF-9F44-64D4-75BBE1738105" +async def test_async_step_user_device_added_between_steps(hass): + """Test the device gets added via another flow between steps.""" + with patch( + "homeassistant.components.inkbird.config_flow.async_discovered_service_info", + return_value=[SPS_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="61DE521B-F0BF-9F44-64D4-75BBE1738105", + ) + entry.add_to_hass(hass) + + with patch("homeassistant.components.inkbird.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "61DE521B-F0BF-9F44-64D4-75BBE1738105"}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "already_configured" + + async def test_async_step_user_with_found_devices_already_setup(hass): """Test setup from service info cache with devices found.""" entry = MockConfigEntry( diff --git a/tests/components/moat/test_config_flow.py b/tests/components/moat/test_config_flow.py index 7ceeb2ad73f..6e92be703a2 100644 --- a/tests/components/moat/test_config_flow.py +++ b/tests/components/moat/test_config_flow.py @@ -74,6 +74,34 @@ async def test_async_step_user_with_found_devices(hass): assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" +async def test_async_step_user_device_added_between_steps(hass): + """Test the device gets added via another flow between steps.""" + with patch( + "homeassistant.components.moat.config_flow.async_discovered_service_info", + return_value=[MOAT_S2_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + with patch("homeassistant.components.moat.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "aa:bb:cc:dd:ee:ff"}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "already_configured" + + async def test_async_step_user_with_found_devices_already_setup(hass): """Test setup from service info cache with devices found.""" entry = MockConfigEntry( diff --git a/tests/components/sensorpush/test_config_flow.py b/tests/components/sensorpush/test_config_flow.py index 1c825640603..244787eecb9 100644 --- a/tests/components/sensorpush/test_config_flow.py +++ b/tests/components/sensorpush/test_config_flow.py @@ -78,6 +78,36 @@ async def test_async_step_user_with_found_devices(hass): assert result2["result"].unique_id == "61DE521B-F0BF-9F44-64D4-75BBE1738105" +async def test_async_step_user_device_added_between_steps(hass): + """Test the device gets added via another flow between steps.""" + with patch( + "homeassistant.components.sensorpush.config_flow.async_discovered_service_info", + return_value=[HTW_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="61DE521B-F0BF-9F44-64D4-75BBE1738105", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.sensorpush.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "61DE521B-F0BF-9F44-64D4-75BBE1738105"}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "already_configured" + + async def test_async_step_user_with_found_devices_already_setup(hass): """Test setup from service info cache with devices found.""" entry = MockConfigEntry( diff --git a/tests/components/xiaomi_ble/test_config_flow.py b/tests/components/xiaomi_ble/test_config_flow.py index 32ba6be3322..2da3ce7140d 100644 --- a/tests/components/xiaomi_ble/test_config_flow.py +++ b/tests/components/xiaomi_ble/test_config_flow.py @@ -708,6 +708,36 @@ async def test_async_step_user_with_found_devices_legacy_encryption_wrong_key_le assert result2["result"].unique_id == "F8:24:41:C5:98:8B" +async def test_async_step_user_device_added_between_steps(hass): + """Test the device gets added via another flow between steps.""" + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_discovered_service_info", + return_value=[LYWSDCGQ_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="58:2D:34:35:93:21", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "58:2D:34:35:93:21"}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "already_configured" + + async def test_async_step_user_with_found_devices_already_setup(hass): """Test setup from service info cache with devices found.""" entry = MockConfigEntry( From 5606b4026f1412a39204cd7d4fb42cb9fef9d752 Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Fri, 12 Aug 2022 15:19:16 +0200 Subject: [PATCH 52/81] Fix non-awaited coroutine in BMW notify (#76664) Co-authored-by: rikroe --- homeassistant/components/bmw_connected_drive/notify.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/notify.py b/homeassistant/components/bmw_connected_drive/notify.py index 14f6c94dff6..b48441ae5fe 100644 --- a/homeassistant/components/bmw_connected_drive/notify.py +++ b/homeassistant/components/bmw_connected_drive/notify.py @@ -56,7 +56,7 @@ class BMWNotificationService(BaseNotificationService): """Set up the notification service.""" self.targets: dict[str, MyBMWVehicle] = targets - def send_message(self, message: str = "", **kwargs: Any) -> None: + async def async_send_message(self, message: str = "", **kwargs: Any) -> None: """Send a message or POI to the car.""" for vehicle in kwargs[ATTR_TARGET]: vehicle = cast(MyBMWVehicle, vehicle) @@ -81,6 +81,6 @@ class BMWNotificationService(BaseNotificationService): } ) - vehicle.remote_services.trigger_send_poi(location_dict) + await vehicle.remote_services.trigger_send_poi(location_dict) else: raise ValueError(f"'data.{ATTR_LOCATION}' is required.") From fdde4d540d5bd0522ba5d2b2235649032132a97d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 12 Aug 2022 14:04:21 -0400 Subject: [PATCH 53/81] Bumped version to 2022.8.4 --- 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 f877b3f34f5..e79c132519d 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 = "3" +PATCH_VERSION: Final = "4" __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 61f6b20af07..6c6b88feb4a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.8.3" +version = "2022.8.4" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 872b2f56ac5f3baf0a31c44e51e31a406cfb788d Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Mon, 15 Aug 2022 14:00:29 +0100 Subject: [PATCH 54/81] Update systembridgeconnector to 3.4.4 (#75362) Co-authored-by: Martin Hjelmare --- .../components/system_bridge/__init__.py | 18 +- .../components/system_bridge/config_flow.py | 32 ++- .../components/system_bridge/coordinator.py | 13 +- .../components/system_bridge/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../system_bridge/test_config_flow.py | 208 ++++++++++++------ 7 files changed, 183 insertions(+), 94 deletions(-) diff --git a/homeassistant/components/system_bridge/__init__.py b/homeassistant/components/system_bridge/__init__.py index 19bcc224a66..74be1faed40 100644 --- a/homeassistant/components/system_bridge/__init__.py +++ b/homeassistant/components/system_bridge/__init__.py @@ -10,6 +10,10 @@ from systembridgeconnector.exceptions import ( ConnectionClosedException, ConnectionErrorException, ) +from systembridgeconnector.models.keyboard_key import KeyboardKey +from systembridgeconnector.models.keyboard_text import KeyboardText +from systembridgeconnector.models.open_path import OpenPath +from systembridgeconnector.models.open_url import OpenUrl from systembridgeconnector.version import SUPPORTED_VERSION, Version import voluptuous as vol @@ -149,7 +153,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][ call.data[CONF_BRIDGE] ] - await coordinator.websocket_client.open_path(call.data[CONF_PATH]) + await coordinator.websocket_client.open_path( + OpenPath(path=call.data[CONF_PATH]) + ) async def handle_open_url(call: ServiceCall) -> None: """Handle the open url service call.""" @@ -157,21 +163,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][ call.data[CONF_BRIDGE] ] - await coordinator.websocket_client.open_url(call.data[CONF_URL]) + await coordinator.websocket_client.open_url(OpenUrl(url=call.data[CONF_URL])) async def handle_send_keypress(call: ServiceCall) -> None: """Handle the send_keypress service call.""" coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][ call.data[CONF_BRIDGE] ] - await coordinator.websocket_client.keyboard_keypress(call.data[CONF_KEY]) + await coordinator.websocket_client.keyboard_keypress( + KeyboardKey(key=call.data[CONF_KEY]) + ) async def handle_send_text(call: ServiceCall) -> None: """Handle the send_keypress service call.""" coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][ call.data[CONF_BRIDGE] ] - await coordinator.websocket_client.keyboard_text(call.data[CONF_TEXT]) + await coordinator.websocket_client.keyboard_text( + KeyboardText(text=call.data[CONF_TEXT]) + ) hass.services.async_register( DOMAIN, diff --git a/homeassistant/components/system_bridge/config_flow.py b/homeassistant/components/system_bridge/config_flow.py index 9d89cf83288..995df6391cc 100644 --- a/homeassistant/components/system_bridge/config_flow.py +++ b/homeassistant/components/system_bridge/config_flow.py @@ -7,12 +7,13 @@ import logging from typing import Any import async_timeout -from systembridgeconnector.const import EVENT_MODULE, EVENT_TYPE, TYPE_DATA_UPDATE from systembridgeconnector.exceptions import ( AuthenticationException, ConnectionClosedException, ConnectionErrorException, ) +from systembridgeconnector.models.get_data import GetData +from systembridgeconnector.models.system import System from systembridgeconnector.websocket_client import WebSocketClient import voluptuous as vol @@ -38,7 +39,7 @@ STEP_USER_DATA_SCHEMA = vol.Schema( ) -async def validate_input( +async def _validate_input( hass: HomeAssistant, data: dict[str, Any], ) -> dict[str, str]: @@ -56,15 +57,12 @@ async def validate_input( try: async with async_timeout.timeout(30): await websocket_client.connect(session=async_get_clientsession(hass)) - await websocket_client.get_data(["system"]) - while True: - message = await websocket_client.receive_message() - _LOGGER.debug("Message: %s", message) - if ( - message[EVENT_TYPE] == TYPE_DATA_UPDATE - and message[EVENT_MODULE] == "system" - ): - break + hass.async_create_task(websocket_client.listen()) + response = await websocket_client.get_data(GetData(modules=["system"])) + _LOGGER.debug("Got response: %s", response.json()) + if response.data is None or not isinstance(response.data, System): + raise CannotConnect("No data received") + system: System = response.data except AuthenticationException as exception: _LOGGER.warning( "Authentication error when connecting to %s: %s", data[CONF_HOST], exception @@ -81,14 +79,12 @@ async def validate_input( except asyncio.TimeoutError as exception: _LOGGER.warning("Timed out connecting to %s: %s", data[CONF_HOST], exception) raise CannotConnect from exception + except ValueError as exception: + raise CannotConnect from exception - _LOGGER.debug("%s Message: %s", TYPE_DATA_UPDATE, message) + _LOGGER.debug("Got System data: %s", system.json()) - if "uuid" not in message["data"]: - error = "No UUID in result!" - raise CannotConnect(error) - - return {"hostname": host, "uuid": message["data"]["uuid"]} + return {"hostname": host, "uuid": system.uuid} async def _async_get_info( @@ -98,7 +94,7 @@ async def _async_get_info( errors = {} try: - info = await validate_input(hass, user_input) + info = await _validate_input(hass, user_input) except CannotConnect: errors["base"] = "cannot_connect" except InvalidAuth: diff --git a/homeassistant/components/system_bridge/coordinator.py b/homeassistant/components/system_bridge/coordinator.py index 1719d951cf0..695dca44342 100644 --- a/homeassistant/components/system_bridge/coordinator.py +++ b/homeassistant/components/system_bridge/coordinator.py @@ -5,6 +5,7 @@ import asyncio from collections.abc import Callable from datetime import timedelta import logging +from typing import Any import async_timeout from pydantic import BaseModel # pylint: disable=no-name-in-module @@ -17,8 +18,10 @@ from systembridgeconnector.models.battery import Battery from systembridgeconnector.models.cpu import Cpu from systembridgeconnector.models.disk import Disk from systembridgeconnector.models.display import Display +from systembridgeconnector.models.get_data import GetData from systembridgeconnector.models.gpu import Gpu from systembridgeconnector.models.memory import Memory +from systembridgeconnector.models.register_data_listener import RegisterDataListener from systembridgeconnector.models.system import System from systembridgeconnector.websocket_client import WebSocketClient @@ -93,12 +96,14 @@ class SystemBridgeDataUpdateCoordinator( if not self.websocket_client.connected: await self._setup_websocket() - self.hass.async_create_task(self.websocket_client.get_data(modules)) + self.hass.async_create_task( + self.websocket_client.get_data(GetData(modules=modules)) + ) async def async_handle_module( self, module_name: str, - module, + module: Any, ) -> None: """Handle data from the WebSocket client.""" self.logger.debug("Set new data for: %s", module_name) @@ -174,7 +179,9 @@ class SystemBridgeDataUpdateCoordinator( self.hass.async_create_task(self._listen_for_data()) - await self.websocket_client.register_data_listener(MODULES) + await self.websocket_client.register_data_listener( + RegisterDataListener(modules=MODULES) + ) self.last_update_success = True self.async_update_listeners() diff --git a/homeassistant/components/system_bridge/manifest.json b/homeassistant/components/system_bridge/manifest.json index 4fb2201e2c7..7968b588814 100644 --- a/homeassistant/components/system_bridge/manifest.json +++ b/homeassistant/components/system_bridge/manifest.json @@ -3,7 +3,7 @@ "name": "System Bridge", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/system_bridge", - "requirements": ["systembridgeconnector==3.3.2"], + "requirements": ["systembridgeconnector==3.4.4"], "codeowners": ["@timmo001"], "zeroconf": ["_system-bridge._tcp.local."], "after_dependencies": ["zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index 96be4dae1b1..bfd752843cb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2281,7 +2281,7 @@ swisshydrodata==0.1.0 synology-srm==0.2.0 # homeassistant.components.system_bridge -systembridgeconnector==3.3.2 +systembridgeconnector==3.4.4 # homeassistant.components.tailscale tailscale==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 59b922c4ef1..1838b50232b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1539,7 +1539,7 @@ sunwatcher==0.2.1 surepy==0.7.2 # homeassistant.components.system_bridge -systembridgeconnector==3.3.2 +systembridgeconnector==3.4.4 # homeassistant.components.tailscale tailscale==0.2.0 diff --git a/tests/components/system_bridge/test_config_flow.py b/tests/components/system_bridge/test_config_flow.py index 45131353550..d01ed9a3ff8 100644 --- a/tests/components/system_bridge/test_config_flow.py +++ b/tests/components/system_bridge/test_config_flow.py @@ -2,21 +2,14 @@ import asyncio from unittest.mock import patch -from systembridgeconnector.const import ( - EVENT_DATA, - EVENT_MESSAGE, - EVENT_MODULE, - EVENT_SUBTYPE, - EVENT_TYPE, - SUBTYPE_BAD_API_KEY, - TYPE_DATA_UPDATE, - TYPE_ERROR, -) +from systembridgeconnector.const import MODEL_SYSTEM, TYPE_DATA_UPDATE from systembridgeconnector.exceptions import ( AuthenticationException, ConnectionClosedException, ConnectionErrorException, ) +from systembridgeconnector.models.response import Response +from systembridgeconnector.models.system import LastUpdated, System from homeassistant import config_entries, data_entry_flow from homeassistant.components import zeroconf @@ -48,8 +41,8 @@ FIXTURE_ZEROCONF = zeroconf.ZeroconfServiceInfo( addresses=["1.1.1.1"], port=9170, hostname="test-bridge.local.", - type="_system-bridge._udp.local.", - name="System Bridge - test-bridge._system-bridge._udp.local.", + type="_system-bridge._tcp.local.", + name="System Bridge - test-bridge._system-bridge._tcp.local.", properties={ "address": "http://test-bridge:9170", "fqdn": "test-bridge", @@ -66,34 +59,70 @@ FIXTURE_ZEROCONF_BAD = zeroconf.ZeroconfServiceInfo( addresses=["1.1.1.1"], port=9170, hostname="test-bridge.local.", - type="_system-bridge._udp.local.", - name="System Bridge - test-bridge._system-bridge._udp.local.", + type="_system-bridge._tcp.local.", + name="System Bridge - test-bridge._system-bridge._tcp.local.", properties={ "something": "bad", }, ) -FIXTURE_DATA_SYSTEM = { - EVENT_TYPE: TYPE_DATA_UPDATE, - EVENT_MESSAGE: "Data changed", - EVENT_MODULE: "system", - EVENT_DATA: { - "uuid": FIXTURE_UUID, - }, -} -FIXTURE_DATA_SYSTEM_BAD = { - EVENT_TYPE: TYPE_DATA_UPDATE, - EVENT_MESSAGE: "Data changed", - EVENT_MODULE: "system", - EVENT_DATA: {}, -} +FIXTURE_SYSTEM = System( + id=FIXTURE_UUID, + boot_time=1, + fqdn="", + hostname="1.1.1.1", + ip_address_4="1.1.1.1", + mac_address=FIXTURE_MAC_ADDRESS, + platform="", + platform_version="", + uptime=1, + uuid=FIXTURE_UUID, + version="", + version_latest="", + version_newer_available=False, + last_updated=LastUpdated( + boot_time=1, + fqdn=1, + hostname=1, + ip_address_4=1, + mac_address=1, + platform=1, + platform_version=1, + uptime=1, + uuid=1, + version=1, + version_latest=1, + version_newer_available=1, + ), +) -FIXTURE_DATA_AUTH_ERROR = { - EVENT_TYPE: TYPE_ERROR, - EVENT_SUBTYPE: SUBTYPE_BAD_API_KEY, - EVENT_MESSAGE: "Invalid api-key", -} +FIXTURE_DATA_RESPONSE = Response( + id="1234", + type=TYPE_DATA_UPDATE, + subtype=None, + message="Data received", + module=MODEL_SYSTEM, + data=FIXTURE_SYSTEM, +) + +FIXTURE_DATA_RESPONSE_BAD = Response( + id="1234", + type=TYPE_DATA_UPDATE, + subtype=None, + message="Data received", + module=MODEL_SYSTEM, + data={}, +) + +FIXTURE_DATA_RESPONSE_BAD = Response( + id="1234", + type=TYPE_DATA_UPDATE, + subtype=None, + message="Data received", + module=MODEL_SYSTEM, + data={}, +) async def test_show_user_form(hass: HomeAssistant) -> None: @@ -117,9 +146,11 @@ async def test_user_flow(hass: HomeAssistant) -> None: with patch( "homeassistant.components.system_bridge.config_flow.WebSocketClient.connect" - ), patch("systembridgeconnector.websocket_client.WebSocketClient.get_data"), patch( - "systembridgeconnector.websocket_client.WebSocketClient.receive_message", - return_value=FIXTURE_DATA_SYSTEM, + ), patch( + "systembridgeconnector.websocket_client.WebSocketClient.get_data", + return_value=FIXTURE_DATA_RESPONSE, + ), patch( + "systembridgeconnector.websocket_client.WebSocketClient.listen" ), patch( "homeassistant.components.system_bridge.async_setup_entry", return_value=True, @@ -167,11 +198,13 @@ async def test_form_connection_closed_cannot_connect(hass: HomeAssistant) -> Non assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None - with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( - "systembridgeconnector.websocket_client.WebSocketClient.get_data" + with patch( + "homeassistant.components.system_bridge.config_flow.WebSocketClient.connect" ), patch( - "systembridgeconnector.websocket_client.WebSocketClient.receive_message", + "systembridgeconnector.websocket_client.WebSocketClient.get_data", side_effect=ConnectionClosedException, + ), patch( + "systembridgeconnector.websocket_client.WebSocketClient.listen", ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], FIXTURE_USER_INPUT @@ -192,11 +225,13 @@ async def test_form_timeout_cannot_connect(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None - with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( - "systembridgeconnector.websocket_client.WebSocketClient.get_data" + with patch( + "homeassistant.components.system_bridge.config_flow.WebSocketClient.connect" ), patch( - "systembridgeconnector.websocket_client.WebSocketClient.receive_message", + "systembridgeconnector.websocket_client.WebSocketClient.get_data", side_effect=asyncio.TimeoutError, + ), patch( + "systembridgeconnector.websocket_client.WebSocketClient.listen", ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], FIXTURE_USER_INPUT @@ -217,11 +252,13 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None - with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( - "systembridgeconnector.websocket_client.WebSocketClient.get_data" + with patch( + "homeassistant.components.system_bridge.config_flow.WebSocketClient.connect" ), patch( - "systembridgeconnector.websocket_client.WebSocketClient.receive_message", + "systembridgeconnector.websocket_client.WebSocketClient.get_data", side_effect=AuthenticationException, + ), patch( + "systembridgeconnector.websocket_client.WebSocketClient.listen", ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], FIXTURE_USER_INPUT @@ -242,11 +279,40 @@ async def test_form_uuid_error(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None - with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( - "systembridgeconnector.websocket_client.WebSocketClient.get_data" + with patch( + "homeassistant.components.system_bridge.config_flow.WebSocketClient.connect" ), patch( - "systembridgeconnector.websocket_client.WebSocketClient.receive_message", - return_value=FIXTURE_DATA_SYSTEM_BAD, + "systembridgeconnector.websocket_client.WebSocketClient.get_data", + side_effect=ValueError, + ), patch( + "systembridgeconnector.websocket_client.WebSocketClient.listen", + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], FIXTURE_USER_INPUT + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_value_error(hass: HomeAssistant) -> None: + """Test we handle error from bad value.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["errors"] is None + + with patch( + "homeassistant.components.system_bridge.config_flow.WebSocketClient.connect" + ), patch( + "systembridgeconnector.websocket_client.WebSocketClient.get_data", + return_value=FIXTURE_DATA_RESPONSE_BAD, + ), patch( + "systembridgeconnector.websocket_client.WebSocketClient.listen", ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], FIXTURE_USER_INPUT @@ -267,11 +333,13 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None - with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( - "systembridgeconnector.websocket_client.WebSocketClient.get_data" + with patch( + "homeassistant.components.system_bridge.config_flow.WebSocketClient.connect" ), patch( - "systembridgeconnector.websocket_client.WebSocketClient.receive_message", + "systembridgeconnector.websocket_client.WebSocketClient.get_data", side_effect=Exception, + ), patch( + "systembridgeconnector.websocket_client.WebSocketClient.listen", ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], FIXTURE_USER_INPUT @@ -292,11 +360,13 @@ async def test_reauth_authorization_error(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "authenticate" - with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( - "systembridgeconnector.websocket_client.WebSocketClient.get_data" + with patch( + "homeassistant.components.system_bridge.config_flow.WebSocketClient.connect" ), patch( - "systembridgeconnector.websocket_client.WebSocketClient.receive_message", + "systembridgeconnector.websocket_client.WebSocketClient.get_data", side_effect=AuthenticationException, + ), patch( + "systembridgeconnector.websocket_client.WebSocketClient.listen", ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], FIXTURE_AUTH_INPUT @@ -340,11 +410,13 @@ async def test_reauth_connection_closed_error(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "authenticate" - with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( - "systembridgeconnector.websocket_client.WebSocketClient.get_data" + with patch( + "homeassistant.components.system_bridge.config_flow.WebSocketClient.connect" ), patch( - "systembridgeconnector.websocket_client.WebSocketClient.receive_message", + "systembridgeconnector.websocket_client.WebSocketClient.get_data", side_effect=ConnectionClosedException, + ), patch( + "systembridgeconnector.websocket_client.WebSocketClient.listen", ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], FIXTURE_AUTH_INPUT @@ -370,11 +442,13 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "authenticate" - with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( - "systembridgeconnector.websocket_client.WebSocketClient.get_data" + with patch( + "homeassistant.components.system_bridge.config_flow.WebSocketClient.connect" ), patch( - "systembridgeconnector.websocket_client.WebSocketClient.receive_message", - return_value=FIXTURE_DATA_SYSTEM, + "systembridgeconnector.websocket_client.WebSocketClient.get_data", + return_value=FIXTURE_DATA_RESPONSE, + ), patch( + "systembridgeconnector.websocket_client.WebSocketClient.listen" ), patch( "homeassistant.components.system_bridge.async_setup_entry", return_value=True, @@ -402,11 +476,13 @@ async def test_zeroconf_flow(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert not result["errors"] - with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( - "systembridgeconnector.websocket_client.WebSocketClient.get_data" + with patch( + "homeassistant.components.system_bridge.config_flow.WebSocketClient.connect" ), patch( - "systembridgeconnector.websocket_client.WebSocketClient.receive_message", - return_value=FIXTURE_DATA_SYSTEM, + "systembridgeconnector.websocket_client.WebSocketClient.get_data", + return_value=FIXTURE_DATA_RESPONSE, + ), patch( + "systembridgeconnector.websocket_client.WebSocketClient.listen" ), patch( "homeassistant.components.system_bridge.async_setup_entry", return_value=True, From 972aad0e99d61189ce440a98a9fc551c54a119bd Mon Sep 17 00:00:00 2001 From: hansgoed Date: Mon, 15 Aug 2022 15:07:39 +0200 Subject: [PATCH 55/81] =?UTF-8?q?=F0=9F=90=9B=20Fix=20"The=20request=20con?= =?UTF-8?q?tent=20was=20malformed"=20error=20in=20home=5Fconnect=20(#76411?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/home_connect/__init__.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/home_connect/__init__.py b/homeassistant/components/home_connect/__init__.py index 6e664ad07e4..0fa14682f44 100644 --- a/homeassistant/components/home_connect/__init__.py +++ b/homeassistant/components/home_connect/__init__.py @@ -138,11 +138,18 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Execute calls to services taking a program.""" program = call.data[ATTR_PROGRAM] device_id = call.data[ATTR_DEVICE_ID] - options = { - ATTR_KEY: call.data.get(ATTR_KEY), - ATTR_VALUE: call.data.get(ATTR_VALUE), - ATTR_UNIT: call.data.get(ATTR_UNIT), - } + + options = [] + + option_key = call.data.get(ATTR_KEY) + if option_key is not None: + option = {ATTR_KEY: option_key, ATTR_VALUE: call.data[ATTR_VALUE]} + + option_unit = call.data.get(ATTR_UNIT) + if option_unit is not None: + option[ATTR_UNIT] = option_unit + + options.append(option) appliance = _get_appliance_by_device_id(hass, device_id) await hass.async_add_executor_job(getattr(appliance, method), program, options) From 666e938b14e3ee2856d048fb3587b015293a0890 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 11 Aug 2022 16:03:31 -1000 Subject: [PATCH 56/81] Bump pySwitchbot to 0.18.5 (#76640) --- homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index b413b44d605..dd04829de15 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,7 +2,7 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.18.4"], + "requirements": ["PySwitchbot==0.18.5"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index bfd752843cb..4bd6175bdd5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -37,7 +37,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.18.4 +PySwitchbot==0.18.5 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1838b50232b..7162289ec9f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -33,7 +33,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.18.4 +PySwitchbot==0.18.5 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 875de80b41f062ed435e41b9eefee2bc2f93eab1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 12 Aug 2022 02:58:48 -1000 Subject: [PATCH 57/81] Bump pySwitchbot to 0.18.6 to fix disconnect race (#76656) --- homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index dd04829de15..e26100108c9 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,7 +2,7 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.18.5"], + "requirements": ["PySwitchbot==0.18.6"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 4bd6175bdd5..8a48eeb9c34 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -37,7 +37,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.18.5 +PySwitchbot==0.18.6 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7162289ec9f..9c521c7828f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -33,7 +33,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.18.5 +PySwitchbot==0.18.6 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From d10e2336e2ab35e2ff29399c7279d1e33c0f85e7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 14 Aug 2022 19:53:27 -1000 Subject: [PATCH 58/81] Bump pySwitchbot to 0.18.10 to handle empty data and disconnects (#76684) * Bump pySwitchbot to 0.18.7 to handle empty data Fixes #76621 * bump again * bump * bump for rssi on disconnect logging --- homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index e26100108c9..e70f467ae74 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,7 +2,7 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.18.6"], + "requirements": ["PySwitchbot==0.18.10"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 8a48eeb9c34..58e06048748 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -37,7 +37,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.18.6 +PySwitchbot==0.18.10 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9c521c7828f..bfaaa4e86bf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -33,7 +33,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.18.6 +PySwitchbot==0.18.10 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From bd40d6f3321dc06ca201e4b85c719bd7bcb7b6d9 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 13 Aug 2022 06:02:32 -0700 Subject: [PATCH 59/81] Fix google calendar disabled entity handling (#76699) Fix google calendar disable entity handling --- homeassistant/components/google/calendar.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index ca98b3da087..77ed922e511 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -192,7 +192,6 @@ async def async_setup_entry( calendar_id, data.get(CONF_SEARCH), ) - await coordinator.async_config_entry_first_refresh() entities.append( GoogleCalendarEntity( coordinator, @@ -342,6 +341,9 @@ class GoogleCalendarEntity(CoordinatorEntity, CalendarEntity): async def async_added_to_hass(self) -> None: """When entity is added to hass.""" await super().async_added_to_hass() + # We do not ask for an update with async_add_entities() + # because it will update disabled entities + await self.coordinator.async_request_refresh() self._apply_coordinator_update() async def async_get_events( From ef6285800f03735e4cebc7cb1cd1eb065c9facb7 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 13 Aug 2022 14:39:04 +0200 Subject: [PATCH 60/81] Motion Blinds fix OperationNotAllowed (#76712) fix OperationNotAllowed homeassistant.config_entries.OperationNotAllowed --- homeassistant/components/motion_blinds/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/motion_blinds/__init__.py b/homeassistant/components/motion_blinds/__init__.py index dfbc6ab74a7..a023fc05d14 100644 --- a/homeassistant/components/motion_blinds/__init__.py +++ b/homeassistant/components/motion_blinds/__init__.py @@ -121,8 +121,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: multicast_interface = entry.data.get(CONF_INTERFACE, DEFAULT_INTERFACE) wait_for_push = entry.options.get(CONF_WAIT_FOR_PUSH, DEFAULT_WAIT_FOR_PUSH) - entry.async_on_unload(entry.add_update_listener(update_listener)) - # Create multicast Listener async with setup_lock: if KEY_MULTICAST_LISTENER not in hass.data[DOMAIN]: @@ -213,6 +211,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload(entry.add_update_listener(update_listener)) + return True From 41faf9092a394c70bee416c8c1c1c85cad1660e1 Mon Sep 17 00:00:00 2001 From: Marvin Wichmann Date: Sun, 14 Aug 2022 06:31:39 +0200 Subject: [PATCH 61/81] =?UTF-8?q?Update=20xknx=20to=201.0.0=20=F0=9F=8E=89?= =?UTF-8?q?=20(#76734)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 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 266eceaacee..0f9b6b4b95a 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -3,7 +3,7 @@ "name": "KNX", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==0.22.1"], + "requirements": ["xknx==1.0.0"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "platinum", "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 58e06048748..afbba9d6531 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2473,7 +2473,7 @@ xboxapi==2.0.1 xiaomi-ble==0.6.4 # homeassistant.components.knx -xknx==0.22.1 +xknx==1.0.0 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bfaaa4e86bf..864c85d0e47 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1665,7 +1665,7 @@ xbox-webapi==2.0.11 xiaomi-ble==0.6.4 # homeassistant.components.knx -xknx==0.22.1 +xknx==1.0.0 # homeassistant.components.bluesound # homeassistant.components.fritz From 69ea07f29d1cae6b601bfd2638390e5ce665e6a1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 13 Aug 2022 21:56:57 -1000 Subject: [PATCH 62/81] Bump aiohomekit to 1.2.10 (#76738) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 4bd9a0b70f9..49635dd797c 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.9"], + "requirements": ["aiohomekit==1.2.10"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index afbba9d6531..f708004d979 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.9 +aiohomekit==1.2.10 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 864c85d0e47..684e8b44b49 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.9 +aiohomekit==1.2.10 # homeassistant.components.emulated_hue # homeassistant.components.http From 71a6128c65e634857260da30e97b9d6ed7bc8f54 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 14 Aug 2022 20:48:06 -1000 Subject: [PATCH 63/81] Fix bad data with inkbird bbq sensors (#76739) --- homeassistant/components/inkbird/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/inkbird/test_config_flow.py | 6 +++--- tests/components/inkbird/test_sensor.py | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/inkbird/manifest.json b/homeassistant/components/inkbird/manifest.json index b0ef08143c2..f65177ab6e2 100644 --- a/homeassistant/components/inkbird/manifest.json +++ b/homeassistant/components/inkbird/manifest.json @@ -10,7 +10,7 @@ { "local_name": "xBBQ*" }, { "local_name": "tps" } ], - "requirements": ["inkbird-ble==0.5.2"], + "requirements": ["inkbird-ble==0.5.5"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index f708004d979..0b29516b23e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -902,7 +902,7 @@ influxdb-client==1.24.0 influxdb==5.3.1 # homeassistant.components.inkbird -inkbird-ble==0.5.2 +inkbird-ble==0.5.5 # homeassistant.components.insteon insteon-frontend-home-assistant==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 684e8b44b49..81c79718e63 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -655,7 +655,7 @@ influxdb-client==1.24.0 influxdb==5.3.1 # homeassistant.components.inkbird -inkbird-ble==0.5.2 +inkbird-ble==0.5.5 # homeassistant.components.insteon insteon-frontend-home-assistant==0.2.0 diff --git a/tests/components/inkbird/test_config_flow.py b/tests/components/inkbird/test_config_flow.py index fe210f75f4b..4d9fbc65df7 100644 --- a/tests/components/inkbird/test_config_flow.py +++ b/tests/components/inkbird/test_config_flow.py @@ -25,7 +25,7 @@ async def test_async_step_bluetooth_valid_device(hass): result["flow_id"], user_input={} ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "iBBQ 6AADDD4CAC3D" + assert result2["title"] == "iBBQ AC3D" assert result2["data"] == {} assert result2["result"].unique_id == "4125DDBA-2774-4851-9889-6AADDD4CAC3D" @@ -69,7 +69,7 @@ async def test_async_step_user_with_found_devices(hass): user_input={"address": "61DE521B-F0BF-9F44-64D4-75BBE1738105"}, ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "IBS-TH 75BBE1738105" + assert result2["title"] == "IBS-TH 8105" assert result2["data"] == {} assert result2["result"].unique_id == "61DE521B-F0BF-9F44-64D4-75BBE1738105" @@ -184,7 +184,7 @@ async def test_async_step_user_takes_precedence_over_discovery(hass): user_input={"address": "61DE521B-F0BF-9F44-64D4-75BBE1738105"}, ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "IBS-TH 75BBE1738105" + assert result2["title"] == "IBS-TH 8105" assert result2["data"] == {} assert result2["result"].unique_id == "61DE521B-F0BF-9F44-64D4-75BBE1738105" diff --git a/tests/components/inkbird/test_sensor.py b/tests/components/inkbird/test_sensor.py index cafc22911c3..c54c6e3c242 100644 --- a/tests/components/inkbird/test_sensor.py +++ b/tests/components/inkbird/test_sensor.py @@ -39,10 +39,10 @@ async def test_sensors(hass): await hass.async_block_till_done() assert len(hass.states.async_all()) == 3 - temp_sensor = hass.states.get("sensor.ibs_th_75bbe1738105_battery") + temp_sensor = hass.states.get("sensor.ibs_th_8105_battery") temp_sensor_attribtes = temp_sensor.attributes assert temp_sensor.state == "87" - assert temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "IBS-TH 75BBE1738105 Battery" + assert temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "IBS-TH 8105 Battery" assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "%" assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" From ae99b53757e549979c5e9d3853217f192f0dd460 Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Mon, 15 Aug 2022 13:55:23 +0200 Subject: [PATCH 64/81] Bump bimmer_connected to 0.10.2 (#76751) Co-authored-by: rikroe --- homeassistant/components/bmw_connected_drive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index 0381035a63e..f540176a837 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -2,7 +2,7 @@ "domain": "bmw_connected_drive", "name": "BMW Connected Drive", "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", - "requirements": ["bimmer_connected==0.10.1"], + "requirements": ["bimmer_connected==0.10.2"], "codeowners": ["@gerard33", "@rikroe"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 0b29516b23e..95f746a761e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -399,7 +399,7 @@ beautifulsoup4==4.11.1 bellows==0.32.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.10.1 +bimmer_connected==0.10.2 # homeassistant.components.bizkaibus bizkaibus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 81c79718e63..2beb50a7b48 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -323,7 +323,7 @@ beautifulsoup4==4.11.1 bellows==0.32.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.10.1 +bimmer_connected==0.10.2 # homeassistant.components.bluetooth bleak==0.15.1 From 30a5e396d3b1b2c713b0062840a980d8eb71c39b Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Mon, 15 Aug 2022 11:23:38 +0200 Subject: [PATCH 65/81] Bump aiohue to 4.5.0 (#76757) --- homeassistant/components/hue/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index b3dbe4df50a..3854b861c98 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -3,7 +3,7 @@ "name": "Philips Hue", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hue", - "requirements": ["aiohue==4.4.2"], + "requirements": ["aiohue==4.5.0"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index 95f746a761e..109562ad0fd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -175,7 +175,7 @@ aiohomekit==1.2.10 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==4.4.2 +aiohue==4.5.0 # homeassistant.components.imap aioimaplib==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2beb50a7b48..15cb74f0bbe 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -159,7 +159,7 @@ aiohomekit==1.2.10 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==4.4.2 +aiohue==4.5.0 # homeassistant.components.apache_kafka aiokafka==0.7.2 From 70541eac616dc1f162885a5c9323dded38574b63 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 14 Aug 2022 20:48:28 -1000 Subject: [PATCH 66/81] Fix stale data with SensorPush sensors (#76771) --- homeassistant/components/sensorpush/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensorpush/manifest.json b/homeassistant/components/sensorpush/manifest.json index a5d900aaf3b..906b5c22f6b 100644 --- a/homeassistant/components/sensorpush/manifest.json +++ b/homeassistant/components/sensorpush/manifest.json @@ -8,7 +8,7 @@ "local_name": "SensorPush*" } ], - "requirements": ["sensorpush-ble==1.5.1"], + "requirements": ["sensorpush-ble==1.5.2"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 109562ad0fd..863460cc9e6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2160,7 +2160,7 @@ sendgrid==6.8.2 sense_energy==0.10.4 # homeassistant.components.sensorpush -sensorpush-ble==1.5.1 +sensorpush-ble==1.5.2 # homeassistant.components.sentry sentry-sdk==1.8.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 15cb74f0bbe..4261d763b1e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1451,7 +1451,7 @@ securetar==2022.2.0 sense_energy==0.10.4 # homeassistant.components.sensorpush -sensorpush-ble==1.5.1 +sensorpush-ble==1.5.2 # homeassistant.components.sentry sentry-sdk==1.8.0 From d91e9f7f988838f725acf6b81f3951128ddfa15d Mon Sep 17 00:00:00 2001 From: Frank <46161394+BraveChicken1@users.noreply.github.com> Date: Mon, 15 Aug 2022 10:44:14 +0200 Subject: [PATCH 67/81] Bump homeconnect to 0.7.2 (#76773) --- homeassistant/components/home_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/home_connect/manifest.json b/homeassistant/components/home_connect/manifest.json index ca6e0f012ac..c9aa5d229b8 100644 --- a/homeassistant/components/home_connect/manifest.json +++ b/homeassistant/components/home_connect/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/home_connect", "dependencies": ["application_credentials"], "codeowners": ["@DavidMStraub"], - "requirements": ["homeconnect==0.7.1"], + "requirements": ["homeconnect==0.7.2"], "config_flow": true, "iot_class": "cloud_push", "loggers": ["homeconnect"] diff --git a/requirements_all.txt b/requirements_all.txt index 863460cc9e6..fe4ecf42602 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -842,7 +842,7 @@ holidays==0.14.2 home-assistant-frontend==20220802.0 # homeassistant.components.home_connect -homeconnect==0.7.1 +homeconnect==0.7.2 # homeassistant.components.homematicip_cloud homematicip==1.0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4261d763b1e..484eb31144d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -619,7 +619,7 @@ holidays==0.14.2 home-assistant-frontend==20220802.0 # homeassistant.components.home_connect -homeconnect==0.7.1 +homeconnect==0.7.2 # homeassistant.components.homematicip_cloud homematicip==1.0.7 From d14261829722c504001df135a148f8ee6d620422 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 14 Aug 2022 20:54:49 -1000 Subject: [PATCH 68/81] Bump aiohomekit to 1.2.11 (#76784) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 49635dd797c..ece53d29406 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.10"], + "requirements": ["aiohomekit==1.2.11"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index fe4ecf42602..300ac05de84 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.10 +aiohomekit==1.2.11 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 484eb31144d..a069c31b87b 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.10 +aiohomekit==1.2.11 # homeassistant.components.emulated_hue # homeassistant.components.http From bae01f188ac4d387933df6a29b8d9f0e87b021c3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 15 Aug 2022 10:19:37 -1000 Subject: [PATCH 69/81] Fix bluetooth callback registration not surviving a reload (#76817) --- .../components/bluetooth/__init__.py | 21 ++-- tests/components/bluetooth/test_init.py | 97 +++++++++++++++++++ 2 files changed, 110 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 2d7b47b7552..f9c6a6aca15 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -358,13 +358,13 @@ class BluetoothManager: async with async_timeout.timeout(START_TIMEOUT): await self.scanner.start() # type: ignore[no-untyped-call] except InvalidMessageError as ex: - self._cancel_device_detected() + self._async_cancel_scanner_callback() _LOGGER.debug("Invalid DBus message received: %s", ex, exc_info=True) raise ConfigEntryNotReady( f"Invalid DBus message received: {ex}; try restarting `dbus`" ) from ex except BrokenPipeError as ex: - self._cancel_device_detected() + self._async_cancel_scanner_callback() _LOGGER.debug("DBus connection broken: %s", ex, exc_info=True) if is_docker_env(): raise ConfigEntryNotReady( @@ -374,7 +374,7 @@ class BluetoothManager: f"DBus connection broken: {ex}; try restarting `bluetooth` and `dbus`" ) from ex except FileNotFoundError as ex: - self._cancel_device_detected() + self._async_cancel_scanner_callback() _LOGGER.debug( "FileNotFoundError while starting bluetooth: %s", ex, exc_info=True ) @@ -386,12 +386,12 @@ class BluetoothManager: f"DBus service not found; make sure the DBus socket is available to Home Assistant: {ex}" ) from ex except asyncio.TimeoutError as ex: - self._cancel_device_detected() + self._async_cancel_scanner_callback() raise ConfigEntryNotReady( f"Timed out starting Bluetooth after {START_TIMEOUT} seconds" ) from ex except BleakError as ex: - self._cancel_device_detected() + self._async_cancel_scanner_callback() _LOGGER.debug("BleakError while starting bluetooth: %s", ex, exc_info=True) raise ConfigEntryNotReady(f"Failed to start Bluetooth: {ex}") from ex self.async_setup_unavailable_tracking() @@ -573,15 +573,20 @@ class BluetoothManager: self._cancel_stop = None await self.async_stop() + @hass_callback + def _async_cancel_scanner_callback(self) -> None: + """Cancel the scanner callback.""" + if self._cancel_device_detected: + self._cancel_device_detected() + self._cancel_device_detected = None + async def async_stop(self) -> None: """Stop bluetooth discovery.""" _LOGGER.debug("Stopping bluetooth discovery") if self._cancel_watchdog: self._cancel_watchdog() self._cancel_watchdog = None - if self._cancel_device_detected: - self._cancel_device_detected() - self._cancel_device_detected = None + self._async_cancel_scanner_callback() if self._cancel_unavailable_tracking: self._cancel_unavailable_tracking() self._cancel_unavailable_tracking = None diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 9432be9f2e4..e69d7ba96e1 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -164,6 +164,43 @@ async def test_setup_and_retry_adapter_not_yet_available(hass, caplog): await hass.async_block_till_done() +async def test_no_race_during_manual_reload_in_retry_state(hass, caplog): + """Test we can successfully reload when the entry is in a retry state.""" + mock_bt = [] + with patch("homeassistant.components.bluetooth.HaBleakScanner.async_setup"), patch( + "homeassistant.components.bluetooth.HaBleakScanner.start", + side_effect=BleakError, + ), patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + entry = hass.config_entries.async_entries(bluetooth.DOMAIN)[0] + + assert "Failed to start Bluetooth" in caplog.text + assert len(bluetooth.async_discovered_service_info(hass)) == 0 + assert entry.state == ConfigEntryState.SETUP_RETRY + + with patch( + "homeassistant.components.bluetooth.HaBleakScanner.start", + ): + await hass.config_entries.async_reload(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == ConfigEntryState.LOADED + + with patch( + "homeassistant.components.bluetooth.HaBleakScanner.stop", + ): + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + + async def test_calling_async_discovered_devices_no_bluetooth(hass, caplog): """Test we fail gracefully when asking for discovered devices and there is no blueooth.""" mock_bt = [] @@ -828,6 +865,66 @@ async def test_register_callback_by_address( assert service_info.manufacturer_id == 89 +async def test_register_callback_survives_reload( + hass, mock_bleak_scanner_start, enable_bluetooth +): + """Test registering a callback by address survives bluetooth being reloaded.""" + mock_bt = [] + callbacks = [] + + def _fake_subscriber( + service_info: BluetoothServiceInfo, change: BluetoothChange + ) -> None: + """Fake subscriber for the BleakScanner.""" + callbacks.append((service_info, change)) + + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + bluetooth.async_register_callback( + hass, + _fake_subscriber, + {"address": "44:44:33:11:23:45"}, + BluetoothScanningMode.ACTIVE, + ) + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") + switchbot_adv = AdvertisementData( + local_name="wohand", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, + ) + + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + assert len(callbacks) == 1 + service_info: BluetoothServiceInfo = callbacks[0][0] + assert service_info.name == "wohand" + assert service_info.manufacturer == "Nordic Semiconductor ASA" + assert service_info.manufacturer_id == 89 + + entry = hass.config_entries.async_entries(bluetooth.DOMAIN)[0] + await hass.config_entries.async_reload(entry.entry_id) + await hass.async_block_till_done() + + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + assert len(callbacks) == 2 + service_info: BluetoothServiceInfo = callbacks[1][0] + assert service_info.name == "wohand" + assert service_info.manufacturer == "Nordic Semiconductor ASA" + assert service_info.manufacturer_id == 89 + + async def test_process_advertisements_bail_on_good_advertisement( hass: HomeAssistant, mock_bleak_scanner_start, enable_bluetooth ): From bf88448ffda1a361be7d444c8c56c1d7b95ca7d6 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 15 Aug 2022 23:35:30 +0200 Subject: [PATCH 70/81] Correct referenced entities and devices for event triggers (#76818) --- .../components/automation/__init__.py | 8 +++-- tests/components/automation/test_init.py | 30 +++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index c743e1f83fd..1914a837ba7 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -36,6 +36,7 @@ from homeassistant.core import ( HomeAssistant, callback, split_entity_id, + valid_entity_id, ) from homeassistant.exceptions import ( ConditionError, @@ -361,7 +362,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): referenced |= condition.async_extract_devices(conf) for conf in self._trigger_config: - referenced |= set(_trigger_extract_device(conf)) + referenced |= set(_trigger_extract_devices(conf)) self._referenced_devices = referenced return referenced @@ -763,7 +764,7 @@ async def _async_process_if(hass, name, config, p_config): @callback -def _trigger_extract_device(trigger_conf: dict) -> list[str]: +def _trigger_extract_devices(trigger_conf: dict) -> list[str]: """Extract devices from a trigger config.""" if trigger_conf[CONF_PLATFORM] == "device": return [trigger_conf[CONF_DEVICE_ID]] @@ -772,6 +773,7 @@ def _trigger_extract_device(trigger_conf: dict) -> list[str]: trigger_conf[CONF_PLATFORM] == "event" and CONF_EVENT_DATA in trigger_conf and CONF_DEVICE_ID in trigger_conf[CONF_EVENT_DATA] + and isinstance(trigger_conf[CONF_EVENT_DATA][CONF_DEVICE_ID], str) ): return [trigger_conf[CONF_EVENT_DATA][CONF_DEVICE_ID]] @@ -803,6 +805,8 @@ def _trigger_extract_entities(trigger_conf: dict) -> list[str]: trigger_conf[CONF_PLATFORM] == "event" and CONF_EVENT_DATA in trigger_conf and CONF_ENTITY_ID in trigger_conf[CONF_EVENT_DATA] + and isinstance(trigger_conf[CONF_EVENT_DATA][CONF_ENTITY_ID], str) + and valid_entity_id(trigger_conf[CONF_EVENT_DATA][CONF_ENTITY_ID]) ): return [trigger_conf[CONF_EVENT_DATA][CONF_ENTITY_ID]] diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index bcbcf382892..cef553653de 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -1103,6 +1103,24 @@ async def test_extraction_functions(hass): "event_type": "state_changed", "event_data": {"entity_id": "sensor.trigger_event"}, }, + # entity_id is a list of strings (not supported) + { + "platform": "event", + "event_type": "state_changed", + "event_data": {"entity_id": ["sensor.trigger_event2"]}, + }, + # entity_id is not a valid entity ID + { + "platform": "event", + "event_type": "state_changed", + "event_data": {"entity_id": "abc"}, + }, + # entity_id is not a string + { + "platform": "event", + "event_type": "state_changed", + "event_data": {"entity_id": 123}, + }, ], "condition": { "condition": "state", @@ -1151,6 +1169,18 @@ async def test_extraction_functions(hass): "event_type": "esphome.button_pressed", "event_data": {"device_id": "device-trigger-event"}, }, + # device_id is a list of strings (not supported) + { + "platform": "event", + "event_type": "esphome.button_pressed", + "event_data": {"device_id": ["device-trigger-event"]}, + }, + # device_id is not a string + { + "platform": "event", + "event_type": "esphome.button_pressed", + "event_data": {"device_id": 123}, + }, ], "condition": { "condition": "device", From f4a09455c0b9291f5c214755a52c1b7b0d4bcf1c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 15 Aug 2022 15:09:13 -1000 Subject: [PATCH 71/81] Fix lifx homekit discoveries not being ignorable or updating the IP (#76825) --- homeassistant/components/lifx/config_flow.py | 10 +++--- tests/components/lifx/test_config_flow.py | 33 +++++++++++++++----- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/lifx/config_flow.py b/homeassistant/components/lifx/config_flow.py index 30b42e640f8..deff53e06c6 100644 --- a/homeassistant/components/lifx/config_flow.py +++ b/homeassistant/components/lifx/config_flow.py @@ -119,18 +119,20 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Confirm discovery.""" assert self._discovered_device is not None + discovered = self._discovered_device _LOGGER.debug( "Confirming discovery: %s with serial %s", - self._discovered_device.label, + discovered.label, self.unique_id, ) if user_input is not None or self._async_discovered_pending_migration(): - return self._async_create_entry_from_device(self._discovered_device) + return self._async_create_entry_from_device(discovered) + self._abort_if_unique_id_configured(updates={CONF_HOST: discovered.ip_addr}) self._set_confirm_only() placeholders = { - "label": self._discovered_device.label, - "host": self._discovered_device.ip_addr, + "label": discovered.label, + "host": discovered.ip_addr, "serial": self.unique_id, } self.context["title_placeholders"] = placeholders diff --git a/tests/components/lifx/test_config_flow.py b/tests/components/lifx/test_config_flow.py index f007e9ee0e8..05bf29f43e5 100644 --- a/tests/components/lifx/test_config_flow.py +++ b/tests/components/lifx/test_config_flow.py @@ -466,21 +466,38 @@ async def test_discovered_by_dhcp_or_discovery_failed_to_get_device(hass, source assert result["reason"] == "cannot_connect" -async def test_discovered_by_dhcp_updates_ip(hass): +@pytest.mark.parametrize( + "source, data", + [ + ( + config_entries.SOURCE_DHCP, + dhcp.DhcpServiceInfo(ip=IP_ADDRESS, macaddress=MAC_ADDRESS, hostname=LABEL), + ), + ( + config_entries.SOURCE_HOMEKIT, + zeroconf.ZeroconfServiceInfo( + host=IP_ADDRESS, + addresses=[IP_ADDRESS], + hostname=LABEL, + name=LABEL, + port=None, + properties={zeroconf.ATTR_PROPERTIES_ID: "any"}, + type="mock_type", + ), + ), + ], +) +async def test_discovered_by_dhcp_or_homekit_updates_ip(hass, source, data): """Update host from dhcp.""" config_entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: "127.0.0.2"}, unique_id=SERIAL ) config_entry.add_to_hass(hass) - with _patch_discovery(no_device=True), _patch_config_flow_try_connect( - no_device=True - ): + with _patch_discovery(), _patch_config_flow_try_connect(): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( - ip=IP_ADDRESS, macaddress=MAC_ADDRESS, hostname=LABEL - ), + context={"source": source}, + data=data, ) await hass.async_block_till_done() assert result["type"] == RESULT_TYPE_ABORT From ec4ff824ada9a4a376449033757583a43c83c2ee Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 15 Aug 2022 21:09:58 -0400 Subject: [PATCH 72/81] Bumped version to 2022.8.5 --- 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 e79c132519d..329c0e483dd 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 = "4" +PATCH_VERSION: Final = "5" __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 6c6b88feb4a..b42b0b91b58 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.8.4" +version = "2022.8.5" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 1f13e332acbdcf921f9b7dc4f18934a5f473d039 Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Tue, 16 Aug 2022 21:36:33 +0200 Subject: [PATCH 73/81] Fix displayed units for BMW Connected Drive (#76613) * Fix displayed units * Add tests for unit conversion * Streamline test config entry init * Refactor test to pytest fixture * Fix renamed mock Co-authored-by: rikroe --- .../components/bmw_connected_drive/sensor.py | 40 ++-- .../bmw_connected_drive/__init__.py | 86 ++++++++ .../bmw_connected_drive/conftest.py | 12 + .../I01/state_WBY00000000REXI01_0.json | 206 ++++++++++++++++++ .../vehicles/I01/vehicles_v2_bmw_0.json | 47 ++++ .../bmw_connected_drive/test_config_flow.py | 8 +- .../bmw_connected_drive/test_sensor.py | 52 +++++ 7 files changed, 420 insertions(+), 31 deletions(-) create mode 100644 tests/components/bmw_connected_drive/conftest.py create mode 100644 tests/components/bmw_connected_drive/fixtures/vehicles/I01/state_WBY00000000REXI01_0.json create mode 100644 tests/components/bmw_connected_drive/fixtures/vehicles/I01/vehicles_v2_bmw_0.json create mode 100644 tests/components/bmw_connected_drive/test_sensor.py diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index f1046881ed3..c46a0133eb6 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -15,13 +15,7 @@ from homeassistant.components.sensor import ( SensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - LENGTH_KILOMETERS, - LENGTH_MILES, - PERCENTAGE, - VOLUME_GALLONS, - VOLUME_LITERS, -) +from homeassistant.const import LENGTH, PERCENTAGE, VOLUME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType @@ -39,8 +33,7 @@ class BMWSensorEntityDescription(SensorEntityDescription): """Describes BMW sensor entity.""" key_class: str | None = None - unit_metric: str | None = None - unit_imperial: str | None = None + unit_type: str | None = None value: Callable = lambda x, y: x @@ -86,56 +79,49 @@ SENSOR_TYPES: dict[str, BMWSensorEntityDescription] = { "remaining_battery_percent": BMWSensorEntityDescription( key="remaining_battery_percent", key_class="fuel_and_battery", - unit_metric=PERCENTAGE, - unit_imperial=PERCENTAGE, + unit_type=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, ), # --- Specific --- "mileage": BMWSensorEntityDescription( key="mileage", icon="mdi:speedometer", - unit_metric=LENGTH_KILOMETERS, - unit_imperial=LENGTH_MILES, + unit_type=LENGTH, value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2), ), "remaining_range_total": BMWSensorEntityDescription( key="remaining_range_total", key_class="fuel_and_battery", icon="mdi:map-marker-distance", - unit_metric=LENGTH_KILOMETERS, - unit_imperial=LENGTH_MILES, + unit_type=LENGTH, value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2), ), "remaining_range_electric": BMWSensorEntityDescription( key="remaining_range_electric", key_class="fuel_and_battery", icon="mdi:map-marker-distance", - unit_metric=LENGTH_KILOMETERS, - unit_imperial=LENGTH_MILES, + unit_type=LENGTH, value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2), ), "remaining_range_fuel": BMWSensorEntityDescription( key="remaining_range_fuel", key_class="fuel_and_battery", icon="mdi:map-marker-distance", - unit_metric=LENGTH_KILOMETERS, - unit_imperial=LENGTH_MILES, + unit_type=LENGTH, value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2), ), "remaining_fuel": BMWSensorEntityDescription( key="remaining_fuel", key_class="fuel_and_battery", icon="mdi:gas-station", - unit_metric=VOLUME_LITERS, - unit_imperial=VOLUME_GALLONS, + unit_type=VOLUME, value=lambda x, hass: convert_and_round(x, hass.config.units.volume, 2), ), "remaining_fuel_percent": BMWSensorEntityDescription( key="remaining_fuel_percent", key_class="fuel_and_battery", icon="mdi:gas-station", - unit_metric=PERCENTAGE, - unit_imperial=PERCENTAGE, + unit_type=PERCENTAGE, ), } @@ -182,8 +168,12 @@ class BMWSensor(BMWBaseEntity, SensorEntity): self._attr_name = f"{vehicle.name} {description.key}" self._attr_unique_id = f"{vehicle.vin}-{description.key}" - # Force metric system as BMW API apparently only returns metric values now - self._attr_native_unit_of_measurement = description.unit_metric + # Set the correct unit of measurement based on the unit_type + if description.unit_type: + self._attr_native_unit_of_measurement = ( + coordinator.hass.config.units.as_dict().get(description.unit_type) + or description.unit_type + ) @callback def _handle_coordinator_update(self) -> None: diff --git a/tests/components/bmw_connected_drive/__init__.py b/tests/components/bmw_connected_drive/__init__.py index 4774032b409..c2bb65b3fa7 100644 --- a/tests/components/bmw_connected_drive/__init__.py +++ b/tests/components/bmw_connected_drive/__init__.py @@ -1,17 +1,29 @@ """Tests for the for the BMW Connected Drive integration.""" +import json +from pathlib import Path + +from bimmer_connected.account import MyBMWAccount +from bimmer_connected.api.utils import log_to_to_file + from homeassistant import config_entries from homeassistant.components.bmw_connected_drive.const import ( CONF_READ_ONLY, + CONF_REFRESH_TOKEN, DOMAIN as BMW_DOMAIN, ) from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.util.dt import utcnow + +from tests.common import MockConfigEntry, get_fixture_path, load_fixture FIXTURE_USER_INPUT = { CONF_USERNAME: "user@domain.com", CONF_PASSWORD: "p4ssw0rd", CONF_REGION: "rest_of_world", } +FIXTURE_REFRESH_TOKEN = "SOME_REFRESH_TOKEN" FIXTURE_CONFIG_ENTRY = { "entry_id": "1", @@ -21,8 +33,82 @@ FIXTURE_CONFIG_ENTRY = { CONF_USERNAME: FIXTURE_USER_INPUT[CONF_USERNAME], CONF_PASSWORD: FIXTURE_USER_INPUT[CONF_PASSWORD], CONF_REGION: FIXTURE_USER_INPUT[CONF_REGION], + CONF_REFRESH_TOKEN: FIXTURE_REFRESH_TOKEN, }, "options": {CONF_READ_ONLY: False}, "source": config_entries.SOURCE_USER, "unique_id": f"{FIXTURE_USER_INPUT[CONF_REGION]}-{FIXTURE_USER_INPUT[CONF_REGION]}", } + + +async def mock_vehicles_from_fixture(account: MyBMWAccount) -> None: + """Load MyBMWVehicle from fixtures and add them to the account.""" + + fixture_path = Path(get_fixture_path("", integration=BMW_DOMAIN)) + + fixture_vehicles_bmw = list(fixture_path.rglob("vehicles_v2_bmw_*.json")) + fixture_vehicles_mini = list(fixture_path.rglob("vehicles_v2_mini_*.json")) + + # Load vehicle base lists as provided by vehicles/v2 API + vehicles = { + "bmw": [ + vehicle + for bmw_file in fixture_vehicles_bmw + for vehicle in json.loads(load_fixture(bmw_file, integration=BMW_DOMAIN)) + ], + "mini": [ + vehicle + for mini_file in fixture_vehicles_mini + for vehicle in json.loads(load_fixture(mini_file, integration=BMW_DOMAIN)) + ], + } + fetched_at = utcnow() + + # simulate storing fingerprints + if account.config.log_response_path: + for brand in ["bmw", "mini"]: + log_to_to_file( + json.dumps(vehicles[brand]), + account.config.log_response_path, + f"vehicles_v2_{brand}", + ) + + # Create a vehicle with base + specific state as provided by state/VIN API + for vehicle_base in [vehicle for brand in vehicles.values() for vehicle in brand]: + vehicle_state_path = ( + Path("vehicles") + / vehicle_base["attributes"]["bodyType"] + / f"state_{vehicle_base['vin']}_0.json" + ) + vehicle_state = json.loads( + load_fixture( + vehicle_state_path, + integration=BMW_DOMAIN, + ) + ) + + account.add_vehicle( + vehicle_base, + vehicle_state, + fetched_at, + ) + + # simulate storing fingerprints + if account.config.log_response_path: + log_to_to_file( + json.dumps(vehicle_state), + account.config.log_response_path, + f"state_{vehicle_base['vin']}", + ) + + +async def setup_mocked_integration(hass: HomeAssistant) -> MockConfigEntry: + """Mock a fully setup config entry and all components based on fixtures.""" + + mock_config_entry = MockConfigEntry(**FIXTURE_CONFIG_ENTRY) + mock_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + return mock_config_entry diff --git a/tests/components/bmw_connected_drive/conftest.py b/tests/components/bmw_connected_drive/conftest.py new file mode 100644 index 00000000000..bf9d32ed9fa --- /dev/null +++ b/tests/components/bmw_connected_drive/conftest.py @@ -0,0 +1,12 @@ +"""Fixtures for BMW tests.""" + +from bimmer_connected.account import MyBMWAccount +import pytest + +from . import mock_vehicles_from_fixture + + +@pytest.fixture +async def bmw_fixture(monkeypatch): + """Patch the vehicle fixtures into a MyBMWAccount.""" + monkeypatch.setattr(MyBMWAccount, "get_vehicles", mock_vehicles_from_fixture) diff --git a/tests/components/bmw_connected_drive/fixtures/vehicles/I01/state_WBY00000000REXI01_0.json b/tests/components/bmw_connected_drive/fixtures/vehicles/I01/state_WBY00000000REXI01_0.json new file mode 100644 index 00000000000..adc2bde3650 --- /dev/null +++ b/tests/components/bmw_connected_drive/fixtures/vehicles/I01/state_WBY00000000REXI01_0.json @@ -0,0 +1,206 @@ +{ + "capabilities": { + "climateFunction": "AIR_CONDITIONING", + "climateNow": true, + "climateTimerTrigger": "DEPARTURE_TIMER", + "horn": true, + "isBmwChargingSupported": true, + "isCarSharingSupported": false, + "isChargeNowForBusinessSupported": false, + "isChargingHistorySupported": true, + "isChargingHospitalityEnabled": false, + "isChargingLoudnessEnabled": false, + "isChargingPlanSupported": true, + "isChargingPowerLimitEnabled": false, + "isChargingSettingsEnabled": false, + "isChargingTargetSocEnabled": false, + "isClimateTimerSupported": true, + "isCustomerEsimSupported": false, + "isDCSContractManagementSupported": true, + "isDataPrivacyEnabled": false, + "isEasyChargeEnabled": false, + "isEvGoChargingSupported": false, + "isMiniChargingSupported": false, + "isNonLscFeatureEnabled": false, + "isRemoteEngineStartSupported": false, + "isRemoteHistoryDeletionSupported": false, + "isRemoteHistorySupported": true, + "isRemoteParkingSupported": false, + "isRemoteServicesActivationRequired": false, + "isRemoteServicesBookingRequired": false, + "isScanAndChargeSupported": false, + "isSustainabilitySupported": false, + "isWifiHotspotServiceSupported": false, + "lastStateCallState": "ACTIVATED", + "lights": true, + "lock": true, + "remoteChargingCommands": {}, + "sendPoi": true, + "specialThemeSupport": [], + "unlock": true, + "vehicleFinder": false, + "vehicleStateSource": "LAST_STATE_CALL" + }, + "state": { + "chargingProfile": { + "chargingControlType": "WEEKLY_PLANNER", + "chargingMode": "DELAYED_CHARGING", + "chargingPreference": "CHARGING_WINDOW", + "chargingSettings": { + "hospitality": "NO_ACTION", + "idcc": "NO_ACTION", + "targetSoc": 100 + }, + "climatisationOn": false, + "departureTimes": [ + { + "action": "DEACTIVATE", + "id": 1, + "timeStamp": { + "hour": 7, + "minute": 35 + }, + "timerWeekDays": [ + "MONDAY", + "TUESDAY", + "WEDNESDAY", + "THURSDAY", + "FRIDAY" + ] + }, + { + "action": "DEACTIVATE", + "id": 2, + "timeStamp": { + "hour": 18, + "minute": 0 + }, + "timerWeekDays": [ + "MONDAY", + "TUESDAY", + "WEDNESDAY", + "THURSDAY", + "FRIDAY", + "SATURDAY", + "SUNDAY" + ] + }, + { + "action": "DEACTIVATE", + "id": 3, + "timeStamp": { + "hour": 7, + "minute": 0 + }, + "timerWeekDays": [] + }, + { + "action": "DEACTIVATE", + "id": 4, + "timerWeekDays": [] + } + ], + "reductionOfChargeCurrent": { + "end": { + "hour": 1, + "minute": 30 + }, + "start": { + "hour": 18, + "minute": 1 + } + } + }, + "checkControlMessages": [], + "climateTimers": [ + { + "departureTime": { + "hour": 6, + "minute": 40 + }, + "isWeeklyTimer": true, + "timerAction": "ACTIVATE", + "timerWeekDays": ["THURSDAY", "SUNDAY"] + }, + { + "departureTime": { + "hour": 12, + "minute": 50 + }, + "isWeeklyTimer": false, + "timerAction": "ACTIVATE", + "timerWeekDays": ["MONDAY"] + }, + { + "departureTime": { + "hour": 18, + "minute": 59 + }, + "isWeeklyTimer": true, + "timerAction": "DEACTIVATE", + "timerWeekDays": ["WEDNESDAY"] + } + ], + "combustionFuelLevel": { + "range": 105, + "remainingFuelLiters": 6, + "remainingFuelPercent": 65 + }, + "currentMileage": 137009, + "doorsState": { + "combinedSecurityState": "UNLOCKED", + "combinedState": "CLOSED", + "hood": "CLOSED", + "leftFront": "CLOSED", + "leftRear": "CLOSED", + "rightFront": "CLOSED", + "rightRear": "CLOSED", + "trunk": "CLOSED" + }, + "driverPreferences": { + "lscPrivacyMode": "OFF" + }, + "electricChargingState": { + "chargingConnectionType": "CONDUCTIVE", + "chargingLevelPercent": 82, + "chargingStatus": "WAITING_FOR_CHARGING", + "chargingTarget": 100, + "isChargerConnected": true, + "range": 174 + }, + "isLeftSteering": true, + "isLscSupported": true, + "lastFetched": "2022-06-22T14:24:23.982Z", + "lastUpdatedAt": "2022-06-22T13:58:52Z", + "range": 174, + "requiredServices": [ + { + "dateTime": "2022-10-01T00:00:00.000Z", + "description": "Next service due by the specified date.", + "status": "OK", + "type": "BRAKE_FLUID" + }, + { + "dateTime": "2023-05-01T00:00:00.000Z", + "description": "Next vehicle check due after the specified distance or date.", + "status": "OK", + "type": "VEHICLE_CHECK" + }, + { + "dateTime": "2023-05-01T00:00:00.000Z", + "description": "Next state inspection due by the specified date.", + "status": "OK", + "type": "VEHICLE_TUV" + } + ], + "roofState": { + "roofState": "CLOSED", + "roofStateType": "SUN_ROOF" + }, + "windowsState": { + "combinedState": "CLOSED", + "leftFront": "CLOSED", + "rightFront": "CLOSED" + } + } +} diff --git a/tests/components/bmw_connected_drive/fixtures/vehicles/I01/vehicles_v2_bmw_0.json b/tests/components/bmw_connected_drive/fixtures/vehicles/I01/vehicles_v2_bmw_0.json new file mode 100644 index 00000000000..145bc13378e --- /dev/null +++ b/tests/components/bmw_connected_drive/fixtures/vehicles/I01/vehicles_v2_bmw_0.json @@ -0,0 +1,47 @@ +[ + { + "appVehicleType": "CONNECTED", + "attributes": { + "a4aType": "USB_ONLY", + "bodyType": "I01", + "brand": "BMW_I", + "color": 4284110934, + "countryOfOrigin": "CZ", + "driveTrain": "ELECTRIC_WITH_RANGE_EXTENDER", + "driverGuideInfo": { + "androidAppScheme": "com.bmwgroup.driversguide.row", + "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row", + "iosAppScheme": "bmwdriversguide:///open", + "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8" + }, + "headUnitType": "NBT", + "hmiVersion": "ID4", + "lastFetched": "2022-07-10T09:25:53.104Z", + "model": "i3 (+ REX)", + "softwareVersionCurrent": { + "iStep": 510, + "puStep": { + "month": 11, + "year": 21 + }, + "seriesCluster": "I001" + }, + "softwareVersionExFactory": { + "iStep": 502, + "puStep": { + "month": 3, + "year": 15 + }, + "seriesCluster": "I001" + }, + "year": 2015 + }, + "mappingInfo": { + "isAssociated": false, + "isLmmEnabled": false, + "isPrimaryUser": true, + "mappingStatus": "CONFIRMED" + }, + "vin": "WBY00000000REXI01" + } +] diff --git a/tests/components/bmw_connected_drive/test_config_flow.py b/tests/components/bmw_connected_drive/test_config_flow.py index 3f22f984a54..daac0c04f7b 100644 --- a/tests/components/bmw_connected_drive/test_config_flow.py +++ b/tests/components/bmw_connected_drive/test_config_flow.py @@ -12,15 +12,11 @@ from homeassistant.components.bmw_connected_drive.const import ( ) from homeassistant.const import CONF_USERNAME -from . import FIXTURE_CONFIG_ENTRY, FIXTURE_USER_INPUT +from . import FIXTURE_CONFIG_ENTRY, FIXTURE_REFRESH_TOKEN, FIXTURE_USER_INPUT from tests.common import MockConfigEntry -FIXTURE_REFRESH_TOKEN = "SOME_REFRESH_TOKEN" -FIXTURE_COMPLETE_ENTRY = { - **FIXTURE_USER_INPUT, - CONF_REFRESH_TOKEN: FIXTURE_REFRESH_TOKEN, -} +FIXTURE_COMPLETE_ENTRY = FIXTURE_CONFIG_ENTRY["data"] FIXTURE_IMPORT_ENTRY = {**FIXTURE_USER_INPUT, CONF_REFRESH_TOKEN: None} diff --git a/tests/components/bmw_connected_drive/test_sensor.py b/tests/components/bmw_connected_drive/test_sensor.py new file mode 100644 index 00000000000..cb1299a274b --- /dev/null +++ b/tests/components/bmw_connected_drive/test_sensor.py @@ -0,0 +1,52 @@ +"""Test BMW sensors.""" +import pytest + +from homeassistant.core import HomeAssistant +from homeassistant.util.unit_system import ( + IMPERIAL_SYSTEM as IMPERIAL, + METRIC_SYSTEM as METRIC, + UnitSystem, +) + +from . import setup_mocked_integration + + +@pytest.mark.parametrize( + "entity_id,unit_system,value,unit_of_measurement", + [ + ("sensor.i3_rex_remaining_range_total", METRIC, "279", "km"), + ("sensor.i3_rex_remaining_range_total", IMPERIAL, "173.36", "mi"), + ("sensor.i3_rex_mileage", METRIC, "137009", "km"), + ("sensor.i3_rex_mileage", IMPERIAL, "85133.42", "mi"), + ("sensor.i3_rex_remaining_battery_percent", METRIC, "82", "%"), + ("sensor.i3_rex_remaining_battery_percent", IMPERIAL, "82", "%"), + ("sensor.i3_rex_remaining_range_electric", METRIC, "174", "km"), + ("sensor.i3_rex_remaining_range_electric", IMPERIAL, "108.12", "mi"), + ("sensor.i3_rex_remaining_fuel", METRIC, "6", "L"), + ("sensor.i3_rex_remaining_fuel", IMPERIAL, "1.59", "gal"), + ("sensor.i3_rex_remaining_range_fuel", METRIC, "105", "km"), + ("sensor.i3_rex_remaining_range_fuel", IMPERIAL, "65.24", "mi"), + ("sensor.i3_rex_remaining_fuel_percent", METRIC, "65", "%"), + ("sensor.i3_rex_remaining_fuel_percent", IMPERIAL, "65", "%"), + ], +) +async def test_unit_conversion( + hass: HomeAssistant, + entity_id: str, + unit_system: UnitSystem, + value: str, + unit_of_measurement: str, + bmw_fixture, +) -> None: + """Test conversion between metric and imperial units for sensors.""" + + # Set unit system + hass.config.units = unit_system + + # Setup component + assert await setup_mocked_integration(hass) + + # Test + entity = hass.states.get(entity_id) + assert entity.state == value + assert entity.attributes.get("unit_of_measurement") == unit_of_measurement From 8e9313840e768f903299c55ca724487c96f3ed9d Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Tue, 16 Aug 2022 07:54:26 +0200 Subject: [PATCH 74/81] Fix Overkiz startup order to prevent unnamed device showing up (#76695) Gateways should be added first, before platform setup --- homeassistant/components/overkiz/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/overkiz/__init__.py b/homeassistant/components/overkiz/__init__.py index 9acdbfb9ec9..a4240bc0550 100644 --- a/homeassistant/components/overkiz/__init__.py +++ b/homeassistant/components/overkiz/__init__.py @@ -111,8 +111,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) or OVERKIZ_DEVICE_TO_PLATFORM.get(device.ui_class): platforms[platform].append(device) - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - device_registry = dr.async_get(hass) for gateway in setup.gateways: @@ -128,6 +126,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: configuration_url=server.configuration_url, ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True From c1d02388d1c5a2d3e79e758cfbe1883fe9e6801a Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 16 Aug 2022 19:49:49 +0200 Subject: [PATCH 75/81] Bump pynetgear to 0.10.7 (#76754) --- homeassistant/components/netgear/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/netgear/manifest.json b/homeassistant/components/netgear/manifest.json index 5fd59faac83..69a21e5aace 100644 --- a/homeassistant/components/netgear/manifest.json +++ b/homeassistant/components/netgear/manifest.json @@ -2,7 +2,7 @@ "domain": "netgear", "name": "NETGEAR", "documentation": "https://www.home-assistant.io/integrations/netgear", - "requirements": ["pynetgear==0.10.6"], + "requirements": ["pynetgear==0.10.7"], "codeowners": ["@hacf-fr", "@Quentame", "@starkillerOG"], "iot_class": "local_polling", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 300ac05de84..01d1c95d878 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1683,7 +1683,7 @@ pymyq==3.1.4 pymysensors==0.22.1 # homeassistant.components.netgear -pynetgear==0.10.6 +pynetgear==0.10.7 # homeassistant.components.netio pynetio==0.1.9.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a069c31b87b..1b5068e81fb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1160,7 +1160,7 @@ pymyq==3.1.4 pymysensors==0.22.1 # homeassistant.components.netgear -pynetgear==0.10.6 +pynetgear==0.10.7 # homeassistant.components.nina pynina==0.1.8 From b4cb9a521a15500ef0944fb6465ce611e403f70f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 17 Aug 2022 10:53:05 +0200 Subject: [PATCH 76/81] Correct restoring of mobile_app sensors (#76886) --- .../components/mobile_app/binary_sensor.py | 5 +- homeassistant/components/mobile_app/entity.py | 5 +- homeassistant/components/mobile_app/sensor.py | 28 ++- tests/components/mobile_app/test_sensor.py | 160 ++++++++++++++++-- 4 files changed, 172 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/mobile_app/binary_sensor.py b/homeassistant/components/mobile_app/binary_sensor.py index fd8545b1f98..69ecb913c98 100644 --- a/homeassistant/components/mobile_app/binary_sensor.py +++ b/homeassistant/components/mobile_app/binary_sensor.py @@ -75,9 +75,8 @@ class MobileAppBinarySensor(MobileAppEntity, BinarySensorEntity): """Return the state of the binary sensor.""" return self._config[ATTR_SENSOR_STATE] - @callback - def async_restore_last_state(self, last_state): + async def async_restore_last_state(self, last_state): """Restore previous state.""" - super().async_restore_last_state(last_state) + await super().async_restore_last_state(last_state) self._config[ATTR_SENSOR_STATE] = last_state.state == STATE_ON diff --git a/homeassistant/components/mobile_app/entity.py b/homeassistant/components/mobile_app/entity.py index d4c4374b8d9..3a2f038a0af 100644 --- a/homeassistant/components/mobile_app/entity.py +++ b/homeassistant/components/mobile_app/entity.py @@ -43,10 +43,9 @@ class MobileAppEntity(RestoreEntity): if (state := await self.async_get_last_state()) is None: return - self.async_restore_last_state(state) + await self.async_restore_last_state(state) - @callback - def async_restore_last_state(self, last_state): + async def async_restore_last_state(self, last_state): """Restore previous state.""" self._config[ATTR_SENSOR_STATE] = last_state.state self._config[ATTR_SENSOR_ATTRIBUTES] = { diff --git a/homeassistant/components/mobile_app/sensor.py b/homeassistant/components/mobile_app/sensor.py index d7cfc9545f6..ef7dd122496 100644 --- a/homeassistant/components/mobile_app/sensor.py +++ b/homeassistant/components/mobile_app/sensor.py @@ -3,9 +3,9 @@ from __future__ import annotations from typing import Any -from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from homeassistant.components.sensor import RestoreSensor, SensorDeviceClass from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_WEBHOOK_ID, STATE_UNKNOWN +from homeassistant.const import CONF_WEBHOOK_ID, STATE_UNKNOWN, TEMP_CELSIUS from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -27,6 +27,7 @@ from .const import ( DOMAIN, ) from .entity import MobileAppEntity +from .webhook import _extract_sensor_unique_id async def async_setup_entry( @@ -73,9 +74,30 @@ async def async_setup_entry( ) -class MobileAppSensor(MobileAppEntity, SensorEntity): +class MobileAppSensor(MobileAppEntity, RestoreSensor): """Representation of an mobile app sensor.""" + async def async_restore_last_state(self, last_state): + """Restore previous state.""" + + await super().async_restore_last_state(last_state) + + if not (last_sensor_data := await self.async_get_last_sensor_data()): + # Workaround to handle migration to RestoreSensor, can be removed + # in HA Core 2023.4 + self._config[ATTR_SENSOR_STATE] = None + webhook_id = self._entry.data[CONF_WEBHOOK_ID] + sensor_unique_id = _extract_sensor_unique_id(webhook_id, self.unique_id) + if ( + self.device_class == SensorDeviceClass.TEMPERATURE + and sensor_unique_id == "battery_temperature" + ): + self._config[ATTR_SENSOR_UOM] = TEMP_CELSIUS + return + + self._config[ATTR_SENSOR_STATE] = last_sensor_data.native_value + self._config[ATTR_SENSOR_UOM] = last_sensor_data.native_unit_of_measurement + @property def native_value(self): """Return the state of the sensor.""" diff --git a/tests/components/mobile_app/test_sensor.py b/tests/components/mobile_app/test_sensor.py index c0f7f126a49..930fb522c4c 100644 --- a/tests/components/mobile_app/test_sensor.py +++ b/tests/components/mobile_app/test_sensor.py @@ -1,15 +1,34 @@ """Entity tests for mobile_app.""" from http import HTTPStatus +from unittest.mock import patch import pytest from homeassistant.components.sensor import SensorDeviceClass -from homeassistant.const import PERCENTAGE, STATE_UNAVAILABLE, STATE_UNKNOWN +from homeassistant.const import ( + PERCENTAGE, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM -async def test_sensor(hass, create_registrations, webhook_client): +@pytest.mark.parametrize( + "unit_system, state_unit, state1, state2", + ( + (METRIC_SYSTEM, TEMP_CELSIUS, "100", "123"), + (IMPERIAL_SYSTEM, TEMP_FAHRENHEIT, "212", "253"), + ), +) +async def test_sensor( + hass, create_registrations, webhook_client, unit_system, state_unit, state1, state2 +): """Test that sensors can be registered and updated.""" + hass.config.units = unit_system + webhook_id = create_registrations[1]["webhook_id"] webhook_url = f"/api/webhook/{webhook_id}" @@ -19,15 +38,15 @@ async def test_sensor(hass, create_registrations, webhook_client): "type": "register_sensor", "data": { "attributes": {"foo": "bar"}, - "device_class": "battery", + "device_class": "temperature", "icon": "mdi:battery", - "name": "Battery State", + "name": "Battery Temperature", "state": 100, "type": "sensor", "entity_category": "diagnostic", - "unique_id": "battery_state", + "unique_id": "battery_temp", "state_class": "total", - "unit_of_measurement": PERCENTAGE, + "unit_of_measurement": TEMP_CELSIUS, }, }, ) @@ -38,20 +57,23 @@ async def test_sensor(hass, create_registrations, webhook_client): assert json == {"success": True} await hass.async_block_till_done() - entity = hass.states.get("sensor.test_1_battery_state") + entity = hass.states.get("sensor.test_1_battery_temperature") assert entity is not None - assert entity.attributes["device_class"] == "battery" + assert entity.attributes["device_class"] == "temperature" assert entity.attributes["icon"] == "mdi:battery" - assert entity.attributes["unit_of_measurement"] == PERCENTAGE + # unit of temperature sensor is automatically converted to the system UoM + assert entity.attributes["unit_of_measurement"] == state_unit assert entity.attributes["foo"] == "bar" assert entity.attributes["state_class"] == "total" assert entity.domain == "sensor" - assert entity.name == "Test 1 Battery State" - assert entity.state == "100" + assert entity.name == "Test 1 Battery Temperature" + assert entity.state == state1 assert ( - er.async_get(hass).async_get("sensor.test_1_battery_state").entity_category + er.async_get(hass) + .async_get("sensor.test_1_battery_temperature") + .entity_category == "diagnostic" ) @@ -64,7 +86,7 @@ async def test_sensor(hass, create_registrations, webhook_client): "icon": "mdi:battery-unknown", "state": 123, "type": "sensor", - "unique_id": "battery_state", + "unique_id": "battery_temp", }, # This invalid data should not invalidate whole request {"type": "sensor", "unique_id": "invalid_state", "invalid": "data"}, @@ -77,8 +99,8 @@ async def test_sensor(hass, create_registrations, webhook_client): json = await update_resp.json() assert json["invalid_state"]["success"] is False - updated_entity = hass.states.get("sensor.test_1_battery_state") - assert updated_entity.state == "123" + updated_entity = hass.states.get("sensor.test_1_battery_temperature") + assert updated_entity.state == state2 assert "foo" not in updated_entity.attributes dev_reg = dr.async_get(hass) @@ -88,16 +110,120 @@ async def test_sensor(hass, create_registrations, webhook_client): config_entry = hass.config_entries.async_entries("mobile_app")[1] await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() - unloaded_entity = hass.states.get("sensor.test_1_battery_state") + unloaded_entity = hass.states.get("sensor.test_1_battery_temperature") assert unloaded_entity.state == STATE_UNAVAILABLE await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - restored_entity = hass.states.get("sensor.test_1_battery_state") + restored_entity = hass.states.get("sensor.test_1_battery_temperature") assert restored_entity.state == updated_entity.state assert restored_entity.attributes == updated_entity.attributes +@pytest.mark.parametrize( + "unique_id, unit_system, state_unit, state1, state2", + ( + ("battery_temperature", METRIC_SYSTEM, TEMP_CELSIUS, "100", "123"), + ("battery_temperature", IMPERIAL_SYSTEM, TEMP_FAHRENHEIT, "212", "253"), + # The unique_id doesn't match that of the mobile app's battery temperature sensor + ("battery_temp", IMPERIAL_SYSTEM, TEMP_FAHRENHEIT, "212", "123"), + ), +) +async def test_sensor_migration( + hass, + create_registrations, + webhook_client, + unique_id, + unit_system, + state_unit, + state1, + state2, +): + """Test migration to RestoreSensor.""" + hass.config.units = unit_system + + webhook_id = create_registrations[1]["webhook_id"] + webhook_url = f"/api/webhook/{webhook_id}" + + reg_resp = await webhook_client.post( + webhook_url, + json={ + "type": "register_sensor", + "data": { + "attributes": {"foo": "bar"}, + "device_class": "temperature", + "icon": "mdi:battery", + "name": "Battery Temperature", + "state": 100, + "type": "sensor", + "entity_category": "diagnostic", + "unique_id": unique_id, + "state_class": "total", + "unit_of_measurement": TEMP_CELSIUS, + }, + }, + ) + + assert reg_resp.status == HTTPStatus.CREATED + + json = await reg_resp.json() + assert json == {"success": True} + await hass.async_block_till_done() + + entity = hass.states.get("sensor.test_1_battery_temperature") + assert entity is not None + + assert entity.attributes["device_class"] == "temperature" + assert entity.attributes["icon"] == "mdi:battery" + # unit of temperature sensor is automatically converted to the system UoM + assert entity.attributes["unit_of_measurement"] == state_unit + assert entity.attributes["foo"] == "bar" + assert entity.attributes["state_class"] == "total" + assert entity.domain == "sensor" + assert entity.name == "Test 1 Battery Temperature" + assert entity.state == state1 + + # Reload to verify state is restored + config_entry = hass.config_entries.async_entries("mobile_app")[1] + await hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done() + unloaded_entity = hass.states.get("sensor.test_1_battery_temperature") + assert unloaded_entity.state == STATE_UNAVAILABLE + + # Simulate migration to RestoreSensor + with patch( + "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_extra_data", + return_value=None, + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + restored_entity = hass.states.get("sensor.test_1_battery_temperature") + assert restored_entity.state == "unknown" + assert restored_entity.attributes == entity.attributes + + # Test unit conversion is working + update_resp = await webhook_client.post( + webhook_url, + json={ + "type": "update_sensor_states", + "data": [ + { + "icon": "mdi:battery-unknown", + "state": 123, + "type": "sensor", + "unique_id": unique_id, + }, + ], + }, + ) + + assert update_resp.status == HTTPStatus.OK + + updated_entity = hass.states.get("sensor.test_1_battery_temperature") + assert updated_entity.state == state2 + assert "foo" not in updated_entity.attributes + + async def test_sensor_must_register(hass, create_registrations, webhook_client): """Test that sensors must be registered before updating.""" webhook_id = create_registrations[1]["webhook_id"] From c0b4eb35fe0714dbe003aa9c574914b14408928d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 17 Aug 2022 17:00:30 +0200 Subject: [PATCH 77/81] Fix acmeda set cover tilt position (#76927) --- homeassistant/components/acmeda/cover.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/acmeda/cover.py b/homeassistant/components/acmeda/cover.py index 887e26cd7fc..3c3a5ba825a 100644 --- a/homeassistant/components/acmeda/cover.py +++ b/homeassistant/components/acmeda/cover.py @@ -16,6 +16,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .base import AcmedaBase from .const import ACMEDA_HUB_UPDATE, DOMAIN from .helpers import async_add_acmeda_entities +from .hub import PulseHub async def async_setup_entry( @@ -24,7 +25,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Acmeda Rollers from a config entry.""" - hub = hass.data[DOMAIN][config_entry.entry_id] + hub: PulseHub = hass.data[DOMAIN][config_entry.entry_id] current: set[int] = set() @@ -122,6 +123,6 @@ class AcmedaCover(AcmedaBase, CoverEntity): """Stop the roller.""" await self.roller.move_stop() - async def async_set_cover_tilt(self, **kwargs): + async def async_set_cover_tilt_position(self, **kwargs: Any) -> None: """Tilt the roller shutter to a specific position.""" await self.roller.move_to(100 - kwargs[ATTR_POSITION]) From 34cb79408a366aa152465786441566c0c2e43ec9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 17 Aug 2022 16:37:47 -1000 Subject: [PATCH 78/81] Fix race in notify setup (#76954) --- homeassistant/components/notify/__init__.py | 12 ++- homeassistant/components/notify/legacy.py | 21 ++--- tests/components/notify/test_init.py | 99 ++++++++++++++++++++- 3 files changed, 119 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index 788c698c0ca..60d24578593 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -1,6 +1,8 @@ """Provides functionality to notify people.""" from __future__ import annotations +import asyncio + import voluptuous as vol import homeassistant.components.persistent_notification as pn @@ -40,13 +42,19 @@ PLATFORM_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the notify services.""" + platform_setups = async_setup_legacy(hass, config) + # We need to add the component here break the deadlock # when setting up integrations from config entries as # they would otherwise wait for notify to be # setup and thus the config entries would not be able to - # setup their platforms. + # setup their platforms, but we need to do it after + # the dispatcher is connected so we don't miss integrations + # that are registered before the dispatcher is connected hass.config.components.add(DOMAIN) - await async_setup_legacy(hass, config) + + if platform_setups: + await asyncio.wait([asyncio.create_task(setup) for setup in platform_setups]) async def persistent_notification(service: ServiceCall) -> None: """Send notification via the built-in persistsent_notify integration.""" diff --git a/homeassistant/components/notify/legacy.py b/homeassistant/components/notify/legacy.py index 50b02324827..f9066b7dff9 100644 --- a/homeassistant/components/notify/legacy.py +++ b/homeassistant/components/notify/legacy.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from collections.abc import Coroutine from functools import partial from typing import Any, cast @@ -32,7 +33,10 @@ NOTIFY_SERVICES = "notify_services" NOTIFY_DISCOVERY_DISPATCHER = "notify_discovery_dispatcher" -async def async_setup_legacy(hass: HomeAssistant, config: ConfigType) -> None: +@callback +def async_setup_legacy( + hass: HomeAssistant, config: ConfigType +) -> list[Coroutine[Any, Any, None]]: """Set up legacy notify services.""" hass.data.setdefault(NOTIFY_SERVICES, {}) hass.data.setdefault(NOTIFY_DISCOVERY_DISPATCHER, None) @@ -101,15 +105,6 @@ async def async_setup_legacy(hass: HomeAssistant, config: ConfigType) -> None: ) hass.config.components.add(f"{DOMAIN}.{integration_name}") - setup_tasks = [ - asyncio.create_task(async_setup_platform(integration_name, p_config)) - for integration_name, p_config in config_per_platform(config, DOMAIN) - if integration_name is not None - ] - - if setup_tasks: - await asyncio.wait(setup_tasks) - async def async_platform_discovered( platform: str, info: DiscoveryInfoType | None ) -> None: @@ -120,6 +115,12 @@ async def async_setup_legacy(hass: HomeAssistant, config: ConfigType) -> None: hass, DOMAIN, async_platform_discovered ) + return [ + async_setup_platform(integration_name, p_config) + for integration_name, p_config in config_per_platform(config, DOMAIN) + if integration_name is not None + ] + @callback def check_templates_warn(hass: HomeAssistant, tpl: template.Template) -> None: diff --git a/tests/components/notify/test_init.py b/tests/components/notify/test_init.py index ae32884add7..b691ed7a051 100644 --- a/tests/components/notify/test_init.py +++ b/tests/components/notify/test_init.py @@ -1,12 +1,13 @@ """The tests for notify services that change targets.""" +import asyncio from unittest.mock import Mock, patch import yaml from homeassistant import config as hass_config from homeassistant.components import notify -from homeassistant.const import SERVICE_RELOAD +from homeassistant.const import SERVICE_RELOAD, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.reload import async_setup_reload_service @@ -330,3 +331,99 @@ async def test_setup_platform_and_reload(hass, caplog, tmp_path): # Check if the dynamically notify services from setup were removed assert not hass.services.has_service(notify.DOMAIN, "testnotify2_c") assert not hass.services.has_service(notify.DOMAIN, "testnotify2_d") + + +async def test_setup_platform_before_notify_setup(hass, caplog, tmp_path): + """Test trying to setup a platform before notify is setup.""" + get_service_called = Mock() + + async def async_get_service(hass, config, discovery_info=None): + """Get notify service for mocked platform.""" + get_service_called(config, discovery_info) + targetlist = {"a": 1, "b": 2} + return NotificationService(hass, targetlist, "testnotify") + + async def async_get_service2(hass, config, discovery_info=None): + """Get notify service for mocked platform.""" + get_service_called(config, discovery_info) + targetlist = {"c": 3, "d": 4} + return NotificationService(hass, targetlist, "testnotify2") + + # Mock first platform + mock_notify_platform( + hass, tmp_path, "testnotify", async_get_service=async_get_service + ) + + # Initialize a second platform testnotify2 + mock_notify_platform( + hass, tmp_path, "testnotify2", async_get_service=async_get_service2 + ) + + hass_config = {"notify": [{"platform": "testnotify"}]} + + # Setup the second testnotify2 platform from discovery + load_coro = async_load_platform( + hass, Platform.NOTIFY, "testnotify2", {}, hass_config=hass_config + ) + + # Setup the testnotify platform + setup_coro = async_setup_component(hass, "notify", hass_config) + + load_task = asyncio.create_task(load_coro) + setup_task = asyncio.create_task(setup_coro) + + await asyncio.gather(load_task, setup_task) + + await hass.async_block_till_done() + assert hass.services.has_service(notify.DOMAIN, "testnotify_a") + assert hass.services.has_service(notify.DOMAIN, "testnotify_b") + assert hass.services.has_service(notify.DOMAIN, "testnotify2_c") + assert hass.services.has_service(notify.DOMAIN, "testnotify2_d") + + +async def test_setup_platform_after_notify_setup(hass, caplog, tmp_path): + """Test trying to setup a platform after notify is setup.""" + get_service_called = Mock() + + async def async_get_service(hass, config, discovery_info=None): + """Get notify service for mocked platform.""" + get_service_called(config, discovery_info) + targetlist = {"a": 1, "b": 2} + return NotificationService(hass, targetlist, "testnotify") + + async def async_get_service2(hass, config, discovery_info=None): + """Get notify service for mocked platform.""" + get_service_called(config, discovery_info) + targetlist = {"c": 3, "d": 4} + return NotificationService(hass, targetlist, "testnotify2") + + # Mock first platform + mock_notify_platform( + hass, tmp_path, "testnotify", async_get_service=async_get_service + ) + + # Initialize a second platform testnotify2 + mock_notify_platform( + hass, tmp_path, "testnotify2", async_get_service=async_get_service2 + ) + + hass_config = {"notify": [{"platform": "testnotify"}]} + + # Setup the second testnotify2 platform from discovery + load_coro = async_load_platform( + hass, Platform.NOTIFY, "testnotify2", {}, hass_config=hass_config + ) + + # Setup the testnotify platform + setup_coro = async_setup_component(hass, "notify", hass_config) + + setup_task = asyncio.create_task(setup_coro) + load_task = asyncio.create_task(load_coro) + + await asyncio.gather(load_task, setup_task) + + await hass.async_block_till_done() + assert hass.services.has_service(notify.DOMAIN, "testnotify_a") + assert hass.services.has_service(notify.DOMAIN, "testnotify_b") + assert hass.services.has_service(notify.DOMAIN, "testnotify2_c") + assert hass.services.has_service(notify.DOMAIN, "testnotify2_d") From 341bf8eff417a1733a887f782ebf8ec042a22315 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 17 Aug 2022 22:41:59 -0400 Subject: [PATCH 79/81] Pass the real config for Discord (#76959) --- homeassistant/components/discord/__init__.py | 16 ++++++++++------ homeassistant/components/discord/const.py | 2 ++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/discord/__init__.py b/homeassistant/components/discord/__init__.py index ae06447f741..a52c079ac8e 100644 --- a/homeassistant/components/discord/__init__.py +++ b/homeassistant/components/discord/__init__.py @@ -7,12 +7,20 @@ from homeassistant.const import CONF_API_TOKEN, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import discovery +from homeassistant.helpers.typing import ConfigType -from .const import DOMAIN +from .const import DATA_HASS_CONFIG, DOMAIN PLATFORMS = [Platform.NOTIFY] +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the Discord component.""" + + hass.data[DATA_HASS_CONFIG] = config + return True + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Discord from a config entry.""" nextcord.VoiceClient.warn_nacl = False @@ -30,11 +38,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.async_create_task( discovery.async_load_platform( - hass, - Platform.NOTIFY, - DOMAIN, - hass.data[DOMAIN][entry.entry_id], - hass.data[DOMAIN], + hass, Platform.NOTIFY, DOMAIN, dict(entry.data), hass.data[DATA_HASS_CONFIG] ) ) diff --git a/homeassistant/components/discord/const.py b/homeassistant/components/discord/const.py index 9f11c3e2d7a..82ddb890685 100644 --- a/homeassistant/components/discord/const.py +++ b/homeassistant/components/discord/const.py @@ -8,3 +8,5 @@ DEFAULT_NAME = "Discord" DOMAIN: Final = "discord" URL_PLACEHOLDER = {CONF_URL: "https://www.home-assistant.io/integrations/discord"} + +DATA_HASS_CONFIG = "discord_hass_config" From b9964c73ed3b60913d23f6d2be15c6c618f4974a Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 17 Aug 2022 22:41:28 -0400 Subject: [PATCH 80/81] Pass the real config for Slack (#76960) --- homeassistant/components/slack/__init__.py | 6 ++++-- homeassistant/components/slack/const.py | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/slack/__init__.py b/homeassistant/components/slack/__init__.py index ae52013621f..a89f645e9b6 100644 --- a/homeassistant/components/slack/__init__.py +++ b/homeassistant/components/slack/__init__.py @@ -12,7 +12,7 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, discovery from homeassistant.helpers.typing import ConfigType -from .const import DATA_CLIENT, DOMAIN +from .const import DATA_CLIENT, DATA_HASS_CONFIG, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -21,6 +21,8 @@ PLATFORMS = [Platform.NOTIFY] async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Slack component.""" + hass.data[DATA_HASS_CONFIG] = config + # Iterate all entries for notify to only get Slack if Platform.NOTIFY in config: for entry in config[Platform.NOTIFY]: @@ -55,7 +57,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: Platform.NOTIFY, DOMAIN, hass.data[DOMAIN][entry.entry_id], - hass.data[DOMAIN], + hass.data[DATA_HASS_CONFIG], ) ) diff --git a/homeassistant/components/slack/const.py b/homeassistant/components/slack/const.py index b7b5707aeeb..83937f4a43e 100644 --- a/homeassistant/components/slack/const.py +++ b/homeassistant/components/slack/const.py @@ -14,3 +14,5 @@ CONF_DEFAULT_CHANNEL = "default_channel" DATA_CLIENT = "client" DEFAULT_TIMEOUT_SECONDS = 15 DOMAIN: Final = "slack" + +DATA_HASS_CONFIG = "slack_hass_config" From d5b6ccab072d34c7f57bacdf3f25a7b026b398a0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 17 Aug 2022 23:09:00 -0400 Subject: [PATCH 81/81] Bumped version to 2022.8.6 --- 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 329c0e483dd..f75c1802375 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 = "5" +PATCH_VERSION: Final = "6" __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 b42b0b91b58..e69bdd747d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.8.5" +version = "2022.8.6" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst"