Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 3 additions & 3 deletions .github/workflows/builder.yml
Original file line number Diff line number Diff line change
Expand Up @@ -342,13 +342,13 @@ jobs:

- name: Login to DockerHub
if: matrix.registry == 'docker.io/homeassistant'
uses: docker/login-action@371161bbe7024a29a25c5e19bfcbc0804fe9ad2c # v4.5.2
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Login to GitHub Container Registry
uses: docker/login-action@371161bbe7024a29a25c5e19bfcbc0804fe9ad2c # v4.5.2
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
Expand Down Expand Up @@ -521,7 +521,7 @@ jobs:
persist-credentials: false

- name: Login to GitHub Container Registry
uses: docker/login-action@371161bbe7024a29a25c5e19bfcbc0804fe9ad2c # v4.5.2
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
Expand Down
1 change: 1 addition & 0 deletions .strict-typing
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@ homeassistant.components.homekit_controller.storage
homeassistant.components.homekit_controller.utils
homeassistant.components.homewizard.*
homeassistant.components.homeworks.*
homeassistant.components.hortimax.*
homeassistant.components.hr_energy_qube.*
homeassistant.components.http.*
homeassistant.components.huawei_lte.*
Expand Down
2 changes: 2 additions & 0 deletions CODEOWNERS

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 25 additions & 3 deletions homeassistant/components/alexa_devices/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
"""Alexa Devices integration."""

from collections.abc import Awaitable, Callable

from homeassistant.const import CONF_COUNTRY, EVENT_HOMEASSISTANT_STOP, Platform
from homeassistant.core import Event, HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client, config_validation as cv, httpx_client
from homeassistant.helpers.typing import ConfigType
from homeassistant.util.ssl import SSL_ALPN_HTTP11_HTTP2
Expand Down Expand Up @@ -31,6 +34,22 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
return True


async def _async_initial_sync(sync_call: Callable[[], Awaitable[None]]) -> None:
"""Run an initial best-effort sync call.

These syncs are not required for setup to succeed: a failing Amazon API
call must not prevent the other syncs from running or abort setup.
"""
try:
await sync_call()
except ConfigEntryNotReady as err:
LOGGER.warning(
"Initial sync failed for %s: %s. Data may be missing or incomplete until updates are pushed by Amazon",
sync_call.__name__,
err,
)


async def async_setup_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool:
"""Set up Alexa Devices platform."""

Expand All @@ -39,9 +58,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bo

await coordinator.async_config_entry_first_refresh()

await coordinator.sync_todo_list_items()
await coordinator.sync_history_state()
await coordinator.sync_media_state()
for sync_call in (
coordinator.sync_todo_list_items,
coordinator.sync_history_state,
coordinator.sync_media_state,
):
await _async_initial_sync(sync_call)

async def _on_http2_reauth_required() -> None:
entry.async_start_reauth(hass)
Expand Down
4 changes: 3 additions & 1 deletion homeassistant/components/alexa_devices/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,9 @@ async def sync_todo_list_items(self) -> None:
async def todo_event_handler(self, list_event: AmazonListEvent) -> None:
"""Handle changes on To-Do lists."""
if list_event.type == AmazonListEventType.DELETED:
self._todo_list_items[list_event.list_id].pop(list_event.item_id, None)
self._todo_list_items.get(list_event.list_id, {}).pop(
list_event.item_id, None
)
elif (
list_event.type
in (AmazonListEventType.UPDATED, AmazonListEventType.CREATED)
Expand Down
37 changes: 24 additions & 13 deletions homeassistant/components/conversation/default_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -409,10 +409,13 @@ async def async_debug_recognize(
}

if successful_match:
satellite_area, _ = self._get_satellite_area_and_device(
user_input.satellite_id, user_input.device_id
)
result_dict["targets"] = {
state.entity_id: {"matched": is_matched}
for state, is_matched in _get_debug_targets(
self.hass, intent_result
self.hass, intent_result, satellite_area
)
}

Expand Down Expand Up @@ -1676,12 +1679,14 @@ def _collect_list_references(expression: Expression, list_names: set[str]) -> No
def _get_debug_targets(
hass: HomeAssistant,
result: RecognizeResult,
satellite_area: ar.AreaEntry | None = None,
) -> Iterable[tuple[State, bool]]:
"""Yield state/is_matched pairs for a hassil recognition."""
entities = result.entities

name: str | None = None
area_name: str | None = None
floor_name: str | None = None
domains: set[str] | None = None
device_classes: set[str] | None = None
state_names: set[str] | None = None
Expand All @@ -1692,6 +1697,9 @@ def _get_debug_targets(
if "area" in entities:
area_name = str(entities["area"].value)

if "floor" in entities:
floor_name = str(entities["floor"].value)

if "domain" in entities:
domains = set(cv.ensure_list(entities["domain"].value))

Expand All @@ -1702,24 +1710,27 @@ def _get_debug_targets(
# HassGetState only
state_names = set(cv.ensure_list(entities["state"].value))

if (
(name is None)
and (area_name is None)
and (not domains)
and (not device_classes)
and (not state_names)
):
# Avoid "matching" all entities when there is no filter
return

states = intent.async_match_states(
hass,
constraints = intent.MatchTargetsConstraints(
name=name,
area_name=area_name,
floor_name=floor_name,
domains=domains,
device_classes=device_classes,
assistant=DOMAIN,
)

if not (constraints.has_constraints or state_names):
# Avoid "matching" all entities when there is no filter
return

# Mirror the preferences used when the intent is actually handled so that
# duplicate names are deduplicated the same way.
preferences = intent.MatchTargetsPreferences(
area_id=satellite_area.id if satellite_area is not None else None
)

states = intent.async_match_targets(hass, constraints, preferences).states

for state in states:
# For queries, a target is "matched" based on its state
is_matched = (state_names is None) or (state.state in state_names)
Expand Down
17 changes: 15 additions & 2 deletions homeassistant/components/duco/__init__.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
"""The Duco integration."""

import re
from typing import TYPE_CHECKING

from duco_connectivity import DucoClient

from homeassistant.const import CONF_HOST
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.helpers.aiohttp_client import async_get_clientsession

from .const import PLATFORMS
from .const import BOX_NODE_ID, DOMAIN, PLATFORMS
from .coordinator import DucoConfigEntry, DucoCoordinator

_REMOVED_SENSOR_RE = re.compile(r"_\d+_(box_)?temperature$")
Expand Down Expand Up @@ -37,6 +38,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: DucoConfigEntry) -> bool

entry.runtime_data = coordinator

# Pre-register the box before forwarding platforms so sub-node entities can
# resolve it as their via_device parent regardless of entity setup order.
mac = entry.unique_id
if TYPE_CHECKING:
assert mac is not None
device_registry = dr.async_get(hass)
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, f"{mac}_{BOX_NODE_ID}")},
connections={(dr.CONNECTION_NETWORK_MAC, mac)},
)

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

