mirror of
https://github.com/home-assistant/core.git
synced 2025-07-18 18:57:06 +00:00
2024.10.1 (#127566)
This commit is contained in:
commit
2182bc3af2
@ -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."]
|
||||
}
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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}),
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -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."
|
||||
|
@ -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))
|
||||
|
@ -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 = (
|
||||
|
@ -47,6 +47,7 @@ PLATFORMS: Final = [
|
||||
Platform.DEVICE_TRACKER,
|
||||
Platform.LOCK,
|
||||
Platform.MEDIA_PLAYER,
|
||||
Platform.NUMBER,
|
||||
Platform.SELECT,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
|
@ -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,6 +1616,8 @@ 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]
|
||||
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]]]]):
|
||||
|
@ -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)
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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"),
|
||||
[
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user