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/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1375,7 +1375,7 @@ jobs:
pattern: coverage-*
- name: Upload coverage to Codecov
if: needs.info.outputs.test_full_suite == 'true'
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
uses: codecov/codecov-action@0b35c9ecc4f0529d0eb674914510c22f85b196b4 # v7.1.0
with:
fail_ci_if_error: true
flags: full-suite
Expand Down Expand Up @@ -1528,7 +1528,7 @@ jobs:
pattern: coverage-*
- name: Upload coverage to Codecov
if: needs.info.outputs.test_full_suite == 'false'
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
uses: codecov/codecov-action@0b35c9ecc4f0529d0eb674914510c22f85b196b4 # v7.1.0
with:
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }} # zizmor: ignore[secrets-outside-env]
Expand Down Expand Up @@ -1556,7 +1556,7 @@ jobs:
with:
pattern: test-results-*
- name: Upload test results to Codecov
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
uses: codecov/codecov-action@0b35c9ecc4f0529d0eb674914510c22f85b196b4 # v7.1.0
with:
report_type: test_results
fail_ci_if_error: true
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/openrgb/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "local_polling",
"quality_scale": "silver",
"requirements": ["openrgb-python==0.3.6"]
"requirements": ["openrgb-python==0.3.7"]
}
59 changes: 46 additions & 13 deletions homeassistant/components/portainer/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
Portainer,
PortainerAuthenticationError,
PortainerConnectionError,
PortainerError,
PortainerEventListener,
PortainerEventListenerResult,
PortainerTimeoutError,
Expand Down Expand Up @@ -217,6 +218,9 @@ def __init__(
self._image_cache: dict[
tuple[int, str], tuple[float, LocalImageInformation]
] = {}
self._image_status_cache: dict[
tuple[int, str], tuple[float, PortainerImageUpdateStatus]
] = {}
self._event_listeners: dict[int, PortainerEventListener] = {}
self._event_listeners_enabled = False
self._container_ids_by_endpoint: dict[int, dict[str, str]] = {}
Expand Down Expand Up @@ -341,19 +345,7 @@ async def update_data(self) -> dict[int, PortainerCoordinatorData]:
container_inspect = container_inspects[container_name]
local_image = local_images[container_name]

image_status = (
(
result.status
if (
result := self.watcher.results.get(
(endpoint.id, container.id)
)
)
else None
)
if self.watcher
else None
)
image_status = await self._get_image_status(endpoint.id, container)

# Check if container belongs to a stack via docker compose label
stack_name: str | None = (
Expand Down Expand Up @@ -618,6 +610,47 @@ async def _get_local_image(
)
return local_image

async def _get_image_status(
self, endpoint_id: int, container: DockerContainer
) -> PortainerImageUpdateStatus | None:
"""Return the image update status, checking containers the watcher has not seen."""
if self.watcher is None:
return None

if result := self.watcher.results.get((endpoint_id, container.id)):
return result.status

# A recreated container gets a new ID, which the watcher only picks up on
# its next run. Check its image now instead of reporting unknown until then.
if (
self.watcher.last_check is None
or container.state != DockerContainerState.RUNNING
or not container.image
):
return None

cache_key = (endpoint_id, container.image)
if cached := self._image_status_cache.get(cache_key):
cached_at, image_status = cached
if cached_at >= self.watcher.last_check:
return image_status

try:
image_status = await self.portainer.container_image_status(
endpoint_id, container.image
)
except PortainerError as err:
_LOGGER.debug(
"Failed to check image %s on endpoint %d: %s",
container.image,
endpoint_id,
err,
)
return None

self._image_status_cache[cache_key] = (time.time(), image_status)
return image_status

def _async_sync_event_listeners(
self, mapped_endpoints: dict[int, PortainerCoordinatorData]
) -> None:
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/recorder/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"iot_class": "local_push",
"quality_scale": "internal",
"requirements": [
"SQLAlchemy==2.0.52",
"SQLAlchemy==2.0.53",
"fnv-hash-fast==2.0.3",
"psutil-home-assistant==0.0.1"
]
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/sftp_storage/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@
"integration_type": "service",
"iot_class": "local_polling",
"quality_scale": "silver",
"requirements": ["asyncssh==2.23.1"]
"requirements": ["asyncssh==2.24.0"]
}
60 changes: 15 additions & 45 deletions homeassistant/components/sleepiq/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from asyncsleepiq import (
AsyncSleepIQ,
SleepIQAPIException,
SleepIQBed,
SleepIQConnectionException,
SleepIQLoginException,
SleepIQTimeoutException,
Expand Down Expand Up @@ -127,57 +126,28 @@ async def async_unload_entry(hass: HomeAssistant, entry: SleepIQConfigEntry) ->
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)


def _foundation_feature_count(bed: SleepIQBed) -> int:
"""Count foundation features on a bed."""
f = bed.foundation
return (
len(f.lights)
+ len(f.actuators)
+ len(f.presets)
+ len(f.foot_warmers)
+ len(f.core_climates)
)
_FIRMNESS_CONTROL = "firmness control"


def _filter_duplicate_beds(gateway: AsyncSleepIQ) -> None:
"""Remove duplicate bed objects that share sleeper IDs.
"""Remove "Firmness Control" controller objects that duplicate a real bed.

Groups beds whose sleeper-ID sets overlap and keeps the one with
the most foundation features so the real bed survives regardless
of API ordering.
The SleepIQ API can return a controller object alongside the real bed,
both carrying the same sleeper IDs. The controller is identified by
"Firmness Control" in its model string.
"""
groups: dict[frozenset[str], list[str]] = {}
to_remove: list[str] = []
for bed_id, bed in gateway.beds.items():
bed_sleeper_ids = frozenset(s.sleeper_id for s in bed.sleepers if s.sleeper_id)
if not bed_sleeper_ids:
continue
matched = None
for key in groups:
if key & bed_sleeper_ids:
matched = key
break
if matched is not None:
groups[matched].append(bed_id)
else:
groups[bed_sleeper_ids] = [bed_id]

for bed_ids in groups.values():
if len(bed_ids) < 2:
continue
best = max(
bed_ids, key=lambda bid: _foundation_feature_count(gateway.beds[bid])
if _FIRMNESS_CONTROL in (bed.model or "").lower():
to_remove.append(bed_id)
for bed_id in to_remove:
_LOGGER.debug(
"Removing controller duplicate '%s' (id=%s, model=%s)",
gateway.beds[bed_id].name,
bed_id,
gateway.beds[bed_id].model,
)
for bed_id in bed_ids:
if bed_id != best:
_LOGGER.debug(
"Removing duplicate bed '%s' (id=%s), keeping '%s' (id=%s)"
" which has more foundation features",
gateway.beds[bed_id].name,
bed_id,
gateway.beds[best].name,
best,
)
del gateway.beds[bed_id]
del gateway.beds[bed_id]


async def _async_migrate_unique_ids(
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/sql/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/sql",
"iot_class": "local_polling",
"requirements": ["SQLAlchemy==2.0.52", "sqlparse==0.6.0"]
"requirements": ["SQLAlchemy==2.0.53", "sqlparse==0.6.0"]
}
38 changes: 16 additions & 22 deletions homeassistant/helpers/target.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,18 @@ def log_missing(self, missing_entities: set[str], logger: Logger) -> None:
)


@callback
def _add_referenced_device(
dev_reg: dr.DeviceRegistry, device_id: str, selected: SelectedEntities
) -> None:
"""Add a device and its child devices."""
selected.referenced_devices.add(device_id)
selected.referenced_devices.update(
child_device.id
for child_device in dr.async_entries_for_parent_device(dev_reg, device_id)
)


@callback
def _resolve_referenced_devices(
dev_reg: dr.DeviceRegistry, device_ids: set[str], selected: SelectedEntities
Expand All @@ -165,32 +177,17 @@ def _resolve_referenced_devices(
if device is None:
selected.missing_devices.add(device_id)
selected.referenced_devices.add(device_id)
elif isinstance(device, dr.ChildDeviceEntry):
selected.referenced_devices.add(device_id)
elif split_devices := dev_reg.async_get_devices_for_composite_device_id(
device_id
):
# A multi config entry composite device id is no longer a device itself;
# it resolves to the devices it was split into so actions targeting it
# still trickle down. Only the splits are referenced, not the composite id,
# so a device-id consumer does not act on the same underlying device twice.
# Each split's children are included too, matching the direct-device branch.
for split_device in split_devices:
selected.referenced_devices.add(split_device.id)
selected.referenced_devices.update(
child_device.id
for child_device in dr.async_entries_for_parent_device(
dev_reg, split_device.id
)
)
_add_referenced_device(dev_reg, split_device.id, selected)
else:
selected.referenced_devices.add(device_id)
selected.referenced_devices.update(
child_device.id
for child_device in dr.async_entries_for_parent_device(
dev_reg, device_id
)
)
_add_referenced_device(dev_reg, device_id, selected)


def async_extract_referenced_entity_ids(
Expand Down Expand Up @@ -255,12 +252,9 @@ def async_extract_referenced_entity_ids(
if entity_entry.hidden_by is None:
selected.indirectly_referenced.add(entity_entry.entity_id)

# Labels are never inherited by child devices (see
# dr.async_entries_for_label): a labeled parent is not expanded into its
# children. Only devices that carry the label themselves are targeted,
# which is consistent with template label_devices() and search.
# Labeled devices expand like directly targeted devices, children included.
for device_entry in dr.async_entries_for_label(dev_reg, label_id):
selected.referenced_devices.add(device_entry.id)
_add_referenced_device(dev_reg, device_entry.id, selected)

for area_entry in area_reg.areas.get_areas_for_label(label_id):
selected.referenced_areas.add(area_entry.id)
Expand Down
4 changes: 2 additions & 2 deletions homeassistant/package_constraints.txt
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,13 @@ PyYAML==6.0.3
requests==2.34.2
securetar==2026.4.1
serialx==1.10.0
SQLAlchemy==2.0.52
SQLAlchemy==2.0.53
standard-aifc==3.13.0
standard-telnetlib==3.13.0
typing-extensions>=4.16.0,<5.0
ulid-transform==2.2.9
urllib3>=2.0
uv==0.12.13
uv==0.12.14
webrtc-models==0.3.0
yarl==1.25.1
zeroconf==0.151.3
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,13 @@ dependencies = [
"PyYAML==6.0.3",
"requests==2.34.2",
"securetar==2026.4.1",
"SQLAlchemy==2.0.52",
"SQLAlchemy==2.0.53",
"standard-aifc==3.13.0",
"standard-telnetlib==3.13.0",
"typing-extensions>=4.16.0,<5.0",
"ulid-transform==2.2.9",
"urllib3>=2.0",
"uv==0.12.13",
"uv==0.12.14",
"probatio==0.12.1",
"yarl==1.25.1",
"webrtc-models==0.3.0",
Expand Down
4 changes: 2 additions & 2 deletions requirements.txt

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

6 changes: 3 additions & 3 deletions requirements_all.txt

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

2 changes: 1 addition & 1 deletion requirements_test.txt
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,4 @@ types-pytz==2026.3.1.20260727
types-PyYAML==6.0.12.20260724
types-requests==2.33.0.20260712
types-xmltodict==1.0.1.20260518
unidiff==1.0.0
unidiff==1.0.1
2 changes: 1 addition & 1 deletion script/check_requirements/requirements.txt

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

Loading
Loading