From 3492171ff8a6291176ddd5bf78c7e91e368e8327 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 26 Jun 2024 16:17:57 +0200 Subject: [PATCH 001/146] Bump version to 2024.7.0b0 --- 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 3a970aefd38..54d7f26a5f0 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "0.dev0" +PATCH_VERSION: Final = "0b0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 4edb1535411..0b490d621a3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.7.0.dev0" +version = "2024.7.0b0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From ba456f256448f6a5d9e9b23f2b64c51c9eb5883a Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Wed, 26 Jun 2024 21:53:02 +0300 Subject: [PATCH 002/146] Align Shelly sleeping devices timeout with non-sleeping (#118969) --- homeassistant/components/shelly/const.py | 4 +--- homeassistant/components/shelly/coordinator.py | 9 ++++----- tests/components/shelly/test_binary_sensor.py | 4 ++-- tests/components/shelly/test_coordinator.py | 9 ++++----- 4 files changed, 11 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index fcc7cc44af9..c5bdb88bbd1 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -83,11 +83,9 @@ REST_SENSORS_UPDATE_INTERVAL: Final = 60 # Refresh interval for RPC polling sensors RPC_SENSORS_POLLING_INTERVAL: Final = 60 -# Multiplier used to calculate the "update_interval" for sleeping devices. -SLEEP_PERIOD_MULTIPLIER: Final = 1.2 CONF_SLEEP_PERIOD: Final = "sleep_period" -# Multiplier used to calculate the "update_interval" for non-sleeping devices. +# Multiplier used to calculate the "update_interval" for shelly devices. UPDATE_PERIOD_MULTIPLIER: Final = 2.2 # Reconnect interval for GEN2 devices diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index 82d358b33d8..a4ff34f7d9a 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -54,7 +54,6 @@ from .const import ( RPC_RECONNECT_INTERVAL, RPC_SENSORS_POLLING_INTERVAL, SHBTN_MODELS, - SLEEP_PERIOD_MULTIPLIER, UPDATE_PERIOD_MULTIPLIER, BLEScannerMode, ) @@ -229,7 +228,7 @@ class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]): """Initialize the Shelly block device coordinator.""" self.entry = entry if self.sleep_period: - update_interval = SLEEP_PERIOD_MULTIPLIER * self.sleep_period + update_interval = UPDATE_PERIOD_MULTIPLIER * self.sleep_period else: update_interval = ( UPDATE_PERIOD_MULTIPLIER * device.settings["coiot"]["update_period"] @@ -429,7 +428,7 @@ class ShellyRestCoordinator(ShellyCoordinatorBase[BlockDevice]): in BATTERY_DEVICES_WITH_PERMANENT_CONNECTION ): update_interval = ( - SLEEP_PERIOD_MULTIPLIER * device.settings["coiot"]["update_period"] + UPDATE_PERIOD_MULTIPLIER * device.settings["coiot"]["update_period"] ) super().__init__(hass, entry, device, update_interval) @@ -459,7 +458,7 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): """Initialize the Shelly RPC device coordinator.""" self.entry = entry if self.sleep_period: - update_interval = SLEEP_PERIOD_MULTIPLIER * self.sleep_period + update_interval = UPDATE_PERIOD_MULTIPLIER * self.sleep_period else: update_interval = RPC_RECONNECT_INTERVAL super().__init__(hass, entry, device, update_interval) @@ -486,7 +485,7 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): data[CONF_SLEEP_PERIOD] = wakeup_period self.hass.config_entries.async_update_entry(self.entry, data=data) - update_interval = SLEEP_PERIOD_MULTIPLIER * wakeup_period + update_interval = UPDATE_PERIOD_MULTIPLIER * wakeup_period self.update_interval = timedelta(seconds=update_interval) return True diff --git a/tests/components/shelly/test_binary_sensor.py b/tests/components/shelly/test_binary_sensor.py index 026a7041863..3bfbf350f7e 100644 --- a/tests/components/shelly/test_binary_sensor.py +++ b/tests/components/shelly/test_binary_sensor.py @@ -7,7 +7,7 @@ from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.components.shelly.const import SLEEP_PERIOD_MULTIPLIER +from homeassistant.components.shelly.const import UPDATE_PERIOD_MULTIPLIER from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN from homeassistant.core import HomeAssistant, State from homeassistant.helpers.device_registry import DeviceRegistry @@ -122,7 +122,7 @@ async def test_block_rest_binary_sensor_connected_battery_devices( assert hass.states.get(entity_id).state == STATE_OFF # Verify update on slow intervals - await mock_rest_update(hass, freezer, seconds=SLEEP_PERIOD_MULTIPLIER * 3600) + await mock_rest_update(hass, freezer, seconds=UPDATE_PERIOD_MULTIPLIER * 3600) assert hass.states.get(entity_id).state == STATE_ON entry = entity_registry.async_get(entity_id) diff --git a/tests/components/shelly/test_coordinator.py b/tests/components/shelly/test_coordinator.py index 1e0af115c9e..35123a2db91 100644 --- a/tests/components/shelly/test_coordinator.py +++ b/tests/components/shelly/test_coordinator.py @@ -20,7 +20,6 @@ from homeassistant.components.shelly.const import ( ENTRY_RELOAD_COOLDOWN, MAX_PUSH_UPDATE_FAILURES, RPC_RECONNECT_INTERVAL, - SLEEP_PERIOD_MULTIPLIER, UPDATE_PERIOD_MULTIPLIER, BLEScannerMode, ) @@ -564,7 +563,7 @@ async def test_rpc_update_entry_sleep_period( # Move time to generate sleep period update monkeypatch.setitem(mock_rpc_device.status["sys"], "wakeup_period", 3600) - freezer.tick(timedelta(seconds=600 * SLEEP_PERIOD_MULTIPLIER)) + freezer.tick(timedelta(seconds=600 * UPDATE_PERIOD_MULTIPLIER)) async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) @@ -596,7 +595,7 @@ async def test_rpc_sleeping_device_no_periodic_updates( assert get_entity_state(hass, entity_id) == "22.9" # Move time to generate polling - freezer.tick(timedelta(seconds=SLEEP_PERIOD_MULTIPLIER * 1000)) + freezer.tick(timedelta(seconds=UPDATE_PERIOD_MULTIPLIER * 1000)) async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) @@ -889,7 +888,7 @@ async def test_block_sleeping_device_connection_error( assert get_entity_state(hass, entity_id) == STATE_ON # Move time to generate sleep period update - freezer.tick(timedelta(seconds=sleep_period * SLEEP_PERIOD_MULTIPLIER)) + freezer.tick(timedelta(seconds=sleep_period * UPDATE_PERIOD_MULTIPLIER)) async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) @@ -934,7 +933,7 @@ async def test_rpc_sleeping_device_connection_error( assert get_entity_state(hass, entity_id) == STATE_ON # Move time to generate sleep period update - freezer.tick(timedelta(seconds=sleep_period * SLEEP_PERIOD_MULTIPLIER)) + freezer.tick(timedelta(seconds=sleep_period * UPDATE_PERIOD_MULTIPLIER)) async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) From 6dd1e09354383cf19eb83946a9722f41b29ee5f5 Mon Sep 17 00:00:00 2001 From: Florian Date: Wed, 26 Jun 2024 21:45:17 +0200 Subject: [PATCH 003/146] Don't allow switch toggle when device in locked in AVM FRITZ!SmartHome (#120132) * fix: set state of the FritzBox-Switch to disabled if the option for manuel switching in the userinterface is disabled * feat: raise an error instead of disabling switch * feat: rename method signature * fix: tests * fix: wrong import * feat: Update homeassistant/components/fritzbox/strings.json Update error message Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com> * Update tests/components/fritzbox/test_switch.py feat: update test Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com> * make ruff happy * fix expected error message check --------- Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com> --- .../components/fritzbox/strings.json | 3 ++ homeassistant/components/fritzbox/switch.py | 12 ++++++++ tests/components/fritzbox/__init__.py | 2 +- tests/components/fritzbox/test_switch.py | 30 +++++++++++++++++++ 4 files changed, 46 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/fritzbox/strings.json b/homeassistant/components/fritzbox/strings.json index cee0afa26c1..d4f59fd1c08 100644 --- a/homeassistant/components/fritzbox/strings.json +++ b/homeassistant/components/fritzbox/strings.json @@ -81,6 +81,9 @@ } }, "exceptions": { + "manual_switching_disabled": { + "message": "Can't toggle switch while manual switching is disabled for the device." + }, "change_preset_while_active_mode": { "message": "Can't change preset while holiday or summer mode is active on the device." }, diff --git a/homeassistant/components/fritzbox/switch.py b/homeassistant/components/fritzbox/switch.py index 0bdf7a9f944..d13f21e1c14 100644 --- a/homeassistant/components/fritzbox/switch.py +++ b/homeassistant/components/fritzbox/switch.py @@ -6,9 +6,11 @@ from typing import Any from homeassistant.components.switch import SwitchEntity from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import FritzBoxDeviceEntity +from .const import DOMAIN from .coordinator import FritzboxConfigEntry @@ -48,10 +50,20 @@ class FritzboxSwitch(FritzBoxDeviceEntity, SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" + self.check_lock_state() await self.hass.async_add_executor_job(self.data.set_switch_state_on) await self.coordinator.async_refresh() async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" + self.check_lock_state() await self.hass.async_add_executor_job(self.data.set_switch_state_off) await self.coordinator.async_refresh() + + def check_lock_state(self) -> None: + """Raise an Error if manual switching via FRITZ!Box user interface is disabled.""" + if self.data.lock: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="manual_switching_disabled", + ) diff --git a/tests/components/fritzbox/__init__.py b/tests/components/fritzbox/__init__.py index 2bd8f26d73b..61312805e91 100644 --- a/tests/components/fritzbox/__init__.py +++ b/tests/components/fritzbox/__init__.py @@ -151,7 +151,7 @@ class FritzDeviceSwitchMock(FritzEntityBaseMock): has_thermostat = False has_blind = False switch_state = "fake_state" - lock = "fake_locked" + lock = False power = 5678 present = True temperature = 1.23 diff --git a/tests/components/fritzbox/test_switch.py b/tests/components/fritzbox/test_switch.py index 417b355b396..ba3b1de9b2f 100644 --- a/tests/components/fritzbox/test_switch.py +++ b/tests/components/fritzbox/test_switch.py @@ -3,6 +3,7 @@ from datetime import timedelta from unittest.mock import Mock +import pytest from requests.exceptions import HTTPError from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN @@ -29,6 +30,7 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er import homeassistant.util.dt as dt_util @@ -130,6 +132,7 @@ async def test_turn_on(hass: HomeAssistant, fritz: Mock) -> None: async def test_turn_off(hass: HomeAssistant, fritz: Mock) -> None: """Test turn device off.""" device = FritzDeviceSwitchMock() + assert await setup_config_entry( hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz ) @@ -137,9 +140,36 @@ async def test_turn_off(hass: HomeAssistant, fritz: Mock) -> None: await hass.services.async_call( DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True ) + assert device.set_switch_state_off.call_count == 1 +async def test_toggle_while_locked(hass: HomeAssistant, fritz: Mock) -> None: + """Test toggling while device is locked.""" + device = FritzDeviceSwitchMock() + device.lock = True + + assert await setup_config_entry( + hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz + ) + + with pytest.raises( + HomeAssistantError, + match="Can't toggle switch while manual switching is disabled for the device", + ): + await hass.services.async_call( + DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + + with pytest.raises( + HomeAssistantError, + match="Can't toggle switch while manual switching is disabled for the device", + ): + await hass.services.async_call( + DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + + async def test_update(hass: HomeAssistant, fritz: Mock) -> None: """Test update without error.""" device = FritzDeviceSwitchMock() From 3d164c672181a68a7b4e01a300ac6b5db54e452a Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Wed, 26 Jun 2024 18:15:53 +0200 Subject: [PATCH 004/146] Bump ZHA dependencies (#120581) --- homeassistant/components/zha/manifest.json | 8 ++++---- requirements_all.txt | 8 ++++---- requirements_test_all.txt | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index f517742f16f..7087ff0b2f0 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -23,12 +23,12 @@ "requirements": [ "bellows==0.39.1", "pyserial==3.5", - "zha-quirks==0.0.116", - "zigpy-deconz==0.23.1", + "zha-quirks==0.0.117", + "zigpy-deconz==0.23.2", "zigpy==0.64.1", "zigpy-xbee==0.20.1", - "zigpy-zigate==0.12.0", - "zigpy-znp==0.12.1", + "zigpy-zigate==0.12.1", + "zigpy-znp==0.12.2", "universal-silabs-flasher==0.0.20", "pyserial-asyncio-fast==0.11" ], diff --git a/requirements_all.txt b/requirements_all.txt index a3a62b58b4b..3aaec74c36f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2966,7 +2966,7 @@ zeroconf==0.132.2 zeversolar==0.3.1 # homeassistant.components.zha -zha-quirks==0.0.116 +zha-quirks==0.0.117 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.12 @@ -2975,16 +2975,16 @@ zhong-hong-hvac==1.0.12 ziggo-mediabox-xl==1.1.0 # homeassistant.components.zha -zigpy-deconz==0.23.1 +zigpy-deconz==0.23.2 # homeassistant.components.zha zigpy-xbee==0.20.1 # homeassistant.components.zha -zigpy-zigate==0.12.0 +zigpy-zigate==0.12.1 # homeassistant.components.zha -zigpy-znp==0.12.1 +zigpy-znp==0.12.2 # homeassistant.components.zha zigpy==0.64.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3f05bcc3d33..4b1777f4bae 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2319,19 +2319,19 @@ zeroconf==0.132.2 zeversolar==0.3.1 # homeassistant.components.zha -zha-quirks==0.0.116 +zha-quirks==0.0.117 # homeassistant.components.zha -zigpy-deconz==0.23.1 +zigpy-deconz==0.23.2 # homeassistant.components.zha zigpy-xbee==0.20.1 # homeassistant.components.zha -zigpy-zigate==0.12.0 +zigpy-zigate==0.12.1 # homeassistant.components.zha -zigpy-znp==0.12.1 +zigpy-znp==0.12.2 # homeassistant.components.zha zigpy==0.64.1 From d3d0e05817938d17c9b7a6095d8043c77d26908c Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Wed, 26 Jun 2024 19:19:28 +0300 Subject: [PATCH 005/146] Change Shelly connect task log message level to error (#120582) --- homeassistant/components/shelly/coordinator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index a4ff34f7d9a..02feef3633b 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -167,7 +167,7 @@ class ShellyCoordinatorBase[_DeviceT: BlockDevice | RpcDevice]( await self.device.initialize() update_device_fw_info(self.hass, self.device, self.entry) except DeviceConnectionError as err: - LOGGER.debug( + LOGGER.error( "Error connecting to Shelly device %s, error: %r", self.name, err ) return False From 1b45069620ed640cf13da880266e3fdff492cf50 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 26 Jun 2024 11:13:01 -0500 Subject: [PATCH 006/146] Bump intents to 2024.6.26 (#120584) Bump intents --- homeassistant/components/conversation/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/conversation/snapshots/test_init.ambr | 12 ++++++------ 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index ee0b29f22fc..2302d03bf4c 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/conversation", "integration_type": "system", "quality_scale": "internal", - "requirements": ["hassil==1.7.1", "home-assistant-intents==2024.6.21"] + "requirements": ["hassil==1.7.1", "home-assistant-intents==2024.6.26"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 18461d6398b..e42ef84d34c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -33,7 +33,7 @@ hass-nabucasa==0.81.1 hassil==1.7.1 home-assistant-bluetooth==1.12.2 home-assistant-frontend==20240626.0 -home-assistant-intents==2024.6.21 +home-assistant-intents==2024.6.26 httpx==0.27.0 ifaddr==0.2.0 Jinja2==3.1.4 diff --git a/requirements_all.txt b/requirements_all.txt index 3aaec74c36f..eca0100e5df 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1093,7 +1093,7 @@ holidays==0.51 home-assistant-frontend==20240626.0 # homeassistant.components.conversation -home-assistant-intents==2024.6.21 +home-assistant-intents==2024.6.26 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4b1777f4bae..bf8fd1dc081 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -898,7 +898,7 @@ holidays==0.51 home-assistant-frontend==20240626.0 # homeassistant.components.conversation -home-assistant-intents==2024.6.21 +home-assistant-intents==2024.6.26 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/tests/components/conversation/snapshots/test_init.ambr b/tests/components/conversation/snapshots/test_init.ambr index 403c72aaa10..6264e61863f 100644 --- a/tests/components/conversation/snapshots/test_init.ambr +++ b/tests/components/conversation/snapshots/test_init.ambr @@ -563,7 +563,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device called kitchen', + 'speech': 'Sorry, I am not aware of any device called kitchen light', }), }), }), @@ -703,7 +703,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device called late added', + 'speech': 'Sorry, I am not aware of any device called late added light', }), }), }), @@ -783,7 +783,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device called kitchen', + 'speech': 'Sorry, I am not aware of any device called kitchen light', }), }), }), @@ -803,7 +803,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device called my cool', + 'speech': 'Sorry, I am not aware of any device called my cool light', }), }), }), @@ -943,7 +943,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device called kitchen', + 'speech': 'Sorry, I am not aware of any device called kitchen light', }), }), }), @@ -993,7 +993,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device called renamed', + 'speech': 'Sorry, I am not aware of any device called renamed light', }), }), }), From b35442ed2de4abfe49aea2a54bb3c151c2fec755 Mon Sep 17 00:00:00 2001 From: Markus Jacobsen Date: Wed, 26 Jun 2024 21:46:59 +0200 Subject: [PATCH 007/146] Improve Bang & Olufsen error messages (#120587) * Convert logger messages to raised errors where applicable * Modify exception types * Improve deezer / tidal error message * Update homeassistant/components/bang_olufsen/strings.json Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com> * Update homeassistant/components/bang_olufsen/media_player.py Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com> --------- Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com> --- .../components/bang_olufsen/media_player.py | 41 ++++++++++++------- .../components/bang_olufsen/strings.json | 12 ++++++ 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/bang_olufsen/media_player.py b/homeassistant/components/bang_olufsen/media_player.py index d23c75046ff..0eff9f2bb85 100644 --- a/homeassistant/components/bang_olufsen/media_player.py +++ b/homeassistant/components/bang_olufsen/media_player.py @@ -45,7 +45,7 @@ from homeassistant.components.media_player import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_MODEL from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -316,7 +316,7 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity): @callback def _async_update_playback_error(self, data: PlaybackError) -> None: """Show playback error.""" - _LOGGER.error(data.error) + raise HomeAssistantError(data.error) @callback def _async_update_playback_progress(self, data: PlaybackProgress) -> None: @@ -516,7 +516,9 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity): self.async_write_ha_state() else: - _LOGGER.error("Seeking is currently only supported when using Deezer") + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="non_deezer_seeking" + ) async def async_media_previous_track(self) -> None: """Send the previous track command.""" @@ -529,12 +531,14 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity): async def async_select_source(self, source: str) -> None: """Select an input source.""" if source not in self._sources.values(): - _LOGGER.error( - "Invalid source: %s. Valid sources are: %s", - source, - list(self._sources.values()), + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="invalid_source", + translation_placeholders={ + "invalid_source": source, + "valid_sources": ",".join(list(self._sources.values())), + }, ) - return key = [x for x in self._sources if self._sources[x] == source][0] @@ -559,12 +563,14 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity): media_type = MediaType.MUSIC if media_type not in VALID_MEDIA_TYPES: - _LOGGER.error( - "%s is an invalid type. Valid values are: %s", - media_type, - VALID_MEDIA_TYPES, + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="invalid_media_type", + translation_placeholders={ + "invalid_media_type": media_type, + "valid_media_types": ",".join(VALID_MEDIA_TYPES), + }, ) - return if media_source.is_media_source_id(media_id): sourced_media = await media_source.async_resolve_media( @@ -681,7 +687,14 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity): ) except ApiException as error: - _LOGGER.error(json.loads(error.body)["message"]) + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="play_media_error", + translation_placeholders={ + "media_type": media_type, + "error_message": json.loads(error.body)["message"], + }, + ) from error async def async_browse_media( self, diff --git a/homeassistant/components/bang_olufsen/strings.json b/homeassistant/components/bang_olufsen/strings.json index 93b55cf0db2..cf5b212d424 100644 --- a/homeassistant/components/bang_olufsen/strings.json +++ b/homeassistant/components/bang_olufsen/strings.json @@ -28,6 +28,18 @@ "exceptions": { "m3u_invalid_format": { "message": "Media sources with the .m3u extension are not supported." + }, + "non_deezer_seeking": { + "message": "Seeking is currently only supported when using Deezer" + }, + "invalid_source": { + "message": "Invalid source: {invalid_source}. Valid sources are: {valid_sources}" + }, + "invalid_media_type": { + "message": "{invalid_media_type} is an invalid type. Valid values are: {valid_media_types}." + }, + "play_media_error": { + "message": "An error occurred while attempting to play {media_type}: {error_message}." } } } From 2e01e169ef96ad5ea9844ae6de1f6f8505d65827 Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 26 Jun 2024 20:55:25 +0200 Subject: [PATCH 008/146] Correct deprecation warning `async_register_static_paths` (#120592) --- homeassistant/components/http/__init__.py | 2 +- tests/components/http/test_init.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 38f0b628b2c..0d86ab57d3f 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -483,7 +483,7 @@ class HomeAssistantHTTP: frame.report( "calls hass.http.register_static_path which is deprecated because " "it does blocking I/O in the event loop, instead " - "call `await hass.http.async_register_static_path(" + "call `await hass.http.async_register_static_paths(" f'[StaticPathConfig("{url_path}", "{path}", {cache_headers})])`; ' "This function will be removed in 2025.7", exclude_integrations={"http"}, diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index 7a9fb329fcd..2895209b5f9 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -543,5 +543,5 @@ async def test_register_static_paths( "Detected code that calls hass.http.register_static_path " "which is deprecated because it does blocking I/O in the " "event loop, instead call " - "`await hass.http.async_register_static_path" + "`await hass.http.async_register_static_paths" ) in caplog.text From 80e70993c8a5a4694e38486ab389aae62208103e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 26 Jun 2024 20:55:41 +0200 Subject: [PATCH 009/146] Remove deprecated run_immediately flag from integration sensor (#120593) --- homeassistant/components/integration/sensor.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index 60cbee5549f..4fca92e9b40 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -446,7 +446,6 @@ class IntegrationSensor(RestoreSensor): event_filter=callback( lambda event_data: event_data["entity_id"] == self._sensor_source_id ), - run_immediately=True, ) ) self.async_on_remove( @@ -456,7 +455,6 @@ class IntegrationSensor(RestoreSensor): event_filter=callback( lambda event_data: event_data["entity_id"] == self._sensor_source_id ), - run_immediately=True, ) ) From b5c34808e6893646593f6e9deb94f43257229815 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Wed, 26 Jun 2024 21:35:23 +0300 Subject: [PATCH 010/146] Add last_error reporting to Shelly diagnostics (#120595) --- homeassistant/components/shelly/diagnostics.py | 10 ++++++++++ tests/components/shelly/test_diagnostics.py | 13 ++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/diagnostics.py b/homeassistant/components/shelly/diagnostics.py index db69abc8f55..e70b76a7c00 100644 --- a/homeassistant/components/shelly/diagnostics.py +++ b/homeassistant/components/shelly/diagnostics.py @@ -24,6 +24,8 @@ async def async_get_config_entry_diagnostics( device_settings: str | dict = "not initialized" device_status: str | dict = "not initialized" bluetooth: str | dict = "not initialized" + last_error: str = "not initialized" + if shelly_entry_data.block: block_coordinator = shelly_entry_data.block assert block_coordinator @@ -55,6 +57,10 @@ async def async_get_config_entry_diagnostics( "uptime", ] } + + if block_coordinator.device.last_error: + last_error = repr(block_coordinator.device.last_error) + else: rpc_coordinator = shelly_entry_data.rpc assert rpc_coordinator @@ -79,6 +85,9 @@ async def async_get_config_entry_diagnostics( "scanner": await scanner.async_diagnostics(), } + if rpc_coordinator.device.last_error: + last_error = repr(rpc_coordinator.device.last_error) + if isinstance(device_status, dict): device_status = async_redact_data(device_status, ["ssid"]) @@ -87,5 +96,6 @@ async def async_get_config_entry_diagnostics( "device_info": device_info, "device_settings": device_settings, "device_status": device_status, + "last_error": last_error, "bluetooth": bluetooth, } diff --git a/tests/components/shelly/test_diagnostics.py b/tests/components/shelly/test_diagnostics.py index f7f238f3327..4fc8ea6ca8f 100644 --- a/tests/components/shelly/test_diagnostics.py +++ b/tests/components/shelly/test_diagnostics.py @@ -1,9 +1,10 @@ """Tests for Shelly diagnostics platform.""" -from unittest.mock import ANY, Mock +from unittest.mock import ANY, Mock, PropertyMock from aioshelly.ble.const import BLE_SCAN_RESULT_EVENT from aioshelly.const import MODEL_25 +from aioshelly.exceptions import DeviceConnectionError import pytest from homeassistant.components.diagnostics import REDACTED @@ -36,6 +37,10 @@ async def test_block_config_entry_diagnostics( {key: REDACTED for key in TO_REDACT if key in entry_dict["data"]} ) + type(mock_block_device).last_error = PropertyMock( + return_value=DeviceConnectionError() + ) + result = await get_diagnostics_for_config_entry(hass, hass_client, entry) assert result == { @@ -48,6 +53,7 @@ async def test_block_config_entry_diagnostics( }, "device_settings": {"coiot": {"update_period": 15}}, "device_status": MOCK_STATUS_COAP, + "last_error": "DeviceConnectionError()", } @@ -91,6 +97,10 @@ async def test_rpc_config_entry_diagnostics( {key: REDACTED for key in TO_REDACT if key in entry_dict["data"]} ) + type(mock_rpc_device).last_error = PropertyMock( + return_value=DeviceConnectionError() + ) + result = await get_diagnostics_for_config_entry(hass, hass_client, entry) assert result == { @@ -152,4 +162,5 @@ async def test_rpc_config_entry_diagnostics( }, "wifi": {"rssi": -63}, }, + "last_error": "DeviceConnectionError()", } From da01635a075a2264f92afc2ba55a9c39b10fdcb9 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 26 Jun 2024 20:57:27 +0200 Subject: [PATCH 011/146] Prevent changes to mutable bmw_connected_drive fixture data (#120600) --- .../bmw_connected_drive/test_config_flow.py | 7 ++++--- tests/components/bmw_connected_drive/test_init.py | 14 +++++++++----- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/tests/components/bmw_connected_drive/test_config_flow.py b/tests/components/bmw_connected_drive/test_config_flow.py index b562e2b898f..3c7f452a011 100644 --- a/tests/components/bmw_connected_drive/test_config_flow.py +++ b/tests/components/bmw_connected_drive/test_config_flow.py @@ -92,7 +92,7 @@ async def test_api_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, - data=FIXTURE_USER_INPUT, + data=deepcopy(FIXTURE_USER_INPUT), ) assert result["type"] is FlowResultType.FORM @@ -116,7 +116,7 @@ async def test_full_user_flow_implementation(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, - data=FIXTURE_USER_INPUT, + data=deepcopy(FIXTURE_USER_INPUT), ) assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == FIXTURE_COMPLETE_ENTRY[CONF_USERNAME] @@ -137,7 +137,8 @@ async def test_options_flow_implementation(hass: HomeAssistant) -> None: return_value=True, ) as mock_setup_entry, ): - config_entry = MockConfigEntry(**FIXTURE_CONFIG_ENTRY) + config_entry_args = deepcopy(FIXTURE_CONFIG_ENTRY) + config_entry = MockConfigEntry(**config_entry_args) config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) diff --git a/tests/components/bmw_connected_drive/test_init.py b/tests/components/bmw_connected_drive/test_init.py index 52bc8a7ce05..5cd6362d6fa 100644 --- a/tests/components/bmw_connected_drive/test_init.py +++ b/tests/components/bmw_connected_drive/test_init.py @@ -1,5 +1,6 @@ """Test Axis component setup process.""" +from copy import deepcopy from unittest.mock import patch import pytest @@ -37,7 +38,7 @@ async def test_migrate_options( ) -> None: """Test successful migration of options.""" - config_entry = FIXTURE_CONFIG_ENTRY.copy() + config_entry = deepcopy(FIXTURE_CONFIG_ENTRY) config_entry["options"] = options mock_config_entry = MockConfigEntry(**config_entry) @@ -55,7 +56,7 @@ async def test_migrate_options( async def test_migrate_options_from_data(hass: HomeAssistant) -> None: """Test successful migration of options.""" - config_entry = FIXTURE_CONFIG_ENTRY.copy() + config_entry = deepcopy(FIXTURE_CONFIG_ENTRY) config_entry["options"] = {} config_entry["data"].update({CONF_READ_ONLY: False}) @@ -107,7 +108,8 @@ async def test_migrate_unique_ids( entity_registry: er.EntityRegistry, ) -> None: """Test successful migration of entity unique_ids.""" - mock_config_entry = MockConfigEntry(**FIXTURE_CONFIG_ENTRY) + confg_entry = deepcopy(FIXTURE_CONFIG_ENTRY) + mock_config_entry = MockConfigEntry(**confg_entry) mock_config_entry.add_to_hass(hass) entity: er.RegistryEntry = entity_registry.async_get_or_create( @@ -153,7 +155,8 @@ async def test_dont_migrate_unique_ids( entity_registry: er.EntityRegistry, ) -> None: """Test successful migration of entity unique_ids.""" - mock_config_entry = MockConfigEntry(**FIXTURE_CONFIG_ENTRY) + confg_entry = deepcopy(FIXTURE_CONFIG_ENTRY) + mock_config_entry = MockConfigEntry(**confg_entry) mock_config_entry.add_to_hass(hass) # create existing entry with new_unique_id @@ -196,7 +199,8 @@ async def test_remove_stale_devices( device_registry: dr.DeviceRegistry, ) -> None: """Test remove stale device registry entries.""" - mock_config_entry = MockConfigEntry(**FIXTURE_CONFIG_ENTRY) + config_entry = deepcopy(FIXTURE_CONFIG_ENTRY) + mock_config_entry = MockConfigEntry(**config_entry) mock_config_entry.add_to_hass(hass) device_registry.async_get_or_create( From 74204e2ee6be38b6a51f6c3fdd4a03f6411d9228 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 26 Jun 2024 21:30:30 +0200 Subject: [PATCH 012/146] Fix mqtt test fixture usage (#120602) --- tests/components/mqtt/test_config_flow.py | 2 +- tests/components/mqtt/test_image.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index 57975fdc309..457bd19c16f 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -1561,7 +1561,7 @@ async def test_setup_with_advanced_settings( } -@pytest.mark.usesfixtures("mock_ssl_context", "mock_process_uploaded_file") +@pytest.mark.usefixtures("mock_ssl_context", "mock_process_uploaded_file") async def test_change_websockets_transport_to_tcp( hass: HomeAssistant, mock_try_connection: MagicMock ) -> None: diff --git a/tests/components/mqtt/test_image.py b/tests/components/mqtt/test_image.py index 29109ee12f4..bb029fba231 100644 --- a/tests/components/mqtt/test_image.py +++ b/tests/components/mqtt/test_image.py @@ -499,7 +499,7 @@ async def test_image_from_url_fails( ), ], ) -@pytest.mark.usesfixtures("hass", "hass_client_no_auth") +@pytest.mark.usefixtures("hass", "hass_client_no_auth") async def test_image_config_fails( mqtt_mock_entry: MqttMockHAClientGenerator, caplog: pytest.LogCaptureFixture, From 242b3fa6099a23bbd409c987d95745ee8a9ab286 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 26 Jun 2024 22:05:30 +0200 Subject: [PATCH 013/146] Update adguardhome to 0.7.0 (#120605) --- homeassistant/components/adguard/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/adguard/manifest.json b/homeassistant/components/adguard/manifest.json index 52add51a663..f1b82177d5b 100644 --- a/homeassistant/components/adguard/manifest.json +++ b/homeassistant/components/adguard/manifest.json @@ -7,5 +7,5 @@ "integration_type": "service", "iot_class": "local_polling", "loggers": ["adguardhome"], - "requirements": ["adguardhome==0.6.3"] + "requirements": ["adguardhome==0.7.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index eca0100e5df..4e7d43ccdd2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -149,7 +149,7 @@ adb-shell[async]==0.4.4 adext==0.4.3 # homeassistant.components.adguard -adguardhome==0.6.3 +adguardhome==0.7.0 # homeassistant.components.advantage_air advantage-air==0.4.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bf8fd1dc081..2f0a793cc28 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -128,7 +128,7 @@ adb-shell[async]==0.4.4 adext==0.4.3 # homeassistant.components.adguard -adguardhome==0.6.3 +adguardhome==0.7.0 # homeassistant.components.advantage_air advantage-air==0.4.4 From 7d5d81b2298c394f946313e2082a51c60327ec4f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 26 Jun 2024 22:51:27 +0200 Subject: [PATCH 014/146] Bump version to 2024.7.0b1 --- 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 54d7f26a5f0..fe0989a54f8 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "0b0" +PATCH_VERSION: Final = "0b1" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 0b490d621a3..ea264a29fc6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.7.0b0" +version = "2024.7.0b1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From bea6fe30b86cd526bd12e159839c7e2535b996c3 Mon Sep 17 00:00:00 2001 From: Luca Angemi Date: Wed, 26 Jun 2024 23:45:47 +0200 Subject: [PATCH 015/146] Fix telegram bot thread_id key error (#120613) --- homeassistant/components/telegram_bot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index f37a84a83a6..fed9021a46e 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -702,7 +702,7 @@ class TelegramNotificationService: } if message_tag is not None: event_data[ATTR_MESSAGE_TAG] = message_tag - if kwargs_msg[ATTR_MESSAGE_THREAD_ID] is not None: + if kwargs_msg.get(ATTR_MESSAGE_THREAD_ID) is not None: event_data[ATTR_MESSAGE_THREAD_ID] = kwargs_msg[ ATTR_MESSAGE_THREAD_ID ] From 0701b0daa93b400a377f4d8c13f513eb25621fa3 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 26 Jun 2024 23:54:07 +0200 Subject: [PATCH 016/146] Update frontend to 20240626.2 (#120614) --- homeassistant/components/frontend/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/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 063f7db34a0..89c8fbe30ca 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20240626.0"] + "requirements": ["home-assistant-frontend==20240626.2"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e42ef84d34c..174de784eba 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ habluetooth==3.1.3 hass-nabucasa==0.81.1 hassil==1.7.1 home-assistant-bluetooth==1.12.2 -home-assistant-frontend==20240626.0 +home-assistant-frontend==20240626.2 home-assistant-intents==2024.6.26 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 4e7d43ccdd2..67ad67799e9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1090,7 +1090,7 @@ hole==0.8.0 holidays==0.51 # homeassistant.components.frontend -home-assistant-frontend==20240626.0 +home-assistant-frontend==20240626.2 # homeassistant.components.conversation home-assistant-intents==2024.6.26 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2f0a793cc28..350b59c0eab 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -895,7 +895,7 @@ hole==0.8.0 holidays==0.51 # homeassistant.components.frontend -home-assistant-frontend==20240626.0 +home-assistant-frontend==20240626.2 # homeassistant.components.conversation home-assistant-intents==2024.6.26 From 3da8d0a741d26cb9d38d1602bb9c6427e023a8f7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 26 Jun 2024 23:55:20 +0200 Subject: [PATCH 017/146] Bump version to 2024.7.0b2 --- 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 fe0989a54f8..8291fb93fd7 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "0b1" +PATCH_VERSION: Final = "0b2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index ea264a29fc6..709022534b1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.7.0b1" +version = "2024.7.0b2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 53e49861a1c9ee061a9a55cd358c05274f1845db Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 27 Jun 2024 23:26:38 +1200 Subject: [PATCH 018/146] Mark esphome integration as platinum (#112565) Co-authored-by: J. Nick Koston --- homeassistant/components/esphome/manifest.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index ab175028bea..6e30febd7db 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -15,6 +15,7 @@ "iot_class": "local_push", "loggers": ["aioesphomeapi", "noiseprotocol", "bleak_esphome"], "mqtt": ["esphome/discover/#"], + "quality_scale": "platinum", "requirements": [ "aioesphomeapi==24.6.1", "esphome-dashboard-api==1.2.3", From 2c2261254b45e074a62ddc3625428181a9d1ba61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexey=20ALERT=20Rubash=D1=91ff?= Date: Thu, 27 Jun 2024 23:05:58 +0300 Subject: [PATCH 019/146] Improve AtlanticDomesticHotWaterProductionMBLComponent support in Overkiz (#114178) * add overkiz AtlanticDHW support Adds support of Overkiz water heater entity selection based on device controllable_name Adds support of Atlantic water heater based on Atlantic Steatite Cube WI-FI VM 150 S4CS 2400W Adds more Overkiz water heater binary_sensors, numbers, and sensors * Changed class annotation * min_temp and max_temp as properties * reverted binary_sensors, number, sensor to make separate PRs * Update homeassistant/components/overkiz/water_heater_entities/atlantic_dhw.py Co-authored-by: Mick Vleeshouwer * Update homeassistant/components/overkiz/water_heater_entities/atlantic_dhw.py Co-authored-by: Mick Vleeshouwer * Update homeassistant/components/overkiz/water_heater_entities/atlantic_dhw.py Co-authored-by: Mick Vleeshouwer * Update homeassistant/components/overkiz/water_heater.py Co-authored-by: Mick Vleeshouwer * Update homeassistant/components/overkiz/water_heater_entities/atlantic_dhw.py Co-authored-by: Mick Vleeshouwer * Update homeassistant/components/overkiz/water_heater_entities/atlantic_dhw.py Co-authored-by: Mick Vleeshouwer * review fixes, typos, and pylint * review fix * review fix * ruff * temperature properties changed to constructor attributes * logger removed * constants usage consistency * redundant mapping removed * Update homeassistant/components/overkiz/water_heater_entities/atlantic_dhw.py Co-authored-by: Mick Vleeshouwer * boost mode method annotation typo * removed away mode for atlantic dwh * absence and boost mode attributes now support 'prog' state * heating status bugfix * electrical consumption sensor * warm water remaining volume sensor * away mode reintroduced * mypy check * boost plus state support * Update homeassistant/components/overkiz/sensor.py Co-authored-by: Mick Vleeshouwer * sensors reverted to separate them into their own PR * check away and boost modes on before switching them off * atlantic_dhw renamed to atlantic_domestic_hot_water_production * annotation changed * AtlanticDomesticHotWaterProductionMBLComponent file renamed, annotation change reverted --------- Co-authored-by: Mick Vleeshouwer --- .../components/overkiz/binary_sensor.py | 9 +- .../components/overkiz/water_heater.py | 29 ++- .../overkiz/water_heater_entities/__init__.py | 7 + ...stic_hot_water_production_mlb_component.py | 182 ++++++++++++++++++ 4 files changed, 216 insertions(+), 11 deletions(-) create mode 100644 homeassistant/components/overkiz/water_heater_entities/atlantic_domestic_hot_water_production_mlb_component.py diff --git a/homeassistant/components/overkiz/binary_sensor.py b/homeassistant/components/overkiz/binary_sensor.py index c37afc9cb0c..8ea86e03e8c 100644 --- a/homeassistant/components/overkiz/binary_sensor.py +++ b/homeassistant/components/overkiz/binary_sensor.py @@ -109,17 +109,20 @@ BINARY_SENSOR_DESCRIPTIONS: list[OverkizBinarySensorDescription] = [ key=OverkizState.CORE_HEATING_STATUS, name="Heating status", device_class=BinarySensorDeviceClass.HEAT, - value_fn=lambda state: state == OverkizCommandParam.ON, + value_fn=lambda state: cast(str, state).lower() + in (OverkizCommandParam.ON, OverkizCommandParam.HEATING), ), OverkizBinarySensorDescription( key=OverkizState.MODBUSLINK_DHW_ABSENCE_MODE, name="Absence mode", - value_fn=lambda state: state == OverkizCommandParam.ON, + value_fn=lambda state: state + in (OverkizCommandParam.ON, OverkizCommandParam.PROG), ), OverkizBinarySensorDescription( key=OverkizState.MODBUSLINK_DHW_BOOST_MODE, name="Boost mode", - value_fn=lambda state: state == OverkizCommandParam.ON, + value_fn=lambda state: state + in (OverkizCommandParam.ON, OverkizCommandParam.PROG), ), ] diff --git a/homeassistant/components/overkiz/water_heater.py b/homeassistant/components/overkiz/water_heater.py index c76f6d5099f..99bfb279e4c 100644 --- a/homeassistant/components/overkiz/water_heater.py +++ b/homeassistant/components/overkiz/water_heater.py @@ -9,7 +9,11 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import HomeAssistantOverkizData from .const import DOMAIN -from .water_heater_entities import WIDGET_TO_WATER_HEATER_ENTITY +from .entity import OverkizEntity +from .water_heater_entities import ( + CONTROLLABLE_NAME_TO_WATER_HEATER_ENTITY, + WIDGET_TO_WATER_HEATER_ENTITY, +) async def async_setup_entry( @@ -19,11 +23,20 @@ async def async_setup_entry( ) -> None: """Set up the Overkiz DHW from a config entry.""" data: HomeAssistantOverkizData = hass.data[DOMAIN][entry.entry_id] + entities: list[OverkizEntity] = [] - async_add_entities( - WIDGET_TO_WATER_HEATER_ENTITY[device.widget]( - device.device_url, data.coordinator - ) - for device in data.platforms[Platform.WATER_HEATER] - if device.widget in WIDGET_TO_WATER_HEATER_ENTITY - ) + for device in data.platforms[Platform.WATER_HEATER]: + if device.controllable_name in CONTROLLABLE_NAME_TO_WATER_HEATER_ENTITY: + entities.append( + CONTROLLABLE_NAME_TO_WATER_HEATER_ENTITY[device.controllable_name]( + device.device_url, data.coordinator + ) + ) + elif device.widget in WIDGET_TO_WATER_HEATER_ENTITY: + entities.append( + WIDGET_TO_WATER_HEATER_ENTITY[device.widget]( + device.device_url, data.coordinator + ) + ) + + async_add_entities(entities) diff --git a/homeassistant/components/overkiz/water_heater_entities/__init__.py b/homeassistant/components/overkiz/water_heater_entities/__init__.py index 6f6539ef659..fdc41f213c6 100644 --- a/homeassistant/components/overkiz/water_heater_entities/__init__.py +++ b/homeassistant/components/overkiz/water_heater_entities/__init__.py @@ -2,6 +2,9 @@ from pyoverkiz.enums.ui import UIWidget +from .atlantic_domestic_hot_water_production_mlb_component import ( + AtlanticDomesticHotWaterProductionMBLComponent, +) from .atlantic_pass_apc_dhw import AtlanticPassAPCDHW from .domestic_hot_water_production import DomesticHotWaterProduction from .hitachi_dhw import HitachiDHW @@ -11,3 +14,7 @@ WIDGET_TO_WATER_HEATER_ENTITY = { UIWidget.DOMESTIC_HOT_WATER_PRODUCTION: DomesticHotWaterProduction, UIWidget.HITACHI_DHW: HitachiDHW, } + +CONTROLLABLE_NAME_TO_WATER_HEATER_ENTITY = { + "modbuslink:AtlanticDomesticHotWaterProductionMBLComponent": AtlanticDomesticHotWaterProductionMBLComponent, +} diff --git a/homeassistant/components/overkiz/water_heater_entities/atlantic_domestic_hot_water_production_mlb_component.py b/homeassistant/components/overkiz/water_heater_entities/atlantic_domestic_hot_water_production_mlb_component.py new file mode 100644 index 00000000000..de995a2bd1a --- /dev/null +++ b/homeassistant/components/overkiz/water_heater_entities/atlantic_domestic_hot_water_production_mlb_component.py @@ -0,0 +1,182 @@ +"""Support for AtlanticDomesticHotWaterProductionMBLComponent.""" + +from typing import Any, cast + +from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState + +from homeassistant.components.water_heater import ( + STATE_ECO, + STATE_OFF, + STATE_PERFORMANCE, + WaterHeaterEntity, + WaterHeaterEntityFeature, +) +from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature + +from .. import OverkizDataUpdateCoordinator +from ..entity import OverkizEntity + + +class AtlanticDomesticHotWaterProductionMBLComponent(OverkizEntity, WaterHeaterEntity): + """Representation of AtlanticDomesticHotWaterProductionMBLComponent (modbuslink).""" + + _attr_temperature_unit = UnitOfTemperature.CELSIUS + _attr_supported_features = ( + WaterHeaterEntityFeature.TARGET_TEMPERATURE + | WaterHeaterEntityFeature.OPERATION_MODE + | WaterHeaterEntityFeature.AWAY_MODE + | WaterHeaterEntityFeature.ON_OFF + ) + _attr_operation_list = [ + OverkizCommandParam.PERFORMANCE, + OverkizCommandParam.ECO, + OverkizCommandParam.MANUAL, + ] + + def __init__( + self, device_url: str, coordinator: OverkizDataUpdateCoordinator + ) -> None: + """Init method.""" + super().__init__(device_url, coordinator) + self._attr_max_temp = cast( + float, + self.executor.select_state( + OverkizState.CORE_MAXIMAL_TEMPERATURE_MANUAL_MODE + ), + ) + self._attr_min_temp = cast( + float, + self.executor.select_state( + OverkizState.CORE_MINIMAL_TEMPERATURE_MANUAL_MODE + ), + ) + + @property + def current_temperature(self) -> float: + """Return the current temperature.""" + return cast( + float, + self.executor.select_state( + OverkizState.MODBUSLINK_MIDDLE_WATER_TEMPERATURE + ), + ) + + @property + def target_temperature(self) -> float: + """Return the temperature corresponding to the PRESET.""" + return cast( + float, + self.executor.select_state(OverkizState.CORE_WATER_TARGET_TEMPERATURE), + ) + + async def async_set_temperature(self, **kwargs: Any) -> None: + """Set new temperature.""" + temperature = kwargs[ATTR_TEMPERATURE] + await self.executor.async_execute_command( + OverkizCommand.SET_TARGET_DHW_TEMPERATURE, temperature + ) + + @property + def is_boost_mode_on(self) -> bool: + """Return true if boost mode is on.""" + return self.executor.select_state(OverkizState.MODBUSLINK_DHW_BOOST_MODE) in ( + OverkizCommandParam.ON, + OverkizCommandParam.PROG, + ) + + @property + def is_eco_mode_on(self) -> bool: + """Return true if eco mode is on.""" + return self.executor.select_state(OverkizState.MODBUSLINK_DHW_MODE) in ( + OverkizCommandParam.MANUAL_ECO_ACTIVE, + OverkizCommandParam.AUTO_MODE, + ) + + @property + def is_away_mode_on(self) -> bool: + """Return true if away mode is on.""" + return ( + self.executor.select_state(OverkizState.MODBUSLINK_DHW_ABSENCE_MODE) + == OverkizCommandParam.ON + ) + + @property + def current_operation(self) -> str: + """Return current operation.""" + if self.is_away_mode_on: + return STATE_OFF + + if self.is_boost_mode_on: + return STATE_PERFORMANCE + + if self.is_eco_mode_on: + return STATE_ECO + + if ( + cast(str, self.executor.select_state(OverkizState.MODBUSLINK_DHW_MODE)) + == OverkizCommandParam.MANUAL_ECO_INACTIVE + ): + return OverkizCommandParam.MANUAL + + return STATE_OFF + + async def async_set_operation_mode(self, operation_mode: str) -> None: + """Set new operation mode.""" + if operation_mode in (STATE_PERFORMANCE, OverkizCommandParam.BOOST): + if self.is_away_mode_on: + await self.async_turn_away_mode_off() + await self.async_turn_boost_mode_on() + elif operation_mode in ( + OverkizCommandParam.ECO, + OverkizCommandParam.MANUAL_ECO_ACTIVE, + ): + if self.is_away_mode_on: + await self.async_turn_away_mode_off() + if self.is_boost_mode_on: + await self.async_turn_boost_mode_off() + await self.executor.async_execute_command( + OverkizCommand.SET_DHW_MODE, OverkizCommandParam.AUTO_MODE + ) + elif operation_mode in ( + OverkizCommandParam.MANUAL, + OverkizCommandParam.MANUAL_ECO_INACTIVE, + ): + if self.is_away_mode_on: + await self.async_turn_away_mode_off() + if self.is_boost_mode_on: + await self.async_turn_boost_mode_off() + await self.executor.async_execute_command( + OverkizCommand.SET_DHW_MODE, OverkizCommandParam.MANUAL_ECO_INACTIVE + ) + else: + if self.is_away_mode_on: + await self.async_turn_away_mode_off() + if self.is_boost_mode_on: + await self.async_turn_boost_mode_off() + await self.executor.async_execute_command( + OverkizCommand.SET_DHW_MODE, operation_mode + ) + + async def async_turn_away_mode_on(self) -> None: + """Turn away mode on.""" + await self.executor.async_execute_command( + OverkizCommand.SET_ABSENCE_MODE, OverkizCommandParam.ON + ) + + async def async_turn_away_mode_off(self) -> None: + """Turn away mode off.""" + await self.executor.async_execute_command( + OverkizCommand.SET_ABSENCE_MODE, OverkizCommandParam.OFF + ) + + async def async_turn_boost_mode_on(self) -> None: + """Turn boost mode on.""" + await self.executor.async_execute_command( + OverkizCommand.SET_BOOST_MODE, OverkizCommandParam.ON + ) + + async def async_turn_boost_mode_off(self) -> None: + """Turn boost mode off.""" + await self.executor.async_execute_command( + OverkizCommand.SET_BOOST_MODE, OverkizCommandParam.OFF + ) From dcffd6bd7ae0a91a01bc2758462b89603b4cae99 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 26 Jun 2024 20:37:08 -0500 Subject: [PATCH 020/146] Remove unused fields from unifiprotect event sensors (#120568) --- homeassistant/components/unifiprotect/binary_sensor.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/unifiprotect/binary_sensor.py b/homeassistant/components/unifiprotect/binary_sensor.py index e35eb6f48f3..c4e1aa87df2 100644 --- a/homeassistant/components/unifiprotect/binary_sensor.py +++ b/homeassistant/components/unifiprotect/binary_sensor.py @@ -426,14 +426,12 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( device_class=BinarySensorDeviceClass.OCCUPANCY, icon="mdi:doorbell-video", ufp_required_field="feature_flags.is_doorbell", - ufp_value="is_ringing", ufp_event_obj="last_ring_event", ), ProtectBinaryEventEntityDescription( key="motion", name="Motion", device_class=BinarySensorDeviceClass.MOTION, - ufp_value="is_motion_currently_detected", ufp_enabled="is_motion_detection_on", ufp_event_obj="last_motion_event", ), From 210e906a4da8d8f950bdcb2ba2cf7d7d8a980267 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Thu, 27 Jun 2024 13:34:12 +0100 Subject: [PATCH 021/146] Store tplink credentials_hash outside of device_config (#120597) --- homeassistant/components/tplink/__init__.py | 42 +++- .../components/tplink/config_flow.py | 43 +++- homeassistant/components/tplink/const.py | 2 + tests/components/tplink/__init__.py | 19 +- tests/components/tplink/test_config_flow.py | 81 ++++++- tests/components/tplink/test_init.py | 217 +++++++++++++++++- 6 files changed, 373 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index 764867f0bee..6d300f68aa0 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -43,6 +43,7 @@ from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType from .const import ( + CONF_CREDENTIALS_HASH, CONF_DEVICE_CONFIG, CONNECT_TIMEOUT, DISCOVERY_TIMEOUT, @@ -73,6 +74,7 @@ def async_trigger_discovery( discovered_devices: dict[str, Device], ) -> None: """Trigger config flows for discovered devices.""" + for formatted_mac, device in discovered_devices.items(): discovery_flow.async_create_flow( hass, @@ -83,7 +85,6 @@ def async_trigger_discovery( CONF_HOST: device.host, CONF_MAC: formatted_mac, CONF_DEVICE_CONFIG: device.config.to_dict( - credentials_hash=device.credentials_hash, exclude_credentials=True, ), }, @@ -133,6 +134,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: TPLinkConfigEntry) -> bo """Set up TPLink from a config entry.""" host: str = entry.data[CONF_HOST] credentials = await get_credentials(hass) + entry_credentials_hash = entry.data.get(CONF_CREDENTIALS_HASH) config: DeviceConfig | None = None if config_dict := entry.data.get(CONF_DEVICE_CONFIG): @@ -151,19 +153,31 @@ async def async_setup_entry(hass: HomeAssistant, entry: TPLinkConfigEntry) -> bo config.timeout = CONNECT_TIMEOUT if config.uses_http is True: config.http_client = create_async_tplink_clientsession(hass) + + # If we have in memory credentials use them otherwise check for credentials_hash if credentials: config.credentials = credentials + elif entry_credentials_hash: + config.credentials_hash = entry_credentials_hash + try: device: Device = await Device.connect(config=config) except AuthenticationError as ex: + # If the stored credentials_hash was used but doesn't work remove it + if not credentials and entry_credentials_hash: + data = {k: v for k, v in entry.data.items() if k != CONF_CREDENTIALS_HASH} + hass.config_entries.async_update_entry(entry, data=data) raise ConfigEntryAuthFailed from ex except KasaException as ex: raise ConfigEntryNotReady from ex - device_config_dict = device.config.to_dict( - credentials_hash=device.credentials_hash, exclude_credentials=True - ) + device_credentials_hash = device.credentials_hash + device_config_dict = device.config.to_dict(exclude_credentials=True) + # Do not store the credentials hash inside the device_config + device_config_dict.pop(CONF_CREDENTIALS_HASH, None) updates: dict[str, Any] = {} + if device_credentials_hash and device_credentials_hash != entry_credentials_hash: + updates[CONF_CREDENTIALS_HASH] = device_credentials_hash if device_config_dict != config_dict: updates[CONF_DEVICE_CONFIG] = device_config_dict if entry.data.get(CONF_ALIAS) != device.alias: @@ -326,7 +340,25 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> minor_version = 3 hass.config_entries.async_update_entry(config_entry, minor_version=3) - _LOGGER.debug("Migration to version %s.%s successful", version, minor_version) + if version == 1 and minor_version == 3: + # credentials_hash stored in the device_config should be moved to data. + updates: dict[str, Any] = {} + if config_dict := config_entry.data.get(CONF_DEVICE_CONFIG): + assert isinstance(config_dict, dict) + if credentials_hash := config_dict.pop(CONF_CREDENTIALS_HASH, None): + updates[CONF_CREDENTIALS_HASH] = credentials_hash + updates[CONF_DEVICE_CONFIG] = config_dict + minor_version = 4 + hass.config_entries.async_update_entry( + config_entry, + data={ + **config_entry.data, + **updates, + }, + minor_version=minor_version, + ) + _LOGGER.debug("Migration to version %s.%s complete", version, minor_version) + return True diff --git a/homeassistant/components/tplink/config_flow.py b/homeassistant/components/tplink/config_flow.py index 7bead2207a3..5608ccfa72f 100644 --- a/homeassistant/components/tplink/config_flow.py +++ b/homeassistant/components/tplink/config_flow.py @@ -44,7 +44,13 @@ from . import ( mac_alias, set_credentials, ) -from .const import CONF_DEVICE_CONFIG, CONNECT_TIMEOUT, DOMAIN +from .const import ( + CONF_CONNECTION_TYPE, + CONF_CREDENTIALS_HASH, + CONF_DEVICE_CONFIG, + CONNECT_TIMEOUT, + DOMAIN, +) STEP_AUTH_DATA_SCHEMA = vol.Schema( {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} @@ -55,7 +61,7 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for tplink.""" VERSION = 1 - MINOR_VERSION = 3 + MINOR_VERSION = 4 reauth_entry: ConfigEntry | None = None def __init__(self) -> None: @@ -95,9 +101,18 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): entry_config_dict = entry_data.get(CONF_DEVICE_CONFIG) if entry_config_dict == config and entry_data[CONF_HOST] == host: return None + updates = {**entry.data, CONF_DEVICE_CONFIG: config, CONF_HOST: host} + # If the connection parameters have changed the credentials_hash will be invalid. + if ( + entry_config_dict + and isinstance(entry_config_dict, dict) + and entry_config_dict.get(CONF_CONNECTION_TYPE) + != config.get(CONF_CONNECTION_TYPE) + ): + updates.pop(CONF_CREDENTIALS_HASH, None) return self.async_update_reload_and_abort( entry, - data={**entry.data, CONF_DEVICE_CONFIG: config, CONF_HOST: host}, + data=updates, reason="already_configured", ) @@ -345,18 +360,22 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): @callback def _async_create_entry_from_device(self, device: Device) -> ConfigFlowResult: """Create a config entry from a smart device.""" + # This is only ever called after a successful device update so we know that + # the credential_hash is correct and should be saved. self._abort_if_unique_id_configured(updates={CONF_HOST: device.host}) + data = { + CONF_HOST: device.host, + CONF_ALIAS: device.alias, + CONF_MODEL: device.model, + CONF_DEVICE_CONFIG: device.config.to_dict( + exclude_credentials=True, + ), + } + if device.credentials_hash: + data[CONF_CREDENTIALS_HASH] = device.credentials_hash return self.async_create_entry( title=f"{device.alias} {device.model}", - data={ - CONF_HOST: device.host, - CONF_ALIAS: device.alias, - CONF_MODEL: device.model, - CONF_DEVICE_CONFIG: device.config.to_dict( - credentials_hash=device.credentials_hash, - exclude_credentials=True, - ), - }, + data=data, ) async def _async_try_discover_and_update( diff --git a/homeassistant/components/tplink/const.py b/homeassistant/components/tplink/const.py index d77d415aa9c..babd92e2c34 100644 --- a/homeassistant/components/tplink/const.py +++ b/homeassistant/components/tplink/const.py @@ -20,6 +20,8 @@ ATTR_TODAY_ENERGY_KWH: Final = "today_energy_kwh" ATTR_TOTAL_ENERGY_KWH: Final = "total_energy_kwh" CONF_DEVICE_CONFIG: Final = "device_config" +CONF_CREDENTIALS_HASH: Final = "credentials_hash" +CONF_CONNECTION_TYPE: Final = "connection_type" PLATFORMS: Final = [ Platform.BINARY_SENSOR, diff --git a/tests/components/tplink/__init__.py b/tests/components/tplink/__init__.py index 9c8aeb99be1..b3092d62904 100644 --- a/tests/components/tplink/__init__.py +++ b/tests/components/tplink/__init__.py @@ -22,6 +22,7 @@ from syrupy import SnapshotAssertion from homeassistant.components.tplink import ( CONF_ALIAS, + CONF_CREDENTIALS_HASH, CONF_DEVICE_CONFIG, CONF_HOST, CONF_MODEL, @@ -53,9 +54,7 @@ MAC_ADDRESS2 = "11:22:33:44:55:66" DEFAULT_ENTRY_TITLE = f"{ALIAS} {MODEL}" CREDENTIALS_HASH_LEGACY = "" DEVICE_CONFIG_LEGACY = DeviceConfig(IP_ADDRESS) -DEVICE_CONFIG_DICT_LEGACY = DEVICE_CONFIG_LEGACY.to_dict( - credentials_hash=CREDENTIALS_HASH_LEGACY, exclude_credentials=True -) +DEVICE_CONFIG_DICT_LEGACY = DEVICE_CONFIG_LEGACY.to_dict(exclude_credentials=True) CREDENTIALS = Credentials("foo", "bar") CREDENTIALS_HASH_AUTH = "abcdefghijklmnopqrstuv==" DEVICE_CONFIG_AUTH = DeviceConfig( @@ -74,12 +73,8 @@ DEVICE_CONFIG_AUTH2 = DeviceConfig( ), uses_http=True, ) -DEVICE_CONFIG_DICT_AUTH = DEVICE_CONFIG_AUTH.to_dict( - credentials_hash=CREDENTIALS_HASH_AUTH, exclude_credentials=True -) -DEVICE_CONFIG_DICT_AUTH2 = DEVICE_CONFIG_AUTH2.to_dict( - credentials_hash=CREDENTIALS_HASH_AUTH, exclude_credentials=True -) +DEVICE_CONFIG_DICT_AUTH = DEVICE_CONFIG_AUTH.to_dict(exclude_credentials=True) +DEVICE_CONFIG_DICT_AUTH2 = DEVICE_CONFIG_AUTH2.to_dict(exclude_credentials=True) CREATE_ENTRY_DATA_LEGACY = { CONF_HOST: IP_ADDRESS, @@ -92,14 +87,20 @@ CREATE_ENTRY_DATA_AUTH = { CONF_HOST: IP_ADDRESS, CONF_ALIAS: ALIAS, CONF_MODEL: MODEL, + CONF_CREDENTIALS_HASH: CREDENTIALS_HASH_AUTH, CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_AUTH, } CREATE_ENTRY_DATA_AUTH2 = { CONF_HOST: IP_ADDRESS2, CONF_ALIAS: ALIAS, CONF_MODEL: MODEL, + CONF_CREDENTIALS_HASH: CREDENTIALS_HASH_AUTH, CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_AUTH2, } +NEW_CONNECTION_TYPE = DeviceConnectionParameters( + DeviceFamily.IotSmartPlugSwitch, DeviceEncryptionType.Aes +) +NEW_CONNECTION_TYPE_DICT = NEW_CONNECTION_TYPE.to_dict() def _load_feature_fixtures(): diff --git a/tests/components/tplink/test_config_flow.py b/tests/components/tplink/test_config_flow.py index 7560ff4a72d..e9ae7957520 100644 --- a/tests/components/tplink/test_config_flow.py +++ b/tests/components/tplink/test_config_flow.py @@ -14,8 +14,12 @@ from homeassistant.components.tplink import ( DeviceConfig, KasaException, ) -from homeassistant.components.tplink.const import CONF_DEVICE_CONFIG -from homeassistant.config_entries import ConfigEntryState +from homeassistant.components.tplink.const import ( + CONF_CONNECTION_TYPE, + CONF_CREDENTIALS_HASH, + CONF_DEVICE_CONFIG, +) +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.const import ( CONF_ALIAS, CONF_DEVICE, @@ -32,6 +36,7 @@ from . import ( CREATE_ENTRY_DATA_AUTH, CREATE_ENTRY_DATA_AUTH2, CREATE_ENTRY_DATA_LEGACY, + CREDENTIALS_HASH_AUTH, DEFAULT_ENTRY_TITLE, DEVICE_CONFIG_DICT_AUTH, DEVICE_CONFIG_DICT_LEGACY, @@ -40,6 +45,7 @@ from . import ( MAC_ADDRESS, MAC_ADDRESS2, MODULE, + NEW_CONNECTION_TYPE_DICT, _mocked_device, _patch_connect, _patch_discovery, @@ -811,6 +817,77 @@ async def test_integration_discovery_with_ip_change( mock_connect["connect"].assert_awaited_once_with(config=config) +async def test_integration_discovery_with_connection_change( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_discovery: AsyncMock, + mock_connect: AsyncMock, +) -> None: + """Test that config entry is updated with new device config. + + And that connection_hash is removed as it will be invalid. + """ + mock_connect["connect"].side_effect = KasaException() + + mock_config_entry = MockConfigEntry( + title="TPLink", + domain=DOMAIN, + data=CREATE_ENTRY_DATA_AUTH, + unique_id=MAC_ADDRESS, + ) + mock_config_entry.add_to_hass(hass) + with patch("homeassistant.components.tplink.Discover.discover", return_value={}): + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done(wait_background_tasks=True) + + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + assert ( + len( + hass.config_entries.flow.async_progress_by_handler( + DOMAIN, match_context={"source": SOURCE_REAUTH} + ) + ) + == 0 + ) + assert mock_config_entry.data[CONF_DEVICE_CONFIG] == DEVICE_CONFIG_DICT_AUTH + assert mock_config_entry.data[CONF_DEVICE_CONFIG].get(CONF_HOST) == "127.0.0.1" + assert mock_config_entry.data[CONF_CREDENTIALS_HASH] == CREDENTIALS_HASH_AUTH + + NEW_DEVICE_CONFIG = { + **DEVICE_CONFIG_DICT_AUTH, + CONF_CONNECTION_TYPE: NEW_CONNECTION_TYPE_DICT, + } + config = DeviceConfig.from_dict(NEW_DEVICE_CONFIG) + # Reset the connect mock so when the config flow reloads the entry it succeeds + mock_connect["connect"].reset_mock(side_effect=True) + bulb = _mocked_device( + device_config=config, + mac=mock_config_entry.unique_id, + ) + mock_connect["connect"].return_value = bulb + + discovery_result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={ + CONF_HOST: "127.0.0.1", + CONF_MAC: MAC_ADDRESS, + CONF_ALIAS: ALIAS, + CONF_DEVICE_CONFIG: NEW_DEVICE_CONFIG, + }, + ) + await hass.async_block_till_done(wait_background_tasks=True) + assert discovery_result["type"] is FlowResultType.ABORT + assert discovery_result["reason"] == "already_configured" + assert mock_config_entry.data[CONF_DEVICE_CONFIG] == NEW_DEVICE_CONFIG + assert mock_config_entry.data[CONF_HOST] == "127.0.0.1" + assert CREDENTIALS_HASH_AUTH not in mock_config_entry.data + + assert mock_config_entry.state is ConfigEntryState.LOADED + + mock_connect["connect"].assert_awaited_once_with(config=config) + + async def test_dhcp_discovery_with_ip_change( hass: HomeAssistant, mock_config_entry: MockConfigEntry, diff --git a/tests/components/tplink/test_init.py b/tests/components/tplink/test_init.py index 61ec9decc10..bfb7e02b63d 100644 --- a/tests/components/tplink/test_init.py +++ b/tests/components/tplink/test_init.py @@ -7,12 +7,16 @@ from datetime import timedelta from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch from freezegun.api import FrozenDateTimeFactory -from kasa import AuthenticationError, Feature, KasaException, Module +from kasa import AuthenticationError, DeviceConfig, Feature, KasaException, Module import pytest from homeassistant import setup from homeassistant.components import tplink -from homeassistant.components.tplink.const import CONF_DEVICE_CONFIG, DOMAIN +from homeassistant.components.tplink.const import ( + CONF_CREDENTIALS_HASH, + CONF_DEVICE_CONFIG, + DOMAIN, +) from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.const import ( CONF_AUTHENTICATION, @@ -458,7 +462,214 @@ async def test_unlink_devices( expected_identifiers = identifiers[:expected_count] assert device_entries[0].identifiers == set(expected_identifiers) assert entry.version == 1 - assert entry.minor_version == 3 + assert entry.minor_version == 4 msg = f"{expected_message} identifiers for device dummy (hs300): {set(identifiers)}" assert msg in caplog.text + + +async def test_move_credentials_hash( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test credentials hash moved to parent. + + As async_setup_entry will succeed the hash on the parent is updated + from the device. + """ + device_config = { + **DEVICE_CONFIG_AUTH.to_dict( + exclude_credentials=True, credentials_hash="theHash" + ) + } + entry_data = {**CREATE_ENTRY_DATA_AUTH, CONF_DEVICE_CONFIG: device_config} + + entry = MockConfigEntry( + title="TPLink", + domain=DOMAIN, + data=entry_data, + entry_id="123456", + unique_id=MAC_ADDRESS, + version=1, + minor_version=3, + ) + assert entry.data[CONF_DEVICE_CONFIG][CONF_CREDENTIALS_HASH] == "theHash" + entry.add_to_hass(hass) + + async def _connect(config): + config.credentials_hash = "theNewHash" + return _mocked_device(device_config=config, credentials_hash="theNewHash") + + with ( + patch("homeassistant.components.tplink.Device.connect", new=_connect), + patch("homeassistant.components.tplink.PLATFORMS", []), + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.minor_version == 4 + assert entry.state is ConfigEntryState.LOADED + assert CONF_CREDENTIALS_HASH not in entry.data[CONF_DEVICE_CONFIG] + assert CONF_CREDENTIALS_HASH in entry.data + # Gets the new hash from the successful connection. + assert entry.data[CONF_CREDENTIALS_HASH] == "theNewHash" + assert "Migration to version 1.4 complete" in caplog.text + + +async def test_move_credentials_hash_auth_error( + hass: HomeAssistant, +) -> None: + """Test credentials hash moved to parent. + + If there is an auth error it should be deleted after migration + in async_setup_entry. + """ + device_config = { + **DEVICE_CONFIG_AUTH.to_dict( + exclude_credentials=True, credentials_hash="theHash" + ) + } + entry_data = {**CREATE_ENTRY_DATA_AUTH, CONF_DEVICE_CONFIG: device_config} + + entry = MockConfigEntry( + title="TPLink", + domain=DOMAIN, + data=entry_data, + unique_id=MAC_ADDRESS, + version=1, + minor_version=3, + ) + assert entry.data[CONF_DEVICE_CONFIG][CONF_CREDENTIALS_HASH] == "theHash" + + with ( + patch( + "homeassistant.components.tplink.Device.connect", + side_effect=AuthenticationError, + ), + patch("homeassistant.components.tplink.PLATFORMS", []), + ): + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.minor_version == 4 + assert entry.state is ConfigEntryState.SETUP_ERROR + assert CONF_CREDENTIALS_HASH not in entry.data[CONF_DEVICE_CONFIG] + # Auth failure deletes the hash + assert CONF_CREDENTIALS_HASH not in entry.data + + +async def test_move_credentials_hash_other_error( + hass: HomeAssistant, +) -> None: + """Test credentials hash moved to parent. + + When there is a KasaException the same hash should still be on the parent + at the end of the test. + """ + device_config = { + **DEVICE_CONFIG_AUTH.to_dict( + exclude_credentials=True, credentials_hash="theHash" + ) + } + entry_data = {**CREATE_ENTRY_DATA_AUTH, CONF_DEVICE_CONFIG: device_config} + + entry = MockConfigEntry( + title="TPLink", + domain=DOMAIN, + data=entry_data, + unique_id=MAC_ADDRESS, + version=1, + minor_version=3, + ) + assert entry.data[CONF_DEVICE_CONFIG][CONF_CREDENTIALS_HASH] == "theHash" + + with ( + patch( + "homeassistant.components.tplink.Device.connect", side_effect=KasaException + ), + patch("homeassistant.components.tplink.PLATFORMS", []), + ): + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.minor_version == 4 + assert entry.state is ConfigEntryState.SETUP_RETRY + assert CONF_CREDENTIALS_HASH not in entry.data[CONF_DEVICE_CONFIG] + assert CONF_CREDENTIALS_HASH in entry.data + assert entry.data[CONF_CREDENTIALS_HASH] == "theHash" + + +async def test_credentials_hash( + hass: HomeAssistant, +) -> None: + """Test credentials_hash used to call connect.""" + device_config = {**DEVICE_CONFIG_AUTH.to_dict(exclude_credentials=True)} + entry_data = { + **CREATE_ENTRY_DATA_AUTH, + CONF_DEVICE_CONFIG: device_config, + CONF_CREDENTIALS_HASH: "theHash", + } + + entry = MockConfigEntry( + title="TPLink", + domain=DOMAIN, + data=entry_data, + unique_id=MAC_ADDRESS, + ) + + async def _connect(config): + config.credentials_hash = "theHash" + return _mocked_device(device_config=config, credentials_hash="theHash") + + with ( + patch("homeassistant.components.tplink.PLATFORMS", []), + patch("homeassistant.components.tplink.Device.connect", new=_connect), + ): + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.LOADED + assert CONF_CREDENTIALS_HASH not in entry.data[CONF_DEVICE_CONFIG] + assert CONF_CREDENTIALS_HASH in entry.data + assert entry.data[CONF_DEVICE_CONFIG] == device_config + assert entry.data[CONF_CREDENTIALS_HASH] == "theHash" + + +async def test_credentials_hash_auth_error( + hass: HomeAssistant, +) -> None: + """Test credentials_hash is deleted after an auth failure.""" + device_config = {**DEVICE_CONFIG_AUTH.to_dict(exclude_credentials=True)} + entry_data = { + **CREATE_ENTRY_DATA_AUTH, + CONF_DEVICE_CONFIG: device_config, + CONF_CREDENTIALS_HASH: "theHash", + } + + entry = MockConfigEntry( + title="TPLink", + domain=DOMAIN, + data=entry_data, + unique_id=MAC_ADDRESS, + ) + + with ( + patch("homeassistant.components.tplink.PLATFORMS", []), + patch( + "homeassistant.components.tplink.Device.connect", + side_effect=AuthenticationError, + ) as connect_mock, + ): + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + expected_config = DeviceConfig.from_dict( + DEVICE_CONFIG_AUTH.to_dict(exclude_credentials=True, credentials_hash="theHash") + ) + connect_mock.assert_called_with(config=expected_config) + assert entry.state is ConfigEntryState.SETUP_ERROR + assert CONF_CREDENTIALS_HASH not in entry.data From 18d283bed6ecef32dfe1abe92229c832a59ab048 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 27 Jun 2024 09:26:31 +0200 Subject: [PATCH 022/146] Don't allow updating a device to have no connections or identifiers (#120603) * Don't allow updating a device to have no connections or identifiers * Move check to the top of the function --- homeassistant/helpers/device_registry.py | 5 +++++ tests/helpers/test_device_registry.py | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index cfafa63ec3a..4579739f0e1 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -869,6 +869,11 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): ) add_config_entry = config_entry + if not new_connections and not new_identifiers: + raise HomeAssistantError( + "A device must have at least one of identifiers or connections" + ) + if merge_connections is not UNDEFINED and new_connections is not UNDEFINED: raise HomeAssistantError( "Cannot define both merge_connections and new_connections" diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index fa57cc7557e..3a525f00870 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -3052,3 +3052,22 @@ async def test_primary_config_entry( model="model", ) assert device.primary_config_entry == mock_config_entry_1.entry_id + + +async def test_update_device_no_connections_or_identifiers( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, +) -> None: + """Test updating a device clearing connections and identifiers.""" + mock_config_entry = MockConfigEntry(domain="mqtt", title=None) + mock_config_entry.add_to_hass(hass) + + device = device_registry.async_get_or_create( + config_entry_id=mock_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers={("bridgeid", "0123")}, + ) + with pytest.raises(HomeAssistantError): + device_registry.async_update_device( + device.id, new_connections=set(), new_identifiers=set() + ) From ef47daad9d398de853989f4abdb317b17e442aca Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Wed, 26 Jun 2024 19:14:18 -0400 Subject: [PATCH 023/146] Bump anova_wifi to 0.14.0 (#120616) --- homeassistant/components/anova/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/anova/manifest.json b/homeassistant/components/anova/manifest.json index 331a4f61118..d75a791a6f5 100644 --- a/homeassistant/components/anova/manifest.json +++ b/homeassistant/components/anova/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/anova", "iot_class": "cloud_push", "loggers": ["anova_wifi"], - "requirements": ["anova-wifi==0.12.0"] + "requirements": ["anova-wifi==0.14.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 67ad67799e9..a3aa0bbacc3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -449,7 +449,7 @@ androidtvremote2==0.1.1 anel-pwrctrl-homeassistant==0.0.1.dev2 # homeassistant.components.anova -anova-wifi==0.12.0 +anova-wifi==0.14.0 # homeassistant.components.anthemav anthemav==1.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 350b59c0eab..9b1d4743c9b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -413,7 +413,7 @@ androidtv[async]==0.0.73 androidtvremote2==0.1.1 # homeassistant.components.anova -anova-wifi==0.12.0 +anova-wifi==0.14.0 # homeassistant.components.anthemav anthemav==1.4.1 From 7519603bf5ead3a979cc14ed792c2f309fe79f25 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 26 Jun 2024 20:37:28 -0500 Subject: [PATCH 024/146] Bump uiprotect to 4.0.0 (#120617) --- 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 8e29f5ffb9f..bdbdacae90e 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==3.7.0", "unifi-discovery==1.1.8"], + "requirements": ["uiprotect==4.0.0", "unifi-discovery==1.1.8"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index a3aa0bbacc3..44bc9f73b1d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2789,7 +2789,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==3.7.0 +uiprotect==4.0.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9b1d4743c9b..45cb1087cb4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2169,7 +2169,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==3.7.0 +uiprotect==4.0.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From 7256f23376be8e28d4e16e210c7020abb107d619 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Jun 2024 01:50:41 -0500 Subject: [PATCH 025/146] Fix performance regression in integration from state_reported (#120621) * Fix performance regression in integration from state_reported Because the callbacks were no longer indexed by entity id, users saw upwards of 1M calls/min https://github.com/home-assistant/core/pull/113869/files#r1655580523 * Update homeassistant/helpers/event.py * coverage --------- Co-authored-by: Paulus Schoutsen --- .../components/integration/sensor.py | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index 4fca92e9b40..8cc5341f081 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -27,8 +27,6 @@ from homeassistant.const import ( CONF_METHOD, CONF_NAME, CONF_UNIQUE_ID, - EVENT_STATE_CHANGED, - EVENT_STATE_REPORTED, STATE_UNAVAILABLE, UnitOfTime, ) @@ -45,7 +43,11 @@ from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.device import async_device_info_to_link_from_entity from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.event import ( + async_call_later, + async_track_state_change_event, + async_track_state_reported_event, +) from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import ( @@ -440,21 +442,17 @@ class IntegrationSensor(RestoreSensor): self._derive_and_set_attributes_from_state(state) self.async_on_remove( - self.hass.bus.async_listen( - EVENT_STATE_CHANGED, + async_track_state_change_event( + self.hass, + self._sensor_source_id, handle_state_change, - event_filter=callback( - lambda event_data: event_data["entity_id"] == self._sensor_source_id - ), ) ) self.async_on_remove( - self.hass.bus.async_listen( - EVENT_STATE_REPORTED, + async_track_state_reported_event( + self.hass, + self._sensor_source_id, handle_state_report, - event_filter=callback( - lambda event_data: event_data["entity_id"] == self._sensor_source_id - ), ) ) From 38601d48ffc155fb0d09ec08ad5090bd3d4163e6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 26 Jun 2024 22:04:27 -0500 Subject: [PATCH 026/146] Add async_track_state_reported_event to fix integration performance regression (#120622) split from https://github.com/home-assistant/core/pull/120621 --- homeassistant/helpers/event.py | 37 ++++++++++++++++++++++++++++------ tests/helpers/test_event.py | 32 ++++++++++++++++++++++++++++- 2 files changed, 62 insertions(+), 7 deletions(-) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 4150d871b6b..ebd51948e3b 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -17,6 +17,7 @@ from typing import TYPE_CHECKING, Any, Concatenate, Generic, TypeVar from homeassistant.const import ( EVENT_CORE_CONFIG_UPDATE, EVENT_STATE_CHANGED, + EVENT_STATE_REPORTED, MATCH_ALL, SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET, @@ -26,6 +27,7 @@ from homeassistant.core import ( Event, # Explicit reexport of 'EventStateChangedData' for backwards compatibility EventStateChangedData as EventStateChangedData, # noqa: PLC0414 + EventStateReportedData, HassJob, HassJobType, HomeAssistant, @@ -57,6 +59,9 @@ from .typing import TemplateVarsType _TRACK_STATE_CHANGE_DATA: HassKey[_KeyedEventData[EventStateChangedData]] = HassKey( "track_state_change_data" ) +_TRACK_STATE_REPORTED_DATA: HassKey[_KeyedEventData[EventStateReportedData]] = HassKey( + "track_state_reported_data" +) _TRACK_STATE_ADDED_DOMAIN_DATA: HassKey[_KeyedEventData[EventStateChangedData]] = ( HassKey("track_state_added_domain_data") ) @@ -324,8 +329,8 @@ def async_track_state_change_event( @callback def _async_dispatch_entity_id_event( hass: HomeAssistant, - callbacks: dict[str, list[HassJob[[Event[EventStateChangedData]], Any]]], - event: Event[EventStateChangedData], + callbacks: dict[str, list[HassJob[[Event[_TypedDictT]], Any]]], + event: Event[_TypedDictT], ) -> None: """Dispatch to listeners.""" if not (callbacks_list := callbacks.get(event.data["entity_id"])): @@ -342,10 +347,10 @@ def _async_dispatch_entity_id_event( @callback -def _async_state_change_filter( +def _async_state_filter( hass: HomeAssistant, - callbacks: dict[str, list[HassJob[[Event[EventStateChangedData]], Any]]], - event_data: EventStateChangedData, + callbacks: dict[str, list[HassJob[[Event[_TypedDictT]], Any]]], + event_data: _TypedDictT, ) -> bool: """Filter state changes by entity_id.""" return event_data["entity_id"] in callbacks @@ -355,7 +360,7 @@ _KEYED_TRACK_STATE_CHANGE = _KeyedEventTracker( key=_TRACK_STATE_CHANGE_DATA, event_type=EVENT_STATE_CHANGED, dispatcher_callable=_async_dispatch_entity_id_event, - filter_callable=_async_state_change_filter, + filter_callable=_async_state_filter, ) @@ -372,6 +377,26 @@ def _async_track_state_change_event( ) +_KEYED_TRACK_STATE_REPORTED = _KeyedEventTracker( + key=_TRACK_STATE_REPORTED_DATA, + event_type=EVENT_STATE_REPORTED, + dispatcher_callable=_async_dispatch_entity_id_event, + filter_callable=_async_state_filter, +) + + +def async_track_state_reported_event( + hass: HomeAssistant, + entity_ids: str | Iterable[str], + action: Callable[[Event[EventStateReportedData]], Any], + job_type: HassJobType | None = None, +) -> CALLBACK_TYPE: + """Track EVENT_STATE_REPORTED by entity_id without lowercasing.""" + return _async_track_event( + _KEYED_TRACK_STATE_REPORTED, hass, entity_ids, action, job_type + ) + + @callback def _remove_empty_listener() -> None: """Remove a listener that does nothing.""" diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index edce36218e8..4f983120e36 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -15,7 +15,13 @@ import pytest from homeassistant.const import MATCH_ALL import homeassistant.core as ha -from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback +from homeassistant.core import ( + Event, + EventStateChangedData, + EventStateReportedData, + HomeAssistant, + callback, +) from homeassistant.exceptions import TemplateError from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED @@ -34,6 +40,7 @@ from homeassistant.helpers.event import ( async_track_state_change_event, async_track_state_change_filtered, async_track_state_removed_domain, + async_track_state_reported_event, async_track_sunrise, async_track_sunset, async_track_template, @@ -4907,3 +4914,26 @@ async def test_track_point_in_time_repr( assert "Exception in callback _TrackPointUTCTime" in caplog.text assert "._raise_exception" in caplog.text await hass.async_block_till_done(wait_background_tasks=True) + + +async def test_async_track_state_reported_event(hass: HomeAssistant) -> None: + """Test async_track_state_reported_event.""" + tracker_called: list[ha.State] = [] + + @ha.callback + def single_run_callback(event: Event[EventStateReportedData]) -> None: + new_state = event.data["new_state"] + tracker_called.append(new_state) + + unsub = async_track_state_reported_event( + hass, ["light.bowl", "light.top"], single_run_callback + ) + hass.states.async_set("light.bowl", "on") + hass.states.async_set("light.top", "on") + await hass.async_block_till_done() + assert len(tracker_called) == 0 + hass.states.async_set("light.bowl", "on") + hass.states.async_set("light.top", "on") + await hass.async_block_till_done() + assert len(tracker_called) == 2 + unsub() From 1933454b76249bf6b9ba971e7acc0244f18b5a69 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 27 Jun 2024 13:45:15 +0200 Subject: [PATCH 027/146] Rename async_track_state_reported_event to async_track_state_report_event (#120637) * Rename async_track_state_reported_event to async_track_state_report_event * Update tests --- homeassistant/components/integration/sensor.py | 4 ++-- homeassistant/helpers/event.py | 12 ++++++------ tests/helpers/test_event.py | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index 8cc5341f081..a053e5cea5c 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -46,7 +46,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import ( async_call_later, async_track_state_change_event, - async_track_state_reported_event, + async_track_state_report_event, ) from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -449,7 +449,7 @@ class IntegrationSensor(RestoreSensor): ) ) self.async_on_remove( - async_track_state_reported_event( + async_track_state_report_event( self.hass, self._sensor_source_id, handle_state_report, diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index ebd51948e3b..51c1a7ba30f 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -59,8 +59,8 @@ from .typing import TemplateVarsType _TRACK_STATE_CHANGE_DATA: HassKey[_KeyedEventData[EventStateChangedData]] = HassKey( "track_state_change_data" ) -_TRACK_STATE_REPORTED_DATA: HassKey[_KeyedEventData[EventStateReportedData]] = HassKey( - "track_state_reported_data" +_TRACK_STATE_REPORT_DATA: HassKey[_KeyedEventData[EventStateReportedData]] = HassKey( + "track_state_report_data" ) _TRACK_STATE_ADDED_DOMAIN_DATA: HassKey[_KeyedEventData[EventStateChangedData]] = ( HassKey("track_state_added_domain_data") @@ -377,15 +377,15 @@ def _async_track_state_change_event( ) -_KEYED_TRACK_STATE_REPORTED = _KeyedEventTracker( - key=_TRACK_STATE_REPORTED_DATA, +_KEYED_TRACK_STATE_REPORT = _KeyedEventTracker( + key=_TRACK_STATE_REPORT_DATA, event_type=EVENT_STATE_REPORTED, dispatcher_callable=_async_dispatch_entity_id_event, filter_callable=_async_state_filter, ) -def async_track_state_reported_event( +def async_track_state_report_event( hass: HomeAssistant, entity_ids: str | Iterable[str], action: Callable[[Event[EventStateReportedData]], Any], @@ -393,7 +393,7 @@ def async_track_state_reported_event( ) -> CALLBACK_TYPE: """Track EVENT_STATE_REPORTED by entity_id without lowercasing.""" return _async_track_event( - _KEYED_TRACK_STATE_REPORTED, hass, entity_ids, action, job_type + _KEYED_TRACK_STATE_REPORT, hass, entity_ids, action, job_type ) diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 4f983120e36..4bb4c1a1967 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -40,7 +40,7 @@ from homeassistant.helpers.event import ( async_track_state_change_event, async_track_state_change_filtered, async_track_state_removed_domain, - async_track_state_reported_event, + async_track_state_report_event, async_track_sunrise, async_track_sunset, async_track_template, @@ -4916,8 +4916,8 @@ async def test_track_point_in_time_repr( await hass.async_block_till_done(wait_background_tasks=True) -async def test_async_track_state_reported_event(hass: HomeAssistant) -> None: - """Test async_track_state_reported_event.""" +async def test_async_track_state_report_event(hass: HomeAssistant) -> None: + """Test async_track_state_report_event.""" tracker_called: list[ha.State] = [] @ha.callback @@ -4925,7 +4925,7 @@ async def test_async_track_state_reported_event(hass: HomeAssistant) -> None: new_state = event.data["new_state"] tracker_called.append(new_state) - unsub = async_track_state_reported_event( + unsub = async_track_state_report_event( hass, ["light.bowl", "light.top"], single_run_callback ) hass.states.async_set("light.bowl", "on") From 89ac3ce832981fac544befde1fea1a6f3347e0c0 Mon Sep 17 00:00:00 2001 From: Josef Zweck <24647999+zweckj@users.noreply.github.com> Date: Thu, 27 Jun 2024 09:21:41 +0200 Subject: [PATCH 028/146] Fix the version that raises the issue (#120638) --- homeassistant/components/lamarzocco/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/lamarzocco/__init__.py b/homeassistant/components/lamarzocco/__init__.py index 9c66fdd1b60..dfcaa54047d 100644 --- a/homeassistant/components/lamarzocco/__init__.py +++ b/homeassistant/components/lamarzocco/__init__.py @@ -112,7 +112,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: LaMarzoccoConfigEntry) - entry.runtime_data = coordinator gateway_version = coordinator.device.firmware[FirmwareType.GATEWAY].current_version - if version.parse(gateway_version) < version.parse("v3.5-rc5"): + if version.parse(gateway_version) < version.parse("v3.4-rc5"): # incompatible gateway firmware, create an issue ir.async_create_issue( hass, From b290e9535055398e4457aae802da5bd4afc073c2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 27 Jun 2024 13:08:19 +0200 Subject: [PATCH 029/146] Improve typing of state event helpers (#120639) --- homeassistant/core.py | 15 +++++++++------ homeassistant/helpers/event.py | 10 ++++++---- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 2b43b2d40ff..71ee5f4bd1d 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -158,26 +158,29 @@ class ConfigSource(enum.StrEnum): YAML = "yaml" -class EventStateChangedData(TypedDict): +class EventStateEventData(TypedDict): + """Base class for EVENT_STATE_CHANGED and EVENT_STATE_CHANGED data.""" + + entity_id: str + new_state: State | None + + +class EventStateChangedData(EventStateEventData): """EVENT_STATE_CHANGED data. A state changed event is fired when on state write when the state is changed. """ - entity_id: str old_state: State | None - new_state: State | None -class EventStateReportedData(TypedDict): +class EventStateReportedData(EventStateEventData): """EVENT_STATE_REPORTED data. A state reported event is fired when on state write when the state is unchanged. """ - entity_id: str old_last_reported: datetime.datetime - new_state: State | None # SOURCE_* are deprecated as of Home Assistant 2022.2, use ConfigSource instead diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 51c1a7ba30f..0c77809079e 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -27,6 +27,7 @@ from homeassistant.core import ( Event, # Explicit reexport of 'EventStateChangedData' for backwards compatibility EventStateChangedData as EventStateChangedData, # noqa: PLC0414 + EventStateEventData, EventStateReportedData, HassJob, HassJobType, @@ -89,6 +90,7 @@ RANDOM_MICROSECOND_MIN = 50000 RANDOM_MICROSECOND_MAX = 500000 _TypedDictT = TypeVar("_TypedDictT", bound=Mapping[str, Any]) +_StateEventDataT = TypeVar("_StateEventDataT", bound=EventStateEventData) @dataclass(slots=True, frozen=True) @@ -329,8 +331,8 @@ def async_track_state_change_event( @callback def _async_dispatch_entity_id_event( hass: HomeAssistant, - callbacks: dict[str, list[HassJob[[Event[_TypedDictT]], Any]]], - event: Event[_TypedDictT], + callbacks: dict[str, list[HassJob[[Event[_StateEventDataT]], Any]]], + event: Event[_StateEventDataT], ) -> None: """Dispatch to listeners.""" if not (callbacks_list := callbacks.get(event.data["entity_id"])): @@ -349,8 +351,8 @@ def _async_dispatch_entity_id_event( @callback def _async_state_filter( hass: HomeAssistant, - callbacks: dict[str, list[HassJob[[Event[_TypedDictT]], Any]]], - event_data: _TypedDictT, + callbacks: dict[str, list[HassJob[[Event[_StateEventDataT]], Any]]], + event_data: _StateEventDataT, ) -> bool: """Filter state changes by entity_id.""" return event_data["entity_id"] in callbacks From 4836d6620b833186d16e120d0825ef1c4b029193 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 27 Jun 2024 10:43:28 +0200 Subject: [PATCH 030/146] Add snapshots to tasmota sensor test (#120647) --- .../tasmota/snapshots/test_sensor.ambr | 1526 +++++++++++++++++ tests/components/tasmota/test_sensor.py | 218 +-- 2 files changed, 1533 insertions(+), 211 deletions(-) create mode 100644 tests/components/tasmota/snapshots/test_sensor.ambr diff --git a/tests/components/tasmota/snapshots/test_sensor.ambr b/tests/components/tasmota/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..744554c7246 --- /dev/null +++ b/tests/components/tasmota/snapshots/test_sensor.ambr @@ -0,0 +1,1526 @@ +# serializer version: 1 +# name: test_controlling_state_via_mqtt[sensor_config0-entity_ids0-messages0] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Tasmota DHT11 Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_dht11_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config0-entity_ids0-messages0].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_dht11_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'DHT11 Temperature', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_DHT11_Temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config0-entity_ids0-messages0].2 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Tasmota DHT11 Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_dht11_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20.5', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config0-entity_ids0-messages0].3 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Tasmota DHT11 Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_dht11_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20.0', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config1-entity_ids1-messages1] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota TX23 Speed Act', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_tx23_speed_act', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config1-entity_ids1-messages1].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_tx23_speed_act', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'TX23 Speed Act', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_TX23_Speed_Act', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config1-entity_ids1-messages1].2 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota TX23 Dir Card', + }), + 'context': , + 'entity_id': 'sensor.tasmota_tx23_dir_card', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config1-entity_ids1-messages1].3 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_tx23_dir_card', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'TX23 Dir Card', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_TX23_Dir_Card', + 'unit_of_measurement': None, + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config1-entity_ids1-messages1].4 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota TX23 Speed Act', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_tx23_speed_act', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '12.3', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config1-entity_ids1-messages1].5 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota TX23 Dir Card', + }), + 'context': , + 'entity_id': 'sensor.tasmota_tx23_dir_card', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'WSW', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config1-entity_ids1-messages1].6 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota TX23 Speed Act', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_tx23_speed_act', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '23.4', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config1-entity_ids1-messages1].7 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota TX23 Dir Card', + }), + 'context': , + 'entity_id': 'sensor.tasmota_tx23_dir_card', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'ESE', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota ENERGY TotalTariff 0', + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_totaltariff_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_energy_totaltariff_0', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'ENERGY TotalTariff 0', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ENERGY_TotalTariff_0', + 'unit_of_measurement': None, + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].2 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota ENERGY TotalTariff 1', + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_totaltariff_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].3 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_energy_totaltariff_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'ENERGY TotalTariff 1', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ENERGY_TotalTariff_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].4 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota ENERGY TotalTariff 0', + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_totaltariff_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.2', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].5 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota ENERGY TotalTariff 1', + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_totaltariff_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3.4', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].6 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota ENERGY TotalTariff 0', + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_totaltariff_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.6', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].7 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota ENERGY TotalTariff 1', + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_totaltariff_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7.8', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config3-entity_ids3-messages3] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Tasmota DS18B20 Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_ds18b20_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config3-entity_ids3-messages3].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_ds18b20_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'DS18B20 Temperature', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_DS18B20_Temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config3-entity_ids3-messages3].2 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota DS18B20 Id', + }), + 'context': , + 'entity_id': 'sensor.tasmota_ds18b20_id', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config3-entity_ids3-messages3].3 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_ds18b20_id', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'DS18B20 Id', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_DS18B20_Id', + 'unit_of_measurement': None, + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config3-entity_ids3-messages3].4 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Tasmota DS18B20 Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_ds18b20_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '12.3', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config3-entity_ids3-messages3].5 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota DS18B20 Id', + }), + 'context': , + 'entity_id': 'sensor.tasmota_ds18b20_id', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '01191ED79190', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config3-entity_ids3-messages3].6 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Tasmota DS18B20 Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_ds18b20_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '23.4', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config3-entity_ids3-messages3].7 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota DS18B20 Id', + }), + 'context': , + 'entity_id': 'sensor.tasmota_ds18b20_id', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'meep', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config4-entity_ids4-messages4] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config4-entity_ids4-messages4].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_energy_total', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ENERGY Total', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ENERGY_Total', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config4-entity_ids4-messages4].2 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.2', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config4-entity_ids4-messages4].3 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.6', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config5-entity_ids5-messages5] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total 0', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config5-entity_ids5-messages5].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_energy_total_0', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ENERGY Total 0', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ENERGY_Total_0', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config5-entity_ids5-messages5].2 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total 1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config5-entity_ids5-messages5].3 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_energy_total_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ENERGY Total 1', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ENERGY_Total_1', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config5-entity_ids5-messages5].4 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total 0', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.2', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config5-entity_ids5-messages5].5 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total 1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3.4', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config5-entity_ids5-messages5].6 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total 0', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.6', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config5-entity_ids5-messages5].7 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total 1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7.8', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config6-entity_ids6-messages6] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total Phase1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total_phase1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config6-entity_ids6-messages6].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_energy_total_phase1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ENERGY Total Phase1', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ENERGY_Total_Phase1', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config6-entity_ids6-messages6].2 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total Phase2', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total_phase2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config6-entity_ids6-messages6].3 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_energy_total_phase2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ENERGY Total Phase2', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ENERGY_Total_Phase2', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config6-entity_ids6-messages6].4 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total Phase1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total_phase1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.2', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config6-entity_ids6-messages6].5 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total Phase2', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total_phase2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3.4', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config6-entity_ids6-messages6].6 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total Phase1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total_phase1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.6', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config6-entity_ids6-messages6].7 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total Phase2', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total_phase2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7.8', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config7-entity_ids7-messages7] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Tasmota ANALOG Temperature1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_temperature1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config7-entity_ids7-messages7].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_analog_temperature1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ANALOG Temperature1', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ANALOG_Temperature1', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config7-entity_ids7-messages7].10 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Tasmota ANALOG Temperature2', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_temperature2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '9.0', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config7-entity_ids7-messages7].11 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'illuminance', + 'friendly_name': 'Tasmota ANALOG Illuminance3', + 'state_class': , + 'unit_of_measurement': 'lx', + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_illuminance3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.2', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config7-entity_ids7-messages7].2 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Tasmota ANALOG Temperature2', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_temperature2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config7-entity_ids7-messages7].3 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_analog_temperature2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ANALOG Temperature2', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ANALOG_Temperature2', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config7-entity_ids7-messages7].4 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'illuminance', + 'friendly_name': 'Tasmota ANALOG Illuminance3', + 'state_class': , + 'unit_of_measurement': 'lx', + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_illuminance3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config7-entity_ids7-messages7].5 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_analog_illuminance3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ANALOG Illuminance3', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ANALOG_Illuminance3', + 'unit_of_measurement': 'lx', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config7-entity_ids7-messages7].6 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Tasmota ANALOG Temperature1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_temperature1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.2', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config7-entity_ids7-messages7].7 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Tasmota ANALOG Temperature2', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_temperature2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3.4', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config7-entity_ids7-messages7].8 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'illuminance', + 'friendly_name': 'Tasmota ANALOG Illuminance3', + 'state_class': , + 'unit_of_measurement': 'lx', + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_illuminance3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.6', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config7-entity_ids7-messages7].9 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Tasmota ANALOG Temperature1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_temperature1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7.8', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ANALOG CTEnergy1 Energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_ctenergy1_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_analog_ctenergy1_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ANALOG CTEnergy1 Energy', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ANALOG_CTEnergy1_Energy', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].10 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Tasmota ANALOG CTEnergy1 Voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_ctenergy1_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '230', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].11 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Tasmota ANALOG CTEnergy1 Current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_ctenergy1_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].12 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ANALOG CTEnergy1 Energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_ctenergy1_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.0', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].13 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Tasmota ANALOG CTEnergy1 Power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_ctenergy1_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1150', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].14 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Tasmota ANALOG CTEnergy1 Voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_ctenergy1_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '230', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].15 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Tasmota ANALOG CTEnergy1 Current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_ctenergy1_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].2 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Tasmota ANALOG CTEnergy1 Power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_ctenergy1_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].3 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_analog_ctenergy1_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ANALOG CTEnergy1 Power', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ANALOG_CTEnergy1_Power', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].4 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Tasmota ANALOG CTEnergy1 Voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_ctenergy1_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].5 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_analog_ctenergy1_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ANALOG CTEnergy1 Voltage', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ANALOG_CTEnergy1_Voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].6 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Tasmota ANALOG CTEnergy1 Current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_ctenergy1_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].7 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_analog_ctenergy1_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ANALOG CTEnergy1 Current', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ANALOG_CTEnergy1_Current', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].8 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ANALOG CTEnergy1 Energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_ctenergy1_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.5', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].9 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Tasmota ANALOG CTEnergy1 Power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_ctenergy1_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2300', + }) +# --- diff --git a/tests/components/tasmota/test_sensor.py b/tests/components/tasmota/test_sensor.py index 2de80de4319..c01485d12a7 100644 --- a/tests/components/tasmota/test_sensor.py +++ b/tests/components/tasmota/test_sensor.py @@ -13,9 +13,9 @@ from hatasmota.utils import ( get_topic_tele_will, ) import pytest +from syrupy import SnapshotAssertion from homeassistant import config_entries -from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorStateClass from homeassistant.components.tasmota.const import DEFAULT_PREFIX from homeassistant.const import ATTR_ASSUMED_STATE, STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant @@ -175,7 +175,7 @@ TEMPERATURE_SENSOR_CONFIG = { @pytest.mark.parametrize( - ("sensor_config", "entity_ids", "messages", "states"), + ("sensor_config", "entity_ids", "messages"), [ ( DEFAULT_SENSOR_CONFIG, @@ -184,20 +184,6 @@ TEMPERATURE_SENSOR_CONFIG = { '{"DHT11":{"Temperature":20.5}}', '{"StatusSNS":{"DHT11":{"Temperature":20.0}}}', ), - ( - { - "sensor.tasmota_dht11_temperature": { - "state": "20.5", - "attributes": { - "device_class": "temperature", - "unit_of_measurement": "°C", - }, - }, - }, - { - "sensor.tasmota_dht11_temperature": {"state": "20.0"}, - }, - ), ), ( DICT_SENSOR_CONFIG_1, @@ -206,22 +192,6 @@ TEMPERATURE_SENSOR_CONFIG = { '{"TX23":{"Speed":{"Act":"12.3"},"Dir": {"Card": "WSW"}}}', '{"StatusSNS":{"TX23":{"Speed":{"Act":"23.4"},"Dir": {"Card": "ESE"}}}}', ), - ( - { - "sensor.tasmota_tx23_speed_act": { - "state": "12.3", - "attributes": { - "device_class": None, - "unit_of_measurement": "km/h", - }, - }, - "sensor.tasmota_tx23_dir_card": {"state": "WSW"}, - }, - { - "sensor.tasmota_tx23_speed_act": {"state": "23.4"}, - "sensor.tasmota_tx23_dir_card": {"state": "ESE"}, - }, - ), ), ( LIST_SENSOR_CONFIG, @@ -233,22 +203,6 @@ TEMPERATURE_SENSOR_CONFIG = { '{"ENERGY":{"TotalTariff":[1.2,3.4]}}', '{"StatusSNS":{"ENERGY":{"TotalTariff":[5.6,7.8]}}}', ), - ( - { - "sensor.tasmota_energy_totaltariff_0": { - "state": "1.2", - "attributes": { - "device_class": None, - "unit_of_measurement": None, - }, - }, - "sensor.tasmota_energy_totaltariff_1": {"state": "3.4"}, - }, - { - "sensor.tasmota_energy_totaltariff_0": {"state": "5.6"}, - "sensor.tasmota_energy_totaltariff_1": {"state": "7.8"}, - }, - ), ), ( TEMPERATURE_SENSOR_CONFIG, @@ -257,22 +211,6 @@ TEMPERATURE_SENSOR_CONFIG = { '{"DS18B20":{"Id": "01191ED79190","Temperature": 12.3}}', '{"StatusSNS":{"DS18B20":{"Id": "meep","Temperature": 23.4}}}', ), - ( - { - "sensor.tasmota_ds18b20_temperature": { - "state": "12.3", - "attributes": { - "device_class": "temperature", - "unit_of_measurement": "°C", - }, - }, - "sensor.tasmota_ds18b20_id": {"state": "01191ED79190"}, - }, - { - "sensor.tasmota_ds18b20_temperature": {"state": "23.4"}, - "sensor.tasmota_ds18b20_id": {"state": "meep"}, - }, - ), ), # Test simple Total sensor ( @@ -282,21 +220,6 @@ TEMPERATURE_SENSOR_CONFIG = { '{"ENERGY":{"Total":1.2,"TotalStartTime":"2018-11-23T15:33:47"}}', '{"StatusSNS":{"ENERGY":{"Total":5.6,"TotalStartTime":"2018-11-23T16:33:47"}}}', ), - ( - { - "sensor.tasmota_energy_total": { - "state": "1.2", - "attributes": { - "device_class": "energy", - ATTR_STATE_CLASS: SensorStateClass.TOTAL, - "unit_of_measurement": "kWh", - }, - }, - }, - { - "sensor.tasmota_energy_total": {"state": "5.6"}, - }, - ), ), # Test list Total sensors ( @@ -306,30 +229,6 @@ TEMPERATURE_SENSOR_CONFIG = { '{"ENERGY":{"Total":[1.2, 3.4],"TotalStartTime":"2018-11-23T15:33:47"}}', '{"StatusSNS":{"ENERGY":{"Total":[5.6, 7.8],"TotalStartTime":"2018-11-23T16:33:47"}}}', ), - ( - { - "sensor.tasmota_energy_total_0": { - "state": "1.2", - "attributes": { - "device_class": "energy", - ATTR_STATE_CLASS: SensorStateClass.TOTAL, - "unit_of_measurement": "kWh", - }, - }, - "sensor.tasmota_energy_total_1": { - "state": "3.4", - "attributes": { - "device_class": "energy", - ATTR_STATE_CLASS: SensorStateClass.TOTAL, - "unit_of_measurement": "kWh", - }, - }, - }, - { - "sensor.tasmota_energy_total_0": {"state": "5.6"}, - "sensor.tasmota_energy_total_1": {"state": "7.8"}, - }, - ), ), # Test dict Total sensors ( @@ -342,30 +241,6 @@ TEMPERATURE_SENSOR_CONFIG = { '{"ENERGY":{"Total":{"Phase1":1.2, "Phase2":3.4},"TotalStartTime":"2018-11-23T15:33:47"}}', '{"StatusSNS":{"ENERGY":{"Total":{"Phase1":5.6, "Phase2":7.8},"TotalStartTime":"2018-11-23T15:33:47"}}}', ), - ( - { - "sensor.tasmota_energy_total_phase1": { - "state": "1.2", - "attributes": { - "device_class": "energy", - ATTR_STATE_CLASS: SensorStateClass.TOTAL, - "unit_of_measurement": "kWh", - }, - }, - "sensor.tasmota_energy_total_phase2": { - "state": "3.4", - "attributes": { - "device_class": "energy", - ATTR_STATE_CLASS: SensorStateClass.TOTAL, - "unit_of_measurement": "kWh", - }, - }, - }, - { - "sensor.tasmota_energy_total_phase1": {"state": "5.6"}, - "sensor.tasmota_energy_total_phase2": {"state": "7.8"}, - }, - ), ), ( NUMBERED_SENSOR_CONFIG, @@ -384,39 +259,6 @@ TEMPERATURE_SENSOR_CONFIG = { '"Illuminance3":1.2}}}' ), ), - ( - { - "sensor.tasmota_analog_temperature1": { - "state": "1.2", - "attributes": { - "device_class": "temperature", - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - "unit_of_measurement": "°C", - }, - }, - "sensor.tasmota_analog_temperature2": { - "state": "3.4", - "attributes": { - "device_class": "temperature", - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - "unit_of_measurement": "°C", - }, - }, - "sensor.tasmota_analog_illuminance3": { - "state": "5.6", - "attributes": { - "device_class": "illuminance", - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - "unit_of_measurement": "lx", - }, - }, - }, - { - "sensor.tasmota_analog_temperature1": {"state": "7.8"}, - "sensor.tasmota_analog_temperature2": {"state": "9.0"}, - "sensor.tasmota_analog_illuminance3": {"state": "1.2"}, - }, - ), ), ( NUMBERED_SENSOR_CONFIG_2, @@ -436,48 +278,6 @@ TEMPERATURE_SENSOR_CONFIG = { '{"Energy":1.0,"Power":1150,"Voltage":230,"Current":5}}}}' ), ), - ( - { - "sensor.tasmota_analog_ctenergy1_energy": { - "state": "0.5", - "attributes": { - "device_class": "energy", - ATTR_STATE_CLASS: SensorStateClass.TOTAL, - "unit_of_measurement": "kWh", - }, - }, - "sensor.tasmota_analog_ctenergy1_power": { - "state": "2300", - "attributes": { - "device_class": "power", - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - "unit_of_measurement": "W", - }, - }, - "sensor.tasmota_analog_ctenergy1_voltage": { - "state": "230", - "attributes": { - "device_class": "voltage", - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - "unit_of_measurement": "V", - }, - }, - "sensor.tasmota_analog_ctenergy1_current": { - "state": "10", - "attributes": { - "device_class": "current", - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - "unit_of_measurement": "A", - }, - }, - }, - { - "sensor.tasmota_analog_ctenergy1_energy": {"state": "1.0"}, - "sensor.tasmota_analog_ctenergy1_power": {"state": "1150"}, - "sensor.tasmota_analog_ctenergy1_voltage": {"state": "230"}, - "sensor.tasmota_analog_ctenergy1_current": {"state": "5"}, - }, - ), ), ], ) @@ -485,11 +285,11 @@ async def test_controlling_state_via_mqtt( hass: HomeAssistant, entity_registry: er.EntityRegistry, mqtt_mock: MqttMockHAClient, + snapshot: SnapshotAssertion, setup_tasmota, sensor_config, entity_ids, messages, - states, ) -> None: """Test state update via MQTT.""" config = copy.deepcopy(DEFAULT_CONFIG) @@ -513,11 +313,13 @@ async def test_controlling_state_via_mqtt( state = hass.states.get(entity_id) assert state.state == "unavailable" assert not state.attributes.get(ATTR_ASSUMED_STATE) + assert state == snapshot entry = entity_registry.async_get(entity_id) assert entry.disabled is False assert entry.disabled_by is None assert entry.entity_category is None + assert entry == snapshot async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") await hass.async_block_till_done() @@ -530,19 +332,13 @@ async def test_controlling_state_via_mqtt( async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/SENSOR", messages[0]) for entity_id in entity_ids: state = hass.states.get(entity_id) - expected_state = states[0][entity_id] - assert state.state == expected_state["state"] - for attribute, expected in expected_state.get("attributes", {}).items(): - assert state.attributes.get(attribute) == expected + assert state == snapshot # Test polled state update async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/STATUS10", messages[1]) for entity_id in entity_ids: state = hass.states.get(entity_id) - expected_state = states[1][entity_id] - assert state.state == expected_state["state"] - for attribute, expected in expected_state.get("attributes", {}).items(): - assert state.attributes.get(attribute) == expected + assert state == snapshot @pytest.mark.parametrize( From 3022d3bfa04400577fa39de44f38d1864f707ec5 Mon Sep 17 00:00:00 2001 From: Josef Zweck <24647999+zweckj@users.noreply.github.com> Date: Thu, 27 Jun 2024 10:34:01 +0200 Subject: [PATCH 031/146] Move Auto On/off switches to Config EntityCategory (#120648) --- homeassistant/components/lamarzocco/switch.py | 2 ++ tests/components/lamarzocco/snapshots/test_switch.ambr | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lamarzocco/switch.py b/homeassistant/components/lamarzocco/switch.py index e21cd2f3d94..c57e0662ab2 100644 --- a/homeassistant/components/lamarzocco/switch.py +++ b/homeassistant/components/lamarzocco/switch.py @@ -9,6 +9,7 @@ from lmcloud.lm_machine import LaMarzoccoMachine from lmcloud.models import LaMarzoccoMachineConfig from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription +from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -105,6 +106,7 @@ class LaMarzoccoAutoOnOffSwitchEntity(LaMarzoccoBaseEntity, SwitchEntity): super().__init__(coordinator, f"auto_on_off_{identifier}") self._identifier = identifier self._attr_translation_placeholders = {"id": identifier} + self.entity_category = EntityCategory.CONFIG async def _async_enable(self, state: bool) -> None: """Enable or disable the auto on/off schedule.""" diff --git a/tests/components/lamarzocco/snapshots/test_switch.ambr b/tests/components/lamarzocco/snapshots/test_switch.ambr index 0f462955a33..edda4ffee3b 100644 --- a/tests/components/lamarzocco/snapshots/test_switch.ambr +++ b/tests/components/lamarzocco/snapshots/test_switch.ambr @@ -10,7 +10,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'switch', - 'entity_category': None, + 'entity_category': , 'entity_id': 'switch.gs01234_auto_on_off_os2oswx', 'has_entity_name': True, 'hidden_by': None, @@ -43,7 +43,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'switch', - 'entity_category': None, + 'entity_category': , 'entity_id': 'switch.gs01234_auto_on_off_axfz5bj', 'has_entity_name': True, 'hidden_by': None, From 68495977643ebf39ff891c8cc727fd18f6c4eb36 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 27 Jun 2024 12:55:49 +0200 Subject: [PATCH 032/146] Bump hatasmota to 0.9.1 (#120649) --- .../components/tasmota/manifest.json | 2 +- homeassistant/components/tasmota/sensor.py | 4 +++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../tasmota/snapshots/test_sensor.ambr | 34 +++++++++++++++---- 5 files changed, 35 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index 2ce81772774..69233de07d8 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["hatasmota"], "mqtt": ["tasmota/discovery/#"], - "requirements": ["HATasmota==0.8.0"] + "requirements": ["HATasmota==0.9.1"] } diff --git a/homeassistant/components/tasmota/sensor.py b/homeassistant/components/tasmota/sensor.py index 546e3eb4539..a7fb415f037 100644 --- a/homeassistant/components/tasmota/sensor.py +++ b/homeassistant/components/tasmota/sensor.py @@ -190,6 +190,10 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = { DEVICE_CLASS: SensorDeviceClass.ENERGY, STATE_CLASS: SensorStateClass.TOTAL, }, + hc.SENSOR_TOTAL_TARIFF: { + DEVICE_CLASS: SensorDeviceClass.ENERGY, + STATE_CLASS: SensorStateClass.TOTAL, + }, hc.SENSOR_TOTAL_START_TIME: {ICON: "mdi:progress-clock"}, hc.SENSOR_TVOC: {ICON: "mdi:air-filter"}, hc.SENSOR_VOLTAGE: { diff --git a/requirements_all.txt b/requirements_all.txt index 44bc9f73b1d..06e5b6ef223 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -22,7 +22,7 @@ DoorBirdPy==2.1.0 HAP-python==4.9.1 # homeassistant.components.tasmota -HATasmota==0.8.0 +HATasmota==0.9.1 # homeassistant.components.mastodon Mastodon.py==1.8.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 45cb1087cb4..58691727bec 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -19,7 +19,7 @@ DoorBirdPy==2.1.0 HAP-python==4.9.1 # homeassistant.components.tasmota -HATasmota==0.8.0 +HATasmota==0.9.1 # homeassistant.components.doods # homeassistant.components.generic diff --git a/tests/components/tasmota/snapshots/test_sensor.ambr b/tests/components/tasmota/snapshots/test_sensor.ambr index 744554c7246..c5d70487749 100644 --- a/tests/components/tasmota/snapshots/test_sensor.ambr +++ b/tests/components/tasmota/snapshots/test_sensor.ambr @@ -232,7 +232,10 @@ # name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'energy', 'friendly_name': 'Tasmota ENERGY TotalTariff 0', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.tasmota_energy_totaltariff_0', @@ -247,7 +250,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -264,7 +269,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'ENERGY TotalTariff 0', 'platform': 'tasmota', @@ -272,13 +277,16 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': '00000049A3BC_sensor_sensor_ENERGY_TotalTariff_0', - 'unit_of_measurement': None, + 'unit_of_measurement': , }) # --- # name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].2 StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'energy', 'friendly_name': 'Tasmota ENERGY TotalTariff 1', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.tasmota_energy_totaltariff_1', @@ -293,7 +301,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -310,7 +320,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'ENERGY TotalTariff 1', 'platform': 'tasmota', @@ -318,13 +328,16 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': '00000049A3BC_sensor_sensor_ENERGY_TotalTariff_1', - 'unit_of_measurement': None, + 'unit_of_measurement': , }) # --- # name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].4 StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'energy', 'friendly_name': 'Tasmota ENERGY TotalTariff 0', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.tasmota_energy_totaltariff_0', @@ -337,7 +350,10 @@ # name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].5 StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'energy', 'friendly_name': 'Tasmota ENERGY TotalTariff 1', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.tasmota_energy_totaltariff_1', @@ -350,7 +366,10 @@ # name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].6 StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'energy', 'friendly_name': 'Tasmota ENERGY TotalTariff 0', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.tasmota_energy_totaltariff_0', @@ -363,7 +382,10 @@ # name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].7 StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'energy', 'friendly_name': 'Tasmota ENERGY TotalTariff 1', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.tasmota_energy_totaltariff_1', From 0e1dc9878f042f9f6ad79377f11e6ba530b49bc8 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Thu, 27 Jun 2024 21:17:15 +1000 Subject: [PATCH 033/146] Fix values at startup for Tessie (#120652) --- homeassistant/components/tessie/entity.py | 1 + .../tessie/snapshots/test_lock.ambr | 48 ------------------- .../tessie/snapshots/test_sensor.ambr | 20 ++++---- 3 files changed, 11 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/tessie/entity.py b/homeassistant/components/tessie/entity.py index 93b9f10ae67..d2a59f205fc 100644 --- a/homeassistant/components/tessie/entity.py +++ b/homeassistant/components/tessie/entity.py @@ -132,6 +132,7 @@ class TessieEnergyEntity(TessieBaseEntity): self._attr_device_info = data.device super().__init__(coordinator, key) + self._async_update_attrs() class TessieWallConnectorEntity(TessieBaseEntity): diff --git a/tests/components/tessie/snapshots/test_lock.ambr b/tests/components/tessie/snapshots/test_lock.ambr index 1eff418b202..cea2bebbddb 100644 --- a/tests/components/tessie/snapshots/test_lock.ambr +++ b/tests/components/tessie/snapshots/test_lock.ambr @@ -93,51 +93,3 @@ 'state': 'locked', }) # --- -# name: test_locks[lock.test_speed_limit-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'lock', - 'entity_category': None, - 'entity_id': 'lock.test_speed_limit', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Speed limit', - 'platform': 'tessie', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'vehicle_state_speed_limit_mode_active', - 'unique_id': 'VINVINVIN-vehicle_state_speed_limit_mode_active', - 'unit_of_measurement': None, - }) -# --- -# name: test_locks[lock.test_speed_limit-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'code_format': '^\\d\\d\\d\\d$', - 'friendly_name': 'Test Speed limit', - 'supported_features': , - }), - 'context': , - 'entity_id': 'lock.test_speed_limit', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unlocked', - }) -# --- diff --git a/tests/components/tessie/snapshots/test_sensor.ambr b/tests/components/tessie/snapshots/test_sensor.ambr index ba7b4eae0a5..afe229feba0 100644 --- a/tests/components/tessie/snapshots/test_sensor.ambr +++ b/tests/components/tessie/snapshots/test_sensor.ambr @@ -53,7 +53,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '5.06', }) # --- # name: test_sensors[sensor.energy_site_energy_left-entry] @@ -110,7 +110,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '38.8964736842105', }) # --- # name: test_sensors[sensor.energy_site_generator_power-entry] @@ -167,7 +167,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '0.0', }) # --- # name: test_sensors[sensor.energy_site_grid_power-entry] @@ -224,7 +224,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '0.0', }) # --- # name: test_sensors[sensor.energy_site_grid_services_power-entry] @@ -281,7 +281,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '0.0', }) # --- # name: test_sensors[sensor.energy_site_load_power-entry] @@ -338,7 +338,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '6.245', }) # --- # name: test_sensors[sensor.energy_site_percentage_charged-entry] @@ -392,7 +392,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '95.5053740373966', }) # --- # name: test_sensors[sensor.energy_site_solar_power-entry] @@ -449,7 +449,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '1.185', }) # --- # name: test_sensors[sensor.energy_site_total_pack_energy-entry] @@ -506,7 +506,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '40.727', }) # --- # name: test_sensors[sensor.energy_site_vpp_backup_reserve-entry] @@ -554,7 +554,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '0', }) # --- # name: test_sensors[sensor.test_battery_level-entry] From a8d6866f9f69de331d0eeb4f325ec140b282b218 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 27 Jun 2024 12:29:32 +0200 Subject: [PATCH 034/146] Disable polling for Knocki (#120656) --- homeassistant/components/knocki/event.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/knocki/event.py b/homeassistant/components/knocki/event.py index adaf344e468..74dc5a0f64c 100644 --- a/homeassistant/components/knocki/event.py +++ b/homeassistant/components/knocki/event.py @@ -48,6 +48,7 @@ class KnockiTrigger(EventEntity): _attr_event_types = [EVENT_TRIGGERED] _attr_has_entity_name = True + _attr_should_poll = False _attr_translation_key = "knocki" def __init__(self, trigger: Trigger, client: KnockiClient) -> None: From 03d198dd645487338e632b2630e4b14d945803bd Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 27 Jun 2024 19:41:21 +0200 Subject: [PATCH 035/146] Fix unknown attribute in MPD (#120657) --- homeassistant/components/mpd/media_player.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py index eb34fb6289f..3538b1c7973 100644 --- a/homeassistant/components/mpd/media_player.py +++ b/homeassistant/components/mpd/media_player.py @@ -421,11 +421,6 @@ class MpdDevice(MediaPlayerEntity): """Name of the current input source.""" return self._current_playlist - @property - def source_list(self): - """Return the list of available input sources.""" - return self._playlists - async def async_select_source(self, source: str) -> None: """Choose a different available playlist and play it.""" await self.async_play_media(MediaType.PLAYLIST, source) From be086c581c43ebb43dff97b03780f1c3d3d697c6 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 27 Jun 2024 12:47:58 +0200 Subject: [PATCH 036/146] Fix Airgradient ABC days name (#120659) --- .../components/airgradient/select.py | 1 + .../components/airgradient/strings.json | 3 +- .../airgradient/snapshots/test_select.ambr | 28 +++++++++++-------- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/airgradient/select.py b/homeassistant/components/airgradient/select.py index a64ce596806..532f7167dff 100644 --- a/homeassistant/components/airgradient/select.py +++ b/homeassistant/components/airgradient/select.py @@ -88,6 +88,7 @@ LEARNING_TIME_OFFSET_OPTIONS = [ ] ABC_DAYS = [ + "1", "8", "30", "90", diff --git a/homeassistant/components/airgradient/strings.json b/homeassistant/components/airgradient/strings.json index 1dd5fc61a16..12049e7b720 100644 --- a/homeassistant/components/airgradient/strings.json +++ b/homeassistant/components/airgradient/strings.json @@ -91,8 +91,9 @@ } }, "co2_automatic_baseline_calibration": { - "name": "CO2 automatic baseline calibration", + "name": "CO2 automatic baseline duration", "state": { + "1": "1 day", "8": "8 days", "30": "30 days", "90": "90 days", diff --git a/tests/components/airgradient/snapshots/test_select.ambr b/tests/components/airgradient/snapshots/test_select.ambr index ece563b40c6..b8fca4a110b 100644 --- a/tests/components/airgradient/snapshots/test_select.ambr +++ b/tests/components/airgradient/snapshots/test_select.ambr @@ -1,11 +1,12 @@ # serializer version: 1 -# name: test_all_entities[indoor][select.airgradient_co2_automatic_baseline_calibration-entry] +# name: test_all_entities[indoor][select.airgradient_co2_automatic_baseline_duration-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, 'capabilities': dict({ 'options': list([ + '1', '8', '30', '90', @@ -19,7 +20,7 @@ 'disabled_by': None, 'domain': 'select', 'entity_category': , - 'entity_id': 'select.airgradient_co2_automatic_baseline_calibration', + 'entity_id': 'select.airgradient_co2_automatic_baseline_duration', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -31,7 +32,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'CO2 automatic baseline calibration', + 'original_name': 'CO2 automatic baseline duration', 'platform': 'airgradient', 'previous_unique_id': None, 'supported_features': 0, @@ -40,11 +41,12 @@ 'unit_of_measurement': None, }) # --- -# name: test_all_entities[indoor][select.airgradient_co2_automatic_baseline_calibration-state] +# name: test_all_entities[indoor][select.airgradient_co2_automatic_baseline_duration-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Airgradient CO2 automatic baseline calibration', + 'friendly_name': 'Airgradient CO2 automatic baseline duration', 'options': list([ + '1', '8', '30', '90', @@ -53,7 +55,7 @@ ]), }), 'context': , - 'entity_id': 'select.airgradient_co2_automatic_baseline_calibration', + 'entity_id': 'select.airgradient_co2_automatic_baseline_duration', 'last_changed': , 'last_reported': , 'last_updated': , @@ -404,13 +406,14 @@ 'state': '12', }) # --- -# name: test_all_entities[outdoor][select.airgradient_co2_automatic_baseline_calibration-entry] +# name: test_all_entities[outdoor][select.airgradient_co2_automatic_baseline_duration-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, 'capabilities': dict({ 'options': list([ + '1', '8', '30', '90', @@ -424,7 +427,7 @@ 'disabled_by': None, 'domain': 'select', 'entity_category': , - 'entity_id': 'select.airgradient_co2_automatic_baseline_calibration', + 'entity_id': 'select.airgradient_co2_automatic_baseline_duration', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -436,7 +439,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'CO2 automatic baseline calibration', + 'original_name': 'CO2 automatic baseline duration', 'platform': 'airgradient', 'previous_unique_id': None, 'supported_features': 0, @@ -445,11 +448,12 @@ 'unit_of_measurement': None, }) # --- -# name: test_all_entities[outdoor][select.airgradient_co2_automatic_baseline_calibration-state] +# name: test_all_entities[outdoor][select.airgradient_co2_automatic_baseline_duration-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Airgradient CO2 automatic baseline calibration', + 'friendly_name': 'Airgradient CO2 automatic baseline duration', 'options': list([ + '1', '8', '30', '90', @@ -458,7 +462,7 @@ ]), }), 'context': , - 'entity_id': 'select.airgradient_co2_automatic_baseline_calibration', + 'entity_id': 'select.airgradient_co2_automatic_baseline_duration', 'last_changed': , 'last_reported': , 'last_updated': , From f9ca85735d089bbbe61abb293e23f43282f73d69 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 27 Jun 2024 23:08:40 +1200 Subject: [PATCH 037/146] [esphome] Add more tests to bring integration to 100% coverage (#120661) --- tests/components/esphome/conftest.py | 147 +++++++++++++++++- tests/components/esphome/test_manager.py | 108 ++++++++++++- .../esphome/test_voice_assistant.py | 14 +- 3 files changed, 258 insertions(+), 11 deletions(-) diff --git a/tests/components/esphome/conftest.py b/tests/components/esphome/conftest.py index f55ab9cbe4a..ac1558b8aa0 100644 --- a/tests/components/esphome/conftest.py +++ b/tests/components/esphome/conftest.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio from asyncio import Event -from collections.abc import Awaitable, Callable +from collections.abc import Awaitable, Callable, Coroutine from pathlib import Path from typing import Any from unittest.mock import AsyncMock, MagicMock, Mock, patch @@ -19,6 +19,8 @@ from aioesphomeapi import ( HomeassistantServiceCall, ReconnectLogic, UserService, + VoiceAssistantAudioSettings, + VoiceAssistantEventType, VoiceAssistantFeature, ) import pytest @@ -32,6 +34,11 @@ from homeassistant.components.esphome.const import ( DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS, DOMAIN, ) +from homeassistant.components.esphome.entry_data import RuntimeEntryData +from homeassistant.components.esphome.voice_assistant import ( + VoiceAssistantAPIPipeline, + VoiceAssistantUDPPipeline, +) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -40,6 +47,8 @@ from . import DASHBOARD_HOST, DASHBOARD_PORT, DASHBOARD_SLUG from tests.common import MockConfigEntry +_ONE_SECOND = 16000 * 2 # 16Khz 16-bit + @pytest.fixture(autouse=True) def mock_bluetooth(enable_bluetooth: None) -> None: @@ -196,6 +205,20 @@ class MockESPHomeDevice: self.home_assistant_state_subscription_callback: Callable[ [str, str | None], None ] + self.voice_assistant_handle_start_callback: Callable[ + [str, int, VoiceAssistantAudioSettings, str | None], + Coroutine[Any, Any, int | None], + ] + self.voice_assistant_handle_stop_callback: Callable[ + [], Coroutine[Any, Any, None] + ] + self.voice_assistant_handle_audio_callback: ( + Callable[ + [bytes], + Coroutine[Any, Any, None], + ] + | None + ) self.device_info = device_info def set_state_callback(self, state_callback: Callable[[EntityState], None]) -> None: @@ -255,6 +278,47 @@ class MockESPHomeDevice: """Mock a state subscription.""" self.home_assistant_state_subscription_callback(entity_id, attribute) + def set_subscribe_voice_assistant_callbacks( + self, + handle_start: Callable[ + [str, int, VoiceAssistantAudioSettings, str | None], + Coroutine[Any, Any, int | None], + ], + handle_stop: Callable[[], Coroutine[Any, Any, None]], + handle_audio: ( + Callable[ + [bytes], + Coroutine[Any, Any, None], + ] + | None + ) = None, + ) -> None: + """Set the voice assistant subscription callbacks.""" + self.voice_assistant_handle_start_callback = handle_start + self.voice_assistant_handle_stop_callback = handle_stop + self.voice_assistant_handle_audio_callback = handle_audio + + async def mock_voice_assistant_handle_start( + self, + conversation_id: str, + flags: int, + settings: VoiceAssistantAudioSettings, + wake_word_phrase: str | None, + ) -> int | None: + """Mock voice assistant handle start.""" + return await self.voice_assistant_handle_start_callback( + conversation_id, flags, settings, wake_word_phrase + ) + + async def mock_voice_assistant_handle_stop(self) -> None: + """Mock voice assistant handle stop.""" + await self.voice_assistant_handle_stop_callback() + + async def mock_voice_assistant_handle_audio(self, audio: bytes) -> None: + """Mock voice assistant handle audio.""" + assert self.voice_assistant_handle_audio_callback is not None + await self.voice_assistant_handle_audio_callback(audio) + async def _mock_generic_device_entry( hass: HomeAssistant, @@ -318,8 +382,33 @@ async def _mock_generic_device_entry( """Subscribe to home assistant states.""" mock_device.set_home_assistant_state_subscription_callback(on_state_sub) + def _subscribe_voice_assistant( + *, + handle_start: Callable[ + [str, int, VoiceAssistantAudioSettings, str | None], + Coroutine[Any, Any, int | None], + ], + handle_stop: Callable[[], Coroutine[Any, Any, None]], + handle_audio: ( + Callable[ + [bytes], + Coroutine[Any, Any, None], + ] + | None + ) = None, + ) -> Callable[[], None]: + """Subscribe to voice assistant.""" + mock_device.set_subscribe_voice_assistant_callbacks( + handle_start, handle_stop, handle_audio + ) + + def unsub(): + pass + + return unsub + mock_client.device_info = AsyncMock(return_value=mock_device.device_info) - mock_client.subscribe_voice_assistant = Mock() + mock_client.subscribe_voice_assistant = _subscribe_voice_assistant mock_client.list_entities_services = AsyncMock( return_value=mock_list_entities_services ) @@ -524,3 +613,57 @@ async def mock_esphome_device( ) return _mock_device + + +@pytest.fixture +def mock_voice_assistant_api_pipeline() -> VoiceAssistantAPIPipeline: + """Return the API Pipeline factory.""" + mock_pipeline = Mock(spec=VoiceAssistantAPIPipeline) + + def mock_constructor( + hass: HomeAssistant, + entry_data: RuntimeEntryData, + handle_event: Callable[[VoiceAssistantEventType, dict[str, str] | None], None], + handle_finished: Callable[[], None], + api_client: APIClient, + ): + """Fake the constructor.""" + mock_pipeline.hass = hass + mock_pipeline.entry_data = entry_data + mock_pipeline.handle_event = handle_event + mock_pipeline.handle_finished = handle_finished + mock_pipeline.api_client = api_client + return mock_pipeline + + mock_pipeline.side_effect = mock_constructor + with patch( + "homeassistant.components.esphome.voice_assistant.VoiceAssistantAPIPipeline", + new=mock_pipeline, + ): + yield mock_pipeline + + +@pytest.fixture +def mock_voice_assistant_udp_pipeline() -> VoiceAssistantUDPPipeline: + """Return the API Pipeline factory.""" + mock_pipeline = Mock(spec=VoiceAssistantUDPPipeline) + + def mock_constructor( + hass: HomeAssistant, + entry_data: RuntimeEntryData, + handle_event: Callable[[VoiceAssistantEventType, dict[str, str] | None], None], + handle_finished: Callable[[], None], + ): + """Fake the constructor.""" + mock_pipeline.hass = hass + mock_pipeline.entry_data = entry_data + mock_pipeline.handle_event = handle_event + mock_pipeline.handle_finished = handle_finished + return mock_pipeline + + mock_pipeline.side_effect = mock_constructor + with patch( + "homeassistant.components.esphome.voice_assistant.VoiceAssistantUDPPipeline", + new=mock_pipeline, + ): + yield mock_pipeline diff --git a/tests/components/esphome/test_manager.py b/tests/components/esphome/test_manager.py index 92c21842e78..01f267581f4 100644 --- a/tests/components/esphome/test_manager.py +++ b/tests/components/esphome/test_manager.py @@ -2,7 +2,7 @@ import asyncio from collections.abc import Awaitable, Callable -from unittest.mock import AsyncMock, call +from unittest.mock import AsyncMock, call, patch from aioesphomeapi import ( APIClient, @@ -17,6 +17,7 @@ from aioesphomeapi import ( UserService, UserServiceArg, UserServiceArgType, + VoiceAssistantFeature, ) import pytest @@ -28,6 +29,10 @@ from homeassistant.components.esphome.const import ( DOMAIN, STABLE_BLE_VERSION_STR, ) +from homeassistant.components.esphome.voice_assistant import ( + VoiceAssistantAPIPipeline, + VoiceAssistantUDPPipeline, +) from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -39,7 +44,7 @@ from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import device_registry as dr, issue_registry as ir from homeassistant.setup import async_setup_component -from .conftest import MockESPHomeDevice +from .conftest import _ONE_SECOND, MockESPHomeDevice from tests.common import MockConfigEntry, async_capture_events, async_mock_service @@ -1181,3 +1186,102 @@ async def test_entry_missing_unique_id( await mock_esphome_device(mock_client=mock_client, mock_storage=True) await hass.async_block_till_done() assert entry.unique_id == "11:22:33:44:55:aa" + + +async def test_manager_voice_assistant_handlers_api( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: Callable[ + [APIClient, list[EntityInfo], list[UserService], list[EntityState]], + Awaitable[MockESPHomeDevice], + ], + caplog: pytest.LogCaptureFixture, + mock_voice_assistant_api_pipeline: VoiceAssistantAPIPipeline, +) -> None: + """Test the handlers are correctly executed in manager.py.""" + + device: MockESPHomeDevice = await mock_esphome_device( + mock_client=mock_client, + entity_info=[], + user_service=[], + states=[], + device_info={ + "voice_assistant_feature_flags": VoiceAssistantFeature.VOICE_ASSISTANT + | VoiceAssistantFeature.API_AUDIO + }, + ) + + await hass.async_block_till_done() + + with ( + patch( + "homeassistant.components.esphome.manager.VoiceAssistantAPIPipeline", + new=mock_voice_assistant_api_pipeline, + ), + ): + port: int | None = await device.mock_voice_assistant_handle_start( + "", 0, None, None + ) + + assert port == 0 + + port: int | None = await device.mock_voice_assistant_handle_start( + "", 0, None, None + ) + + assert "Voice assistant UDP server was not stopped" in caplog.text + + await device.mock_voice_assistant_handle_audio(bytes(_ONE_SECOND)) + + mock_voice_assistant_api_pipeline.receive_audio_bytes.assert_called_with( + bytes(_ONE_SECOND) + ) + + mock_voice_assistant_api_pipeline.receive_audio_bytes.reset_mock() + + await device.mock_voice_assistant_handle_stop() + mock_voice_assistant_api_pipeline.handle_finished() + + await device.mock_voice_assistant_handle_audio(bytes(_ONE_SECOND)) + + mock_voice_assistant_api_pipeline.receive_audio_bytes.assert_not_called() + + +async def test_manager_voice_assistant_handlers_udp( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: Callable[ + [APIClient, list[EntityInfo], list[UserService], list[EntityState]], + Awaitable[MockESPHomeDevice], + ], + mock_voice_assistant_udp_pipeline: VoiceAssistantUDPPipeline, +) -> None: + """Test the handlers are correctly executed in manager.py.""" + + device: MockESPHomeDevice = await mock_esphome_device( + mock_client=mock_client, + entity_info=[], + user_service=[], + states=[], + device_info={ + "voice_assistant_feature_flags": VoiceAssistantFeature.VOICE_ASSISTANT + }, + ) + + await hass.async_block_till_done() + + with ( + patch( + "homeassistant.components.esphome.manager.VoiceAssistantUDPPipeline", + new=mock_voice_assistant_udp_pipeline, + ), + ): + await device.mock_voice_assistant_handle_start("", 0, None, None) + + mock_voice_assistant_udp_pipeline.run_pipeline.assert_called() + + await device.mock_voice_assistant_handle_stop() + mock_voice_assistant_udp_pipeline.handle_finished() + + mock_voice_assistant_udp_pipeline.stop.assert_called() + mock_voice_assistant_udp_pipeline.close.assert_called() diff --git a/tests/components/esphome/test_voice_assistant.py b/tests/components/esphome/test_voice_assistant.py index c347c3dc7d3..eafc0243dc6 100644 --- a/tests/components/esphome/test_voice_assistant.py +++ b/tests/components/esphome/test_voice_assistant.py @@ -37,15 +37,13 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import intent as intent_helper import homeassistant.helpers.device_registry as dr -from .conftest import MockESPHomeDevice +from .conftest import _ONE_SECOND, MockESPHomeDevice _TEST_INPUT_TEXT = "This is an input test" _TEST_OUTPUT_TEXT = "This is an output test" _TEST_OUTPUT_URL = "output.mp3" _TEST_MEDIA_ID = "12345" -_ONE_SECOND = 16000 * 2 # 16Khz 16-bit - @pytest.fixture def voice_assistant_udp_pipeline( @@ -813,6 +811,7 @@ async def test_wake_word_abort_exception( async def test_timer_events( hass: HomeAssistant, + device_registry: dr.DeviceRegistry, mock_client: APIClient, mock_esphome_device: Callable[ [APIClient, list[EntityInfo], list[UserService], list[EntityState]], @@ -831,8 +830,8 @@ async def test_timer_events( | VoiceAssistantFeature.TIMERS }, ) - dev_reg = dr.async_get(hass) - dev = dev_reg.async_get_device( + await hass.async_block_till_done() + dev = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, mock_device.entry.unique_id)} ) @@ -886,6 +885,7 @@ async def test_timer_events( async def test_unknown_timer_event( hass: HomeAssistant, + device_registry: dr.DeviceRegistry, mock_client: APIClient, mock_esphome_device: Callable[ [APIClient, list[EntityInfo], list[UserService], list[EntityState]], @@ -904,8 +904,8 @@ async def test_unknown_timer_event( | VoiceAssistantFeature.TIMERS }, ) - dev_reg = dr.async_get(hass) - dev = dev_reg.async_get_device( + await hass.async_block_till_done() + dev = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, mock_device.entry.unique_id)} ) From f6aa25c717125e2af4b5f6c7d295b43261049f58 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 27 Jun 2024 15:00:14 +0200 Subject: [PATCH 038/146] Fix docstring for EventStateEventData (#120662) --- homeassistant/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 71ee5f4bd1d..c4392f62c52 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -159,7 +159,7 @@ class ConfigSource(enum.StrEnum): class EventStateEventData(TypedDict): - """Base class for EVENT_STATE_CHANGED and EVENT_STATE_CHANGED data.""" + """Base class for EVENT_STATE_CHANGED and EVENT_STATE_REPORTED data.""" entity_id: str new_state: State | None From 94f8f8281f66f150c894af80044115074fac6a44 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Jun 2024 12:58:42 -0500 Subject: [PATCH 039/146] Bump uiprotect to 4.2.0 (#120669) --- 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 bdbdacae90e..6f61bb97480 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==4.0.0", "unifi-discovery==1.1.8"], + "requirements": ["uiprotect==4.2.0", "unifi-discovery==1.1.8"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 06e5b6ef223..a4f34b61e3f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2789,7 +2789,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==4.0.0 +uiprotect==4.2.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 58691727bec..8c9cb2c8a23 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2169,7 +2169,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==4.0.0 +uiprotect==4.2.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From f9c5661c669f357eee0073ac155f8054cc8578bc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Jun 2024 15:10:48 -0500 Subject: [PATCH 040/146] Bump unifi-discovery to 1.2.0 (#120684) --- 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 6f61bb97480..06716e5342a 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==4.2.0", "unifi-discovery==1.1.8"], + "requirements": ["uiprotect==4.2.0", "unifi-discovery==1.2.0"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index a4f34b61e3f..397a967788a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2795,7 +2795,7 @@ uiprotect==4.2.0 ultraheat-api==0.5.7 # homeassistant.components.unifiprotect -unifi-discovery==1.1.8 +unifi-discovery==1.2.0 # homeassistant.components.unifi_direct unifi_ap==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8c9cb2c8a23..87ec9e11e0f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2175,7 +2175,7 @@ uiprotect==4.2.0 ultraheat-api==0.5.7 # homeassistant.components.unifiprotect -unifi-discovery==1.1.8 +unifi-discovery==1.2.0 # homeassistant.components.zha universal-silabs-flasher==0.0.20 From 07dd832c58d86efa90398b1e17f9f249763131b9 Mon Sep 17 00:00:00 2001 From: Glenn Waters Date: Thu, 27 Jun 2024 13:08:18 -0400 Subject: [PATCH 041/146] Bump Environment Canada to 0.7.0 (#120686) --- .../components/environment_canada/manifest.json | 2 +- homeassistant/components/environment_canada/sensor.py | 9 --------- homeassistant/components/environment_canada/strings.json | 3 --- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 3 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index a0bdd5d4919..69a6cd7c69b 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/environment_canada", "iot_class": "cloud_polling", "loggers": ["env_canada"], - "requirements": ["env-canada==0.6.3"] + "requirements": ["env-canada==0.7.0"] } diff --git a/homeassistant/components/environment_canada/sensor.py b/homeassistant/components/environment_canada/sensor.py index 8a734f74dd6..1a5d096203d 100644 --- a/homeassistant/components/environment_canada/sensor.py +++ b/homeassistant/components/environment_canada/sensor.py @@ -19,7 +19,6 @@ from homeassistant.const import ( PERCENTAGE, UV_INDEX, UnitOfLength, - UnitOfPrecipitationDepth, UnitOfPressure, UnitOfSpeed, UnitOfTemperature, @@ -114,14 +113,6 @@ SENSOR_TYPES: tuple[ECSensorEntityDescription, ...] = ( native_unit_of_measurement=PERCENTAGE, value_fn=lambda data: data.conditions.get("pop", {}).get("value"), ), - ECSensorEntityDescription( - key="precip_yesterday", - translation_key="precip_yesterday", - device_class=SensorDeviceClass.PRECIPITATION, - native_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS, - state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda data: data.conditions.get("precip_yesterday", {}).get("value"), - ), ECSensorEntityDescription( key="pressure", translation_key="pressure", diff --git a/homeassistant/components/environment_canada/strings.json b/homeassistant/components/environment_canada/strings.json index fc03550b64e..28ca55c6195 100644 --- a/homeassistant/components/environment_canada/strings.json +++ b/homeassistant/components/environment_canada/strings.json @@ -52,9 +52,6 @@ "pop": { "name": "Chance of precipitation" }, - "precip_yesterday": { - "name": "Precipitation yesterday" - }, "pressure": { "name": "Barometric pressure" }, diff --git a/requirements_all.txt b/requirements_all.txt index 397a967788a..2b99124d5b3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -816,7 +816,7 @@ enocean==0.50 enturclient==0.2.4 # homeassistant.components.environment_canada -env-canada==0.6.3 +env-canada==0.7.0 # homeassistant.components.season ephem==4.1.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 87ec9e11e0f..1a230af870f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -673,7 +673,7 @@ energyzero==2.1.0 enocean==0.50 # homeassistant.components.environment_canada -env-canada==0.6.3 +env-canada==0.7.0 # homeassistant.components.season ephem==4.1.5 From 09dbd8e7eb02bf8f4c56202a2c5a9ba05b64b0a2 Mon Sep 17 00:00:00 2001 From: MatthewFlamm <39341281+MatthewFlamm@users.noreply.github.com> Date: Thu, 27 Jun 2024 13:47:25 -0400 Subject: [PATCH 042/146] Use more observations in NWS (#120687) Use more observations --- homeassistant/components/nws/const.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nws/const.py b/homeassistant/components/nws/const.py index ba3a22e5818..381537775da 100644 --- a/homeassistant/components/nws/const.py +++ b/homeassistant/components/nws/const.py @@ -78,8 +78,8 @@ HOURLY = "hourly" OBSERVATION_VALID_TIME = timedelta(minutes=60) FORECAST_VALID_TIME = timedelta(minutes=45) -# A lot of stations update once hourly plus some wiggle room -UPDATE_TIME_PERIOD = timedelta(minutes=70) +# Ask for observations for last four hours +UPDATE_TIME_PERIOD = timedelta(minutes=240) DEBOUNCE_TIME = 10 * 60 # in seconds DEFAULT_SCAN_INTERVAL = timedelta(minutes=10) From b9c9921847c6c27e618845c814c18215dfab6186 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Jun 2024 12:20:37 -0500 Subject: [PATCH 043/146] Add newer models to unifi integrations discovery (#120688) --- homeassistant/components/unifi/manifest.json | 4 ++++ homeassistant/components/unifiprotect/manifest.json | 4 ++++ homeassistant/generated/ssdp.py | 8 ++++++++ 3 files changed, 16 insertions(+) diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index f4bfaec2d42..aa9b553cb67 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -21,6 +21,10 @@ { "manufacturer": "Ubiquiti Networks", "modelDescription": "UniFi Dream Machine SE" + }, + { + "manufacturer": "Ubiquiti Networks", + "modelDescription": "UniFi Dream Machine Pro Max" } ] } diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 06716e5342a..6691d738cd0 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -53,6 +53,10 @@ { "manufacturer": "Ubiquiti Networks", "modelDescription": "UniFi Dream Machine SE" + }, + { + "manufacturer": "Ubiquiti Networks", + "modelDescription": "UniFi Dream Machine Pro Max" } ] } diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index 8e7319917f0..9ed65bab868 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -297,6 +297,10 @@ SSDP = { "manufacturer": "Ubiquiti Networks", "modelDescription": "UniFi Dream Machine SE", }, + { + "manufacturer": "Ubiquiti Networks", + "modelDescription": "UniFi Dream Machine Pro Max", + }, ], "unifiprotect": [ { @@ -311,6 +315,10 @@ SSDP = { "manufacturer": "Ubiquiti Networks", "modelDescription": "UniFi Dream Machine SE", }, + { + "manufacturer": "Ubiquiti Networks", + "modelDescription": "UniFi Dream Machine Pro Max", + }, ], "upnp": [ { From e756328d523042910a6a147655f267c44ae71620 Mon Sep 17 00:00:00 2001 From: Glenn Waters Date: Thu, 27 Jun 2024 13:41:36 -0400 Subject: [PATCH 044/146] Bump upb-lib to 0.5.7 (#120689) --- homeassistant/components/upb/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/upb/manifest.json b/homeassistant/components/upb/manifest.json index a5e32dd298e..b208edbc0e5 100644 --- a/homeassistant/components/upb/manifest.json +++ b/homeassistant/components/upb/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/upb", "iot_class": "local_push", "loggers": ["upb_lib"], - "requirements": ["upb-lib==0.5.6"] + "requirements": ["upb-lib==0.5.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2b99124d5b3..9cbfe64afd0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2807,7 +2807,7 @@ unifiled==0.11 universal-silabs-flasher==0.0.20 # homeassistant.components.upb -upb-lib==0.5.6 +upb-lib==0.5.7 # homeassistant.components.upcloud upcloud-api==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1a230af870f..01b356747b2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2181,7 +2181,7 @@ unifi-discovery==1.2.0 universal-silabs-flasher==0.0.20 # homeassistant.components.upb -upb-lib==0.5.6 +upb-lib==0.5.7 # homeassistant.components.upcloud upcloud-api==2.5.1 From 476b9909ac879cf60b6d2d9de877fa8d5099cab6 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 27 Jun 2024 22:06:30 +0200 Subject: [PATCH 045/146] Update frontend to 20240627.0 (#120693) --- homeassistant/components/frontend/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/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 89c8fbe30ca..cd46b358335 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20240626.2"] + "requirements": ["home-assistant-frontend==20240627.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 174de784eba..91db2564fa6 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ habluetooth==3.1.3 hass-nabucasa==0.81.1 hassil==1.7.1 home-assistant-bluetooth==1.12.2 -home-assistant-frontend==20240626.2 +home-assistant-frontend==20240627.0 home-assistant-intents==2024.6.26 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 9cbfe64afd0..0086de879db 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1090,7 +1090,7 @@ hole==0.8.0 holidays==0.51 # homeassistant.components.frontend -home-assistant-frontend==20240626.2 +home-assistant-frontend==20240627.0 # homeassistant.components.conversation home-assistant-intents==2024.6.26 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 01b356747b2..0862fc33ea4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -895,7 +895,7 @@ hole==0.8.0 holidays==0.51 # homeassistant.components.frontend -home-assistant-frontend==20240626.2 +home-assistant-frontend==20240627.0 # homeassistant.components.conversation home-assistant-intents==2024.6.26 From f3ab3bd5cbe60d3fce5c8e46d40cb69b58ceef0c Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Thu, 27 Jun 2024 21:29:17 +0200 Subject: [PATCH 046/146] Bump aioautomower to 2024.6.3 (#120697) --- homeassistant/components/husqvarna_automower/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/husqvarna_automower/manifest.json b/homeassistant/components/husqvarna_automower/manifest.json index 5ca1b500340..7883b057a3f 100644 --- a/homeassistant/components/husqvarna_automower/manifest.json +++ b/homeassistant/components/husqvarna_automower/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/husqvarna_automower", "iot_class": "cloud_push", "loggers": ["aioautomower"], - "requirements": ["aioautomower==2024.6.1"] + "requirements": ["aioautomower==2024.6.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 0086de879db..a8d7a62d848 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -204,7 +204,7 @@ aioaseko==0.1.1 aioasuswrt==1.4.0 # homeassistant.components.husqvarna_automower -aioautomower==2024.6.1 +aioautomower==2024.6.3 # homeassistant.components.azure_devops aioazuredevops==2.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0862fc33ea4..802231b63d1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -183,7 +183,7 @@ aioaseko==0.1.1 aioasuswrt==1.4.0 # homeassistant.components.husqvarna_automower -aioautomower==2024.6.1 +aioautomower==2024.6.3 # homeassistant.components.azure_devops aioazuredevops==2.1.1 From 411633d3b3786a9b210c9c1bf925c09e31b5a6b8 Mon Sep 17 00:00:00 2001 From: Glenn Waters Date: Thu, 27 Jun 2024 16:10:11 -0400 Subject: [PATCH 047/146] Bump Environment Canada to 0.7.1 (#120699) --- homeassistant/components/environment_canada/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index 69a6cd7c69b..c77d35b1769 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/environment_canada", "iot_class": "cloud_polling", "loggers": ["env_canada"], - "requirements": ["env-canada==0.7.0"] + "requirements": ["env-canada==0.7.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index a8d7a62d848..657e961d803 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -816,7 +816,7 @@ enocean==0.50 enturclient==0.2.4 # homeassistant.components.environment_canada -env-canada==0.7.0 +env-canada==0.7.1 # homeassistant.components.season ephem==4.1.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 802231b63d1..3eb005b1dac 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -673,7 +673,7 @@ energyzero==2.1.0 enocean==0.50 # homeassistant.components.environment_canada -env-canada==0.7.0 +env-canada==0.7.1 # homeassistant.components.season ephem==4.1.5 From 0b8dd738f1a07074029e04845a8e731f1724fbfa Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 27 Jun 2024 22:09:33 +0200 Subject: [PATCH 048/146] Bump ttls to 1.8.3 (#120700) --- homeassistant/components/twinkly/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/twinkly/manifest.json b/homeassistant/components/twinkly/manifest.json index 6ec89261b3d..a84eebf0f28 100644 --- a/homeassistant/components/twinkly/manifest.json +++ b/homeassistant/components/twinkly/manifest.json @@ -14,5 +14,5 @@ "documentation": "https://www.home-assistant.io/integrations/twinkly", "iot_class": "local_polling", "loggers": ["ttls"], - "requirements": ["ttls==1.5.1"] + "requirements": ["ttls==1.8.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 657e961d803..98b5548cd38 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2768,7 +2768,7 @@ tplink-omada-client==1.3.12 transmission-rpc==7.0.3 # homeassistant.components.twinkly -ttls==1.5.1 +ttls==1.8.3 # homeassistant.components.thethingsnetwork ttn_client==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3eb005b1dac..2857aee8c9b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2148,7 +2148,7 @@ tplink-omada-client==1.3.12 transmission-rpc==7.0.3 # homeassistant.components.twinkly -ttls==1.5.1 +ttls==1.8.3 # homeassistant.components.thethingsnetwork ttn_client==1.0.0 From 23056f839b1dba2f2469176a5896a99d46179322 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Thu, 27 Jun 2024 13:54:34 +0100 Subject: [PATCH 049/146] Update tplink unlink identifiers to deal with ids from other domains (#120596) --- homeassistant/components/tplink/__init__.py | 98 +++++++++++++-------- tests/components/tplink/__init__.py | 1 + tests/components/tplink/test_init.py | 82 ++++++++++++----- 3 files changed, 123 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index 6d300f68aa0..83cfc733716 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio +from collections.abc import Iterable from datetime import timedelta import logging from typing import Any @@ -282,6 +283,28 @@ def mac_alias(mac: str) -> str: return mac.replace(":", "")[-4:].upper() +def _mac_connection_or_none(device: dr.DeviceEntry) -> str | None: + return next( + ( + conn + for type_, conn in device.connections + if type_ == dr.CONNECTION_NETWORK_MAC + ), + None, + ) + + +def _device_id_is_mac_or_none(mac: str, device_ids: Iterable[str]) -> str | None: + # Previously only iot devices had child devices and iot devices use + # the upper and lcase MAC addresses as device_id so match on case + # insensitive mac address as the parent device. + upper_mac = mac.upper() + return next( + (device_id for device_id in device_ids if device_id.upper() == upper_mac), + None, + ) + + async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Migrate old entry.""" version = config_entry.version @@ -298,49 +321,48 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> # always be linked into one device. dev_reg = dr.async_get(hass) for device in dr.async_entries_for_config_entry(dev_reg, config_entry.entry_id): - new_identifiers: set[tuple[str, str]] | None = None - if len(device.identifiers) > 1 and ( - mac := next( - iter( - [ - conn[1] - for conn in device.connections - if conn[0] == dr.CONNECTION_NETWORK_MAC - ] - ), - None, + original_identifiers = device.identifiers + # Get only the tplink identifier, could be tapo or other integrations. + tplink_identifiers = [ + ident[1] for ident in original_identifiers if ident[0] == DOMAIN + ] + # Nothing to fix if there's only one identifier. mac connection + # should never be none but if it is there's no problem. + if len(tplink_identifiers) <= 1 or not ( + mac := _mac_connection_or_none(device) + ): + continue + if not ( + tplink_parent_device_id := _device_id_is_mac_or_none( + mac, tplink_identifiers ) ): - for identifier in device.identifiers: - # Previously only iot devices that use the MAC address as - # device_id had child devices so check for mac as the - # parent device. - if identifier[0] == DOMAIN and identifier[1].upper() == mac.upper(): - new_identifiers = {identifier} - break - if new_identifiers: - dev_reg.async_update_device( - device.id, new_identifiers=new_identifiers - ) - _LOGGER.debug( - "Replaced identifiers for device %s (%s): %s with: %s", - device.name, - device.model, - device.identifiers, - new_identifiers, - ) - else: - # No match on mac so raise an error. - _LOGGER.error( - "Unable to replace identifiers for device %s (%s): %s", - device.name, - device.model, - device.identifiers, - ) + # No match on mac so raise an error. + _LOGGER.error( + "Unable to replace identifiers for device %s (%s): %s", + device.name, + device.model, + device.identifiers, + ) + continue + # Retain any identifiers for other domains + new_identifiers = { + ident for ident in device.identifiers if ident[0] != DOMAIN + } + new_identifiers.add((DOMAIN, tplink_parent_device_id)) + dev_reg.async_update_device(device.id, new_identifiers=new_identifiers) + _LOGGER.debug( + "Replaced identifiers for device %s (%s): %s with: %s", + device.name, + device.model, + original_identifiers, + new_identifiers, + ) minor_version = 3 hass.config_entries.async_update_entry(config_entry, minor_version=3) - _LOGGER.debug("Migration to version %s.%s successful", version, minor_version) + + _LOGGER.debug("Migration to version %s.%s complete", version, minor_version) if version == 1 and minor_version == 3: # credentials_hash stored in the device_config should be moved to data. diff --git a/tests/components/tplink/__init__.py b/tests/components/tplink/__init__.py index b3092d62904..d12858017cc 100644 --- a/tests/components/tplink/__init__.py +++ b/tests/components/tplink/__init__.py @@ -49,6 +49,7 @@ ALIAS = "My Bulb" MODEL = "HS100" MAC_ADDRESS = "aa:bb:cc:dd:ee:ff" DEVICE_ID = "123456789ABCDEFGH" +DEVICE_ID_MAC = "AA:BB:CC:DD:EE:FF" DHCP_FORMATTED_MAC_ADDRESS = MAC_ADDRESS.replace(":", "") MAC_ADDRESS2 = "11:22:33:44:55:66" DEFAULT_ENTRY_TITLE = f"{ALIAS} {MODEL}" diff --git a/tests/components/tplink/test_init.py b/tests/components/tplink/test_init.py index bfb7e02b63d..c5c5e2ce6db 100644 --- a/tests/components/tplink/test_init.py +++ b/tests/components/tplink/test_init.py @@ -36,6 +36,8 @@ from . import ( CREATE_ENTRY_DATA_AUTH, CREATE_ENTRY_DATA_LEGACY, DEVICE_CONFIG_AUTH, + DEVICE_ID, + DEVICE_ID_MAC, IP_ADDRESS, MAC_ADDRESS, _mocked_device, @@ -404,19 +406,48 @@ async def test_feature_no_category( @pytest.mark.parametrize( - ("identifier_base", "expected_message", "expected_count"), + ("device_id", "id_count", "domains", "expected_message"), [ - pytest.param("C0:06:C3:42:54:2B", "Replaced", 1, id="success"), - pytest.param("123456789", "Unable to replace", 3, id="failure"), + pytest.param(DEVICE_ID_MAC, 1, [DOMAIN], None, id="mac-id-no-children"), + pytest.param(DEVICE_ID_MAC, 3, [DOMAIN], "Replaced", id="mac-id-children"), + pytest.param( + DEVICE_ID_MAC, + 1, + [DOMAIN, "other"], + None, + id="mac-id-no-children-other-domain", + ), + pytest.param( + DEVICE_ID_MAC, + 3, + [DOMAIN, "other"], + "Replaced", + id="mac-id-children-other-domain", + ), + pytest.param(DEVICE_ID, 1, [DOMAIN], None, id="not-mac-id-no-children"), + pytest.param( + DEVICE_ID, 3, [DOMAIN], "Unable to replace", id="not-mac-children" + ), + pytest.param( + DEVICE_ID, 1, [DOMAIN, "other"], None, id="not-mac-no-children-other-domain" + ), + pytest.param( + DEVICE_ID, + 3, + [DOMAIN, "other"], + "Unable to replace", + id="not-mac-children-other-domain", + ), ], ) async def test_unlink_devices( hass: HomeAssistant, device_registry: dr.DeviceRegistry, caplog: pytest.LogCaptureFixture, - identifier_base, + device_id, + id_count, + domains, expected_message, - expected_count, ) -> None: """Test for unlinking child device ids.""" entry = MockConfigEntry( @@ -429,43 +460,54 @@ async def test_unlink_devices( ) entry.add_to_hass(hass) - # Setup initial device registry, with linkages - mac = "C0:06:C3:42:54:2B" - identifiers = [ - (DOMAIN, identifier_base), - (DOMAIN, f"{identifier_base}_0001"), - (DOMAIN, f"{identifier_base}_0002"), + # Generate list of test identifiers + test_identifiers = [ + (domain, f"{device_id}{"" if i == 0 else f"_000{i}"}") + for i in range(id_count) + for domain in domains ] + update_msg_fragment = "identifiers for device dummy (hs300):" + update_msg = f"{expected_message} {update_msg_fragment}" if expected_message else "" + + # Expected identifiers should include all other domains or all the newer non-mac device ids + # or just the parent mac device id + expected_identifiers = [ + (domain, device_id) + for domain, device_id in test_identifiers + if domain != DOMAIN + or device_id.startswith(DEVICE_ID) + or device_id == DEVICE_ID_MAC + ] + device_registry.async_get_or_create( config_entry_id="123456", connections={ - (dr.CONNECTION_NETWORK_MAC, mac.lower()), + (dr.CONNECTION_NETWORK_MAC, MAC_ADDRESS), }, - identifiers=set(identifiers), + identifiers=set(test_identifiers), model="hs300", name="dummy", ) device_entries = dr.async_entries_for_config_entry(device_registry, entry.entry_id) assert device_entries[0].connections == { - (dr.CONNECTION_NETWORK_MAC, mac.lower()), + (dr.CONNECTION_NETWORK_MAC, MAC_ADDRESS), } - assert device_entries[0].identifiers == set(identifiers) + assert device_entries[0].identifiers == set(test_identifiers) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() device_entries = dr.async_entries_for_config_entry(device_registry, entry.entry_id) - assert device_entries[0].connections == {(dr.CONNECTION_NETWORK_MAC, mac.lower())} - # If expected count is 1 will be the first identifier only - expected_identifiers = identifiers[:expected_count] + assert device_entries[0].connections == {(dr.CONNECTION_NETWORK_MAC, MAC_ADDRESS)} + assert device_entries[0].identifiers == set(expected_identifiers) assert entry.version == 1 assert entry.minor_version == 4 - msg = f"{expected_message} identifiers for device dummy (hs300): {set(identifiers)}" - assert msg in caplog.text + assert update_msg in caplog.text + assert "Migration to version 1.3 complete" in caplog.text async def test_move_credentials_hash( From 9b5d0f72dcdee18e07746eb45995fe159006e1b1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 27 Jun 2024 22:20:25 +0200 Subject: [PATCH 050/146] Bump version to 2024.7.0b3 --- 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 8291fb93fd7..33a86f57a5e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "0b2" +PATCH_VERSION: Final = "0b3" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 709022534b1..e4ccd9898e0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.7.0b2" +version = "2024.7.0b3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From f28cbf1909f89d508d35a80c0bf6d7b58886ec7d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 28 Jun 2024 08:42:47 +0200 Subject: [PATCH 051/146] Set stateclass on unknown numeric Tasmota sensors (#120650) --- homeassistant/components/tasmota/sensor.py | 9 + .../tasmota/snapshots/test_sensor.ambr | 298 ++++++++++++++++++ tests/components/tasmota/test_sensor.py | 25 ++ 3 files changed, 332 insertions(+) diff --git a/homeassistant/components/tasmota/sensor.py b/homeassistant/components/tasmota/sensor.py index a7fb415f037..db404884e67 100644 --- a/homeassistant/components/tasmota/sensor.py +++ b/homeassistant/components/tasmota/sensor.py @@ -298,6 +298,15 @@ class TasmotaSensor(TasmotaAvailability, TasmotaDiscoveryUpdate, SensorEntity): self._attr_native_unit_of_measurement = SENSOR_UNIT_MAP.get( self._tasmota_entity.unit, self._tasmota_entity.unit ) + if ( + self._attr_device_class is None + and self._attr_state_class is None + and self._attr_native_unit_of_measurement is None + ): + # If the sensor has a numeric value, but we couldn't detect what it is, + # set state class to measurement. + if self._tasmota_entity.discovered_as_numeric: + self._attr_state_class = SensorStateClass.MEASUREMENT async def async_added_to_hass(self) -> None: """Subscribe to MQTT events.""" diff --git a/tests/components/tasmota/snapshots/test_sensor.ambr b/tests/components/tasmota/snapshots/test_sensor.ambr index c5d70487749..b56115f189c 100644 --- a/tests/components/tasmota/snapshots/test_sensor.ambr +++ b/tests/components/tasmota/snapshots/test_sensor.ambr @@ -1546,3 +1546,301 @@ 'state': '2300', }) # --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota SENSOR1 Unknown', + }), + 'context': , + 'entity_id': 'sensor.tasmota_sensor1_unknown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_sensor1_unknown', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'SENSOR1 Unknown', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_SENSOR1_Unknown', + 'unit_of_measurement': None, + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].10 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota SENSOR3 Unknown', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_sensor3_unknown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20.5', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].11 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota SENSOR4 Unknown', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_sensor4_unknown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20.5', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].12 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota SENSOR1 Unknown', + }), + 'context': , + 'entity_id': 'sensor.tasmota_sensor1_unknown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].13 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota SENSOR2 Unknown', + }), + 'context': , + 'entity_id': 'sensor.tasmota_sensor2_unknown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].14 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota SENSOR3 Unknown', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_sensor3_unknown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].15 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota SENSOR4 Unknown', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_sensor4_unknown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].2 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota SENSOR2 Unknown', + }), + 'context': , + 'entity_id': 'sensor.tasmota_sensor2_unknown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].3 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_sensor2_unknown', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'SENSOR2 Unknown', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_SENSOR2_Unknown', + 'unit_of_measurement': None, + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].4 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota SENSOR3 Unknown', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_sensor3_unknown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].5 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_sensor3_unknown', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'SENSOR3 Unknown', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_SENSOR3_Unknown', + 'unit_of_measurement': None, + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].6 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota SENSOR4 Unknown', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_sensor4_unknown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].7 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_sensor4_unknown', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'SENSOR4 Unknown', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_SENSOR4_Unknown', + 'unit_of_measurement': None, + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].8 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota SENSOR1 Unknown', + }), + 'context': , + 'entity_id': 'sensor.tasmota_sensor1_unknown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20.5', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].9 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota SENSOR2 Unknown', + }), + 'context': , + 'entity_id': 'sensor.tasmota_sensor2_unknown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20.5', + }) +# --- diff --git a/tests/components/tasmota/test_sensor.py b/tests/components/tasmota/test_sensor.py index c01485d12a7..44a6ce65fd1 100644 --- a/tests/components/tasmota/test_sensor.py +++ b/tests/components/tasmota/test_sensor.py @@ -50,6 +50,17 @@ BAD_LIST_SENSOR_CONFIG_3 = { } } +# This configuration has sensors which type we can't guess +DEFAULT_SENSOR_CONFIG_UNKNOWN = { + "sn": { + "Time": "2020-09-25T12:47:15", + "SENSOR1": {"Unknown": None}, + "SENSOR2": {"Unknown": "123"}, + "SENSOR3": {"Unknown": 123}, + "SENSOR4": {"Unknown": 123.0}, + } +} + # This configuration has some sensors where values are lists # Home Assistant maps this to one sensor for each list item LIST_SENSOR_CONFIG = { @@ -279,6 +290,20 @@ TEMPERATURE_SENSOR_CONFIG = { ), ), ), + # Test we automatically set state class to measurement on unknown numerical sensors + ( + DEFAULT_SENSOR_CONFIG_UNKNOWN, + [ + "sensor.tasmota_sensor1_unknown", + "sensor.tasmota_sensor2_unknown", + "sensor.tasmota_sensor3_unknown", + "sensor.tasmota_sensor4_unknown", + ], + ( + '{"SENSOR1":{"Unknown":20.5},"SENSOR2":{"Unknown":20.5},"SENSOR3":{"Unknown":20.5},"SENSOR4":{"Unknown":20.5}}', + '{"StatusSNS":{"SENSOR1":{"Unknown":20},"SENSOR2":{"Unknown":20},"SENSOR3":{"Unknown":20},"SENSOR4":{"Unknown":20}}}', + ), + ), ], ) async def test_controlling_state_via_mqtt( From 876fb234ceb906e71d672ba3d94856d5a0137b61 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 27 Jun 2024 22:56:22 +0200 Subject: [PATCH 052/146] Bump hatasmota to 0.9.2 (#120670) --- .../components/tasmota/manifest.json | 2 +- homeassistant/components/tasmota/sensor.py | 78 +++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../tasmota/snapshots/test_sensor.ambr | 232 +++++++++++++++--- tests/components/tasmota/test_sensor.py | 6 +- 6 files changed, 247 insertions(+), 75 deletions(-) diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index 69233de07d8..783483c6ffd 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["hatasmota"], "mqtt": ["tasmota/discovery/#"], - "requirements": ["HATasmota==0.9.1"] + "requirements": ["HATasmota==0.9.2"] } diff --git a/homeassistant/components/tasmota/sensor.py b/homeassistant/components/tasmota/sensor.py index db404884e67..e87ff88092e 100644 --- a/homeassistant/components/tasmota/sensor.py +++ b/homeassistant/components/tasmota/sensor.py @@ -53,26 +53,10 @@ ICON = "icon" # A Tasmota sensor type may be mapped to either a device class or an icon, # both can only be set if the default device class icon is not appropriate SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = { - hc.SENSOR_ACTIVE_ENERGYEXPORT: { - DEVICE_CLASS: SensorDeviceClass.ENERGY, - STATE_CLASS: SensorStateClass.TOTAL, - }, - hc.SENSOR_ACTIVE_ENERGYIMPORT: { - DEVICE_CLASS: SensorDeviceClass.ENERGY, - STATE_CLASS: SensorStateClass.TOTAL, - }, - hc.SENSOR_ACTIVE_POWERUSAGE: { - DEVICE_CLASS: SensorDeviceClass.POWER, - STATE_CLASS: SensorStateClass.MEASUREMENT, - }, hc.SENSOR_AMBIENT: { DEVICE_CLASS: SensorDeviceClass.ILLUMINANCE, STATE_CLASS: SensorStateClass.MEASUREMENT, }, - hc.SENSOR_APPARENT_POWERUSAGE: { - DEVICE_CLASS: SensorDeviceClass.APPARENT_POWER, - STATE_CLASS: SensorStateClass.MEASUREMENT, - }, hc.SENSOR_BATTERY: { DEVICE_CLASS: SensorDeviceClass.BATTERY, STATE_CLASS: SensorStateClass.MEASUREMENT, @@ -92,7 +76,7 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = { DEVICE_CLASS: SensorDeviceClass.CURRENT, STATE_CLASS: SensorStateClass.MEASUREMENT, }, - hc.SENSOR_CURRENTNEUTRAL: { + hc.SENSOR_CURRENT_NEUTRAL: { DEVICE_CLASS: SensorDeviceClass.CURRENT, STATE_CLASS: SensorStateClass.MEASUREMENT, }, @@ -110,6 +94,34 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = { DEVICE_CLASS: SensorDeviceClass.ENERGY, STATE_CLASS: SensorStateClass.TOTAL, }, + hc.SENSOR_ENERGY_EXPORT_ACTIVE: { + DEVICE_CLASS: SensorDeviceClass.ENERGY, + STATE_CLASS: SensorStateClass.TOTAL, + }, + hc.SENSOR_ENERGY_EXPORT_REACTIVE: {STATE_CLASS: SensorStateClass.TOTAL}, + hc.SENSOR_ENERGY_EXPORT_TARIFF: { + DEVICE_CLASS: SensorDeviceClass.ENERGY, + STATE_CLASS: SensorStateClass.TOTAL, + }, + hc.SENSOR_ENERGY_IMPORT_ACTIVE: { + DEVICE_CLASS: SensorDeviceClass.ENERGY, + STATE_CLASS: SensorStateClass.TOTAL, + }, + hc.SENSOR_ENERGY_IMPORT_REACTIVE: {STATE_CLASS: SensorStateClass.TOTAL}, + hc.SENSOR_ENERGY_IMPORT_TODAY: { + DEVICE_CLASS: SensorDeviceClass.ENERGY, + STATE_CLASS: SensorStateClass.TOTAL_INCREASING, + }, + hc.SENSOR_ENERGY_IMPORT_TOTAL: { + DEVICE_CLASS: SensorDeviceClass.ENERGY, + STATE_CLASS: SensorStateClass.TOTAL, + }, + hc.SENSOR_ENERGY_IMPORT_TOTAL_TARIFF: { + DEVICE_CLASS: SensorDeviceClass.ENERGY, + STATE_CLASS: SensorStateClass.TOTAL, + }, + hc.SENSOR_ENERGY_IMPORT_YESTERDAY: {DEVICE_CLASS: SensorDeviceClass.ENERGY}, + hc.SENSOR_ENERGY_TOTAL_START_TIME: {ICON: "mdi:progress-clock"}, hc.SENSOR_FREQUENCY: { DEVICE_CLASS: SensorDeviceClass.FREQUENCY, STATE_CLASS: SensorStateClass.MEASUREMENT, @@ -122,6 +134,14 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = { DEVICE_CLASS: SensorDeviceClass.ILLUMINANCE, STATE_CLASS: SensorStateClass.MEASUREMENT, }, + hc.SENSOR_POWER_ACTIVE: { + DEVICE_CLASS: SensorDeviceClass.POWER, + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + hc.SENSOR_POWER_APPARENT: { + DEVICE_CLASS: SensorDeviceClass.APPARENT_POWER, + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, hc.SENSOR_STATUS_IP: {ICON: "mdi:ip-network"}, hc.SENSOR_STATUS_LINK_COUNT: {ICON: "mdi:counter"}, hc.SENSOR_MOISTURE: {DEVICE_CLASS: SensorDeviceClass.MOISTURE}, @@ -144,11 +164,11 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = { DEVICE_CLASS: SensorDeviceClass.PM25, STATE_CLASS: SensorStateClass.MEASUREMENT, }, - hc.SENSOR_POWERFACTOR: { + hc.SENSOR_POWER_FACTOR: { DEVICE_CLASS: SensorDeviceClass.POWER_FACTOR, STATE_CLASS: SensorStateClass.MEASUREMENT, }, - hc.SENSOR_POWERUSAGE: { + hc.SENSOR_POWER: { DEVICE_CLASS: SensorDeviceClass.POWER, STATE_CLASS: SensorStateClass.MEASUREMENT, }, @@ -156,14 +176,12 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = { DEVICE_CLASS: SensorDeviceClass.PRESSURE, STATE_CLASS: SensorStateClass.MEASUREMENT, }, - hc.SENSOR_PRESSUREATSEALEVEL: { + hc.SENSOR_PRESSURE_AT_SEA_LEVEL: { DEVICE_CLASS: SensorDeviceClass.PRESSURE, STATE_CLASS: SensorStateClass.MEASUREMENT, }, hc.SENSOR_PROXIMITY: {ICON: "mdi:ruler"}, - hc.SENSOR_REACTIVE_ENERGYEXPORT: {STATE_CLASS: SensorStateClass.TOTAL}, - hc.SENSOR_REACTIVE_ENERGYIMPORT: {STATE_CLASS: SensorStateClass.TOTAL}, - hc.SENSOR_REACTIVE_POWERUSAGE: { + hc.SENSOR_POWER_REACTIVE: { DEVICE_CLASS: SensorDeviceClass.REACTIVE_POWER, STATE_CLASS: SensorStateClass.MEASUREMENT, }, @@ -182,19 +200,6 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = { DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, STATE_CLASS: SensorStateClass.MEASUREMENT, }, - hc.SENSOR_TODAY: { - DEVICE_CLASS: SensorDeviceClass.ENERGY, - STATE_CLASS: SensorStateClass.TOTAL_INCREASING, - }, - hc.SENSOR_TOTAL: { - DEVICE_CLASS: SensorDeviceClass.ENERGY, - STATE_CLASS: SensorStateClass.TOTAL, - }, - hc.SENSOR_TOTAL_TARIFF: { - DEVICE_CLASS: SensorDeviceClass.ENERGY, - STATE_CLASS: SensorStateClass.TOTAL, - }, - hc.SENSOR_TOTAL_START_TIME: {ICON: "mdi:progress-clock"}, hc.SENSOR_TVOC: {ICON: "mdi:air-filter"}, hc.SENSOR_VOLTAGE: { DEVICE_CLASS: SensorDeviceClass.VOLTAGE, @@ -204,7 +209,6 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = { DEVICE_CLASS: SensorDeviceClass.WEIGHT, STATE_CLASS: SensorStateClass.MEASUREMENT, }, - hc.SENSOR_YESTERDAY: {DEVICE_CLASS: SensorDeviceClass.ENERGY}, } SENSOR_UNIT_MAP = { diff --git a/requirements_all.txt b/requirements_all.txt index 98b5548cd38..d2e51a365e7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -22,7 +22,7 @@ DoorBirdPy==2.1.0 HAP-python==4.9.1 # homeassistant.components.tasmota -HATasmota==0.9.1 +HATasmota==0.9.2 # homeassistant.components.mastodon Mastodon.py==1.8.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2857aee8c9b..21e6eedebc7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -19,7 +19,7 @@ DoorBirdPy==2.1.0 HAP-python==4.9.1 # homeassistant.components.tasmota -HATasmota==0.9.1 +HATasmota==0.9.2 # homeassistant.components.doods # homeassistant.components.generic diff --git a/tests/components/tasmota/snapshots/test_sensor.ambr b/tests/components/tasmota/snapshots/test_sensor.ambr index b56115f189c..be011e595b9 100644 --- a/tests/components/tasmota/snapshots/test_sensor.ambr +++ b/tests/components/tasmota/snapshots/test_sensor.ambr @@ -280,6 +280,102 @@ 'unit_of_measurement': , }) # --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].10 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY ExportTariff 0', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_exporttariff_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.6', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].11 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY ExportTariff 1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_exporttariff_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7.8', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].12 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY TotalTariff 0', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_totaltariff_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.6', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].13 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY TotalTariff 1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_totaltariff_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7.8', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].14 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY ExportTariff 0', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_exporttariff_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.2', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].15 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY ExportTariff 1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_exporttariff_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3.4', + }) +# --- # name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].2 StateSnapshot({ 'attributes': ReadOnlyDict({ @@ -332,6 +428,108 @@ }) # --- # name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].4 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY ExportTariff 0', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_exporttariff_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].5 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_energy_exporttariff_0', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ENERGY ExportTariff 0', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ENERGY_ExportTariff_0', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].6 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY ExportTariff 1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_exporttariff_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].7 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_energy_exporttariff_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ENERGY ExportTariff 1', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ENERGY_ExportTariff_1', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].8 StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'energy', @@ -347,7 +545,7 @@ 'state': '1.2', }) # --- -# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].5 +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].9 StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'energy', @@ -363,38 +561,6 @@ 'state': '3.4', }) # --- -# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].6 - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Tasmota ENERGY TotalTariff 0', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.tasmota_energy_totaltariff_0', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '5.6', - }) -# --- -# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].7 - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Tasmota ENERGY TotalTariff 1', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.tasmota_energy_totaltariff_1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '7.8', - }) -# --- # name: test_controlling_state_via_mqtt[sensor_config3-entity_ids3-messages3] StateSnapshot({ 'attributes': ReadOnlyDict({ diff --git a/tests/components/tasmota/test_sensor.py b/tests/components/tasmota/test_sensor.py index 44a6ce65fd1..78235f7ebf5 100644 --- a/tests/components/tasmota/test_sensor.py +++ b/tests/components/tasmota/test_sensor.py @@ -209,10 +209,12 @@ TEMPERATURE_SENSOR_CONFIG = { [ "sensor.tasmota_energy_totaltariff_0", "sensor.tasmota_energy_totaltariff_1", + "sensor.tasmota_energy_exporttariff_0", + "sensor.tasmota_energy_exporttariff_1", ], ( - '{"ENERGY":{"TotalTariff":[1.2,3.4]}}', - '{"StatusSNS":{"ENERGY":{"TotalTariff":[5.6,7.8]}}}', + '{"ENERGY":{"ExportTariff":[5.6,7.8],"TotalTariff":[1.2,3.4]}}', + '{"StatusSNS":{"ENERGY":{"ExportTariff":[1.2,3.4],"TotalTariff":[5.6,7.8]}}}', ), ), ( From ca515f740e53a83c8b1b5a4fefb2fa2309807a25 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 28 Jun 2024 11:15:44 +0200 Subject: [PATCH 053/146] Bump panasonic_viera to 0.4.2 (#120692) * Bump panasonic_viera to 0.4.2 * Bump panasonic_viera to 0.4.2 * Bump panasonic_viera to 0.4.2 * Fix Keys --- .../components/panasonic_viera/__init__.py | 8 ++++---- .../components/panasonic_viera/manifest.json | 2 +- .../components/panasonic_viera/media_player.py | 18 +++++++++--------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/panasonic_viera/test_remote.py | 2 +- 6 files changed, 17 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/panasonic_viera/__init__.py b/homeassistant/components/panasonic_viera/__init__.py index b2f3bbba91a..2cf91792800 100644 --- a/homeassistant/components/panasonic_viera/__init__.py +++ b/homeassistant/components/panasonic_viera/__init__.py @@ -196,10 +196,10 @@ class Remote: self.muted = self._control.get_mute() self.volume = self._control.get_volume() / 100 - async def async_send_key(self, key): + async def async_send_key(self, key: Keys | str) -> None: """Send a key to the TV and handle exceptions.""" try: - key = getattr(Keys, key) + key = getattr(Keys, key.upper()) except (AttributeError, TypeError): key = getattr(key, "value", key) @@ -211,13 +211,13 @@ class Remote: await self._on_action.async_run(context=context) await self.async_update() elif self.state != STATE_ON: - await self.async_send_key(Keys.power) + await self.async_send_key(Keys.POWER) await self.async_update() async def async_turn_off(self): """Turn off the TV.""" if self.state != STATE_OFF: - await self.async_send_key(Keys.power) + await self.async_send_key(Keys.POWER) self.state = STATE_OFF await self.async_update() diff --git a/homeassistant/components/panasonic_viera/manifest.json b/homeassistant/components/panasonic_viera/manifest.json index 2afa6599cb2..d9809e5883a 100644 --- a/homeassistant/components/panasonic_viera/manifest.json +++ b/homeassistant/components/panasonic_viera/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/panasonic_viera", "iot_class": "local_polling", "loggers": ["panasonic_viera"], - "requirements": ["panasonic-viera==0.3.6"] + "requirements": ["panasonic-viera==0.4.2"] } diff --git a/homeassistant/components/panasonic_viera/media_player.py b/homeassistant/components/panasonic_viera/media_player.py index 44063022129..76ca76c1ca6 100644 --- a/homeassistant/components/panasonic_viera/media_player.py +++ b/homeassistant/components/panasonic_viera/media_player.py @@ -126,11 +126,11 @@ class PanasonicVieraTVEntity(MediaPlayerEntity): async def async_volume_up(self) -> None: """Volume up the media player.""" - await self._remote.async_send_key(Keys.volume_up) + await self._remote.async_send_key(Keys.VOLUME_UP) async def async_volume_down(self) -> None: """Volume down media player.""" - await self._remote.async_send_key(Keys.volume_down) + await self._remote.async_send_key(Keys.VOLUME_DOWN) async def async_mute_volume(self, mute: bool) -> None: """Send mute command.""" @@ -143,33 +143,33 @@ class PanasonicVieraTVEntity(MediaPlayerEntity): async def async_media_play_pause(self) -> None: """Simulate play pause media player.""" if self._remote.playing: - await self._remote.async_send_key(Keys.pause) + await self._remote.async_send_key(Keys.PAUSE) self._remote.playing = False else: - await self._remote.async_send_key(Keys.play) + await self._remote.async_send_key(Keys.PLAY) self._remote.playing = True async def async_media_play(self) -> None: """Send play command.""" - await self._remote.async_send_key(Keys.play) + await self._remote.async_send_key(Keys.PLAY) self._remote.playing = True async def async_media_pause(self) -> None: """Send pause command.""" - await self._remote.async_send_key(Keys.pause) + await self._remote.async_send_key(Keys.PAUSE) self._remote.playing = False async def async_media_stop(self) -> None: """Stop playback.""" - await self._remote.async_send_key(Keys.stop) + await self._remote.async_send_key(Keys.STOP) async def async_media_next_track(self) -> None: """Send the fast forward command.""" - await self._remote.async_send_key(Keys.fast_forward) + await self._remote.async_send_key(Keys.FAST_FORWARD) async def async_media_previous_track(self) -> None: """Send the rewind command.""" - await self._remote.async_send_key(Keys.rewind) + await self._remote.async_send_key(Keys.REWIND) async def async_play_media( self, media_type: MediaType | str, media_id: str, **kwargs: Any diff --git a/requirements_all.txt b/requirements_all.txt index d2e51a365e7..57cbb7b7710 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1528,7 +1528,7 @@ paho-mqtt==1.6.1 panacotta==0.2 # homeassistant.components.panasonic_viera -panasonic-viera==0.3.6 +panasonic-viera==0.4.2 # homeassistant.components.dunehd pdunehd==1.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 21e6eedebc7..306b791488a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1228,7 +1228,7 @@ p1monitor==3.0.0 paho-mqtt==1.6.1 # homeassistant.components.panasonic_viera -panasonic-viera==0.3.6 +panasonic-viera==0.4.2 # homeassistant.components.dunehd pdunehd==1.3.2 diff --git a/tests/components/panasonic_viera/test_remote.py b/tests/components/panasonic_viera/test_remote.py index 05254753d3f..3ae241fc5e9 100644 --- a/tests/components/panasonic_viera/test_remote.py +++ b/tests/components/panasonic_viera/test_remote.py @@ -46,7 +46,7 @@ async def test_onoff(hass: HomeAssistant, mock_remote) -> None: await hass.services.async_call(REMOTE_DOMAIN, SERVICE_TURN_ON, data) await hass.async_block_till_done() - power = getattr(Keys.power, "value", Keys.power) + power = getattr(Keys.POWER, "value", Keys.POWER) assert mock_remote.send_key.call_args_list == [call(power), call(power)] From ef3ecb618350a1f60b7650cd935399b3f74ff9c0 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 28 Jun 2024 08:36:10 +0200 Subject: [PATCH 054/146] Bump apsystems-ez1 to 1.3.3 (#120702) --- homeassistant/components/apsystems/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/apsystems/manifest.json b/homeassistant/components/apsystems/manifest.json index 8e0ac00796d..cba3e59dba0 100644 --- a/homeassistant/components/apsystems/manifest.json +++ b/homeassistant/components/apsystems/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/apsystems", "integration_type": "device", "iot_class": "local_polling", - "requirements": ["apsystems-ez1==1.3.1"] + "requirements": ["apsystems-ez1==1.3.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 57cbb7b7710..cd2a2335987 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -464,7 +464,7 @@ apprise==1.8.0 aprslib==0.7.2 # homeassistant.components.apsystems -apsystems-ez1==1.3.1 +apsystems-ez1==1.3.3 # homeassistant.components.aqualogic aqualogic==2.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 306b791488a..6bc406c6982 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -428,7 +428,7 @@ apprise==1.8.0 aprslib==0.7.2 # homeassistant.components.apsystems -apsystems-ez1==1.3.1 +apsystems-ez1==1.3.3 # homeassistant.components.aranet aranet4==2.3.4 From 1227d56aa219391484b2a07c0100109690ae782c Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 27 Jun 2024 23:12:20 +0200 Subject: [PATCH 055/146] Bump `nextdns` to version 3.1.0 (#120703) Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- homeassistant/components/nextdns/__init__.py | 6 +++--- homeassistant/components/nextdns/config_flow.py | 11 +++++------ homeassistant/components/nextdns/coordinator.py | 12 ++++++++---- homeassistant/components/nextdns/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/nextdns/test_config_flow.py | 2 ++ tests/components/nextdns/test_init.py | 9 +++++++-- tests/components/nextdns/test_switch.py | 13 +++++++++++-- 9 files changed, 39 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/nextdns/__init__.py b/homeassistant/components/nextdns/__init__.py index f11611007c2..4256126b3c7 100644 --- a/homeassistant/components/nextdns/__init__.py +++ b/homeassistant/components/nextdns/__init__.py @@ -18,6 +18,7 @@ from nextdns import ( NextDns, Settings, ) +from tenacity import RetryError from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, Platform @@ -84,9 +85,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: NextDnsConfigEntry) -> b websession = async_get_clientsession(hass) try: - async with asyncio.timeout(10): - nextdns = await NextDns.create(websession, api_key) - except (ApiError, ClientConnectorError, TimeoutError) as err: + nextdns = await NextDns.create(websession, api_key) + except (ApiError, ClientConnectorError, RetryError, TimeoutError) as err: raise ConfigEntryNotReady from err tasks = [] diff --git a/homeassistant/components/nextdns/config_flow.py b/homeassistant/components/nextdns/config_flow.py index 4955bbb4cad..bd79112b1f9 100644 --- a/homeassistant/components/nextdns/config_flow.py +++ b/homeassistant/components/nextdns/config_flow.py @@ -2,11 +2,11 @@ from __future__ import annotations -import asyncio from typing import Any from aiohttp.client_exceptions import ClientConnectorError from nextdns import ApiError, InvalidApiKeyError, NextDns +from tenacity import RetryError import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult @@ -37,13 +37,12 @@ class NextDnsFlowHandler(ConfigFlow, domain=DOMAIN): if user_input is not None: self.api_key = user_input[CONF_API_KEY] try: - async with asyncio.timeout(10): - self.nextdns = await NextDns.create( - websession, user_input[CONF_API_KEY] - ) + self.nextdns = await NextDns.create( + websession, user_input[CONF_API_KEY] + ) except InvalidApiKeyError: errors["base"] = "invalid_api_key" - except (ApiError, ClientConnectorError, TimeoutError): + except (ApiError, ClientConnectorError, RetryError, TimeoutError): errors["base"] = "cannot_connect" except Exception: # noqa: BLE001 errors["base"] = "unknown" diff --git a/homeassistant/components/nextdns/coordinator.py b/homeassistant/components/nextdns/coordinator.py index cad1aeac070..5210807bd3c 100644 --- a/homeassistant/components/nextdns/coordinator.py +++ b/homeassistant/components/nextdns/coordinator.py @@ -1,6 +1,5 @@ """NextDns coordinator.""" -import asyncio from datetime import timedelta import logging from typing import TypeVar @@ -19,6 +18,7 @@ from nextdns import ( Settings, ) from nextdns.model import NextDnsData +from tenacity import RetryError from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo @@ -58,9 +58,13 @@ class NextDnsUpdateCoordinator(DataUpdateCoordinator[CoordinatorDataT]): async def _async_update_data(self) -> CoordinatorDataT: """Update data via internal method.""" try: - async with asyncio.timeout(10): - return await self._async_update_data_internal() - except (ApiError, ClientConnectorError, InvalidApiKeyError) as err: + return await self._async_update_data_internal() + except ( + ApiError, + ClientConnectorError, + InvalidApiKeyError, + RetryError, + ) as err: raise UpdateFailed(err) from err async def _async_update_data_internal(self) -> CoordinatorDataT: diff --git a/homeassistant/components/nextdns/manifest.json b/homeassistant/components/nextdns/manifest.json index 1e7145ef6d1..b65706ef1ce 100644 --- a/homeassistant/components/nextdns/manifest.json +++ b/homeassistant/components/nextdns/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["nextdns"], "quality_scale": "platinum", - "requirements": ["nextdns==3.0.0"] + "requirements": ["nextdns==3.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index cd2a2335987..cf0e22167a0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1404,7 +1404,7 @@ nextcloudmonitor==1.5.0 nextcord==2.6.0 # homeassistant.components.nextdns -nextdns==3.0.0 +nextdns==3.1.0 # homeassistant.components.nibe_heatpump nibe==2.8.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6bc406c6982..f7254727d3c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1143,7 +1143,7 @@ nextcloudmonitor==1.5.0 nextcord==2.6.0 # homeassistant.components.nextdns -nextdns==3.0.0 +nextdns==3.1.0 # homeassistant.components.nibe_heatpump nibe==2.8.0 diff --git a/tests/components/nextdns/test_config_flow.py b/tests/components/nextdns/test_config_flow.py index 9247288eebf..7571eef347e 100644 --- a/tests/components/nextdns/test_config_flow.py +++ b/tests/components/nextdns/test_config_flow.py @@ -4,6 +4,7 @@ from unittest.mock import patch from nextdns import ApiError, InvalidApiKeyError import pytest +from tenacity import RetryError from homeassistant.components.nextdns.const import CONF_PROFILE_ID, DOMAIN from homeassistant.config_entries import SOURCE_USER @@ -57,6 +58,7 @@ async def test_form_create_entry(hass: HomeAssistant) -> None: [ (ApiError("API Error"), "cannot_connect"), (InvalidApiKeyError, "invalid_api_key"), + (RetryError("Retry Error"), "cannot_connect"), (TimeoutError, "cannot_connect"), (ValueError, "unknown"), ], diff --git a/tests/components/nextdns/test_init.py b/tests/components/nextdns/test_init.py index f7b85bb8a54..61a487d917c 100644 --- a/tests/components/nextdns/test_init.py +++ b/tests/components/nextdns/test_init.py @@ -3,6 +3,8 @@ from unittest.mock import patch from nextdns import ApiError +import pytest +from tenacity import RetryError from homeassistant.components.nextdns.const import CONF_PROFILE_ID, DOMAIN from homeassistant.config_entries import ConfigEntryState @@ -24,7 +26,10 @@ async def test_async_setup_entry(hass: HomeAssistant) -> None: assert state.state == "20.0" -async def test_config_not_ready(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + "exc", [ApiError("API Error"), RetryError("Retry Error"), TimeoutError] +) +async def test_config_not_ready(hass: HomeAssistant, exc: Exception) -> None: """Test for setup failure if the connection to the service fails.""" entry = MockConfigEntry( domain=DOMAIN, @@ -35,7 +40,7 @@ async def test_config_not_ready(hass: HomeAssistant) -> None: with patch( "homeassistant.components.nextdns.NextDns.get_profiles", - side_effect=ApiError("API Error"), + side_effect=exc, ): entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) diff --git a/tests/components/nextdns/test_switch.py b/tests/components/nextdns/test_switch.py index 059585e9ffe..6e344e34336 100644 --- a/tests/components/nextdns/test_switch.py +++ b/tests/components/nextdns/test_switch.py @@ -8,6 +8,7 @@ from aiohttp.client_exceptions import ClientConnectorError from nextdns import ApiError import pytest from syrupy import SnapshotAssertion +from tenacity import RetryError from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.const import ( @@ -94,7 +95,15 @@ async def test_switch_off(hass: HomeAssistant) -> None: mock_switch_on.assert_called_once() -async def test_availability(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + "exc", + [ + ApiError("API Error"), + RetryError("Retry Error"), + TimeoutError, + ], +) +async def test_availability(hass: HomeAssistant, exc: Exception) -> None: """Ensure that we mark the entities unavailable correctly when service causes an error.""" await init_integration(hass) @@ -106,7 +115,7 @@ async def test_availability(hass: HomeAssistant) -> None: future = utcnow() + timedelta(minutes=10) with patch( "homeassistant.components.nextdns.NextDns.get_settings", - side_effect=ApiError("API Error"), + side_effect=exc, ): async_fire_time_changed(hass, future) await hass.async_block_till_done(wait_background_tasks=True) From 35d145d3bc1d34f126864ad71c2d6f82694487b9 Mon Sep 17 00:00:00 2001 From: dougiteixeira <31328123+dougiteixeira@users.noreply.github.com> Date: Fri, 28 Jun 2024 07:22:24 -0300 Subject: [PATCH 056/146] Link the Statistics helper entity to the source entity device (#120705) --- .../components/statistics/__init__.py | 11 ++- homeassistant/components/statistics/sensor.py | 8 ++ tests/components/statistics/test_init.py | 92 +++++++++++++++++++ tests/components/statistics/test_sensor.py | 49 +++++++++- 4 files changed, 158 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/statistics/__init__.py b/homeassistant/components/statistics/__init__.py index 70739c618f7..f71274e0ee7 100644 --- a/homeassistant/components/statistics/__init__.py +++ b/homeassistant/components/statistics/__init__.py @@ -1,8 +1,11 @@ """The statistics component.""" from homeassistant.config_entries import ConfigEntry -from homeassistant.const import Platform +from homeassistant.const import CONF_ENTITY_ID, Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers.device import ( + async_remove_stale_devices_links_keep_entity_device, +) DOMAIN = "statistics" PLATFORMS = [Platform.SENSOR] @@ -11,6 +14,12 @@ PLATFORMS = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Statistics from a config entry.""" + async_remove_stale_devices_links_keep_entity_device( + hass, + entry.entry_id, + entry.options[CONF_ENTITY_ID], + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(update_listener)) diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index 8d28254ad61..ca1d75b57ed 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -43,6 +43,7 @@ from homeassistant.core import ( split_entity_id, ) from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.device import async_device_info_to_link_from_entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import ( async_track_point_in_utc_time, @@ -268,6 +269,7 @@ async def async_setup_platform( async_add_entities( new_entities=[ StatisticsSensor( + hass=hass, source_entity_id=config[CONF_ENTITY_ID], name=config[CONF_NAME], unique_id=config.get(CONF_UNIQUE_ID), @@ -304,6 +306,7 @@ async def async_setup_entry( async_add_entities( [ StatisticsSensor( + hass=hass, source_entity_id=entry.options[CONF_ENTITY_ID], name=entry.options[CONF_NAME], unique_id=entry.entry_id, @@ -327,6 +330,7 @@ class StatisticsSensor(SensorEntity): def __init__( self, + hass: HomeAssistant, source_entity_id: str, name: str, unique_id: str | None, @@ -341,6 +345,10 @@ class StatisticsSensor(SensorEntity): self._attr_name: str = name self._attr_unique_id: str | None = unique_id self._source_entity_id: str = source_entity_id + self._attr_device_info = async_device_info_to_link_from_entity( + hass, + source_entity_id, + ) self.is_binary: bool = ( split_entity_id(self._source_entity_id)[0] == BINARY_SENSOR_DOMAIN ) diff --git a/tests/components/statistics/test_init.py b/tests/components/statistics/test_init.py index 6cb943c0687..64829ea7d66 100644 --- a/tests/components/statistics/test_init.py +++ b/tests/components/statistics/test_init.py @@ -2,8 +2,10 @@ from __future__ import annotations +from homeassistant.components.statistics import DOMAIN as STATISTICS_DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er from tests.common import MockConfigEntry @@ -15,3 +17,93 @@ async def test_unload_entry(hass: HomeAssistant, loaded_entry: MockConfigEntry) assert await hass.config_entries.async_unload(loaded_entry.entry_id) await hass.async_block_till_done() assert loaded_entry.state is ConfigEntryState.NOT_LOADED + + +async def test_device_cleaning( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test the cleaning of devices linked to the helper Statistics.""" + + # Source entity device config entry + source_config_entry = MockConfigEntry() + source_config_entry.add_to_hass(hass) + + # Device entry of the source entity + source_device1_entry = device_registry.async_get_or_create( + config_entry_id=source_config_entry.entry_id, + identifiers={("sensor", "identifier_test1")}, + connections={("mac", "30:31:32:33:34:01")}, + ) + + # Source entity registry + source_entity = entity_registry.async_get_or_create( + "sensor", + "test", + "source", + config_entry=source_config_entry, + device_id=source_device1_entry.id, + ) + await hass.async_block_till_done() + assert entity_registry.async_get("sensor.test_source") is not None + + # Configure the configuration entry for Statistics + statistics_config_entry = MockConfigEntry( + data={}, + domain=STATISTICS_DOMAIN, + options={ + "name": "Statistics", + "entity_id": "sensor.test_source", + "state_characteristic": "mean", + "keep_last_sample": False, + "percentile": 50.0, + "precision": 2.0, + "sampling_size": 20.0, + }, + title="Statistics", + ) + statistics_config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(statistics_config_entry.entry_id) + await hass.async_block_till_done() + + # Confirm the link between the source entity device and the statistics sensor + statistics_entity = entity_registry.async_get("sensor.statistics") + assert statistics_entity is not None + assert statistics_entity.device_id == source_entity.device_id + + # Device entry incorrectly linked to Statistics config entry + device_registry.async_get_or_create( + config_entry_id=statistics_config_entry.entry_id, + identifiers={("sensor", "identifier_test2")}, + connections={("mac", "30:31:32:33:34:02")}, + ) + device_registry.async_get_or_create( + config_entry_id=statistics_config_entry.entry_id, + identifiers={("sensor", "identifier_test3")}, + connections={("mac", "30:31:32:33:34:03")}, + ) + await hass.async_block_till_done() + + # Before reloading the config entry, two devices are expected to be linked + devices_before_reload = device_registry.devices.get_devices_for_config_entry_id( + statistics_config_entry.entry_id + ) + assert len(devices_before_reload) == 3 + + # Config entry reload + await hass.config_entries.async_reload(statistics_config_entry.entry_id) + await hass.async_block_till_done() + + # Confirm the link between the source entity device and the statistics sensor + statistics_entity = entity_registry.async_get("sensor.statistics") + assert statistics_entity is not None + assert statistics_entity.device_id == source_entity.device_id + + # After reloading the config entry, only one linked device is expected + devices_after_reload = device_registry.devices.get_devices_for_config_entry_id( + statistics_config_entry.entry_id + ) + assert len(devices_after_reload) == 1 + + assert devices_after_reload[0].id == source_device1_entry.id diff --git a/tests/components/statistics/test_sensor.py b/tests/components/statistics/test_sensor.py index 269c17e34b9..c90d685714c 100644 --- a/tests/components/statistics/test_sensor.py +++ b/tests/components/statistics/test_sensor.py @@ -41,7 +41,7 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util @@ -1654,3 +1654,50 @@ async def test_reload(recorder_mock: Recorder, hass: HomeAssistant) -> None: assert hass.states.get("sensor.test") is None assert hass.states.get("sensor.cputest") + + +async def test_device_id( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, +) -> None: + """Test for source entity device for Statistics.""" + source_config_entry = MockConfigEntry() + source_config_entry.add_to_hass(hass) + source_device_entry = device_registry.async_get_or_create( + config_entry_id=source_config_entry.entry_id, + identifiers={("sensor", "identifier_test")}, + connections={("mac", "30:31:32:33:34:35")}, + ) + source_entity = entity_registry.async_get_or_create( + "sensor", + "test", + "source", + config_entry=source_config_entry, + device_id=source_device_entry.id, + ) + await hass.async_block_till_done() + assert entity_registry.async_get("sensor.test_source") is not None + + statistics_config_entry = MockConfigEntry( + data={}, + domain=STATISTICS_DOMAIN, + options={ + "name": "Statistics", + "entity_id": "sensor.test_source", + "state_characteristic": "mean", + "keep_last_sample": False, + "percentile": 50.0, + "precision": 2.0, + "sampling_size": 20.0, + }, + title="Statistics", + ) + statistics_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(statistics_config_entry.entry_id) + await hass.async_block_till_done() + + statistics_entity = entity_registry.async_get("sensor.statistics") + assert statistics_entity is not None + assert statistics_entity.device_id == source_entity.device_id From 3932ce57b9da11ffced4b76b7aa5c55186c628d4 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Fri, 28 Jun 2024 19:21:59 +1000 Subject: [PATCH 057/146] Check Tessie scopes to fix startup bug (#120710) * Add scope check * Add tests * Bump Teslemetry --- .../components/teslemetry/manifest.json | 2 +- homeassistant/components/tessie/__init__.py | 68 +++++++++++-------- homeassistant/components/tessie/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/tessie/common.py | 11 +++ tests/components/tessie/conftest.py | 11 +++ tests/components/tessie/test_init.py | 14 +++- 8 files changed, 76 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/teslemetry/manifest.json b/homeassistant/components/teslemetry/manifest.json index 2eb3e221855..49d73909a71 100644 --- a/homeassistant/components/teslemetry/manifest.json +++ b/homeassistant/components/teslemetry/manifest.json @@ -7,5 +7,5 @@ "iot_class": "cloud_polling", "loggers": ["tesla-fleet-api"], "quality_scale": "platinum", - "requirements": ["tesla-fleet-api==0.6.1"] + "requirements": ["tesla-fleet-api==0.6.2"] } diff --git a/homeassistant/components/tessie/__init__.py b/homeassistant/components/tessie/__init__.py index e8891d6665f..1d6e2a27786 100644 --- a/homeassistant/components/tessie/__init__.py +++ b/homeassistant/components/tessie/__init__.py @@ -6,6 +6,7 @@ import logging from aiohttp import ClientError, ClientResponseError from tesla_fleet_api import EnergySpecific, Tessie +from tesla_fleet_api.const import Scope from tesla_fleet_api.exceptions import TeslaFleetError from tessie_api import get_state_of_all_vehicles @@ -94,41 +95,48 @@ async def async_setup_entry(hass: HomeAssistant, entry: TessieConfigEntry) -> bo # Energy Sites tessie = Tessie(session, api_key) + energysites: list[TessieEnergyData] = [] + try: - products = (await tessie.products())["response"] + scopes = await tessie.scopes() except TeslaFleetError as e: raise ConfigEntryNotReady from e - energysites: list[TessieEnergyData] = [] - for product in products: - if "energy_site_id" in product: - site_id = product["energy_site_id"] - api = EnergySpecific(tessie.energy, site_id) - energysites.append( - TessieEnergyData( - api=api, - id=site_id, - live_coordinator=TessieEnergySiteLiveCoordinator(hass, api), - info_coordinator=TessieEnergySiteInfoCoordinator(hass, api), - device=DeviceInfo( - identifiers={(DOMAIN, str(site_id))}, - manufacturer="Tesla", - name=product.get("site_name", "Energy Site"), - ), - ) - ) + if Scope.ENERGY_DEVICE_DATA in scopes: + try: + products = (await tessie.products())["response"] + except TeslaFleetError as e: + raise ConfigEntryNotReady from e - # Populate coordinator data before forwarding to platforms - await asyncio.gather( - *( - energysite.live_coordinator.async_config_entry_first_refresh() - for energysite in energysites - ), - *( - energysite.info_coordinator.async_config_entry_first_refresh() - for energysite in energysites - ), - ) + for product in products: + if "energy_site_id" in product: + site_id = product["energy_site_id"] + api = EnergySpecific(tessie.energy, site_id) + energysites.append( + TessieEnergyData( + api=api, + id=site_id, + live_coordinator=TessieEnergySiteLiveCoordinator(hass, api), + info_coordinator=TessieEnergySiteInfoCoordinator(hass, api), + device=DeviceInfo( + identifiers={(DOMAIN, str(site_id))}, + manufacturer="Tesla", + name=product.get("site_name", "Energy Site"), + ), + ) + ) + + # Populate coordinator data before forwarding to platforms + await asyncio.gather( + *( + energysite.live_coordinator.async_config_entry_first_refresh() + for energysite in energysites + ), + *( + energysite.info_coordinator.async_config_entry_first_refresh() + for energysite in energysites + ), + ) entry.runtime_data = TessieData(vehicles, energysites) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/tessie/manifest.json b/homeassistant/components/tessie/manifest.json index bf1ab5f61e4..493feeaa77e 100644 --- a/homeassistant/components/tessie/manifest.json +++ b/homeassistant/components/tessie/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/tessie", "iot_class": "cloud_polling", "loggers": ["tessie"], - "requirements": ["tessie-api==0.0.9", "tesla-fleet-api==0.6.1"] + "requirements": ["tessie-api==0.0.9", "tesla-fleet-api==0.6.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index cf0e22167a0..ba007cb230f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2711,7 +2711,7 @@ temperusb==1.6.1 # homeassistant.components.teslemetry # homeassistant.components.tessie -tesla-fleet-api==0.6.1 +tesla-fleet-api==0.6.2 # homeassistant.components.powerwall tesla-powerwall==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f7254727d3c..05397f24983 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2109,7 +2109,7 @@ temperusb==1.6.1 # homeassistant.components.teslemetry # homeassistant.components.tessie -tesla-fleet-api==0.6.1 +tesla-fleet-api==0.6.2 # homeassistant.components.powerwall tesla-powerwall==0.5.2 diff --git a/tests/components/tessie/common.py b/tests/components/tessie/common.py index 3d24c6b233a..37a38fffaa4 100644 --- a/tests/components/tessie/common.py +++ b/tests/components/tessie/common.py @@ -54,6 +54,17 @@ LIVE_STATUS = load_json_object_fixture("live_status.json", DOMAIN) SITE_INFO = load_json_object_fixture("site_info.json", DOMAIN) RESPONSE_OK = {"response": {}, "error": None} COMMAND_OK = {"response": {"result": True, "reason": ""}} +SCOPES = [ + "user_data", + "vehicle_device_data", + "vehicle_cmds", + "vehicle_charging_cmds", + "energy_device_data", + "energy_cmds", + "offline_access", + "openid", +] +NO_SCOPES = ["user_data", "offline_access", "openid"] async def setup_platform( diff --git a/tests/components/tessie/conftest.py b/tests/components/tessie/conftest.py index 79cc9aa44c6..e0aba73af17 100644 --- a/tests/components/tessie/conftest.py +++ b/tests/components/tessie/conftest.py @@ -11,6 +11,7 @@ from .common import ( COMMAND_OK, LIVE_STATUS, PRODUCTS, + SCOPES, SITE_INFO, TEST_STATE_OF_ALL_VEHICLES, TEST_VEHICLE_STATE_ONLINE, @@ -51,6 +52,16 @@ def mock_get_state_of_all_vehicles(): # Fleet API +@pytest.fixture(autouse=True) +def mock_scopes(): + """Mock scopes function.""" + with patch( + "homeassistant.components.tessie.Tessie.scopes", + return_value=SCOPES, + ) as mock_scopes: + yield mock_scopes + + @pytest.fixture(autouse=True) def mock_products(): """Mock Tesla Fleet Api products method.""" diff --git a/tests/components/tessie/test_init.py b/tests/components/tessie/test_init.py index e37512ea8c4..921ef93b1ae 100644 --- a/tests/components/tessie/test_init.py +++ b/tests/components/tessie/test_init.py @@ -50,11 +50,21 @@ async def test_connection_failure( assert entry.state is ConfigEntryState.SETUP_RETRY -async def test_fleet_error(hass: HomeAssistant) -> None: - """Test init with a fleet error.""" +async def test_products_error(hass: HomeAssistant) -> None: + """Test init with a fleet error on products.""" with patch( "homeassistant.components.tessie.Tessie.products", side_effect=TeslaFleetError ): entry = await setup_platform(hass) assert entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_scopes_error(hass: HomeAssistant) -> None: + """Test init with a fleet error on scopes.""" + + with patch( + "homeassistant.components.tessie.Tessie.scopes", side_effect=TeslaFleetError + ): + entry = await setup_platform(hass) + assert entry.state is ConfigEntryState.SETUP_RETRY From 76780ca04eaf4c23ed1029d9c0acf2b1c9d2e37f Mon Sep 17 00:00:00 2001 From: Dave Leaver Date: Fri, 28 Jun 2024 19:44:54 +1200 Subject: [PATCH 058/146] Bump airtouch5py to 1.2.0 (#120715) * Bump airtouch5py to fix console 1.2.0 * Bump airtouch5py again --- homeassistant/components/airtouch5/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airtouch5/manifest.json b/homeassistant/components/airtouch5/manifest.json index 0d4cbc32761..312a627d0e8 100644 --- a/homeassistant/components/airtouch5/manifest.json +++ b/homeassistant/components/airtouch5/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/airtouch5", "iot_class": "local_push", "loggers": ["airtouch5py"], - "requirements": ["airtouch5py==0.2.8"] + "requirements": ["airtouch5py==0.2.10"] } diff --git a/requirements_all.txt b/requirements_all.txt index ba007cb230f..788a1ff1be7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -428,7 +428,7 @@ airthings-cloud==0.2.0 airtouch4pyapi==1.0.5 # homeassistant.components.airtouch5 -airtouch5py==0.2.8 +airtouch5py==0.2.10 # homeassistant.components.alpha_vantage alpha-vantage==2.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 05397f24983..d41bfa7f997 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -401,7 +401,7 @@ airthings-cloud==0.2.0 airtouch4pyapi==1.0.5 # homeassistant.components.airtouch5 -airtouch5py==0.2.8 +airtouch5py==0.2.10 # homeassistant.components.amberelectric amberelectric==1.1.0 From 0ae11b033576ae41303f9ad68a8e29827a491084 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 28 Jun 2024 11:03:01 +0200 Subject: [PATCH 059/146] Bump renault-api to 0.2.4 (#120727) --- homeassistant/components/renault/binary_sensor.py | 2 +- homeassistant/components/renault/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/renault/fixtures/hvac_status.1.json | 2 +- tests/components/renault/fixtures/hvac_status.2.json | 2 +- tests/components/renault/snapshots/test_diagnostics.ambr | 4 ++-- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/renault/binary_sensor.py b/homeassistant/components/renault/binary_sensor.py index 7ebc77b8e77..2041499b711 100644 --- a/homeassistant/components/renault/binary_sensor.py +++ b/homeassistant/components/renault/binary_sensor.py @@ -81,7 +81,7 @@ BINARY_SENSOR_TYPES: tuple[RenaultBinarySensorEntityDescription, ...] = tuple( key="hvac_status", coordinator="hvac_status", on_key="hvacStatus", - on_value=2, + on_value="on", translation_key="hvac_status", ), RenaultBinarySensorEntityDescription( diff --git a/homeassistant/components/renault/manifest.json b/homeassistant/components/renault/manifest.json index 8407893011c..ffa1cd6acef 100644 --- a/homeassistant/components/renault/manifest.json +++ b/homeassistant/components/renault/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["renault_api"], "quality_scale": "platinum", - "requirements": ["renault-api==0.2.3"] + "requirements": ["renault-api==0.2.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index 788a1ff1be7..e6ce960ebfc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2454,7 +2454,7 @@ refoss-ha==1.2.1 regenmaschine==2024.03.0 # homeassistant.components.renault -renault-api==0.2.3 +renault-api==0.2.4 # homeassistant.components.renson renson-endura-delta==1.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d41bfa7f997..37c5e47c5fa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1918,7 +1918,7 @@ refoss-ha==1.2.1 regenmaschine==2024.03.0 # homeassistant.components.renault -renault-api==0.2.3 +renault-api==0.2.4 # homeassistant.components.renson renson-endura-delta==1.7.1 diff --git a/tests/components/renault/fixtures/hvac_status.1.json b/tests/components/renault/fixtures/hvac_status.1.json index 7cbd7a9fe37..f48cbae68ae 100644 --- a/tests/components/renault/fixtures/hvac_status.1.json +++ b/tests/components/renault/fixtures/hvac_status.1.json @@ -2,6 +2,6 @@ "data": { "type": "Car", "id": "VF1AAAAA555777999", - "attributes": { "externalTemperature": 8.0, "hvacStatus": 1 } + "attributes": { "externalTemperature": 8.0, "hvacStatus": "off" } } } diff --git a/tests/components/renault/fixtures/hvac_status.2.json b/tests/components/renault/fixtures/hvac_status.2.json index 8bb4f941e06..a2ca08a71e9 100644 --- a/tests/components/renault/fixtures/hvac_status.2.json +++ b/tests/components/renault/fixtures/hvac_status.2.json @@ -4,7 +4,7 @@ "id": "VF1AAAAA555777999", "attributes": { "socThreshold": 30.0, - "hvacStatus": 1, + "hvacStatus": "off", "lastUpdateTime": "2020-12-03T00:00:00Z" } } diff --git a/tests/components/renault/snapshots/test_diagnostics.ambr b/tests/components/renault/snapshots/test_diagnostics.ambr index ae90115fcb6..a2921dff35e 100644 --- a/tests/components/renault/snapshots/test_diagnostics.ambr +++ b/tests/components/renault/snapshots/test_diagnostics.ambr @@ -22,7 +22,7 @@ }), 'hvac_status': dict({ 'externalTemperature': 8.0, - 'hvacStatus': 1, + 'hvacStatus': 'off', }), 'res_state': dict({ }), @@ -227,7 +227,7 @@ }), 'hvac_status': dict({ 'externalTemperature': 8.0, - 'hvacStatus': 1, + 'hvacStatus': 'off', }), 'res_state': dict({ }), From fe8b5656dd166bf25f4f3504140f6baf6619daef Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 28 Jun 2024 13:26:31 +0200 Subject: [PATCH 060/146] Separate renault strings (#120737) --- homeassistant/components/renault/strings.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/renault/strings.json b/homeassistant/components/renault/strings.json index 322f7a207d7..5217b4ff65a 100644 --- a/homeassistant/components/renault/strings.json +++ b/homeassistant/components/renault/strings.json @@ -73,9 +73,9 @@ "charge_mode": { "name": "Charge mode", "state": { - "always": "Instant", - "always_charging": "[%key:component::renault::entity::select::charge_mode::state::always%]", - "schedule_mode": "Planner", + "always": "Always", + "always_charging": "Always charging", + "schedule_mode": "Schedule mode", "scheduled": "Scheduled" } } From c5fa9ad2729f83eef666e58a21f6a7db9721df88 Mon Sep 17 00:00:00 2001 From: Illia <146177275+ikalnyi@users.noreply.github.com> Date: Fri, 28 Jun 2024 12:14:44 +0200 Subject: [PATCH 061/146] Bump asyncarve to 0.1.1 (#120740) --- homeassistant/components/arve/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/arve/manifest.json b/homeassistant/components/arve/manifest.json index fa33b3309ce..4c63d377371 100644 --- a/homeassistant/components/arve/manifest.json +++ b/homeassistant/components/arve/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/arve", "iot_class": "cloud_polling", - "requirements": ["asyncarve==0.0.9"] + "requirements": ["asyncarve==0.1.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index e6ce960ebfc..34eb86f9ddc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -493,7 +493,7 @@ asterisk_mbox==0.5.0 async-upnp-client==0.39.0 # homeassistant.components.arve -asyncarve==0.0.9 +asyncarve==0.1.1 # homeassistant.components.keyboard_remote asyncinotify==4.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 37c5e47c5fa..c08fe273b8e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -448,7 +448,7 @@ asterisk_mbox==0.5.0 async-upnp-client==0.39.0 # homeassistant.components.arve -asyncarve==0.0.9 +asyncarve==0.1.1 # homeassistant.components.sleepiq asyncsleepiq==1.5.2 From cada78496b048e33f9843ef72f43a76c30772494 Mon Sep 17 00:00:00 2001 From: tronikos Date: Fri, 28 Jun 2024 04:25:55 -0700 Subject: [PATCH 062/146] Fix Google Generative AI: 400 Request contains an invalid argument (#120741) --- .../conversation.py | 9 +- .../snapshots/test_conversation.ambr | 166 ++++++++++++++++++ .../test_conversation.py | 83 +++++++++ 3 files changed, 255 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google_generative_ai_conversation/conversation.py b/homeassistant/components/google_generative_ai_conversation/conversation.py index fb7f5c3b21c..8052ee66f40 100644 --- a/homeassistant/components/google_generative_ai_conversation/conversation.py +++ b/homeassistant/components/google_generative_ai_conversation/conversation.py @@ -95,9 +95,12 @@ def _format_tool( ) -> dict[str, Any]: """Format tool specification.""" - parameters = _format_schema( - convert(tool.parameters, custom_serializer=custom_serializer) - ) + if tool.parameters.schema: + parameters = _format_schema( + convert(tool.parameters, custom_serializer=custom_serializer) + ) + else: + parameters = None return protos.Tool( { diff --git a/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr b/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr index b0a0ce967de..7f28c172970 100644 --- a/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr +++ b/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr @@ -409,3 +409,169 @@ ), ]) # --- +# name: test_function_call + list([ + tuple( + '', + tuple( + ), + dict({ + 'generation_config': dict({ + 'max_output_tokens': 150, + 'temperature': 1.0, + 'top_k': 64, + 'top_p': 0.95, + }), + 'model_name': 'models/gemini-1.5-flash-latest', + 'safety_settings': dict({ + 'DANGEROUS': 'BLOCK_MEDIUM_AND_ABOVE', + 'HARASSMENT': 'BLOCK_MEDIUM_AND_ABOVE', + 'HATE': 'BLOCK_MEDIUM_AND_ABOVE', + 'SEXUAL': 'BLOCK_MEDIUM_AND_ABOVE', + }), + 'system_instruction': ''' + Current time is 05:00:00. Today's date is 2024-05-24. + You are a voice assistant for Home Assistant. + Answer questions about the world truthfully. + Answer in plain text. Keep it simple and to the point. + Only if the user wants to control a device, tell them to expose entities to their voice assistant in Home Assistant. + ''', + 'tools': list([ + function_declarations { + name: "test_tool" + description: "Test function" + parameters { + type_: OBJECT + properties { + key: "param1" + value { + type_: ARRAY + description: "Test parameters" + items { + type_: STRING + format_: "lower" + } + } + } + } + } + , + ]), + }), + ), + tuple( + '().start_chat', + tuple( + ), + dict({ + 'history': list([ + ]), + }), + ), + tuple( + '().start_chat().send_message_async', + tuple( + 'Please call the test function', + ), + dict({ + }), + ), + tuple( + '().start_chat().send_message_async', + tuple( + parts { + function_response { + name: "test_tool" + response { + fields { + key: "result" + value { + string_value: "Test response" + } + } + } + } + } + , + ), + dict({ + }), + ), + ]) +# --- +# name: test_function_call_without_parameters + list([ + tuple( + '', + tuple( + ), + dict({ + 'generation_config': dict({ + 'max_output_tokens': 150, + 'temperature': 1.0, + 'top_k': 64, + 'top_p': 0.95, + }), + 'model_name': 'models/gemini-1.5-flash-latest', + 'safety_settings': dict({ + 'DANGEROUS': 'BLOCK_MEDIUM_AND_ABOVE', + 'HARASSMENT': 'BLOCK_MEDIUM_AND_ABOVE', + 'HATE': 'BLOCK_MEDIUM_AND_ABOVE', + 'SEXUAL': 'BLOCK_MEDIUM_AND_ABOVE', + }), + 'system_instruction': ''' + Current time is 05:00:00. Today's date is 2024-05-24. + You are a voice assistant for Home Assistant. + Answer questions about the world truthfully. + Answer in plain text. Keep it simple and to the point. + Only if the user wants to control a device, tell them to expose entities to their voice assistant in Home Assistant. + ''', + 'tools': list([ + function_declarations { + name: "test_tool" + description: "Test function" + } + , + ]), + }), + ), + tuple( + '().start_chat', + tuple( + ), + dict({ + 'history': list([ + ]), + }), + ), + tuple( + '().start_chat().send_message_async', + tuple( + 'Please call the test function', + ), + dict({ + }), + ), + tuple( + '().start_chat().send_message_async', + tuple( + parts { + function_response { + name: "test_tool" + response { + fields { + key: "result" + value { + string_value: "Test response" + } + } + } + } + } + , + ), + dict({ + }), + ), + ]) +# --- diff --git a/tests/components/google_generative_ai_conversation/test_conversation.py b/tests/components/google_generative_ai_conversation/test_conversation.py index 990058aa89d..30016335f3b 100644 --- a/tests/components/google_generative_ai_conversation/test_conversation.py +++ b/tests/components/google_generative_ai_conversation/test_conversation.py @@ -172,6 +172,7 @@ async def test_function_call( hass: HomeAssistant, mock_config_entry_with_assist: MockConfigEntry, mock_init_component, + snapshot: SnapshotAssertion, ) -> None: """Test function calling.""" agent_id = mock_config_entry_with_assist.entry_id @@ -256,6 +257,7 @@ async def test_function_call( device_id="test_device", ), ) + assert [tuple(mock_call) for mock_call in mock_model.mock_calls] == snapshot # Test conversating tracing traces = trace.async_get_traces() @@ -272,6 +274,87 @@ async def test_function_call( assert "Answer in plain text" in detail_event["data"]["prompt"] +@patch( + "homeassistant.components.google_generative_ai_conversation.conversation.llm.AssistAPI._async_get_tools" +) +async def test_function_call_without_parameters( + mock_get_tools, + hass: HomeAssistant, + mock_config_entry_with_assist: MockConfigEntry, + mock_init_component, + snapshot: SnapshotAssertion, +) -> None: + """Test function calling without parameters.""" + agent_id = mock_config_entry_with_assist.entry_id + context = Context() + + mock_tool = AsyncMock() + mock_tool.name = "test_tool" + mock_tool.description = "Test function" + mock_tool.parameters = vol.Schema({}) + + mock_get_tools.return_value = [mock_tool] + + with patch("google.generativeai.GenerativeModel") as mock_model: + mock_chat = AsyncMock() + mock_model.return_value.start_chat.return_value = mock_chat + chat_response = MagicMock() + mock_chat.send_message_async.return_value = chat_response + mock_part = MagicMock() + mock_part.function_call = FunctionCall(name="test_tool", args={}) + + def tool_call(hass, tool_input, tool_context): + mock_part.function_call = None + mock_part.text = "Hi there!" + return {"result": "Test response"} + + mock_tool.async_call.side_effect = tool_call + chat_response.parts = [mock_part] + result = await conversation.async_converse( + hass, + "Please call the test function", + None, + context, + agent_id=agent_id, + device_id="test_device", + ) + + assert result.response.response_type == intent.IntentResponseType.ACTION_DONE + assert result.response.as_dict()["speech"]["plain"]["speech"] == "Hi there!" + mock_tool_call = mock_chat.send_message_async.mock_calls[1][1][0] + mock_tool_call = type(mock_tool_call).to_dict(mock_tool_call) + assert mock_tool_call == { + "parts": [ + { + "function_response": { + "name": "test_tool", + "response": { + "result": "Test response", + }, + }, + }, + ], + "role": "", + } + + mock_tool.async_call.assert_awaited_once_with( + hass, + llm.ToolInput( + tool_name="test_tool", + tool_args={}, + ), + llm.LLMContext( + platform="google_generative_ai_conversation", + context=context, + user_prompt="Please call the test function", + language="en", + assistant="conversation", + device_id="test_device", + ), + ) + assert [tuple(mock_call) for mock_call in mock_model.mock_calls] == snapshot + + @patch( "homeassistant.components.google_generative_ai_conversation.conversation.llm.AssistAPI._async_get_tools" ) From d7a59748cf7d80b0922a7e24578a42b5ade63504 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 28 Jun 2024 13:38:24 +0200 Subject: [PATCH 063/146] Bump version to 2024.7.0b4 --- 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 33a86f57a5e..1ab2a3f6893 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "0b3" +PATCH_VERSION: Final = "0b4" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index e4ccd9898e0..e96c329fd5a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.7.0b3" +version = "2024.7.0b4" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 2d5961fa4f606e15422a4d0a8fe1c9fd00d3b105 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 29 Jun 2024 08:45:51 -0700 Subject: [PATCH 064/146] Bump gcal_sync to 6.1.3 (#120278) --- homeassistant/components/google/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/google/test_calendar.py | 12 ++++++++++++ 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index 062bf58d2f5..5fc28d2f398 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/calendar.google", "iot_class": "cloud_polling", "loggers": ["googleapiclient"], - "requirements": ["gcal-sync==6.0.4", "oauth2client==4.1.3", "ical==8.0.1"] + "requirements": ["gcal-sync==6.1.3", "oauth2client==4.1.3", "ical==8.0.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 34eb86f9ddc..f351dc563ed 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -924,7 +924,7 @@ gardena-bluetooth==1.4.2 gassist-text==0.0.11 # homeassistant.components.google -gcal-sync==6.0.4 +gcal-sync==6.1.3 # homeassistant.components.geniushub geniushub-client==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c08fe273b8e..a29f8c7d5cf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -762,7 +762,7 @@ gardena-bluetooth==1.4.2 gassist-text==0.0.11 # homeassistant.components.google -gcal-sync==6.0.4 +gcal-sync==6.1.3 # homeassistant.components.geocaching geocachingapi==0.2.1 diff --git a/tests/components/google/test_calendar.py b/tests/components/google/test_calendar.py index 8e934925f46..5fe26585fe5 100644 --- a/tests/components/google/test_calendar.py +++ b/tests/components/google/test_calendar.py @@ -385,6 +385,9 @@ async def test_update_error( with patch("homeassistant.util.utcnow", return_value=now): async_fire_time_changed(hass, now) await hass.async_block_till_done() + # Ensure coordinator update completes + await hass.async_block_till_done() + await hass.async_block_till_done() # Entity is marked uanvailable due to API failure state = hass.states.get(TEST_ENTITY) @@ -414,6 +417,9 @@ async def test_update_error( with patch("homeassistant.util.utcnow", return_value=now): async_fire_time_changed(hass, now) await hass.async_block_till_done() + # Ensure coordinator update completes + await hass.async_block_till_done() + await hass.async_block_till_done() # State updated with new API response state = hass.states.get(TEST_ENTITY) @@ -606,6 +612,9 @@ async def test_future_event_update_behavior( freezer.move_to(now) async_fire_time_changed(hass, now) await hass.async_block_till_done() + # Ensure coordinator update completes + await hass.async_block_till_done() + await hass.async_block_till_done() # Event has started state = hass.states.get(TEST_ENTITY) @@ -643,6 +652,9 @@ async def test_future_event_offset_update_behavior( freezer.move_to(now) async_fire_time_changed(hass, now) await hass.async_block_till_done() + # Ensure coordinator update completes + await hass.async_block_till_done() + await hass.async_block_till_done() # Event has not started, but the offset was reached state = hass.states.get(TEST_ENTITY) From 5fd589053aa17b390d138a19cb9c38d95e2397d9 Mon Sep 17 00:00:00 2001 From: wittypluck Date: Fri, 28 Jun 2024 22:47:20 +0200 Subject: [PATCH 065/146] Reject small uptime updates for Unifi clients (#120398) Extend logic to reject small uptime updates to Unifi clients + add unit tests --- homeassistant/components/unifi/sensor.py | 5 ++-- tests/components/unifi/test_sensor.py | 36 ++++++++++++++++++------ 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index 028d70d8880..071230a9652 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -139,7 +139,7 @@ def async_device_uptime_value_fn(hub: UnifiHub, device: Device) -> datetime | No @callback -def async_device_uptime_value_changed_fn( +def async_uptime_value_changed_fn( old: StateType | date | datetime | Decimal, new: datetime | float | str | None ) -> bool: """Reject the new uptime value if it's too similar to the old one. Avoids unwanted fluctuation.""" @@ -310,6 +310,7 @@ ENTITY_DESCRIPTIONS: tuple[UnifiSensorEntityDescription, ...] = ( supported_fn=lambda hub, _: hub.config.option_allow_uptime_sensors, unique_id_fn=lambda hub, obj_id: f"uptime-{obj_id}", value_fn=async_client_uptime_value_fn, + value_changed_fn=async_uptime_value_changed_fn, ), UnifiSensorEntityDescription[Wlans, Wlan]( key="WLAN clients", @@ -396,7 +397,7 @@ ENTITY_DESCRIPTIONS: tuple[UnifiSensorEntityDescription, ...] = ( object_fn=lambda api, obj_id: api.devices[obj_id], unique_id_fn=lambda hub, obj_id: f"device_uptime-{obj_id}", value_fn=async_device_uptime_value_fn, - value_changed_fn=async_device_uptime_value_changed_fn, + value_changed_fn=async_uptime_value_changed_fn, ), UnifiSensorEntityDescription[Devices, Device]( key="Device temperature", diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index 960a5d3e529..48e524aef76 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -484,12 +484,12 @@ async def test_bandwidth_sensors( ], ) @pytest.mark.parametrize( - ("initial_uptime", "event_uptime", "new_uptime"), + ("initial_uptime", "event_uptime", "small_variation_uptime", "new_uptime"), [ # Uptime listed in epoch time should never change - (1609462800, 1609462800, 1612141200), + (1609462800, 1609462800, 1609462800, 1612141200), # Uptime counted in seconds increases with every event - (60, 64, 60), + (60, 240, 480, 60), ], ) @pytest.mark.usefixtures("entity_registry_enabled_by_default") @@ -503,6 +503,7 @@ async def test_uptime_sensors( client_payload: list[dict[str, Any]], initial_uptime, event_uptime, + small_variation_uptime, new_uptime, ) -> None: """Verify that uptime sensors are working as expected.""" @@ -519,15 +520,24 @@ async def test_uptime_sensors( ) # Verify normal new event doesn't change uptime - # 4 seconds has passed + # 4 minutes have passed uptime_client["uptime"] = event_uptime - now = datetime(2021, 1, 1, 1, 1, 4, tzinfo=dt_util.UTC) + now = datetime(2021, 1, 1, 1, 4, 0, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.now", return_value=now): mock_websocket_message(message=MessageKey.CLIENT, data=uptime_client) await hass.async_block_till_done() assert hass.states.get("sensor.client1_uptime").state == "2021-01-01T01:00:00+00:00" + # Verify small variation of uptime (<120 seconds) is ignored + # 15 seconds variation after 8 minutes + uptime_client["uptime"] = small_variation_uptime + now = datetime(2021, 1, 1, 1, 8, 15, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.now", return_value=now): + mock_websocket_message(message=MessageKey.CLIENT, data=uptime_client) + + assert hass.states.get("sensor.client1_uptime").state == "2021-01-01T01:00:00+00:00" + # Verify new event change uptime # 1 month has passed uptime_client["uptime"] = new_uptime @@ -911,10 +921,20 @@ async def test_device_uptime( ) # Verify normal new event doesn't change uptime - # 4 seconds has passed + # 4 minutes have passed device = device_payload[0] - device["uptime"] = 64 - now = datetime(2021, 1, 1, 1, 1, 4, tzinfo=dt_util.UTC) + device["uptime"] = 240 + now = datetime(2021, 1, 1, 1, 4, 0, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.now", return_value=now): + mock_websocket_message(message=MessageKey.DEVICE, data=device) + + assert hass.states.get("sensor.device_uptime").state == "2021-01-01T01:00:00+00:00" + + # Verify small variation of uptime (<120 seconds) is ignored + # 15 seconds variation after 8 minutes + device = device_payload[0] + device["uptime"] = 480 + now = datetime(2021, 1, 1, 1, 8, 15, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.now", return_value=now): mock_websocket_message(message=MessageKey.DEVICE, data=device) From b350ba9657c88f7edf3b27786de8dbcee1ab7371 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexey=20ALERT=20Rubash=D1=91ff?= Date: Fri, 28 Jun 2024 11:45:27 +0300 Subject: [PATCH 066/146] Add electrical consumption sensor to Overkiz (#120717) electrical consumption sensor --- homeassistant/components/overkiz/sensor.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/overkiz/sensor.py b/homeassistant/components/overkiz/sensor.py index c62840eea97..d313faf0c1d 100644 --- a/homeassistant/components/overkiz/sensor.py +++ b/homeassistant/components/overkiz/sensor.py @@ -182,6 +182,13 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ native_unit_of_measurement=UnitOfPower.WATT, state_class=SensorStateClass.MEASUREMENT, ), + OverkizSensorDescription( + key=OverkizState.MODBUSLINK_POWER_HEAT_ELECTRICAL, + name="Electric power consumption", + device_class=SensorDeviceClass.POWER, + native_unit_of_measurement=UnitOfPower.WATT, + state_class=SensorStateClass.MEASUREMENT, + ), OverkizSensorDescription( key=OverkizState.CORE_CONSUMPTION_TARIFF1, name="Consumption tariff 1", From 8994ab1686f729e83cc45b44182da5c1564446de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexey=20ALERT=20Rubash=D1=91ff?= Date: Fri, 28 Jun 2024 11:53:07 +0300 Subject: [PATCH 067/146] Add warm water remaining volume sensor to Overkiz (#120718) * warm water remaining volume sensor * Update homeassistant/components/overkiz/sensor.py Co-authored-by: Dave T <17680170+davet2001@users.noreply.github.com> --------- Co-authored-by: Dave T <17680170+davet2001@users.noreply.github.com> --- homeassistant/components/overkiz/sensor.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/overkiz/sensor.py b/homeassistant/components/overkiz/sensor.py index d313faf0c1d..bf9608358eb 100644 --- a/homeassistant/components/overkiz/sensor.py +++ b/homeassistant/components/overkiz/sensor.py @@ -420,6 +420,13 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfTemperature.CELSIUS, ), + OverkizSensorDescription( + key=OverkizState.CORE_REMAINING_HOT_WATER, + name="Warm water remaining", + device_class=SensorDeviceClass.VOLUME, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfVolume.LITERS, + ), # Cover OverkizSensorDescription( key=OverkizState.CORE_TARGET_CLOSURE, From f57c9429019bde0b16fa675143deea7f54201642 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 28 Jun 2024 14:53:29 +0200 Subject: [PATCH 068/146] Bump sense-energy to 0.12.4 (#120744) * Bump sense-energy to 0.12.4 * Fix --- homeassistant/components/emulated_kasa/manifest.json | 2 +- homeassistant/components/sense/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/emulated_kasa/manifest.json b/homeassistant/components/emulated_kasa/manifest.json index 843aeddde7b..640a2113d6f 100644 --- a/homeassistant/components/emulated_kasa/manifest.json +++ b/homeassistant/components/emulated_kasa/manifest.json @@ -6,5 +6,5 @@ "iot_class": "local_push", "loggers": ["sense_energy"], "quality_scale": "internal", - "requirements": ["sense-energy==0.12.2"] + "requirements": ["sense-energy==0.12.4"] } diff --git a/homeassistant/components/sense/manifest.json b/homeassistant/components/sense/manifest.json index 7ef1caefe48..116b714ba82 100644 --- a/homeassistant/components/sense/manifest.json +++ b/homeassistant/components/sense/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/sense", "iot_class": "cloud_polling", "loggers": ["sense_energy"], - "requirements": ["sense-energy==0.12.2"] + "requirements": ["sense-energy==0.12.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index f351dc563ed..b26425b30bc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2539,7 +2539,7 @@ sendgrid==6.8.2 # homeassistant.components.emulated_kasa # homeassistant.components.sense -sense-energy==0.12.2 +sense-energy==0.12.4 # homeassistant.components.sensirion_ble sensirion-ble==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a29f8c7d5cf..e95c0b7d600 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1976,7 +1976,7 @@ securetar==2024.2.1 # homeassistant.components.emulated_kasa # homeassistant.components.sense -sense-energy==0.12.2 +sense-energy==0.12.4 # homeassistant.components.sensirion_ble sensirion-ble==0.1.1 From 20ac0aa7b116683adcaabd8be8c72c1dd097fae3 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 28 Jun 2024 16:21:32 +0200 Subject: [PATCH 069/146] Bump govee-local-api to 1.5.1 (#120747) --- homeassistant/components/govee_light_local/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_light_local/manifest.json b/homeassistant/components/govee_light_local/manifest.json index 93a19408182..168a13e2477 100644 --- a/homeassistant/components/govee_light_local/manifest.json +++ b/homeassistant/components/govee_light_local/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["network"], "documentation": "https://www.home-assistant.io/integrations/govee_light_local", "iot_class": "local_push", - "requirements": ["govee-local-api==1.5.0"] + "requirements": ["govee-local-api==1.5.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index b26425b30bc..c2bcb3ed65e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -995,7 +995,7 @@ gotailwind==0.2.3 govee-ble==0.31.3 # homeassistant.components.govee_light_local -govee-local-api==1.5.0 +govee-local-api==1.5.1 # homeassistant.components.remote_rpi_gpio gpiozero==1.6.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e95c0b7d600..7197bc17a04 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -821,7 +821,7 @@ gotailwind==0.2.3 govee-ble==0.31.3 # homeassistant.components.govee_light_local -govee-local-api==1.5.0 +govee-local-api==1.5.1 # homeassistant.components.gpsd gps3==0.33.3 From 83df47030738164f1454c5ec731444bf9f1ca911 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Fri, 28 Jun 2024 16:14:31 +0200 Subject: [PATCH 070/146] Bump easyenergy lib to v2.1.2 (#120753) --- homeassistant/components/easyenergy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/easyenergy/manifest.json b/homeassistant/components/easyenergy/manifest.json index 4dcce0fd705..4d45dc2d399 100644 --- a/homeassistant/components/easyenergy/manifest.json +++ b/homeassistant/components/easyenergy/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/easyenergy", "iot_class": "cloud_polling", "quality_scale": "platinum", - "requirements": ["easyenergy==2.1.1"] + "requirements": ["easyenergy==2.1.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index c2bcb3ed65e..a98be6b1a9f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -771,7 +771,7 @@ dynalite-panel==0.0.4 eagle100==0.1.1 # homeassistant.components.easyenergy -easyenergy==2.1.1 +easyenergy==2.1.2 # homeassistant.components.ebusd ebusdpy==0.0.17 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7197bc17a04..834c6c5bf99 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -643,7 +643,7 @@ dynalite-panel==0.0.4 eagle100==0.1.1 # homeassistant.components.easyenergy -easyenergy==2.1.1 +easyenergy==2.1.2 # homeassistant.components.electric_kiwi electrickiwi-api==0.8.5 From 6028e5b77add83c795e1be3c61271bd296da5711 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Fri, 28 Jun 2024 16:20:44 +0200 Subject: [PATCH 071/146] Bump p1monitor lib to v3.0.1 (#120756) --- homeassistant/components/p1_monitor/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/p1_monitor/manifest.json b/homeassistant/components/p1_monitor/manifest.json index 0dfe1f3a46c..4702de3546d 100644 --- a/homeassistant/components/p1_monitor/manifest.json +++ b/homeassistant/components/p1_monitor/manifest.json @@ -7,5 +7,5 @@ "iot_class": "local_polling", "loggers": ["p1monitor"], "quality_scale": "platinum", - "requirements": ["p1monitor==3.0.0"] + "requirements": ["p1monitor==3.0.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index a98be6b1a9f..3ce72af693b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1519,7 +1519,7 @@ ourgroceries==1.5.4 ovoenergy==2.0.0 # homeassistant.components.p1_monitor -p1monitor==3.0.0 +p1monitor==3.0.1 # homeassistant.components.mqtt paho-mqtt==1.6.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 834c6c5bf99..f87edeff701 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1222,7 +1222,7 @@ ourgroceries==1.5.4 ovoenergy==2.0.0 # homeassistant.components.p1_monitor -p1monitor==3.0.0 +p1monitor==3.0.1 # homeassistant.components.mqtt paho-mqtt==1.6.1 From 59bb8b360e636ce115d92e27e9668b39ba7ed725 Mon Sep 17 00:00:00 2001 From: Clifford Roche <1007595+cmroche@users.noreply.github.com> Date: Sat, 29 Jun 2024 02:25:22 -0400 Subject: [PATCH 072/146] Bump greeclimate to 1.4.6 (#120758) --- homeassistant/components/gree/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/gree/manifest.json b/homeassistant/components/gree/manifest.json index 58404e90353..a7c884c4042 100644 --- a/homeassistant/components/gree/manifest.json +++ b/homeassistant/components/gree/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/gree", "iot_class": "local_polling", "loggers": ["greeclimate"], - "requirements": ["greeclimate==1.4.1"] + "requirements": ["greeclimate==1.4.6"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3ce72af693b..873a2460c1f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1004,7 +1004,7 @@ gpiozero==1.6.2 gps3==0.33.3 # homeassistant.components.gree -greeclimate==1.4.1 +greeclimate==1.4.6 # homeassistant.components.greeneye_monitor greeneye_monitor==3.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f87edeff701..51afc6123ec 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -827,7 +827,7 @@ govee-local-api==1.5.1 gps3==0.33.3 # homeassistant.components.gree -greeclimate==1.4.1 +greeclimate==1.4.6 # homeassistant.components.greeneye_monitor greeneye_monitor==3.0.3 From 917eeba98422c8a00e678507f54a91e3779a28f6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Jun 2024 10:50:55 -0500 Subject: [PATCH 073/146] Increase mqtt availablity timeout to 50s (#120760) --- homeassistant/components/mqtt/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/util.py b/homeassistant/components/mqtt/util.py index 97fa616fdd1..27bdb4f2a35 100644 --- a/homeassistant/components/mqtt/util.py +++ b/homeassistant/components/mqtt/util.py @@ -36,7 +36,7 @@ from .const import ( ) from .models import DATA_MQTT, DATA_MQTT_AVAILABLE, ReceiveMessage -AVAILABILITY_TIMEOUT = 30.0 +AVAILABILITY_TIMEOUT = 50.0 TEMP_DIR_NAME = f"home-assistant-{DOMAIN}" From d1a96ef3621ff6ef09c7baeb8648bb7f4501c34f Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 28 Jun 2024 18:34:24 +0200 Subject: [PATCH 074/146] Do not call async_delete_issue() if there is no issue to delete in Shelly integration (#120762) Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- homeassistant/components/shelly/coordinator.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index 02feef3633b..33ed07c35de 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -377,12 +377,13 @@ class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]): eager_start=True, ) elif update_type is BlockUpdateType.COAP_PERIODIC: + if self._push_update_failures >= MAX_PUSH_UPDATE_FAILURES: + ir.async_delete_issue( + self.hass, + DOMAIN, + PUSH_UPDATE_ISSUE_ID.format(unique=self.mac), + ) self._push_update_failures = 0 - ir.async_delete_issue( - self.hass, - DOMAIN, - PUSH_UPDATE_ISSUE_ID.format(unique=self.mac), - ) elif update_type is BlockUpdateType.COAP_REPLY: self._push_update_failures += 1 if self._push_update_failures == MAX_PUSH_UPDATE_FAILURES: From 0f3ed3bb674d4df1d99e99bd5a7f5630883064f8 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 28 Jun 2024 17:51:34 +0200 Subject: [PATCH 075/146] Bump aiowithings to 3.0.2 (#120765) --- homeassistant/components/withings/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/withings/manifest.json b/homeassistant/components/withings/manifest.json index 4c97f43fd80..090f8c4588e 100644 --- a/homeassistant/components/withings/manifest.json +++ b/homeassistant/components/withings/manifest.json @@ -9,5 +9,5 @@ "iot_class": "cloud_push", "loggers": ["aiowithings"], "quality_scale": "platinum", - "requirements": ["aiowithings==3.0.1"] + "requirements": ["aiowithings==3.0.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 873a2460c1f..598fef62903 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -407,7 +407,7 @@ aiowatttime==0.1.1 aiowebostv==0.4.0 # homeassistant.components.withings -aiowithings==3.0.1 +aiowithings==3.0.2 # homeassistant.components.yandex_transport aioymaps==1.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 51afc6123ec..5e34afbdcf2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -380,7 +380,7 @@ aiowatttime==0.1.1 aiowebostv==0.4.0 # homeassistant.components.withings -aiowithings==3.0.1 +aiowithings==3.0.2 # homeassistant.components.yandex_transport aioymaps==1.2.2 From 8165acddeb8a35f9556d32ce666f96405b3f153e Mon Sep 17 00:00:00 2001 From: Matthew FitzGerald-Chamberlain Date: Fri, 28 Jun 2024 15:15:34 -0500 Subject: [PATCH 076/146] Bump pyaprilaire to 0.7.4 (#120782) --- homeassistant/components/aprilaire/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aprilaire/manifest.json b/homeassistant/components/aprilaire/manifest.json index 43ba4417638..3cc44786989 100644 --- a/homeassistant/components/aprilaire/manifest.json +++ b/homeassistant/components/aprilaire/manifest.json @@ -7,5 +7,5 @@ "integration_type": "device", "iot_class": "local_push", "loggers": ["pyaprilaire"], - "requirements": ["pyaprilaire==0.7.0"] + "requirements": ["pyaprilaire==0.7.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index 598fef62903..4e407a30b48 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1710,7 +1710,7 @@ pyairnow==1.2.1 pyairvisual==2023.08.1 # homeassistant.components.aprilaire -pyaprilaire==0.7.0 +pyaprilaire==0.7.4 # homeassistant.components.asuswrt pyasuswrt==0.1.21 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5e34afbdcf2..b899463e178 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1360,7 +1360,7 @@ pyairnow==1.2.1 pyairvisual==2023.08.1 # homeassistant.components.aprilaire -pyaprilaire==0.7.0 +pyaprilaire==0.7.4 # homeassistant.components.asuswrt pyasuswrt==0.1.21 From b30b4d5a3a63e27460b2e8bbfaf7172a7397727f Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Fri, 28 Jun 2024 22:23:44 +0200 Subject: [PATCH 077/146] Bump energyzero lib to v2.1.1 (#120783) --- homeassistant/components/energyzero/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/energyzero/manifest.json b/homeassistant/components/energyzero/manifest.json index 025f929a4f6..807a0419967 100644 --- a/homeassistant/components/energyzero/manifest.json +++ b/homeassistant/components/energyzero/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/energyzero", "iot_class": "cloud_polling", "quality_scale": "platinum", - "requirements": ["energyzero==2.1.0"] + "requirements": ["energyzero==2.1.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 4e407a30b48..d7a5eaa4a62 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -807,7 +807,7 @@ emulated-roku==0.3.0 energyflip-client==0.2.2 # homeassistant.components.energyzero -energyzero==2.1.0 +energyzero==2.1.1 # homeassistant.components.enocean enocean==0.50 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b899463e178..a6cf6b7e6d1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -667,7 +667,7 @@ emulated-roku==0.3.0 energyflip-client==0.2.2 # homeassistant.components.energyzero -energyzero==2.1.0 +energyzero==2.1.1 # homeassistant.components.enocean enocean==0.50 From 723c4a1eb5276307a11b138f66c26dab9b0df640 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Sat, 29 Jun 2024 06:14:00 +0200 Subject: [PATCH 078/146] Update frontend to 20240628.0 (#120785) Co-authored-by: J. Nick Koston --- homeassistant/components/frontend/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/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index cd46b358335..70f1f5f4f4f 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20240627.0"] + "requirements": ["home-assistant-frontend==20240628.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 91db2564fa6..7cccd58d73f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ habluetooth==3.1.3 hass-nabucasa==0.81.1 hassil==1.7.1 home-assistant-bluetooth==1.12.2 -home-assistant-frontend==20240627.0 +home-assistant-frontend==20240628.0 home-assistant-intents==2024.6.26 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index d7a5eaa4a62..34d6daa8ab2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1090,7 +1090,7 @@ hole==0.8.0 holidays==0.51 # homeassistant.components.frontend -home-assistant-frontend==20240627.0 +home-assistant-frontend==20240628.0 # homeassistant.components.conversation home-assistant-intents==2024.6.26 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a6cf6b7e6d1..d76492f1c65 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -895,7 +895,7 @@ hole==0.8.0 holidays==0.51 # homeassistant.components.frontend -home-assistant-frontend==20240627.0 +home-assistant-frontend==20240628.0 # homeassistant.components.conversation home-assistant-intents==2024.6.26 From ec577c7bd333cceff2020ed220c7236c5fa4acde Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Fri, 28 Jun 2024 23:20:16 +0200 Subject: [PATCH 079/146] Bump odp-amsterdam lib to v6.0.2 (#120788) --- homeassistant/components/garages_amsterdam/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/garages_amsterdam/manifest.json b/homeassistant/components/garages_amsterdam/manifest.json index ebda913abbb..4d4bb9f6fb5 100644 --- a/homeassistant/components/garages_amsterdam/manifest.json +++ b/homeassistant/components/garages_amsterdam/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/garages_amsterdam", "iot_class": "cloud_polling", - "requirements": ["odp-amsterdam==6.0.1"] + "requirements": ["odp-amsterdam==6.0.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 34d6daa8ab2..7f8d32f59d7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1453,7 +1453,7 @@ oauth2client==4.1.3 objgraph==3.5.0 # homeassistant.components.garages_amsterdam -odp-amsterdam==6.0.1 +odp-amsterdam==6.0.2 # homeassistant.components.oem oemthermostat==1.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d76492f1c65..f1d09d9c406 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1177,7 +1177,7 @@ oauth2client==4.1.3 objgraph==3.5.0 # homeassistant.components.garages_amsterdam -odp-amsterdam==6.0.1 +odp-amsterdam==6.0.2 # homeassistant.components.ollama ollama-hass==0.1.7 From b45eff9a2b7bcd7c386d45db04f061facf1516bf Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Sat, 29 Jun 2024 00:24:43 +0200 Subject: [PATCH 080/146] Bump gridnet lib to v5.0.1 (#120793) --- homeassistant/components/pure_energie/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/pure_energie/manifest.json b/homeassistant/components/pure_energie/manifest.json index 19098c41208..ff52ec0ecf9 100644 --- a/homeassistant/components/pure_energie/manifest.json +++ b/homeassistant/components/pure_energie/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/pure_energie", "iot_class": "local_polling", "quality_scale": "platinum", - "requirements": ["gridnet==5.0.0"], + "requirements": ["gridnet==5.0.1"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 7f8d32f59d7..60346dd1959 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1013,7 +1013,7 @@ greeneye_monitor==3.0.3 greenwavereality==0.5.1 # homeassistant.components.pure_energie -gridnet==5.0.0 +gridnet==5.0.1 # homeassistant.components.growatt_server growattServer==1.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f1d09d9c406..b3725e8e237 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -833,7 +833,7 @@ greeclimate==1.4.6 greeneye_monitor==3.0.3 # homeassistant.components.pure_energie -gridnet==5.0.0 +gridnet==5.0.1 # homeassistant.components.growatt_server growattServer==1.5.0 From 0dcfd38cdc1d7b924f4356e88fd5b8dbdfb7db70 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 29 Jun 2024 00:01:18 -0500 Subject: [PATCH 081/146] Fix missing f-string in loop util (#120800) --- homeassistant/util/loop.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/util/loop.py b/homeassistant/util/loop.py index 8a469569601..866f35e79e2 100644 --- a/homeassistant/util/loop.py +++ b/homeassistant/util/loop.py @@ -106,10 +106,10 @@ def raise_for_blocking_call( if strict: raise RuntimeError( - "Caught blocking call to {func.__name__} with args " - f"{mapped_args.get('args')} inside the event loop by" + f"Caught blocking call to {func.__name__} with args " + f"{mapped_args.get('args')} inside the event loop by " f"{'custom ' if integration_frame.custom_integration else ''}" - "integration '{integration_frame.integration}' at " + f"integration '{integration_frame.integration}' at " f"{integration_frame.relative_filename}, line {integration_frame.line_number}:" f" {integration_frame.line}. (offender: {offender_filename}, line " f"{offender_lineno}: {offender_line}), please {report_issue}\n" From 0ec07001bd275da93709a0b7df4c7d05e1d43e75 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 29 Jun 2024 00:40:35 -0500 Subject: [PATCH 082/146] Fix blocking I/O in xmpp notify to read uploaded files (#120801) detected by ruff in https://github.com/home-assistant/core/pull/120799 --- homeassistant/components/xmpp/notify.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/xmpp/notify.py b/homeassistant/components/xmpp/notify.py index 824f996c675..c73248f2524 100644 --- a/homeassistant/components/xmpp/notify.py +++ b/homeassistant/components/xmpp/notify.py @@ -305,16 +305,20 @@ async def async_send_message( # noqa: C901 timeout=timeout, ) - async def upload_file_from_path(self, path, timeout=None): + def _read_upload_file(self, path: str) -> bytes: + """Read file from path.""" + with open(path, "rb") as upfile: + _LOGGER.debug("Reading file %s", path) + return upfile.read() + + async def upload_file_from_path(self, path: str, timeout=None): """Upload a file from a local file path via XEP_0363.""" _LOGGER.info("Uploading file from path, %s", path) if not hass.config.is_allowed_path(path): raise PermissionError("Could not access file. Path not allowed") - with open(path, "rb") as upfile: - _LOGGER.debug("Reading file %s", path) - input_file = upfile.read() + input_file = await hass.async_add_executor_job(self._read_upload_file, path) filesize = len(input_file) _LOGGER.debug("Filesize is %s bytes", filesize) From 66932e3d9a264df08bac40219064e411cbc976a5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 29 Jun 2024 00:42:08 -0500 Subject: [PATCH 083/146] Fix unneeded dict values for MATCH_ALL recorder attrs exclude (#120804) * Small cleanup to handling MATCH_ALL recorder attrs exclude * Fix unneeded dict values for MATCH_ALL recorder attrs exclude The exclude is a set so the dict values were not needed * Fix unneeded dict values for MATCH_ALL recorder attrs exclude The exclude is a set so the dict values were not needed * Fix unneeded dict values for MATCH_ALL recorder attrs exclude The exclude is a set so the dict values were not needed --- .../components/recorder/db_schema.py | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/recorder/db_schema.py b/homeassistant/components/recorder/db_schema.py index ce463067824..ba4a6106bce 100644 --- a/homeassistant/components/recorder/db_schema.py +++ b/homeassistant/components/recorder/db_schema.py @@ -142,6 +142,13 @@ _DEFAULT_TABLE_ARGS = { "mariadb_engine": MYSQL_ENGINE, } +_MATCH_ALL_KEEP = { + ATTR_DEVICE_CLASS, + ATTR_STATE_CLASS, + ATTR_UNIT_OF_MEASUREMENT, + ATTR_FRIENDLY_NAME, +} + class UnusedDateTime(DateTime): """An unused column type that behaves like a datetime.""" @@ -597,19 +604,8 @@ class StateAttributes(Base): if MATCH_ALL in unrecorded_attributes: # Don't exclude device class, state class, unit of measurement # or friendly name when using the MATCH_ALL exclude constant - _exclude_attributes = { - k: v - for k, v in state.attributes.items() - if k - not in ( - ATTR_DEVICE_CLASS, - ATTR_STATE_CLASS, - ATTR_UNIT_OF_MEASUREMENT, - ATTR_FRIENDLY_NAME, - ) - } - exclude_attrs.update(_exclude_attributes) - + exclude_attrs.update(state.attributes) + exclude_attrs -= _MATCH_ALL_KEEP else: exclude_attrs = ALL_DOMAIN_EXCLUDE_ATTRS encoder = json_bytes_strip_null if dialect == PSQL_DIALECT else json_bytes From 7319492bf3a14c51e59b5be6fb67c01e5015e5d7 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 29 Jun 2024 13:48:43 +0200 Subject: [PATCH 084/146] Bump aiomealie to 0.5.0 (#120815) --- homeassistant/components/mealie/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mealie/manifest.json b/homeassistant/components/mealie/manifest.json index fb81ff850b8..918dd743726 100644 --- a/homeassistant/components/mealie/manifest.json +++ b/homeassistant/components/mealie/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/mealie", "integration_type": "service", "iot_class": "local_polling", - "requirements": ["aiomealie==0.4.0"] + "requirements": ["aiomealie==0.5.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 60346dd1959..e2badab1ccd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -294,7 +294,7 @@ aiolookin==1.0.0 aiolyric==1.1.0 # homeassistant.components.mealie -aiomealie==0.4.0 +aiomealie==0.5.0 # homeassistant.components.modern_forms aiomodernforms==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b3725e8e237..7320c66cbdb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -267,7 +267,7 @@ aiolookin==1.0.0 aiolyric==1.1.0 # homeassistant.components.mealie -aiomealie==0.4.0 +aiomealie==0.5.0 # homeassistant.components.modern_forms aiomodernforms==0.1.8 From bb52bfd73d048eef06edcf1ebe247a25de66abd7 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 29 Jun 2024 17:48:28 +0200 Subject: [PATCH 085/146] Add unique id to Mealie config entry (#120816) --- homeassistant/components/mealie/calendar.py | 5 +--- .../components/mealie/config_flow.py | 5 ++-- homeassistant/components/mealie/entity.py | 7 ++++-- tests/components/mealie/conftest.py | 6 ++++- .../mealie/fixtures/users_self.json | 24 +++++++++++++++++++ .../mealie/snapshots/test_calendar.ambr | 8 +++---- .../mealie/snapshots/test_init.ambr | 2 +- tests/components/mealie/test_config_flow.py | 5 ++-- tests/components/mealie/test_init.py | 2 +- 9 files changed, 47 insertions(+), 17 deletions(-) create mode 100644 tests/components/mealie/fixtures/users_self.json diff --git a/homeassistant/components/mealie/calendar.py b/homeassistant/components/mealie/calendar.py index 08e90ebf5ea..62c1473057d 100644 --- a/homeassistant/components/mealie/calendar.py +++ b/homeassistant/components/mealie/calendar.py @@ -50,12 +50,9 @@ class MealieMealplanCalendarEntity(MealieEntity, CalendarEntity): self, coordinator: MealieCoordinator, entry_type: MealplanEntryType ) -> None: """Create the Calendar entity.""" - super().__init__(coordinator) + super().__init__(coordinator, entry_type.name.lower()) self._entry_type = entry_type self._attr_translation_key = entry_type.name.lower() - self._attr_unique_id = ( - f"{self.coordinator.config_entry.entry_id}_{entry_type.name.lower()}" - ) @property def event(self) -> CalendarEvent | None: diff --git a/homeassistant/components/mealie/config_flow.py b/homeassistant/components/mealie/config_flow.py index b25cade148a..550e4679720 100644 --- a/homeassistant/components/mealie/config_flow.py +++ b/homeassistant/components/mealie/config_flow.py @@ -28,14 +28,13 @@ class MealieConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a flow initialized by the user.""" errors: dict[str, str] = {} if user_input: - self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]}) client = MealieClient( user_input[CONF_HOST], token=user_input[CONF_API_TOKEN], session=async_get_clientsession(self.hass), ) try: - await client.get_mealplan_today() + info = await client.get_user_info() except MealieConnectionError: errors["base"] = "cannot_connect" except MealieAuthenticationError: @@ -44,6 +43,8 @@ class MealieConfigFlow(ConfigFlow, domain=DOMAIN): LOGGER.exception("Unexpected error") errors["base"] = "unknown" else: + await self.async_set_unique_id(info.user_id) + self._abort_if_unique_id_configured() return self.async_create_entry( title="Mealie", data=user_input, diff --git a/homeassistant/components/mealie/entity.py b/homeassistant/components/mealie/entity.py index 5e339c1d4b8..765ae2b99d7 100644 --- a/homeassistant/components/mealie/entity.py +++ b/homeassistant/components/mealie/entity.py @@ -12,10 +12,13 @@ class MealieEntity(CoordinatorEntity[MealieCoordinator]): _attr_has_entity_name = True - def __init__(self, coordinator: MealieCoordinator) -> None: + def __init__(self, coordinator: MealieCoordinator, key: str) -> None: """Initialize Mealie entity.""" super().__init__(coordinator) + unique_id = coordinator.config_entry.unique_id + assert unique_id is not None + self._attr_unique_id = f"{unique_id}_{key}" self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, coordinator.config_entry.entry_id)}, + identifiers={(DOMAIN, unique_id)}, entry_type=DeviceEntryType.SERVICE, ) diff --git a/tests/components/mealie/conftest.py b/tests/components/mealie/conftest.py index dd6309cb524..9bda9e3c46d 100644 --- a/tests/components/mealie/conftest.py +++ b/tests/components/mealie/conftest.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from aiomealie import Mealplan, MealplanResponse +from aiomealie import Mealplan, MealplanResponse, UserInfo from mashumaro.codecs.orjson import ORJSONDecoder import pytest from typing_extensions import Generator @@ -44,6 +44,9 @@ def mock_mealie_client() -> Generator[AsyncMock]: client.get_mealplan_today.return_value = ORJSONDecoder(list[Mealplan]).decode( load_fixture("get_mealplan_today.json", DOMAIN) ) + client.get_user_info.return_value = UserInfo.from_json( + load_fixture("users_self.json", DOMAIN) + ) yield client @@ -55,4 +58,5 @@ def mock_config_entry() -> MockConfigEntry: title="Mealie", data={CONF_HOST: "demo.mealie.io", CONF_API_TOKEN: "token"}, entry_id="01J0BC4QM2YBRP6H5G933CETT7", + unique_id="bf1c62fe-4941-4332-9886-e54e88dbdba0", ) diff --git a/tests/components/mealie/fixtures/users_self.json b/tests/components/mealie/fixtures/users_self.json new file mode 100644 index 00000000000..6d5901c8cc0 --- /dev/null +++ b/tests/components/mealie/fixtures/users_self.json @@ -0,0 +1,24 @@ +{ + "id": "bf1c62fe-4941-4332-9886-e54e88dbdba0", + "username": "admin", + "fullName": "Change Me", + "email": "changeme@example.com", + "authMethod": "Mealie", + "admin": true, + "group": "home", + "advanced": true, + "canInvite": true, + "canManage": true, + "canOrganize": true, + "groupId": "24477569-f6af-4b53-9e3f-6d04b0ca6916", + "groupSlug": "home", + "tokens": [ + { + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb25nX3Rva2VuIjp0cnVlLCJpZCI6ImJmMWM2MmZlLTQ5NDEtNDMzMi05ODg2LWU1NGU4OGRiZGJhMCIsIm5hbWUiOiJ0ZXN0aW5nIiwiaW50ZWdyYXRpb25faWQiOiJnZW5lcmljIiwiZXhwIjoxODczOTA5ODk4fQ.xwXZp4fL2g1RbIqGtBeOaS6RDfsYbQDHj8XtRM3wlX0", + "name": "testing", + "id": 2, + "createdAt": "2024-05-20T10:31:38.179669" + } + ], + "cacheKey": "1234" +} diff --git a/tests/components/mealie/snapshots/test_calendar.ambr b/tests/components/mealie/snapshots/test_calendar.ambr index 6af53c112de..3db0da0d765 100644 --- a/tests/components/mealie/snapshots/test_calendar.ambr +++ b/tests/components/mealie/snapshots/test_calendar.ambr @@ -178,7 +178,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'breakfast', - 'unique_id': '01J0BC4QM2YBRP6H5G933CETT7_breakfast', + 'unique_id': 'bf1c62fe-4941-4332-9886-e54e88dbdba0_breakfast', 'unit_of_measurement': None, }) # --- @@ -230,7 +230,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'dinner', - 'unique_id': '01J0BC4QM2YBRP6H5G933CETT7_dinner', + 'unique_id': 'bf1c62fe-4941-4332-9886-e54e88dbdba0_dinner', 'unit_of_measurement': None, }) # --- @@ -282,7 +282,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'lunch', - 'unique_id': '01J0BC4QM2YBRP6H5G933CETT7_lunch', + 'unique_id': 'bf1c62fe-4941-4332-9886-e54e88dbdba0_lunch', 'unit_of_measurement': None, }) # --- @@ -334,7 +334,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'side', - 'unique_id': '01J0BC4QM2YBRP6H5G933CETT7_side', + 'unique_id': 'bf1c62fe-4941-4332-9886-e54e88dbdba0_side', 'unit_of_measurement': None, }) # --- diff --git a/tests/components/mealie/snapshots/test_init.ambr b/tests/components/mealie/snapshots/test_init.ambr index 1333b292dac..8f800676945 100644 --- a/tests/components/mealie/snapshots/test_init.ambr +++ b/tests/components/mealie/snapshots/test_init.ambr @@ -13,7 +13,7 @@ 'identifiers': set({ tuple( 'mealie', - '01J0BC4QM2YBRP6H5G933CETT7', + 'bf1c62fe-4941-4332-9886-e54e88dbdba0', ), }), 'is_new': False, diff --git a/tests/components/mealie/test_config_flow.py b/tests/components/mealie/test_config_flow.py index ac68ed2fac5..777bb1e4ad1 100644 --- a/tests/components/mealie/test_config_flow.py +++ b/tests/components/mealie/test_config_flow.py @@ -37,6 +37,7 @@ async def test_full_flow( CONF_HOST: "demo.mealie.io", CONF_API_TOKEN: "token", } + assert result["result"].unique_id == "bf1c62fe-4941-4332-9886-e54e88dbdba0" @pytest.mark.parametrize( @@ -55,7 +56,7 @@ async def test_flow_errors( error: str, ) -> None: """Test flow errors.""" - mock_mealie_client.get_mealplan_today.side_effect = exception + mock_mealie_client.get_user_info.side_effect = exception result = await hass.config_entries.flow.async_init( DOMAIN, @@ -72,7 +73,7 @@ async def test_flow_errors( assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": error} - mock_mealie_client.get_mealplan_today.side_effect = None + mock_mealie_client.get_user_info.side_effect = None result = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/mealie/test_init.py b/tests/components/mealie/test_init.py index 7d63ad135f9..5a7a5387897 100644 --- a/tests/components/mealie/test_init.py +++ b/tests/components/mealie/test_init.py @@ -26,7 +26,7 @@ async def test_device_info( """Test device registry integration.""" await setup_integration(hass, mock_config_entry) device_entry = device_registry.async_get_device( - identifiers={(DOMAIN, mock_config_entry.entry_id)} + identifiers={(DOMAIN, mock_config_entry.unique_id)} ) assert device_entry is not None assert device_entry == snapshot From 05c63eb88491ca0c1bfe18a2f91ed9aae54cf749 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 29 Jun 2024 14:53:42 +0200 Subject: [PATCH 086/146] Bump python-opensky to 1.0.1 (#120818) --- homeassistant/components/opensky/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/opensky/manifest.json b/homeassistant/components/opensky/manifest.json index 106103cf752..831abbc9cbf 100644 --- a/homeassistant/components/opensky/manifest.json +++ b/homeassistant/components/opensky/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/opensky", "iot_class": "cloud_polling", - "requirements": ["python-opensky==1.0.0"] + "requirements": ["python-opensky==1.0.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index e2badab1ccd..ba3e2f5fadb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2296,7 +2296,7 @@ python-mystrom==2.2.0 python-opendata-transport==0.4.0 # homeassistant.components.opensky -python-opensky==1.0.0 +python-opensky==1.0.1 # homeassistant.components.otbr # homeassistant.components.thread diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7320c66cbdb..b0a6fb54b3a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1793,7 +1793,7 @@ python-mystrom==2.2.0 python-opendata-transport==0.4.0 # homeassistant.components.opensky -python-opensky==1.0.0 +python-opensky==1.0.1 # homeassistant.components.otbr # homeassistant.components.thread From e866417c01c932dcf8ba894e6ca15eff82afb71b Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 29 Jun 2024 17:46:44 +0200 Subject: [PATCH 087/146] Add icons to Airgradient (#120820) --- .../components/airgradient/icons.json | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/homeassistant/components/airgradient/icons.json b/homeassistant/components/airgradient/icons.json index 45d1e12d46e..22188d72faa 100644 --- a/homeassistant/components/airgradient/icons.json +++ b/homeassistant/components/airgradient/icons.json @@ -8,6 +8,34 @@ "default": "mdi:lightbulb-on-outline" } }, + "number": { + "led_bar_brightness": { + "default": "mdi:brightness-percent" + }, + "display_brightness": { + "default": "mdi:brightness-percent" + } + }, + "select": { + "configuration_control": { + "default": "mdi:cloud-cog" + }, + "display_temperature_unit": { + "default": "mdi:thermometer-lines" + }, + "led_bar_mode": { + "default": "mdi:led-strip" + }, + "nox_index_learning_time_offset": { + "default": "mdi:clock-outline" + }, + "voc_index_learning_time_offset": { + "default": "mdi:clock-outline" + }, + "co2_automatic_baseline_calibration": { + "default": "mdi:molecule-co2" + } + }, "sensor": { "total_volatile_organic_component_index": { "default": "mdi:molecule" @@ -17,6 +45,32 @@ }, "pm003_count": { "default": "mdi:blur" + }, + "led_bar_brightness": { + "default": "mdi:brightness-percent" + }, + "display_brightness": { + "default": "mdi:brightness-percent" + }, + "display_temperature_unit": { + "default": "mdi:thermometer-lines" + }, + "led_bar_mode": { + "default": "mdi:led-strip" + }, + "nox_index_learning_time_offset": { + "default": "mdi:clock-outline" + }, + "voc_index_learning_time_offset": { + "default": "mdi:clock-outline" + }, + "co2_automatic_baseline_calibration": { + "default": "mdi:molecule-co2" + } + }, + "switch": { + "post_data_to_airgradient": { + "default": "mdi:cogs" } } } From 3ee8f6edbac8fc1a6c8ec8e45887f3457505c247 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 29 Jun 2024 17:47:21 +0200 Subject: [PATCH 088/146] Use meal note as fallback in Mealie (#120828) --- homeassistant/components/mealie/calendar.py | 4 ++-- .../components/mealie/fixtures/get_mealplans.json | 11 +++++++++++ .../components/mealie/snapshots/test_calendar.ambr | 14 ++++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mealie/calendar.py b/homeassistant/components/mealie/calendar.py index 62c1473057d..fb628754f06 100644 --- a/homeassistant/components/mealie/calendar.py +++ b/homeassistant/components/mealie/calendar.py @@ -30,8 +30,8 @@ async def async_setup_entry( def _get_event_from_mealplan(mealplan: Mealplan) -> CalendarEvent: """Create a CalendarEvent from a Mealplan.""" - description: str | None = None - name = "No recipe" + description: str | None = mealplan.description + name = mealplan.title or "No recipe" if mealplan.recipe: name = mealplan.recipe.name description = mealplan.recipe.description diff --git a/tests/components/mealie/fixtures/get_mealplans.json b/tests/components/mealie/fixtures/get_mealplans.json index 2d63b753d99..9255f9b7396 100644 --- a/tests/components/mealie/fixtures/get_mealplans.json +++ b/tests/components/mealie/fixtures/get_mealplans.json @@ -605,6 +605,17 @@ "updateAt": "2024-01-02T06:35:05.209189", "lastMade": "2024-01-02T22:59:59" } + }, + { + "date": "2024-01-21", + "entryType": "dinner", + "title": "Aquavite", + "text": "Dineren met de boys", + "recipeId": null, + "id": 1, + "groupId": "3931df86-0679-4579-8c63-4bedc9ca9a85", + "userId": "6caa6e4d-521f-4ef4-9ed7-388bdd63f47d", + "recipe": null } ], "next": null, diff --git a/tests/components/mealie/snapshots/test_calendar.ambr b/tests/components/mealie/snapshots/test_calendar.ambr index 3db0da0d765..c3b26e1e9e2 100644 --- a/tests/components/mealie/snapshots/test_calendar.ambr +++ b/tests/components/mealie/snapshots/test_calendar.ambr @@ -147,6 +147,20 @@ 'summary': 'Mousse de saumon', 'uid': None, }), + dict({ + 'description': 'Dineren met de boys', + 'end': dict({ + 'date': '2024-01-22', + }), + 'location': None, + 'recurrence_id': None, + 'rrule': None, + 'start': dict({ + 'date': '2024-01-21', + }), + 'summary': 'Aquavite', + 'uid': None, + }), ]) # --- # name: test_entities[calendar.mealie_breakfast-entry] From 08a0eaf1847d995995909d2f7ae53c5821cd594e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 29 Jun 2024 17:51:45 +0200 Subject: [PATCH 089/146] Bump version to 2024.7.0b5 --- 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 1ab2a3f6893..fa19aa7349e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "0b4" +PATCH_VERSION: Final = "0b5" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index e96c329fd5a..3b42dfa2d6b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.7.0b4" +version = "2024.7.0b5" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 38a30b343dee78b3715ad8dab28a982ea367be3a Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 30 Jun 2024 15:28:01 +0200 Subject: [PATCH 090/146] Bump pizzapi to 0.0.6 (#120691) --- homeassistant/components/dominos/__init__.py | 14 +++++++------- homeassistant/components/dominos/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/dominos/__init__.py b/homeassistant/components/dominos/__init__.py index ce7b36f2280..9b11b667e84 100644 --- a/homeassistant/components/dominos/__init__.py +++ b/homeassistant/components/dominos/__init__.py @@ -4,11 +4,11 @@ from datetime import timedelta import logging from pizzapi import Address, Customer, Order -from pizzapi.address import StoreException import voluptuous as vol from homeassistant.components import http from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent @@ -118,7 +118,7 @@ class Dominos: self.country = conf.get(ATTR_COUNTRY) try: self.closest_store = self.address.closest_store() - except StoreException: + except Exception: # noqa: BLE001 self.closest_store = None def handle_order(self, call: ServiceCall) -> None: @@ -139,7 +139,7 @@ class Dominos: """Update the shared closest store (if open).""" try: self.closest_store = self.address.closest_store() - except StoreException: + except Exception: # noqa: BLE001 self.closest_store = None return False return True @@ -219,7 +219,7 @@ class DominosOrder(Entity): """Update the order state and refreshes the store.""" try: self.dominos.update_closest_store() - except StoreException: + except Exception: # noqa: BLE001 self._orderable = False return @@ -227,13 +227,13 @@ class DominosOrder(Entity): order = self.order() order.pay_with() self._orderable = True - except StoreException: + except Exception: # noqa: BLE001 self._orderable = False def order(self): """Create the order object.""" if self.dominos.closest_store is None: - raise StoreException + raise HomeAssistantError("No store available") order = Order( self.dominos.closest_store, @@ -252,7 +252,7 @@ class DominosOrder(Entity): try: order = self.order() order.place() - except StoreException: + except Exception: # noqa: BLE001 self._orderable = False _LOGGER.warning( "Attempted to order Dominos - Order invalid or store closed" diff --git a/homeassistant/components/dominos/manifest.json b/homeassistant/components/dominos/manifest.json index dfb8966013f..442f433db7c 100644 --- a/homeassistant/components/dominos/manifest.json +++ b/homeassistant/components/dominos/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/dominos", "iot_class": "cloud_polling", "loggers": ["pizzapi"], - "requirements": ["pizzapi==0.0.3"] + "requirements": ["pizzapi==0.0.6"] } diff --git a/requirements_all.txt b/requirements_all.txt index ba3e2f5fadb..d44ea880636 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1557,7 +1557,7 @@ pigpio==1.78 pilight==0.1.1 # homeassistant.components.dominos -pizzapi==0.0.3 +pizzapi==0.0.6 # homeassistant.components.plex plexauth==0.0.6 From a7246400b3db10a4daa24c6c989368f187f7506c Mon Sep 17 00:00:00 2001 From: mkmer Date: Sun, 30 Jun 2024 09:30:52 -0400 Subject: [PATCH 091/146] Allow EM heat on from any mode in Honeywell (#120750) --- homeassistant/components/honeywell/switch.py | 13 ++++++------ tests/components/honeywell/test_switch.py | 21 +------------------- 2 files changed, 7 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/honeywell/switch.py b/homeassistant/components/honeywell/switch.py index 53a9b27ee72..b90dd339593 100644 --- a/homeassistant/components/honeywell/switch.py +++ b/homeassistant/components/honeywell/switch.py @@ -71,13 +71,12 @@ class HoneywellSwitch(SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on if heat mode is enabled.""" - if self._device.system_mode == "heat": - try: - await self._device.set_system_mode("emheat") - except SomeComfortError as err: - raise HomeAssistantError( - translation_domain=DOMAIN, translation_key="switch_failed_on" - ) from err + try: + await self._device.set_system_mode("emheat") + except SomeComfortError as err: + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="switch_failed_on" + ) from err async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off if on.""" diff --git a/tests/components/honeywell/test_switch.py b/tests/components/honeywell/test_switch.py index 73052871ef1..482b9837b93 100644 --- a/tests/components/honeywell/test_switch.py +++ b/tests/components/honeywell/test_switch.py @@ -24,26 +24,6 @@ async def test_emheat_switch( await init_integration(hass, config_entry) entity_id = f"switch.{device.name}_emergency_heat" - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: entity_id}, - blocking=True, - ) - device.set_system_mode.assert_not_called() - - device.set_system_mode.reset_mock() - - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: entity_id}, - blocking=True, - ) - device.set_system_mode.assert_not_called() - - device.system_mode = "heat" - await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_ON, @@ -53,6 +33,7 @@ async def test_emheat_switch( device.set_system_mode.assert_called_once_with("emheat") device.set_system_mode.reset_mock() + device.system_mode = "emheat" await hass.services.async_call( SWITCH_DOMAIN, From f58eafe8fca3782cd59989a57a5aabfd8a276f8a Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Sun, 30 Jun 2024 15:16:41 +0200 Subject: [PATCH 092/146] Fix routes with transfer in nmbs integration (#120808) --- homeassistant/components/nmbs/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/nmbs/sensor.py b/homeassistant/components/nmbs/sensor.py index 82fc6143b2d..6ccdc742430 100644 --- a/homeassistant/components/nmbs/sensor.py +++ b/homeassistant/components/nmbs/sensor.py @@ -261,7 +261,7 @@ class NMBSSensor(SensorEntity): attrs["via_arrival_platform"] = via["arrival"]["platform"] attrs["via_transfer_platform"] = via["departure"]["platform"] attrs["via_transfer_time"] = get_delay_in_minutes( - via["timeBetween"] + via["timebetween"] ) + get_delay_in_minutes(via["departure"]["delay"]) if delay > 0: From ad9e0ef8e49a17ec48c19d0ce1c436b9a442d607 Mon Sep 17 00:00:00 2001 From: Etienne Soufflet Date: Sun, 30 Jun 2024 20:38:35 +0200 Subject: [PATCH 093/146] Fix Tado fan mode (#120809) --- homeassistant/components/tado/climate.py | 17 ++- homeassistant/components/tado/helper.py | 12 ++ .../tado/fixtures/smartac4.with_fanlevel.json | 88 ++++++++++++ .../components/tado/fixtures/zone_states.json | 73 ++++++++++ ...th_fanlevel_horizontal_vertical_swing.json | 130 ++++++++++++++++++ tests/components/tado/fixtures/zones.json | 40 ++++++ tests/components/tado/test_climate.py | 32 +++++ tests/components/tado/util.py | 18 +++ 8 files changed, 401 insertions(+), 9 deletions(-) create mode 100644 tests/components/tado/fixtures/smartac4.with_fanlevel.json create mode 100644 tests/components/tado/fixtures/zone_with_fanlevel_horizontal_vertical_swing.json diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index 40bdb19b31b..116985796d5 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -73,7 +73,7 @@ from .const import ( TYPE_HEATING, ) from .entity import TadoZoneEntity -from .helper import decide_duration, decide_overlay_mode +from .helper import decide_duration, decide_overlay_mode, generate_supported_fanmodes _LOGGER = logging.getLogger(__name__) @@ -200,15 +200,14 @@ def create_climate_entity( continue if capabilities[mode].get("fanSpeeds"): - supported_fan_modes = [ - TADO_TO_HA_FAN_MODE_MAP_LEGACY[speed] - for speed in capabilities[mode]["fanSpeeds"] - ] + supported_fan_modes = generate_supported_fanmodes( + TADO_TO_HA_FAN_MODE_MAP_LEGACY, capabilities[mode]["fanSpeeds"] + ) + else: - supported_fan_modes = [ - TADO_TO_HA_FAN_MODE_MAP[level] - for level in capabilities[mode]["fanLevel"] - ] + supported_fan_modes = generate_supported_fanmodes( + TADO_TO_HA_FAN_MODE_MAP, capabilities[mode]["fanLevel"] + ) cool_temperatures = capabilities[CONST_MODE_COOL]["temperatures"] else: diff --git a/homeassistant/components/tado/helper.py b/homeassistant/components/tado/helper.py index efcd3e7c4ea..81bff1e36c3 100644 --- a/homeassistant/components/tado/helper.py +++ b/homeassistant/components/tado/helper.py @@ -49,3 +49,15 @@ def decide_duration( ) return duration + + +def generate_supported_fanmodes(tado_to_ha_mapping: dict[str, str], options: list[str]): + """Return correct list of fan modes or None.""" + supported_fanmodes = [ + tado_to_ha_mapping.get(option) + for option in options + if tado_to_ha_mapping.get(option) is not None + ] + if not supported_fanmodes: + return None + return supported_fanmodes diff --git a/tests/components/tado/fixtures/smartac4.with_fanlevel.json b/tests/components/tado/fixtures/smartac4.with_fanlevel.json new file mode 100644 index 00000000000..ea1f9cbd8e5 --- /dev/null +++ b/tests/components/tado/fixtures/smartac4.with_fanlevel.json @@ -0,0 +1,88 @@ +{ + "tadoMode": "HOME", + "geolocationOverride": false, + "geolocationOverrideDisableTime": null, + "preparation": null, + "setting": { + "type": "AIR_CONDITIONING", + "power": "ON", + "mode": "HEAT", + "temperature": { + "celsius": 25.0, + "fahrenheit": 77.0 + }, + "fanLevel": "LEVEL3", + "verticalSwing": "ON", + "horizontalSwing": "ON" + }, + "overlayType": "MANUAL", + "overlay": { + "type": "MANUAL", + "setting": { + "type": "AIR_CONDITIONING", + "power": "ON", + "mode": "HEAT", + "temperature": { + "celsius": 25.0, + "fahrenheit": 77.0 + }, + "fanLevel": "LEVEL3", + "verticalSwing": "ON" + }, + "termination": { + "type": "MANUAL", + "typeSkillBasedApp": "MANUAL", + "projectedExpiry": null + } + }, + "openWindow": null, + "nextScheduleChange": { + "start": "2024-07-01T05: 45: 00Z", + "setting": { + "type": "AIR_CONDITIONING", + "power": "ON", + "mode": "HEAT", + "temperature": { + "celsius": 24.0, + "fahrenheit": 75.2 + }, + "fanLevel": "LEVEL3", + "verticalSwing": "ON", + "horizontalSwing": "ON" + } + }, + "nextTimeBlock": { + "start": "2024-07-01T05: 45: 00.000Z" + }, + "link": { + "state": "ONLINE" + }, + "runningOfflineSchedule": false, + "activityDataPoints": { + "acPower": { + "timestamp": "2022-07-13T18: 06: 58.183Z", + "type": "POWER", + "value": "ON" + } + }, + "sensorDataPoints": { + "insideTemperature": { + "celsius": 24.3, + "fahrenheit": 75.74, + "timestamp": "2024-06-28T22: 23: 15.679Z", + "type": "TEMPERATURE", + "precision": { + "celsius": 0.1, + "fahrenheit": 0.1 + } + }, + "humidity": { + "type": "PERCENTAGE", + "percentage": 70.9, + "timestamp": "2024-06-28T22: 23: 15.679Z" + } + }, + "terminationCondition": { + "type": "MANUAL" + } +} diff --git a/tests/components/tado/fixtures/zone_states.json b/tests/components/tado/fixtures/zone_states.json index 64d457f3b50..df1a99a80f3 100644 --- a/tests/components/tado/fixtures/zone_states.json +++ b/tests/components/tado/fixtures/zone_states.json @@ -287,6 +287,79 @@ "timestamp": "2020-03-28T02:09:27.830Z" } } + }, + "6": { + "tadoMode": "HOME", + "geolocationOverride": false, + "geolocationOverrideDisableTime": null, + "preparation": null, + "setting": { + "type": "AIR_CONDITIONING", + "power": "OFF" + }, + "overlayType": "MANUAL", + "overlay": { + "type": "MANUAL", + "setting": { + "type": "AIR_CONDITIONING", + "power": "OFF" + }, + "termination": { + "type": "MANUAL", + "typeSkillBasedApp": "MANUAL", + "projectedExpiry": null + } + }, + "openWindow": null, + "nextScheduleChange": { + "start": "2024-07-01T05: 45: 00Z", + "setting": { + "type": "AIR_CONDITIONING", + "power": "ON", + "mode": "HEAT", + "temperature": { + "celsius": 24.0, + "fahrenheit": 75.2 + }, + "fanLevel": "LEVEL3", + "verticalSwing": "ON", + "horizontalSwing": "ON" + } + }, + "nextTimeBlock": { + "start": "2024-07-01T05: 45: 00.000Z" + }, + "link": { + "state": "ONLINE" + }, + "runningOfflineSchedule": false, + "activityDataPoints": { + "acPower": { + "timestamp": "2022-07-13T18: 06: 58.183Z", + "type": "POWER", + "value": "OFF" + } + }, + "sensorDataPoints": { + "insideTemperature": { + "celsius": 24.21, + "fahrenheit": 75.58, + "timestamp": "2024-06-28T21: 43: 51.067Z", + "type": "TEMPERATURE", + "precision": { + "celsius": 0.1, + "fahrenheit": 0.1 + } + }, + "humidity": { + "type": "PERCENTAGE", + "percentage": 71.4, + "timestamp": "2024-06-28T21: 43: 51.067Z" + } + }, + "terminationCondition": { + "type": "MANUAL" + } } } } diff --git a/tests/components/tado/fixtures/zone_with_fanlevel_horizontal_vertical_swing.json b/tests/components/tado/fixtures/zone_with_fanlevel_horizontal_vertical_swing.json new file mode 100644 index 00000000000..51ba70b4065 --- /dev/null +++ b/tests/components/tado/fixtures/zone_with_fanlevel_horizontal_vertical_swing.json @@ -0,0 +1,130 @@ +{ + "type": "AIR_CONDITIONING", + "COOL": { + "temperatures": { + "celsius": { + "min": 16, + "max": 31, + "step": 1.0 + }, + "fahrenheit": { + "min": 61, + "max": 88, + "step": 1.0 + } + }, + "fanLevel": ["LEVEL3", "LEVEL2", "AUTO", "LEVEL1", "LEVEL4", "LEVEL5"], + "verticalSwing": ["MID_UP", "MID_DOWN", "ON", "OFF", "UP", "MID", "DOWN"], + "horizontalSwing": ["OFF", "ON"], + "light": ["ON", "OFF"] + }, + "FAN": { + "temperatures": { + "celsius": { + "min": 16, + "max": 31, + "step": 1.0 + }, + "fahrenheit": { + "min": 61, + "max": 88, + "step": 1.0 + } + }, + "fanLevel": ["LEVEL3", "LEVEL2", "AUTO", "LEVEL1", "LEVEL4", "LEVEL5"], + "verticalSwing": ["MID_UP", "MID_DOWN", "ON", "OFF", "UP", "MID", "DOWN"], + "horizontalSwing": ["OFF", "ON"], + "light": ["ON", "OFF"] + }, + "AUTO": { + "fanLevel": ["LEVEL3", "LEVEL2", "AUTO", "LEVEL1", "LEVEL4", "LEVEL5"], + "verticalSwing": ["MID_UP", "MID_DOWN", "ON", "OFF", "UP", "MID", "DOWN"], + "horizontalSwing": ["OFF", "ON"], + "light": ["ON", "OFF"] + }, + "HEAT": { + "temperatures": { + "celsius": { + "min": 16, + "max": 31, + "step": 1.0 + }, + "fahrenheit": { + "min": 61, + "max": 88, + "step": 1.0 + } + }, + "fanLevel": ["LEVEL3", "LEVEL2", "AUTO", "LEVEL1", "LEVEL4", "LEVEL5"], + "verticalSwing": ["MID_UP", "MID_DOWN", "ON", "OFF", "UP", "MID", "DOWN"], + "horizontalSwing": ["OFF", "ON"], + "light": ["ON", "OFF"] + }, + "DRY": { + "temperatures": { + "celsius": { + "min": 16, + "max": 31, + "step": 1.0 + }, + "fahrenheit": { + "min": 61, + "max": 88, + "step": 1.0 + } + }, + "verticalSwing": ["MID_UP", "MID_DOWN", "ON", "OFF", "UP", "MID", "DOWN"], + "horizontalSwing": ["OFF", "ON"], + "light": ["ON", "OFF"] + }, + "initialStates": { + "mode": "COOL", + "modes": { + "COOL": { + "temperature": { + "celsius": 24, + "fahrenheit": 75 + }, + "fanLevel": "LEVEL3", + "verticalSwing": "OFF", + "horizontalSwing": "OFF", + "light": "ON" + }, + "HEAT": { + "temperature": { + "celsius": 24, + "fahrenheit": 75 + }, + "fanLevel": "LEVEL3", + "verticalSwing": "OFF", + "horizontalSwing": "OFF", + "light": "ON" + }, + "DRY": { + "temperature": { + "celsius": 24, + "fahrenheit": 75 + }, + "verticalSwing": "OFF", + "horizontalSwing": "OFF", + "light": "ON" + }, + "FAN": { + "temperature": { + "celsius": 24, + "fahrenheit": 75 + }, + "fanLevel": "LEVEL3", + "verticalSwing": "OFF", + "horizontalSwing": "OFF", + "light": "ON" + }, + "AUTO": { + "fanLevel": "LEVEL3", + "verticalSwing": "OFF", + "horizontalSwing": "OFF", + "light": "ON" + } + } + } +} diff --git a/tests/components/tado/fixtures/zones.json b/tests/components/tado/fixtures/zones.json index 5ef7374a660..e1d2ec759ba 100644 --- a/tests/components/tado/fixtures/zones.json +++ b/tests/components/tado/fixtures/zones.json @@ -178,5 +178,45 @@ "deviceTypes": ["WR02"], "reportAvailable": false, "type": "AIR_CONDITIONING" + }, + { + "id": 6, + "name": "Air Conditioning with fanlevel", + "type": "AIR_CONDITIONING", + "dateCreated": "2022-07-13T18: 06: 58.183Z", + "deviceTypes": ["WR02"], + "devices": [ + { + "deviceType": "WR02", + "serialNo": "WR5", + "shortSerialNo": "WR5", + "currentFwVersion": "118.7", + "connectionState": { + "value": true, + "timestamp": "2024-06-28T21: 04: 23.463Z" + }, + "characteristics": { + "capabilities": ["INSIDE_TEMPERATURE_MEASUREMENT", "IDENTIFY"] + }, + "accessPointWiFi": { + "ssid": "tado8480" + }, + "commandTableUploadState": "FINISHED", + "duties": ["ZONE_UI", "ZONE_DRIVER", "ZONE_LEADER"] + } + ], + "reportAvailable": false, + "showScheduleSetup": false, + "supportsDazzle": true, + "dazzleEnabled": true, + "dazzleMode": { + "supported": true, + "enabled": true + }, + "openWindowDetection": { + "supported": true, + "enabled": true, + "timeoutInSeconds": 900 + } } ] diff --git a/tests/components/tado/test_climate.py b/tests/components/tado/test_climate.py index 98fd2d753a4..5a43c728b6e 100644 --- a/tests/components/tado/test_climate.py +++ b/tests/components/tado/test_climate.py @@ -89,3 +89,35 @@ async def test_smartac_with_swing(hass: HomeAssistant) -> None: # Only test for a subset of attributes in case # HA changes the implementation and a new one appears assert all(item in state.attributes.items() for item in expected_attributes.items()) + + +async def test_smartac_with_fanlevel_vertical_and_horizontal_swing( + hass: HomeAssistant, +) -> None: + """Test creation of smart ac with swing climate.""" + + await async_init_integration(hass) + + state = hass.states.get("climate.air_conditioning_with_fanlevel") + assert state.state == "heat" + + expected_attributes = { + "current_humidity": 70.9, + "current_temperature": 24.3, + "fan_mode": "high", + "fan_modes": ["high", "medium", "auto", "low"], + "friendly_name": "Air Conditioning with fanlevel", + "hvac_action": "heating", + "hvac_modes": ["off", "auto", "heat", "cool", "heat_cool", "dry", "fan_only"], + "max_temp": 31.0, + "min_temp": 16.0, + "preset_mode": "auto", + "preset_modes": ["away", "home", "auto"], + "swing_modes": ["vertical", "horizontal", "both", "off"], + "supported_features": 441, + "target_temp_step": 1.0, + "temperature": 25.0, + } + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + assert all(item in state.attributes.items() for item in expected_attributes.items()) diff --git a/tests/components/tado/util.py b/tests/components/tado/util.py index dd7c108c984..de4fd515e5a 100644 --- a/tests/components/tado/util.py +++ b/tests/components/tado/util.py @@ -27,6 +27,12 @@ async def async_init_integration( # WR1 Device device_wr1_fixture = "tado/device_wr1.json" + # Smart AC with fanLevel, Vertical and Horizontal swings + zone_6_state_fixture = "tado/smartac4.with_fanlevel.json" + zone_6_capabilities_fixture = ( + "tado/zone_with_fanlevel_horizontal_vertical_swing.json" + ) + # Smart AC with Swing zone_5_state_fixture = "tado/smartac3.with_swing.json" zone_5_capabilities_fixture = "tado/zone_with_swing_capabilities.json" @@ -95,6 +101,10 @@ async def async_init_integration( "https://my.tado.com/api/v2/homes/1/zoneStates", text=load_fixture(zone_states_fixture), ) + m.get( + "https://my.tado.com/api/v2/homes/1/zones/6/capabilities", + text=load_fixture(zone_6_capabilities_fixture), + ) m.get( "https://my.tado.com/api/v2/homes/1/zones/5/capabilities", text=load_fixture(zone_5_capabilities_fixture), @@ -135,6 +145,14 @@ async def async_init_integration( "https://my.tado.com/api/v2/homes/1/zones/5/defaultOverlay", text=load_fixture(zone_def_overlay), ) + m.get( + "https://my.tado.com/api/v2/homes/1/zones/6/defaultOverlay", + text=load_fixture(zone_def_overlay), + ) + m.get( + "https://my.tado.com/api/v2/homes/1/zones/6/state", + text=load_fixture(zone_6_state_fixture), + ) m.get( "https://my.tado.com/api/v2/homes/1/zones/5/state", text=load_fixture(zone_5_state_fixture), From becf9fcce205f0c0e171c3f276f53d5f1e81d556 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sat, 29 Jun 2024 22:07:37 +0300 Subject: [PATCH 094/146] Bump aiowebostv to 0.4.1 (#120838) --- homeassistant/components/webostv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/webostv/manifest.json b/homeassistant/components/webostv/manifest.json index ed8e1a6cc6e..bcafb82a4b0 100644 --- a/homeassistant/components/webostv/manifest.json +++ b/homeassistant/components/webostv/manifest.json @@ -7,7 +7,7 @@ "iot_class": "local_push", "loggers": ["aiowebostv"], "quality_scale": "platinum", - "requirements": ["aiowebostv==0.4.0"], + "requirements": ["aiowebostv==0.4.1"], "ssdp": [ { "st": "urn:lge-com:service:webos-second-screen:1" diff --git a/requirements_all.txt b/requirements_all.txt index d44ea880636..d15c2d13ae0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -404,7 +404,7 @@ aiowaqi==3.1.0 aiowatttime==0.1.1 # homeassistant.components.webostv -aiowebostv==0.4.0 +aiowebostv==0.4.1 # homeassistant.components.withings aiowithings==3.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b0a6fb54b3a..20a9b828069 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -377,7 +377,7 @@ aiowaqi==3.1.0 aiowatttime==0.1.1 # homeassistant.components.webostv -aiowebostv==0.4.0 +aiowebostv==0.4.1 # homeassistant.components.withings aiowithings==3.0.2 From bcec268c047e30b7fa634070c9f112596679e39f Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Sun, 30 Jun 2024 01:08:24 +0300 Subject: [PATCH 095/146] Fix Jewish calendar unique id move to entity (#120842) --- homeassistant/components/jewish_calendar/entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/jewish_calendar/entity.py b/homeassistant/components/jewish_calendar/entity.py index aba76599f63..c11925df954 100644 --- a/homeassistant/components/jewish_calendar/entity.py +++ b/homeassistant/components/jewish_calendar/entity.py @@ -28,7 +28,7 @@ class JewishCalendarEntity(Entity): ) -> None: """Initialize a Jewish Calendar entity.""" self.entity_description = description - self._attr_unique_id = f"{config_entry.entry_id}_{description.key}" + self._attr_unique_id = f"{config_entry.entry_id}-{description.key}" self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, config_entry.entry_id)}, From 4fc89e886198c599007807d8989acfbf9272a1ed Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 29 Jun 2024 21:35:48 -0700 Subject: [PATCH 096/146] Rollback PyFlume to 0.6.5 (#120846) --- homeassistant/components/flume/coordinator.py | 2 +- homeassistant/components/flume/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/flume/coordinator.py b/homeassistant/components/flume/coordinator.py index c75bffdc615..30e7962304c 100644 --- a/homeassistant/components/flume/coordinator.py +++ b/homeassistant/components/flume/coordinator.py @@ -98,7 +98,7 @@ class FlumeNotificationDataUpdateCoordinator(DataUpdateCoordinator[None]): # The related binary sensors (leak detected, high flow, low battery) # will be active until the notification is deleted in the Flume app. self.notifications = pyflume.FlumeNotificationList( - self.auth, read=None, sort_direction="DESC" + self.auth, read=None ).notification_list _LOGGER.debug("Notifications %s", self.notifications) diff --git a/homeassistant/components/flume/manifest.json b/homeassistant/components/flume/manifest.json index bb6783bafbe..953d9791f2f 100644 --- a/homeassistant/components/flume/manifest.json +++ b/homeassistant/components/flume/manifest.json @@ -11,5 +11,5 @@ "documentation": "https://www.home-assistant.io/integrations/flume", "iot_class": "cloud_polling", "loggers": ["pyflume"], - "requirements": ["PyFlume==0.8.7"] + "requirements": ["PyFlume==0.6.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index d15c2d13ae0..d7a7ecb6eb5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -54,7 +54,7 @@ PyChromecast==14.0.1 PyFlick==0.0.2 # homeassistant.components.flume -PyFlume==0.8.7 +PyFlume==0.6.5 # homeassistant.components.fronius PyFronius==0.7.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 20a9b828069..507b40f8e47 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -45,7 +45,7 @@ PyChromecast==14.0.1 PyFlick==0.0.2 # homeassistant.components.flume -PyFlume==0.8.7 +PyFlume==0.6.5 # homeassistant.components.fronius PyFronius==0.7.3 From af733425c2d85d3ae559ee3f272cca7bdbddfd1a Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 30 Jun 2024 14:56:12 +0200 Subject: [PATCH 097/146] Bump pyfritzhome to 0.6.12 (#120861) --- homeassistant/components/fritzbox/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/fritzbox/manifest.json b/homeassistant/components/fritzbox/manifest.json index de2e9e0200a..3735c16571e 100644 --- a/homeassistant/components/fritzbox/manifest.json +++ b/homeassistant/components/fritzbox/manifest.json @@ -8,7 +8,7 @@ "iot_class": "local_polling", "loggers": ["pyfritzhome"], "quality_scale": "gold", - "requirements": ["pyfritzhome==0.6.11"], + "requirements": ["pyfritzhome==0.6.12"], "ssdp": [ { "st": "urn:schemas-upnp-org:device:fritzbox:1" diff --git a/requirements_all.txt b/requirements_all.txt index d7a7ecb6eb5..dd68902baae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1866,7 +1866,7 @@ pyforked-daapd==0.1.14 pyfreedompro==1.1.0 # homeassistant.components.fritzbox -pyfritzhome==0.6.11 +pyfritzhome==0.6.12 # homeassistant.components.ifttt pyfttt==0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 507b40f8e47..54e86d60186 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1465,7 +1465,7 @@ pyforked-daapd==0.1.14 pyfreedompro==1.1.0 # homeassistant.components.fritzbox -pyfritzhome==0.6.11 +pyfritzhome==0.6.12 # homeassistant.components.ifttt pyfttt==0.3 From 14af3661f3fe68332bbdee929c1b6586264f384d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 30 Jun 2024 20:42:10 +0200 Subject: [PATCH 098/146] Bump version to 2024.7.0b6 --- 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 fa19aa7349e..e97f14f830c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "0b5" +PATCH_VERSION: Final = "0b6" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 3b42dfa2d6b..0f4b25eb0cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.7.0b5" +version = "2024.7.0b6" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 3bbf8df6d640bbc748b942996196c124e2585215 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 26 Jun 2024 23:57:41 +0200 Subject: [PATCH 099/146] Cleanup mqtt platform tests part 4 (init) (#120574) --- tests/components/mqtt/test_init.py | 91 ++++++++---------------------- 1 file changed, 23 insertions(+), 68 deletions(-) diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 231379601c6..bcadf4a6506 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -5,7 +5,6 @@ from copy import deepcopy from datetime import datetime, timedelta from functools import partial import json -import logging import socket import ssl import time @@ -16,15 +15,11 @@ import certifi from freezegun.api import FrozenDateTimeFactory import paho.mqtt.client as paho_mqtt import pytest -from typing_extensions import Generator import voluptuous as vol from homeassistant.components import mqtt from homeassistant.components.mqtt import debug_info -from homeassistant.components.mqtt.client import ( - _LOGGER as CLIENT_LOGGER, - RECONNECT_INTERVAL_SECONDS, -) +from homeassistant.components.mqtt.client import RECONNECT_INTERVAL_SECONDS from homeassistant.components.mqtt.models import ( MessageCallbackType, MqttCommandTemplateException, @@ -100,15 +95,6 @@ def mock_storage(hass_storage: dict[str, Any]) -> None: """Autouse hass_storage for the TestCase tests.""" -@pytest.fixture -def client_debug_log() -> Generator[None]: - """Set the mqtt client log level to DEBUG.""" - logger = logging.getLogger("mqtt_client_tests_debug") - logger.setLevel(logging.DEBUG) - with patch.object(CLIENT_LOGGER, "parent", logger): - yield - - def help_assert_message( msg: ReceiveMessage, topic: str | None = None, @@ -130,8 +116,7 @@ def help_assert_message( async def test_mqtt_connects_on_home_assistant_mqtt_setup( - hass: HomeAssistant, - setup_with_birth_msg_client_mock: MqttMockPahoClient, + hass: HomeAssistant, setup_with_birth_msg_client_mock: MqttMockPahoClient ) -> None: """Test if client is connected after mqtt init on bootstrap.""" mqtt_client_mock = setup_with_birth_msg_client_mock @@ -150,9 +135,7 @@ async def test_mqtt_does_not_disconnect_on_home_assistant_stop( assert mqtt_client_mock.disconnect.call_count == 0 -async def test_mqtt_await_ack_at_disconnect( - hass: HomeAssistant, -) -> None: +async def test_mqtt_await_ack_at_disconnect(hass: HomeAssistant) -> None: """Test if ACK is awaited correctly when disconnecting.""" class FakeInfo: @@ -208,8 +191,7 @@ async def test_mqtt_await_ack_at_disconnect( @pytest.mark.parametrize("mqtt_config_entry_data", [ENTRY_DEFAULT_BIRTH_MESSAGE]) async def test_publish( - hass: HomeAssistant, - setup_with_birth_msg_client_mock: MqttMockPahoClient, + hass: HomeAssistant, setup_with_birth_msg_client_mock: MqttMockPahoClient ) -> None: """Test the publish function.""" publish_mock: MagicMock = setup_with_birth_msg_client_mock.publish @@ -340,9 +322,7 @@ async def test_command_template_value(hass: HomeAssistant) -> None: ], ) async def test_command_template_variables( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - config: ConfigType, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, config: ConfigType ) -> None: """Test the rendering of entity variables.""" topic = "test/select" @@ -888,7 +868,7 @@ def test_entity_device_info_schema() -> None: {"identifiers": [], "connections": [], "name": "Beer"} ) - # not an valid URL + # not a valid URL with pytest.raises(vol.Invalid): MQTT_ENTITY_DEVICE_INFO_SCHEMA( { @@ -1049,10 +1029,9 @@ async def test_subscribe_topic( unsub() +@pytest.mark.usefixtures("mqtt_mock_entry") async def test_subscribe_topic_not_initialize( - hass: HomeAssistant, - record_calls: MessageCallbackType, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, record_calls: MessageCallbackType ) -> None: """Test the subscription of a topic when MQTT was not initialized.""" with pytest.raises( @@ -1084,7 +1063,6 @@ async def test_subscribe_mqtt_config_entry_disabled( async def test_subscribe_and_resubscribe( hass: HomeAssistant, - client_debug_log: None, mock_debouncer: asyncio.Event, setup_with_birth_msg_client_mock: MqttMockPahoClient, recorded_calls: list[ReceiveMessage], @@ -1892,10 +1870,10 @@ async def test_subscribed_at_highest_qos( assert help_all_subscribe_calls(mqtt_client_mock) == [("test/state", 2)] +@pytest.mark.usefixtures("mqtt_client_mock") async def test_reload_entry_with_restored_subscriptions( hass: HomeAssistant, mock_debouncer: asyncio.Event, - mqtt_client_mock: MqttMockPahoClient, record_calls: MessageCallbackType, recorded_calls: list[ReceiveMessage], ) -> None: @@ -1995,7 +1973,6 @@ async def test_logs_error_if_no_connect_broker( @pytest.mark.parametrize("return_code", [4, 5]) async def test_triggers_reauth_flow_if_auth_fails( hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, setup_with_birth_msg_client_mock: MqttMockPahoClient, return_code: int, ) -> None: @@ -2132,9 +2109,7 @@ async def test_handle_message_callback( ], ) async def test_setup_manual_mqtt_with_platform_key( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + mqtt_mock_entry: MqttMockHAClientGenerator, caplog: pytest.LogCaptureFixture ) -> None: """Test set up a manual MQTT item with a platform key.""" assert await mqtt_mock_entry() @@ -2146,9 +2121,7 @@ async def test_setup_manual_mqtt_with_platform_key( @pytest.mark.parametrize("hass_config", [{mqtt.DOMAIN: {"light": {"name": "test"}}}]) async def test_setup_manual_mqtt_with_invalid_config( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + mqtt_mock_entry: MqttMockHAClientGenerator, caplog: pytest.LogCaptureFixture ) -> None: """Test set up a manual MQTT item with an invalid config.""" assert await mqtt_mock_entry() @@ -2182,9 +2155,7 @@ async def test_setup_manual_mqtt_with_invalid_config( ], ) async def test_setup_mqtt_client_protocol( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - protocol: int, + mqtt_mock_entry: MqttMockHAClientGenerator, protocol: int ) -> None: """Test MQTT client protocol setup.""" with patch( @@ -2383,8 +2354,7 @@ async def test_custom_birth_message( [ENTRY_DEFAULT_BIRTH_MESSAGE], ) async def test_default_birth_message( - hass: HomeAssistant, - setup_with_birth_msg_client_mock: MqttMockPahoClient, + hass: HomeAssistant, setup_with_birth_msg_client_mock: MqttMockPahoClient ) -> None: """Test sending birth message.""" mqtt_client_mock = setup_with_birth_msg_client_mock @@ -2470,10 +2440,7 @@ async def test_delayed_birth_message( [ENTRY_DEFAULT_BIRTH_MESSAGE], ) async def test_subscription_done_when_birth_message_is_sent( - hass: HomeAssistant, - mqtt_config_entry_data: dict[str, Any], setup_with_birth_msg_client_mock: MqttMockPahoClient, - record_calls: MessageCallbackType, ) -> None: """Test sending birth message until initial subscription has been completed.""" mqtt_client_mock = setup_with_birth_msg_client_mock @@ -2517,7 +2484,6 @@ async def test_custom_will_message( async def test_default_will_message( - hass: HomeAssistant, setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test will message.""" @@ -2647,11 +2613,9 @@ async def test_mqtt_subscribes_and_unsubscribes_in_chunks( assert len(mqtt_client_mock.unsubscribe.mock_calls[1][1][0]) == 2 +@pytest.mark.usefixtures("mqtt_client_mock") async def test_default_entry_setting_are_applied( - hass: HomeAssistant, - device_registry: dr.DeviceRegistry, - mqtt_client_mock: MqttMockPahoClient, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, device_registry: dr.DeviceRegistry ) -> None: """Test if the MQTT component loads when config entry data not has all default settings.""" data = ( @@ -2704,11 +2668,9 @@ async def test_message_callback_exception_gets_logged( @pytest.mark.no_fail_on_log_exception +@pytest.mark.usefixtures("mock_debouncer", "setup_with_birth_msg_client_mock") async def test_message_partial_callback_exception_gets_logged( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_debouncer: asyncio.Event ) -> None: """Test exception raised by message handler.""" @@ -3730,9 +3692,7 @@ async def test_setup_manual_items_with_unique_ids( ], ) async def test_link_config_entry( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test manual and dynamically setup entities are linked to the config entry.""" # set up manual item @@ -3818,9 +3778,7 @@ async def test_link_config_entry( ], ) async def test_reload_config_entry( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test manual entities reloaded and set up correctly.""" await mqtt_mock_entry() @@ -3966,8 +3924,7 @@ async def test_reload_config_entry( ], ) async def test_reload_with_invalid_config( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test reloading yaml config fails.""" await mqtt_mock_entry() @@ -4007,8 +3964,7 @@ async def test_reload_with_invalid_config( ], ) async def test_reload_with_empty_config( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test reloading yaml config fails.""" await mqtt_mock_entry() @@ -4043,8 +3999,7 @@ async def test_reload_with_empty_config( ], ) async def test_reload_with_new_platform_config( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test reloading yaml with new platform config.""" await mqtt_mock_entry() @@ -4389,6 +4344,6 @@ async def test_loop_write_failure( "valid_subscribe_topic", ], ) -async def test_mqtt_integration_level_imports(hass: HomeAssistant, attr: str) -> None: +async def test_mqtt_integration_level_imports(attr: str) -> None: """Test mqtt integration level public published imports are available.""" assert hasattr(mqtt, attr) From 40384b9acdfac3eab46ebe19e902b89de3a6d755 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 27 Jun 2024 19:37:43 +0200 Subject: [PATCH 100/146] Split mqtt client tests (#120636) --- tests/components/mqtt/test_client.py | 1980 ++++++++++++++++++++++++++ tests/components/mqtt/test_init.py | 1962 +------------------------ 2 files changed, 1983 insertions(+), 1959 deletions(-) create mode 100644 tests/components/mqtt/test_client.py diff --git a/tests/components/mqtt/test_client.py b/tests/components/mqtt/test_client.py new file mode 100644 index 00000000000..49b590383d1 --- /dev/null +++ b/tests/components/mqtt/test_client.py @@ -0,0 +1,1980 @@ +"""The tests for the MQTT client.""" + +import asyncio +from datetime import datetime, timedelta +import socket +import ssl +from typing import Any +from unittest.mock import MagicMock, Mock, call, patch + +import certifi +import paho.mqtt.client as paho_mqtt +import pytest + +from homeassistant.components import mqtt +from homeassistant.components.mqtt.client import RECONNECT_INTERVAL_SECONDS +from homeassistant.components.mqtt.models import MessageCallbackType, ReceiveMessage +from homeassistant.config_entries import ConfigEntryDisabler, ConfigEntryState +from homeassistant.const import ( + CONF_PROTOCOL, + EVENT_HOMEASSISTANT_STARTED, + EVENT_HOMEASSISTANT_STOP, + UnitOfTemperature, +) +from homeassistant.core import CALLBACK_TYPE, CoreState, HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.util.dt import utcnow + +from .conftest import ENTRY_DEFAULT_BIRTH_MESSAGE +from .test_common import help_all_subscribe_calls + +from tests.common import ( + MockConfigEntry, + async_fire_mqtt_message, + async_fire_time_changed, +) +from tests.typing import MqttMockHAClient, MqttMockHAClientGenerator, MqttMockPahoClient + + +@pytest.fixture(autouse=True) +def mock_storage(hass_storage: dict[str, Any]) -> None: + """Autouse hass_storage for the TestCase tests.""" + + +def help_assert_message( + msg: ReceiveMessage, + topic: str | None = None, + payload: str | None = None, + qos: int | None = None, + retain: bool | None = None, +) -> bool: + """Return True if all of the given attributes match with the message.""" + match: bool = True + if topic is not None: + match &= msg.topic == topic + if payload is not None: + match &= msg.payload == payload + if qos is not None: + match &= msg.qos == qos + if retain is not None: + match &= msg.retain == retain + return match + + +async def test_mqtt_connects_on_home_assistant_mqtt_setup( + hass: HomeAssistant, setup_with_birth_msg_client_mock: MqttMockPahoClient +) -> None: + """Test if client is connected after mqtt init on bootstrap.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + assert mqtt_client_mock.connect.call_count == 1 + + +async def test_mqtt_does_not_disconnect_on_home_assistant_stop( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, +) -> None: + """Test if client is not disconnected on HA stop.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + hass.bus.fire(EVENT_HOMEASSISTANT_STOP) + await mock_debouncer.wait() + assert mqtt_client_mock.disconnect.call_count == 0 + + +async def test_mqtt_await_ack_at_disconnect(hass: HomeAssistant) -> None: + """Test if ACK is awaited correctly when disconnecting.""" + + class FakeInfo: + """Returns a simulated client publish response.""" + + mid = 100 + rc = 0 + + with patch( + "homeassistant.components.mqtt.async_client.AsyncMQTTClient" + ) as mock_client: + mqtt_client = mock_client.return_value + mqtt_client.connect = MagicMock( + return_value=0, + side_effect=lambda *args, **kwargs: hass.loop.call_soon_threadsafe( + mqtt_client.on_connect, mqtt_client, None, 0, 0, 0 + ), + ) + mqtt_client.publish = MagicMock(return_value=FakeInfo()) + entry = MockConfigEntry( + domain=mqtt.DOMAIN, + data={ + "certificate": "auto", + mqtt.CONF_BROKER: "test-broker", + mqtt.CONF_DISCOVERY: False, + }, + ) + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + + mqtt_client = mock_client.return_value + + # publish from MQTT client without awaiting + hass.async_create_task( + mqtt.async_publish(hass, "test-topic", "some-payload", 0, False) + ) + await asyncio.sleep(0) + # Simulate late ACK callback from client with mid 100 + mqtt_client.on_publish(0, 0, 100) + # disconnect the MQTT client + await hass.async_stop() + await hass.async_block_till_done() + # assert the payload was sent through the client + assert mqtt_client.publish.called + assert mqtt_client.publish.call_args[0] == ( + "test-topic", + "some-payload", + 0, + False, + ) + await hass.async_block_till_done(wait_background_tasks=True) + + +@pytest.mark.parametrize("mqtt_config_entry_data", [ENTRY_DEFAULT_BIRTH_MESSAGE]) +async def test_publish( + hass: HomeAssistant, setup_with_birth_msg_client_mock: MqttMockPahoClient +) -> None: + """Test the publish function.""" + publish_mock: MagicMock = setup_with_birth_msg_client_mock.publish + await mqtt.async_publish(hass, "test-topic", "test-payload") + await hass.async_block_till_done() + assert publish_mock.called + assert publish_mock.call_args[0] == ( + "test-topic", + "test-payload", + 0, + False, + ) + publish_mock.reset_mock() + + await mqtt.async_publish(hass, "test-topic", "test-payload", 2, True) + await hass.async_block_till_done() + assert publish_mock.called + assert publish_mock.call_args[0] == ( + "test-topic", + "test-payload", + 2, + True, + ) + publish_mock.reset_mock() + + mqtt.publish(hass, "test-topic2", "test-payload2") + await hass.async_block_till_done() + assert publish_mock.called + assert publish_mock.call_args[0] == ( + "test-topic2", + "test-payload2", + 0, + False, + ) + publish_mock.reset_mock() + + mqtt.publish(hass, "test-topic2", "test-payload2", 2, True) + await hass.async_block_till_done() + assert publish_mock.called + assert publish_mock.call_args[0] == ( + "test-topic2", + "test-payload2", + 2, + True, + ) + publish_mock.reset_mock() + + # test binary pass-through + mqtt.publish( + hass, + "test-topic3", + b"\xde\xad\xbe\xef", + 0, + False, + ) + await hass.async_block_till_done() + assert publish_mock.called + assert publish_mock.call_args[0] == ( + "test-topic3", + b"\xde\xad\xbe\xef", + 0, + False, + ) + publish_mock.reset_mock() + + # test null payload + mqtt.publish( + hass, + "test-topic3", + None, + 0, + False, + ) + await hass.async_block_till_done() + assert publish_mock.called + assert publish_mock.call_args[0] == ( + "test-topic3", + None, + 0, + False, + ) + + publish_mock.reset_mock() + + +async def test_convert_outgoing_payload(hass: HomeAssistant) -> None: + """Test the converting of outgoing MQTT payloads without template.""" + command_template = mqtt.MqttCommandTemplate(None, hass=hass) + assert command_template.async_render(b"\xde\xad\xbe\xef") == b"\xde\xad\xbe\xef" + assert ( + command_template.async_render("b'\\xde\\xad\\xbe\\xef'") + == "b'\\xde\\xad\\xbe\\xef'" + ) + assert command_template.async_render(1234) == 1234 + assert command_template.async_render(1234.56) == 1234.56 + assert command_template.async_render(None) is None + + +async def test_all_subscriptions_run_when_decode_fails( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test all other subscriptions still run when decode fails for one.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "test-topic", record_calls, encoding="ascii") + await mqtt.async_subscribe(hass, "test-topic", record_calls) + + async_fire_mqtt_message(hass, "test-topic", UnitOfTemperature.CELSIUS) + + await hass.async_block_till_done() + assert len(recorded_calls) == 1 + + +async def test_subscribe_topic( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of a topic.""" + await mqtt_mock_entry() + unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) + + async_fire_mqtt_message(hass, "test-topic", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 1 + assert recorded_calls[0].topic == "test-topic" + assert recorded_calls[0].payload == "test-payload" + + unsub() + + async_fire_mqtt_message(hass, "test-topic", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 1 + + # Cannot unsubscribe twice + with pytest.raises(HomeAssistantError): + unsub() + + +@pytest.mark.usefixtures("mqtt_mock_entry") +async def test_subscribe_topic_not_initialize( + hass: HomeAssistant, record_calls: MessageCallbackType +) -> None: + """Test the subscription of a topic when MQTT was not initialized.""" + with pytest.raises( + HomeAssistantError, match=r".*make sure MQTT is set up correctly" + ): + await mqtt.async_subscribe(hass, "test-topic", record_calls) + + +async def test_subscribe_mqtt_config_entry_disabled( + hass: HomeAssistant, mqtt_mock: MqttMockHAClient, record_calls: MessageCallbackType +) -> None: + """Test the subscription of a topic when MQTT config entry is disabled.""" + mqtt_mock.connected = True + + mqtt_config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] + assert mqtt_config_entry.state is ConfigEntryState.LOADED + + assert await hass.config_entries.async_unload(mqtt_config_entry.entry_id) + assert mqtt_config_entry.state is ConfigEntryState.NOT_LOADED + + await hass.config_entries.async_set_disabled_by( + mqtt_config_entry.entry_id, ConfigEntryDisabler.USER + ) + mqtt_mock.connected = False + + with pytest.raises(HomeAssistantError, match=r".*MQTT is not enabled"): + await mqtt.async_subscribe(hass, "test-topic", record_calls) + + +async def test_subscribe_and_resubscribe( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test resubscribing within the debounce time.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + with ( + patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.4), + patch("homeassistant.components.mqtt.client.UNSUBSCRIBE_COOLDOWN", 0.4), + ): + mock_debouncer.clear() + unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) + # This unsub will be un-done with the following subscribe + # unsubscribe should not be called at the broker + unsub() + unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) + await mock_debouncer.wait() + mock_debouncer.clear() + + async_fire_mqtt_message(hass, "test-topic", "test-payload") + + assert len(recorded_calls) == 1 + assert recorded_calls[0].topic == "test-topic" + assert recorded_calls[0].payload == "test-payload" + # assert unsubscribe was not called + mqtt_client_mock.unsubscribe.assert_not_called() + + mock_debouncer.clear() + unsub() + + await mock_debouncer.wait() + mqtt_client_mock.unsubscribe.assert_called_once_with(["test-topic"]) + + +async def test_subscribe_topic_non_async( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of a topic using the non-async function.""" + await mqtt_mock_entry() + await mock_debouncer.wait() + mock_debouncer.clear() + unsub = await hass.async_add_executor_job( + mqtt.subscribe, hass, "test-topic", record_calls + ) + await mock_debouncer.wait() + + async_fire_mqtt_message(hass, "test-topic", "test-payload") + + assert len(recorded_calls) == 1 + assert recorded_calls[0].topic == "test-topic" + assert recorded_calls[0].payload == "test-payload" + + mock_debouncer.clear() + await hass.async_add_executor_job(unsub) + await mock_debouncer.wait() + + async_fire_mqtt_message(hass, "test-topic", "test-payload") + + assert len(recorded_calls) == 1 + + +async def test_subscribe_bad_topic( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of a topic.""" + await mqtt_mock_entry() + with pytest.raises(HomeAssistantError): + await mqtt.async_subscribe(hass, 55, record_calls) # type: ignore[arg-type] + + +async def test_subscribe_topic_not_match( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test if subscribed topic is not a match.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "test-topic", record_calls) + + async_fire_mqtt_message(hass, "another-test-topic", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 0 + + +async def test_subscribe_topic_level_wildcard( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of wildcard topics.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "test-topic/+/on", record_calls) + + async_fire_mqtt_message(hass, "test-topic/bier/on", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 1 + assert recorded_calls[0].topic == "test-topic/bier/on" + assert recorded_calls[0].payload == "test-payload" + + +async def test_subscribe_topic_level_wildcard_no_subtree_match( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of wildcard topics.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "test-topic/+/on", record_calls) + + async_fire_mqtt_message(hass, "test-topic/bier", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 0 + + +async def test_subscribe_topic_level_wildcard_root_topic_no_subtree_match( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of wildcard topics.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "test-topic/#", record_calls) + + async_fire_mqtt_message(hass, "test-topic-123", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 0 + + +async def test_subscribe_topic_subtree_wildcard_subtree_topic( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of wildcard topics.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "test-topic/#", record_calls) + + async_fire_mqtt_message(hass, "test-topic/bier/on", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 1 + assert recorded_calls[0].topic == "test-topic/bier/on" + assert recorded_calls[0].payload == "test-payload" + + +async def test_subscribe_topic_subtree_wildcard_root_topic( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of wildcard topics.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "test-topic/#", record_calls) + + async_fire_mqtt_message(hass, "test-topic", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 1 + assert recorded_calls[0].topic == "test-topic" + assert recorded_calls[0].payload == "test-payload" + + +async def test_subscribe_topic_subtree_wildcard_no_match( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of wildcard topics.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "test-topic/#", record_calls) + + async_fire_mqtt_message(hass, "another-test-topic", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 0 + + +async def test_subscribe_topic_level_wildcard_and_wildcard_root_topic( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of wildcard topics.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "+/test-topic/#", record_calls) + + async_fire_mqtt_message(hass, "hi/test-topic", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 1 + assert recorded_calls[0].topic == "hi/test-topic" + assert recorded_calls[0].payload == "test-payload" + + +async def test_subscribe_topic_level_wildcard_and_wildcard_subtree_topic( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of wildcard topics.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "+/test-topic/#", record_calls) + + async_fire_mqtt_message(hass, "hi/test-topic/here-iam", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 1 + assert recorded_calls[0].topic == "hi/test-topic/here-iam" + assert recorded_calls[0].payload == "test-payload" + + +async def test_subscribe_topic_level_wildcard_and_wildcard_level_no_match( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of wildcard topics.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "+/test-topic/#", record_calls) + + async_fire_mqtt_message(hass, "hi/here-iam/test-topic", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 0 + + +async def test_subscribe_topic_level_wildcard_and_wildcard_no_match( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of wildcard topics.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "+/test-topic/#", record_calls) + + async_fire_mqtt_message(hass, "hi/another-test-topic", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 0 + + +async def test_subscribe_topic_sys_root( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of $ root topics.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "$test-topic/subtree/on", record_calls) + + async_fire_mqtt_message(hass, "$test-topic/subtree/on", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 1 + assert recorded_calls[0].topic == "$test-topic/subtree/on" + assert recorded_calls[0].payload == "test-payload" + + +async def test_subscribe_topic_sys_root_and_wildcard_topic( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of $ root and wildcard topics.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "$test-topic/#", record_calls) + + async_fire_mqtt_message(hass, "$test-topic/some-topic", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 1 + assert recorded_calls[0].topic == "$test-topic/some-topic" + assert recorded_calls[0].payload == "test-payload" + + +async def test_subscribe_topic_sys_root_and_wildcard_subtree_topic( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of $ root and wildcard subtree topics.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "$test-topic/subtree/#", record_calls) + + async_fire_mqtt_message(hass, "$test-topic/subtree/some-topic", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 1 + assert recorded_calls[0].topic == "$test-topic/subtree/some-topic" + assert recorded_calls[0].payload == "test-payload" + + +async def test_subscribe_special_characters( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription to topics with special characters.""" + await mqtt_mock_entry() + topic = "/test-topic/$(.)[^]{-}" + payload = "p4y.l[]a|> ?" + + await mqtt.async_subscribe(hass, topic, record_calls) + + async_fire_mqtt_message(hass, topic, payload) + await hass.async_block_till_done() + assert len(recorded_calls) == 1 + assert recorded_calls[0].topic == topic + assert recorded_calls[0].payload == payload + + +async def test_subscribe_same_topic( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, +) -> None: + """Test subscribing to same topic twice and simulate retained messages. + + When subscribing to the same topic again, SUBSCRIBE must be sent to the broker again + for it to resend any retained messages. + """ + mqtt_client_mock = setup_with_birth_msg_client_mock + calls_a: list[ReceiveMessage] = [] + calls_b: list[ReceiveMessage] = [] + + @callback + def _callback_a(msg: ReceiveMessage) -> None: + calls_a.append(msg) + + @callback + def _callback_b(msg: ReceiveMessage) -> None: + calls_b.append(msg) + + mqtt_client_mock.reset_mock() + mock_debouncer.clear() + await mqtt.async_subscribe(hass, "test/state", _callback_a, qos=0) + # Simulate a non retained message after the first subscription + async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=False) + await mock_debouncer.wait() + assert len(calls_a) == 1 + mqtt_client_mock.subscribe.assert_called() + calls_a = [] + mqtt_client_mock.reset_mock() + + await hass.async_block_till_done() + mock_debouncer.clear() + await mqtt.async_subscribe(hass, "test/state", _callback_b, qos=1) + # Simulate an other non retained message after the second subscription + async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=False) + await mock_debouncer.wait() + # Both subscriptions should receive updates + assert len(calls_a) == 1 + assert len(calls_b) == 1 + mqtt_client_mock.subscribe.assert_called() + + +async def test_replaying_payload_same_topic( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, +) -> None: + """Test replaying retained messages. + + When subscribing to the same topic again, SUBSCRIBE must be sent to the broker again + for it to resend any retained messages for new subscriptions. + Retained messages must only be replayed for new subscriptions, except + when the MQTT client is reconnecting. + """ + mqtt_client_mock = setup_with_birth_msg_client_mock + calls_a: list[ReceiveMessage] = [] + calls_b: list[ReceiveMessage] = [] + + @callback + def _callback_a(msg: ReceiveMessage) -> None: + calls_a.append(msg) + + @callback + def _callback_b(msg: ReceiveMessage) -> None: + calls_b.append(msg) + + mqtt_client_mock.reset_mock() + mock_debouncer.clear() + await mqtt.async_subscribe(hass, "test/state", _callback_a) + await mock_debouncer.wait() + async_fire_mqtt_message( + hass, "test/state", "online", qos=0, retain=True + ) # Simulate a (retained) message played back + assert len(calls_a) == 1 + mqtt_client_mock.subscribe.assert_called() + calls_a = [] + mqtt_client_mock.reset_mock() + + mock_debouncer.clear() + await mqtt.async_subscribe(hass, "test/state", _callback_b) + await mock_debouncer.wait() + + # Simulate edge case where non retained message was received + # after subscription at HA but before the debouncer delay was passed. + # The message without retain flag directly after a subscription should + # be processed by both subscriptions. + async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=False) + + # Simulate a (retained) message played back on new subscriptions + async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=True) + + # The current subscription only received the message without retain flag + assert len(calls_a) == 1 + assert help_assert_message(calls_a[0], "test/state", "online", qos=0, retain=False) + # The retained message playback should only be processed by the new subscription. + # The existing subscription already got the latest update, hence the existing + # subscription should not receive the replayed (retained) message. + # Messages without retain flag are received on both subscriptions. + assert len(calls_b) == 2 + assert help_assert_message(calls_b[0], "test/state", "online", qos=0, retain=False) + assert help_assert_message(calls_b[1], "test/state", "online", qos=0, retain=True) + mqtt_client_mock.subscribe.assert_called() + + calls_a = [] + calls_b = [] + mqtt_client_mock.reset_mock() + + # Simulate new message played back on new subscriptions + # After connecting the retain flag will not be set, even if the + # payload published was retained, we cannot see that + async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=False) + assert len(calls_a) == 1 + assert help_assert_message(calls_a[0], "test/state", "online", qos=0, retain=False) + assert len(calls_b) == 1 + assert help_assert_message(calls_b[0], "test/state", "online", qos=0, retain=False) + + # Now simulate the broker was disconnected shortly + calls_a = [] + calls_b = [] + mqtt_client_mock.reset_mock() + mqtt_client_mock.on_disconnect(None, None, 0) + + mock_debouncer.clear() + mqtt_client_mock.on_connect(None, None, None, 0) + await mock_debouncer.wait() + mqtt_client_mock.subscribe.assert_called() + # Simulate a (retained) message played back after reconnecting + async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=True) + # Both subscriptions now should replay the retained message + assert len(calls_a) == 1 + assert help_assert_message(calls_a[0], "test/state", "online", qos=0, retain=True) + assert len(calls_b) == 1 + assert help_assert_message(calls_b[0], "test/state", "online", qos=0, retain=True) + + +async def test_replaying_payload_after_resubscribing( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, +) -> None: + """Test replaying and filtering retained messages after resubscribing. + + When subscribing to the same topic again, SUBSCRIBE must be sent to the broker again + for it to resend any retained messages for new subscriptions. + Retained messages must only be replayed for new subscriptions, except + when the MQTT client is reconnection. + """ + mqtt_client_mock = setup_with_birth_msg_client_mock + calls_a: list[ReceiveMessage] = [] + + @callback + def _callback_a(msg: ReceiveMessage) -> None: + calls_a.append(msg) + + mqtt_client_mock.reset_mock() + mock_debouncer.clear() + unsub = await mqtt.async_subscribe(hass, "test/state", _callback_a) + await mock_debouncer.wait() + mqtt_client_mock.subscribe.assert_called() + + # Simulate a (retained) message played back + async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=True) + assert help_assert_message(calls_a[0], "test/state", "online", qos=0, retain=True) + calls_a.clear() + + # Test we get updates + async_fire_mqtt_message(hass, "test/state", "offline", qos=0, retain=False) + assert help_assert_message(calls_a[0], "test/state", "offline", qos=0, retain=False) + calls_a.clear() + + # Test we filter new retained updates + async_fire_mqtt_message(hass, "test/state", "offline", qos=0, retain=True) + await hass.async_block_till_done() + assert len(calls_a) == 0 + + # Unsubscribe an resubscribe again + mock_debouncer.clear() + unsub() + unsub = await mqtt.async_subscribe(hass, "test/state", _callback_a) + await mock_debouncer.wait() + mqtt_client_mock.subscribe.assert_called() + + # Simulate we can receive a (retained) played back message again + async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=True) + assert help_assert_message(calls_a[0], "test/state", "online", qos=0, retain=True) + + +async def test_replaying_payload_wildcard_topic( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, +) -> None: + """Test replaying retained messages. + + When we have multiple subscriptions to the same wildcard topic, + SUBSCRIBE must be sent to the broker again + for it to resend any retained messages for new subscriptions. + Retained messages should only be replayed for new subscriptions, except + when the MQTT client is reconnection. + """ + mqtt_client_mock = setup_with_birth_msg_client_mock + calls_a: list[ReceiveMessage] = [] + calls_b: list[ReceiveMessage] = [] + + @callback + def _callback_a(msg: ReceiveMessage) -> None: + calls_a.append(msg) + + @callback + def _callback_b(msg: ReceiveMessage) -> None: + calls_b.append(msg) + + mqtt_client_mock.reset_mock() + mock_debouncer.clear() + await mqtt.async_subscribe(hass, "test/#", _callback_a) + await mock_debouncer.wait() + # Simulate (retained) messages being played back on new subscriptions + async_fire_mqtt_message(hass, "test/state1", "new_value_1", qos=0, retain=True) + async_fire_mqtt_message(hass, "test/state2", "new_value_2", qos=0, retain=True) + assert len(calls_a) == 2 + mqtt_client_mock.subscribe.assert_called() + calls_a = [] + mqtt_client_mock.reset_mock() + + # resubscribe to the wild card topic again + mock_debouncer.clear() + await mqtt.async_subscribe(hass, "test/#", _callback_b) + await mock_debouncer.wait() + # Simulate (retained) messages being played back on new subscriptions + async_fire_mqtt_message(hass, "test/state1", "initial_value_1", qos=0, retain=True) + async_fire_mqtt_message(hass, "test/state2", "initial_value_2", qos=0, retain=True) + # The retained messages playback should only be processed for the new subscriptions + assert len(calls_a) == 0 + assert len(calls_b) == 2 + mqtt_client_mock.subscribe.assert_called() + + calls_a = [] + calls_b = [] + mqtt_client_mock.reset_mock() + + # Simulate new messages being received + async_fire_mqtt_message(hass, "test/state1", "update_value_1", qos=0, retain=False) + async_fire_mqtt_message(hass, "test/state2", "update_value_2", qos=0, retain=False) + assert len(calls_a) == 2 + assert len(calls_b) == 2 + + # Now simulate the broker was disconnected shortly + calls_a = [] + calls_b = [] + mqtt_client_mock.reset_mock() + mqtt_client_mock.on_disconnect(None, None, 0) + + mock_debouncer.clear() + mqtt_client_mock.on_connect(None, None, None, 0) + await mock_debouncer.wait() + + mqtt_client_mock.subscribe.assert_called() + # Simulate the (retained) messages are played back after reconnecting + # for all subscriptions + async_fire_mqtt_message(hass, "test/state1", "update_value_1", qos=0, retain=True) + async_fire_mqtt_message(hass, "test/state2", "update_value_2", qos=0, retain=True) + # Both subscriptions should replay + assert len(calls_a) == 2 + assert len(calls_b) == 2 + + +async def test_not_calling_unsubscribe_with_active_subscribers( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + record_calls: MessageCallbackType, +) -> None: + """Test not calling unsubscribe() when other subscribers are active.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + mqtt_client_mock.reset_mock() + mock_debouncer.clear() + unsub = await mqtt.async_subscribe(hass, "test/state", record_calls, 2) + await mqtt.async_subscribe(hass, "test/state", record_calls, 1) + await mock_debouncer.wait() + assert mqtt_client_mock.subscribe.called + + mock_debouncer.clear() + unsub() + await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown + assert not mqtt_client_mock.unsubscribe.called + assert not mock_debouncer.is_set() + + +async def test_not_calling_subscribe_when_unsubscribed_within_cooldown( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + mqtt_mock_entry: MqttMockHAClientGenerator, + record_calls: MessageCallbackType, +) -> None: + """Test not calling subscribe() when it is unsubscribed. + + Make sure subscriptions are cleared if unsubscribed before + the subscribe cool down period has ended. + """ + mqtt_mock = await mqtt_mock_entry() + mqtt_client_mock = mqtt_mock._mqttc + await mock_debouncer.wait() + + mock_debouncer.clear() + mqtt_client_mock.subscribe.reset_mock() + unsub = await mqtt.async_subscribe(hass, "test/state", record_calls) + unsub() + await mock_debouncer.wait() + # The debouncer executes without an pending subscribes + assert not mqtt_client_mock.subscribe.called + + +async def test_unsubscribe_race( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, +) -> None: + """Test not calling unsubscribe() when other subscribers are active.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + calls_a: list[ReceiveMessage] = [] + calls_b: list[ReceiveMessage] = [] + + @callback + def _callback_a(msg: ReceiveMessage) -> None: + calls_a.append(msg) + + @callback + def _callback_b(msg: ReceiveMessage) -> None: + calls_b.append(msg) + + mqtt_client_mock.reset_mock() + + mock_debouncer.clear() + unsub = await mqtt.async_subscribe(hass, "test/state", _callback_a) + unsub() + await mqtt.async_subscribe(hass, "test/state", _callback_b) + await mock_debouncer.wait() + + async_fire_mqtt_message(hass, "test/state", "online") + assert not calls_a + assert calls_b + + # We allow either calls [subscribe, unsubscribe, subscribe], [subscribe, subscribe] or + # when both subscriptions were combined [subscribe] + expected_calls_1 = [ + call.subscribe([("test/state", 0)]), + call.unsubscribe("test/state"), + call.subscribe([("test/state", 0)]), + ] + expected_calls_2 = [ + call.subscribe([("test/state", 0)]), + call.subscribe([("test/state", 0)]), + ] + expected_calls_3 = [ + call.subscribe([("test/state", 0)]), + ] + assert mqtt_client_mock.mock_calls in ( + expected_calls_1, + expected_calls_2, + expected_calls_3, + ) + + +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], +) +async def test_restore_subscriptions_on_reconnect( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + record_calls: MessageCallbackType, +) -> None: + """Test subscriptions are restored on reconnect.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + + mqtt_client_mock.reset_mock() + + mock_debouncer.clear() + await mqtt.async_subscribe(hass, "test/state", record_calls) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown + await mock_debouncer.wait() + assert ("test/state", 0) in help_all_subscribe_calls(mqtt_client_mock) + + mqtt_client_mock.reset_mock() + mqtt_client_mock.on_disconnect(None, None, 0) + + mock_debouncer.clear() + mqtt_client_mock.on_connect(None, None, None, 0) + await mock_debouncer.wait() + assert ("test/state", 0) in help_all_subscribe_calls(mqtt_client_mock) + + +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], +) +async def test_restore_all_active_subscriptions_on_reconnect( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + record_calls: MessageCallbackType, +) -> None: + """Test active subscriptions are restored correctly on reconnect.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + mqtt_client_mock.reset_mock() + mock_debouncer.clear() + unsub = await mqtt.async_subscribe(hass, "test/state", record_calls, qos=2) + await mqtt.async_subscribe(hass, "test/state", record_calls, qos=1) + await mqtt.async_subscribe(hass, "test/state", record_calls, qos=0) + # cooldown + await mock_debouncer.wait() + + # the subscription with the highest QoS should survive + expected = [ + call([("test/state", 2)]), + ] + assert mqtt_client_mock.subscribe.mock_calls == expected + + unsub() + assert mqtt_client_mock.unsubscribe.call_count == 0 + + mqtt_client_mock.on_disconnect(None, None, 0) + + mock_debouncer.clear() + mqtt_client_mock.on_connect(None, None, None, 0) + # wait for cooldown + await mock_debouncer.wait() + + expected.append(call([("test/state", 1)])) + for expected_call in expected: + assert mqtt_client_mock.subscribe.hass_call(expected_call) + + +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], +) +async def test_subscribed_at_highest_qos( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + record_calls: MessageCallbackType, +) -> None: + """Test the highest qos as assigned when subscribing to the same topic.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + mqtt_client_mock.reset_mock() + mock_debouncer.clear() + await mqtt.async_subscribe(hass, "test/state", record_calls, qos=0) + await hass.async_block_till_done() + # cooldown + await mock_debouncer.wait() + assert ("test/state", 0) in help_all_subscribe_calls(mqtt_client_mock) + mqtt_client_mock.reset_mock() + + mock_debouncer.clear() + await mqtt.async_subscribe(hass, "test/state", record_calls, qos=1) + await mqtt.async_subscribe(hass, "test/state", record_calls, qos=2) + # cooldown + await mock_debouncer.wait() + + # the subscription with the highest QoS should survive + assert help_all_subscribe_calls(mqtt_client_mock) == [("test/state", 2)] + + +async def test_initial_setup_logs_error( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + mqtt_client_mock: MqttMockPahoClient, +) -> None: + """Test for setup failure if initial client connection fails.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) + entry.add_to_hass(hass) + mqtt_client_mock.connect.side_effect = MagicMock(return_value=1) + try: + assert await hass.config_entries.async_setup(entry.entry_id) + except HomeAssistantError: + assert True + assert "Failed to connect to MQTT server:" in caplog.text + + +async def test_logs_error_if_no_connect_broker( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + setup_with_birth_msg_client_mock: MqttMockPahoClient, +) -> None: + """Test for setup failure if connection to broker is missing.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + # test with rc = 3 -> broker unavailable + mqtt_client_mock.on_disconnect(Mock(), None, 0) + mqtt_client_mock.on_connect(Mock(), None, None, 3) + await hass.async_block_till_done() + assert ( + "Unable to connect to the MQTT broker: Connection Refused: broker unavailable." + in caplog.text + ) + + +@pytest.mark.parametrize("return_code", [4, 5]) +async def test_triggers_reauth_flow_if_auth_fails( + hass: HomeAssistant, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + return_code: int, +) -> None: + """Test re-auth is triggered if authentication is failing.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + # test with rc = 4 -> CONNACK_REFUSED_NOT_AUTHORIZED and 5 -> CONNACK_REFUSED_BAD_USERNAME_PASSWORD + mqtt_client_mock.on_disconnect(Mock(), None, 0) + mqtt_client_mock.on_connect(Mock(), None, None, return_code) + await hass.async_block_till_done() + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert flows[0]["context"]["source"] == "reauth" + + +@patch("homeassistant.components.mqtt.client.TIMEOUT_ACK", 0.3) +async def test_handle_mqtt_on_callback( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + setup_with_birth_msg_client_mock: MqttMockPahoClient, +) -> None: + """Test receiving an ACK callback before waiting for it.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + with patch.object(mqtt_client_mock, "get_mid", return_value=100): + # Simulate an ACK for mid == 100, this will call mqtt_mock._async_get_mid_future(mid) + mqtt_client_mock.on_publish(mqtt_client_mock, None, 100) + await hass.async_block_till_done() + # Make sure the ACK has been received + await hass.async_block_till_done() + # Now call publish without call back, this will call _async_async_wait_for_mid(msg_info.mid) + await mqtt.async_publish(hass, "no_callback/test-topic", "test-payload") + # Since the mid event was already set, we should not see any timeout warning in the log + await hass.async_block_till_done() + assert "No ACK from MQTT server" not in caplog.text + + +async def test_handle_mqtt_on_callback_after_timeout( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + mqtt_mock_entry: MqttMockHAClientGenerator, + mqtt_client_mock: MqttMockPahoClient, +) -> None: + """Test receiving an ACK after a timeout.""" + mqtt_mock = await mqtt_mock_entry() + # Simulate the mid future getting a timeout + mqtt_mock()._async_get_mid_future(101).set_exception(asyncio.TimeoutError) + # Simulate an ACK for mid == 101, being received after the timeout + mqtt_client_mock.on_publish(mqtt_client_mock, None, 101) + await hass.async_block_till_done() + assert "No ACK from MQTT server" not in caplog.text + assert "InvalidStateError" not in caplog.text + + +async def test_publish_error( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test publish error.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) + entry.add_to_hass(hass) + + # simulate an Out of memory error + with patch( + "homeassistant.components.mqtt.async_client.AsyncMQTTClient" + ) as mock_client: + mock_client().connect = lambda *args: 1 + mock_client().publish().rc = 1 + assert await hass.config_entries.async_setup(entry.entry_id) + with pytest.raises(HomeAssistantError): + await mqtt.async_publish( + hass, "some-topic", b"test-payload", qos=0, retain=False, encoding=None + ) + assert "Failed to connect to MQTT server: Out of memory." in caplog.text + + +async def test_subscribe_error( + hass: HomeAssistant, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + record_calls: MessageCallbackType, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test publish error.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + mqtt_client_mock.reset_mock() + # simulate client is not connected error before subscribing + mqtt_client_mock.subscribe.side_effect = lambda *args: (4, None) + await mqtt.async_subscribe(hass, "some-topic", record_calls) + while mqtt_client_mock.subscribe.call_count == 0: + await hass.async_block_till_done() + await hass.async_block_till_done() + assert ( + "Error talking to MQTT: The client is not currently connected." in caplog.text + ) + + +async def test_handle_message_callback( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, +) -> None: + """Test for handling an incoming message callback.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + callbacks = [] + + @callback + def _callback(args) -> None: + callbacks.append(args) + + msg = ReceiveMessage( + "some-topic", b"test-payload", 1, False, "some-topic", datetime.now() + ) + mock_debouncer.clear() + await mqtt.async_subscribe(hass, "some-topic", _callback) + await mock_debouncer.wait() + mqtt_client_mock.reset_mock() + mqtt_client_mock.on_message(None, None, msg) + + assert len(callbacks) == 1 + assert callbacks[0].topic == "some-topic" + assert callbacks[0].qos == 1 + assert callbacks[0].payload == "test-payload" + + +@pytest.mark.parametrize( + ("mqtt_config_entry_data", "protocol"), + [ + ( + { + mqtt.CONF_BROKER: "mock-broker", + CONF_PROTOCOL: "3.1", + }, + 3, + ), + ( + { + mqtt.CONF_BROKER: "mock-broker", + CONF_PROTOCOL: "3.1.1", + }, + 4, + ), + ( + { + mqtt.CONF_BROKER: "mock-broker", + CONF_PROTOCOL: "5", + }, + 5, + ), + ], +) +async def test_setup_mqtt_client_protocol( + mqtt_mock_entry: MqttMockHAClientGenerator, protocol: int +) -> None: + """Test MQTT client protocol setup.""" + with patch( + "homeassistant.components.mqtt.async_client.AsyncMQTTClient" + ) as mock_client: + await mqtt_mock_entry() + + # check if protocol setup was correctly + assert mock_client.call_args[1]["protocol"] == protocol + + +@patch("homeassistant.components.mqtt.client.TIMEOUT_ACK", 0.2) +async def test_handle_mqtt_timeout_on_callback( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_debouncer: asyncio.Event +) -> None: + """Test publish without receiving an ACK callback.""" + mid = 0 + + class FakeInfo: + """Returns a simulated client publish response.""" + + mid = 102 + rc = 0 + + with patch( + "homeassistant.components.mqtt.async_client.AsyncMQTTClient" + ) as mock_client: + + def _mock_ack(topic: str, qos: int = 0) -> tuple[int, int]: + # Handle ACK for subscribe normally + nonlocal mid + mid += 1 + mock_client.on_subscribe(0, 0, mid) + return (0, mid) + + # We want to simulate the publish behaviour MQTT client + mock_client = mock_client.return_value + mock_client.publish.return_value = FakeInfo() + # Mock we get a mid and rc=0 + mock_client.subscribe.side_effect = _mock_ack + mock_client.unsubscribe.side_effect = _mock_ack + mock_client.connect = MagicMock( + return_value=0, + side_effect=lambda *args, **kwargs: hass.loop.call_soon_threadsafe( + mock_client.on_connect, mock_client, None, 0, 0, 0 + ), + ) + + entry = MockConfigEntry( + domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"} + ) + entry.add_to_hass(hass) + + # Set up the integration + mock_debouncer.clear() + assert await hass.config_entries.async_setup(entry.entry_id) + + # Now call we publish without simulating and ACK callback + await mqtt.async_publish(hass, "no_callback/test-topic", "test-payload") + await hass.async_block_till_done() + # There is no ACK so we should see a timeout in the log after publishing + assert len(mock_client.publish.mock_calls) == 1 + assert "No ACK from MQTT server" in caplog.text + # Ensure we stop lingering background tasks + await hass.config_entries.async_unload(entry.entry_id) + # Assert we did not have any completed subscribes, + # because the debouncer subscribe job failed to receive an ACK, + # and the time auto caused the debouncer job to fail. + assert not mock_debouncer.is_set() + + +async def test_setup_raises_config_entry_not_ready_if_no_connect_broker( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test for setup failure if connection to broker is missing.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.mqtt.async_client.AsyncMQTTClient" + ) as mock_client: + mock_client().connect = MagicMock(side_effect=OSError("Connection error")) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert "Failed to connect to MQTT server due to exception:" in caplog.text + + +@pytest.mark.parametrize( + ("mqtt_config_entry_data", "insecure_param"), + [ + ({"broker": "test-broker", "certificate": "auto"}, "not set"), + ( + {"broker": "test-broker", "certificate": "auto", "tls_insecure": False}, + False, + ), + ({"broker": "test-broker", "certificate": "auto", "tls_insecure": True}, True), + ], +) +async def test_setup_uses_certificate_on_certificate_set_to_auto_and_insecure( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + insecure_param: bool | str, +) -> None: + """Test setup uses bundled certs when certificate is set to auto and insecure.""" + calls = [] + insecure_check = {"insecure": "not set"} + + def mock_tls_set( + certificate, certfile=None, keyfile=None, tls_version=None + ) -> None: + calls.append((certificate, certfile, keyfile, tls_version)) + + def mock_tls_insecure_set(insecure_param) -> None: + insecure_check["insecure"] = insecure_param + + with patch( + "homeassistant.components.mqtt.async_client.AsyncMQTTClient" + ) as mock_client: + mock_client().tls_set = mock_tls_set + mock_client().tls_insecure_set = mock_tls_insecure_set + await mqtt_mock_entry() + await hass.async_block_till_done() + + assert calls + + expected_certificate = certifi.where() + assert calls[0][0] == expected_certificate + + # test if insecure is set + assert insecure_check["insecure"] == insecure_param + + +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [ + { + mqtt.CONF_BROKER: "mock-broker", + mqtt.CONF_CERTIFICATE: "auto", + } + ], +) +async def test_tls_version( + hass: HomeAssistant, + mqtt_client_mock: MqttMockPahoClient, + mqtt_mock_entry: MqttMockHAClientGenerator, +) -> None: + """Test setup defaults for tls.""" + await mqtt_mock_entry() + await hass.async_block_till_done() + assert ( + mqtt_client_mock.tls_set.mock_calls[0][2]["tls_version"] + == ssl.PROTOCOL_TLS_CLIENT + ) + + +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [ + { + mqtt.CONF_BROKER: "mock-broker", + mqtt.CONF_BIRTH_MESSAGE: { + mqtt.ATTR_TOPIC: "birth", + mqtt.ATTR_PAYLOAD: "birth", + mqtt.ATTR_QOS: 0, + mqtt.ATTR_RETAIN: False, + }, + } + ], +) +@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) +@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) +@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) +async def test_custom_birth_message( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + mqtt_config_entry_data: dict[str, Any], + mqtt_client_mock: MqttMockPahoClient, +) -> None: + """Test sending birth message.""" + + entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) + entry.add_to_hass(hass) + hass.config.components.add(mqtt.DOMAIN) + assert await hass.config_entries.async_setup(entry.entry_id) + mock_debouncer.clear() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + # discovery cooldown + await mock_debouncer.wait() + # Wait for publish call to finish + await hass.async_block_till_done(wait_background_tasks=True) + mqtt_client_mock.publish.assert_called_with("birth", "birth", 0, False) + + +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [ENTRY_DEFAULT_BIRTH_MESSAGE], +) +async def test_default_birth_message( + hass: HomeAssistant, setup_with_birth_msg_client_mock: MqttMockPahoClient +) -> None: + """Test sending birth message.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + await hass.async_block_till_done(wait_background_tasks=True) + mqtt_client_mock.publish.assert_called_with( + "homeassistant/status", "online", 0, False + ) + + +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_BIRTH_MESSAGE: {}}], +) +@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) +@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) +@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) +async def test_no_birth_message( + hass: HomeAssistant, + record_calls: MessageCallbackType, + mock_debouncer: asyncio.Event, + mqtt_config_entry_data: dict[str, Any], + mqtt_client_mock: MqttMockPahoClient, +) -> None: + """Test disabling birth message.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) + entry.add_to_hass(hass) + hass.config.components.add(mqtt.DOMAIN) + mock_debouncer.clear() + assert await hass.config_entries.async_setup(entry.entry_id) + # Wait for discovery cooldown + await mock_debouncer.wait() + # Ensure any publishing could have been processed + await hass.async_block_till_done(wait_background_tasks=True) + mqtt_client_mock.publish.assert_not_called() + + mqtt_client_mock.reset_mock() + mock_debouncer.clear() + await mqtt.async_subscribe(hass, "homeassistant/some-topic", record_calls) + # Wait for discovery cooldown + await mock_debouncer.wait() + mqtt_client_mock.subscribe.assert_called() + + +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [ENTRY_DEFAULT_BIRTH_MESSAGE], +) +@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.2) +async def test_delayed_birth_message( + hass: HomeAssistant, + mqtt_config_entry_data: dict[str, Any], + mqtt_client_mock: MqttMockPahoClient, +) -> None: + """Test sending birth message does not happen until Home Assistant starts.""" + hass.set_state(CoreState.starting) + await hass.async_block_till_done() + birth = asyncio.Event() + entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) + entry.add_to_hass(hass) + hass.config.components.add(mqtt.DOMAIN) + assert await hass.config_entries.async_setup(entry.entry_id) + + @callback + def wait_birth(msg: ReceiveMessage) -> None: + """Handle birth message.""" + birth.set() + + await mqtt.async_subscribe(hass, "homeassistant/status", wait_birth) + with pytest.raises(TimeoutError): + await asyncio.wait_for(birth.wait(), 0.05) + assert not mqtt_client_mock.publish.called + assert not birth.is_set() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await birth.wait() + mqtt_client_mock.publish.assert_called_with( + "homeassistant/status", "online", 0, False + ) + + +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [ENTRY_DEFAULT_BIRTH_MESSAGE], +) +async def test_subscription_done_when_birth_message_is_sent( + setup_with_birth_msg_client_mock: MqttMockPahoClient, +) -> None: + """Test sending birth message until initial subscription has been completed.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + subscribe_calls = help_all_subscribe_calls(mqtt_client_mock) + assert ("homeassistant/+/+/config", 0) in subscribe_calls + assert ("homeassistant/+/+/+/config", 0) in subscribe_calls + mqtt_client_mock.publish.assert_called_with( + "homeassistant/status", "online", 0, False + ) + + +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [ + { + mqtt.CONF_BROKER: "mock-broker", + mqtt.CONF_WILL_MESSAGE: { + mqtt.ATTR_TOPIC: "death", + mqtt.ATTR_PAYLOAD: "death", + mqtt.ATTR_QOS: 0, + mqtt.ATTR_RETAIN: False, + }, + } + ], +) +async def test_custom_will_message( + hass: HomeAssistant, + mqtt_config_entry_data: dict[str, Any], + mqtt_client_mock: MqttMockPahoClient, +) -> None: + """Test will message.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) + entry.add_to_hass(hass) + hass.config.components.add(mqtt.DOMAIN) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + mqtt_client_mock.will_set.assert_called_with( + topic="death", payload="death", qos=0, retain=False + ) + + +async def test_default_will_message( + setup_with_birth_msg_client_mock: MqttMockPahoClient, +) -> None: + """Test will message.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + mqtt_client_mock.will_set.assert_called_with( + topic="homeassistant/status", payload="offline", qos=0, retain=False + ) + + +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_WILL_MESSAGE: {}}], +) +async def test_no_will_message( + hass: HomeAssistant, + mqtt_config_entry_data: dict[str, Any], + mqtt_client_mock: MqttMockPahoClient, +) -> None: + """Test will message.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) + entry.add_to_hass(hass) + hass.config.components.add(mqtt.DOMAIN) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + mqtt_client_mock.will_set.assert_not_called() + + +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [ENTRY_DEFAULT_BIRTH_MESSAGE | {mqtt.CONF_DISCOVERY: False}], +) +async def test_mqtt_subscribes_topics_on_connect( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + record_calls: MessageCallbackType, +) -> None: + """Test subscription to topic on connect.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + + mock_debouncer.clear() + await mqtt.async_subscribe(hass, "topic/test", record_calls) + await mqtt.async_subscribe(hass, "home/sensor", record_calls, 2) + await mqtt.async_subscribe(hass, "still/pending", record_calls) + await mqtt.async_subscribe(hass, "still/pending", record_calls, 1) + await mock_debouncer.wait() + + mqtt_client_mock.on_disconnect(Mock(), None, 0) + + mqtt_client_mock.reset_mock() + + mock_debouncer.clear() + mqtt_client_mock.on_connect(Mock(), None, 0, 0) + await mock_debouncer.wait() + + subscribe_calls = help_all_subscribe_calls(mqtt_client_mock) + assert ("topic/test", 0) in subscribe_calls + assert ("home/sensor", 2) in subscribe_calls + assert ("still/pending", 1) in subscribe_calls + + +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [ENTRY_DEFAULT_BIRTH_MESSAGE], +) +async def test_mqtt_subscribes_in_single_call( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + record_calls: MessageCallbackType, +) -> None: + """Test bundled client subscription to topic.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + mqtt_client_mock.subscribe.reset_mock() + mock_debouncer.clear() + await mqtt.async_subscribe(hass, "topic/test", record_calls) + await mqtt.async_subscribe(hass, "home/sensor", record_calls) + # Make sure the debouncer finishes + await mock_debouncer.wait() + + assert mqtt_client_mock.subscribe.call_count == 1 + # Assert we have a single subscription call with both subscriptions + assert mqtt_client_mock.subscribe.mock_calls[0][1][0] in [ + [("topic/test", 0), ("home/sensor", 0)], + [("home/sensor", 0), ("topic/test", 0)], + ] + + +@pytest.mark.parametrize("mqtt_config_entry_data", [ENTRY_DEFAULT_BIRTH_MESSAGE]) +@patch("homeassistant.components.mqtt.client.MAX_SUBSCRIBES_PER_CALL", 2) +@patch("homeassistant.components.mqtt.client.MAX_UNSUBSCRIBES_PER_CALL", 2) +async def test_mqtt_subscribes_and_unsubscribes_in_chunks( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + record_calls: MessageCallbackType, +) -> None: + """Test chunked client subscriptions.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + + mqtt_client_mock.subscribe.reset_mock() + unsub_tasks: list[CALLBACK_TYPE] = [] + mock_debouncer.clear() + unsub_tasks.append(await mqtt.async_subscribe(hass, "topic/test1", record_calls)) + unsub_tasks.append(await mqtt.async_subscribe(hass, "home/sensor1", record_calls)) + unsub_tasks.append(await mqtt.async_subscribe(hass, "topic/test2", record_calls)) + unsub_tasks.append(await mqtt.async_subscribe(hass, "home/sensor2", record_calls)) + # Make sure the debouncer finishes + await mock_debouncer.wait() + + assert mqtt_client_mock.subscribe.call_count == 2 + # Assert we have a 2 subscription calls with both 2 subscriptions + assert len(mqtt_client_mock.subscribe.mock_calls[0][1][0]) == 2 + assert len(mqtt_client_mock.subscribe.mock_calls[1][1][0]) == 2 + + # Unsubscribe all topics + mock_debouncer.clear() + for task in unsub_tasks: + task() + # Make sure the debouncer finishes + await mock_debouncer.wait() + + assert mqtt_client_mock.unsubscribe.call_count == 2 + # Assert we have a 2 unsubscribe calls with both 2 topic + assert len(mqtt_client_mock.unsubscribe.mock_calls[0][1][0]) == 2 + assert len(mqtt_client_mock.unsubscribe.mock_calls[1][1][0]) == 2 + + +async def test_auto_reconnect( + hass: HomeAssistant, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test reconnection is automatically done.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + assert mqtt_client_mock.connect.call_count == 1 + mqtt_client_mock.reconnect.reset_mock() + + mqtt_client_mock.disconnect() + mqtt_client_mock.on_disconnect(None, None, 0) + await hass.async_block_till_done() + + mqtt_client_mock.reconnect.side_effect = OSError("foo") + async_fire_time_changed( + hass, utcnow() + timedelta(seconds=RECONNECT_INTERVAL_SECONDS) + ) + await hass.async_block_till_done() + assert len(mqtt_client_mock.reconnect.mock_calls) == 1 + assert "Error re-connecting to MQTT server due to exception: foo" in caplog.text + + mqtt_client_mock.reconnect.side_effect = None + async_fire_time_changed( + hass, utcnow() + timedelta(seconds=RECONNECT_INTERVAL_SECONDS) + ) + await hass.async_block_till_done() + assert len(mqtt_client_mock.reconnect.mock_calls) == 2 + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + + mqtt_client_mock.disconnect() + mqtt_client_mock.on_disconnect(None, None, 0) + await hass.async_block_till_done() + + async_fire_time_changed( + hass, utcnow() + timedelta(seconds=RECONNECT_INTERVAL_SECONDS) + ) + await hass.async_block_till_done() + # Should not reconnect after stop + assert len(mqtt_client_mock.reconnect.mock_calls) == 2 + + +async def test_server_sock_connect_and_disconnect( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test handling the socket connected and disconnected.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + assert mqtt_client_mock.connect.call_count == 1 + + mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS + + client, server = socket.socketpair( + family=socket.AF_UNIX, type=socket.SOCK_STREAM, proto=0 + ) + client.setblocking(False) + server.setblocking(False) + mqtt_client_mock.on_socket_open(mqtt_client_mock, None, client) + mqtt_client_mock.on_socket_register_write(mqtt_client_mock, None, client) + await hass.async_block_till_done() + + server.close() # mock the server closing the connection on us + + mock_debouncer.clear() + unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) + await mock_debouncer.wait() + + mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_CONN_LOST + mqtt_client_mock.on_socket_unregister_write(mqtt_client_mock, None, client) + mqtt_client_mock.on_socket_close(mqtt_client_mock, None, client) + mqtt_client_mock.on_disconnect(mqtt_client_mock, None, client) + await hass.async_block_till_done() + mock_debouncer.clear() + unsub() + await hass.async_block_till_done() + assert not mock_debouncer.is_set() + + # Should have failed + assert len(recorded_calls) == 0 + + +async def test_server_sock_buffer_size( + hass: HomeAssistant, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test handling the socket buffer size fails.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + assert mqtt_client_mock.connect.call_count == 1 + + mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS + + client, server = socket.socketpair( + family=socket.AF_UNIX, type=socket.SOCK_STREAM, proto=0 + ) + client.setblocking(False) + server.setblocking(False) + with patch.object(client, "setsockopt", side_effect=OSError("foo")): + mqtt_client_mock.on_socket_open(mqtt_client_mock, None, client) + mqtt_client_mock.on_socket_register_write(mqtt_client_mock, None, client) + await hass.async_block_till_done() + assert "Unable to increase the socket buffer size" in caplog.text + + +async def test_server_sock_buffer_size_with_websocket( + hass: HomeAssistant, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test handling the socket buffer size fails.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + assert mqtt_client_mock.connect.call_count == 1 + + mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS + + client, server = socket.socketpair( + family=socket.AF_UNIX, type=socket.SOCK_STREAM, proto=0 + ) + client.setblocking(False) + server.setblocking(False) + + class FakeWebsocket(paho_mqtt.WebsocketWrapper): + def _do_handshake(self, *args, **kwargs): + pass + + wrapped_socket = FakeWebsocket(client, "127.0.01", 1, False, "/", None) + + with patch.object(client, "setsockopt", side_effect=OSError("foo")): + mqtt_client_mock.on_socket_open(mqtt_client_mock, None, wrapped_socket) + mqtt_client_mock.on_socket_register_write( + mqtt_client_mock, None, wrapped_socket + ) + await hass.async_block_till_done() + assert "Unable to increase the socket buffer size" in caplog.text + + +async def test_client_sock_failure_after_connect( + hass: HomeAssistant, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test handling the socket connected and disconnected.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + assert mqtt_client_mock.connect.call_count == 1 + + mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS + + client, server = socket.socketpair( + family=socket.AF_UNIX, type=socket.SOCK_STREAM, proto=0 + ) + client.setblocking(False) + server.setblocking(False) + mqtt_client_mock.on_socket_open(mqtt_client_mock, None, client) + mqtt_client_mock.on_socket_register_writer(mqtt_client_mock, None, client) + await hass.async_block_till_done() + + mqtt_client_mock.loop_write.side_effect = OSError("foo") + client.close() # close the client socket out from under the client + + assert mqtt_client_mock.connect.call_count == 1 + unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=5)) + await hass.async_block_till_done() + + unsub() + # Should have failed + assert len(recorded_calls) == 0 + + +async def test_loop_write_failure( + hass: HomeAssistant, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test handling the socket connected and disconnected.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + assert mqtt_client_mock.connect.call_count == 1 + + mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS + + client, server = socket.socketpair( + family=socket.AF_UNIX, type=socket.SOCK_STREAM, proto=0 + ) + client.setblocking(False) + server.setblocking(False) + mqtt_client_mock.on_socket_open(mqtt_client_mock, None, client) + mqtt_client_mock.on_socket_register_write(mqtt_client_mock, None, client) + mqtt_client_mock.loop_write.return_value = paho_mqtt.MQTT_ERR_CONN_LOST + mqtt_client_mock.loop_read.return_value = paho_mqtt.MQTT_ERR_CONN_LOST + + # Fill up the outgoing buffer to ensure that loop_write + # and loop_read are called that next time control is + # returned to the event loop + try: + for _ in range(1000): + server.send(b"long" * 100) + except BlockingIOError: + pass + + server.close() + # Once for the reader callback + await hass.async_block_till_done() + # Another for the writer callback + await hass.async_block_till_done() + # Final for the disconnect callback + await hass.async_block_till_done() + + assert "Disconnected from MQTT server test-broker:1883" in caplog.text diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index bcadf4a6506..403f7974878 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -1,25 +1,20 @@ -"""The tests for the MQTT component.""" +"""The tests for the MQTT component setup and helpers.""" import asyncio from copy import deepcopy from datetime import datetime, timedelta from functools import partial import json -import socket -import ssl import time from typing import Any, TypedDict -from unittest.mock import ANY, MagicMock, Mock, call, mock_open, patch +from unittest.mock import ANY, MagicMock, Mock, mock_open, patch -import certifi from freezegun.api import FrozenDateTimeFactory -import paho.mqtt.client as paho_mqtt import pytest import voluptuous as vol from homeassistant.components import mqtt from homeassistant.components.mqtt import debug_info -from homeassistant.components.mqtt.client import RECONNECT_INTERVAL_SECONDS from homeassistant.components.mqtt.models import ( MessageCallbackType, MqttCommandTemplateException, @@ -31,16 +26,12 @@ from homeassistant.components.sensor import SensorDeviceClass from homeassistant.config_entries import ConfigEntryDisabler, ConfigEntryState from homeassistant.const import ( ATTR_ASSUMED_STATE, - CONF_PROTOCOL, - EVENT_HOMEASSISTANT_STARTED, - EVENT_HOMEASSISTANT_STOP, SERVICE_RELOAD, STATE_UNAVAILABLE, STATE_UNKNOWN, - UnitOfTemperature, ) import homeassistant.core as ha -from homeassistant.core import CALLBACK_TYPE, CoreState, HomeAssistant, callback +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import device_registry as dr, entity_registry as er, template from homeassistant.helpers.entity import Entity @@ -50,9 +41,6 @@ from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util from homeassistant.util.dt import utcnow -from .conftest import ENTRY_DEFAULT_BIRTH_MESSAGE -from .test_common import help_all_subscribe_calls - from tests.common import ( MockConfigEntry, MockEntity, @@ -63,7 +51,6 @@ from tests.common import ( ) from tests.components.sensor.common import MockSensor from tests.typing import ( - MqttMockHAClient, MqttMockHAClientGenerator, MqttMockPahoClient, WebSocketGenerator, @@ -95,205 +82,6 @@ def mock_storage(hass_storage: dict[str, Any]) -> None: """Autouse hass_storage for the TestCase tests.""" -def help_assert_message( - msg: ReceiveMessage, - topic: str | None = None, - payload: str | None = None, - qos: int | None = None, - retain: bool | None = None, -) -> bool: - """Return True if all of the given attributes match with the message.""" - match: bool = True - if topic is not None: - match &= msg.topic == topic - if payload is not None: - match &= msg.payload == payload - if qos is not None: - match &= msg.qos == qos - if retain is not None: - match &= msg.retain == retain - return match - - -async def test_mqtt_connects_on_home_assistant_mqtt_setup( - hass: HomeAssistant, setup_with_birth_msg_client_mock: MqttMockPahoClient -) -> None: - """Test if client is connected after mqtt init on bootstrap.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - assert mqtt_client_mock.connect.call_count == 1 - - -async def test_mqtt_does_not_disconnect_on_home_assistant_stop( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, -) -> None: - """Test if client is not disconnected on HA stop.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - hass.bus.fire(EVENT_HOMEASSISTANT_STOP) - await mock_debouncer.wait() - assert mqtt_client_mock.disconnect.call_count == 0 - - -async def test_mqtt_await_ack_at_disconnect(hass: HomeAssistant) -> None: - """Test if ACK is awaited correctly when disconnecting.""" - - class FakeInfo: - """Returns a simulated client publish response.""" - - mid = 100 - rc = 0 - - with patch( - "homeassistant.components.mqtt.async_client.AsyncMQTTClient" - ) as mock_client: - mqtt_client = mock_client.return_value - mqtt_client.connect = MagicMock( - return_value=0, - side_effect=lambda *args, **kwargs: hass.loop.call_soon_threadsafe( - mqtt_client.on_connect, mqtt_client, None, 0, 0, 0 - ), - ) - mqtt_client.publish = MagicMock(return_value=FakeInfo()) - entry = MockConfigEntry( - domain=mqtt.DOMAIN, - data={ - "certificate": "auto", - mqtt.CONF_BROKER: "test-broker", - mqtt.CONF_DISCOVERY: False, - }, - ) - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) - - mqtt_client = mock_client.return_value - - # publish from MQTT client without awaiting - hass.async_create_task( - mqtt.async_publish(hass, "test-topic", "some-payload", 0, False) - ) - await asyncio.sleep(0) - # Simulate late ACK callback from client with mid 100 - mqtt_client.on_publish(0, 0, 100) - # disconnect the MQTT client - await hass.async_stop() - await hass.async_block_till_done() - # assert the payload was sent through the client - assert mqtt_client.publish.called - assert mqtt_client.publish.call_args[0] == ( - "test-topic", - "some-payload", - 0, - False, - ) - await hass.async_block_till_done(wait_background_tasks=True) - - -@pytest.mark.parametrize("mqtt_config_entry_data", [ENTRY_DEFAULT_BIRTH_MESSAGE]) -async def test_publish( - hass: HomeAssistant, setup_with_birth_msg_client_mock: MqttMockPahoClient -) -> None: - """Test the publish function.""" - publish_mock: MagicMock = setup_with_birth_msg_client_mock.publish - await mqtt.async_publish(hass, "test-topic", "test-payload") - await hass.async_block_till_done() - assert publish_mock.called - assert publish_mock.call_args[0] == ( - "test-topic", - "test-payload", - 0, - False, - ) - publish_mock.reset_mock() - - await mqtt.async_publish(hass, "test-topic", "test-payload", 2, True) - await hass.async_block_till_done() - assert publish_mock.called - assert publish_mock.call_args[0] == ( - "test-topic", - "test-payload", - 2, - True, - ) - publish_mock.reset_mock() - - mqtt.publish(hass, "test-topic2", "test-payload2") - await hass.async_block_till_done() - assert publish_mock.called - assert publish_mock.call_args[0] == ( - "test-topic2", - "test-payload2", - 0, - False, - ) - publish_mock.reset_mock() - - mqtt.publish(hass, "test-topic2", "test-payload2", 2, True) - await hass.async_block_till_done() - assert publish_mock.called - assert publish_mock.call_args[0] == ( - "test-topic2", - "test-payload2", - 2, - True, - ) - publish_mock.reset_mock() - - # test binary pass-through - mqtt.publish( - hass, - "test-topic3", - b"\xde\xad\xbe\xef", - 0, - False, - ) - await hass.async_block_till_done() - assert publish_mock.called - assert publish_mock.call_args[0] == ( - "test-topic3", - b"\xde\xad\xbe\xef", - 0, - False, - ) - publish_mock.reset_mock() - - # test null payload - mqtt.publish( - hass, - "test-topic3", - None, - 0, - False, - ) - await hass.async_block_till_done() - assert publish_mock.called - assert publish_mock.call_args[0] == ( - "test-topic3", - None, - 0, - False, - ) - - publish_mock.reset_mock() - - -async def test_convert_outgoing_payload(hass: HomeAssistant) -> None: - """Test the converting of outgoing MQTT payloads without template.""" - command_template = mqtt.MqttCommandTemplate(None, hass=hass) - assert command_template.async_render(b"\xde\xad\xbe\xef") == b"\xde\xad\xbe\xef" - - assert ( - command_template.async_render("b'\\xde\\xad\\xbe\\xef'") - == "b'\\xde\\xad\\xbe\\xef'" - ) - - assert command_template.async_render(1234) == 1234 - - assert command_template.async_render(1234.56) == 1234.56 - - assert command_template.async_render(None) is None - - async def test_command_template_value(hass: HomeAssistant) -> None: """Test the rendering of MQTT command template.""" @@ -983,893 +771,6 @@ async def test_receiving_message_with_non_utf8_topic_gets_logged( ) -async def test_all_subscriptions_run_when_decode_fails( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test all other subscriptions still run when decode fails for one.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "test-topic", record_calls, encoding="ascii") - await mqtt.async_subscribe(hass, "test-topic", record_calls) - - async_fire_mqtt_message(hass, "test-topic", UnitOfTemperature.CELSIUS) - - await hass.async_block_till_done() - assert len(recorded_calls) == 1 - - -async def test_subscribe_topic( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of a topic.""" - await mqtt_mock_entry() - unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) - - async_fire_mqtt_message(hass, "test-topic", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 1 - assert recorded_calls[0].topic == "test-topic" - assert recorded_calls[0].payload == "test-payload" - - unsub() - - async_fire_mqtt_message(hass, "test-topic", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 1 - - # Cannot unsubscribe twice - with pytest.raises(HomeAssistantError): - unsub() - - -@pytest.mark.usefixtures("mqtt_mock_entry") -async def test_subscribe_topic_not_initialize( - hass: HomeAssistant, record_calls: MessageCallbackType -) -> None: - """Test the subscription of a topic when MQTT was not initialized.""" - with pytest.raises( - HomeAssistantError, match=r".*make sure MQTT is set up correctly" - ): - await mqtt.async_subscribe(hass, "test-topic", record_calls) - - -async def test_subscribe_mqtt_config_entry_disabled( - hass: HomeAssistant, mqtt_mock: MqttMockHAClient, record_calls: MessageCallbackType -) -> None: - """Test the subscription of a topic when MQTT config entry is disabled.""" - mqtt_mock.connected = True - - mqtt_config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] - assert mqtt_config_entry.state is ConfigEntryState.LOADED - - assert await hass.config_entries.async_unload(mqtt_config_entry.entry_id) - assert mqtt_config_entry.state is ConfigEntryState.NOT_LOADED - - await hass.config_entries.async_set_disabled_by( - mqtt_config_entry.entry_id, ConfigEntryDisabler.USER - ) - mqtt_mock.connected = False - - with pytest.raises(HomeAssistantError, match=r".*MQTT is not enabled"): - await mqtt.async_subscribe(hass, "test-topic", record_calls) - - -async def test_subscribe_and_resubscribe( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test resubscribing within the debounce time.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - with ( - patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.4), - patch("homeassistant.components.mqtt.client.UNSUBSCRIBE_COOLDOWN", 0.4), - ): - mock_debouncer.clear() - unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) - # This unsub will be un-done with the following subscribe - # unsubscribe should not be called at the broker - unsub() - unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) - await mock_debouncer.wait() - mock_debouncer.clear() - - async_fire_mqtt_message(hass, "test-topic", "test-payload") - - assert len(recorded_calls) == 1 - assert recorded_calls[0].topic == "test-topic" - assert recorded_calls[0].payload == "test-payload" - # assert unsubscribe was not called - mqtt_client_mock.unsubscribe.assert_not_called() - - mock_debouncer.clear() - unsub() - - await mock_debouncer.wait() - mqtt_client_mock.unsubscribe.assert_called_once_with(["test-topic"]) - - -async def test_subscribe_topic_non_async( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of a topic using the non-async function.""" - await mqtt_mock_entry() - await mock_debouncer.wait() - mock_debouncer.clear() - unsub = await hass.async_add_executor_job( - mqtt.subscribe, hass, "test-topic", record_calls - ) - await mock_debouncer.wait() - - async_fire_mqtt_message(hass, "test-topic", "test-payload") - - assert len(recorded_calls) == 1 - assert recorded_calls[0].topic == "test-topic" - assert recorded_calls[0].payload == "test-payload" - - mock_debouncer.clear() - await hass.async_add_executor_job(unsub) - await mock_debouncer.wait() - - async_fire_mqtt_message(hass, "test-topic", "test-payload") - - assert len(recorded_calls) == 1 - - -async def test_subscribe_bad_topic( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of a topic.""" - await mqtt_mock_entry() - with pytest.raises(HomeAssistantError): - await mqtt.async_subscribe(hass, 55, record_calls) # type: ignore[arg-type] - - -async def test_subscribe_topic_not_match( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test if subscribed topic is not a match.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "test-topic", record_calls) - - async_fire_mqtt_message(hass, "another-test-topic", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 0 - - -async def test_subscribe_topic_level_wildcard( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of wildcard topics.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "test-topic/+/on", record_calls) - - async_fire_mqtt_message(hass, "test-topic/bier/on", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 1 - assert recorded_calls[0].topic == "test-topic/bier/on" - assert recorded_calls[0].payload == "test-payload" - - -async def test_subscribe_topic_level_wildcard_no_subtree_match( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of wildcard topics.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "test-topic/+/on", record_calls) - - async_fire_mqtt_message(hass, "test-topic/bier", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 0 - - -async def test_subscribe_topic_level_wildcard_root_topic_no_subtree_match( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of wildcard topics.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "test-topic/#", record_calls) - - async_fire_mqtt_message(hass, "test-topic-123", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 0 - - -async def test_subscribe_topic_subtree_wildcard_subtree_topic( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of wildcard topics.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "test-topic/#", record_calls) - - async_fire_mqtt_message(hass, "test-topic/bier/on", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 1 - assert recorded_calls[0].topic == "test-topic/bier/on" - assert recorded_calls[0].payload == "test-payload" - - -async def test_subscribe_topic_subtree_wildcard_root_topic( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of wildcard topics.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "test-topic/#", record_calls) - - async_fire_mqtt_message(hass, "test-topic", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 1 - assert recorded_calls[0].topic == "test-topic" - assert recorded_calls[0].payload == "test-payload" - - -async def test_subscribe_topic_subtree_wildcard_no_match( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of wildcard topics.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "test-topic/#", record_calls) - - async_fire_mqtt_message(hass, "another-test-topic", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 0 - - -async def test_subscribe_topic_level_wildcard_and_wildcard_root_topic( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of wildcard topics.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "+/test-topic/#", record_calls) - - async_fire_mqtt_message(hass, "hi/test-topic", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 1 - assert recorded_calls[0].topic == "hi/test-topic" - assert recorded_calls[0].payload == "test-payload" - - -async def test_subscribe_topic_level_wildcard_and_wildcard_subtree_topic( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of wildcard topics.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "+/test-topic/#", record_calls) - - async_fire_mqtt_message(hass, "hi/test-topic/here-iam", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 1 - assert recorded_calls[0].topic == "hi/test-topic/here-iam" - assert recorded_calls[0].payload == "test-payload" - - -async def test_subscribe_topic_level_wildcard_and_wildcard_level_no_match( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of wildcard topics.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "+/test-topic/#", record_calls) - - async_fire_mqtt_message(hass, "hi/here-iam/test-topic", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 0 - - -async def test_subscribe_topic_level_wildcard_and_wildcard_no_match( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of wildcard topics.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "+/test-topic/#", record_calls) - - async_fire_mqtt_message(hass, "hi/another-test-topic", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 0 - - -async def test_subscribe_topic_sys_root( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of $ root topics.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "$test-topic/subtree/on", record_calls) - - async_fire_mqtt_message(hass, "$test-topic/subtree/on", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 1 - assert recorded_calls[0].topic == "$test-topic/subtree/on" - assert recorded_calls[0].payload == "test-payload" - - -async def test_subscribe_topic_sys_root_and_wildcard_topic( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of $ root and wildcard topics.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "$test-topic/#", record_calls) - - async_fire_mqtt_message(hass, "$test-topic/some-topic", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 1 - assert recorded_calls[0].topic == "$test-topic/some-topic" - assert recorded_calls[0].payload == "test-payload" - - -async def test_subscribe_topic_sys_root_and_wildcard_subtree_topic( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of $ root and wildcard subtree topics.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "$test-topic/subtree/#", record_calls) - - async_fire_mqtt_message(hass, "$test-topic/subtree/some-topic", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 1 - assert recorded_calls[0].topic == "$test-topic/subtree/some-topic" - assert recorded_calls[0].payload == "test-payload" - - -async def test_subscribe_special_characters( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription to topics with special characters.""" - await mqtt_mock_entry() - topic = "/test-topic/$(.)[^]{-}" - payload = "p4y.l[]a|> ?" - - await mqtt.async_subscribe(hass, topic, record_calls) - - async_fire_mqtt_message(hass, topic, payload) - await hass.async_block_till_done() - assert len(recorded_calls) == 1 - assert recorded_calls[0].topic == topic - assert recorded_calls[0].payload == payload - - -async def test_subscribe_same_topic( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, -) -> None: - """Test subscribing to same topic twice and simulate retained messages. - - When subscribing to the same topic again, SUBSCRIBE must be sent to the broker again - for it to resend any retained messages. - """ - mqtt_client_mock = setup_with_birth_msg_client_mock - calls_a: list[ReceiveMessage] = [] - calls_b: list[ReceiveMessage] = [] - - @callback - def _callback_a(msg: ReceiveMessage) -> None: - calls_a.append(msg) - - @callback - def _callback_b(msg: ReceiveMessage) -> None: - calls_b.append(msg) - - mqtt_client_mock.reset_mock() - mock_debouncer.clear() - await mqtt.async_subscribe(hass, "test/state", _callback_a, qos=0) - # Simulate a non retained message after the first subscription - async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=False) - await mock_debouncer.wait() - assert len(calls_a) == 1 - mqtt_client_mock.subscribe.assert_called() - calls_a = [] - mqtt_client_mock.reset_mock() - - await hass.async_block_till_done() - mock_debouncer.clear() - await mqtt.async_subscribe(hass, "test/state", _callback_b, qos=1) - # Simulate an other non retained message after the second subscription - async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=False) - await mock_debouncer.wait() - # Both subscriptions should receive updates - assert len(calls_a) == 1 - assert len(calls_b) == 1 - mqtt_client_mock.subscribe.assert_called() - - -async def test_replaying_payload_same_topic( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, -) -> None: - """Test replaying retained messages. - - When subscribing to the same topic again, SUBSCRIBE must be sent to the broker again - for it to resend any retained messages for new subscriptions. - Retained messages must only be replayed for new subscriptions, except - when the MQTT client is reconnecting. - """ - mqtt_client_mock = setup_with_birth_msg_client_mock - calls_a: list[ReceiveMessage] = [] - calls_b: list[ReceiveMessage] = [] - - @callback - def _callback_a(msg: ReceiveMessage) -> None: - calls_a.append(msg) - - @callback - def _callback_b(msg: ReceiveMessage) -> None: - calls_b.append(msg) - - mqtt_client_mock.reset_mock() - mock_debouncer.clear() - await mqtt.async_subscribe(hass, "test/state", _callback_a) - await mock_debouncer.wait() - async_fire_mqtt_message( - hass, "test/state", "online", qos=0, retain=True - ) # Simulate a (retained) message played back - assert len(calls_a) == 1 - mqtt_client_mock.subscribe.assert_called() - calls_a = [] - mqtt_client_mock.reset_mock() - - mock_debouncer.clear() - await mqtt.async_subscribe(hass, "test/state", _callback_b) - await mock_debouncer.wait() - - # Simulate edge case where non retained message was received - # after subscription at HA but before the debouncer delay was passed. - # The message without retain flag directly after a subscription should - # be processed by both subscriptions. - async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=False) - - # Simulate a (retained) message played back on new subscriptions - async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=True) - - # The current subscription only received the message without retain flag - assert len(calls_a) == 1 - assert help_assert_message(calls_a[0], "test/state", "online", qos=0, retain=False) - # The retained message playback should only be processed by the new subscription. - # The existing subscription already got the latest update, hence the existing - # subscription should not receive the replayed (retained) message. - # Messages without retain flag are received on both subscriptions. - assert len(calls_b) == 2 - assert help_assert_message(calls_b[0], "test/state", "online", qos=0, retain=False) - assert help_assert_message(calls_b[1], "test/state", "online", qos=0, retain=True) - mqtt_client_mock.subscribe.assert_called() - - calls_a = [] - calls_b = [] - mqtt_client_mock.reset_mock() - - # Simulate new message played back on new subscriptions - # After connecting the retain flag will not be set, even if the - # payload published was retained, we cannot see that - async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=False) - assert len(calls_a) == 1 - assert help_assert_message(calls_a[0], "test/state", "online", qos=0, retain=False) - assert len(calls_b) == 1 - assert help_assert_message(calls_b[0], "test/state", "online", qos=0, retain=False) - - # Now simulate the broker was disconnected shortly - calls_a = [] - calls_b = [] - mqtt_client_mock.reset_mock() - mqtt_client_mock.on_disconnect(None, None, 0) - - mock_debouncer.clear() - mqtt_client_mock.on_connect(None, None, None, 0) - await mock_debouncer.wait() - mqtt_client_mock.subscribe.assert_called() - # Simulate a (retained) message played back after reconnecting - async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=True) - # Both subscriptions now should replay the retained message - assert len(calls_a) == 1 - assert help_assert_message(calls_a[0], "test/state", "online", qos=0, retain=True) - assert len(calls_b) == 1 - assert help_assert_message(calls_b[0], "test/state", "online", qos=0, retain=True) - - -async def test_replaying_payload_after_resubscribing( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, -) -> None: - """Test replaying and filtering retained messages after resubscribing. - - When subscribing to the same topic again, SUBSCRIBE must be sent to the broker again - for it to resend any retained messages for new subscriptions. - Retained messages must only be replayed for new subscriptions, except - when the MQTT client is reconnection. - """ - mqtt_client_mock = setup_with_birth_msg_client_mock - calls_a: list[ReceiveMessage] = [] - - @callback - def _callback_a(msg: ReceiveMessage) -> None: - calls_a.append(msg) - - mqtt_client_mock.reset_mock() - mock_debouncer.clear() - unsub = await mqtt.async_subscribe(hass, "test/state", _callback_a) - await mock_debouncer.wait() - mqtt_client_mock.subscribe.assert_called() - - # Simulate a (retained) message played back - async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=True) - assert help_assert_message(calls_a[0], "test/state", "online", qos=0, retain=True) - calls_a.clear() - - # Test we get updates - async_fire_mqtt_message(hass, "test/state", "offline", qos=0, retain=False) - assert help_assert_message(calls_a[0], "test/state", "offline", qos=0, retain=False) - calls_a.clear() - - # Test we filter new retained updates - async_fire_mqtt_message(hass, "test/state", "offline", qos=0, retain=True) - await hass.async_block_till_done() - assert len(calls_a) == 0 - - # Unsubscribe an resubscribe again - mock_debouncer.clear() - unsub() - unsub = await mqtt.async_subscribe(hass, "test/state", _callback_a) - await mock_debouncer.wait() - mqtt_client_mock.subscribe.assert_called() - - # Simulate we can receive a (retained) played back message again - async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=True) - assert help_assert_message(calls_a[0], "test/state", "online", qos=0, retain=True) - - -async def test_replaying_payload_wildcard_topic( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, -) -> None: - """Test replaying retained messages. - - When we have multiple subscriptions to the same wildcard topic, - SUBSCRIBE must be sent to the broker again - for it to resend any retained messages for new subscriptions. - Retained messages should only be replayed for new subscriptions, except - when the MQTT client is reconnection. - """ - mqtt_client_mock = setup_with_birth_msg_client_mock - calls_a: list[ReceiveMessage] = [] - calls_b: list[ReceiveMessage] = [] - - @callback - def _callback_a(msg: ReceiveMessage) -> None: - calls_a.append(msg) - - @callback - def _callback_b(msg: ReceiveMessage) -> None: - calls_b.append(msg) - - mqtt_client_mock.reset_mock() - mock_debouncer.clear() - await mqtt.async_subscribe(hass, "test/#", _callback_a) - await mock_debouncer.wait() - # Simulate (retained) messages being played back on new subscriptions - async_fire_mqtt_message(hass, "test/state1", "new_value_1", qos=0, retain=True) - async_fire_mqtt_message(hass, "test/state2", "new_value_2", qos=0, retain=True) - assert len(calls_a) == 2 - mqtt_client_mock.subscribe.assert_called() - calls_a = [] - mqtt_client_mock.reset_mock() - - # resubscribe to the wild card topic again - mock_debouncer.clear() - await mqtt.async_subscribe(hass, "test/#", _callback_b) - await mock_debouncer.wait() - # Simulate (retained) messages being played back on new subscriptions - async_fire_mqtt_message(hass, "test/state1", "initial_value_1", qos=0, retain=True) - async_fire_mqtt_message(hass, "test/state2", "initial_value_2", qos=0, retain=True) - # The retained messages playback should only be processed for the new subscriptions - assert len(calls_a) == 0 - assert len(calls_b) == 2 - mqtt_client_mock.subscribe.assert_called() - - calls_a = [] - calls_b = [] - mqtt_client_mock.reset_mock() - - # Simulate new messages being received - async_fire_mqtt_message(hass, "test/state1", "update_value_1", qos=0, retain=False) - async_fire_mqtt_message(hass, "test/state2", "update_value_2", qos=0, retain=False) - assert len(calls_a) == 2 - assert len(calls_b) == 2 - - # Now simulate the broker was disconnected shortly - calls_a = [] - calls_b = [] - mqtt_client_mock.reset_mock() - mqtt_client_mock.on_disconnect(None, None, 0) - - mock_debouncer.clear() - mqtt_client_mock.on_connect(None, None, None, 0) - await mock_debouncer.wait() - - mqtt_client_mock.subscribe.assert_called() - # Simulate the (retained) messages are played back after reconnecting - # for all subscriptions - async_fire_mqtt_message(hass, "test/state1", "update_value_1", qos=0, retain=True) - async_fire_mqtt_message(hass, "test/state2", "update_value_2", qos=0, retain=True) - # Both subscriptions should replay - assert len(calls_a) == 2 - assert len(calls_b) == 2 - - -async def test_not_calling_unsubscribe_with_active_subscribers( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - record_calls: MessageCallbackType, -) -> None: - """Test not calling unsubscribe() when other subscribers are active.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - mqtt_client_mock.reset_mock() - mock_debouncer.clear() - unsub = await mqtt.async_subscribe(hass, "test/state", record_calls, 2) - await mqtt.async_subscribe(hass, "test/state", record_calls, 1) - await mock_debouncer.wait() - assert mqtt_client_mock.subscribe.called - - mock_debouncer.clear() - unsub() - await hass.async_block_till_done() - await hass.async_block_till_done(wait_background_tasks=True) - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown - assert not mqtt_client_mock.unsubscribe.called - assert not mock_debouncer.is_set() - - -async def test_not_calling_subscribe_when_unsubscribed_within_cooldown( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - mqtt_mock_entry: MqttMockHAClientGenerator, - record_calls: MessageCallbackType, -) -> None: - """Test not calling subscribe() when it is unsubscribed. - - Make sure subscriptions are cleared if unsubscribed before - the subscribe cool down period has ended. - """ - mqtt_mock = await mqtt_mock_entry() - mqtt_client_mock = mqtt_mock._mqttc - await mock_debouncer.wait() - - mock_debouncer.clear() - mqtt_client_mock.subscribe.reset_mock() - unsub = await mqtt.async_subscribe(hass, "test/state", record_calls) - unsub() - await mock_debouncer.wait() - # The debouncer executes without an pending subscribes - assert not mqtt_client_mock.subscribe.called - - -async def test_unsubscribe_race( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, -) -> None: - """Test not calling unsubscribe() when other subscribers are active.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - calls_a: list[ReceiveMessage] = [] - calls_b: list[ReceiveMessage] = [] - - @callback - def _callback_a(msg: ReceiveMessage) -> None: - calls_a.append(msg) - - @callback - def _callback_b(msg: ReceiveMessage) -> None: - calls_b.append(msg) - - mqtt_client_mock.reset_mock() - - mock_debouncer.clear() - unsub = await mqtt.async_subscribe(hass, "test/state", _callback_a) - unsub() - await mqtt.async_subscribe(hass, "test/state", _callback_b) - await mock_debouncer.wait() - - async_fire_mqtt_message(hass, "test/state", "online") - assert not calls_a - assert calls_b - - # We allow either calls [subscribe, unsubscribe, subscribe], [subscribe, subscribe] or - # when both subscriptions were combined [subscribe] - expected_calls_1 = [ - call.subscribe([("test/state", 0)]), - call.unsubscribe("test/state"), - call.subscribe([("test/state", 0)]), - ] - expected_calls_2 = [ - call.subscribe([("test/state", 0)]), - call.subscribe([("test/state", 0)]), - ] - expected_calls_3 = [ - call.subscribe([("test/state", 0)]), - ] - assert mqtt_client_mock.mock_calls in ( - expected_calls_1, - expected_calls_2, - expected_calls_3, - ) - - -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], -) -async def test_restore_subscriptions_on_reconnect( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - record_calls: MessageCallbackType, -) -> None: - """Test subscriptions are restored on reconnect.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - - mqtt_client_mock.reset_mock() - - mock_debouncer.clear() - await mqtt.async_subscribe(hass, "test/state", record_calls) - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown - await mock_debouncer.wait() - assert ("test/state", 0) in help_all_subscribe_calls(mqtt_client_mock) - - mqtt_client_mock.reset_mock() - mqtt_client_mock.on_disconnect(None, None, 0) - - mock_debouncer.clear() - mqtt_client_mock.on_connect(None, None, None, 0) - await mock_debouncer.wait() - assert ("test/state", 0) in help_all_subscribe_calls(mqtt_client_mock) - - -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], -) -async def test_restore_all_active_subscriptions_on_reconnect( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - record_calls: MessageCallbackType, -) -> None: - """Test active subscriptions are restored correctly on reconnect.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - mqtt_client_mock.reset_mock() - mock_debouncer.clear() - unsub = await mqtt.async_subscribe(hass, "test/state", record_calls, qos=2) - await mqtt.async_subscribe(hass, "test/state", record_calls, qos=1) - await mqtt.async_subscribe(hass, "test/state", record_calls, qos=0) - # cooldown - await mock_debouncer.wait() - - # the subscription with the highest QoS should survive - expected = [ - call([("test/state", 2)]), - ] - assert mqtt_client_mock.subscribe.mock_calls == expected - - unsub() - assert mqtt_client_mock.unsubscribe.call_count == 0 - - mqtt_client_mock.on_disconnect(None, None, 0) - - mock_debouncer.clear() - mqtt_client_mock.on_connect(None, None, None, 0) - # wait for cooldown - await mock_debouncer.wait() - - expected.append(call([("test/state", 1)])) - for expected_call in expected: - assert mqtt_client_mock.subscribe.hass_call(expected_call) - - -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], -) -async def test_subscribed_at_highest_qos( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - record_calls: MessageCallbackType, -) -> None: - """Test the highest qos as assigned when subscribing to the same topic.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - mqtt_client_mock.reset_mock() - mock_debouncer.clear() - await mqtt.async_subscribe(hass, "test/state", record_calls, qos=0) - await hass.async_block_till_done() - # cooldown - await mock_debouncer.wait() - assert ("test/state", 0) in help_all_subscribe_calls(mqtt_client_mock) - mqtt_client_mock.reset_mock() - - mock_debouncer.clear() - await mqtt.async_subscribe(hass, "test/state", record_calls, qos=1) - await mqtt.async_subscribe(hass, "test/state", record_calls, qos=2) - # cooldown - await mock_debouncer.wait() - - # the subscription with the highest QoS should survive - assert help_all_subscribe_calls(mqtt_client_mock) == [("test/state", 2)] - - @pytest.mark.usefixtures("mqtt_client_mock") async def test_reload_entry_with_restored_subscriptions( hass: HomeAssistant, @@ -1937,163 +838,6 @@ async def test_reload_entry_with_restored_subscriptions( assert recorded_calls[1].payload == "wild-card-payload3" -async def test_initial_setup_logs_error( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - mqtt_client_mock: MqttMockPahoClient, -) -> None: - """Test for setup failure if initial client connection fails.""" - entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) - entry.add_to_hass(hass) - mqtt_client_mock.connect.side_effect = MagicMock(return_value=1) - try: - assert await hass.config_entries.async_setup(entry.entry_id) - except HomeAssistantError: - assert True - assert "Failed to connect to MQTT server:" in caplog.text - - -async def test_logs_error_if_no_connect_broker( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - setup_with_birth_msg_client_mock: MqttMockPahoClient, -) -> None: - """Test for setup failure if connection to broker is missing.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - # test with rc = 3 -> broker unavailable - mqtt_client_mock.on_disconnect(Mock(), None, 0) - mqtt_client_mock.on_connect(Mock(), None, None, 3) - await hass.async_block_till_done() - assert ( - "Unable to connect to the MQTT broker: Connection Refused: broker unavailable." - in caplog.text - ) - - -@pytest.mark.parametrize("return_code", [4, 5]) -async def test_triggers_reauth_flow_if_auth_fails( - hass: HomeAssistant, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - return_code: int, -) -> None: - """Test re-auth is triggered if authentication is failing.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - # test with rc = 4 -> CONNACK_REFUSED_NOT_AUTHORIZED and 5 -> CONNACK_REFUSED_BAD_USERNAME_PASSWORD - mqtt_client_mock.on_disconnect(Mock(), None, 0) - mqtt_client_mock.on_connect(Mock(), None, None, return_code) - await hass.async_block_till_done() - flows = hass.config_entries.flow.async_progress() - assert len(flows) == 1 - assert flows[0]["context"]["source"] == "reauth" - - -@patch("homeassistant.components.mqtt.client.TIMEOUT_ACK", 0.3) -async def test_handle_mqtt_on_callback( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - setup_with_birth_msg_client_mock: MqttMockPahoClient, -) -> None: - """Test receiving an ACK callback before waiting for it.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - with patch.object(mqtt_client_mock, "get_mid", return_value=100): - # Simulate an ACK for mid == 100, this will call mqtt_mock._async_get_mid_future(mid) - mqtt_client_mock.on_publish(mqtt_client_mock, None, 100) - await hass.async_block_till_done() - # Make sure the ACK has been received - await hass.async_block_till_done() - # Now call publish without call back, this will call _async_async_wait_for_mid(msg_info.mid) - await mqtt.async_publish(hass, "no_callback/test-topic", "test-payload") - # Since the mid event was already set, we should not see any timeout warning in the log - await hass.async_block_till_done() - assert "No ACK from MQTT server" not in caplog.text - - -async def test_handle_mqtt_on_callback_after_timeout( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - mqtt_mock_entry: MqttMockHAClientGenerator, - mqtt_client_mock: MqttMockPahoClient, -) -> None: - """Test receiving an ACK after a timeout.""" - mqtt_mock = await mqtt_mock_entry() - # Simulate the mid future getting a timeout - mqtt_mock()._async_get_mid_future(101).set_exception(asyncio.TimeoutError) - # Simulate an ACK for mid == 101, being received after the timeout - mqtt_client_mock.on_publish(mqtt_client_mock, None, 101) - await hass.async_block_till_done() - assert "No ACK from MQTT server" not in caplog.text - assert "InvalidStateError" not in caplog.text - - -async def test_publish_error( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture -) -> None: - """Test publish error.""" - entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) - entry.add_to_hass(hass) - - # simulate an Out of memory error - with patch( - "homeassistant.components.mqtt.async_client.AsyncMQTTClient" - ) as mock_client: - mock_client().connect = lambda *args: 1 - mock_client().publish().rc = 1 - assert await hass.config_entries.async_setup(entry.entry_id) - with pytest.raises(HomeAssistantError): - await mqtt.async_publish( - hass, "some-topic", b"test-payload", qos=0, retain=False, encoding=None - ) - assert "Failed to connect to MQTT server: Out of memory." in caplog.text - - -async def test_subscribe_error( - hass: HomeAssistant, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - record_calls: MessageCallbackType, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test publish error.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - mqtt_client_mock.reset_mock() - # simulate client is not connected error before subscribing - mqtt_client_mock.subscribe.side_effect = lambda *args: (4, None) - await mqtt.async_subscribe(hass, "some-topic", record_calls) - while mqtt_client_mock.subscribe.call_count == 0: - await hass.async_block_till_done() - await hass.async_block_till_done() - assert ( - "Error talking to MQTT: The client is not currently connected." in caplog.text - ) - - -async def test_handle_message_callback( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, -) -> None: - """Test for handling an incoming message callback.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - callbacks = [] - - @callback - def _callback(args) -> None: - callbacks.append(args) - - msg = ReceiveMessage( - "some-topic", b"test-payload", 1, False, "some-topic", datetime.now() - ) - mock_debouncer.clear() - await mqtt.async_subscribe(hass, "some-topic", _callback) - await mock_debouncer.wait() - mqtt_client_mock.reset_mock() - mqtt_client_mock.on_message(None, None, msg) - - assert len(callbacks) == 1 - assert callbacks[0].topic == "some-topic" - assert callbacks[0].qos == 1 - assert callbacks[0].payload == "test-payload" - - @pytest.mark.parametrize( "hass_config", [ @@ -2128,491 +872,6 @@ async def test_setup_manual_mqtt_with_invalid_config( assert "required key not provided" in caplog.text -@pytest.mark.parametrize( - ("mqtt_config_entry_data", "protocol"), - [ - ( - { - mqtt.CONF_BROKER: "mock-broker", - CONF_PROTOCOL: "3.1", - }, - 3, - ), - ( - { - mqtt.CONF_BROKER: "mock-broker", - CONF_PROTOCOL: "3.1.1", - }, - 4, - ), - ( - { - mqtt.CONF_BROKER: "mock-broker", - CONF_PROTOCOL: "5", - }, - 5, - ), - ], -) -async def test_setup_mqtt_client_protocol( - mqtt_mock_entry: MqttMockHAClientGenerator, protocol: int -) -> None: - """Test MQTT client protocol setup.""" - with patch( - "homeassistant.components.mqtt.async_client.AsyncMQTTClient" - ) as mock_client: - await mqtt_mock_entry() - - # check if protocol setup was correctly - assert mock_client.call_args[1]["protocol"] == protocol - - -@patch("homeassistant.components.mqtt.client.TIMEOUT_ACK", 0.2) -async def test_handle_mqtt_timeout_on_callback( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_debouncer: asyncio.Event -) -> None: - """Test publish without receiving an ACK callback.""" - mid = 0 - - class FakeInfo: - """Returns a simulated client publish response.""" - - mid = 102 - rc = 0 - - with patch( - "homeassistant.components.mqtt.async_client.AsyncMQTTClient" - ) as mock_client: - - def _mock_ack(topic: str, qos: int = 0) -> tuple[int, int]: - # Handle ACK for subscribe normally - nonlocal mid - mid += 1 - mock_client.on_subscribe(0, 0, mid) - return (0, mid) - - # We want to simulate the publish behaviour MQTT client - mock_client = mock_client.return_value - mock_client.publish.return_value = FakeInfo() - # Mock we get a mid and rc=0 - mock_client.subscribe.side_effect = _mock_ack - mock_client.unsubscribe.side_effect = _mock_ack - mock_client.connect = MagicMock( - return_value=0, - side_effect=lambda *args, **kwargs: hass.loop.call_soon_threadsafe( - mock_client.on_connect, mock_client, None, 0, 0, 0 - ), - ) - - entry = MockConfigEntry( - domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"} - ) - entry.add_to_hass(hass) - - # Set up the integration - mock_debouncer.clear() - assert await hass.config_entries.async_setup(entry.entry_id) - - # Now call we publish without simulating and ACK callback - await mqtt.async_publish(hass, "no_callback/test-topic", "test-payload") - await hass.async_block_till_done() - # There is no ACK so we should see a timeout in the log after publishing - assert len(mock_client.publish.mock_calls) == 1 - assert "No ACK from MQTT server" in caplog.text - # Ensure we stop lingering background tasks - await hass.config_entries.async_unload(entry.entry_id) - # Assert we did not have any completed subscribes, - # because the debouncer subscribe job failed to receive an ACK, - # and the time auto caused the debouncer job to fail. - assert not mock_debouncer.is_set() - - -async def test_setup_raises_config_entry_not_ready_if_no_connect_broker( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture -) -> None: - """Test for setup failure if connection to broker is missing.""" - entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) - entry.add_to_hass(hass) - - with patch( - "homeassistant.components.mqtt.async_client.AsyncMQTTClient" - ) as mock_client: - mock_client().connect = MagicMock(side_effect=OSError("Connection error")) - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - assert "Failed to connect to MQTT server due to exception:" in caplog.text - - -@pytest.mark.parametrize( - ("mqtt_config_entry_data", "insecure_param"), - [ - ({"broker": "test-broker", "certificate": "auto"}, "not set"), - ( - {"broker": "test-broker", "certificate": "auto", "tls_insecure": False}, - False, - ), - ({"broker": "test-broker", "certificate": "auto", "tls_insecure": True}, True), - ], -) -async def test_setup_uses_certificate_on_certificate_set_to_auto_and_insecure( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - insecure_param: bool | str, -) -> None: - """Test setup uses bundled certs when certificate is set to auto and insecure.""" - calls = [] - insecure_check = {"insecure": "not set"} - - def mock_tls_set( - certificate, certfile=None, keyfile=None, tls_version=None - ) -> None: - calls.append((certificate, certfile, keyfile, tls_version)) - - def mock_tls_insecure_set(insecure_param) -> None: - insecure_check["insecure"] = insecure_param - - with patch( - "homeassistant.components.mqtt.async_client.AsyncMQTTClient" - ) as mock_client: - mock_client().tls_set = mock_tls_set - mock_client().tls_insecure_set = mock_tls_insecure_set - await mqtt_mock_entry() - await hass.async_block_till_done() - - assert calls - - expected_certificate = certifi.where() - assert calls[0][0] == expected_certificate - - # test if insecure is set - assert insecure_check["insecure"] == insecure_param - - -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [ - { - mqtt.CONF_BROKER: "mock-broker", - mqtt.CONF_CERTIFICATE: "auto", - } - ], -) -async def test_tls_version( - hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, -) -> None: - """Test setup defaults for tls.""" - await mqtt_mock_entry() - await hass.async_block_till_done() - assert ( - mqtt_client_mock.tls_set.mock_calls[0][2]["tls_version"] - == ssl.PROTOCOL_TLS_CLIENT - ) - - -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [ - { - mqtt.CONF_BROKER: "mock-broker", - mqtt.CONF_BIRTH_MESSAGE: { - mqtt.ATTR_TOPIC: "birth", - mqtt.ATTR_PAYLOAD: "birth", - mqtt.ATTR_QOS: 0, - mqtt.ATTR_RETAIN: False, - }, - } - ], -) -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) -async def test_custom_birth_message( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - mqtt_config_entry_data: dict[str, Any], - mqtt_client_mock: MqttMockPahoClient, -) -> None: - """Test sending birth message.""" - - entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) - entry.add_to_hass(hass) - hass.config.components.add(mqtt.DOMAIN) - assert await hass.config_entries.async_setup(entry.entry_id) - mock_debouncer.clear() - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - # discovery cooldown - await mock_debouncer.wait() - # Wait for publish call to finish - await hass.async_block_till_done(wait_background_tasks=True) - mqtt_client_mock.publish.assert_called_with("birth", "birth", 0, False) - - -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [ENTRY_DEFAULT_BIRTH_MESSAGE], -) -async def test_default_birth_message( - hass: HomeAssistant, setup_with_birth_msg_client_mock: MqttMockPahoClient -) -> None: - """Test sending birth message.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - await hass.async_block_till_done(wait_background_tasks=True) - mqtt_client_mock.publish.assert_called_with( - "homeassistant/status", "online", 0, False - ) - - -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_BIRTH_MESSAGE: {}}], -) -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) -async def test_no_birth_message( - hass: HomeAssistant, - record_calls: MessageCallbackType, - mock_debouncer: asyncio.Event, - mqtt_config_entry_data: dict[str, Any], - mqtt_client_mock: MqttMockPahoClient, -) -> None: - """Test disabling birth message.""" - entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) - entry.add_to_hass(hass) - hass.config.components.add(mqtt.DOMAIN) - mock_debouncer.clear() - assert await hass.config_entries.async_setup(entry.entry_id) - # Wait for discovery cooldown - await mock_debouncer.wait() - # Ensure any publishing could have been processed - await hass.async_block_till_done(wait_background_tasks=True) - mqtt_client_mock.publish.assert_not_called() - - mqtt_client_mock.reset_mock() - mock_debouncer.clear() - await mqtt.async_subscribe(hass, "homeassistant/some-topic", record_calls) - # Wait for discovery cooldown - await mock_debouncer.wait() - mqtt_client_mock.subscribe.assert_called() - - -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [ENTRY_DEFAULT_BIRTH_MESSAGE], -) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.2) -async def test_delayed_birth_message( - hass: HomeAssistant, - mqtt_config_entry_data: dict[str, Any], - mqtt_client_mock: MqttMockPahoClient, -) -> None: - """Test sending birth message does not happen until Home Assistant starts.""" - hass.set_state(CoreState.starting) - await hass.async_block_till_done() - birth = asyncio.Event() - entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) - entry.add_to_hass(hass) - hass.config.components.add(mqtt.DOMAIN) - assert await hass.config_entries.async_setup(entry.entry_id) - - @callback - def wait_birth(msg: ReceiveMessage) -> None: - """Handle birth message.""" - birth.set() - - await mqtt.async_subscribe(hass, "homeassistant/status", wait_birth) - with pytest.raises(TimeoutError): - await asyncio.wait_for(birth.wait(), 0.05) - assert not mqtt_client_mock.publish.called - assert not birth.is_set() - - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - await birth.wait() - mqtt_client_mock.publish.assert_called_with( - "homeassistant/status", "online", 0, False - ) - - -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [ENTRY_DEFAULT_BIRTH_MESSAGE], -) -async def test_subscription_done_when_birth_message_is_sent( - setup_with_birth_msg_client_mock: MqttMockPahoClient, -) -> None: - """Test sending birth message until initial subscription has been completed.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - subscribe_calls = help_all_subscribe_calls(mqtt_client_mock) - assert ("homeassistant/+/+/config", 0) in subscribe_calls - assert ("homeassistant/+/+/+/config", 0) in subscribe_calls - mqtt_client_mock.publish.assert_called_with( - "homeassistant/status", "online", 0, False - ) - - -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [ - { - mqtt.CONF_BROKER: "mock-broker", - mqtt.CONF_WILL_MESSAGE: { - mqtt.ATTR_TOPIC: "death", - mqtt.ATTR_PAYLOAD: "death", - mqtt.ATTR_QOS: 0, - mqtt.ATTR_RETAIN: False, - }, - } - ], -) -async def test_custom_will_message( - hass: HomeAssistant, - mqtt_config_entry_data: dict[str, Any], - mqtt_client_mock: MqttMockPahoClient, -) -> None: - """Test will message.""" - entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) - entry.add_to_hass(hass) - hass.config.components.add(mqtt.DOMAIN) - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - mqtt_client_mock.will_set.assert_called_with( - topic="death", payload="death", qos=0, retain=False - ) - - -async def test_default_will_message( - setup_with_birth_msg_client_mock: MqttMockPahoClient, -) -> None: - """Test will message.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - mqtt_client_mock.will_set.assert_called_with( - topic="homeassistant/status", payload="offline", qos=0, retain=False - ) - - -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_WILL_MESSAGE: {}}], -) -async def test_no_will_message( - hass: HomeAssistant, - mqtt_config_entry_data: dict[str, Any], - mqtt_client_mock: MqttMockPahoClient, -) -> None: - """Test will message.""" - entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) - entry.add_to_hass(hass) - hass.config.components.add(mqtt.DOMAIN) - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - mqtt_client_mock.will_set.assert_not_called() - - -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [ENTRY_DEFAULT_BIRTH_MESSAGE | {mqtt.CONF_DISCOVERY: False}], -) -async def test_mqtt_subscribes_topics_on_connect( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - record_calls: MessageCallbackType, -) -> None: - """Test subscription to topic on connect.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - - mock_debouncer.clear() - await mqtt.async_subscribe(hass, "topic/test", record_calls) - await mqtt.async_subscribe(hass, "home/sensor", record_calls, 2) - await mqtt.async_subscribe(hass, "still/pending", record_calls) - await mqtt.async_subscribe(hass, "still/pending", record_calls, 1) - await mock_debouncer.wait() - - mqtt_client_mock.on_disconnect(Mock(), None, 0) - - mqtt_client_mock.reset_mock() - - mock_debouncer.clear() - mqtt_client_mock.on_connect(Mock(), None, 0, 0) - await mock_debouncer.wait() - - subscribe_calls = help_all_subscribe_calls(mqtt_client_mock) - assert ("topic/test", 0) in subscribe_calls - assert ("home/sensor", 2) in subscribe_calls - assert ("still/pending", 1) in subscribe_calls - - -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [ENTRY_DEFAULT_BIRTH_MESSAGE], -) -async def test_mqtt_subscribes_in_single_call( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - record_calls: MessageCallbackType, -) -> None: - """Test bundled client subscription to topic.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - mqtt_client_mock.subscribe.reset_mock() - mock_debouncer.clear() - await mqtt.async_subscribe(hass, "topic/test", record_calls) - await mqtt.async_subscribe(hass, "home/sensor", record_calls) - # Make sure the debouncer finishes - await mock_debouncer.wait() - - assert mqtt_client_mock.subscribe.call_count == 1 - # Assert we have a single subscription call with both subscriptions - assert mqtt_client_mock.subscribe.mock_calls[0][1][0] in [ - [("topic/test", 0), ("home/sensor", 0)], - [("home/sensor", 0), ("topic/test", 0)], - ] - - -@pytest.mark.parametrize("mqtt_config_entry_data", [ENTRY_DEFAULT_BIRTH_MESSAGE]) -@patch("homeassistant.components.mqtt.client.MAX_SUBSCRIBES_PER_CALL", 2) -@patch("homeassistant.components.mqtt.client.MAX_UNSUBSCRIBES_PER_CALL", 2) -async def test_mqtt_subscribes_and_unsubscribes_in_chunks( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - record_calls: MessageCallbackType, -) -> None: - """Test chunked client subscriptions.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - - mqtt_client_mock.subscribe.reset_mock() - unsub_tasks: list[CALLBACK_TYPE] = [] - mock_debouncer.clear() - unsub_tasks.append(await mqtt.async_subscribe(hass, "topic/test1", record_calls)) - unsub_tasks.append(await mqtt.async_subscribe(hass, "home/sensor1", record_calls)) - unsub_tasks.append(await mqtt.async_subscribe(hass, "topic/test2", record_calls)) - unsub_tasks.append(await mqtt.async_subscribe(hass, "home/sensor2", record_calls)) - # Make sure the debouncer finishes - await mock_debouncer.wait() - - assert mqtt_client_mock.subscribe.call_count == 2 - # Assert we have a 2 subscription calls with both 2 subscriptions - assert len(mqtt_client_mock.subscribe.mock_calls[0][1][0]) == 2 - assert len(mqtt_client_mock.subscribe.mock_calls[1][1][0]) == 2 - - # Unsubscribe all topics - mock_debouncer.clear() - for task in unsub_tasks: - task() - # Make sure the debouncer finishes - await mock_debouncer.wait() - - assert mqtt_client_mock.unsubscribe.call_count == 2 - # Assert we have a 2 unsubscribe calls with both 2 topic - assert len(mqtt_client_mock.unsubscribe.mock_calls[0][1][0]) == 2 - assert len(mqtt_client_mock.unsubscribe.mock_calls[1][1][0]) == 2 - - @pytest.mark.usefixtures("mqtt_client_mock") async def test_default_entry_setting_are_applied( hass: HomeAssistant, device_registry: dr.DeviceRegistry @@ -4106,221 +2365,6 @@ async def test_multi_platform_discovery( ) -async def test_auto_reconnect( - hass: HomeAssistant, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test reconnection is automatically done.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - assert mqtt_client_mock.connect.call_count == 1 - mqtt_client_mock.reconnect.reset_mock() - - mqtt_client_mock.disconnect() - mqtt_client_mock.on_disconnect(None, None, 0) - await hass.async_block_till_done() - - mqtt_client_mock.reconnect.side_effect = OSError("foo") - async_fire_time_changed( - hass, utcnow() + timedelta(seconds=RECONNECT_INTERVAL_SECONDS) - ) - await hass.async_block_till_done() - assert len(mqtt_client_mock.reconnect.mock_calls) == 1 - assert "Error re-connecting to MQTT server due to exception: foo" in caplog.text - - mqtt_client_mock.reconnect.side_effect = None - async_fire_time_changed( - hass, utcnow() + timedelta(seconds=RECONNECT_INTERVAL_SECONDS) - ) - await hass.async_block_till_done() - assert len(mqtt_client_mock.reconnect.mock_calls) == 2 - - hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) - - mqtt_client_mock.disconnect() - mqtt_client_mock.on_disconnect(None, None, 0) - await hass.async_block_till_done() - - async_fire_time_changed( - hass, utcnow() + timedelta(seconds=RECONNECT_INTERVAL_SECONDS) - ) - await hass.async_block_till_done() - # Should not reconnect after stop - assert len(mqtt_client_mock.reconnect.mock_calls) == 2 - - -async def test_server_sock_connect_and_disconnect( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test handling the socket connected and disconnected.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - assert mqtt_client_mock.connect.call_count == 1 - - mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS - - client, server = socket.socketpair( - family=socket.AF_UNIX, type=socket.SOCK_STREAM, proto=0 - ) - client.setblocking(False) - server.setblocking(False) - mqtt_client_mock.on_socket_open(mqtt_client_mock, None, client) - mqtt_client_mock.on_socket_register_write(mqtt_client_mock, None, client) - await hass.async_block_till_done() - - server.close() # mock the server closing the connection on us - - mock_debouncer.clear() - unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) - await mock_debouncer.wait() - - mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_CONN_LOST - mqtt_client_mock.on_socket_unregister_write(mqtt_client_mock, None, client) - mqtt_client_mock.on_socket_close(mqtt_client_mock, None, client) - mqtt_client_mock.on_disconnect(mqtt_client_mock, None, client) - await hass.async_block_till_done() - mock_debouncer.clear() - unsub() - await hass.async_block_till_done() - assert not mock_debouncer.is_set() - - # Should have failed - assert len(recorded_calls) == 0 - - -async def test_server_sock_buffer_size( - hass: HomeAssistant, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test handling the socket buffer size fails.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - assert mqtt_client_mock.connect.call_count == 1 - - mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS - - client, server = socket.socketpair( - family=socket.AF_UNIX, type=socket.SOCK_STREAM, proto=0 - ) - client.setblocking(False) - server.setblocking(False) - with patch.object(client, "setsockopt", side_effect=OSError("foo")): - mqtt_client_mock.on_socket_open(mqtt_client_mock, None, client) - mqtt_client_mock.on_socket_register_write(mqtt_client_mock, None, client) - await hass.async_block_till_done() - assert "Unable to increase the socket buffer size" in caplog.text - - -async def test_server_sock_buffer_size_with_websocket( - hass: HomeAssistant, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test handling the socket buffer size fails.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - assert mqtt_client_mock.connect.call_count == 1 - - mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS - - client, server = socket.socketpair( - family=socket.AF_UNIX, type=socket.SOCK_STREAM, proto=0 - ) - client.setblocking(False) - server.setblocking(False) - - class FakeWebsocket(paho_mqtt.WebsocketWrapper): - def _do_handshake(self, *args, **kwargs): - pass - - wrapped_socket = FakeWebsocket(client, "127.0.01", 1, False, "/", None) - - with patch.object(client, "setsockopt", side_effect=OSError("foo")): - mqtt_client_mock.on_socket_open(mqtt_client_mock, None, wrapped_socket) - mqtt_client_mock.on_socket_register_write( - mqtt_client_mock, None, wrapped_socket - ) - await hass.async_block_till_done() - assert "Unable to increase the socket buffer size" in caplog.text - - -async def test_client_sock_failure_after_connect( - hass: HomeAssistant, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test handling the socket connected and disconnected.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - assert mqtt_client_mock.connect.call_count == 1 - - mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS - - client, server = socket.socketpair( - family=socket.AF_UNIX, type=socket.SOCK_STREAM, proto=0 - ) - client.setblocking(False) - server.setblocking(False) - mqtt_client_mock.on_socket_open(mqtt_client_mock, None, client) - mqtt_client_mock.on_socket_register_writer(mqtt_client_mock, None, client) - await hass.async_block_till_done() - - mqtt_client_mock.loop_write.side_effect = OSError("foo") - client.close() # close the client socket out from under the client - - assert mqtt_client_mock.connect.call_count == 1 - unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) - async_fire_time_changed(hass, utcnow() + timedelta(seconds=5)) - await hass.async_block_till_done() - - unsub() - # Should have failed - assert len(recorded_calls) == 0 - - -async def test_loop_write_failure( - hass: HomeAssistant, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test handling the socket connected and disconnected.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - assert mqtt_client_mock.connect.call_count == 1 - - mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS - - client, server = socket.socketpair( - family=socket.AF_UNIX, type=socket.SOCK_STREAM, proto=0 - ) - client.setblocking(False) - server.setblocking(False) - mqtt_client_mock.on_socket_open(mqtt_client_mock, None, client) - mqtt_client_mock.on_socket_register_write(mqtt_client_mock, None, client) - mqtt_client_mock.loop_write.return_value = paho_mqtt.MQTT_ERR_CONN_LOST - mqtt_client_mock.loop_read.return_value = paho_mqtt.MQTT_ERR_CONN_LOST - - # Fill up the outgoing buffer to ensure that loop_write - # and loop_read are called that next time control is - # returned to the event loop - try: - for _ in range(1000): - server.send(b"long" * 100) - except BlockingIOError: - pass - - server.close() - # Once for the reader callback - await hass.async_block_till_done() - # Another for the writer callback - await hass.async_block_till_done() - # Final for the disconnect callback - await hass.async_block_till_done() - - assert "Disconnected from MQTT server test-broker:1883" in caplog.text - - @pytest.mark.parametrize( "attr", [ From 6f716c175387e8dc61c0db0383d3be61d5094e93 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 29 Jun 2024 11:06:56 -0500 Subject: [PATCH 101/146] Fix publish cancellation handling in MQTT (#120826) --- homeassistant/components/mqtt/client.py | 4 ++-- tests/components/mqtt/test_client.py | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index 7788c1db641..f65769badfa 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -1141,8 +1141,8 @@ class MQTT: # see https://github.com/eclipse/paho.mqtt.python/issues/687 # properties and reason codes are not used in Home Assistant future = self._async_get_mid_future(mid) - if future.done() and future.exception(): - # Timed out + if future.done() and (future.cancelled() or future.exception()): + # Timed out or cancelled return future.set_result(None) diff --git a/tests/components/mqtt/test_client.py b/tests/components/mqtt/test_client.py index 49b590383d1..cd02d805e1c 100644 --- a/tests/components/mqtt/test_client.py +++ b/tests/components/mqtt/test_client.py @@ -1194,6 +1194,23 @@ async def test_handle_mqtt_on_callback( assert "No ACK from MQTT server" not in caplog.text +async def test_handle_mqtt_on_callback_after_cancellation( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + mqtt_mock_entry: MqttMockHAClientGenerator, + mqtt_client_mock: MqttMockPahoClient, +) -> None: + """Test receiving an ACK after a cancellation.""" + mqtt_mock = await mqtt_mock_entry() + # Simulate the mid future getting a cancellation + mqtt_mock()._async_get_mid_future(101).cancel() + # Simulate an ACK for mid == 101, being received after the cancellation + mqtt_client_mock.on_publish(mqtt_client_mock, None, 101) + await hass.async_block_till_done() + assert "No ACK from MQTT server" not in caplog.text + assert "InvalidStateError" not in caplog.text + + async def test_handle_mqtt_on_callback_after_timeout( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, From c19fb35d0233da33b50c357f590a0b84428587c1 Mon Sep 17 00:00:00 2001 From: Yuxin Wang Date: Mon, 1 Jul 2024 01:30:08 -0400 Subject: [PATCH 102/146] Add handling for different STATFLAG formats in APCUPSD (#120870) * Add handling for different STATFLAG formats * Just use removesuffix --- .../components/apcupsd/binary_sensor.py | 6 +++++- .../components/apcupsd/test_binary_sensor.py | 21 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/apcupsd/binary_sensor.py b/homeassistant/components/apcupsd/binary_sensor.py index 77b2b8591e5..5f86ceb6eec 100644 --- a/homeassistant/components/apcupsd/binary_sensor.py +++ b/homeassistant/components/apcupsd/binary_sensor.py @@ -68,4 +68,8 @@ class OnlineStatus(CoordinatorEntity[APCUPSdCoordinator], BinarySensorEntity): """Returns true if the UPS is online.""" # Check if ONLINE bit is set in STATFLAG. key = self.entity_description.key.upper() - return int(self.coordinator.data[key], 16) & _VALUE_ONLINE_MASK != 0 + # The daemon could either report just a hex ("0x05000008"), or a hex with a "Status Flag" + # suffix ("0x05000008 Status Flag") in older versions. + # Here we trim the suffix if it exists to support both. + flag = self.coordinator.data[key].removesuffix(" Status Flag") + return int(flag, 16) & _VALUE_ONLINE_MASK != 0 diff --git a/tests/components/apcupsd/test_binary_sensor.py b/tests/components/apcupsd/test_binary_sensor.py index 7616a960b21..02351109603 100644 --- a/tests/components/apcupsd/test_binary_sensor.py +++ b/tests/components/apcupsd/test_binary_sensor.py @@ -1,5 +1,7 @@ """Test binary sensors of APCUPSd integration.""" +import pytest + from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.util import slugify @@ -31,3 +33,22 @@ async def test_no_binary_sensor(hass: HomeAssistant) -> None: device_slug = slugify(MOCK_STATUS["UPSNAME"]) state = hass.states.get(f"binary_sensor.{device_slug}_online_status") assert state is None + + +@pytest.mark.parametrize( + ("override", "expected"), + [ + ("0x008", "on"), + ("0x02040010 Status Flag", "off"), + ], +) +async def test_statflag(hass: HomeAssistant, override: str, expected: str) -> None: + """Test binary sensor for different STATFLAG values.""" + status = MOCK_STATUS.copy() + status["STATFLAG"] = override + await async_init_integration(hass, status=status) + + device_slug = slugify(MOCK_STATUS["UPSNAME"]) + assert ( + hass.states.get(f"binary_sensor.{device_slug}_online_status").state == expected + ) From 3a0e85beb8e4cb68f2b1ae00408fae05e7888b31 Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Mon, 1 Jul 2024 01:12:33 +0200 Subject: [PATCH 103/146] Bump aioautomower to 2024.6.4 (#120875) --- homeassistant/components/husqvarna_automower/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/husqvarna_automower/manifest.json b/homeassistant/components/husqvarna_automower/manifest.json index 7883b057a3f..f27b04ef0c0 100644 --- a/homeassistant/components/husqvarna_automower/manifest.json +++ b/homeassistant/components/husqvarna_automower/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/husqvarna_automower", "iot_class": "cloud_push", "loggers": ["aioautomower"], - "requirements": ["aioautomower==2024.6.3"] + "requirements": ["aioautomower==2024.6.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index dd68902baae..358147bfe73 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -204,7 +204,7 @@ aioaseko==0.1.1 aioasuswrt==1.4.0 # homeassistant.components.husqvarna_automower -aioautomower==2024.6.3 +aioautomower==2024.6.4 # homeassistant.components.azure_devops aioazuredevops==2.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 54e86d60186..9abd9e10de7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -183,7 +183,7 @@ aioaseko==0.1.1 aioasuswrt==1.4.0 # homeassistant.components.husqvarna_automower -aioautomower==2024.6.3 +aioautomower==2024.6.4 # homeassistant.components.azure_devops aioazuredevops==2.1.1 From a9740faeda331c46d3cfa7b9d0ffc7b8b85a4412 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Mon, 1 Jul 2024 20:06:56 +0300 Subject: [PATCH 104/146] Fix Shelly device shutdown (#120881) --- homeassistant/components/shelly/__init__.py | 6 ++++++ .../components/shelly/config_flow.py | 19 ++++++++++++------- homeassistant/components/shelly/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 21 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 184b7c8bb6b..75f66d0bced 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -174,10 +174,13 @@ async def _async_setup_block_entry( await device.initialize() if not device.firmware_supported: async_create_issue_unsupported_firmware(hass, entry) + await device.shutdown() raise ConfigEntryNotReady except (DeviceConnectionError, MacAddressMismatchError) as err: + await device.shutdown() raise ConfigEntryNotReady(repr(err)) from err except InvalidAuthError as err: + await device.shutdown() raise ConfigEntryAuthFailed(repr(err)) from err runtime_data.block = ShellyBlockCoordinator(hass, entry, device) @@ -247,10 +250,13 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ShellyConfigEntry) await device.initialize() if not device.firmware_supported: async_create_issue_unsupported_firmware(hass, entry) + await device.shutdown() raise ConfigEntryNotReady except (DeviceConnectionError, MacAddressMismatchError) as err: + await device.shutdown() raise ConfigEntryNotReady(repr(err)) from err except InvalidAuthError as err: + await device.shutdown() raise ConfigEntryAuthFailed(repr(err)) from err runtime_data.rpc = ShellyRpcCoordinator(hass, entry, device) diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index c044d032170..cb3bca6aa47 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -102,10 +102,11 @@ async def validate_input( ws_context, options, ) - await rpc_device.initialize() - await rpc_device.shutdown() - - sleep_period = get_rpc_device_wakeup_period(rpc_device.status) + try: + await rpc_device.initialize() + sleep_period = get_rpc_device_wakeup_period(rpc_device.status) + finally: + await rpc_device.shutdown() return { "title": rpc_device.name, @@ -121,11 +122,15 @@ async def validate_input( coap_context, options, ) - await block_device.initialize() - await block_device.shutdown() + try: + await block_device.initialize() + sleep_period = get_block_device_sleep_period(block_device.settings) + finally: + await block_device.shutdown() + return { "title": block_device.name, - CONF_SLEEP_PERIOD: get_block_device_sleep_period(block_device.settings), + CONF_SLEEP_PERIOD: sleep_period, "model": block_device.model, CONF_GEN: gen, } diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index b1b00e40c66..4076f53c28c 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -9,7 +9,7 @@ "iot_class": "local_push", "loggers": ["aioshelly"], "quality_scale": "platinum", - "requirements": ["aioshelly==10.0.1"], + "requirements": ["aioshelly==11.0.0"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 358147bfe73..5649fd1d86c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -362,7 +362,7 @@ aioruuvigateway==0.1.0 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==10.0.1 +aioshelly==11.0.0 # homeassistant.components.skybell aioskybell==22.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9abd9e10de7..d6755f889b7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -335,7 +335,7 @@ aioruuvigateway==0.1.0 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==10.0.1 +aioshelly==11.0.0 # homeassistant.components.skybell aioskybell==22.7.0 From 779a7ddaa23e265e395f0c2f7fd1b15bf3b50576 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 1 Jul 2024 01:46:10 -0700 Subject: [PATCH 105/146] Bump ical to 8.1.1 (#120888) --- homeassistant/components/google/manifest.json | 2 +- homeassistant/components/local_calendar/manifest.json | 2 +- homeassistant/components/local_todo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index 5fc28d2f398..d40daa89b0e 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/calendar.google", "iot_class": "cloud_polling", "loggers": ["googleapiclient"], - "requirements": ["gcal-sync==6.1.3", "oauth2client==4.1.3", "ical==8.0.1"] + "requirements": ["gcal-sync==6.1.3", "oauth2client==4.1.3", "ical==8.1.1"] } diff --git a/homeassistant/components/local_calendar/manifest.json b/homeassistant/components/local_calendar/manifest.json index 73619b6bfe9..95c65089c79 100644 --- a/homeassistant/components/local_calendar/manifest.json +++ b/homeassistant/components/local_calendar/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/local_calendar", "iot_class": "local_polling", "loggers": ["ical"], - "requirements": ["ical==8.0.1"] + "requirements": ["ical==8.1.1"] } diff --git a/homeassistant/components/local_todo/manifest.json b/homeassistant/components/local_todo/manifest.json index 4fa8e2982f9..313315a34f6 100644 --- a/homeassistant/components/local_todo/manifest.json +++ b/homeassistant/components/local_todo/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/local_todo", "iot_class": "local_polling", - "requirements": ["ical==8.0.1"] + "requirements": ["ical==8.1.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5649fd1d86c..d946ef51dcf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1131,7 +1131,7 @@ ibmiotf==0.3.4 # homeassistant.components.google # homeassistant.components.local_calendar # homeassistant.components.local_todo -ical==8.0.1 +ical==8.1.1 # homeassistant.components.ping icmplib==3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d6755f889b7..7980e3e4b64 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -927,7 +927,7 @@ ibeacon-ble==1.2.0 # homeassistant.components.google # homeassistant.components.local_calendar # homeassistant.components.local_todo -ical==8.0.1 +ical==8.1.1 # homeassistant.components.ping icmplib==3.0 From 5a052feb87b561dda15c8d434f4834b7fbe08a39 Mon Sep 17 00:00:00 2001 From: dougiteixeira <31328123+dougiteixeira@users.noreply.github.com> Date: Mon, 1 Jul 2024 09:21:41 -0300 Subject: [PATCH 106/146] Add missing translations for device class in Scrape (#120891) --- homeassistant/components/scrape/strings.json | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/scrape/strings.json b/homeassistant/components/scrape/strings.json index 9b534aed77b..42cf3001b75 100644 --- a/homeassistant/components/scrape/strings.json +++ b/homeassistant/components/scrape/strings.json @@ -139,18 +139,19 @@ "selector": { "device_class": { "options": { - "date": "[%key:component::sensor::entity_component::date::name%]", - "duration": "[%key:component::sensor::entity_component::duration::name%]", "apparent_power": "[%key:component::sensor::entity_component::apparent_power::name%]", "aqi": "[%key:component::sensor::entity_component::aqi::name%]", "atmospheric_pressure": "[%key:component::sensor::entity_component::atmospheric_pressure::name%]", "battery": "[%key:component::sensor::entity_component::battery::name%]", - "carbon_monoxide": "[%key:component::sensor::entity_component::carbon_monoxide::name%]", "carbon_dioxide": "[%key:component::sensor::entity_component::carbon_dioxide::name%]", + "carbon_monoxide": "[%key:component::sensor::entity_component::carbon_monoxide::name%]", + "conductivity": "[%key:component::sensor::entity_component::conductivity::name%]", "current": "[%key:component::sensor::entity_component::current::name%]", "data_rate": "[%key:component::sensor::entity_component::data_rate::name%]", "data_size": "[%key:component::sensor::entity_component::data_size::name%]", + "date": "[%key:component::sensor::entity_component::date::name%]", "distance": "[%key:component::sensor::entity_component::distance::name%]", + "duration": "[%key:component::sensor::entity_component::duration::name%]", "energy": "[%key:component::sensor::entity_component::energy::name%]", "energy_storage": "[%key:component::sensor::entity_component::energy_storage::name%]", "frequency": "[%key:component::sensor::entity_component::frequency::name%]", @@ -168,8 +169,8 @@ "pm1": "[%key:component::sensor::entity_component::pm1::name%]", "pm10": "[%key:component::sensor::entity_component::pm10::name%]", "pm25": "[%key:component::sensor::entity_component::pm25::name%]", - "power_factor": "[%key:component::sensor::entity_component::power_factor::name%]", "power": "[%key:component::sensor::entity_component::power::name%]", + "power_factor": "[%key:component::sensor::entity_component::power_factor::name%]", "precipitation": "[%key:component::sensor::entity_component::precipitation::name%]", "precipitation_intensity": "[%key:component::sensor::entity_component::precipitation_intensity::name%]", "pressure": "[%key:component::sensor::entity_component::pressure::name%]", @@ -184,6 +185,7 @@ "volatile_organic_compounds_parts": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]", "voltage": "[%key:component::sensor::entity_component::voltage::name%]", "volume": "[%key:component::sensor::entity_component::volume::name%]", + "volume_flow_rate": "[%key:component::sensor::entity_component::volume_flow_rate::name%]", "volume_storage": "[%key:component::sensor::entity_component::volume_storage::name%]", "water": "[%key:component::sensor::entity_component::water::name%]", "weight": "[%key:component::sensor::entity_component::weight::name%]", From a0f8012f4858e4c6ff3f86515e11e06a6a90414b Mon Sep 17 00:00:00 2001 From: dougiteixeira <31328123+dougiteixeira@users.noreply.github.com> Date: Mon, 1 Jul 2024 02:44:59 -0300 Subject: [PATCH 107/146] Add missing translations for device class in SQL (#120892) --- homeassistant/components/sql/strings.json | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sql/strings.json b/homeassistant/components/sql/strings.json index 361585b8876..cd36ccf7731 100644 --- a/homeassistant/components/sql/strings.json +++ b/homeassistant/components/sql/strings.json @@ -71,18 +71,19 @@ "selector": { "device_class": { "options": { - "date": "[%key:component::sensor::entity_component::date::name%]", - "duration": "[%key:component::sensor::entity_component::duration::name%]", "apparent_power": "[%key:component::sensor::entity_component::apparent_power::name%]", "aqi": "[%key:component::sensor::entity_component::aqi::name%]", "atmospheric_pressure": "[%key:component::sensor::entity_component::atmospheric_pressure::name%]", "battery": "[%key:component::sensor::entity_component::battery::name%]", - "carbon_monoxide": "[%key:component::sensor::entity_component::carbon_monoxide::name%]", "carbon_dioxide": "[%key:component::sensor::entity_component::carbon_dioxide::name%]", + "carbon_monoxide": "[%key:component::sensor::entity_component::carbon_monoxide::name%]", + "conductivity": "[%key:component::sensor::entity_component::conductivity::name%]", "current": "[%key:component::sensor::entity_component::current::name%]", "data_rate": "[%key:component::sensor::entity_component::data_rate::name%]", "data_size": "[%key:component::sensor::entity_component::data_size::name%]", + "date": "[%key:component::sensor::entity_component::date::name%]", "distance": "[%key:component::sensor::entity_component::distance::name%]", + "duration": "[%key:component::sensor::entity_component::duration::name%]", "energy": "[%key:component::sensor::entity_component::energy::name%]", "energy_storage": "[%key:component::sensor::entity_component::energy_storage::name%]", "frequency": "[%key:component::sensor::entity_component::frequency::name%]", @@ -100,8 +101,8 @@ "pm1": "[%key:component::sensor::entity_component::pm1::name%]", "pm10": "[%key:component::sensor::entity_component::pm10::name%]", "pm25": "[%key:component::sensor::entity_component::pm25::name%]", - "power_factor": "[%key:component::sensor::entity_component::power_factor::name%]", "power": "[%key:component::sensor::entity_component::power::name%]", + "power_factor": "[%key:component::sensor::entity_component::power_factor::name%]", "precipitation": "[%key:component::sensor::entity_component::precipitation::name%]", "precipitation_intensity": "[%key:component::sensor::entity_component::precipitation_intensity::name%]", "pressure": "[%key:component::sensor::entity_component::pressure::name%]", @@ -116,6 +117,7 @@ "volatile_organic_compounds_parts": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]", "voltage": "[%key:component::sensor::entity_component::voltage::name%]", "volume": "[%key:component::sensor::entity_component::volume::name%]", + "volume_flow_rate": "[%key:component::sensor::entity_component::volume_flow_rate::name%]", "volume_storage": "[%key:component::sensor::entity_component::volume_storage::name%]", "water": "[%key:component::sensor::entity_component::water::name%]", "weight": "[%key:component::sensor::entity_component::weight::name%]", From 16d7764f18f65712b4e78b9e3345fff5e060b9e8 Mon Sep 17 00:00:00 2001 From: dougiteixeira <31328123+dougiteixeira@users.noreply.github.com> Date: Mon, 1 Jul 2024 02:55:13 -0300 Subject: [PATCH 108/146] Add missing translations for device class in Template (#120893) --- homeassistant/components/template/strings.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/template/strings.json b/homeassistant/components/template/strings.json index 4a1377cbf0b..dc481b76ff8 100644 --- a/homeassistant/components/template/strings.json +++ b/homeassistant/components/template/strings.json @@ -105,6 +105,7 @@ "battery": "[%key:component::sensor::entity_component::battery::name%]", "carbon_dioxide": "[%key:component::sensor::entity_component::carbon_dioxide::name%]", "carbon_monoxide": "[%key:component::sensor::entity_component::carbon_monoxide::name%]", + "conductivity": "[%key:component::sensor::entity_component::conductivity::name%]", "current": "[%key:component::sensor::entity_component::current::name%]", "data_rate": "[%key:component::sensor::entity_component::data_rate::name%]", "data_size": "[%key:component::sensor::entity_component::data_size::name%]", From 88ed43c7792ff8732ad756522a39c925da6de408 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Mon, 1 Jul 2024 18:27:40 +0200 Subject: [PATCH 109/146] Improve add user error messages (#120909) --- homeassistant/auth/providers/homeassistant.py | 20 ++++++++-------- homeassistant/components/auth/strings.json | 5 +++- .../config/auth_provider_homeassistant.py | 24 ++++--------------- .../test_auth_provider_homeassistant.py | 16 +++++++++++-- 4 files changed, 32 insertions(+), 33 deletions(-) diff --git a/homeassistant/auth/providers/homeassistant.py b/homeassistant/auth/providers/homeassistant.py index 4e38260dd2f..ec39bdbdcdc 100644 --- a/homeassistant/auth/providers/homeassistant.py +++ b/homeassistant/auth/providers/homeassistant.py @@ -55,13 +55,6 @@ class InvalidUser(HomeAssistantError): Will not be raised when validating authentication. """ - -class InvalidUsername(InvalidUser): - """Raised when invalid username is specified. - - Will not be raised when validating authentication. - """ - def __init__( self, *args: object, @@ -77,6 +70,13 @@ class InvalidUsername(InvalidUser): ) +class InvalidUsername(InvalidUser): + """Raised when invalid username is specified. + + Will not be raised when validating authentication. + """ + + class Data: """Hold the user data.""" @@ -216,7 +216,7 @@ class Data: break if index is None: - raise InvalidUser + raise InvalidUser(translation_key="user_not_found") self.users.pop(index) @@ -232,7 +232,7 @@ class Data: user["password"] = self.hash_password(new_password, True).decode() break else: - raise InvalidUser + raise InvalidUser(translation_key="user_not_found") @callback def _validate_new_username(self, new_username: str) -> None: @@ -275,7 +275,7 @@ class Data: self._async_check_for_not_normalized_usernames(self._data) break else: - raise InvalidUser + raise InvalidUser(translation_key="user_not_found") async def async_save(self) -> None: """Save data.""" diff --git a/homeassistant/components/auth/strings.json b/homeassistant/components/auth/strings.json index 0e4cede78a3..c8622880f0f 100644 --- a/homeassistant/components/auth/strings.json +++ b/homeassistant/components/auth/strings.json @@ -37,7 +37,10 @@ "message": "Username \"{username}\" already exists" }, "username_not_normalized": { - "message": "Username \"{new_username}\" is not normalized" + "message": "Username \"{new_username}\" is not normalized. Please make sure the username is lowercase and does not contain any whitespace." + }, + "user_not_found": { + "message": "User not found" } }, "issues": { diff --git a/homeassistant/components/config/auth_provider_homeassistant.py b/homeassistant/components/config/auth_provider_homeassistant.py index 1cfcda6d4b2..8513c53bd07 100644 --- a/homeassistant/components/config/auth_provider_homeassistant.py +++ b/homeassistant/components/config/auth_provider_homeassistant.py @@ -53,11 +53,7 @@ async def websocket_create( ) return - try: - await provider.async_add_auth(msg["username"], msg["password"]) - except auth_ha.InvalidUser: - connection.send_error(msg["id"], "username_exists", "Username already exists") - return + await provider.async_add_auth(msg["username"], msg["password"]) credentials = await provider.async_get_or_create_credentials( {"username": msg["username"]} @@ -94,13 +90,7 @@ async def websocket_delete( connection.send_result(msg["id"]) return - try: - await provider.async_remove_auth(msg["username"]) - except auth_ha.InvalidUser: - connection.send_error( - msg["id"], "auth_not_found", "Given username was not found." - ) - return + await provider.async_remove_auth(msg["username"]) connection.send_result(msg["id"]) @@ -187,14 +177,8 @@ async def websocket_admin_change_password( ) return - try: - await provider.async_change_password(username, msg["password"]) - connection.send_result(msg["id"]) - except auth_ha.InvalidUser: - connection.send_error( - msg["id"], "credentials_not_found", "Credentials not found" - ) - return + await provider.async_change_password(username, msg["password"]) + connection.send_result(msg["id"]) @websocket_api.websocket_command( diff --git a/tests/components/config/test_auth_provider_homeassistant.py b/tests/components/config/test_auth_provider_homeassistant.py index ffee88f91ec..6b580013968 100644 --- a/tests/components/config/test_auth_provider_homeassistant.py +++ b/tests/components/config/test_auth_provider_homeassistant.py @@ -183,7 +183,13 @@ async def test_create_auth_duplicate_username( result = await client.receive_json() assert not result["success"], result - assert result["error"]["code"] == "username_exists" + assert result["error"] == { + "code": "home_assistant_error", + "message": "username_already_exists", + "translation_key": "username_already_exists", + "translation_placeholders": {"username": "test-user"}, + "translation_domain": "auth", + } async def test_delete_removes_just_auth( @@ -282,7 +288,13 @@ async def test_delete_unknown_auth( result = await client.receive_json() assert not result["success"], result - assert result["error"]["code"] == "auth_not_found" + assert result["error"] == { + "code": "home_assistant_error", + "message": "user_not_found", + "translation_key": "user_not_found", + "translation_placeholders": None, + "translation_domain": "auth", + } async def test_change_password( From a787ce863371efc0620d6ea7e557d954dc637874 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 1 Jul 2024 13:06:14 +0200 Subject: [PATCH 110/146] Bump incomfort-client dependency to 0.6.3 (#120913) --- homeassistant/components/incomfort/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/incomfort/manifest.json b/homeassistant/components/incomfort/manifest.json index c0b536dabe5..93f350a8e2c 100644 --- a/homeassistant/components/incomfort/manifest.json +++ b/homeassistant/components/incomfort/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/incomfort", "iot_class": "local_polling", "loggers": ["incomfortclient"], - "requirements": ["incomfort-client==0.6.2"] + "requirements": ["incomfort-client==0.6.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index d946ef51dcf..6e51947fca1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1152,7 +1152,7 @@ ihcsdk==2.8.5 imgw_pib==1.0.5 # homeassistant.components.incomfort -incomfort-client==0.6.2 +incomfort-client==0.6.3 # homeassistant.components.influxdb influxdb-client==1.24.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7980e3e4b64..a0f675bc256 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -942,7 +942,7 @@ ifaddr==0.2.0 imgw_pib==1.0.5 # homeassistant.components.incomfort -incomfort-client==0.6.2 +incomfort-client==0.6.3 # homeassistant.components.influxdb influxdb-client==1.24.0 From 887ab1dc58c118f69bf3f2009042b3c4ccd93018 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 1 Jul 2024 17:52:30 +0200 Subject: [PATCH 111/146] Bump openai to 1.35.1 (#120926) Bump openai to 1.35.7 --- homeassistant/components/openai_conversation/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/openai_conversation/manifest.json b/homeassistant/components/openai_conversation/manifest.json index 0c06a3d4cd8..fcbdc996ce5 100644 --- a/homeassistant/components/openai_conversation/manifest.json +++ b/homeassistant/components/openai_conversation/manifest.json @@ -8,5 +8,5 @@ "documentation": "https://www.home-assistant.io/integrations/openai_conversation", "integration_type": "service", "iot_class": "cloud_polling", - "requirements": ["openai==1.3.8"] + "requirements": ["openai==1.35.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index 6e51947fca1..d96b0266043 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1477,7 +1477,7 @@ open-garage==0.2.0 open-meteo==0.3.1 # homeassistant.components.openai_conversation -openai==1.3.8 +openai==1.35.7 # homeassistant.components.openerz openerz-api==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a0f675bc256..2753f42ee93 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1198,7 +1198,7 @@ open-garage==0.2.0 open-meteo==0.3.1 # homeassistant.components.openai_conversation -openai==1.3.8 +openai==1.35.7 # homeassistant.components.openerz openerz-api==0.3.0 From 8a7e2c05a5844d6d2d72d0d5969a439030bd9a0e Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Mon, 1 Jul 2024 17:30:23 +0200 Subject: [PATCH 112/146] Mark dry/fan-only climate modes as supported for Panasonic room air conditioner (#120939) --- homeassistant/components/matter/climate.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/matter/climate.py b/homeassistant/components/matter/climate.py index d2656d59138..c97124f4305 100644 --- a/homeassistant/components/matter/climate.py +++ b/homeassistant/components/matter/climate.py @@ -60,6 +60,7 @@ SUPPORT_DRY_MODE_DEVICES: set[tuple[int, int]] = { # In the list below specify tuples of (vendorid, productid) of devices that # support dry mode. (0x0001, 0x0108), + (0x0001, 0x010A), (0x1209, 0x8007), } @@ -68,6 +69,7 @@ SUPPORT_FAN_MODE_DEVICES: set[tuple[int, int]] = { # In the list below specify tuples of (vendorid, productid) of devices that # support fan-only mode. (0x0001, 0x0108), + (0x0001, 0x010A), (0x1209, 0x8007), } From 4b2be448f0195b7db2d4a093e38ee187cec6b040 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Mon, 1 Jul 2024 15:50:35 +0100 Subject: [PATCH 113/146] Bump python-kasa to 0.7.0.2 (#120940) --- homeassistant/components/tplink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index 74b80771c65..1270bb3469b 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -297,5 +297,5 @@ "iot_class": "local_polling", "loggers": ["kasa"], "quality_scale": "platinum", - "requirements": ["python-kasa[speedups]==0.7.0.1"] + "requirements": ["python-kasa[speedups]==0.7.0.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index d96b0266043..c18ed2f439a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2275,7 +2275,7 @@ python-join-api==0.0.9 python-juicenet==1.1.0 # homeassistant.components.tplink -python-kasa[speedups]==0.7.0.1 +python-kasa[speedups]==0.7.0.2 # homeassistant.components.lirc # python-lirc==1.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2753f42ee93..6291a3dddca 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1775,7 +1775,7 @@ python-izone==1.2.9 python-juicenet==1.1.0 # homeassistant.components.tplink -python-kasa[speedups]==0.7.0.1 +python-kasa[speedups]==0.7.0.2 # homeassistant.components.matter python-matter-server==6.2.0b1 From d8f55763c50aa6c61b787a9364c5092cd559d223 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 1 Jul 2024 09:26:20 -0700 Subject: [PATCH 114/146] Downgrade logging previously reported asyncio block to debug (#120942) --- homeassistant/util/loop.py | 123 +++++++++++----- tests/util/test_loop.py | 282 +++++++++++++++++++++---------------- 2 files changed, 244 insertions(+), 161 deletions(-) diff --git a/homeassistant/util/loop.py b/homeassistant/util/loop.py index 866f35e79e2..d7593013046 100644 --- a/homeassistant/util/loop.py +++ b/homeassistant/util/loop.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections.abc import Callable import functools +from functools import cache import linecache import logging import threading @@ -26,6 +27,11 @@ def _get_line_from_cache(filename: str, lineno: int) -> str: return (linecache.getline(filename, lineno) or "?").strip() +# Set of previously reported blocking calls +# (integration, filename, lineno) +_PREVIOUSLY_REPORTED: set[tuple[str | None, str, int | Any]] = set() + + def raise_for_blocking_call( func: Callable[..., Any], check_allowed: Callable[[dict[str, Any]], bool] | None = None, @@ -42,28 +48,48 @@ def raise_for_blocking_call( offender_filename = offender_frame.f_code.co_filename offender_lineno = offender_frame.f_lineno offender_line = _get_line_from_cache(offender_filename, offender_lineno) + report_key: tuple[str | None, str, int | Any] try: integration_frame = get_integration_frame() except MissingIntegrationFrame: # Did not source from integration? Hard error. + report_key = (None, offender_filename, offender_lineno) + was_reported = report_key in _PREVIOUSLY_REPORTED + _PREVIOUSLY_REPORTED.add(report_key) if not strict_core: - _LOGGER.warning( - "Detected blocking call to %s with args %s in %s, " - "line %s: %s inside the event loop; " - "This is causing stability issues. " - "Please create a bug report at " - "https://github.com/home-assistant/core/issues?q=is%%3Aopen+is%%3Aissue\n" - "%s\n" - "Traceback (most recent call last):\n%s", - func.__name__, - mapped_args.get("args"), - offender_filename, - offender_lineno, - offender_line, - _dev_help_message(func.__name__), - "".join(traceback.format_stack(f=offender_frame)), - ) + if was_reported: + _LOGGER.debug( + "Detected blocking call to %s with args %s in %s, " + "line %s: %s inside the event loop; " + "This is causing stability issues. " + "Please create a bug report at " + "https://github.com/home-assistant/core/issues?q=is%%3Aopen+is%%3Aissue\n" + "%s\n", + func.__name__, + mapped_args.get("args"), + offender_filename, + offender_lineno, + offender_line, + _dev_help_message(func.__name__), + ) + else: + _LOGGER.warning( + "Detected blocking call to %s with args %s in %s, " + "line %s: %s inside the event loop; " + "This is causing stability issues. " + "Please create a bug report at " + "https://github.com/home-assistant/core/issues?q=is%%3Aopen+is%%3Aissue\n" + "%s\n" + "Traceback (most recent call last):\n%s", + func.__name__, + mapped_args.get("args"), + offender_filename, + offender_lineno, + offender_line, + _dev_help_message(func.__name__), + "".join(traceback.format_stack(f=offender_frame)), + ) return if found_frame is None: @@ -77,32 +103,56 @@ def raise_for_blocking_call( f"{_dev_help_message(func.__name__)}" ) + report_key = (integration_frame.integration, offender_filename, offender_lineno) + was_reported = report_key in _PREVIOUSLY_REPORTED + _PREVIOUSLY_REPORTED.add(report_key) + report_issue = async_suggest_report_issue( async_get_hass_or_none(), integration_domain=integration_frame.integration, module=integration_frame.module, ) - _LOGGER.warning( - "Detected blocking call to %s with args %s " - "inside the event loop by %sintegration '%s' " - "at %s, line %s: %s (offender: %s, line %s: %s), please %s\n" - "%s\n" - "Traceback (most recent call last):\n%s", - func.__name__, - mapped_args.get("args"), - "custom " if integration_frame.custom_integration else "", - integration_frame.integration, - integration_frame.relative_filename, - integration_frame.line_number, - integration_frame.line, - offender_filename, - offender_lineno, - offender_line, - report_issue, - _dev_help_message(func.__name__), - "".join(traceback.format_stack(f=integration_frame.frame)), - ) + if was_reported: + _LOGGER.debug( + "Detected blocking call to %s with args %s " + "inside the event loop by %sintegration '%s' " + "at %s, line %s: %s (offender: %s, line %s: %s), please %s\n" + "%s\n", + func.__name__, + mapped_args.get("args"), + "custom " if integration_frame.custom_integration else "", + integration_frame.integration, + integration_frame.relative_filename, + integration_frame.line_number, + integration_frame.line, + offender_filename, + offender_lineno, + offender_line, + report_issue, + _dev_help_message(func.__name__), + ) + else: + _LOGGER.warning( + "Detected blocking call to %s with args %s " + "inside the event loop by %sintegration '%s' " + "at %s, line %s: %s (offender: %s, line %s: %s), please %s\n" + "%s\n" + "Traceback (most recent call last):\n%s", + func.__name__, + mapped_args.get("args"), + "custom " if integration_frame.custom_integration else "", + integration_frame.integration, + integration_frame.relative_filename, + integration_frame.line_number, + integration_frame.line, + offender_filename, + offender_lineno, + offender_line, + report_issue, + _dev_help_message(func.__name__), + "".join(traceback.format_stack(f=integration_frame.frame)), + ) if strict: raise RuntimeError( @@ -117,6 +167,7 @@ def raise_for_blocking_call( ) +@cache def _dev_help_message(what: str) -> str: """Generate help message to guide developers.""" return ( diff --git a/tests/util/test_loop.py b/tests/util/test_loop.py index 585f32a965f..f4846d98898 100644 --- a/tests/util/test_loop.py +++ b/tests/util/test_loop.py @@ -1,5 +1,7 @@ """Tests for async util methods from Python source.""" +from collections.abc import Generator +import contextlib import threading from unittest.mock import Mock, patch @@ -15,57 +17,14 @@ def banned_function(): """Mock banned function.""" -async def test_raise_for_blocking_call_async() -> None: - """Test raise_for_blocking_call detects when called from event loop without integration context.""" - with pytest.raises(RuntimeError): - haloop.raise_for_blocking_call(banned_function) - - -async def test_raise_for_blocking_call_async_non_strict_core( - caplog: pytest.LogCaptureFixture, -) -> None: - """Test non_strict_core raise_for_blocking_call detects from event loop without integration context.""" - haloop.raise_for_blocking_call(banned_function, strict_core=False) - assert "Detected blocking call to banned_function" in caplog.text - assert "Traceback (most recent call last)" in caplog.text - assert ( - "Please create a bug report at https://github.com/home-assistant/core/issues" - in caplog.text - ) - assert ( - "For developers, please see " - "https://developers.home-assistant.io/docs/asyncio_blocking_operations/#banned_function" - ) in caplog.text - - -async def test_raise_for_blocking_call_async_integration( - caplog: pytest.LogCaptureFixture, -) -> None: - """Test raise_for_blocking_call detects and raises when called from event loop from integration context.""" - frames = extract_stack_to_frame( - [ - Mock( - filename="/home/paulus/homeassistant/core.py", - lineno="23", - line="do_something()", - ), - Mock( - filename="/home/paulus/homeassistant/components/hue/light.py", - lineno="23", - line="self.light.is_on", - ), - Mock( - filename="/home/paulus/aiohue/lights.py", - lineno="2", - line="something()", - ), - ] - ) +@contextlib.contextmanager +def patch_get_current_frame(stack: list[Mock]) -> Generator[None, None, None]: + """Patch get_current_frame.""" + frames = extract_stack_to_frame(stack) with ( - pytest.raises(RuntimeError), patch( "homeassistant.helpers.frame.linecache.getline", - return_value="self.light.is_on", + return_value=stack[1].line, ), patch( "homeassistant.util.loop._get_line_from_cache", @@ -79,13 +38,104 @@ async def test_raise_for_blocking_call_async_integration( "homeassistant.helpers.frame.get_current_frame", return_value=frames, ), + ): + yield + + +async def test_raise_for_blocking_call_async() -> None: + """Test raise_for_blocking_call detects when called from event loop without integration context.""" + with pytest.raises(RuntimeError): + haloop.raise_for_blocking_call(banned_function) + + +async def test_raise_for_blocking_call_async_non_strict_core( + caplog: pytest.LogCaptureFixture, +) -> None: + """Test non_strict_core raise_for_blocking_call detects from event loop without integration context.""" + stack = [ + Mock( + filename="/home/paulus/homeassistant/core.py", + lineno="12", + line="do_something()", + ), + Mock( + filename="/home/paulus/homeassistant/core.py", + lineno="12", + line="self.light.is_on", + ), + Mock( + filename="/home/paulus/aiohue/lights.py", + lineno="2", + line="something()", + ), + ] + with patch_get_current_frame(stack): + haloop.raise_for_blocking_call(banned_function, strict_core=False) + assert "Detected blocking call to banned_function" in caplog.text + assert "Traceback (most recent call last)" in caplog.text + assert ( + "Please create a bug report at https://github.com/home-assistant/core/issues" + in caplog.text + ) + assert ( + "For developers, please see " + "https://developers.home-assistant.io/docs/asyncio_blocking_operations/#banned_function" + ) in caplog.text + + warnings = [ + record for record in caplog.get_records("call") if record.levelname == "WARNING" + ] + assert len(warnings) == 1 + caplog.clear() + + # Second call should log at debug + with patch_get_current_frame(stack): + haloop.raise_for_blocking_call(banned_function, strict_core=False) + + warnings = [ + record for record in caplog.get_records("call") if record.levelname == "WARNING" + ] + assert len(warnings) == 0 + assert ( + "For developers, please see " + "https://developers.home-assistant.io/docs/asyncio_blocking_operations/#banned_function" + ) in caplog.text + + # no expensive traceback on debug + assert "Traceback (most recent call last)" not in caplog.text + + +async def test_raise_for_blocking_call_async_integration( + caplog: pytest.LogCaptureFixture, +) -> None: + """Test raise_for_blocking_call detects and raises when called from event loop from integration context.""" + stack = [ + Mock( + filename="/home/paulus/homeassistant/core.py", + lineno="18", + line="do_something()", + ), + Mock( + filename="/home/paulus/homeassistant/components/hue/light.py", + lineno="18", + line="self.light.is_on", + ), + Mock( + filename="/home/paulus/aiohue/lights.py", + lineno="8", + line="something()", + ), + ] + with ( + pytest.raises(RuntimeError), + patch_get_current_frame(stack), ): haloop.raise_for_blocking_call(banned_function) assert ( "Detected blocking call to banned_function with args None" " inside the event loop by integration" - " 'hue' at homeassistant/components/hue/light.py, line 23: self.light.is_on " - "(offender: /home/paulus/aiohue/lights.py, line 2: mock_line), please create " + " 'hue' at homeassistant/components/hue/light.py, line 18: self.light.is_on " + "(offender: /home/paulus/aiohue/lights.py, line 8: mock_line), please create " "a bug report at https://github.com/home-assistant/core/issues?" "q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+hue%22" in caplog.text ) @@ -99,55 +149,37 @@ async def test_raise_for_blocking_call_async_integration_non_strict( caplog: pytest.LogCaptureFixture, ) -> None: """Test raise_for_blocking_call detects when called from event loop from integration context.""" - frames = extract_stack_to_frame( - [ - Mock( - filename="/home/paulus/homeassistant/core.py", - lineno="23", - line="do_something()", - ), - Mock( - filename="/home/paulus/homeassistant/components/hue/light.py", - lineno="23", - line="self.light.is_on", - ), - Mock( - filename="/home/paulus/aiohue/lights.py", - lineno="2", - line="something()", - ), - ] - ) - with ( - patch( - "homeassistant.helpers.frame.linecache.getline", - return_value="self.light.is_on", + stack = [ + Mock( + filename="/home/paulus/homeassistant/core.py", + lineno="15", + line="do_something()", ), - patch( - "homeassistant.util.loop._get_line_from_cache", - return_value="mock_line", + Mock( + filename="/home/paulus/homeassistant/components/hue/light.py", + lineno="15", + line="self.light.is_on", ), - patch( - "homeassistant.util.loop.get_current_frame", - return_value=frames, + Mock( + filename="/home/paulus/aiohue/lights.py", + lineno="1", + line="something()", ), - patch( - "homeassistant.helpers.frame.get_current_frame", - return_value=frames, - ), - ): + ] + with patch_get_current_frame(stack): haloop.raise_for_blocking_call(banned_function, strict=False) + assert ( "Detected blocking call to banned_function with args None" " inside the event loop by integration" - " 'hue' at homeassistant/components/hue/light.py, line 23: self.light.is_on " - "(offender: /home/paulus/aiohue/lights.py, line 2: mock_line), " + " 'hue' at homeassistant/components/hue/light.py, line 15: self.light.is_on " + "(offender: /home/paulus/aiohue/lights.py, line 1: mock_line), " "please create a bug report at https://github.com/home-assistant/core/issues?" "q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+hue%22" in caplog.text ) assert "Traceback (most recent call last)" in caplog.text assert ( - 'File "/home/paulus/homeassistant/components/hue/light.py", line 23' + 'File "/home/paulus/homeassistant/components/hue/light.py", line 15' in caplog.text ) assert ( @@ -158,62 +190,62 @@ async def test_raise_for_blocking_call_async_integration_non_strict( "For developers, please see " "https://developers.home-assistant.io/docs/asyncio_blocking_operations/#banned_function" ) in caplog.text + warnings = [ + record for record in caplog.get_records("call") if record.levelname == "WARNING" + ] + assert len(warnings) == 1 + caplog.clear() + + # Second call should log at debug + with patch_get_current_frame(stack): + haloop.raise_for_blocking_call(banned_function, strict=False) + + warnings = [ + record for record in caplog.get_records("call") if record.levelname == "WARNING" + ] + assert len(warnings) == 0 + assert ( + "For developers, please see " + "https://developers.home-assistant.io/docs/asyncio_blocking_operations/#banned_function" + ) in caplog.text + # no expensive traceback on debug + assert "Traceback (most recent call last)" not in caplog.text async def test_raise_for_blocking_call_async_custom( caplog: pytest.LogCaptureFixture, ) -> None: """Test raise_for_blocking_call detects when called from event loop with custom component context.""" - frames = extract_stack_to_frame( - [ - Mock( - filename="/home/paulus/homeassistant/core.py", - lineno="23", - line="do_something()", - ), - Mock( - filename="/home/paulus/config/custom_components/hue/light.py", - lineno="23", - line="self.light.is_on", - ), - Mock( - filename="/home/paulus/aiohue/lights.py", - lineno="2", - line="something()", - ), - ] - ) - with ( - pytest.raises(RuntimeError), - patch( - "homeassistant.helpers.frame.linecache.getline", - return_value="self.light.is_on", + stack = [ + Mock( + filename="/home/paulus/homeassistant/core.py", + lineno="12", + line="do_something()", ), - patch( - "homeassistant.util.loop._get_line_from_cache", - return_value="mock_line", + Mock( + filename="/home/paulus/config/custom_components/hue/light.py", + lineno="12", + line="self.light.is_on", ), - patch( - "homeassistant.util.loop.get_current_frame", - return_value=frames, + Mock( + filename="/home/paulus/aiohue/lights.py", + lineno="3", + line="something()", ), - patch( - "homeassistant.helpers.frame.get_current_frame", - return_value=frames, - ), - ): + ] + with pytest.raises(RuntimeError), patch_get_current_frame(stack): haloop.raise_for_blocking_call(banned_function) assert ( "Detected blocking call to banned_function with args None" " inside the event loop by custom " - "integration 'hue' at custom_components/hue/light.py, line 23: self.light.is_on" - " (offender: /home/paulus/aiohue/lights.py, line 2: mock_line), " + "integration 'hue' at custom_components/hue/light.py, line 12: self.light.is_on" + " (offender: /home/paulus/aiohue/lights.py, line 3: mock_line), " "please create a bug report at https://github.com/home-assistant/core/issues?" "q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+hue%22" ) in caplog.text assert "Traceback (most recent call last)" in caplog.text assert ( - 'File "/home/paulus/config/custom_components/hue/light.py", line 23' + 'File "/home/paulus/config/custom_components/hue/light.py", line 12' in caplog.text ) assert ( From 2f307d6a8a8b08cbd793f5c2f6de84019fa5641b Mon Sep 17 00:00:00 2001 From: Markus Jacobsen Date: Mon, 1 Jul 2024 19:02:43 +0200 Subject: [PATCH 115/146] Fix Bang & Olufsen jumping volume bar (#120946) --- homeassistant/components/bang_olufsen/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/bang_olufsen/media_player.py b/homeassistant/components/bang_olufsen/media_player.py index 0eff9f2bb85..07e38d633a1 100644 --- a/homeassistant/components/bang_olufsen/media_player.py +++ b/homeassistant/components/bang_olufsen/media_player.py @@ -366,7 +366,7 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity): @property def volume_level(self) -> float | None: """Volume level of the media player (0..1).""" - if self._volume.level and self._volume.level.level: + if self._volume.level and self._volume.level.level is not None: return float(self._volume.level.level / 100) return None From 74687f3b6009715e1685fbe260c00c2a2274ec53 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 1 Jul 2024 19:44:51 +0200 Subject: [PATCH 116/146] Bump version to 2024.7.0b7 --- 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 e97f14f830c..5f020a02624 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "0b6" +PATCH_VERSION: Final = "0b7" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 0f4b25eb0cc..6320551a082 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.7.0b6" +version = "2024.7.0b7" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 1e6dc74812000d4fd98de4031b3157afa71517b5 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Tue, 2 Jul 2024 08:23:07 +0200 Subject: [PATCH 117/146] Minor polishing for tplink (#120868) --- homeassistant/components/tplink/climate.py | 11 ++++--- homeassistant/components/tplink/entity.py | 24 +++++++------- homeassistant/components/tplink/fan.py | 3 +- homeassistant/components/tplink/light.py | 32 +++++++++---------- homeassistant/components/tplink/sensor.py | 18 +---------- homeassistant/components/tplink/switch.py | 19 +---------- .../components/tplink/fixtures/features.json | 2 +- .../tplink/snapshots/test_sensor.ambr | 4 +-- tests/components/tplink/test_light.py | 16 ++++++---- 9 files changed, 51 insertions(+), 78 deletions(-) diff --git a/homeassistant/components/tplink/climate.py b/homeassistant/components/tplink/climate.py index 99a8c43fac3..3bd6aba5c26 100644 --- a/homeassistant/components/tplink/climate.py +++ b/homeassistant/components/tplink/climate.py @@ -77,16 +77,17 @@ class TPLinkClimateEntity(CoordinatedTPLinkEntity, ClimateEntity): parent: Device, ) -> None: """Initialize the climate entity.""" - super().__init__(device, coordinator, parent=parent) - self._state_feature = self._device.features["state"] - self._mode_feature = self._device.features["thermostat_mode"] - self._temp_feature = self._device.features["temperature"] - self._target_feature = self._device.features["target_temperature"] + self._state_feature = device.features["state"] + self._mode_feature = device.features["thermostat_mode"] + self._temp_feature = device.features["temperature"] + self._target_feature = device.features["target_temperature"] self._attr_min_temp = self._target_feature.minimum_value self._attr_max_temp = self._target_feature.maximum_value self._attr_temperature_unit = UNIT_MAPPING[cast(str, self._temp_feature.unit)] + super().__init__(device, coordinator, parent=parent) + @async_refresh_after async def async_set_temperature(self, **kwargs: Any) -> None: """Set target temperature.""" diff --git a/homeassistant/components/tplink/entity.py b/homeassistant/components/tplink/entity.py index 4e8ec0e0779..4ec0480cf82 100644 --- a/homeassistant/components/tplink/entity.py +++ b/homeassistant/components/tplink/entity.py @@ -56,15 +56,21 @@ DEVICETYPES_WITH_SPECIALIZED_PLATFORMS = { DeviceType.Thermostat, } +# Primary features to always include even when the device type has its own platform +FEATURES_ALLOW_LIST = { + # lights have current_consumption and a specialized platform + "current_consumption" +} + + # Features excluded due to future platform additions EXCLUDED_FEATURES = { # update "current_firmware_version", "available_firmware_version", - # fan - "fan_speed_level", } + LEGACY_KEY_MAPPING = { "current": ATTR_CURRENT_A, "current_consumption": ATTR_CURRENT_POWER_W, @@ -179,15 +185,12 @@ class CoordinatedTPLinkEntity(CoordinatorEntity[TPLinkDataUpdateCoordinator], AB self._attr_unique_id = self._get_unique_id() + self._async_call_update_attrs() + def _get_unique_id(self) -> str: """Return unique ID for the entity.""" return legacy_device_id(self._device) - async def async_added_to_hass(self) -> None: - """Handle being added to hass.""" - self._async_call_update_attrs() - return await super().async_added_to_hass() - @abstractmethod @callback def _async_update_attrs(self) -> None: @@ -196,11 +199,7 @@ class CoordinatedTPLinkEntity(CoordinatorEntity[TPLinkDataUpdateCoordinator], AB @callback def _async_call_update_attrs(self) -> None: - """Call update_attrs and make entity unavailable on error. - - update_attrs can sometimes fail if a device firmware update breaks the - downstream library. - """ + """Call update_attrs and make entity unavailable on errors.""" try: self._async_update_attrs() except Exception as ex: # noqa: BLE001 @@ -358,6 +357,7 @@ class CoordinatedTPLinkFeatureEntity(CoordinatedTPLinkEntity, ABC): and ( feat.category is not Feature.Category.Primary or device.device_type not in DEVICETYPES_WITH_SPECIALIZED_PLATFORMS + or feat.id in FEATURES_ALLOW_LIST ) and ( desc := cls._description_for_feature( diff --git a/homeassistant/components/tplink/fan.py b/homeassistant/components/tplink/fan.py index 947a9072329..292240bca94 100644 --- a/homeassistant/components/tplink/fan.py +++ b/homeassistant/components/tplink/fan.py @@ -69,11 +69,12 @@ class TPLinkFanEntity(CoordinatedTPLinkEntity, FanEntity): parent: Device | None = None, ) -> None: """Initialize the fan.""" - super().__init__(device, coordinator, parent=parent) self.fan_module = fan_module # If _attr_name is None the entity name will be the device name self._attr_name = None if parent is None else device.alias + super().__init__(device, coordinator, parent=parent) + @async_refresh_after async def async_turn_on( self, diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index 633648bbf23..a736a0ba1e1 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -140,9 +140,7 @@ async def async_setup_entry( parent_coordinator = data.parent_coordinator device = parent_coordinator.device entities: list[TPLinkLightEntity | TPLinkLightEffectEntity] = [] - if ( - effect_module := device.modules.get(Module.LightEffect) - ) and effect_module.has_custom_effects: + if effect_module := device.modules.get(Module.LightEffect): entities.append( TPLinkLightEffectEntity( device, @@ -151,17 +149,18 @@ async def async_setup_entry( effect_module=effect_module, ) ) - platform = entity_platform.async_get_current_platform() - platform.async_register_entity_service( - SERVICE_RANDOM_EFFECT, - RANDOM_EFFECT_DICT, - "async_set_random_effect", - ) - platform.async_register_entity_service( - SERVICE_SEQUENCE_EFFECT, - SEQUENCE_EFFECT_DICT, - "async_set_sequence_effect", - ) + if effect_module.has_custom_effects: + platform = entity_platform.async_get_current_platform() + platform.async_register_entity_service( + SERVICE_RANDOM_EFFECT, + RANDOM_EFFECT_DICT, + "async_set_random_effect", + ) + platform.async_register_entity_service( + SERVICE_SEQUENCE_EFFECT, + SEQUENCE_EFFECT_DICT, + "async_set_sequence_effect", + ) elif Module.Light in device.modules: entities.append( TPLinkLightEntity( @@ -197,7 +196,6 @@ class TPLinkLightEntity(CoordinatedTPLinkEntity, LightEntity): ) -> None: """Initialize the light.""" self._parent = parent - super().__init__(device, coordinator, parent=parent) self._light_module = light_module # If _attr_name is None the entity name will be the device name self._attr_name = None if parent is None else device.alias @@ -215,7 +213,8 @@ class TPLinkLightEntity(CoordinatedTPLinkEntity, LightEntity): if len(self._attr_supported_color_modes) == 1: # If the light supports only a single color mode, set it now self._fixed_color_mode = next(iter(self._attr_supported_color_modes)) - self._async_call_update_attrs() + + super().__init__(device, coordinator, parent=parent) def _get_unique_id(self) -> str: """Return unique ID for the entity.""" @@ -371,6 +370,7 @@ class TPLinkLightEffectEntity(TPLinkLightEntity): effect_module = self._effect_module if effect_module.effect != LightEffect.LIGHT_EFFECTS_OFF: self._attr_effect = effect_module.effect + self._attr_color_mode = ColorMode.BRIGHTNESS else: self._attr_effect = EFFECT_OFF if effect_list := effect_module.effect_list: diff --git a/homeassistant/components/tplink/sensor.py b/homeassistant/components/tplink/sensor.py index 474ee6bfacf..3da414d74d3 100644 --- a/homeassistant/components/tplink/sensor.py +++ b/homeassistant/components/tplink/sensor.py @@ -5,7 +5,7 @@ from __future__ import annotations from dataclasses import dataclass from typing import cast -from kasa import Device, Feature +from kasa import Feature from homeassistant.components.sensor import ( SensorDeviceClass, @@ -18,7 +18,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import TPLinkConfigEntry from .const import UNIT_MAPPING -from .coordinator import TPLinkDataUpdateCoordinator from .entity import CoordinatedTPLinkFeatureEntity, TPLinkFeatureEntityDescription @@ -144,21 +143,6 @@ class TPLinkSensorEntity(CoordinatedTPLinkFeatureEntity, SensorEntity): entity_description: TPLinkSensorEntityDescription - def __init__( - self, - device: Device, - coordinator: TPLinkDataUpdateCoordinator, - *, - feature: Feature, - description: TPLinkSensorEntityDescription, - parent: Device | None = None, - ) -> None: - """Initialize the sensor.""" - super().__init__( - device, coordinator, description=description, feature=feature, parent=parent - ) - self._async_call_update_attrs() - @callback def _async_update_attrs(self) -> None: """Update the entity's attributes.""" diff --git a/homeassistant/components/tplink/switch.py b/homeassistant/components/tplink/switch.py index 2520de9dd3e..62957d48ac4 100644 --- a/homeassistant/components/tplink/switch.py +++ b/homeassistant/components/tplink/switch.py @@ -6,14 +6,13 @@ from dataclasses import dataclass import logging from typing import Any -from kasa import Device, Feature +from kasa import Feature from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import TPLinkConfigEntry -from .coordinator import TPLinkDataUpdateCoordinator from .entity import ( CoordinatedTPLinkFeatureEntity, TPLinkFeatureEntityDescription, @@ -80,22 +79,6 @@ class TPLinkSwitch(CoordinatedTPLinkFeatureEntity, SwitchEntity): entity_description: TPLinkSwitchEntityDescription - def __init__( - self, - device: Device, - coordinator: TPLinkDataUpdateCoordinator, - *, - feature: Feature, - description: TPLinkSwitchEntityDescription, - parent: Device | None = None, - ) -> None: - """Initialize the switch.""" - super().__init__( - device, coordinator, description=description, feature=feature, parent=parent - ) - - self._async_call_update_attrs() - @async_refresh_after async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" diff --git a/tests/components/tplink/fixtures/features.json b/tests/components/tplink/fixtures/features.json index daf86a74643..7cfe979ea25 100644 --- a/tests/components/tplink/fixtures/features.json +++ b/tests/components/tplink/fixtures/features.json @@ -73,7 +73,7 @@ "value": 121.1, "type": "Sensor", "category": "Primary", - "unit": "v", + "unit": "V", "precision_hint": 1 }, "device_id": { diff --git a/tests/components/tplink/snapshots/test_sensor.ambr b/tests/components/tplink/snapshots/test_sensor.ambr index 46fe897500f..9ea22af45fd 100644 --- a/tests/components/tplink/snapshots/test_sensor.ambr +++ b/tests/components/tplink/snapshots/test_sensor.ambr @@ -770,7 +770,7 @@ 'supported_features': 0, 'translation_key': 'voltage', 'unique_id': '123456789ABCDEFGH_voltage', - 'unit_of_measurement': 'v', + 'unit_of_measurement': 'V', }) # --- # name: test_states[sensor.my_device_voltage-state] @@ -779,7 +779,7 @@ 'device_class': 'voltage', 'friendly_name': 'my_device Voltage', 'state_class': , - 'unit_of_measurement': 'v', + 'unit_of_measurement': 'V', }), 'context': , 'entity_id': 'sensor.my_device_voltage', diff --git a/tests/components/tplink/test_light.py b/tests/components/tplink/test_light.py index c2f40f47e3d..6fce04ec454 100644 --- a/tests/components/tplink/test_light.py +++ b/tests/components/tplink/test_light.py @@ -140,13 +140,17 @@ async def test_color_light( assert state.state == "on" attributes = state.attributes assert attributes[ATTR_BRIGHTNESS] == 128 - assert attributes[ATTR_COLOR_MODE] == "hs" assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["color_temp", "hs"] - assert attributes[ATTR_MIN_MIREDS] == 111 - assert attributes[ATTR_MAX_MIREDS] == 250 - assert attributes[ATTR_HS_COLOR] == (10, 30) - assert attributes[ATTR_RGB_COLOR] == (255, 191, 178) - assert attributes[ATTR_XY_COLOR] == (0.42, 0.336) + # If effect is active, only the brightness can be controlled + if attributes.get(ATTR_EFFECT) is not None: + assert attributes[ATTR_COLOR_MODE] == "brightness" + else: + assert attributes[ATTR_COLOR_MODE] == "hs" + assert attributes[ATTR_MIN_MIREDS] == 111 + assert attributes[ATTR_MAX_MIREDS] == 250 + assert attributes[ATTR_HS_COLOR] == (10, 30) + assert attributes[ATTR_RGB_COLOR] == (255, 191, 178) + assert attributes[ATTR_XY_COLOR] == (0.42, 0.336) await hass.services.async_call( LIGHT_DOMAIN, "turn_off", BASE_PAYLOAD, blocking=True From 3b6acd538042a3b2d4b7c164ae0f992c944c2fb5 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 2 Jul 2024 23:51:14 +1200 Subject: [PATCH 118/146] [ESPHome] Disable dashboard based update entities by default (#120907) Co-authored-by: Paulus Schoutsen --- homeassistant/components/esphome/update.py | 1 + tests/components/esphome/test_update.py | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/homeassistant/components/esphome/update.py b/homeassistant/components/esphome/update.py index cb3d36dab9d..e86c88ddf5b 100644 --- a/homeassistant/components/esphome/update.py +++ b/homeassistant/components/esphome/update.py @@ -97,6 +97,7 @@ class ESPHomeDashboardUpdateEntity( _attr_title = "ESPHome" _attr_name = "Firmware" _attr_release_url = "https://esphome.io/changelog/" + _attr_entity_registry_enabled_default = False def __init__( self, entry_data: RuntimeEntryData, coordinator: ESPHomeDashboardCoordinator diff --git a/tests/components/esphome/test_update.py b/tests/components/esphome/test_update.py index fc845299142..cca1dd1851f 100644 --- a/tests/components/esphome/test_update.py +++ b/tests/components/esphome/test_update.py @@ -33,6 +33,11 @@ from homeassistant.exceptions import HomeAssistantError from .conftest import MockESPHomeDevice +@pytest.fixture(autouse=True) +def enable_entity(entity_registry_enabled_by_default: None) -> None: + """Enable update entity.""" + + @pytest.fixture def stub_reconnect(): """Stub reconnect.""" From efd3252849aa3e4a9d2994bb623f6952ba834c49 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 2 Jul 2024 15:48:35 +0200 Subject: [PATCH 119/146] Create log files in an executor thread (#120912) --- homeassistant/bootstrap.py | 59 +++++++++++++++++++++----------------- tests/test_bootstrap.py | 28 +++++++++--------- 2 files changed, 47 insertions(+), 40 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 8435fe73d40..c5229634053 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -8,7 +8,7 @@ import contextlib from functools import partial from itertools import chain import logging -import logging.handlers +from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler import mimetypes from operator import contains, itemgetter import os @@ -257,12 +257,12 @@ async def async_setup_hass( ) -> core.HomeAssistant | None: """Set up Home Assistant.""" - def create_hass() -> core.HomeAssistant: + async def create_hass() -> core.HomeAssistant: """Create the hass object and do basic setup.""" hass = core.HomeAssistant(runtime_config.config_dir) loader.async_setup(hass) - async_enable_logging( + await async_enable_logging( hass, runtime_config.verbose, runtime_config.log_rotate_days, @@ -287,7 +287,7 @@ async def async_setup_hass( async with hass.timeout.async_timeout(10): await hass.async_stop() - hass = create_hass() + hass = await create_hass() if runtime_config.skip_pip or runtime_config.skip_pip_packages: _LOGGER.warning( @@ -326,13 +326,13 @@ async def async_setup_hass( if config_dict is None: recovery_mode = True await stop_hass(hass) - hass = create_hass() + hass = await create_hass() elif not basic_setup_success: _LOGGER.warning("Unable to set up core integrations. Activating recovery mode") recovery_mode = True await stop_hass(hass) - hass = create_hass() + hass = await create_hass() elif any(domain not in hass.config.components for domain in CRITICAL_INTEGRATIONS): _LOGGER.warning( @@ -345,7 +345,7 @@ async def async_setup_hass( recovery_mode = True await stop_hass(hass) - hass = create_hass() + hass = await create_hass() if old_logging: hass.data[DATA_LOGGING] = old_logging @@ -523,8 +523,7 @@ async def async_from_config_dict( return hass -@core.callback -def async_enable_logging( +async def async_enable_logging( hass: core.HomeAssistant, verbose: bool = False, log_rotate_days: int | None = None, @@ -607,23 +606,9 @@ def async_enable_logging( if (err_path_exists and os.access(err_log_path, os.W_OK)) or ( not err_path_exists and os.access(err_dir, os.W_OK) ): - err_handler: ( - logging.handlers.RotatingFileHandler - | logging.handlers.TimedRotatingFileHandler + err_handler = await hass.async_add_executor_job( + _create_log_file, err_log_path, log_rotate_days ) - if log_rotate_days: - err_handler = logging.handlers.TimedRotatingFileHandler( - err_log_path, when="midnight", backupCount=log_rotate_days - ) - else: - err_handler = _RotatingFileHandlerWithoutShouldRollOver( - err_log_path, backupCount=1 - ) - - try: - err_handler.doRollover() - except OSError as err: - _LOGGER.error("Error rolling over log file: %s", err) err_handler.setLevel(logging.INFO if verbose else logging.WARNING) err_handler.setFormatter(logging.Formatter(fmt, datefmt=FORMAT_DATETIME)) @@ -640,7 +625,29 @@ def async_enable_logging( async_activate_log_queue_handler(hass) -class _RotatingFileHandlerWithoutShouldRollOver(logging.handlers.RotatingFileHandler): +def _create_log_file( + err_log_path: str, log_rotate_days: int | None +) -> RotatingFileHandler | TimedRotatingFileHandler: + """Create log file and do roll over.""" + err_handler: RotatingFileHandler | TimedRotatingFileHandler + if log_rotate_days: + err_handler = TimedRotatingFileHandler( + err_log_path, when="midnight", backupCount=log_rotate_days + ) + else: + err_handler = _RotatingFileHandlerWithoutShouldRollOver( + err_log_path, backupCount=1 + ) + + try: + err_handler.doRollover() + except OSError as err: + _LOGGER.error("Error rolling over log file: %s", err) + + return err_handler + + +class _RotatingFileHandlerWithoutShouldRollOver(RotatingFileHandler): """RotatingFileHandler that does not check if it should roll over on every log.""" def shouldRollover(self, record: logging.LogRecord) -> bool: diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index ca864006852..56599a15d34 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -70,7 +70,7 @@ def mock_http_start_stop() -> Generator[None]: yield -@patch("homeassistant.bootstrap.async_enable_logging", Mock()) +@patch("homeassistant.bootstrap.async_enable_logging", AsyncMock()) async def test_home_assistant_core_config_validation(hass: HomeAssistant) -> None: """Test if we pass in wrong information for HA conf.""" # Extensive HA conf validation testing is done @@ -94,10 +94,10 @@ async def test_async_enable_logging( side_effect=OSError, ), ): - bootstrap.async_enable_logging(hass) + await bootstrap.async_enable_logging(hass) mock_async_activate_log_queue_handler.assert_called_once() mock_async_activate_log_queue_handler.reset_mock() - bootstrap.async_enable_logging( + await bootstrap.async_enable_logging( hass, log_rotate_days=5, log_file="test.log", @@ -141,7 +141,7 @@ async def test_config_does_not_turn_off_debug(hass: HomeAssistant) -> None: @pytest.mark.parametrize("hass_config", [{"frontend": {}}]) @pytest.mark.usefixtures("mock_hass_config") async def test_asyncio_debug_on_turns_hass_debug_on( - mock_enable_logging: Mock, + mock_enable_logging: AsyncMock, mock_is_virtual_env: Mock, mock_mount_local_lib_path: AsyncMock, mock_ensure_config_exists: AsyncMock, @@ -598,7 +598,7 @@ def mock_is_virtual_env() -> Generator[Mock]: @pytest.fixture -def mock_enable_logging() -> Generator[Mock]: +def mock_enable_logging() -> Generator[AsyncMock]: """Mock enable logging.""" with patch("homeassistant.bootstrap.async_enable_logging") as enable_logging: yield enable_logging @@ -634,7 +634,7 @@ def mock_ensure_config_exists() -> Generator[AsyncMock]: @pytest.mark.parametrize("hass_config", [{"browser": {}, "frontend": {}}]) @pytest.mark.usefixtures("mock_hass_config") async def test_setup_hass( - mock_enable_logging: Mock, + mock_enable_logging: AsyncMock, mock_is_virtual_env: Mock, mock_mount_local_lib_path: AsyncMock, mock_ensure_config_exists: AsyncMock, @@ -687,7 +687,7 @@ async def test_setup_hass( @pytest.mark.parametrize("hass_config", [{"browser": {}, "frontend": {}}]) @pytest.mark.usefixtures("mock_hass_config") async def test_setup_hass_takes_longer_than_log_slow_startup( - mock_enable_logging: Mock, + mock_enable_logging: AsyncMock, mock_is_virtual_env: Mock, mock_mount_local_lib_path: AsyncMock, mock_ensure_config_exists: AsyncMock, @@ -728,7 +728,7 @@ async def test_setup_hass_takes_longer_than_log_slow_startup( async def test_setup_hass_invalid_yaml( - mock_enable_logging: Mock, + mock_enable_logging: AsyncMock, mock_is_virtual_env: Mock, mock_mount_local_lib_path: AsyncMock, mock_ensure_config_exists: AsyncMock, @@ -755,7 +755,7 @@ async def test_setup_hass_invalid_yaml( async def test_setup_hass_config_dir_nonexistent( - mock_enable_logging: Mock, + mock_enable_logging: AsyncMock, mock_is_virtual_env: Mock, mock_mount_local_lib_path: AsyncMock, mock_ensure_config_exists: AsyncMock, @@ -781,7 +781,7 @@ async def test_setup_hass_config_dir_nonexistent( async def test_setup_hass_recovery_mode( - mock_enable_logging: Mock, + mock_enable_logging: AsyncMock, mock_is_virtual_env: Mock, mock_mount_local_lib_path: AsyncMock, mock_ensure_config_exists: AsyncMock, @@ -817,7 +817,7 @@ async def test_setup_hass_recovery_mode( @pytest.mark.usefixtures("mock_hass_config") async def test_setup_hass_safe_mode( - mock_enable_logging: Mock, + mock_enable_logging: AsyncMock, mock_is_virtual_env: Mock, mock_mount_local_lib_path: AsyncMock, mock_ensure_config_exists: AsyncMock, @@ -852,7 +852,7 @@ async def test_setup_hass_safe_mode( @pytest.mark.usefixtures("mock_hass_config") async def test_setup_hass_recovery_mode_and_safe_mode( - mock_enable_logging: Mock, + mock_enable_logging: AsyncMock, mock_is_virtual_env: Mock, mock_mount_local_lib_path: AsyncMock, mock_ensure_config_exists: AsyncMock, @@ -888,7 +888,7 @@ async def test_setup_hass_recovery_mode_and_safe_mode( @pytest.mark.parametrize("hass_config", [{"homeassistant": {"non-existing": 1}}]) @pytest.mark.usefixtures("mock_hass_config") async def test_setup_hass_invalid_core_config( - mock_enable_logging: Mock, + mock_enable_logging: AsyncMock, mock_is_virtual_env: Mock, mock_mount_local_lib_path: AsyncMock, mock_ensure_config_exists: AsyncMock, @@ -927,7 +927,7 @@ async def test_setup_hass_invalid_core_config( ) @pytest.mark.usefixtures("mock_hass_config") async def test_setup_recovery_mode_if_no_frontend( - mock_enable_logging: Mock, + mock_enable_logging: AsyncMock, mock_is_virtual_env: Mock, mock_mount_local_lib_path: AsyncMock, mock_ensure_config_exists: AsyncMock, From de458493f895be7fce2a194eea19db3dbd0c1907 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 1 Jul 2024 20:36:35 +0200 Subject: [PATCH 120/146] Fix missing airgradient string (#120957) --- homeassistant/components/airgradient/strings.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/airgradient/strings.json b/homeassistant/components/airgradient/strings.json index 12049e7b720..6bf7242f2f1 100644 --- a/homeassistant/components/airgradient/strings.json +++ b/homeassistant/components/airgradient/strings.json @@ -16,6 +16,7 @@ }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "invalid_version": "This firmware version is unsupported. Please upgrade the firmware of the device to at least version 3.1.1." }, "error": { From 23b905b4226ec00d4432c3ce89cd9ce9b685b607 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 2 Jul 2024 08:23:31 +0200 Subject: [PATCH 121/146] Bump airgradient to 0.6.1 (#120962) --- homeassistant/components/airgradient/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airgradient/manifest.json b/homeassistant/components/airgradient/manifest.json index 7b892c4658a..d523aa4ca03 100644 --- a/homeassistant/components/airgradient/manifest.json +++ b/homeassistant/components/airgradient/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/airgradient", "integration_type": "device", "iot_class": "local_polling", - "requirements": ["airgradient==0.6.0"], + "requirements": ["airgradient==0.6.1"], "zeroconf": ["_airgradient._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index c18ed2f439a..2683ff24549 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -413,7 +413,7 @@ aiowithings==3.0.2 aioymaps==1.2.2 # homeassistant.components.airgradient -airgradient==0.6.0 +airgradient==0.6.1 # homeassistant.components.airly airly==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6291a3dddca..72c0b47ad61 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -386,7 +386,7 @@ aiowithings==3.0.2 aioymaps==1.2.2 # homeassistant.components.airgradient -airgradient==0.6.0 +airgradient==0.6.1 # homeassistant.components.airly airly==1.1.0 From 65d2ca53cb25209dc207b314818b3aa6074d71bd Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 2 Jul 2024 09:25:33 +0200 Subject: [PATCH 122/146] Bump reolink-aio to 0.9.4 (#120964) --- homeassistant/components/reolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index 172a43a91b3..ee3ebe8a13a 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -18,5 +18,5 @@ "documentation": "https://www.home-assistant.io/integrations/reolink", "iot_class": "local_push", "loggers": ["reolink_aio"], - "requirements": ["reolink-aio==0.9.3"] + "requirements": ["reolink-aio==0.9.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2683ff24549..93d38bf3b73 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2460,7 +2460,7 @@ renault-api==0.2.4 renson-endura-delta==1.7.1 # homeassistant.components.reolink -reolink-aio==0.9.3 +reolink-aio==0.9.4 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 72c0b47ad61..9a5c062d76e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1924,7 +1924,7 @@ renault-api==0.2.4 renson-endura-delta==1.7.1 # homeassistant.components.reolink -reolink-aio==0.9.3 +reolink-aio==0.9.4 # homeassistant.components.rflink rflink==0.0.66 From 24afbde79e3caad272ff46ac1a5cf4b9f656373f Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 2 Jul 2024 12:35:10 +0200 Subject: [PATCH 123/146] Bump yt-dlp to 2024.07.01 (#120978) --- homeassistant/components/media_extractor/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index 7ed4e93bb56..cfe44f5176b 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -8,6 +8,6 @@ "iot_class": "calculated", "loggers": ["yt_dlp"], "quality_scale": "internal", - "requirements": ["yt-dlp==2024.05.27"], + "requirements": ["yt-dlp==2024.07.01"], "single_config_entry": true } diff --git a/requirements_all.txt b/requirements_all.txt index 93d38bf3b73..7ba781583f5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2951,7 +2951,7 @@ youless-api==2.1.0 youtubeaio==1.1.5 # homeassistant.components.media_extractor -yt-dlp==2024.05.27 +yt-dlp==2024.07.01 # homeassistant.components.zamg zamg==0.3.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9a5c062d76e..65f9b4b1770 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2307,7 +2307,7 @@ youless-api==2.1.0 youtubeaio==1.1.5 # homeassistant.components.media_extractor -yt-dlp==2024.05.27 +yt-dlp==2024.07.01 # homeassistant.components.zamg zamg==0.3.6 From 98a2e46d4ac10fb9874108c9a74f0cc2bdf11b50 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 2 Jul 2024 13:51:44 +0200 Subject: [PATCH 124/146] Remove Aladdin Connect integration (#120980) --- .coveragerc | 5 - CODEOWNERS | 2 - .../components/aladdin_connect/__init__.py | 108 ++------ .../components/aladdin_connect/api.py | 33 --- .../application_credentials.py | 14 -- .../components/aladdin_connect/config_flow.py | 71 +----- .../components/aladdin_connect/const.py | 6 - .../components/aladdin_connect/coordinator.py | 38 --- .../components/aladdin_connect/cover.py | 84 ------- .../components/aladdin_connect/entity.py | 27 -- .../components/aladdin_connect/manifest.json | 8 +- .../components/aladdin_connect/ruff.toml | 5 - .../components/aladdin_connect/sensor.py | 80 ------ .../components/aladdin_connect/strings.json | 29 +-- .../generated/application_credentials.py | 1 - homeassistant/generated/config_flows.py | 1 - homeassistant/generated/integrations.json | 6 - tests/components/aladdin_connect/conftest.py | 29 --- .../aladdin_connect/test_config_flow.py | 230 ------------------ tests/components/aladdin_connect/test_init.py | 50 ++++ 20 files changed, 89 insertions(+), 738 deletions(-) delete mode 100644 homeassistant/components/aladdin_connect/api.py delete mode 100644 homeassistant/components/aladdin_connect/application_credentials.py delete mode 100644 homeassistant/components/aladdin_connect/const.py delete mode 100644 homeassistant/components/aladdin_connect/coordinator.py delete mode 100644 homeassistant/components/aladdin_connect/cover.py delete mode 100644 homeassistant/components/aladdin_connect/entity.py delete mode 100644 homeassistant/components/aladdin_connect/ruff.toml delete mode 100644 homeassistant/components/aladdin_connect/sensor.py delete mode 100644 tests/components/aladdin_connect/conftest.py delete mode 100644 tests/components/aladdin_connect/test_config_flow.py create mode 100644 tests/components/aladdin_connect/test_init.py diff --git a/.coveragerc b/.coveragerc index 0784977ff55..99a48360b41 100644 --- a/.coveragerc +++ b/.coveragerc @@ -58,11 +58,6 @@ omit = homeassistant/components/airvisual/sensor.py homeassistant/components/airvisual_pro/__init__.py homeassistant/components/airvisual_pro/sensor.py - homeassistant/components/aladdin_connect/__init__.py - homeassistant/components/aladdin_connect/api.py - homeassistant/components/aladdin_connect/application_credentials.py - homeassistant/components/aladdin_connect/cover.py - homeassistant/components/aladdin_connect/sensor.py homeassistant/components/alarmdecoder/__init__.py homeassistant/components/alarmdecoder/alarm_control_panel.py homeassistant/components/alarmdecoder/binary_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 7834add43f6..765f1624c33 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -80,8 +80,6 @@ build.json @home-assistant/supervisor /tests/components/airzone/ @Noltari /homeassistant/components/airzone_cloud/ @Noltari /tests/components/airzone_cloud/ @Noltari -/homeassistant/components/aladdin_connect/ @swcloudgenie -/tests/components/aladdin_connect/ @swcloudgenie /homeassistant/components/alarm_control_panel/ @home-assistant/core /tests/components/alarm_control_panel/ @home-assistant/core /homeassistant/components/alert/ @home-assistant/core @frenck diff --git a/homeassistant/components/aladdin_connect/__init__.py b/homeassistant/components/aladdin_connect/__init__.py index ed284c0e6bb..6d3f1d642b5 100644 --- a/homeassistant/components/aladdin_connect/__init__.py +++ b/homeassistant/components/aladdin_connect/__init__.py @@ -1,94 +1,38 @@ """The Aladdin Connect Genie integration.""" -# mypy: ignore-errors from __future__ import annotations -# from genie_partner_sdk.client import AladdinConnectClient -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import Platform +from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.config_entry_oauth2_flow import ( - OAuth2Session, - async_get_config_entry_implementation, -) +from homeassistant.helpers import issue_registry as ir -from .api import AsyncConfigEntryAuth -from .const import DOMAIN -from .coordinator import AladdinConnectCoordinator - -PLATFORMS: list[Platform] = [Platform.COVER, Platform.SENSOR] - -type AladdinConnectConfigEntry = ConfigEntry[AladdinConnectCoordinator] +DOMAIN = "aladdin_connect" -async def async_setup_entry( - hass: HomeAssistant, entry: AladdinConnectConfigEntry -) -> bool: - """Set up Aladdin Connect Genie from a config entry.""" - implementation = await async_get_config_entry_implementation(hass, entry) - - session = OAuth2Session(hass, entry, implementation) - auth = AsyncConfigEntryAuth(async_get_clientsession(hass), session) - coordinator = AladdinConnectCoordinator(hass, AladdinConnectClient(auth)) - - await coordinator.async_setup() - await coordinator.async_config_entry_first_refresh() - - entry.runtime_data = coordinator - - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - - async_remove_stale_devices(hass, entry) - - return True - - -async def async_unload_entry( - hass: HomeAssistant, entry: AladdinConnectConfigEntry -) -> bool: - """Unload a config entry.""" - return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - - -async def async_migrate_entry( - hass: HomeAssistant, config_entry: AladdinConnectConfigEntry -) -> bool: - """Migrate old config.""" - if config_entry.version < 2: - config_entry.async_start_reauth(hass) - hass.config_entries.async_update_entry( - config_entry, - version=2, - minor_version=1, - ) - - return True - - -def async_remove_stale_devices( - hass: HomeAssistant, config_entry: AladdinConnectConfigEntry -) -> None: - """Remove stale devices from device registry.""" - device_registry = dr.async_get(hass) - device_entries = dr.async_entries_for_config_entry( - device_registry, config_entry.entry_id +async def async_setup_entry(hass: HomeAssistant, _: ConfigEntry) -> bool: + """Set up Aladdin Connect from a config entry.""" + ir.async_create_issue( + hass, + DOMAIN, + DOMAIN, + is_fixable=False, + severity=ir.IssueSeverity.ERROR, + translation_key="integration_removed", + translation_placeholders={ + "entries": "/config/integrations/integration/aladdin_connect", + }, ) - all_device_ids = {door.unique_id for door in config_entry.runtime_data.doors} - for device_entry in device_entries: - device_id: str | None = None + return True - for identifier in device_entry.identifiers: - if identifier[0] == DOMAIN: - device_id = identifier[1] - break - if device_id is None or device_id not in all_device_ids: - # If device_id is None an invalid device entry was found for this config entry. - # If the device_id is not in existing device ids it's a stale device entry. - # Remove config entry from this device entry in either case. - device_registry.async_update_device( - device_entry.id, remove_config_entry_id=config_entry.entry_id - ) +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if all( + config_entry.state is ConfigEntryState.NOT_LOADED + for config_entry in hass.config_entries.async_entries(DOMAIN) + if config_entry.entry_id != entry.entry_id + ): + ir.async_delete_issue(hass, DOMAIN, DOMAIN) + + return True diff --git a/homeassistant/components/aladdin_connect/api.py b/homeassistant/components/aladdin_connect/api.py deleted file mode 100644 index 4377fc8fbcb..00000000000 --- a/homeassistant/components/aladdin_connect/api.py +++ /dev/null @@ -1,33 +0,0 @@ -"""API for Aladdin Connect Genie bound to Home Assistant OAuth.""" - -# mypy: ignore-errors -from typing import cast - -from aiohttp import ClientSession - -# from genie_partner_sdk.auth import Auth -from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session - -API_URL = "https://twdvzuefzh.execute-api.us-east-2.amazonaws.com/v1" -API_KEY = "k6QaiQmcTm2zfaNns5L1Z8duBtJmhDOW8JawlCC3" - - -class AsyncConfigEntryAuth(Auth): # type: ignore[misc] - """Provide Aladdin Connect Genie authentication tied to an OAuth2 based config entry.""" - - def __init__( - self, - websession: ClientSession, - oauth_session: OAuth2Session, - ) -> None: - """Initialize Aladdin Connect Genie auth.""" - super().__init__( - websession, API_URL, oauth_session.token["access_token"], API_KEY - ) - self._oauth_session = oauth_session - - async def async_get_access_token(self) -> str: - """Return a valid access token.""" - await self._oauth_session.async_ensure_token_valid() - - return cast(str, self._oauth_session.token["access_token"]) diff --git a/homeassistant/components/aladdin_connect/application_credentials.py b/homeassistant/components/aladdin_connect/application_credentials.py deleted file mode 100644 index e8e959f1fa3..00000000000 --- a/homeassistant/components/aladdin_connect/application_credentials.py +++ /dev/null @@ -1,14 +0,0 @@ -"""application_credentials platform the Aladdin Connect Genie integration.""" - -from homeassistant.components.application_credentials import AuthorizationServer -from homeassistant.core import HomeAssistant - -from .const import OAUTH2_AUTHORIZE, OAUTH2_TOKEN - - -async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationServer: - """Return authorization server.""" - return AuthorizationServer( - authorize_url=OAUTH2_AUTHORIZE, - token_url=OAUTH2_TOKEN, - ) diff --git a/homeassistant/components/aladdin_connect/config_flow.py b/homeassistant/components/aladdin_connect/config_flow.py index 507085fa27f..a508ff89c68 100644 --- a/homeassistant/components/aladdin_connect/config_flow.py +++ b/homeassistant/components/aladdin_connect/config_flow.py @@ -1,70 +1,11 @@ -"""Config flow for Aladdin Connect Genie.""" +"""Config flow for Aladdin Connect integration.""" -from collections.abc import Mapping -import logging -from typing import Any +from homeassistant.config_entries import ConfigFlow -import jwt - -from homeassistant.config_entries import ConfigEntry, ConfigFlowResult -from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN -from homeassistant.helpers.config_entry_oauth2_flow import AbstractOAuth2FlowHandler - -from .const import DOMAIN +from . import DOMAIN -class AladdinConnectOAuth2FlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): - """Config flow to handle Aladdin Connect Genie OAuth2 authentication.""" +class AladdinConnectConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Aladdin Connect.""" - DOMAIN = DOMAIN - VERSION = 2 - MINOR_VERSION = 1 - - reauth_entry: ConfigEntry | None = None - - async def async_step_reauth( - self, user_input: Mapping[str, Any] - ) -> ConfigFlowResult: - """Perform reauth upon API auth error or upgrade from v1 to v2.""" - self.reauth_entry = self.hass.config_entries.async_get_entry( - self.context["entry_id"] - ) - return await self.async_step_reauth_confirm() - - async def async_step_reauth_confirm( - self, user_input: Mapping[str, Any] | None = None - ) -> ConfigFlowResult: - """Dialog that informs the user that reauth is required.""" - if user_input is None: - return self.async_show_form(step_id="reauth_confirm") - return await self.async_step_user() - - async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult: - """Create an oauth config entry or update existing entry for reauth.""" - token_payload = jwt.decode( - data[CONF_TOKEN][CONF_ACCESS_TOKEN], options={"verify_signature": False} - ) - if not self.reauth_entry: - await self.async_set_unique_id(token_payload["sub"]) - self._abort_if_unique_id_configured() - - return self.async_create_entry( - title=token_payload["username"], - data=data, - ) - - if self.reauth_entry.unique_id == token_payload["username"]: - return self.async_update_reload_and_abort( - self.reauth_entry, - data=data, - unique_id=token_payload["sub"], - ) - if self.reauth_entry.unique_id == token_payload["sub"]: - return self.async_update_reload_and_abort(self.reauth_entry, data=data) - - return self.async_abort(reason="wrong_account") - - @property - def logger(self) -> logging.Logger: - """Return logger.""" - return logging.getLogger(__name__) + VERSION = 1 diff --git a/homeassistant/components/aladdin_connect/const.py b/homeassistant/components/aladdin_connect/const.py deleted file mode 100644 index a87147c8f09..00000000000 --- a/homeassistant/components/aladdin_connect/const.py +++ /dev/null @@ -1,6 +0,0 @@ -"""Constants for the Aladdin Connect Genie integration.""" - -DOMAIN = "aladdin_connect" - -OAUTH2_AUTHORIZE = "https://app.aladdinconnect.net/login.html" -OAUTH2_TOKEN = "https://twdvzuefzh.execute-api.us-east-2.amazonaws.com/v1/oauth2/token" diff --git a/homeassistant/components/aladdin_connect/coordinator.py b/homeassistant/components/aladdin_connect/coordinator.py deleted file mode 100644 index 9af3e330409..00000000000 --- a/homeassistant/components/aladdin_connect/coordinator.py +++ /dev/null @@ -1,38 +0,0 @@ -"""Define an object to coordinate fetching Aladdin Connect data.""" - -# mypy: ignore-errors -from datetime import timedelta -import logging - -# from genie_partner_sdk.client import AladdinConnectClient -# from genie_partner_sdk.model import GarageDoor -from homeassistant.core import HomeAssistant -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator - -from .const import DOMAIN - -_LOGGER = logging.getLogger(__name__) - - -class AladdinConnectCoordinator(DataUpdateCoordinator[None]): - """Aladdin Connect Data Update Coordinator.""" - - def __init__(self, hass: HomeAssistant, acc: AladdinConnectClient) -> None: - """Initialize.""" - super().__init__( - hass, - logger=_LOGGER, - name=DOMAIN, - update_interval=timedelta(seconds=15), - ) - self.acc = acc - self.doors: list[GarageDoor] = [] - - async def async_setup(self) -> None: - """Fetch initial data.""" - self.doors = await self.acc.get_doors() - - async def _async_update_data(self) -> None: - """Fetch data from API endpoint.""" - for door in self.doors: - await self.acc.update_door(door.device_id, door.door_number) diff --git a/homeassistant/components/aladdin_connect/cover.py b/homeassistant/components/aladdin_connect/cover.py deleted file mode 100644 index 1be41e6b516..00000000000 --- a/homeassistant/components/aladdin_connect/cover.py +++ /dev/null @@ -1,84 +0,0 @@ -"""Cover Entity for Genie Garage Door.""" - -# mypy: ignore-errors -from typing import Any - -# from genie_partner_sdk.model import GarageDoor -from homeassistant.components.cover import ( - CoverDeviceClass, - CoverEntity, - CoverEntityFeature, -) -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from . import AladdinConnectConfigEntry, AladdinConnectCoordinator -from .entity import AladdinConnectEntity - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: AladdinConnectConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up the Aladdin Connect platform.""" - coordinator = config_entry.runtime_data - - async_add_entities(AladdinDevice(coordinator, door) for door in coordinator.doors) - - -class AladdinDevice(AladdinConnectEntity, CoverEntity): - """Representation of Aladdin Connect cover.""" - - _attr_device_class = CoverDeviceClass.GARAGE - _attr_supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE - _attr_name = None - - def __init__( - self, coordinator: AladdinConnectCoordinator, device: GarageDoor - ) -> None: - """Initialize the Aladdin Connect cover.""" - super().__init__(coordinator, device) - self._attr_unique_id = device.unique_id - - async def async_open_cover(self, **kwargs: Any) -> None: - """Issue open command to cover.""" - await self.coordinator.acc.open_door( - self._device.device_id, self._device.door_number - ) - - async def async_close_cover(self, **kwargs: Any) -> None: - """Issue close command to cover.""" - await self.coordinator.acc.close_door( - self._device.device_id, self._device.door_number - ) - - @property - def is_closed(self) -> bool | None: - """Update is closed attribute.""" - value = self.coordinator.acc.get_door_status( - self._device.device_id, self._device.door_number - ) - if value is None: - return None - return bool(value == "closed") - - @property - def is_closing(self) -> bool | None: - """Update is closing attribute.""" - value = self.coordinator.acc.get_door_status( - self._device.device_id, self._device.door_number - ) - if value is None: - return None - return bool(value == "closing") - - @property - def is_opening(self) -> bool | None: - """Update is opening attribute.""" - value = self.coordinator.acc.get_door_status( - self._device.device_id, self._device.door_number - ) - if value is None: - return None - return bool(value == "opening") diff --git a/homeassistant/components/aladdin_connect/entity.py b/homeassistant/components/aladdin_connect/entity.py deleted file mode 100644 index 2615cbc636e..00000000000 --- a/homeassistant/components/aladdin_connect/entity.py +++ /dev/null @@ -1,27 +0,0 @@ -"""Defines a base Aladdin Connect entity.""" -# mypy: ignore-errors -# from genie_partner_sdk.model import GarageDoor - -from homeassistant.helpers.device_registry import DeviceInfo -from homeassistant.helpers.update_coordinator import CoordinatorEntity - -from .const import DOMAIN -from .coordinator import AladdinConnectCoordinator - - -class AladdinConnectEntity(CoordinatorEntity[AladdinConnectCoordinator]): - """Defines a base Aladdin Connect entity.""" - - _attr_has_entity_name = True - - def __init__( - self, coordinator: AladdinConnectCoordinator, device: GarageDoor - ) -> None: - """Initialize the entity.""" - super().__init__(coordinator) - self._device = device - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, device.unique_id)}, - name=device.name, - manufacturer="Overhead Door", - ) diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index dce95492272..adf0d9c9b5b 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -1,11 +1,9 @@ { "domain": "aladdin_connect", "name": "Aladdin Connect", - "codeowners": ["@swcloudgenie"], - "config_flow": true, - "dependencies": ["application_credentials"], - "disabled": "This integration is disabled because it uses non-open source code to operate.", + "codeowners": [], "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", + "integration_type": "system", "iot_class": "cloud_polling", - "requirements": ["genie-partner-sdk==1.0.2"] + "requirements": [] } diff --git a/homeassistant/components/aladdin_connect/ruff.toml b/homeassistant/components/aladdin_connect/ruff.toml deleted file mode 100644 index 38f6f586aef..00000000000 --- a/homeassistant/components/aladdin_connect/ruff.toml +++ /dev/null @@ -1,5 +0,0 @@ -extend = "../../../pyproject.toml" - -lint.extend-ignore = [ - "F821" -] diff --git a/homeassistant/components/aladdin_connect/sensor.py b/homeassistant/components/aladdin_connect/sensor.py deleted file mode 100644 index cd1fff12c97..00000000000 --- a/homeassistant/components/aladdin_connect/sensor.py +++ /dev/null @@ -1,80 +0,0 @@ -"""Support for Aladdin Connect Garage Door sensors.""" - -# mypy: ignore-errors -from __future__ import annotations - -from collections.abc import Callable -from dataclasses import dataclass - -# from genie_partner_sdk.client import AladdinConnectClient -# from genie_partner_sdk.model import GarageDoor -from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorEntity, - SensorEntityDescription, - SensorStateClass, -) -from homeassistant.const import PERCENTAGE -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from . import AladdinConnectConfigEntry, AladdinConnectCoordinator -from .entity import AladdinConnectEntity - - -@dataclass(frozen=True, kw_only=True) -class AccSensorEntityDescription(SensorEntityDescription): - """Describes AladdinConnect sensor entity.""" - - value_fn: Callable[[AladdinConnectClient, str, int], float | None] - - -SENSORS: tuple[AccSensorEntityDescription, ...] = ( - AccSensorEntityDescription( - key="battery_level", - device_class=SensorDeviceClass.BATTERY, - entity_registry_enabled_default=False, - native_unit_of_measurement=PERCENTAGE, - state_class=SensorStateClass.MEASUREMENT, - value_fn=AladdinConnectClient.get_battery_status, - ), -) - - -async def async_setup_entry( - hass: HomeAssistant, - entry: AladdinConnectConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up Aladdin Connect sensor devices.""" - coordinator = entry.runtime_data - - async_add_entities( - AladdinConnectSensor(coordinator, door, description) - for description in SENSORS - for door in coordinator.doors - ) - - -class AladdinConnectSensor(AladdinConnectEntity, SensorEntity): - """A sensor implementation for Aladdin Connect devices.""" - - entity_description: AccSensorEntityDescription - - def __init__( - self, - coordinator: AladdinConnectCoordinator, - device: GarageDoor, - description: AccSensorEntityDescription, - ) -> None: - """Initialize a sensor for an Aladdin Connect device.""" - super().__init__(coordinator, device) - self.entity_description = description - self._attr_unique_id = f"{device.unique_id}-{description.key}" - - @property - def native_value(self) -> float | None: - """Return the state of the sensor.""" - return self.entity_description.value_fn( - self.coordinator.acc, self._device.device_id, self._device.door_number - ) diff --git a/homeassistant/components/aladdin_connect/strings.json b/homeassistant/components/aladdin_connect/strings.json index 48f9b299a1d..f62e68de64e 100644 --- a/homeassistant/components/aladdin_connect/strings.json +++ b/homeassistant/components/aladdin_connect/strings.json @@ -1,29 +1,8 @@ { - "config": { - "step": { - "pick_implementation": { - "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" - }, - "reauth_confirm": { - "title": "[%key:common::config_flow::title::reauth%]", - "description": "Aladdin Connect needs to re-authenticate your account" - } - }, - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", - "oauth_error": "[%key:common::config_flow::abort::oauth2_error%]", - "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]", - "oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]", - "oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]", - "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", - "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", - "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", - "user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]", - "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" - }, - "create_entry": { - "default": "[%key:common::config_flow::create_entry::authenticated%]" + "issues": { + "integration_removed": { + "title": "The Aladdin Connect integration has been removed", + "description": "The Aladdin Connect integration has been removed from Home Assistant.\n\nTo resolve this issue, please remove the (now defunct) integration entries from your Home Assistant setup. [Click here to see your existing Aladdin Connect integration entries]({entries})." } } } diff --git a/homeassistant/generated/application_credentials.py b/homeassistant/generated/application_credentials.py index bc6b29e4c23..c576f242e30 100644 --- a/homeassistant/generated/application_credentials.py +++ b/homeassistant/generated/application_credentials.py @@ -4,7 +4,6 @@ To update, run python3 -m script.hassfest """ APPLICATION_CREDENTIALS = [ - "aladdin_connect", "electric_kiwi", "fitbit", "geocaching", diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 23a13bcbfd8..463a38feb9f 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -42,7 +42,6 @@ FLOWS = { "airvisual_pro", "airzone", "airzone_cloud", - "aladdin_connect", "alarmdecoder", "amberelectric", "ambient_network", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 3371c8de0fa..0ad8ac09c9e 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -180,12 +180,6 @@ } } }, - "aladdin_connect": { - "name": "Aladdin Connect", - "integration_type": "hub", - "config_flow": true, - "iot_class": "cloud_polling" - }, "alarmdecoder": { "name": "AlarmDecoder", "integration_type": "device", diff --git a/tests/components/aladdin_connect/conftest.py b/tests/components/aladdin_connect/conftest.py deleted file mode 100644 index 2c158998f49..00000000000 --- a/tests/components/aladdin_connect/conftest.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Test fixtures for the Aladdin Connect Garage Door integration.""" - -from unittest.mock import AsyncMock, patch - -import pytest -from typing_extensions import Generator - -from tests.common import MockConfigEntry - - -@pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock]: - """Override async_setup_entry.""" - with patch( - "homeassistant.components.aladdin_connect.async_setup_entry", return_value=True - ) as mock_setup_entry: - yield mock_setup_entry - - -@pytest.fixture -def mock_config_entry() -> MockConfigEntry: - """Return an Aladdin Connect config entry.""" - return MockConfigEntry( - domain="aladdin_connect", - data={}, - title="test@test.com", - unique_id="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", - version=2, - ) diff --git a/tests/components/aladdin_connect/test_config_flow.py b/tests/components/aladdin_connect/test_config_flow.py deleted file mode 100644 index 7154c53b9f6..00000000000 --- a/tests/components/aladdin_connect/test_config_flow.py +++ /dev/null @@ -1,230 +0,0 @@ -"""Test the Aladdin Connect Garage Door config flow.""" - -# from unittest.mock import AsyncMock -# -# import pytest -# -# from homeassistant.components.aladdin_connect.const import ( -# DOMAIN, -# OAUTH2_AUTHORIZE, -# OAUTH2_TOKEN, -# ) -# from homeassistant.components.application_credentials import ( -# ClientCredential, -# async_import_client_credential, -# ) -# from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER, ConfigFlowResult -# from homeassistant.core import HomeAssistant -# from homeassistant.data_entry_flow import FlowResultType -# from homeassistant.helpers import config_entry_oauth2_flow -# from homeassistant.setup import async_setup_component -# -# from tests.common import MockConfigEntry -# from tests.test_util.aiohttp import AiohttpClientMocker -# from tests.typing import ClientSessionGenerator -# -# CLIENT_ID = "1234" -# CLIENT_SECRET = "5678" -# -# EXAMPLE_TOKEN = ( -# "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhYWFhYWFhYS1iYmJiLWNjY2MtZGRk" -# "ZC1lZWVlZWVlZWVlZWUiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjIsInVzZXJuYW" -# "1lIjoidGVzdEB0ZXN0LmNvbSJ9.CTU1YItIrUl8nSM3koJxlFJr5CjLghgc9gS6h45D8dE" -# ) -# -# -# @pytest.fixture -# async def setup_credentials(hass: HomeAssistant) -> None: -# """Fixture to setup credentials.""" -# assert await async_setup_component(hass, "application_credentials", {}) -# await async_import_client_credential( -# hass, -# DOMAIN, -# ClientCredential(CLIENT_ID, CLIENT_SECRET), -# ) -# -# -# async def _oauth_actions( -# hass: HomeAssistant, -# result: ConfigFlowResult, -# hass_client_no_auth: ClientSessionGenerator, -# aioclient_mock: AiohttpClientMocker, -# ) -> None: -# state = config_entry_oauth2_flow._encode_jwt( -# hass, -# { -# "flow_id": result["flow_id"], -# "redirect_uri": "https://example.com/auth/external/callback", -# }, -# ) -# -# assert result["url"] == ( -# f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" -# "&redirect_uri=https://example.com/auth/external/callback" -# f"&state={state}" -# ) -# -# client = await hass_client_no_auth() -# resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") -# assert resp.status == 200 -# assert resp.headers["content-type"] == "text/html; charset=utf-8" -# -# aioclient_mock.post( -# OAUTH2_TOKEN, -# json={ -# "refresh_token": "mock-refresh-token", -# "access_token": EXAMPLE_TOKEN, -# "type": "Bearer", -# "expires_in": 60, -# }, -# ) -# -# -# @pytest.mark.skip(reason="Integration disabled") -# @pytest.mark.usefixtures("current_request_with_host") -# async def test_full_flow( -# hass: HomeAssistant, -# hass_client_no_auth: ClientSessionGenerator, -# aioclient_mock: AiohttpClientMocker, -# setup_credentials: None, -# mock_setup_entry: AsyncMock, -# ) -> None: -# """Check full flow.""" -# result = await hass.config_entries.flow.async_init( -# DOMAIN, context={"source": SOURCE_USER} -# ) -# await _oauth_actions(hass, result, hass_client_no_auth, aioclient_mock) -# -# result = await hass.config_entries.flow.async_configure(result["flow_id"]) -# assert result["type"] is FlowResultType.CREATE_ENTRY -# assert result["title"] == "test@test.com" -# assert result["data"]["token"]["access_token"] == EXAMPLE_TOKEN -# assert result["data"]["token"]["refresh_token"] == "mock-refresh-token" -# assert result["result"].unique_id == "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" -# -# assert len(hass.config_entries.async_entries(DOMAIN)) == 1 -# assert len(mock_setup_entry.mock_calls) == 1 -# -# -# @pytest.mark.skip(reason="Integration disabled") -# @pytest.mark.usefixtures("current_request_with_host") -# async def test_duplicate_entry( -# hass: HomeAssistant, -# hass_client_no_auth: ClientSessionGenerator, -# aioclient_mock: AiohttpClientMocker, -# setup_credentials: None, -# mock_config_entry: MockConfigEntry, -# ) -> None: -# """Test we abort with duplicate entry.""" -# mock_config_entry.add_to_hass(hass) -# result = await hass.config_entries.flow.async_init( -# DOMAIN, context={"source": SOURCE_USER} -# ) -# await _oauth_actions(hass, result, hass_client_no_auth, aioclient_mock) -# -# result = await hass.config_entries.flow.async_configure(result["flow_id"]) -# assert result["type"] is FlowResultType.ABORT -# assert result["reason"] == "already_configured" -# -# -# @pytest.mark.skip(reason="Integration disabled") -# @pytest.mark.usefixtures("current_request_with_host") -# async def test_reauth( -# hass: HomeAssistant, -# hass_client_no_auth: ClientSessionGenerator, -# aioclient_mock: AiohttpClientMocker, -# setup_credentials: None, -# mock_config_entry: MockConfigEntry, -# mock_setup_entry: AsyncMock, -# ) -> None: -# """Test reauthentication.""" -# mock_config_entry.add_to_hass(hass) -# result = await hass.config_entries.flow.async_init( -# DOMAIN, -# context={ -# "source": SOURCE_REAUTH, -# "entry_id": mock_config_entry.entry_id, -# }, -# data=mock_config_entry.data, -# ) -# assert result["type"] is FlowResultType.FORM -# assert result["step_id"] == "reauth_confirm" -# result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) -# await _oauth_actions(hass, result, hass_client_no_auth, aioclient_mock) -# -# result = await hass.config_entries.flow.async_configure(result["flow_id"]) -# assert result["type"] is FlowResultType.ABORT -# assert result["reason"] == "reauth_successful" -# -# -# @pytest.mark.skip(reason="Integration disabled") -# @pytest.mark.usefixtures("current_request_with_host") -# async def test_reauth_wrong_account( -# hass: HomeAssistant, -# hass_client_no_auth: ClientSessionGenerator, -# aioclient_mock: AiohttpClientMocker, -# setup_credentials: None, -# mock_setup_entry: AsyncMock, -# ) -> None: -# """Test reauthentication with wrong account.""" -# config_entry = MockConfigEntry( -# domain=DOMAIN, -# data={}, -# title="test@test.com", -# unique_id="aaaaaaaa-bbbb-ffff-dddd-eeeeeeeeeeee", -# version=2, -# ) -# config_entry.add_to_hass(hass) -# result = await hass.config_entries.flow.async_init( -# DOMAIN, -# context={ -# "source": SOURCE_REAUTH, -# "entry_id": config_entry.entry_id, -# }, -# data=config_entry.data, -# ) -# assert result["type"] is FlowResultType.FORM -# assert result["step_id"] == "reauth_confirm" -# result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) -# await _oauth_actions(hass, result, hass_client_no_auth, aioclient_mock) -# -# result = await hass.config_entries.flow.async_configure(result["flow_id"]) -# assert result["type"] is FlowResultType.ABORT -# assert result["reason"] == "wrong_account" -# -# -# @pytest.mark.skip(reason="Integration disabled") -# @pytest.mark.usefixtures("current_request_with_host") -# async def test_reauth_old_account( -# hass: HomeAssistant, -# hass_client_no_auth: ClientSessionGenerator, -# aioclient_mock: AiohttpClientMocker, -# setup_credentials: None, -# mock_setup_entry: AsyncMock, -# ) -> None: -# """Test reauthentication with old account.""" -# config_entry = MockConfigEntry( -# domain=DOMAIN, -# data={}, -# title="test@test.com", -# unique_id="test@test.com", -# version=2, -# ) -# config_entry.add_to_hass(hass) -# result = await hass.config_entries.flow.async_init( -# DOMAIN, -# context={ -# "source": SOURCE_REAUTH, -# "entry_id": config_entry.entry_id, -# }, -# data=config_entry.data, -# ) -# assert result["type"] is FlowResultType.FORM -# assert result["step_id"] == "reauth_confirm" -# result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) -# await _oauth_actions(hass, result, hass_client_no_auth, aioclient_mock) -# -# result = await hass.config_entries.flow.async_configure(result["flow_id"]) -# assert result["type"] is FlowResultType.ABORT -# assert result["reason"] == "reauth_successful" -# assert config_entry.unique_id == "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" diff --git a/tests/components/aladdin_connect/test_init.py b/tests/components/aladdin_connect/test_init.py new file mode 100644 index 00000000000..b01af287b7b --- /dev/null +++ b/tests/components/aladdin_connect/test_init.py @@ -0,0 +1,50 @@ +"""Tests for the Aladdin Connect integration.""" + +from homeassistant.components.aladdin_connect import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir + +from tests.common import MockConfigEntry + + +async def test_aladdin_connect_repair_issue( + hass: HomeAssistant, issue_registry: ir.IssueRegistry +) -> None: + """Test the Aladdin Connect configuration entry loading/unloading handles the repair.""" + config_entry_1 = MockConfigEntry( + title="Example 1", + domain=DOMAIN, + ) + config_entry_1.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry_1.entry_id) + await hass.async_block_till_done() + assert config_entry_1.state is ConfigEntryState.LOADED + + # Add a second one + config_entry_2 = MockConfigEntry( + title="Example 2", + domain=DOMAIN, + ) + config_entry_2.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry_2.entry_id) + await hass.async_block_till_done() + + assert config_entry_2.state is ConfigEntryState.LOADED + assert issue_registry.async_get_issue(DOMAIN, DOMAIN) + + # Remove the first one + await hass.config_entries.async_remove(config_entry_1.entry_id) + await hass.async_block_till_done() + + assert config_entry_1.state is ConfigEntryState.NOT_LOADED + assert config_entry_2.state is ConfigEntryState.LOADED + assert issue_registry.async_get_issue(DOMAIN, DOMAIN) + + # Remove the second one + await hass.config_entries.async_remove(config_entry_2.entry_id) + await hass.async_block_till_done() + + assert config_entry_1.state is ConfigEntryState.NOT_LOADED + assert config_entry_2.state is ConfigEntryState.NOT_LOADED + assert issue_registry.async_get_issue(DOMAIN, DOMAIN) is None From 5cb41106b5628df354c17b81ec65429f6276c27c Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 2 Jul 2024 13:31:23 +0200 Subject: [PATCH 125/146] Reolink replace automatic removal of devices by manual removal (#120981) Co-authored-by: Robert Resch --- homeassistant/components/reolink/__init__.py | 87 ++++++++++---------- tests/components/reolink/test_init.py | 31 +++++-- 2 files changed, 70 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index 150a23dc64e..02d3cc16419 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -147,9 +147,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b firmware_coordinator=firmware_coordinator, ) - # first migrate and then cleanup, otherwise entities lost migrate_entity_ids(hass, config_entry.entry_id, host) - cleanup_disconnected_cams(hass, config_entry.entry_id, host) await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) @@ -179,6 +177,50 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> return unload_ok +async def async_remove_config_entry_device( + hass: HomeAssistant, config_entry: ConfigEntry, device: dr.DeviceEntry +) -> bool: + """Remove a device from a config entry.""" + host: ReolinkHost = hass.data[DOMAIN][config_entry.entry_id].host + (device_uid, ch) = get_device_uid_and_ch(device, host) + + if not host.api.is_nvr or ch is None: + _LOGGER.warning( + "Cannot remove Reolink device %s, because it is not a camera connected " + "to a NVR/Hub, please remove the integration entry instead", + device.name, + ) + return False # Do not remove the host/NVR itself + + if ch not in host.api.channels: + _LOGGER.debug( + "Removing Reolink device %s, " + "since no camera is connected to NVR channel %s anymore", + device.name, + ch, + ) + return True + + await host.api.get_state(cmd="GetChannelstatus") # update the camera_online status + if not host.api.camera_online(ch): + _LOGGER.debug( + "Removing Reolink device %s, " + "since the camera connected to channel %s is offline", + device.name, + ch, + ) + return True + + _LOGGER.warning( + "Cannot remove Reolink device %s on channel %s, because it is still connected " + "to the NVR/Hub, please first remove the camera from the NVR/Hub " + "in the reolink app", + device.name, + ch, + ) + return False + + def get_device_uid_and_ch( device: dr.DeviceEntry, host: ReolinkHost ) -> tuple[list[str], int | None]: @@ -197,47 +239,6 @@ def get_device_uid_and_ch( return (device_uid, ch) -def cleanup_disconnected_cams( - hass: HomeAssistant, config_entry_id: str, host: ReolinkHost -) -> None: - """Clean-up disconnected camera channels.""" - if not host.api.is_nvr: - return - - device_reg = dr.async_get(hass) - devices = dr.async_entries_for_config_entry(device_reg, config_entry_id) - for device in devices: - (device_uid, ch) = get_device_uid_and_ch(device, host) - if ch is None: - continue # Do not consider the NVR itself - - ch_model = host.api.camera_model(ch) - remove = False - if ch not in host.api.channels: - remove = True - _LOGGER.debug( - "Removing Reolink device %s, " - "since no camera is connected to NVR channel %s anymore", - device.name, - ch, - ) - if ch_model not in [device.model, "Unknown"]: - remove = True - _LOGGER.debug( - "Removing Reolink device %s, " - "since the camera model connected to channel %s changed from %s to %s", - device.name, - ch, - device.model, - ch_model, - ) - if not remove: - continue - - # clean device registry and associated entities - device_reg.async_remove_device(device.id) - - def migrate_entity_ids( hass: HomeAssistant, config_entry_id: str, host: ReolinkHost ) -> None: diff --git a/tests/components/reolink/test_init.py b/tests/components/reolink/test_init.py index a6c798f9415..f70fd312051 100644 --- a/tests/components/reolink/test_init.py +++ b/tests/components/reolink/test_init.py @@ -36,6 +36,7 @@ from .conftest import ( ) from tests.common import MockConfigEntry, async_fire_time_changed +from tests.typing import WebSocketGenerator pytestmark = pytest.mark.usefixtures("reolink_connect", "reolink_platforms") @@ -179,16 +180,27 @@ async def test_entry_reloading( None, [TEST_HOST_MODEL, TEST_CAM_MODEL], ), + ( + "is_nvr", + False, + [TEST_HOST_MODEL, TEST_CAM_MODEL], + ), ("channels", [], [TEST_HOST_MODEL]), ( - "camera_model", - Mock(return_value="RLC-567"), - [TEST_HOST_MODEL, "RLC-567"], + "camera_online", + Mock(return_value=False), + [TEST_HOST_MODEL], + ), + ( + "channel_for_uid", + Mock(return_value=-1), + [TEST_HOST_MODEL], ), ], ) -async def test_cleanup_disconnected_cams( +async def test_removing_disconnected_cams( hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, config_entry: MockConfigEntry, reolink_connect: MagicMock, device_registry: dr.DeviceRegistry, @@ -197,8 +209,10 @@ async def test_cleanup_disconnected_cams( value: Any, expected_models: list[str], ) -> None: - """Test device and entity registry are cleaned up when camera is disconnected from NVR.""" + """Test device and entity registry are cleaned up when camera is removed.""" reolink_connect.channels = [0] + assert await async_setup_component(hass, "config", {}) + client = await hass_ws_client(hass) # setup CH 0 and NVR switch entities/device with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SWITCH]): assert await hass.config_entries.async_setup(config_entry.entry_id) @@ -215,6 +229,13 @@ async def test_cleanup_disconnected_cams( setattr(reolink_connect, attr, value) with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SWITCH]): assert await hass.config_entries.async_reload(config_entry.entry_id) + await hass.async_block_till_done() + + expected_success = TEST_CAM_MODEL not in expected_models + for device in device_entries: + if device.model == TEST_CAM_MODEL: + response = await client.remove_device(device.id, config_entry.entry_id) + assert response["success"] == expected_success device_entries = dr.async_entries_for_config_entry( device_registry, config_entry.entry_id From 807ed0ce106c4ee448682561775b49bfd3ef7d35 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 2 Jul 2024 12:28:32 +0200 Subject: [PATCH 126/146] Do not hold core startup with reolink firmware check task (#120985) --- homeassistant/components/reolink/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index 02d3cc16419..1caf4e79cd5 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -133,7 +133,11 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b ) # If camera WAN blocked, firmware check fails and takes long, do not prevent setup - config_entry.async_create_task(hass, firmware_coordinator.async_refresh()) + config_entry.async_create_background_task( + hass, + firmware_coordinator.async_refresh(), + f"Reolink firmware check {config_entry.entry_id}", + ) # Fetch initial data so we have data when entities subscribe try: await device_coordinator.async_config_entry_first_refresh() From b3e833f677bf027efbc825193b271d49c6285761 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Tue, 2 Jul 2024 21:03:01 +0200 Subject: [PATCH 127/146] Fix setting target temperature for single setpoint Matter thermostat (#121011) --- homeassistant/components/matter/climate.py | 28 ++++++++++++---------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/matter/climate.py b/homeassistant/components/matter/climate.py index c97124f4305..2c05fd3373e 100644 --- a/homeassistant/components/matter/climate.py +++ b/homeassistant/components/matter/climate.py @@ -267,19 +267,13 @@ class MatterClimate(MatterEntity, ClimateEntity): self._attr_hvac_action = HVACAction.FAN case _: self._attr_hvac_action = HVACAction.OFF - # update target_temperature - if self._attr_hvac_mode == HVACMode.HEAT_COOL: - self._attr_target_temperature = None - elif self._attr_hvac_mode == HVACMode.COOL: - self._attr_target_temperature = self._get_temperature_in_degrees( - clusters.Thermostat.Attributes.OccupiedCoolingSetpoint - ) - else: - self._attr_target_temperature = self._get_temperature_in_degrees( - clusters.Thermostat.Attributes.OccupiedHeatingSetpoint - ) # update target temperature high/low - if self._attr_hvac_mode == HVACMode.HEAT_COOL: + supports_range = ( + self._attr_supported_features + & ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + ) + if supports_range and self._attr_hvac_mode == HVACMode.HEAT_COOL: + self._attr_target_temperature = None self._attr_target_temperature_high = self._get_temperature_in_degrees( clusters.Thermostat.Attributes.OccupiedCoolingSetpoint ) @@ -289,6 +283,16 @@ class MatterClimate(MatterEntity, ClimateEntity): else: self._attr_target_temperature_high = None self._attr_target_temperature_low = None + # update target_temperature + if self._attr_hvac_mode == HVACMode.COOL: + self._attr_target_temperature = self._get_temperature_in_degrees( + clusters.Thermostat.Attributes.OccupiedCoolingSetpoint + ) + else: + self._attr_target_temperature = self._get_temperature_in_degrees( + clusters.Thermostat.Attributes.OccupiedHeatingSetpoint + ) + # update min_temp if self._attr_hvac_mode == HVACMode.COOL: attribute = clusters.Thermostat.Attributes.AbsMinCoolSetpointLimit From 1fa6972a665052cc5f7c7dbb3d7fc5b32a8209fd Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Tue, 2 Jul 2024 21:02:29 +0200 Subject: [PATCH 128/146] Handle mains power for Matter appliances (#121023) --- homeassistant/components/matter/climate.py | 7 +++++++ homeassistant/components/matter/fan.py | 16 +++++++++++++++- tests/components/matter/test_climate.py | 9 +++++++-- tests/components/matter/test_fan.py | 6 ++++++ 4 files changed, 35 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/matter/climate.py b/homeassistant/components/matter/climate.py index 2c05fd3373e..192cb6b3bb4 100644 --- a/homeassistant/components/matter/climate.py +++ b/homeassistant/components/matter/climate.py @@ -227,6 +227,13 @@ class MatterClimate(MatterEntity, ClimateEntity): self._attr_current_temperature = self._get_temperature_in_degrees( clusters.Thermostat.Attributes.LocalTemperature ) + if self.get_matter_attribute_value(clusters.OnOff.Attributes.OnOff) is False: + # special case: the appliance has a dedicated Power switch on the OnOff cluster + # if the mains power is off - treat it as if the HVAC mode is off + self._attr_hvac_mode = HVACMode.OFF + self._attr_hvac_action = None + return + # update hvac_mode from SystemMode system_mode_value = int( self.get_matter_attribute_value(clusters.Thermostat.Attributes.SystemMode) diff --git a/homeassistant/components/matter/fan.py b/homeassistant/components/matter/fan.py index 0ce42f14d39..86f03dc7a03 100644 --- a/homeassistant/components/matter/fan.py +++ b/homeassistant/components/matter/fan.py @@ -170,6 +170,14 @@ class MatterFan(MatterEntity, FanEntity): """Update from device.""" if not hasattr(self, "_attr_preset_modes"): self._calculate_features() + + if self.get_matter_attribute_value(clusters.OnOff.Attributes.OnOff) is False: + # special case: the appliance has a dedicated Power switch on the OnOff cluster + # if the mains power is off - treat it as if the fan mode is off + self._attr_preset_mode = None + self._attr_percentage = 0 + return + if self._attr_supported_features & FanEntityFeature.DIRECTION: direction_value = self.get_matter_attribute_value( clusters.FanControl.Attributes.AirflowDirection @@ -200,7 +208,13 @@ class MatterFan(MatterEntity, FanEntity): wind_setting = self.get_matter_attribute_value( clusters.FanControl.Attributes.WindSetting ) - if ( + fan_mode = self.get_matter_attribute_value( + clusters.FanControl.Attributes.FanMode + ) + if fan_mode == clusters.FanControl.Enums.FanModeEnum.kOff: + self._attr_preset_mode = None + self._attr_percentage = 0 + elif ( self._attr_preset_modes and PRESET_NATURAL_WIND in self._attr_preset_modes and wind_setting & WindBitmap.kNaturalWind diff --git a/tests/components/matter/test_climate.py b/tests/components/matter/test_climate.py index 6a4cf34a640..e0015e8b445 100644 --- a/tests/components/matter/test_climate.py +++ b/tests/components/matter/test_climate.py @@ -315,14 +315,19 @@ async def test_room_airconditioner( state = hass.states.get("climate.room_airconditioner_thermostat") assert state assert state.attributes["current_temperature"] == 20 - assert state.attributes["min_temp"] == 16 - assert state.attributes["max_temp"] == 32 + # room airconditioner has mains power on OnOff cluster with value set to False + assert state.state == HVACMode.OFF # test supported features correctly parsed # WITHOUT temperature_range support mask = ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.TURN_OFF assert state.attributes["supported_features"] & mask == mask + # set mains power to ON (OnOff cluster) + set_node_attribute(room_airconditioner, 1, 6, 0, True) + await trigger_subscription_callback(hass, matter_client) + state = hass.states.get("climate.room_airconditioner_thermostat") + # test supported HVAC modes include fan and dry modes assert state.attributes["hvac_modes"] == [ HVACMode.OFF, diff --git a/tests/components/matter/test_fan.py b/tests/components/matter/test_fan.py index 30bd7f4a009..7e964d672ca 100644 --- a/tests/components/matter/test_fan.py +++ b/tests/components/matter/test_fan.py @@ -92,6 +92,12 @@ async def test_fan_base( await trigger_subscription_callback(hass, matter_client) state = hass.states.get(entity_id) assert state.attributes["preset_mode"] == "sleep_wind" + # set mains power to OFF (OnOff cluster) + set_node_attribute(air_purifier, 1, 6, 0, False) + await trigger_subscription_callback(hass, matter_client) + state = hass.states.get(entity_id) + assert state.attributes["preset_mode"] is None + assert state.attributes["percentage"] == 0 async def test_fan_turn_on_with_percentage( From 6b045a7d7bd0e7fb8cc916c78de2a41ca4f0858a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 2 Jul 2024 21:09:55 +0200 Subject: [PATCH 129/146] Bump version to 2024.7.0b8 --- 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 5f020a02624..3828f2cfbf7 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "0b7" +PATCH_VERSION: Final = "0b8" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 6320551a082..bd34e19c555 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.7.0b7" +version = "2024.7.0b8" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 4377f4cbea5782686e83950e56854a1d90fc7a4b Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Tue, 2 Jul 2024 22:13:19 +0200 Subject: [PATCH 130/146] Temporarily set apprise log level to debug in tests (#121029) Co-authored-by: Franck Nijhof --- tests/components/apprise/test_notify.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/components/apprise/test_notify.py b/tests/components/apprise/test_notify.py index 7d37d7a5d99..d73fa72d6c7 100644 --- a/tests/components/apprise/test_notify.py +++ b/tests/components/apprise/test_notify.py @@ -1,14 +1,27 @@ """The tests for the apprise notification platform.""" +import logging from pathlib import Path from unittest.mock import MagicMock, patch +import pytest + from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component BASE_COMPONENT = "notify" +@pytest.fixture(autouse=True) +def reset_log_level(): + """Set and reset log level after each test case.""" + logger = logging.getLogger("apprise") + orig_level = logger.level + logger.setLevel(logging.DEBUG) + yield + logger.setLevel(orig_level) + + async def test_apprise_config_load_fail01(hass: HomeAssistant) -> None: """Test apprise configuration failures 1.""" From d1e76d5c3cf374e5d4ace63e0a6734d36e4e0037 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 2 Jul 2024 22:13:07 +0200 Subject: [PATCH 131/146] Update frontend to 20240702.0 (#121032) --- homeassistant/components/frontend/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/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 70f1f5f4f4f..0d32624cf57 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20240628.0"] + "requirements": ["home-assistant-frontend==20240702.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7cccd58d73f..3ffa9d92f63 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ habluetooth==3.1.3 hass-nabucasa==0.81.1 hassil==1.7.1 home-assistant-bluetooth==1.12.2 -home-assistant-frontend==20240628.0 +home-assistant-frontend==20240702.0 home-assistant-intents==2024.6.26 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 7ba781583f5..de7cc9fa13f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1090,7 +1090,7 @@ hole==0.8.0 holidays==0.51 # homeassistant.components.frontend -home-assistant-frontend==20240628.0 +home-assistant-frontend==20240702.0 # homeassistant.components.conversation home-assistant-intents==2024.6.26 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 65f9b4b1770..0a63b696617 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -895,7 +895,7 @@ hole==0.8.0 holidays==0.51 # homeassistant.components.frontend -home-assistant-frontend==20240628.0 +home-assistant-frontend==20240702.0 # homeassistant.components.conversation home-assistant-intents==2024.6.26 From 1b9f27fab753997148f4cd962dee89847970a31e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 2 Jul 2024 22:15:17 +0200 Subject: [PATCH 132/146] Bump version to 2024.7.0b9 --- 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 3828f2cfbf7..5d20a8507bf 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "0b8" +PATCH_VERSION: Final = "0b9" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index bd34e19c555..1ebd3acf1e0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.7.0b8" +version = "2024.7.0b9" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 1665cb40acc05cf4263aa473f2c7dd74c9e1bb51 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 1 Jul 2024 08:52:19 -0700 Subject: [PATCH 133/146] Bump gcal_sync to 6.1.4 (#120941) --- homeassistant/components/google/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index d40daa89b0e..163ad91fb7c 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/calendar.google", "iot_class": "cloud_polling", "loggers": ["googleapiclient"], - "requirements": ["gcal-sync==6.1.3", "oauth2client==4.1.3", "ical==8.1.1"] + "requirements": ["gcal-sync==6.1.4", "oauth2client==4.1.3", "ical==8.1.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index de7cc9fa13f..cfc4f6f72bb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -924,7 +924,7 @@ gardena-bluetooth==1.4.2 gassist-text==0.0.11 # homeassistant.components.google -gcal-sync==6.1.3 +gcal-sync==6.1.4 # homeassistant.components.geniushub geniushub-client==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0a63b696617..775d3533c4c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -762,7 +762,7 @@ gardena-bluetooth==1.4.2 gassist-text==0.0.11 # homeassistant.components.google -gcal-sync==6.1.3 +gcal-sync==6.1.4 # homeassistant.components.geocaching geocachingapi==0.2.1 From febd1a377203e55544c38d48906a8b4fb163a0a0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 2 Jul 2024 19:12:17 -0700 Subject: [PATCH 134/146] Bump inkbird-ble to 0.5.7 (#121039) changelog: https://github.com/Bluetooth-Devices/inkbird-ble/compare/v0.5.6...v0.5.7 --- homeassistant/components/inkbird/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/inkbird/manifest.json b/homeassistant/components/inkbird/manifest.json index fcd95eadf9c..fb74d1c565a 100644 --- a/homeassistant/components/inkbird/manifest.json +++ b/homeassistant/components/inkbird/manifest.json @@ -28,5 +28,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/inkbird", "iot_class": "local_push", - "requirements": ["inkbird-ble==0.5.6"] + "requirements": ["inkbird-ble==0.5.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index cfc4f6f72bb..480fbe162d6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1161,7 +1161,7 @@ influxdb-client==1.24.0 influxdb==5.3.1 # homeassistant.components.inkbird -inkbird-ble==0.5.6 +inkbird-ble==0.5.7 # homeassistant.components.insteon insteon-frontend-home-assistant==0.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 775d3533c4c..a2cffe6a526 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -951,7 +951,7 @@ influxdb-client==1.24.0 influxdb==5.3.1 # homeassistant.components.inkbird -inkbird-ble==0.5.6 +inkbird-ble==0.5.7 # homeassistant.components.insteon insteon-frontend-home-assistant==0.5.0 From 84204c38be7c0dd0b49a5f7216e8454b69ea7408 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 3 Jul 2024 08:59:52 +0200 Subject: [PATCH 135/146] Bump version to 2024.7.0b10 --- 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 5d20a8507bf..b6800b44063 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "0b9" +PATCH_VERSION: Final = "0b10" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 1ebd3acf1e0..36ca9abe1b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.7.0b9" +version = "2024.7.0b10" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From c4956b66b0da95811cdd2b493ea836c74a9f55a3 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Mon, 1 Jul 2024 00:23:42 +0200 Subject: [PATCH 136/146] Bump here-routing to 1.0.1 (#120877) --- homeassistant/components/here_travel_time/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/here_travel_time/manifest.json b/homeassistant/components/here_travel_time/manifest.json index 19c5c4d73d9..2d6621c7c61 100644 --- a/homeassistant/components/here_travel_time/manifest.json +++ b/homeassistant/components/here_travel_time/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/here_travel_time", "iot_class": "cloud_polling", "loggers": ["here_routing", "here_transit", "homeassistant.helpers.location"], - "requirements": ["here-routing==0.2.0", "here-transit==1.2.0"] + "requirements": ["here-routing==1.0.1", "here-transit==1.2.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 480fbe162d6..a04485044d5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1065,7 +1065,7 @@ hdate==0.10.9 heatmiserV3==1.1.18 # homeassistant.components.here_travel_time -here-routing==0.2.0 +here-routing==1.0.1 # homeassistant.components.here_travel_time here-transit==1.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a2cffe6a526..2a93dd5d739 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -876,7 +876,7 @@ hassil==1.7.1 hdate==0.10.9 # homeassistant.components.here_travel_time -here-routing==0.2.0 +here-routing==1.0.1 # homeassistant.components.here_travel_time here-transit==1.2.0 From 16827ea09e1942f175ff5616f76bd10fe84ea0c3 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Wed, 3 Jul 2024 15:35:08 +0200 Subject: [PATCH 137/146] Bump here-transit to 1.2.1 (#120900) --- homeassistant/components/here_travel_time/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/here_travel_time/manifest.json b/homeassistant/components/here_travel_time/manifest.json index 2d6621c7c61..0365cf51d97 100644 --- a/homeassistant/components/here_travel_time/manifest.json +++ b/homeassistant/components/here_travel_time/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/here_travel_time", "iot_class": "cloud_polling", "loggers": ["here_routing", "here_transit", "homeassistant.helpers.location"], - "requirements": ["here-routing==1.0.1", "here-transit==1.2.0"] + "requirements": ["here-routing==1.0.1", "here-transit==1.2.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index a04485044d5..1a6fd81ecaa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1068,7 +1068,7 @@ heatmiserV3==1.1.18 here-routing==1.0.1 # homeassistant.components.here_travel_time -here-transit==1.2.0 +here-transit==1.2.1 # homeassistant.components.hikvisioncam hikvision==0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2a93dd5d739..b4042f70640 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -879,7 +879,7 @@ hdate==0.10.9 here-routing==1.0.1 # homeassistant.components.here_travel_time -here-transit==1.2.0 +here-transit==1.2.1 # homeassistant.components.hko hko==0.3.2 From 36e74cd9a6afae0b3d7733b449babf4486a47c1d Mon Sep 17 00:00:00 2001 From: Anton Tolchanov <1687799+knyar@users.noreply.github.com> Date: Wed, 3 Jul 2024 14:41:01 +0100 Subject: [PATCH 138/146] Generate Prometheus metrics in an executor job (#121058) --- homeassistant/components/prometheus/__init__.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index 2159656f129..a0f0d69ce46 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -26,7 +26,7 @@ from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, ATTR_CURRENT_TILT_POSITION, ) -from homeassistant.components.http import HomeAssistantView +from homeassistant.components.http import KEY_HASS, HomeAssistantView from homeassistant.components.humidifier import ATTR_AVAILABLE_MODES, ATTR_HUMIDITY from homeassistant.components.light import ATTR_BRIGHTNESS from homeassistant.components.sensor import SensorDeviceClass @@ -729,7 +729,11 @@ class PrometheusView(HomeAssistantView): """Handle request for Prometheus metrics.""" _LOGGER.debug("Received Prometheus metrics request") + hass = request.app[KEY_HASS] + body = await hass.async_add_executor_job( + prometheus_client.generate_latest, prometheus_client.REGISTRY + ) return web.Response( - body=prometheus_client.generate_latest(prometheus_client.REGISTRY), + body=body, content_type=CONTENT_TYPE_TEXT_PLAIN, ) From 6621cf475a7b66f5e5a377bbbc09e5ebe1363734 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 3 Jul 2024 15:41:43 +0200 Subject: [PATCH 139/146] Update frontend to 20240703.0 (#121063) --- homeassistant/components/frontend/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/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 0d32624cf57..525ba507121 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20240702.0"] + "requirements": ["home-assistant-frontend==20240703.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3ffa9d92f63..04dc11f0183 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ habluetooth==3.1.3 hass-nabucasa==0.81.1 hassil==1.7.1 home-assistant-bluetooth==1.12.2 -home-assistant-frontend==20240702.0 +home-assistant-frontend==20240703.0 home-assistant-intents==2024.6.26 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 1a6fd81ecaa..ee0a07a2148 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1090,7 +1090,7 @@ hole==0.8.0 holidays==0.51 # homeassistant.components.frontend -home-assistant-frontend==20240702.0 +home-assistant-frontend==20240703.0 # homeassistant.components.conversation home-assistant-intents==2024.6.26 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b4042f70640..0033bda8949 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -895,7 +895,7 @@ hole==0.8.0 holidays==0.51 # homeassistant.components.frontend -home-assistant-frontend==20240702.0 +home-assistant-frontend==20240703.0 # homeassistant.components.conversation home-assistant-intents==2024.6.26 From 13631250b460f138ef4d78eae1004378c3668b5f Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 3 Jul 2024 16:16:13 +0200 Subject: [PATCH 140/146] Bump axis to v62 (#121070) --- homeassistant/components/axis/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/axis/manifest.json b/homeassistant/components/axis/manifest.json index 2f057f96286..e028736f4ca 100644 --- a/homeassistant/components/axis/manifest.json +++ b/homeassistant/components/axis/manifest.json @@ -30,7 +30,7 @@ "iot_class": "local_push", "loggers": ["axis"], "quality_scale": "platinum", - "requirements": ["axis==61"], + "requirements": ["axis==62"], "ssdp": [ { "manufacturer": "AXIS" diff --git a/requirements_all.txt b/requirements_all.txt index ee0a07a2148..dbc5f480b0f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -520,7 +520,7 @@ aurorapy==0.2.7 # avion==0.10 # homeassistant.components.axis -axis==61 +axis==62 # homeassistant.components.azure_event_hub azure-eventhub==5.11.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0033bda8949..6920608e656 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -460,7 +460,7 @@ auroranoaa==0.0.3 aurorapy==0.2.7 # homeassistant.components.axis -axis==61 +axis==62 # homeassistant.components.azure_event_hub azure-eventhub==5.11.1 From c89a9b5ce04967f73674f12a3f63637c8371836e Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 3 Jul 2024 16:27:45 +0200 Subject: [PATCH 141/146] Bump python-matter-server to 6.2.2 (#121072) --- homeassistant/components/matter/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/matter/manifest.json b/homeassistant/components/matter/manifest.json index 8c88fcc8be2..1dac5ef0cb2 100644 --- a/homeassistant/components/matter/manifest.json +++ b/homeassistant/components/matter/manifest.json @@ -6,6 +6,6 @@ "dependencies": ["websocket_api"], "documentation": "https://www.home-assistant.io/integrations/matter", "iot_class": "local_push", - "requirements": ["python-matter-server==6.2.0b1"], + "requirements": ["python-matter-server==6.2.2"], "zeroconf": ["_matter._tcp.local.", "_matterc._udp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index dbc5f480b0f..092536e3012 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2281,7 +2281,7 @@ python-kasa[speedups]==0.7.0.2 # python-lirc==1.2.3 # homeassistant.components.matter -python-matter-server==6.2.0b1 +python-matter-server==6.2.2 # homeassistant.components.xiaomi_miio python-miio==0.5.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6920608e656..cf29ec479a7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1778,7 +1778,7 @@ python-juicenet==1.1.0 python-kasa[speedups]==0.7.0.2 # homeassistant.components.matter -python-matter-server==6.2.0b1 +python-matter-server==6.2.2 # homeassistant.components.xiaomi_miio python-miio==0.5.12 From e8bcb3e11eb7b212db4f76827da7418bafbf650b Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 3 Jul 2024 09:55:21 -0500 Subject: [PATCH 142/146] Bump intents to 2024.7.3 (#121076) --- homeassistant/components/conversation/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/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index 2302d03bf4c..6eeb461d79d 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/conversation", "integration_type": "system", "quality_scale": "internal", - "requirements": ["hassil==1.7.1", "home-assistant-intents==2024.6.26"] + "requirements": ["hassil==1.7.1", "home-assistant-intents==2024.7.3"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 04dc11f0183..6058a781e2a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -33,7 +33,7 @@ hass-nabucasa==0.81.1 hassil==1.7.1 home-assistant-bluetooth==1.12.2 home-assistant-frontend==20240703.0 -home-assistant-intents==2024.6.26 +home-assistant-intents==2024.7.3 httpx==0.27.0 ifaddr==0.2.0 Jinja2==3.1.4 diff --git a/requirements_all.txt b/requirements_all.txt index 092536e3012..3a8edbba7a2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1093,7 +1093,7 @@ holidays==0.51 home-assistant-frontend==20240703.0 # homeassistant.components.conversation -home-assistant-intents==2024.6.26 +home-assistant-intents==2024.7.3 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cf29ec479a7..a56a072d62a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -898,7 +898,7 @@ holidays==0.51 home-assistant-frontend==20240703.0 # homeassistant.components.conversation -home-assistant-intents==2024.6.26 +home-assistant-intents==2024.7.3 # homeassistant.components.home_connect homeconnect==0.7.2 From 547b24ce583f91a223ea7b44e00dce43b6eb5c79 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 3 Jul 2024 17:11:09 +0200 Subject: [PATCH 143/146] Bump deebot-client to 8.1.0 (#121078) --- homeassistant/components/ecovacs/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ecovacs/manifest.json b/homeassistant/components/ecovacs/manifest.json index d14291576ff..9568bf2c3ac 100644 --- a/homeassistant/components/ecovacs/manifest.json +++ b/homeassistant/components/ecovacs/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/ecovacs", "iot_class": "cloud_push", "loggers": ["sleekxmppfs", "sucks", "deebot_client"], - "requirements": ["py-sucks==0.9.10", "deebot-client==8.0.0"] + "requirements": ["py-sucks==0.9.10", "deebot-client==8.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3a8edbba7a2..5fd773147dc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -709,7 +709,7 @@ debugpy==1.8.1 # decora==0.6 # homeassistant.components.ecovacs -deebot-client==8.0.0 +deebot-client==8.1.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a56a072d62a..d5c4945da95 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -590,7 +590,7 @@ dbus-fast==2.22.1 debugpy==1.8.1 # homeassistant.components.ecovacs -deebot-client==8.0.0 +deebot-client==8.1.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns From 85168239cdcfa8f3fcb04812f30e495c99ce3ff8 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 3 Jul 2024 17:16:51 +0200 Subject: [PATCH 144/146] Matter fix Energy sensor discovery schemas (#121080) --- homeassistant/components/matter/sensor.py | 98 +++++++++++++++++++++-- 1 file changed, 91 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/matter/sensor.py b/homeassistant/components/matter/sensor.py index d91d4d33471..9c19be7ee08 100644 --- a/homeassistant/components/matter/sensor.py +++ b/homeassistant/components/matter/sensor.py @@ -6,7 +6,11 @@ from dataclasses import dataclass from chip.clusters import Objects as clusters from chip.clusters.Types import Nullable, NullValue -from matter_server.common.custom_clusters import EveCluster +from matter_server.common.custom_clusters import ( + EveCluster, + NeoCluster, + ThirdRealityMeteringCluster, +) from homeassistant.components.sensor import ( SensorDeviceClass, @@ -171,9 +175,6 @@ DISCOVERY_SCHEMAS = [ ), entity_class=MatterSensor, required_attributes=(EveCluster.Attributes.Watt,), - # Add OnOff Attribute as optional attribute to poll - # the primary value when the relay is toggled - optional_attributes=(clusters.OnOff.Attributes.OnOff,), ), MatterDiscoverySchema( platform=Platform.SENSOR, @@ -213,9 +214,6 @@ DISCOVERY_SCHEMAS = [ ), entity_class=MatterSensor, required_attributes=(EveCluster.Attributes.Current,), - # Add OnOff Attribute as optional attribute to poll - # the primary value when the relay is toggled - optional_attributes=(clusters.OnOff.Attributes.OnOff,), ), MatterDiscoverySchema( platform=Platform.SENSOR, @@ -364,4 +362,90 @@ DISCOVERY_SCHEMAS = [ clusters.ActivatedCarbonFilterMonitoring.Attributes.Condition, ), ), + MatterDiscoverySchema( + platform=Platform.SENSOR, + entity_description=MatterSensorEntityDescription( + key="ThirdRealityEnergySensorWatt", + device_class=SensorDeviceClass.POWER, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=UnitOfPower.WATT, + suggested_display_precision=2, + state_class=SensorStateClass.MEASUREMENT, + measurement_to_ha=lambda x: x / 1000, + ), + entity_class=MatterSensor, + required_attributes=( + ThirdRealityMeteringCluster.Attributes.InstantaneousDemand, + ), + ), + MatterDiscoverySchema( + platform=Platform.SENSOR, + entity_description=MatterSensorEntityDescription( + key="ThirdRealityEnergySensorWattAccumulated", + device_class=SensorDeviceClass.ENERGY, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + suggested_display_precision=3, + state_class=SensorStateClass.TOTAL_INCREASING, + measurement_to_ha=lambda x: x / 1000, + ), + entity_class=MatterSensor, + required_attributes=( + ThirdRealityMeteringCluster.Attributes.CurrentSummationDelivered, + ), + ), + MatterDiscoverySchema( + platform=Platform.SENSOR, + entity_description=MatterSensorEntityDescription( + key="NeoEnergySensorWatt", + device_class=SensorDeviceClass.POWER, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=UnitOfPower.WATT, + suggested_display_precision=2, + state_class=SensorStateClass.MEASUREMENT, + measurement_to_ha=lambda x: x / 10, + ), + entity_class=MatterSensor, + required_attributes=(NeoCluster.Attributes.Watt,), + ), + MatterDiscoverySchema( + platform=Platform.SENSOR, + entity_description=MatterSensorEntityDescription( + key="NeoEnergySensorWattAccumulated", + device_class=SensorDeviceClass.ENERGY, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + suggested_display_precision=1, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + entity_class=MatterSensor, + required_attributes=(NeoCluster.Attributes.WattAccumulated,), + ), + MatterDiscoverySchema( + platform=Platform.SENSOR, + entity_description=MatterSensorEntityDescription( + key="NeoEnergySensorVoltage", + device_class=SensorDeviceClass.VOLTAGE, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + suggested_display_precision=0, + state_class=SensorStateClass.MEASUREMENT, + measurement_to_ha=lambda x: x / 10, + ), + entity_class=MatterSensor, + required_attributes=(NeoCluster.Attributes.Voltage,), + ), + MatterDiscoverySchema( + platform=Platform.SENSOR, + entity_description=MatterSensorEntityDescription( + key="NeoEnergySensorWattCurrent", + device_class=SensorDeviceClass.CURRENT, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=UnitOfElectricCurrent.MILLIAMPERE, + suggested_display_precision=0, + state_class=SensorStateClass.MEASUREMENT, + ), + entity_class=MatterSensor, + required_attributes=(NeoCluster.Attributes.Current,), + ), ] From d94b36cfbbf486f3d873cc55aeb9b8411bdbb2be Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 3 Jul 2024 17:29:08 +0200 Subject: [PATCH 145/146] Bump version to 2024.7.0b11 --- 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 b6800b44063..c76db961e8b 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "0b10" +PATCH_VERSION: Final = "0b11" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 36ca9abe1b5..198d3438fc5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.7.0b10" +version = "2024.7.0b11" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 1080a4ef1e99a5f567f4a2b5aca058b84acf7352 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 3 Jul 2024 17:55:58 +0200 Subject: [PATCH 146/146] Bump version to 2024.7.0 --- 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 c76db961e8b..d5c64823890 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "0b11" +PATCH_VERSION: Final = "0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 198d3438fc5..777ec8bb6a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.7.0b11" +version = "2024.7.0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst"