Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
e3cc4ac
Remove deprecated `max_health`, `habits` and `rewards` sensors from H…
tr4nt0r Jul 8, 2025
b151a9b
Add missing connection for gardena ble device (#148376)
elupus Jul 8, 2025
4b8dcc3
Bump holidays to 0.76 (#148363)
gjohansson-ST Jul 8, 2025
19951d9
Handle when heat pump rejects same value writes in nibe_heatpump (#14…
elupus Jul 8, 2025
9ce03c7
Switch to box default for numbers in nibe_heatpump integration (#148364)
elupus Jul 8, 2025
f478812
Allow multiple set-cookie headers with hassio ingress (#148148)
RubenNL Jul 8, 2025
7875290
Adds claude-code feature to the devcontainer (#148338)
ludeeus Jul 8, 2025
b0f7c98
Add snapshots tests for new platforms in tuya (#148334)
epenet Jul 8, 2025
ccc80c7
Add huawei_lte device registry upnp udn connection (#148370)
scop Jul 8, 2025
dcf8d7f
Track ESPHome entities by (device_id, key) to support sub-devices wit…
bdraco Jul 8, 2025
7a7e16b
Change how subscription information is fetched (#148337)
ludeeus Jul 8, 2025
f780b97
Add support for ELV-SH-CTV Sensor to homematicip_cloud (#143737)
hahn-th Jul 8, 2025
87b00fd
Emoncms add reconfigure flow (#145108)
alexandrecuer Jul 8, 2025
73730e3
Bump aiolifx to 1.2.0 (#148382)
Djelibeybi Jul 8, 2025
6d0891e
OpenAI: Extract file attachment logic (#148288)
balloob Jul 8, 2025
d44b822
Add play media support to Russound RIO (#148240)
noahhusby Jul 8, 2025
ac5d4f4
Fix CI issues due to nibe heatpump (#148388)
elupus Jul 8, 2025
0dc145a
Fix tuya vacuum return_to_base function (#144362)
mjc0608 Jul 8, 2025
a77a071
Bump aioamazondevices to 3.2.8 (#148365)
chemelli74 Jul 8, 2025
f58c76c
Fix error when `personalDetail` is missing in PlayStation Network int…
tr4nt0r Jul 8, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"PYTHONASYNCIODEBUG": "1"
},
"features": {
"ghcr.io/anthropics/devcontainer-features/claude-code:1.0": {},
"ghcr.io/devcontainers/features/github-cli:1": {}
},
// Port 5683 udp is used by Shelly integration
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/alexa_devices/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["aioamazondevices"],
"quality_scale": "bronze",
"requirements": ["aioamazondevices==3.2.3"]
"requirements": ["aioamazondevices==3.2.8"]
}
6 changes: 3 additions & 3 deletions homeassistant/components/cloud/repairs.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
from __future__ import annotations

import asyncio
from typing import Any

from hass_nabucasa.payments_api import SubscriptionInfo
import voluptuous as vol

from homeassistant.components.repairs import (
Expand All @@ -26,7 +26,7 @@
@callback
def async_manage_legacy_subscription_issue(
hass: HomeAssistant,
subscription_info: dict[str, Any],
subscription_info: SubscriptionInfo,
) -> None:
"""Manage the legacy subscription issue.

Expand All @@ -50,7 +50,7 @@ class LegacySubscriptionRepairFlow(RepairsFlow):
"""Handler for an issue fixing flow."""

wait_task: asyncio.Task | None = None
_data: dict[str, Any] | None = None
_data: SubscriptionInfo | None = None

async def async_step_init(self, _: None = None) -> FlowResult:
"""Handle the first step of a fix flow."""
Expand Down
17 changes: 5 additions & 12 deletions homeassistant/components/cloud/subscription.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,21 @@

from aiohttp.client_exceptions import ClientError
from hass_nabucasa import Cloud, cloud_api
from hass_nabucasa.payments_api import PaymentsApiError, SubscriptionInfo

from .client import CloudClient
from .const import REQUEST_TIMEOUT

_LOGGER = logging.getLogger(__name__)


async def async_subscription_info(cloud: Cloud[CloudClient]) -> dict[str, Any] | None:
async def async_subscription_info(cloud: Cloud[CloudClient]) -> SubscriptionInfo | None:
"""Fetch the subscription info."""
try:
async with asyncio.timeout(REQUEST_TIMEOUT):
return await cloud_api.async_subscription_info(cloud)
except TimeoutError:
_LOGGER.error(
(
"A timeout of %s was reached while trying to fetch subscription"
" information"
),
REQUEST_TIMEOUT,
)
except ClientError:
_LOGGER.error("Failed to fetch subscription information")
return await cloud.payments.subscription_info()
except PaymentsApiError as exception:
_LOGGER.error("Failed to fetch subscription information - %s", exception)

return None

Expand Down
41 changes: 41 additions & 0 deletions homeassistant/components/emoncms/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,47 @@ async def async_step_choose_feeds(
errors=errors,
)

async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Reconfigure the entry."""
errors: dict[str, str] = {}
description_placeholders = {}
reconfig_entry = self._get_reconfigure_entry()
if user_input is not None:
url = user_input[CONF_URL]
api_key = user_input[CONF_API_KEY]
emoncms_client = EmoncmsClient(
url, api_key, session=async_get_clientsession(self.hass)
)
result = await get_feed_list(emoncms_client)
if not result[CONF_SUCCESS]:
errors["base"] = "api_error"
description_placeholders = {"details": result[CONF_MESSAGE]}
else:
await self.async_set_unique_id(await emoncms_client.async_get_uuid())
self._abort_if_unique_id_mismatch()
return self.async_update_reload_and_abort(
reconfig_entry,
title=sensor_name(url),
data=user_input,
reload_even_if_entry_is_unchanged=False,
)
return self.async_show_form(
step_id="reconfigure",
data_schema=self.add_suggested_values_to_schema(
vol.Schema(
{
vol.Required(CONF_URL): str,
vol.Required(CONF_API_KEY): str,
}
),
user_input or reconfig_entry.data,
),
errors=errors,
description_placeholders=description_placeholders,
)


class EmoncmsOptionsFlow(OptionsFlow):
"""Emoncms Options flow handler."""
Expand Down
4 changes: 3 additions & 1 deletion homeassistant/components/emoncms/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@
}
},
"abort": {
"already_configured": "This server is already configured"
"already_configured": "This server is already configured",
"unique_id_mismatch": "This emoncms serial number does not match the previous serial number",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
}
},
"selector": {
Expand Down
82 changes: 74 additions & 8 deletions homeassistant/components/esphome/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,12 @@
from .const import DOMAIN

# Import config flow so that it's added to the registry
from .entry_data import ESPHomeConfigEntry, RuntimeEntryData, build_device_unique_id
from .entry_data import (
DeviceEntityKey,
ESPHomeConfigEntry,
RuntimeEntryData,
build_device_unique_id,
)
from .enum_mapper import EsphomeEnumMapper

_LOGGER = logging.getLogger(__name__)
Expand All @@ -59,17 +64,32 @@ def async_static_info_updated(
device_info = entry_data.device_info
if TYPE_CHECKING:
assert device_info is not None
new_infos: dict[int, EntityInfo] = {}
new_infos: dict[DeviceEntityKey, EntityInfo] = {}
add_entities: list[_EntityT] = []

ent_reg = er.async_get(hass)
dev_reg = dr.async_get(hass)

# Track info by (info.device_id, info.key) to properly handle entities
# moving between devices and support sub-devices with overlapping keys
for info in infos:
new_infos[info.key] = info
info_key = (info.device_id, info.key)
new_infos[info_key] = info

# Try to find existing entity - first with current device_id
old_info = current_infos.pop(info_key, None)

# If not found, search for entity with same key but different device_id
# This handles the case where entity moved between devices
if not old_info:
for existing_device_id, existing_key in list(current_infos):
if existing_key == info.key:
# Found entity with same key but different device_id
old_info = current_infos.pop((existing_device_id, existing_key))
break

# Create new entity if it doesn't exist
if not (old_info := current_infos.pop(info.key, None)):
if not old_info:
entity = entity_type(entry_data, platform.domain, info, state_type)
add_entities.append(entity)
continue
Expand All @@ -78,7 +98,7 @@ def async_static_info_updated(
if old_info.device_id == info.device_id:
continue

# Entity has switched devices, need to migrate unique_id
# Entity has switched devices, need to migrate unique_id and handle state subscriptions
old_unique_id = build_device_unique_id(device_info.mac_address, old_info)
entity_id = ent_reg.async_get_entity_id(platform.domain, DOMAIN, old_unique_id)

Expand All @@ -103,7 +123,7 @@ def async_static_info_updated(
if old_unique_id != new_unique_id:
updates["new_unique_id"] = new_unique_id

# Update device assignment
# Update device assignment in registry
if info.device_id:
# Entity now belongs to a sub device
new_device = dev_reg.async_get_device(
Expand All @@ -118,10 +138,32 @@ def async_static_info_updated(
if new_device:
updates["device_id"] = new_device.id

# Apply all updates at once
# Apply all registry updates at once
if updates:
ent_reg.async_update_entity(entity_id, **updates)

# IMPORTANT: The entity's device assignment in Home Assistant is only read when the entity
# is first added. Updating the registry alone won't move the entity to the new device
# in the UI. Additionally, the entity's state subscription is tied to the old device_id,
# so it won't receive state updates for the new device_id.
#
# We must remove the old entity and re-add it to ensure:
# 1. The entity appears under the correct device in the UI
# 2. The entity's state subscription is updated to use the new device_id
_LOGGER.debug(
"Entity %s moving from device_id %s to %s",
info.key,
old_info.device_id,
info.device_id,
)

# Signal the existing entity to remove itself
# The entity is registered with the old device_id, so we signal with that
entry_data.async_signal_entity_removal(info_type, old_info.device_id, info.key)

# Create new entity with the new device_id
add_entities.append(entity_type(entry_data, platform.domain, info, state_type))

# Anything still in current_infos is now gone
if current_infos:
entry_data.async_remove_entities(
Expand Down Expand Up @@ -341,16 +383,40 @@ async def async_added_to_hass(self) -> None:
)
self.async_on_remove(
entry_data.async_subscribe_state_update(
self._state_type, self._key, self._on_state_update
self._static_info.device_id,
self._state_type,
self._key,
self._on_state_update,
)
)
self.async_on_remove(
entry_data.async_register_key_static_info_updated_callback(
self._static_info, self._on_static_info_update
)
)
# Register to be notified when this entity should remove itself
# This happens when the entity moves to a different device
self.async_on_remove(
entry_data.async_register_entity_removal_callback(
type(self._static_info),
self._static_info.device_id,
self._key,
self._on_removal_signal,
)
)
self._update_state_from_entry_data()

@callback
def _on_removal_signal(self) -> None:
"""Handle signal to remove this entity."""
_LOGGER.debug(
"Entity %s received removal signal due to device_id change",
self.entity_id,
)
# Schedule the entity to be removed
# This must be done as a task since we're in a callback
self.hass.async_create_task(self.async_remove())

@callback
def _on_static_info_update(self, static_info: EntityInfo) -> None:
"""Save the static info for this entity when it changes.
Expand Down
Loading
Loading