diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index 27b5ba52d79..1d06ae23ca2 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -14,6 +14,6 @@ "documentation": "https://www.home-assistant.io/integrations/cast", "iot_class": "local_polling", "loggers": ["casttube", "pychromecast"], - "requirements": ["PyChromecast==14.0.2"], + "requirements": ["PyChromecast==14.0.1"], "zeroconf": ["_googlecast._tcp.local."] } diff --git a/homeassistant/components/matrix/manifest.json b/homeassistant/components/matrix/manifest.json index cd4e5327608..520bd0550cc 100644 --- a/homeassistant/components/matrix/manifest.json +++ b/homeassistant/components/matrix/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/matrix", "iot_class": "cloud_push", "loggers": ["matrix_client"], - "requirements": ["matrix-nio==0.25.1", "Pillow==10.4.0"] + "requirements": ["matrix-nio==0.25.2", "Pillow==10.4.0"] } diff --git a/homeassistant/components/mealie/manifest.json b/homeassistant/components/mealie/manifest.json index 4fabdffadc4..f594f1398e3 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.9.2"] + "requirements": ["aiomealie==0.9.3"] } diff --git a/homeassistant/components/nyt_games/__init__.py b/homeassistant/components/nyt_games/__init__.py index ae35b40d29f..94dc22fe89e 100644 --- a/homeassistant/components/nyt_games/__init__.py +++ b/homeassistant/components/nyt_games/__init__.py @@ -7,7 +7,7 @@ from nyt_games import NYTGamesClient from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_TOKEN, Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.aiohttp_client import async_create_clientsession from .coordinator import NYTGamesCoordinator @@ -23,7 +23,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: NYTGamesConfigEntry) -> """Set up NYTGames from a config entry.""" client = NYTGamesClient( - entry.data[CONF_TOKEN], session=async_get_clientsession(hass) + entry.data[CONF_TOKEN], session=async_create_clientsession(hass) ) coordinator = NYTGamesCoordinator(hass, client) diff --git a/homeassistant/components/nyt_games/config_flow.py b/homeassistant/components/nyt_games/config_flow.py index 03247d6c194..bfed1f47c41 100644 --- a/homeassistant/components/nyt_games/config_flow.py +++ b/homeassistant/components/nyt_games/config_flow.py @@ -7,7 +7,7 @@ import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_TOKEN -from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.aiohttp_client import async_create_clientsession from .const import DOMAIN, LOGGER @@ -21,8 +21,9 @@ class NYTGamesConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a flow initialized by the user.""" errors: dict[str, str] = {} if user_input: - session = async_get_clientsession(self.hass) - client = NYTGamesClient(user_input[CONF_TOKEN], session=session) + session = async_create_clientsession(self.hass) + token = user_input[CONF_TOKEN].strip() + client = NYTGamesClient(token, session=session) try: user_id = await client.get_user_id() except NYTGamesAuthenticationError: @@ -35,7 +36,9 @@ class NYTGamesConfigFlow(ConfigFlow, domain=DOMAIN): else: await self.async_set_unique_id(str(user_id)) self._abort_if_unique_id_configured() - return self.async_create_entry(title="NYT Games", data=user_input) + return self.async_create_entry( + title="NYT Games", data={CONF_TOKEN: token} + ) return self.async_show_form( step_id="user", data_schema=vol.Schema({vol.Required(CONF_TOKEN): str}), diff --git a/homeassistant/components/rituals_perfume_genie/__init__.py b/homeassistant/components/rituals_perfume_genie/__init__.py index 792a470ca3c..d0d16ba6324 100644 --- a/homeassistant/components/rituals_perfume_genie/__init__.py +++ b/homeassistant/components/rituals_perfume_genie/__init__.py @@ -12,7 +12,7 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import ACCOUNT_HASH, DOMAIN +from .const import ACCOUNT_HASH, DOMAIN, UPDATE_INTERVAL from .coordinator import RitualsDataUpdateCoordinator PLATFORMS = [ @@ -37,9 +37,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Migrate old unique_ids to the new format async_migrate_entities_unique_ids(hass, entry, account_devices) + # The API provided by Rituals is currently rate limited to 30 requests + # per hour per IP address. To avoid hitting this limit, we will adjust + # the polling interval based on the number of diffusers one has. + update_interval = UPDATE_INTERVAL * len(account_devices) + # Create a coordinator for each diffuser coordinators = { - diffuser.hublot: RitualsDataUpdateCoordinator(hass, diffuser) + diffuser.hublot: RitualsDataUpdateCoordinator(hass, diffuser, update_interval) for diffuser in account_devices } diff --git a/homeassistant/components/rituals_perfume_genie/config_flow.py b/homeassistant/components/rituals_perfume_genie/config_flow.py index 4f108d9bc22..f6736ab78e4 100644 --- a/homeassistant/components/rituals_perfume_genie/config_flow.py +++ b/homeassistant/components/rituals_perfume_genie/config_flow.py @@ -45,6 +45,7 @@ class RitualsPerfumeGenieConfigFlow(ConfigFlow, domain=DOMAIN): try: await account.authenticate() except ClientResponseError: + _LOGGER.exception("Unexpected response") errors["base"] = "cannot_connect" except AuthenticationException: errors["base"] = "invalid_auth" diff --git a/homeassistant/components/rituals_perfume_genie/const.py b/homeassistant/components/rituals_perfume_genie/const.py index 35d1c32d306..45428ced9d2 100644 --- a/homeassistant/components/rituals_perfume_genie/const.py +++ b/homeassistant/components/rituals_perfume_genie/const.py @@ -6,4 +6,8 @@ DOMAIN = "rituals_perfume_genie" ACCOUNT_HASH = "account_hash" -UPDATE_INTERVAL = timedelta(minutes=2) +# The API provided by Rituals is currently rate limited to 30 requests +# per hour per IP address. To avoid hitting this limit, the polling +# interval is set to 3 minutes. This also gives a little room for +# Home Assistant restarts. +UPDATE_INTERVAL = timedelta(minutes=3) diff --git a/homeassistant/components/rituals_perfume_genie/coordinator.py b/homeassistant/components/rituals_perfume_genie/coordinator.py index 4c86f110b17..a83e823bd4e 100644 --- a/homeassistant/components/rituals_perfume_genie/coordinator.py +++ b/homeassistant/components/rituals_perfume_genie/coordinator.py @@ -1,5 +1,6 @@ """The Rituals Perfume Genie data update coordinator.""" +from datetime import timedelta import logging from pyrituals import Diffuser @@ -7,7 +8,7 @@ from pyrituals import Diffuser from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import DOMAIN, UPDATE_INTERVAL +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -15,14 +16,19 @@ _LOGGER = logging.getLogger(__name__) class RitualsDataUpdateCoordinator(DataUpdateCoordinator[None]): """Class to manage fetching Rituals Perfume Genie device data from single endpoint.""" - def __init__(self, hass: HomeAssistant, diffuser: Diffuser) -> None: + def __init__( + self, + hass: HomeAssistant, + diffuser: Diffuser, + update_interval: timedelta, + ) -> None: """Initialize global Rituals Perfume Genie data updater.""" self.diffuser = diffuser super().__init__( hass, _LOGGER, name=f"{DOMAIN}-{diffuser.hublot}", - update_interval=UPDATE_INTERVAL, + update_interval=update_interval, ) async def _async_update_data(self) -> None: diff --git a/homeassistant/components/smlight/manifest.json b/homeassistant/components/smlight/manifest.json index 3f4a0c69b24..10984e8efb1 100644 --- a/homeassistant/components/smlight/manifest.json +++ b/homeassistant/components/smlight/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/smlight", "integration_type": "device", "iot_class": "local_push", - "requirements": ["pysmlight==0.1.1"], + "requirements": ["pysmlight==0.1.2"], "zeroconf": [ { "type": "_slzb-06._tcp.local." diff --git a/homeassistant/components/tellduslive/sensor.py b/homeassistant/components/tellduslive/sensor.py index e588ea6318f..9bd2b1fe599 100644 --- a/homeassistant/components/tellduslive/sensor.py +++ b/homeassistant/components/tellduslive/sensor.py @@ -194,4 +194,4 @@ class TelldusLiveSensor(TelldusLiveEntity, SensorEntity): @property def unique_id(self) -> str: """Return a unique ID.""" - return "-".join(self._id) + return "-".join(map(str, self._id)) diff --git a/homeassistant/components/template/alarm_control_panel.py b/homeassistant/components/template/alarm_control_panel.py index 0d9e5ebc8ce..6c8a70b328e 100644 --- a/homeassistant/components/template/alarm_control_panel.py +++ b/homeassistant/components/template/alarm_control_panel.py @@ -38,6 +38,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import TemplateError from homeassistant.helpers import selector import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device import async_device_info_to_link_from_device_id from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity @@ -233,7 +234,10 @@ class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity, Restore self._trigger_script = Script(hass, trigger_action, name, DOMAIN) self._state: str | None = None - + self._attr_device_info = async_device_info_to_link_from_device_id( + hass, + config.get(CONF_DEVICE_ID), + ) supported_features = AlarmControlPanelEntityFeature(0) if self._arm_night_script is not None: supported_features = ( diff --git a/homeassistant/components/tesla_fleet/__init__.py b/homeassistant/components/tesla_fleet/__init__.py index 61f9dc66ffc..4cd8c5c7142 100644 --- a/homeassistant/components/tesla_fleet/__init__.py +++ b/homeassistant/components/tesla_fleet/__init__.py @@ -47,6 +47,7 @@ PLATFORMS: Final = [ Platform.DEVICE_TRACKER, Platform.LOCK, Platform.MEDIA_PLAYER, + Platform.NUMBER, Platform.SELECT, Platform.SENSOR, Platform.SWITCH, diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 404ae1c91dd..f9dc9191c8e 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -1558,7 +1558,7 @@ class ConfigEntryItems(UserDict[str, ConfigEntry]): super().__init__() self._hass = hass self._domain_index: dict[str, list[ConfigEntry]] = {} - self._domain_unique_id_index: dict[str, dict[str, ConfigEntry]] = {} + self._domain_unique_id_index: dict[str, dict[str, list[ConfigEntry]]] = {} def values(self) -> ValuesView[ConfigEntry]: """Return the underlying values to avoid __iter__ overhead.""" @@ -1601,9 +1601,9 @@ class ConfigEntryItems(UserDict[str, ConfigEntry]): report_issue, ) - self._domain_unique_id_index.setdefault(entry.domain, {})[ - unique_id_hash - ] = entry + self._domain_unique_id_index.setdefault(entry.domain, {}).setdefault( + unique_id_hash, [] + ).append(entry) def _unindex_entry(self, entry_id: str) -> None: """Unindex an entry.""" @@ -1616,7 +1616,9 @@ class ConfigEntryItems(UserDict[str, ConfigEntry]): # Check type first to avoid expensive isinstance call if type(unique_id) is not str and not isinstance(unique_id, Hashable): # noqa: E721 unique_id = str(entry.unique_id) # type: ignore[unreachable] - del self._domain_unique_id_index[domain][unique_id] + self._domain_unique_id_index[domain][unique_id].remove(entry) + if not self._domain_unique_id_index[domain][unique_id]: + del self._domain_unique_id_index[domain][unique_id] if not self._domain_unique_id_index[domain]: del self._domain_unique_id_index[domain] @@ -1647,7 +1649,10 @@ class ConfigEntryItems(UserDict[str, ConfigEntry]): # Check type first to avoid expensive isinstance call if type(unique_id) is not str and not isinstance(unique_id, Hashable): # noqa: E721 unique_id = str(unique_id) # type: ignore[unreachable] - return self._domain_unique_id_index.get(domain, {}).get(unique_id) + entries = self._domain_unique_id_index.get(domain, {}).get(unique_id) + if not entries: + return None + return entries[0] class ConfigEntryStore(storage.Store[dict[str, list[dict[str, Any]]]]): diff --git a/homeassistant/const.py b/homeassistant/const.py index b1ac28494c9..26049ed326b 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 = 10 -PATCH_VERSION: Final = "0" +PATCH_VERSION: Final = "1" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 465cbf0de5f..955aac83f36 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.10.0" +version = "2024.10.1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" diff --git a/requirements_all.txt b/requirements_all.txt index 78c90a57fe6..2563b7a1eb9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -45,7 +45,7 @@ ProgettiHWSW==0.1.3 # PyBluez==0.22 # homeassistant.components.cast -PyChromecast==14.0.2 +PyChromecast==14.0.1 # homeassistant.components.flick_electric PyFlick==0.0.2 @@ -294,7 +294,7 @@ aiolookin==1.0.0 aiolyric==2.0.1 # homeassistant.components.mealie -aiomealie==0.9.2 +aiomealie==0.9.3 # homeassistant.components.modern_forms aiomodernforms==0.1.8 @@ -1324,7 +1324,7 @@ lw12==0.9.2 lxml==5.3.0 # homeassistant.components.matrix -matrix-nio==0.25.1 +matrix-nio==0.25.2 # homeassistant.components.maxcube maxcube-api==0.4.3 @@ -2244,7 +2244,7 @@ pysmarty2==0.10.1 pysml==0.0.12 # homeassistant.components.smlight -pysmlight==0.1.1 +pysmlight==0.1.2 # homeassistant.components.snmp pysnmp==6.2.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9281f059bef..bee8274ca60 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -42,7 +42,7 @@ PlexAPI==4.15.16 ProgettiHWSW==0.1.3 # homeassistant.components.cast -PyChromecast==14.0.2 +PyChromecast==14.0.1 # homeassistant.components.flick_electric PyFlick==0.0.2 @@ -276,7 +276,7 @@ aiolookin==1.0.0 aiolyric==2.0.1 # homeassistant.components.mealie -aiomealie==0.9.2 +aiomealie==0.9.3 # homeassistant.components.modern_forms aiomodernforms==0.1.8 @@ -1099,7 +1099,7 @@ lupupy==0.3.2 lxml==5.3.0 # homeassistant.components.matrix -matrix-nio==0.25.1 +matrix-nio==0.25.2 # homeassistant.components.maxcube maxcube-api==0.4.3 @@ -1798,7 +1798,7 @@ pysmartthings==0.7.8 pysml==0.0.12 # homeassistant.components.smlight -pysmlight==0.1.1 +pysmlight==0.1.2 # homeassistant.components.snmp pysnmp==6.2.6 diff --git a/tests/components/nyt_games/test_config_flow.py b/tests/components/nyt_games/test_config_flow.py index 144b3a3ad17..bd17724887e 100644 --- a/tests/components/nyt_games/test_config_flow.py +++ b/tests/components/nyt_games/test_config_flow.py @@ -37,6 +37,27 @@ async def test_full_flow( assert result["result"].unique_id == "218886794" +async def test_stripping_token( + hass: HomeAssistant, + mock_nyt_games_client: AsyncMock, + mock_setup_entry: AsyncMock, +) -> None: + """Test stripping token.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_TOKEN: " token "}, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["data"] == {CONF_TOKEN: "token"} + + @pytest.mark.parametrize( ("exception", "error"), [ diff --git a/tests/components/template/test_alarm_control_panel.py b/tests/components/template/test_alarm_control_panel.py index 1532197d738..8890d790b87 100644 --- a/tests/components/template/test_alarm_control_panel.py +++ b/tests/components/template/test_alarm_control_panel.py @@ -23,6 +23,7 @@ from homeassistant.const import ( STATE_UNKNOWN, ) from homeassistant.core import Event, HomeAssistant, State, callback +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, assert_setup_component, mock_restore_cache @@ -503,3 +504,45 @@ async def test_restore_state( state = hass.states.get("alarm_control_panel.test_template_panel") assert state.state == initial_state + + +async def test_device_id( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test for device for button template.""" + + device_config_entry = MockConfigEntry() + device_config_entry.add_to_hass(hass) + device_entry = device_registry.async_get_or_create( + config_entry_id=device_config_entry.entry_id, + identifiers={("test", "identifier_test")}, + connections={("mac", "30:31:32:33:34:35")}, + ) + await hass.async_block_till_done() + assert device_entry is not None + assert device_entry.id is not None + + template_config_entry = MockConfigEntry( + data={}, + domain=template.DOMAIN, + options={ + "name": "My template", + "value_template": "disarmed", + "template_type": "alarm_control_panel", + "code_arm_required": True, + "code_format": "number", + "device_id": device_entry.id, + }, + title="My template", + ) + + template_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(template_config_entry.entry_id) + await hass.async_block_till_done() + + template_entity = entity_registry.async_get("alarm_control_panel.my_template") + assert template_entity is not None + assert template_entity.device_id == device_entry.id diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 9cba19ef3b1..92cec00ccdf 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -512,6 +512,41 @@ async def test_remove_entry( assert not entity_entry_list +async def test_remove_entry_non_unique_unique_id( + hass: HomeAssistant, + manager: config_entries.ConfigEntries, + entity_registry: er.EntityRegistry, +) -> None: + """Test that we can remove entry with colliding unique_id.""" + entry_1 = MockConfigEntry( + domain="test_other", entry_id="test1", unique_id="not_unique" + ) + entry_1.add_to_manager(manager) + entry_2 = MockConfigEntry( + domain="test_other", entry_id="test2", unique_id="not_unique" + ) + entry_2.add_to_manager(manager) + entry_3 = MockConfigEntry( + domain="test_other", entry_id="test3", unique_id="not_unique" + ) + entry_3.add_to_manager(manager) + + # Check all config entries exist + assert manager.async_entry_ids() == [ + "test1", + "test2", + "test3", + ] + + # Remove entries + assert await manager.async_remove("test1") == {"require_restart": False} + await hass.async_block_till_done() + assert await manager.async_remove("test2") == {"require_restart": False} + await hass.async_block_till_done() + assert await manager.async_remove("test3") == {"require_restart": False} + await hass.async_block_till_done() + + async def test_remove_entry_cancels_reauth( hass: HomeAssistant, manager: config_entries.ConfigEntries,