Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
458b11e
Raise error on deprecated Libre Hardware Monitor version (#181928)
Sab44 Sep 11, 2026
7c9fd38
Adjust search to not access DeviceEntry.config_entries (#181888)
emontnemery Sep 11, 2026
f3442b2
Fix stop service in modbus (#181915)
epenet Sep 11, 2026
5f3caf3
Bump zhong-hong-hvac to 1.0.21 (#181896)
crhan Sep 11, 2026
4381f31
Refactor reolink util get_device_uid_and_ch (#181922)
emontnemery Sep 11, 2026
0092419
Allow Nest fan mode when HVAC mode is off (#181911)
allenporter Sep 11, 2026
86584ea
Give ZhongHong a coordinator for polling and availability (#181897)
crhan Sep 11, 2026
f7a35f9
Add idle state and stop action to lawn mower entity (#181188)
piitaya Sep 11, 2026
57a74e1
Adjust logbook to not access DeviceEntry.config_entries (#181891)
emontnemery Sep 11, 2026
bf86709
Add real-time updates for Roborock wet/dry vacuums (#180493)
piitaya Sep 11, 2026
f07aac3
Adjust lg_netcast to not access DeviceEntry.config_entries (#181726)
emontnemery Sep 11, 2026
bcb9bc5
Adjust unifiprotect to not access DeviceEntry.config_entries (#181766)
emontnemery Sep 11, 2026
c3483bf
Adjust service helper to not access DeviceEntry.config_entries (#181887)
emontnemery Sep 11, 2026
37675b3
Add Flexit sensor entities (#181882)
troelde Sep 11, 2026
b385fc9
Teach helpers returning device and config entry for domain about chil…
emontnemery Sep 11, 2026
e2418c5
Adjust hue to not access DeviceEntry.config_entries (#181932)
emontnemery Sep 11, 2026
3151ca8
Fix shared wakelock across Tesla Fleet vehicles (#181658)
Bre77 Sep 11, 2026
d907b6a
Expose full price validity range in EnergyZero service response (#181…
klaasnicolaas Sep 11, 2026
9e443bc
Add stop action to MQTT lawn mower (#181192)
piitaya Sep 11, 2026
fc1e755
Make file_upload handle cancellations better (#181711)
emontnemery Sep 11, 2026
241b64e
bump pyenphase to 4.0.3 (#181940)
catsmanac Sep 11, 2026
8c64cf2
Fix HomeKit cover direction when moved from outside HomeKit (#180889)
kmifka Sep 11, 2026
8f40062
Add Bluetooth control for Teslemetry vehicles (#176296)
Bre77 Sep 11, 2026
3909d47
Add endpoint device tree benchmark snapshots for multi-endpoint Z-Wav…
MartinHjelmare Sep 11, 2026
78ebfb3
Normalize Hive auth username before login (#168827)
Oluwatobi-Mustapha Sep 11, 2026
41448c3
Rejseplan api 2 0 (#152200)
Jawar19 Sep 11, 2026
96c22f9
Handle transient transport errors in tesla_fleet setup (#180218)
asjoyner Sep 11, 2026
20485cb
Add Smartthings soundmode using networkaudio (#168698)
barryvdh Sep 11, 2026
caa25bf
Ignore core authorization errors in translation checks (#180817)
JakeMartin-ICL Sep 11, 2026
02a802a
Update frontend to 20260826.7 (#181950)
bramkragten Sep 11, 2026
646bb57
Preserve form values on error in Remote Calendar config flow (#181908)
allenporter Sep 11, 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
2 changes: 2 additions & 0 deletions CODEOWNERS

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

3 changes: 2 additions & 1 deletion homeassistant/components/alexa_devices/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,9 @@ def async_get_entry_id_for_service_call(
) -> tuple[dr.DeviceEntry, AmazonConfigEntry]:
"""Get the entry ID related to a service call (by device ID)."""
config_entry: AmazonConfigEntry
# Callers read the device's serial number, which only a main device has
device, config_entry = service.async_get_device_and_config_entry(
call.hass, DOMAIN, call.data[ATTR_DEVICE_ID]
call.hass, DOMAIN, call.data[ATTR_DEVICE_ID], include_child_devices=False
)
return (device, config_entry)

Expand Down
6 changes: 3 additions & 3 deletions homeassistant/components/bsblan/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ def _build_weekly_schedule_days(

def _resolve_config_entry(
service_call: ServiceCall,
) -> tuple[BSBLanConfigEntry, dr.DeviceEntry]:
) -> tuple[BSBLanConfigEntry, dr.AnyDeviceEntry]:
"""Resolve device_id from a service call into a loaded BSBLAN config entry."""
config_entry: BSBLanConfigEntry
device, config_entry = service.async_get_device_and_config_entry(
Expand All @@ -136,12 +136,12 @@ def _resolve_config_entry(
return config_entry, device


def _device_name(device_entry: dr.DeviceEntry) -> str:
def _device_name(device_entry: dr.AnyDeviceEntry) -> str:
"""Return the best available display name for a device."""
return device_entry.name_by_user or device_entry.name or device_entry.id


def _ensure_water_heater_device(device_entry: dr.DeviceEntry) -> None:
def _ensure_water_heater_device(device_entry: dr.AnyDeviceEntry) -> None:
"""Validate the service targets the water heater sub-device."""
for domain, identifier in device_entry.identifiers:
if domain == DOMAIN and identifier.endswith("-water-heater"):
Expand Down
17 changes: 5 additions & 12 deletions homeassistant/components/device_automation/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,24 +130,17 @@ async def async_validate_device_automation_config(
)

# Find a config entry with the same domain as the device automation
device_config_entry = None
for entry_id in device.config_entries:
if (
not (entry := hass.config_entries.async_get_entry(entry_id))
or entry.domain != validated_config[CONF_DOMAIN]
):
continue
device_config_entry = entry
break

if not device_config_entry:
_, config_entry = dr.async_get_device_and_config_entry_for_domain(
hass, device.id, domain=validated_config[CONF_DOMAIN]
)
if not config_entry:
# There's no config entry with the same domain as the device automation
raise InvalidDeviceAutomationConfig(
f"Device '{validated_config[CONF_DEVICE_ID]}' has no config entry from "
f"domain '{validated_config[CONF_DOMAIN]}'"
)

if not await hass.config_entries.async_wait_component(device_config_entry):
if not await hass.config_entries.async_wait_component(config_entry):
# The component could not be loaded, skip the dynamic validation
return validated_config

Expand Down
16 changes: 13 additions & 3 deletions homeassistant/components/energyzero/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ def __serialize_prices(
{
"price": price,
"timestamp": str(time_range.start_including),
"start": str(time_range.start_including),
"end": str(time_range.end_excluding),
}
for price_data in prices
for time_range, price in price_data.prices.items()
Expand All @@ -111,12 +113,20 @@ async def __get_prices(
local_tz = ZoneInfo(call.hass.config.time_zone)
start_date, start_datetime = __get_date(call.data.get(ATTR_START), local_tz)
end_date, end_datetime = __get_date(call.data.get(ATTR_END), local_tz)

filter_start = start_datetime or dt_util.as_utc(
dt_util.start_of_local_day(start_date)
)
filter_end = end_datetime or dt_util.as_utc(
dt_util.start_of_local_day(end_date + timedelta(days=1))
)
end_start = end_datetime or dt_util.as_utc(dt_util.start_of_local_day(end_date))

if filter_start == end_start:
day = filter_start.astimezone(local_tz).date()
filter_start = dt_util.as_utc(dt_util.start_of_local_day(day))
filter_end = dt_util.as_utc(dt_util.start_of_local_day(day + timedelta(days=1)))
else:
filter_end = end_datetime or dt_util.as_utc(
dt_util.start_of_local_day(end_date + timedelta(days=1))
)

if filter_end <= filter_start:
raise ServiceValidationError(
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/enphase_envoy/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"iot_class": "local_polling",
"loggers": ["pyenphase"],
"quality_scale": "platinum",
"requirements": ["pyenphase==4.0.1"],
"requirements": ["pyenphase==4.0.3"],
"zeroconf": [
{
"type": "_enphase-envoy._tcp.local."
Expand Down
30 changes: 20 additions & 10 deletions homeassistant/components/file_upload/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ async def _receive_file_field(
queue: SimpleQueue[tuple[bytes, asyncio.Future[None] | None] | None] = SimpleQueue()

def _sync_queue_consumer() -> None:
file_path.parent.mkdir()
with file_path.open("wb") as file_handle:
while True:
if (_chunk_future := queue.get()) is None:
Expand All @@ -128,6 +129,7 @@ def _sync_queue_consumer() -> None:
file_handle.write(_chunk)

fut: asyncio.Future[None] | None = None
cancelled: asyncio.CancelledError | None = None
try:
fut = hass.async_add_executor_job(_sync_queue_consumer)
chunks_sent = 0
Expand All @@ -143,23 +145,31 @@ def _sync_queue_consumer() -> None:
if fut.done():
# The executor job failed
break
except asyncio.CancelledError as err:
# Remember a cancellation from the streaming loop so the join below re-raises
# it instead of a later writer error.
cancelled = err
raise
finally:
# Always terminate the queue consumer, also if the stream raised or the task
# was cancelled, otherwise awaiting the consumer future deadlocks.
queue.put_nowait(None)
if fut is not None:
# The executor thread can't be cancelled and is guaranteed to finish once
# it reads the sentinel queued above. Await it even if this task is
# cancelled: awaiting only through a shield keeps the executor future
# itself uncancelled, so the thread is fully done (file written and closed)
# before the caller cleans up. Re-raise any cancellation after.
cancelled: asyncio.CancelledError | None = None
# it reads the sentinel queued above. Wait for it even if this task is
# cancelled: asyncio.wait neither cancels the future nor raises its
# exception, so the thread is fully done (file written and closed) before
# the caller cleans up. The loop re-waits through repeated cancellations.
while not fut.done():
try:
await asyncio.shield(fut)
await asyncio.wait({fut})
except asyncio.CancelledError as err:
cancelled = err
if cancelled is not None:
# A cancellation takes precedence over a writer error; retrieve the
# writer result so its exception isn't flagged as never-retrieved.
if not fut.cancelled():
fut.exception()
raise cancelled
fut.result()

Expand Down Expand Up @@ -216,12 +226,12 @@ async def _upload_file(self, request: web.Request) -> web.Response:
file_dir = file_upload_data.file_dir(file_id)

try:
await hass.async_add_executor_job(file_dir.mkdir)
await _receive_file_field(hass, file_field_reader, file_dir / filename)
except Exception, asyncio.CancelledError:
# Upload failed: _receive_file_field has joined the writer and closed the
# file, so removing the directory now cannot race the writer. ignore_errors
# covers a failure that happened before the directory was created.
# Upload failed: _receive_file_field has joined the writer, which created
# the directory and closed the file, so removing the directory now cannot
# race the writer. ignore_errors covers a failure that happened before the
# directory was created.
await hass.async_add_executor_job(
lambda: shutil.rmtree(file_dir, ignore_errors=True)
)
Expand Down
6 changes: 5 additions & 1 deletion homeassistant/components/flexit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@
from .const import CONF_BAUDRATE, CONF_UNIT, DEFAULT_PORT, TYPE_SERIAL
from .coordinator import FlexitConfigEntry, FlexitDataCoordinator

_PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.CLIMATE]
_PLATFORMS: list[Platform] = [
Platform.BINARY_SENSOR,
Platform.CLIMATE,
Platform.SENSOR,
]


def create_modbus_params(
Expand Down
117 changes: 117 additions & 0 deletions homeassistant/components/flexit/sensor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
"""Sensor platform for the Flexit integration."""

from collections.abc import Callable
from dataclasses import dataclass
from typing import override

from flexit_modbus import Measurements

from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import (
PERCENTAGE,
EntityCategory,
UnitOfTemperature,
UnitOfTime,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType

from .coordinator import FlexitConfigEntry, FlexitDataCoordinator
from .entity import FlexitEntity


@dataclass(kw_only=True, frozen=True)
class FlexitSensorEntityDescription(SensorEntityDescription):
"""Describe a Flexit sensor entity."""

value_fn: Callable[[Measurements], StateType]


SENSORS: tuple[FlexitSensorEntityDescription, ...] = (
FlexitSensorEntityDescription(
key="air_filter_operating_time",
translation_key="air_filter_operating_time",
device_class=SensorDeviceClass.DURATION,
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=UnitOfTime.HOURS,
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=0,
value_fn=lambda measurements: measurements.filter_running_hours,
),
FlexitSensorEntityDescription(
key="heat_exchanger_regulation",
translation_key="heat_exchanger_regulation",
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda measurements: measurements.heat_exchanger_regulation,
),
FlexitSensorEntityDescription(
key="electric_heater_regulation",
translation_key="electric_heater_regulation",
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda measurements: measurements.electric_heater_regulation,
),
FlexitSensorEntityDescription(
key="cooling_regulation",
translation_key="cooling_regulation",
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda measurements: measurements.cooling_regulation,
),
FlexitSensorEntityDescription(
key="outdoor_air_temperature",
translation_key="outdoor_air_temperature",
device_class=SensorDeviceClass.TEMPERATURE,
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda measurements: measurements.outdoor_air_temperature,
),
)


async def async_setup_entry(
hass: HomeAssistant,
entry: FlexitConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Flexit sensor entities."""
coordinator = entry.runtime_data
async_add_entities(
FlexitSensor(coordinator, description) for description in SENSORS
)


class FlexitSensor(FlexitEntity, SensorEntity):
"""Representation of a Flexit sensor."""

entity_description: FlexitSensorEntityDescription

def __init__(
self,
coordinator: FlexitDataCoordinator,
entity_description: FlexitSensorEntityDescription,
) -> None:
"""Initialize the sensor."""
assert coordinator.config_entry is not None
super().__init__(coordinator)
self.entity_description = entity_description
self._attr_unique_id = (
f"{coordinator.config_entry.entry_id}-{entity_description.key}"
)

@property
@override
def native_value(self) -> StateType:
"""Return the sensor value."""
return self.entity_description.value_fn(self.coordinator.device.measurements)
17 changes: 17 additions & 0 deletions homeassistant/components/flexit/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,23 @@
"filter_alarm": {
"name": "Filter alarm"
}
},
"sensor": {
"air_filter_operating_time": {
"name": "Air filter operating time"
},
"cooling_regulation": {
"name": "Cooling regulation"
},
"electric_heater_regulation": {
"name": "Electric heater regulation"
},
"heat_exchanger_regulation": {
"name": "Heat exchanger regulation"
},
"outdoor_air_temperature": {
"name": "Outdoor air temperature"
}
}
},
"issues": {
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/frontend/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,5 @@
"integration_type": "system",
"preview_features": { "winter_mode": {} },
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20260826.6"]
"requirements": ["home-assistant-frontend==20260826.7"]
}
3 changes: 2 additions & 1 deletion homeassistant/components/hassio/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -452,8 +452,9 @@ def async_register_network_storage_services(

async def async_mount_reload(service: ServiceCall) -> None:
"""Handle service calls for Hass.io."""
# A mount is always a main device, and the check below reads its model
device, _ = async_get_device_and_config_entry(
hass, DOMAIN, service.data[ATTR_DEVICE_ID]
hass, DOMAIN, service.data[ATTR_DEVICE_ID], include_child_devices=False
)

if device.name is None or device.model != SupervisorEntityModel.MOUNT:
Expand Down
6 changes: 4 additions & 2 deletions homeassistant/components/hive/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,14 @@ async def async_step_user(
# Login to Hive with user data.
if user_input is not None:
self.data.update(user_input)
username = self.data[CONF_USERNAME].lower()
self.hive_auth = Auth(
username=self.data[CONF_USERNAME], password=self.data[CONF_PASSWORD]
username=username,
password=self.data[CONF_PASSWORD],
)

# Get user from existing entry and abort if already setup
await self.async_set_unique_id(self.data[CONF_USERNAME])
await self.async_set_unique_id(username)
if self.context["source"] != SOURCE_REAUTH:
self._abort_if_unique_id_configured()

Expand Down
Loading
Loading