Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
08b6a0a
Add device class filter to switcher_kis services (#153248)
emontnemery Oct 1, 2025
06d143b
Fix Bayesian ConfigFlow templates in 2025.10 (#153289)
HarvsG Oct 1, 2025
faf226f
Fix ZHA unable to select "none" flow control (#153235)
TheJulianJES Oct 1, 2025
77c8426
Use hardware bootloader reset methods for firmware config flows (#153…
puddly Oct 1, 2025
a3089b8
Replace remaining ZHA "radio" strings with "adapter" (#153234)
TheJulianJES Oct 1, 2025
790bdde
Improve ZHA multi-pan firmware repair text (#153232)
TheJulianJES Oct 1, 2025
0555b84
Add new cover fixture for Tuya (#153310)
epenet Oct 1, 2025
b411a11
Add analytics platform to esphome (#153311)
arturpragacz Oct 1, 2025
07da0cf
Stop writing to config dir log file on supervised install (#146675)
abmantis Oct 1, 2025
7c93d91
Filter out service type devices in extended analytics (#153271)
arturpragacz Oct 1, 2025
653b73c
Fix device_automation RuntimeWarning in tests (#153319)
cdce8p Oct 1, 2025
7ae3340
Add test for full device snapshot for Shelly Wall Display XL (#153305)
bieniu Oct 1, 2025
7c623a8
Use pytest.mark.usefixtures in some recorder tests (#153313)
emontnemery Oct 1, 2025
b164531
Bayesian - add config entry tests (#153316)
HarvsG Oct 1, 2025
c91ed96
Use pytest.mark.usefixtures in history tests (#153306)
emontnemery Oct 1, 2025
d76e947
Bump intents to 2025.10.1 (#153340)
synesthesiam Oct 1, 2025
fd8ccb8
Improve `mac_address_from_name()` function to avoid double discovery …
bieniu Oct 1, 2025
3c20325
Bump pyfirefly 0.1.6 (#153335)
erwindouna Oct 1, 2025
c1bf11d
Bump pyportainer 1.0.2 (#153326)
erwindouna Oct 1, 2025
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
52 changes: 31 additions & 21 deletions homeassistant/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -616,34 +616,44 @@ async def async_enable_logging(
),
)

# Log errors to a file if we have write access to file or config dir
logger = logging.getLogger()
logger.setLevel(logging.INFO if verbose else logging.WARNING)

if log_file is None:
err_log_path = hass.config.path(ERROR_LOG_FILENAME)
default_log_path = hass.config.path(ERROR_LOG_FILENAME)
if "SUPERVISOR" in os.environ:
_LOGGER.info("Running in Supervisor, not logging to file")
# Rename the default log file if it exists, since previous versions created
# it even on Supervisor
if os.path.isfile(default_log_path):
with contextlib.suppress(OSError):
os.rename(default_log_path, f"{default_log_path}.old")
err_log_path = None
else:
err_log_path = default_log_path
else:
err_log_path = os.path.abspath(log_file)

err_path_exists = os.path.isfile(err_log_path)
err_dir = os.path.dirname(err_log_path)

# Check if we can write to the error log if it exists or that
# we can create files in the containing directory if not.
if (err_path_exists and os.access(err_log_path, os.W_OK)) or (
not err_path_exists and os.access(err_dir, os.W_OK)
):
err_handler = await hass.async_add_executor_job(
_create_log_file, err_log_path, log_rotate_days
)
if err_log_path:
err_path_exists = os.path.isfile(err_log_path)
err_dir = os.path.dirname(err_log_path)

err_handler.setFormatter(logging.Formatter(fmt, datefmt=FORMAT_DATETIME))
# Check if we can write to the error log if it exists or that
# we can create files in the containing directory if not.
if (err_path_exists and os.access(err_log_path, os.W_OK)) or (
not err_path_exists and os.access(err_dir, os.W_OK)
):
err_handler = await hass.async_add_executor_job(
_create_log_file, err_log_path, log_rotate_days
)

logger = logging.getLogger()
logger.addHandler(err_handler)
logger.setLevel(logging.INFO if verbose else logging.WARNING)
err_handler.setFormatter(logging.Formatter(fmt, datefmt=FORMAT_DATETIME))
logger.addHandler(err_handler)

# Save the log file location for access by other components.
hass.data[DATA_LOGGING] = err_log_path
else:
_LOGGER.error("Unable to set up error log %s (access denied)", err_log_path)
# Save the log file location for access by other components.
hass.data[DATA_LOGGING] = err_log_path
else:
_LOGGER.error("Unable to set up error log %s (access denied)", err_log_path)

async_activate_log_queue_handler(hass)

Expand Down
35 changes: 23 additions & 12 deletions homeassistant/components/analytics/analytics.py
Original file line number Diff line number Diff line change
Expand Up @@ -505,14 +505,16 @@ def _domains_from_yaml_config(yaml_configuration: dict[str, Any]) -> set[str]:
DEFAULT_ENTITY_ANALYTICS_CONFIG = EntityAnalyticsModifications()


async def async_devices_payload(hass: HomeAssistant) -> dict:
async def async_devices_payload(hass: HomeAssistant) -> dict: # noqa: C901
"""Return detailed information about entities and devices."""
dev_reg = dr.async_get(hass)
ent_reg = er.async_get(hass)

integration_inputs: dict[str, tuple[list[str], list[str]]] = {}
integration_configs: dict[str, AnalyticsModifications] = {}

removed_devices: set[str] = set()

# Get device list
for device_entry in dev_reg.devices.values():
if not device_entry.primary_config_entry:
Expand All @@ -525,6 +527,10 @@ async def async_devices_payload(hass: HomeAssistant) -> dict:
if config_entry is None:
continue

if device_entry.entry_type is dr.DeviceEntryType.SERVICE:
removed_devices.add(device_entry.id)
continue

integration_domain = config_entry.domain

integration_input = integration_inputs.setdefault(integration_domain, ([], []))
Expand Down Expand Up @@ -614,11 +620,12 @@ async def async_devices_payload(hass: HomeAssistant) -> dict:
device_config = integration_config.devices.get(device_id, device_config)

if device_config.remove:
removed_devices.add(device_id)
continue

device_entry = dev_reg.devices[device_id]

device_id_mapping[device_entry.id] = (integration_domain, len(devices_info))
device_id_mapping[device_id] = (integration_domain, len(devices_info))

devices_info.append(
{
Expand Down Expand Up @@ -669,7 +676,7 @@ async def async_devices_payload(hass: HomeAssistant) -> dict:

entity_entry = ent_reg.entities[entity_id]

entity_state = hass.states.get(entity_entry.entity_id)
entity_state = hass.states.get(entity_id)

entity_info = {
# LIMITATION: `assumed_state` can be overridden by users;
Expand All @@ -690,15 +697,19 @@ async def async_devices_payload(hass: HomeAssistant) -> dict:
"unit_of_measurement": entity_entry.unit_of_measurement,
}

if (
((device_id_ := entity_entry.device_id) is not None)
and ((new_device_id := device_id_mapping.get(device_id_)) is not None)
and (new_device_id[0] == integration_domain)
):
device_info = devices_info[new_device_id[1]]
device_info["entities"].append(entity_info)
else:
entities_info.append(entity_info)
if (device_id_ := entity_entry.device_id) is not None:
if device_id_ in removed_devices:
# The device was removed, so we remove the entity too
continue

if (
new_device_id := device_id_mapping.get(device_id_)
) is not None and (new_device_id[0] == integration_domain):
device_info = devices_info[new_device_id[1]]
device_info["entities"].append(entity_info)
continue

entities_info.append(entity_info)

return {
"version": "home-assistant:1",
Expand Down
7 changes: 7 additions & 0 deletions homeassistant/components/bayesian/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,13 @@ async def async_setup_entry(
observations: list[ConfigType] = [
dict(subentry.data) for subentry in config_entry.subentries.values()
]

for observation in observations:
if observation[CONF_PLATFORM] == CONF_TEMPLATE:
observation[CONF_VALUE_TEMPLATE] = Template(
observation[CONF_VALUE_TEMPLATE], hass
)

prior: float = config[CONF_PRIOR]
probability_threshold: float = config[CONF_PROBABILITY_THRESHOLD]
device_class: BinarySensorDeviceClass | None = config.get(CONF_DEVICE_CLASS)
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/conversation/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/conversation",
"integration_type": "entity",
"quality_scale": "internal",
"requirements": ["hassil==3.2.0", "home-assistant-intents==2025.9.24"]
"requirements": ["hassil==3.2.0", "home-assistant-intents==2025.10.1"]
}
11 changes: 11 additions & 0 deletions homeassistant/components/esphome/analytics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""Analytics platform."""

from homeassistant.components.analytics import AnalyticsInput, AnalyticsModifications
from homeassistant.core import HomeAssistant


async def async_modify_analytics(
hass: HomeAssistant, analytics_input: AnalyticsInput
) -> AnalyticsModifications:
"""Modify the analytics."""
return AnalyticsModifications(remove=True)
2 changes: 1 addition & 1 deletion homeassistant/components/firefly_iii/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/firefly_iii",
"iot_class": "local_polling",
"quality_scale": "bronze",
"requirements": ["pyfirefly==0.1.5"]
"requirements": ["pyfirefly==0.1.6"]
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from homeassistant.components.homeassistant_hardware.util import (
ApplicationType,
FirmwareInfo,
ResetTarget,
)
from homeassistant.config_entries import (
ConfigEntry,
Expand Down Expand Up @@ -67,6 +68,11 @@ class ZBT2FirmwareMixin(ConfigEntryBaseFlow, FirmwareInstallFlowProtocol):

context: ConfigFlowContext

# `rts_dtr` targets older adapters, `baudrate` works for newer ones. The reason we
# try them in this order is that on older adapters `baudrate` entered the ESP32-S3
# bootloader instead of the MG24 bootloader.
BOOTLOADER_RESET_METHODS = [ResetTarget.RTS_DTR, ResetTarget.BAUDRATE]

async def async_step_install_zigbee_firmware(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from homeassistant.components.homeassistant_hardware.util import (
ApplicationType,
FirmwareInfo,
ResetTarget,
)
from homeassistant.components.update import UpdateDeviceClass
from homeassistant.config_entries import ConfigEntry
Expand Down Expand Up @@ -156,7 +157,7 @@ async def async_setup_entry(
class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
"""Connect ZBT-2 firmware update entity."""

bootloader_reset_type = None
bootloader_reset_methods = [ResetTarget.RTS_DTR, ResetTarget.BAUDRATE]

def __init__(
self,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
FirmwareInfo,
OwningAddon,
OwningIntegration,
ResetTarget,
async_flash_silabs_firmware,
get_otbr_addon_manager,
guess_firmware_info,
Expand Down Expand Up @@ -79,6 +80,8 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
"""Base flow to install firmware."""

ZIGBEE_BAUDRATE = 115200 # Default, subclasses may override
BOOTLOADER_RESET_METHODS: list[ResetTarget] = [] # Default, subclasses may override

_picked_firmware_type: PickedFirmwareType
_zigbee_flow_strategy: ZigbeeFlowStrategy = ZigbeeFlowStrategy.RECOMMENDED

Expand Down Expand Up @@ -274,7 +277,7 @@ async def _install_firmware(
device=self._device,
fw_data=fw_data,
expected_installed_firmware_type=expected_installed_firmware_type,
bootloader_reset_type=None,
bootloader_reset_methods=self.BOOTLOADER_RESET_METHODS,
progress_callback=lambda offset, total: self.async_update_progress(
offset / total
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"documentation": "https://www.home-assistant.io/integrations/homeassistant_hardware",
"integration_type": "system",
"requirements": [
"universal-silabs-flasher==0.0.32",
"universal-silabs-flasher==0.0.34",
"ha-silabs-firmware-client==0.2.0"
]
}
11 changes: 8 additions & 3 deletions homeassistant/components/homeassistant_hardware/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@

from .coordinator import FirmwareUpdateCoordinator
from .helpers import async_register_firmware_info_callback
from .util import ApplicationType, FirmwareInfo, async_flash_silabs_firmware
from .util import (
ApplicationType,
FirmwareInfo,
ResetTarget,
async_flash_silabs_firmware,
)

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -81,7 +86,7 @@ class BaseFirmwareUpdateEntity(

# Subclasses provide the mapping between firmware types and entity descriptions
entity_description: FirmwareUpdateEntityDescription
bootloader_reset_type: str | None = None
bootloader_reset_methods: list[ResetTarget] = []

_attr_supported_features = (
UpdateEntityFeature.INSTALL | UpdateEntityFeature.PROGRESS
Expand Down Expand Up @@ -268,7 +273,7 @@ async def async_install(
device=self._current_device,
fw_data=fw_data,
expected_installed_firmware_type=self.entity_description.expected_firmware_type,
bootloader_reset_type=self.bootloader_reset_type,
bootloader_reset_methods=self.bootloader_reset_methods,
progress_callback=self._update_progress,
)
finally:
Expand Down
25 changes: 21 additions & 4 deletions homeassistant/components/homeassistant_hardware/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@

import asyncio
from collections import defaultdict
from collections.abc import AsyncIterator, Callable, Iterable
from collections.abc import AsyncIterator, Callable, Iterable, Sequence
from contextlib import AsyncExitStack, asynccontextmanager
from dataclasses import dataclass
from enum import StrEnum
import logging

from universal_silabs_flasher.const import ApplicationType as FlasherApplicationType
from universal_silabs_flasher.const import (
ApplicationType as FlasherApplicationType,
ResetTarget as FlasherResetTarget,
)
from universal_silabs_flasher.firmware import parse_firmware_image
from universal_silabs_flasher.flasher import Flasher

Expand Down Expand Up @@ -59,6 +62,18 @@ def as_flasher_application_type(self) -> FlasherApplicationType:
return FlasherApplicationType(self.value)


class ResetTarget(StrEnum):
"""Methods to reset a device into bootloader mode."""

RTS_DTR = "rts_dtr"
BAUDRATE = "baudrate"
YELLOW = "yellow"

def as_flasher_reset_target(self) -> FlasherResetTarget:
"""Convert the reset target enum into one compatible with USF."""
return FlasherResetTarget(self.value)


@singleton(OTBR_ADDON_MANAGER_DATA)
@callback
def get_otbr_addon_manager(hass: HomeAssistant) -> WaitingAddonManager:
Expand Down Expand Up @@ -342,7 +357,7 @@ async def async_flash_silabs_firmware(
device: str,
fw_data: bytes,
expected_installed_firmware_type: ApplicationType,
bootloader_reset_type: str | None = None,
bootloader_reset_methods: Sequence[ResetTarget] = (),
progress_callback: Callable[[int, int], None] | None = None,
) -> FirmwareInfo:
"""Flash firmware to the SiLabs device."""
Expand All @@ -359,7 +374,9 @@ async def async_flash_silabs_firmware(
ApplicationType.SPINEL.as_flasher_application_type(),
ApplicationType.CPC.as_flasher_application_type(),
),
bootloader_reset=bootloader_reset_type,
bootloader_reset=tuple(
m.as_flasher_reset_target() for m in bootloader_reset_methods
),
)

async with AsyncExitStack() as stack:
Expand Down
3 changes: 2 additions & 1 deletion homeassistant/components/homeassistant_sky_connect/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,8 @@ async def async_setup_entry(
class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
"""SkyConnect firmware update entity."""

bootloader_reset_type = None
# The ZBT-1 does not have a hardware bootloader trigger
bootloader_reset_methods = []

def __init__(
self,
Expand Down
3 changes: 3 additions & 0 deletions homeassistant/components/homeassistant_yellow/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from homeassistant.components.homeassistant_hardware.util import (
ApplicationType,
FirmwareInfo,
ResetTarget,
probe_silabs_firmware_info,
)
from homeassistant.config_entries import (
Expand Down Expand Up @@ -83,6 +84,8 @@ async def _install_firmware_step(
class YellowFirmwareMixin(ConfigEntryBaseFlow, FirmwareInstallFlowProtocol):
"""Mixin for Home Assistant Yellow firmware methods."""

BOOTLOADER_RESET_METHODS = [ResetTarget.YELLOW]

async def async_step_install_zigbee_firmware(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
Expand Down
3 changes: 2 additions & 1 deletion homeassistant/components/homeassistant_yellow/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from homeassistant.components.homeassistant_hardware.util import (
ApplicationType,
FirmwareInfo,
ResetTarget,
)
from homeassistant.components.update import UpdateDeviceClass
from homeassistant.config_entries import ConfigEntry
Expand Down Expand Up @@ -173,7 +174,7 @@ async def async_setup_entry(
class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
"""Yellow firmware update entity."""

bootloader_reset_type = "yellow" # Triggers a GPIO reset
bootloader_reset_methods = [ResetTarget.YELLOW] # Triggers a GPIO reset

def __init__(
self,
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/portainer/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/portainer",
"iot_class": "local_polling",
"quality_scale": "bronze",
"requirements": ["pyportainer==0.1.7"]
"requirements": ["pyportainer==1.0.2"]
}
Loading
Loading