return True
Expand Down
12 changes: 10 additions & 2 deletions homeassistant/components/duco/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from duco_connectivity.models import Node, NodeType

from homeassistant.const import ATTR_VIA_DEVICE
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity

Expand Down Expand Up @@ -38,7 +38,15 @@ def __init__(self, coordinator: DucoCoordinator, node: Node) -> None:
"serial_number": coordinator.board_info.serial_board_box,
}
if node.general.node_type == NodeType.BOX
else {ATTR_VIA_DEVICE: (DOMAIN, f"{mac}_1")}
# The box is pre-registered in async_setup_entry, so it is always
# resolvable here even if a sub-node entity is added first.
else {
"via_device_id": dr.async_get_device_id_by_identifier(
coordinator.hass,
(DOMAIN, f"{mac}_1"),
config_entry_id=coordinator.config_entry.entry_id,
)
}
)
self._attr_device_info = device_info

Expand Down
7 changes: 6 additions & 1 deletion homeassistant/components/hive/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: HiveConfigEntry) -> bool
connections.add((dr.CONNECTION_NETWORK_MAC, mac))

device_registry = dr.async_get(hass)
device_registry.async_get_or_create(
hub_device = device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, hub_data["device_id"])},
connections=connections,
Expand All @@ -59,6 +59,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: HiveConfigEntry) -> bool
sw_version=hub_data["deviceData"]["version"],
manufacturer=hub_data["deviceData"]["manufacturer"],
)
if hub_device.via_device_id is not None:
# Older versions linked the hub's own diagnostic sensor to the hub itself;
# clear the stale self-reference since async_get_or_create leaves
# via_device_id untouched when it's not passed.
device_registry.async_update_device(hub_device.id, via_device_id=None)

await hass.config_entries.async_forward_entry_setups(
entry,
Expand Down
14 changes: 9 additions & 5 deletions homeassistant/components/hive/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,22 @@ def __init__(
self.device = hive_device
self._attr_name = self.device["haName"]
self._attr_unique_id = f"{self.device['hiveID']}-{self.device['hiveType']}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self.device["device_id"])},
device_id = self.device["device_id"]
device_info = DeviceInfo(
identifiers={(DOMAIN, device_id)},
model=self.device["deviceData"]["model"],
manufacturer=self.device["deviceData"]["manufacturer"],
name=self.device["device_name"],
sw_version=self.device["deviceData"]["version"],
via_device_id=dr.async_get_device_id_by_identifier(
)
# Hive reports the hub itself as its parent.
if self.device["parentDevice"] != device_id:
device_info["via_device_id"] = dr.async_get_device_id_by_identifier(
hass,
(DOMAIN, self.device["parentDevice"]),
config_entry_id=entry.entry_id,
),
)
)
self._attr_device_info = device_info
self.attributes: dict[str, Any] = {}

@override
Expand Down
44 changes: 44 additions & 0 deletions homeassistant/components/hortimax/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""The Ridder HortiMaX Pro (HortOS) integration."""

from aiohortos import HortosClient

from homeassistant.const import CONF_API_KEY, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession

from .const import DOMAIN, MANUFACTURER
from .coordinator import HortimaxConfigEntry, HortimaxCoordinator

PLATFORMS: list[Platform] = [Platform.SENSOR]


async def async_setup_entry(hass: HomeAssistant, entry: HortimaxConfigEntry) -> bool:
"""Set up Ridder HortiMaX Pro from a config entry."""
client = HortosClient(
entry.data[CONF_API_KEY], session=async_get_clientsession(hass)
)
coordinator = HortimaxCoordinator(hass, entry, client)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator

# Registered up front so the sensor platform's source devices can point at
# them through via_device.
device_registry = dr.async_get(hass)
for device in coordinator.devices:
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, device.name)},
manufacturer=MANUFACTURER,
name=device.label or device.name,
model="HortiMaX Pro",
serial_number=device.name,
)

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True


async def async_unload_entry(hass: HomeAssistant, entry: HortimaxConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
Loading
Loading