Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
53276b2
Don't offer Z-Wave JS migration to the configured adapter (#179597)
balloobbot Aug 20, 2026
f1e44ab
Add parameter include_composite_devices to DeviceRegistry.async_get (…
emontnemery Aug 20, 2026
32ef1c8
Bump music-assistant-client to 1.5.1 (#179633)
MarvinSchenkel Aug 20, 2026
d4f370e
Avoid potential exceptions in zwave_js logbook platform (#179566)
emontnemery Aug 20, 2026
363a68e
Add camera platform to Shelly integration (#179072)
bieniu Aug 20, 2026
c18462b
Add repairs for upcoming bumps of minimum supported db engine version…
emontnemery Aug 20, 2026
d7ca936
Bump actron-neo-api to 0.5.14 (#179624)
kclif9 Aug 20, 2026
85300ae
Add reconfiguration flow to Lyngdorf (#179411)
fishloa Aug 20, 2026
cb24f2f
Split user flow init and data submission in Airly tests (#179630)
bieniu Aug 20, 2026
c2ba23e
Split user flow init and data submission in AccuWeather tests (#179628)
bieniu Aug 20, 2026
761b98f
Split user flow init and data submission in BraviaTV tests (#179626)
bieniu Aug 20, 2026
3ddbc9b
Add virtual integration Ariston (#179640)
chemelli74 Aug 20, 2026
496c6d7
Support privacy switch for Shelly Camera (#179637)
bieniu Aug 20, 2026
086249e
Add now playing, position and transport control to Lyngdorf (#179471)
fishloa Aug 20, 2026
181f8f3
Bump tuya-device-sharing-sdk to 0.2.15 (#179634)
ritchie-cai Aug 20, 2026
27516c6
Improve user impersonation in Music Assistant (#178393)
arturpragacz Aug 20, 2026
1d9bb18
Update the diagnostic platform protocol for child devices (#179570)
emontnemery Aug 20, 2026
b1e8662
Bump hass-nabucasa from 2.2.0 to 2.3.0 (#179662)
ludeeus 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
26 changes: 26 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,32 @@ jobs:
run: |
uv run --no-project python -m script.gen_copilot_instructions validate

gen-recorder-db-versions:
name: Check recorder database versions
runs-on: ubuntu-24.04
permissions:
contents: read
needs:
- info
# Only run on push to the dev branch; this job reaches out to endoflife.date, and
# we do not want a new MariaDB/MySQL release to fail CI on PR runs or the rc/master
# branches.
if: github.event_name == 'push' && github.ref == 'refs/heads/dev'
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Set up Python
id: python
uses: ./.github/actions/setup-uv-python
with:
uv-version: ${{ needs.info.outputs.uv_version }}
python-version: ${{ needs.info.outputs.default_python }}
- name: Check MariaDB and MySQL versions are up to date
run: |
uv run --no-project python -m script.gen_recorder_db_versions validate

dependency-review:
name: Dependency review
runs-on: ubuntu-24.04
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/actron_air/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"quality_scale": "silver",
"requirements": ["actron-neo-api==0.5.13"]
"requirements": ["actron-neo-api==0.5.14"]
}
1 change: 1 addition & 0 deletions homeassistant/components/ariston/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Virtual integration: Ariston."""
6 changes: 6 additions & 0 deletions homeassistant/components/ariston/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"domain": "ariston",
"name": "Ariston",
"integration_type": "virtual",
"supported_by": "midea"
}
4 changes: 4 additions & 0 deletions homeassistant/components/cloud/http_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@
HTTPStatus.BAD_GATEWAY,
"Unable to reach the Home Assistant Cloud.",
),
auth.AuthTimeoutError: (
HTTPStatus.GATEWAY_TIMEOUT,
"Authentication timed out.",
),
aiohttp.ClientError: (
HTTPStatus.INTERNAL_SERVER_ERROR,
"Error making internal request",
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/cloud/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@
"integration_type": "system",
"iot_class": "cloud_push",
"loggers": ["acme", "hass_nabucasa", "snitun"],
"requirements": ["hass-nabucasa==2.2.0", "openai==2.45.0"],
"requirements": ["hass-nabucasa==2.3.0", "openai==2.45.0"],
"single_config_entry": true
}
15 changes: 11 additions & 4 deletions homeassistant/components/config/device_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,8 @@ def websocket_update_device(
msg["labels"] = set(msg["labels"])

entry: dr.AnyDeviceEntry | None
if msg["device_id"] in registry.child_devices:
device = registry.async_get(msg["device_id"], include_composite_devices=False)
if isinstance(device, dr.ChildDeviceEntry):
entry = registry.async_update_child_device(**msg)
else:
entry = registry.async_update_device(**msg)
Expand All @@ -207,10 +208,16 @@ async def _async_remove_device(
device_id = msg["device_id"]

# A composite device id has no single underlying device to remove; reject it.
if registry.async_is_composite_device_id(device_id):
if (
registry.async_get(
device_id, include_main_devices=False, include_child_devices=False
)
is not None
):
raise HomeAssistantError("Cannot remove a composite device")

if (device_entry := registry.async_get(device_id)) is None:
if (
device_entry := registry.async_get(device_id, include_composite_devices=False)
) is None:
raise HomeAssistantError("Unknown device")

if (
Expand Down
5 changes: 4 additions & 1 deletion homeassistant/components/device_automation/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,10 @@ def _resolve_device_id(hass: HomeAssistant, device_id: str, domain: str) -> str:
knows the current device id, not the removed composite id.
"""
device_registry = dr.async_get(hass)
if device_id in device_registry.devices:
if (
device_registry.async_get(device_id, include_composite_devices=False)
is not None
):
return device_id
if not (
split_devices := device_registry.async_get_devices_for_composite_device_id(
Expand Down
15 changes: 7 additions & 8 deletions homeassistant/components/diagnostics/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
integration_platform,
issue_registry as ir,
)
from homeassistant.helpers.device_registry import DeviceEntry
from homeassistant.helpers.json import (
ExtendedJSONEncoder,
find_paths_unserializable_data,
Expand Down Expand Up @@ -62,7 +61,7 @@ class DiagnosticsPlatformData:
)
device_diagnostics: (
Callable[
[HomeAssistant, ConfigEntry, DeviceEntry],
[HomeAssistant, ConfigEntry, dr.AnyDeviceEntry],
Coroutine[Any, Any, Mapping[str, Any]],
]
| None
Expand Down Expand Up @@ -100,9 +99,12 @@ async def async_get_config_entry_diagnostics(
"""Return diagnostics for a config entry."""

async def async_get_device_diagnostics(
self, hass: HomeAssistant, config_entry: ConfigEntry, device: DeviceEntry
self, hass: HomeAssistant, config_entry: ConfigEntry, device: dr.AnyDeviceEntry
) -> Mapping[str, Any]:
"""Return diagnostics for a device."""
"""Return diagnostics for a device.

Only integrations that register child devices can receive a child device.
"""


@callback
Expand Down Expand Up @@ -314,10 +316,7 @@ async def get(
if info.device_diagnostics is None:
return web.Response(status=HTTPStatus.NOT_FOUND)

# A device's diagnostics may be requested for a child device, but the
# callback is currently typed for a main device. Ignoring the mismatch until
# DiagnosticsPlatformData.device_diagnostics is widened to accept AnyDeviceEntry.
data = await info.device_diagnostics(hass, config_entry, device) # type: ignore[arg-type]
data = await info.device_diagnostics(hass, config_entry, device)
return await _async_get_json_file_response(
hass, data, data_issues, filename, config_entry.domain, d_id, sub_id
)
18 changes: 10 additions & 8 deletions homeassistant/components/homekit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1071,26 +1071,28 @@ async def _async_add_trigger_accessories(self) -> None:
dev_reg = dr.async_get(self.hass)
valid_device_ids = []
for device_id in self._devices:
if dev_reg.async_get(device_id, include_child_devices=False):
valid_device_ids.append(device_id)
elif dev_reg.async_get(device_id, include_main_devices=False):
device = dev_reg.async_get(device_id)
if device is None:
_LOGGER.warning(
(
"HomeKit %s cannot add device %s because a child device cannot"
" be a HomeKit accessory"
"HomeKit %s cannot add device %s because it is missing from the"
" device registry"
),
self._name,
device_id,
)
else:
elif isinstance(device, dr.ChildDeviceEntry):
_LOGGER.warning(
(
"HomeKit %s cannot add device %s because it is missing from the"
" device registry"
"HomeKit %s cannot add device %s because a child device cannot"
" be a HomeKit accessory"
),
self._name,
device_id,
)
else:
# A main or composite device is a valid HomeKit accessory
valid_device_ids.append(device_id)
for device_id, device_triggers in (
await device_automation.async_get_device_automations(
self.hass,
Expand Down
120 changes: 101 additions & 19 deletions homeassistant/components/lyngdorf/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from typing import Any, override
from urllib.parse import urlparse

from lyngdorf.const import LyngdorfModel
from lyngdorf.device import (
async_find_receiver_model,
async_get_device_serial,
Expand Down Expand Up @@ -54,31 +55,26 @@ async def async_step_user(

if user_input is not None:
self._host = user_input[CONF_HOST]

try:
model = await async_find_receiver_model(self._host)
except TimeoutError:
model, serial = await self._async_probe(self._host)
except TimeoutConnect:
errors["base"] = "timeout_connect"
except OSError:
except CannotConnect:
errors["base"] = "cannot_connect"
except Exception: # noqa: BLE001
errors["base"] = "unknown"

if not errors and not model:
except UnsupportedModel:
errors["base"] = "unsupported_model"

if not errors and model:
except CannotDetermineId:
errors["base"] = "cannot_determine_id"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
self._device_model = model.model_name
self._name = model.model_name

serial = await async_get_device_serial(self._host)
if not serial:
errors["base"] = "cannot_determine_id"
else:
self._device_serial_number = serial.lower()
await self.async_set_unique_id(self._device_serial_number)
self._abort_if_unique_id_configured()
return await self._create_entry()
self._device_serial_number = serial
await self.async_set_unique_id(serial)
self._abort_if_unique_id_configured()
return await self._create_entry()

return self.async_show_form(
step_id="user",
Expand All @@ -90,6 +86,76 @@ async def async_step_user(
errors=errors,
)

async def _async_probe(self, host: str) -> tuple[LyngdorfModel, str]:
"""Return the model and serial of the device at a host."""
try:
model = await async_find_receiver_model(host)
except TimeoutError as err:
raise TimeoutConnect from err
except OSError as err:
raise CannotConnect from err
if not model:
raise UnsupportedModel

try:
serial = await async_get_device_serial(host)
except TimeoutError as err:
raise TimeoutConnect from err
except OSError as err:
raise CannotConnect from err
if not serial:
raise CannotDetermineId

return model, serial.lower()

async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle reconfiguration of an existing entry.

SSDP rediscovery only recovers a changed address while the device is
still announcing somewhere Home Assistant can hear it, which a move to
a static address or another subnet can end.
"""
errors: dict[str, str] = {}
reconfigure_entry = self._get_reconfigure_entry()

if user_input is not None:
host = user_input[CONF_HOST]
try:
model, serial = await self._async_probe(host)
except TimeoutConnect:
errors["base"] = "timeout_connect"
except CannotConnect:
errors["base"] = "cannot_connect"
except UnsupportedModel:
errors["base"] = "unsupported_model"
except CannotDetermineId:
errors["base"] = "cannot_determine_id"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
await self.async_set_unique_id(serial)
self._abort_if_unique_id_mismatch()
return self.async_update_reload_and_abort(
reconfigure_entry,
data_updates={
CONF_HOST: host,
CONF_MODEL: model.model_name,
CONF_SERIAL_NUMBER: serial,
},
)

return self.async_show_form(
step_id="reconfigure",
data_schema=self.add_suggested_values_to_schema(
vol.Schema({vol.Required(CONF_HOST): cv.string}),
reconfigure_entry.data,
),
errors=errors,
)

@override
async def async_step_ssdp(
self, discovery_info: SsdpServiceInfo
Expand Down Expand Up @@ -181,3 +247,19 @@ async def _async_set_info_from_discovery(
raise AbortFlow("cannot_determine_id")
await self.async_set_unique_id(self._device_serial_number)
self._abort_if_unique_id_configured(updates={CONF_HOST: self._host})


class CannotConnect(Exception):
"""Error to indicate we cannot connect."""


class TimeoutConnect(Exception):
"""Error to indicate the device did not answer in time."""


class UnsupportedModel(Exception):
"""Error to indicate the device is not a model we support."""


class CannotDetermineId(Exception):
"""Error to indicate the device did not report a serial."""
Loading
Loading