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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ env:
CACHE_VERSION: 3
UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 1
HA_SHORT_VERSION: "2025.7"
HA_SHORT_VERSION: "2025.8"
DEFAULT_PYTHON: "3.13"
ALL_PYTHON_VERSIONS: "['3.13']"
# 10.3 is the oldest supported version
Expand Down
4 changes: 2 additions & 2 deletions CODEOWNERS

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

2 changes: 2 additions & 0 deletions homeassistant/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
restore_state,
template,
translation,
trigger,
)
from .helpers.dispatcher import async_dispatcher_send_internal
from .helpers.storage import get_internal_store_manager
Expand Down Expand Up @@ -452,6 +453,7 @@ async def async_load_base_functionality(hass: core.HomeAssistant) -> None:
create_eager_task(restore_state.async_load(hass)),
create_eager_task(hass.config_entries.async_initialize()),
create_eager_task(async_get_system_info(hass)),
create_eager_task(trigger.async_setup(hass)),
)


Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/alexa_devices/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["aioamazondevices"],
"quality_scale": "bronze",
"requirements": ["aioamazondevices==3.1.14"]
"requirements": ["aioamazondevices==3.1.19"]
}
49 changes: 34 additions & 15 deletions homeassistant/components/device_automation/condition.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.condition import (
Condition,
ConditionCheckerType,
trace_condition_function,
)
Expand Down Expand Up @@ -51,20 +52,38 @@ async def async_get_conditions(
"""List conditions."""


async def async_validate_condition_config(
hass: HomeAssistant, config: ConfigType
) -> ConfigType:
"""Validate device condition config."""
return await async_validate_device_automation_config(
hass, config, cv.DEVICE_CONDITION_SCHEMA, DeviceAutomationType.CONDITION
)
class DeviceCondition(Condition):
"""Device condition."""

def __init__(self, hass: HomeAssistant, config: ConfigType) -> None:
"""Initialize condition."""
self._config = config
self._hass = hass

async def async_condition_from_config(
hass: HomeAssistant, config: ConfigType
) -> condition.ConditionCheckerType:
"""Test a device condition."""
platform = await async_get_device_automation_platform(
hass, config[CONF_DOMAIN], DeviceAutomationType.CONDITION
)
return trace_condition_function(platform.async_condition_from_config(hass, config))
@classmethod
async def async_validate_condition_config(
cls, hass: HomeAssistant, config: ConfigType
) -> ConfigType:
"""Validate device condition config."""
return await async_validate_device_automation_config(
hass, config, cv.DEVICE_CONDITION_SCHEMA, DeviceAutomationType.CONDITION
)

async def async_condition_from_config(self) -> condition.ConditionCheckerType:
"""Test a device condition."""
platform = await async_get_device_automation_platform(
self._hass, self._config[CONF_DOMAIN], DeviceAutomationType.CONDITION
)
return trace_condition_function(
platform.async_condition_from_config(self._hass, self._config)
)


CONDITIONS: dict[str, type[Condition]] = {
"device": DeviceCondition,
}


async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]:
"""Return the sun conditions."""
return CONDITIONS
2 changes: 1 addition & 1 deletion homeassistant/components/frontend/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20250531.4"]
"requirements": ["home-assistant-frontend==20250625.0"]
}
5 changes: 5 additions & 0 deletions homeassistant/components/mqtt/icons.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,10 @@
"reload": {
"service": "mdi:reload"
}
},
"triggers": {
"mqtt": {
"trigger": "mdi:swap-horizontal"
}
}
}
21 changes: 19 additions & 2 deletions homeassistant/components/mqtt/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -222,8 +222,8 @@
"unit_of_measurement": "Unit of measurement"
},
"data_description": {
"device_class": "The Device class of the {platform} entity. [Learn more.]({url}#device_class)",
"entity_category": "Allow marking an entity as device configuration or diagnostics. An entity with a category will not be exposed to cloud, Alexa, or Google Assistant components, nor included in indirect action calls to devices or areas. Sensor entities cannot be assigned a device configiuration class. [Learn more.](https://developers.home-assistant.io/docs/core/entity/#registry-properties)",
"device_class": "The device class of the {platform} entity. [Learn more.]({url}#device_class)",
"entity_category": "Allows marking an entity as device configuration or diagnostics. An entity with a category will not be exposed to cloud, Alexa, or Google Assistant components, nor included in indirect action calls to devices or areas. Sensor entities cannot be assigned a device configuration class. [Learn more.](https://developers.home-assistant.io/docs/core/entity/#registry-properties)",
"fan_feature_speed": "The fan supports multiple speeds.",
"fan_feature_preset_modes": "The fan supports preset modes.",
"fan_feature_oscillation": "The fan supports oscillation.",
Expand Down Expand Up @@ -992,6 +992,23 @@
"description": "Reloads MQTT entities from the YAML-configuration."
}
},
"triggers": {
"mqtt": {
"name": "MQTT",
"description": "When a specific message is received on a given MQTT topic.",
"description_configured": "When an MQTT message has been received",
"fields": {
"payload": {
"name": "Payload",
"description": "The payload to trigger on."
},
"topic": {
"name": "Topic",
"description": "MQTT topic to listen to."
}
}
}
},
"exceptions": {
"addon_start_failed": {
"message": "Failed to correctly start {addon} add-on."
Expand Down
14 changes: 14 additions & 0 deletions homeassistant/components/mqtt/triggers.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Describes the format for MQTT triggers

mqtt:
fields:
payload:
example: "on"
required: false
selector:
text:
topic:
example: "living_room/switch/ac"
required: true
selector:
text:
2 changes: 1 addition & 1 deletion homeassistant/components/playstation_network/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"domain": "playstation_network",
"name": "PlayStation Network",
"codeowners": ["@jackjpowell"],
"codeowners": ["@jackjpowell", "@tr4nt0r"],
"config_flow": true,
"dhcp": [
{
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/plugwise/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@
"iot_class": "local_polling",
"loggers": ["plugwise"],
"quality_scale": "platinum",
"requirements": ["plugwise==1.7.4"],
"requirements": ["plugwise==1.7.6"],
"zeroconf": ["_plugwise._tcp.local."]
}
19 changes: 16 additions & 3 deletions homeassistant/components/sonos/diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from typing import Any

from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.device_registry import DeviceEntry

from .const import DOMAIN
Expand Down Expand Up @@ -132,11 +133,23 @@ def get_contents(
value = getattr(speaker, attrib)
payload[attrib] = get_contents(value)

entity_registry = er.async_get(hass)
payload["enabled_entities"] = sorted(
entity_id
for entity_id, s in config_entry.runtime_data.entity_id_mappings.items()
if s is speaker
registry_entry.entity_id
for registry_entry in entity_registry.entities.get_entries_for_config_entry_id(
config_entry.entry_id
)
if (
(
entity_speaker
:= config_entry.runtime_data.unique_id_speaker_mappings.get(
registry_entry.unique_id
)
)
and speaker.uid == entity_speaker.uid
)
)

payload["media"] = await async_generate_media_info(hass, speaker)
payload["activity_stats"] = speaker.activity_stats.report()
payload["event_stats"] = speaker.event_stats.report()
Expand Down
8 changes: 6 additions & 2 deletions homeassistant/components/sonos/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ def __init__(self, speaker: SonosSpeaker, config_entry: SonosConfigEntry) -> Non

async def async_added_to_hass(self) -> None:
"""Handle common setup when added to hass."""
self.config_entry.runtime_data.entity_id_mappings[self.entity_id] = self.speaker
assert self.unique_id
self.config_entry.runtime_data.unique_id_speaker_mappings[self.unique_id] = (
self.speaker
)
self.async_on_remove(
async_dispatcher_connect(
self.hass,
Expand All @@ -52,7 +55,8 @@ async def async_added_to_hass(self) -> None:

async def async_will_remove_from_hass(self) -> None:
"""Clean up when entity is removed."""
del self.config_entry.runtime_data.entity_id_mappings[self.entity_id]
assert self.unique_id
del self.config_entry.runtime_data.unique_id_speaker_mappings[self.unique_id]

async def async_fallback_poll(self, now: datetime.datetime) -> None:
"""Poll the entity if subscriptions fail."""
Expand Down
3 changes: 2 additions & 1 deletion homeassistant/components/sonos/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,8 @@ class SonosData:
discovery_known: set[str] = field(default_factory=set)
boot_counts: dict[str, int] = field(default_factory=dict)
mdns_names: dict[str, str] = field(default_factory=dict)
entity_id_mappings: dict[str, SonosSpeaker] = field(default_factory=dict)
# Maps the entity unique id to the associated SonosSpeaker instance.
unique_id_speaker_mappings: dict[str, SonosSpeaker] = field(default_factory=dict)
unjoin_data: dict[str, UnjoinData] = field(default_factory=dict)


Expand Down
32 changes: 26 additions & 6 deletions homeassistant/components/sonos/media_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,12 @@
from homeassistant.const import ATTR_TIME
from homeassistant.core import HomeAssistant, ServiceCall, SupportsResponse, callback
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import config_validation as cv, entity_platform, service
from homeassistant.helpers import (
config_validation as cv,
entity_platform,
entity_registry as er,
service,
)
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.event import async_call_later
Expand Down Expand Up @@ -880,13 +885,28 @@ async def async_browse_media(
async def async_join_players(self, group_members: list[str]) -> None:
"""Join `group_members` as a player group with the current player."""
speakers = []

entity_registry = er.async_get(self.hass)
for entity_id in group_members:
if speaker := self.config_entry.runtime_data.entity_id_mappings.get(
entity_id
if not (entity_reg_entry := entity_registry.async_get(entity_id)):
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="entity_not_found",
translation_placeholders={"entity_id": entity_id},
)
if not (
speaker
:= self.config_entry.runtime_data.unique_id_speaker_mappings.get(
entity_reg_entry.unique_id
)
):
speakers.append(speaker)
else:
raise HomeAssistantError(f"Not a known Sonos entity_id: {entity_id}")
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="speaker_not_found",
translation_placeholders={"entity_id": entity_id},
)

speakers.append(speaker)

await SonosSpeaker.join_multi(
self.hass, self.config_entry, self.speaker, speakers
Expand Down
6 changes: 6 additions & 0 deletions homeassistant/components/sonos/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,12 @@
"announce_media_error": {
"message": "Announcing clip {media_id} failed {response}"
},
"entity_not_found": {
"message": "Entity {entity_id} not found."
},
"speaker_not_found": {
"message": "{entity_id} is not a known Sonos speaker."
},
"timeout_join": {
"message": "Timeout while waiting for Sonos player to join the group {group_description}"
}
Expand Down
55 changes: 37 additions & 18 deletions homeassistant/components/sun/condition.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.condition import (
Condition,
ConditionCheckerType,
condition_trace_set_result,
condition_trace_update_result,
Expand All @@ -37,13 +38,6 @@
)


async def async_validate_condition_config(
hass: HomeAssistant, config: ConfigType
) -> ConfigType:
"""Validate config."""
return _CONDITION_SCHEMA(config) # type: ignore[no-any-return]


def sun(
hass: HomeAssistant,
before: str | None = None,
Expand Down Expand Up @@ -128,16 +122,41 @@ def sun(
return True


def async_condition_from_config(config: ConfigType) -> ConditionCheckerType:
"""Wrap action method with sun based condition."""
before = config.get("before")
after = config.get("after")
before_offset = config.get("before_offset")
after_offset = config.get("after_offset")
class SunCondition(Condition):
"""Sun condition."""

def __init__(self, hass: HomeAssistant, config: ConfigType) -> None:
"""Initialize condition."""
self._config = config
self._hass = hass

@classmethod
async def async_validate_condition_config(
cls, hass: HomeAssistant, config: ConfigType
) -> ConfigType:
"""Validate config."""
return _CONDITION_SCHEMA(config) # type: ignore[no-any-return]

async def async_condition_from_config(self) -> ConditionCheckerType:
"""Wrap action method with sun based condition."""
before = self._config.get("before")
after = self._config.get("after")
before_offset = self._config.get("before_offset")
after_offset = self._config.get("after_offset")

@trace_condition_function
def sun_if(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool:
"""Validate time based if-condition."""
return sun(hass, before, after, before_offset, after_offset)

return sun_if


CONDITIONS: dict[str, type[Condition]] = {
"sun": SunCondition,
}

@trace_condition_function
def sun_if(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool:
"""Validate time based if-condition."""
return sun(hass, before, after, before_offset, after_offset)

return sun_if
async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]:
"""Return the sun conditions."""
return CONDITIONS
Loading
Loading