Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
4d1ad0e
Prevent second Z-Wave JS config entry using the add-on (#179405)
balloobbot Aug 20, 2026
ac63da9
Log warning when event triggers filter on composite device (#179490)
emontnemery Aug 20, 2026
2c87f11
Fix UniFi SmartPower outlet power sensor detection (#178529)
jimstrang Aug 20, 2026
02b5b95
Migrate integrations to call DeviceRegistry.async_get (#179571)
emontnemery Aug 20, 2026
9470ea9
Fix flaky statistics_meta duplicate tests on PostgreSQL (#179596)
balloob Aug 20, 2026
255dd0b
Fix the Lyngdorf volume range (#179572)
fishloa Aug 20, 2026
c30101f
Bump github/codeql-action/analyze from 4.37.6 to 4.37.7 (#179612)
dependabot[bot] Aug 20, 2026
691db68
Bump github/codeql-action/init from 4.37.6 to 4.37.7 (#179610)
dependabot[bot] Aug 20, 2026
ddefcf2
Update postgresql versions and add postgresql 18 in the CI (#179533)
edenhaus Aug 20, 2026
62c8b01
Update RestrictedPython to 8.5 (#179554)
cdce8p Aug 20, 2026
e778e67
Add direction prefix to androidtv_remote send_command (#170117)
PassionateBytes Aug 20, 2026
639e8f3
Add web session injection in Actron Air (#179563)
kclif9 Aug 20, 2026
0c614fa
Add `next_flow` to `RepairsFlow` (#172489)
iluvdata Aug 20, 2026
ea8c1fd
Add Leviton VRF01 fan controller discovery to zwave_js (#175631)
gdrapp Aug 20, 2026
a0795b3
Prevent Sonos from pinging disabled device (#176893)
PeteRager Aug 20, 2026
8e36e85
Add support for Tuya HCDD category (Chasing Tape Light) (#177044)
AnthonyZavala Aug 20, 2026
73d9236
Fix swallowed exceptions in action handlers for NFAndroidTV (#177417)
mstu01 Aug 20, 2026
05a7749
Report the outcome of adding a Thread dataset (#178306)
LorbusChris Aug 20, 2026
36c3588
Update brightness optimistically when turning on a dimmable light (#1…
speijnik Aug 20, 2026
e836c1c
Fix loading IP bans without banned_at (#178607)
tollenceld Aug 20, 2026
43ec449
Add Monzo spent today sensor (#179069)
JakeMartin-ICL Aug 20, 2026
657c3bc
Bump lunatone-rest-api-client to 0.10.0 (#179562)
MoonDevLT Aug 20, 2026
ace0e5a
Fix Sonos disabled-device test using deprecated async_get_device (#17…
balloob Aug 20, 2026
840d981
Add move local data option to hassio mount failed repair (#179369)
agners Aug 20, 2026
06df20d
Split user flow init and data submission in NAM tests (#179575)
bieniu Aug 20, 2026
0c5bac8
Split user flow init and data submission in Brother tests (#179577)
bieniu Aug 20, 2026
7e107da
Use clearer icons for Rachio standby and rain delay (#179621)
kingces95 Aug 20, 2026
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
13 changes: 9 additions & 4 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,12 @@ env:
# and some queries that work on MariaDB do not work on MySQL
MARIADB_VERSIONS: "['mariadb:10.3.32','mariadb:10.6.10','mariadb:10.10.3','mariadb:10.11.2','mariadb:11.4.9','mysql:8.0.32']"
# 12 is the oldest supported version
# - 12.14 is the latest (as of 9 Feb 2023)
# 15 is the latest version
# - 15.2 is the latest (as of 9 Feb 2023)
POSTGRESQL_VERSIONS: "['postgres:12.14','postgres:15.2']"
# - 12.22 is the latest (as of 19 Aug 2026)
# 15 is a supported version
# - 15.19 is the latest (as of 19 Aug 2026)
# 18 is the latest version
# - 18.6 is the latest (as of 19 Aug 2026)
POSTGRESQL_VERSIONS: "['postgres:12.22','postgres:15.19', 'postgres:18.6']"
UV_CACHE_DIR: /tmp/uv-cache
APT_CACHE_VERSION: 1
SQLALCHEMY_WARN_20: 1
Expand Down Expand Up @@ -1064,6 +1066,9 @@ jobs:
- 5432:5432
env:
POSTGRES_PASSWORD: password
# 18+ images default PGDATA to /var/lib/postgresql/<major>/docker,
# which is not covered by the tmpfs below
PGDATA: /var/lib/postgresql/data
options: >-
--health-cmd="pg_isready -hlocalhost -Upostgres"
--health-interval=5s --health-timeout=2s --health-retries=3
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@ jobs:
persist-credentials: false

- name: Initialize CodeQL
uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
uses: github/codeql-action/init@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
with:
languages: python

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
uses: github/codeql-action/analyze@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
with:
category: "/language:python"
6 changes: 5 additions & 1 deletion homeassistant/components/actron_air/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession

from .const import DOMAIN, LOGGER
from .coordinator import (
Expand All @@ -27,7 +28,10 @@
async def async_setup_entry(hass: HomeAssistant, entry: ActronAirConfigEntry) -> bool:
"""Set up Actron Air integration from a config entry."""

api = ActronAirAPI(refresh_token=entry.data[CONF_API_TOKEN])
api = ActronAirAPI(
refresh_token=entry.data[CONF_API_TOKEN],
session=async_get_clientsession(hass),
)
systems: list[ActronAirSystemInfo] = []

try:
Expand Down
3 changes: 2 additions & 1 deletion homeassistant/components/actron_air/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
)
from homeassistant.const import CONF_API_TOKEN
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession

from .const import DOMAIN, LOGGER

Expand All @@ -37,7 +38,7 @@ async def async_step_user(
"""Handle the initial step."""
if self._api is None:
LOGGER.debug("Initiating device authorization")
self._api = ActronAirAPI()
self._api = ActronAirAPI(session=async_get_clientsession(self.hass))
try:
device_code_response = await self._api.request_device_code()
except ActronAirAuthError as err:
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/actron_air/quality_scale.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -74,5 +74,5 @@ rules:

# Platinum
async-dependency: done
inject-websession: todo
inject-websession: done
strict-typing: done
45 changes: 40 additions & 5 deletions homeassistant/components/androidtv_remote/remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import asyncio
from collections.abc import Iterable
from typing import Any, override
from typing import Any, Final, override

from homeassistant.components.remote import (
ATTR_ACTIVITY,
Expand All @@ -16,14 +16,35 @@
RemoteEntityFeature,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback

from .const import CONF_APP_NAME
from .const import CONF_APP_NAME, DOMAIN
from .entity import AndroidTVRemoteBaseEntity
from .helpers import AndroidTVRemoteConfigEntry

PARALLEL_UPDATES = 0

PREFIX_SEPARATOR: Final[str] = ":"
# Only direction prefixes are stripped; other colon conventions (e.g. text:) pass through to the library unchanged.
VALID_PREFIXES: Final[frozenset[str]] = frozenset(
{
"SHORT",
"START_LONG",
"END_LONG",
}
)


def _parse_command(single_command: str) -> tuple[str, str | None]:
"""Split an optional prefix from the key code."""
prefix, separator, rest = single_command.partition(PREFIX_SEPARATOR)
if separator:
normalized = prefix.upper()
if normalized in VALID_PREFIXES:
return rest, normalized
return single_command, None


async def async_setup_entry(
hass: HomeAssistant,
Expand Down Expand Up @@ -105,10 +126,24 @@ async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> Non

for _ in range(num_repeats):
for single_command in command:
key_code, direction = _parse_command(single_command)
if direction is not None:
if not key_code:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="empty_key_code",
translation_placeholders={"command": single_command},
)
if hold_secs:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="direction_prefix_with_hold_secs",
translation_placeholders={"command": single_command},
)
if hold_secs:
self._send_key_command(single_command, "START_LONG")
self._send_key_command(key_code, "START_LONG")
await asyncio.sleep(hold_secs)
self._send_key_command(single_command, "END_LONG")
self._send_key_command(key_code, "END_LONG")
else:
self._send_key_command(single_command, "SHORT")
self._send_key_command(key_code, direction or "SHORT")
await asyncio.sleep(delay_secs)
6 changes: 6 additions & 0 deletions homeassistant/components/androidtv_remote/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@
"connection_closed": {
"message": "Connection to the Android TV device is closed"
},
"direction_prefix_with_hold_secs": {
"message": "Command \"{command}\" combines a direction prefix with hold_secs; specify only one"
},
"empty_key_code": {
"message": "Command \"{command}\" is missing a key code after the direction prefix"
},
"invalid_channel": {
"message": "Channel must be numeric: {media_id}"
},
Expand Down
8 changes: 6 additions & 2 deletions homeassistant/components/deconz/logbook.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,9 @@ def async_describe_events(
@callback
def async_describe_deconz_alarm_event(event: Event) -> dict[str, str]:
"""Describe deCONZ logbook alarm event."""
if device := device_registry.devices.get(event.data[ATTR_DEVICE_ID]):
if device := device_registry.async_get(
event.data[ATTR_DEVICE_ID], include_child_devices=False
):
deconz_alarm_event = _get_deconz_event_from_device(hass, device)
name = deconz_alarm_event.device.name
else:
Expand All @@ -153,7 +155,9 @@ def async_describe_deconz_alarm_event(event: Event) -> dict[str, str]:
@callback
def async_describe_deconz_event(event: Event) -> dict[str, str]:
"""Describe deCONZ logbook event."""
if device := device_registry.devices.get(event.data[ATTR_DEVICE_ID]):
if device := device_registry.async_get(
event.data[ATTR_DEVICE_ID], include_child_devices=False
):
deconz_event = _get_deconz_event_from_device(hass, device)
name = deconz_event.device.name
else:
Expand Down
26 changes: 19 additions & 7 deletions homeassistant/components/hassio/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
callback,
is_callback_check_partial,
)
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers import device_registry as dr, issue_registry as ir
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect
Expand Down Expand Up @@ -399,16 +399,28 @@ def _process_issue_deltas(
current_data: SupervisorIssuesData,
) -> None:
"""Create/delete issue repairs and notify subscribers based on issue deltas."""
issue_registry = ir.async_get(self.hass)
for issue in current_data.issues.values():
previous_issue = previous_data.issues.get(issue.uuid)
if previous_issue is not None and self._issue_equal(previous_issue, issue):
continue

self._create_or_update_issue_repair(issue)
self._process_issue_change(
IssueSubscriptionEvent(event="changed", issue=issue)
changed = previous_issue is None or not self._issue_equal(
previous_issue, issue
)

# Update the repair on changes, and re-create it if the registry
# entry went missing: a finished repair flow deletes the entry
# even when applying the suggestion failed in Supervisor and the
# issue is unchanged.
if changed or (
issue.key in ISSUE_KEYS_FOR_REPAIRS
and not issue_registry.async_get_issue(DOMAIN, issue.uuid.hex)
):
self._create_or_update_issue_repair(issue)

if changed:
self._process_issue_change(
IssueSubscriptionEvent(event="changed", issue=issue)
)

for issue_uuid, issue in previous_data.issues.items():
if issue_uuid not in current_data.issues:
self._delete_issue_repair(issue)
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/hassio/repairs.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@

SUGGESTION_CONFIRMATION_REQUIRED = {
"addon_execute_remove",
"mount_move_local_data",
"system_adopt_data_disk",
"system_execute_reboot",
}
Expand Down
8 changes: 6 additions & 2 deletions homeassistant/components/hassio/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -152,11 +152,15 @@
},
"step": {
"fix_menu": {
"description": "Could not connect to `{reference}`. Check host logs for errors from the mount service for more details.\n\nUse reload to try to connect again. If you need to update `{reference}`, go to [storage]({storage_url}).",
"description": "Could not set up `{reference}`. This can happen when the storage device is unreachable, or when local data is blocking the mount location. Check host logs for errors from the mount service for more details.\n\nUse reload to try again. If you need to update `{reference}`, go to [storage]({storage_url}).",
"menu_options": {
"mount_execute_reload": "[%key:common::action::reload%]",
"mount_execute_remove": "Remove"
"mount_execute_remove": "Remove",
"mount_move_local_data": "Move blocking local data away"
}
},
"mount_move_local_data": {
"description": "Select **Submit** to move the local data blocking `{reference}` to a `{reference}_local_recovery` folder and set up the mount again.\n\nNo files are deleted. For media and share mounts the recovery folder is created next to the mount, where you can review and remove it. For backup mounts it is placed in local backup storage."
}
}
},
Expand Down
69 changes: 67 additions & 2 deletions homeassistant/components/homeassistant/triggers/event.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,29 @@
"""Offer event listening automation rules."""

from collections.abc import ItemsView, Mapping
import logging
from typing import Any

import voluptuous as vol

from homeassistant.const import CONF_EVENT_DATA, CONF_PLATFORM, EVENT_STATE_REPORTED
from homeassistant.const import (
CONF_DEVICE_ID,
CONF_EVENT_DATA,
CONF_PLATFORM,
EVENT_STATE_REPORTED,
)
from homeassistant.core import CALLBACK_TYPE, Event, HassJob, HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, template
from homeassistant.helpers import (
config_validation as cv,
device_registry as dr,
template,
)
from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
from homeassistant.helpers.typing import ConfigType
from homeassistant.util import yaml as yaml_util

_LOGGER = logging.getLogger(__name__)

CONF_EVENT_TYPE = "event_type"
CONF_EVENT_CONTEXT = "context"
Expand Down Expand Up @@ -39,6 +52,58 @@ def _validate_event_types(value: Any) -> Any:
)


async def async_validate_trigger_config(
hass: HomeAssistant, config: ConfigType
) -> ConfigType:
"""Validate trigger config.

Warn if the trigger filters event_data.device_id on a pre-migration composite device
id - a device that was split into one device per config entry.
A templated device id is a Template (not a plain string) and is left alone.
"""
validated_config: ConfigType = TRIGGER_SCHEMA(config)
if (
CONF_EVENT_DATA in validated_config
and isinstance(
device_id := validated_config[CONF_EVENT_DATA].get(CONF_DEVICE_ID), str
)
and (
split_devices := dr.async_get(
hass
).async_get_devices_for_composite_device_id(device_id)
)
):
_log_composite_device_id_warning(hass, config, device_id, split_devices)
return validated_config


@callback
def _log_composite_device_id_warning(
hass: HomeAssistant,
config: ConfigType,
device_id: str,
split_devices: list[dr.DeviceEntry],
) -> None:
"""Warn that an event trigger filters on a split (pre-migration) device id."""

device_summaries: list[str] = []
for device in split_devices:
entry = hass.config_entries.async_get_entry(device.config_entry_id)
domain = entry.domain if entry else "unknown"
name = device.name_by_user or device.name or device.id
device_summaries.append(f"{name} ({device.id}) from the {domain} integration")

_LOGGER.warning(
"Event trigger filters on device '%s', which was split into one device per "
"integration and no longer exists, so the trigger can no longer fire. Update the "
"automation, script or template entity to filter on one of these devices instead: "
"%s.\nThe affected trigger is configured as:\n%s",
device_id,
", ".join(device_summaries),
yaml_util.dump(config),
)


def _schema_value(value: Any) -> Any:
if isinstance(value, list):
return vol.In(value)
Expand Down
4 changes: 3 additions & 1 deletion homeassistant/components/homekit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1231,7 +1231,9 @@ async def _async_set_device_info_attributes(
dev_reg_ent = dev_reg.async_get(ent_reg_ent.device_id)
if isinstance(dev_reg_ent, dr.ChildDeviceEntry):
# A child device has no hardware info of its own; use the parent's
dev_reg_ent = dev_reg.devices.get(dev_reg_ent.parent_device_id)
dev_reg_ent = dev_reg.async_get(
dev_reg_ent.parent_device_id, include_child_devices=False
)
if dev_reg_ent is not None:
self._fill_config_from_device_registry_entry(dev_reg_ent, ent_cfg)
if ATTR_MANUFACTURER not in ent_cfg:
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/http/ban.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
ATTR_BANNED_AT: Final = "banned_at"

SCHEMA_IP_BAN_ENTRY: Final = vol.Schema(
{vol.Optional("banned_at"): vol.Any(None, cv.datetime)}
{vol.Optional(ATTR_BANNED_AT, default=None): vol.Any(None, cv.datetime)}
)


Expand Down
Loading
Loading