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
1fc624c
Update LLM selector serializer to support ObjectSelector fields and a…
allenporter Jul 4, 2025
4be2e84
Add backward compatibility with older versions of Traccar server (#14…
robin-thoni Jul 4, 2025
99d63c4
Add comment about error assigning in frame.report_usage (#148105)
emontnemery Jul 4, 2025
b3d9908
Add AI task structured output (#148083)
allenporter Jul 4, 2025
e47bdc0
Set docstyle convention to google in ruff (#148142)
epenet Jul 4, 2025
510fd09
Allow core integrations to describe their conditions (#147529)
emontnemery Jul 4, 2025
40fcc3b
Rename Matter device conversion methods (#148090)
harryh Jul 4, 2025
40ec51c
Add redirect URL in Google Assistant SDK setup (#148076)
tronikos Jul 4, 2025
e98fe7d
Add data_description to Opower forms (#148099)
tronikos Jul 4, 2025
1cb9767
Enable strict typing for Opower (#148096)
tronikos Jul 4, 2025
83ae5f5
Bump pydrawise to 2025.7.0 (#148088)
dknowles2 Jul 4, 2025
cde17fc
add extra tests for media source URI parsing (#148114)
balloob Jul 4, 2025
8ce30d9
Add tests of legacy entity without platform writing state (#148109)
emontnemery Jul 4, 2025
783102f
[ci] Fix typing issue with aiohttp and aiosignal (#148141)
cdce8p Jul 4, 2025
3f752e1
Replace MediaPlayerState.STANDBY with MediaPlayerState.OFF in roku (#…
emontnemery Jul 4, 2025
b7f8305
Update frontend to 20250702.1 (#148131)
bramkragten Jul 4, 2025
fd86a43
Replace MediaPlayerState.STANDBY with MediaPlayerState.OFF in ps4 (#1…
emontnemery Jul 4, 2025
811f085
Replace MediaPlayerState.STANDBY with MediaPlayerState.IDLE in androi…
emontnemery Jul 4, 2025
dc20375
Replace MediaPlayerState.STANDBY with MediaPlayerState.OFF in snapcas…
emontnemery Jul 4, 2025
631523d
Replace MediaPlayerState.STANDBY with MediaPlayerState.OFF in lookin …
emontnemery Jul 4, 2025
a046530
Replace MediaPlayerState.STANDBY with MediaPlayerState.IDLE in mediar…
emontnemery Jul 4, 2025
04bd196
Replace MediaPlayerState.STANDBY with MediaPlayerState.OFF in apple_t…
emontnemery Jul 4, 2025
cc2aca2
Fix Telegram bots using plain text parser failing to load on restart …
hanwg Jul 4, 2025
5d258c2
Bump aioamazondevices to 3.2.3 (#148082)
chemelli74 Jul 4, 2025
6235adc
Fix flaky emulated_roku/test_binding.py::test_events_fired_properly t…
frenck Jul 4, 2025
3250a2f
Bump aioautomower to 1.2.0 (#148078)
Thomas55555 Jul 4, 2025
cf931a7
Remove incorrect use of via_device in roon component (#146572)
pavoni Jul 4, 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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ on:
type: boolean

env:
CACHE_VERSION: 3
CACHE_VERSION: 4
UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 1
HA_SHORT_VERSION: "2025.8"
Expand Down
1 change: 1 addition & 0 deletions .strict-typing
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,7 @@ homeassistant.components.openai_conversation.*
homeassistant.components.openexchangerates.*
homeassistant.components.opensky.*
homeassistant.components.openuv.*
homeassistant.components.opower.*
homeassistant.components.oralb.*
homeassistant.components.otbr.*
homeassistant.components.overkiz.*
Expand Down
2 changes: 2 additions & 0 deletions homeassistant/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
from .helpers import (
area_registry,
category_registry,
condition,
config_validation as cv,
device_registry,
entity,
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(condition.async_setup(hass)),
create_eager_task(trigger.async_setup(hass)),
)

Expand Down
32 changes: 30 additions & 2 deletions homeassistant/components/ai_task/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
"""Integration to offer AI tasks to Home Assistant."""

import logging
from typing import Any

import voluptuous as vol

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.const import ATTR_ENTITY_ID, CONF_DESCRIPTION, CONF_SELECTOR
from homeassistant.core import (
HassJobType,
HomeAssistant,
Expand All @@ -14,12 +15,14 @@
SupportsResponse,
callback,
)
from homeassistant.helpers import config_validation as cv, storage
from homeassistant.helpers import config_validation as cv, selector, storage
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.typing import UNDEFINED, ConfigType, UndefinedType

from .const import (
ATTR_INSTRUCTIONS,
ATTR_REQUIRED,
ATTR_STRUCTURE,
ATTR_TASK_NAME,
DATA_COMPONENT,
DATA_PREFERENCES,
Expand Down Expand Up @@ -47,6 +50,27 @@

CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)

STRUCTURE_FIELD_SCHEMA = vol.Schema(
{
vol.Optional(CONF_DESCRIPTION): str,
vol.Optional(ATTR_REQUIRED): bool,
vol.Required(CONF_SELECTOR): selector.validate_selector,
}
)


def _validate_structure_fields(value: dict[str, Any]) -> vol.Schema:
"""Validate the structure fields as a voluptuous Schema."""
if not isinstance(value, dict):
raise vol.Invalid("Structure must be a dictionary")
fields = {}
for k, v in value.items():
field_class = vol.Required if v.get(ATTR_REQUIRED, False) else vol.Optional
fields[field_class(k, description=v.get(CONF_DESCRIPTION))] = selector.selector(
v[CONF_SELECTOR]
)
return vol.Schema(fields, extra=vol.PREVENT_EXTRA)


async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Register the process service."""
Expand All @@ -64,6 +88,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
vol.Required(ATTR_TASK_NAME): cv.string,
vol.Optional(ATTR_ENTITY_ID): cv.entity_id,
vol.Required(ATTR_INSTRUCTIONS): cv.string,
vol.Optional(ATTR_STRUCTURE): vol.All(
vol.Schema({str: STRUCTURE_FIELD_SCHEMA}),
_validate_structure_fields,
),
}
),
supports_response=SupportsResponse.ONLY,
Expand Down
2 changes: 2 additions & 0 deletions homeassistant/components/ai_task/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@

ATTR_INSTRUCTIONS: Final = "instructions"
ATTR_TASK_NAME: Final = "task_name"
ATTR_STRUCTURE: Final = "structure"
ATTR_REQUIRED: Final = "required"

DEFAULT_SYSTEM_PROMPT = (
"You are a Home Assistant expert and help users with their tasks."
Expand Down
6 changes: 6 additions & 0 deletions homeassistant/components/ai_task/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,9 @@ generate_data:
domain: ai_task
supported_features:
- ai_task.AITaskEntityFeature.GENERATE_DATA
structure:
advanced: true
required: false
example: '{ "name": { "selector": { "text": }, "description": "Name of the user", "required": "True" } } }, "age": { "selector": { "number": }, "description": "Age of the user" } }'
selector:
object:
4 changes: 4 additions & 0 deletions homeassistant/components/ai_task/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
"entity_id": {
"name": "Entity ID",
"description": "Entity ID to run the task on. If not provided, the preferred entity will be used."
},
"structure": {
"name": "Structured output",
"description": "When set, the AI Task will output fields with this in structure. The structure is a dictionary where the keys are the field names and the values contain a 'description', a 'selector', and an optional 'required' field."
}
}
}
Expand Down
7 changes: 7 additions & 0 deletions homeassistant/components/ai_task/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from dataclasses import dataclass
from typing import Any

import voluptuous as vol

from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError

Expand All @@ -17,6 +19,7 @@ async def async_generate_data(
task_name: str,
entity_id: str | None = None,
instructions: str,
structure: vol.Schema | None = None,
) -> GenDataTaskResult:
"""Run a task in the AI Task integration."""
if entity_id is None:
Expand All @@ -38,6 +41,7 @@ async def async_generate_data(
GenDataTask(
name=task_name,
instructions=instructions,
structure=structure,
)
)

Expand All @@ -52,6 +56,9 @@ class GenDataTask:
instructions: str
"""Instructions on what needs to be done."""

structure: vol.Schema | None = None
"""Optional structure for the data to be generated."""

def __str__(self) -> str:
"""Return task as a string."""
return f"<GenDataTask {self.name}: {id(self)}>"
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.2.2"]
"requirements": ["aioamazondevices==3.2.3"]
}
2 changes: 1 addition & 1 deletion homeassistant/components/androidtv/media_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
ANDROIDTV_STATES = {
"off": MediaPlayerState.OFF,
"idle": MediaPlayerState.IDLE,
"standby": MediaPlayerState.STANDBY,
"standby": MediaPlayerState.IDLE,
"playing": MediaPlayerState.PLAYING,
"paused": MediaPlayerState.PAUSED,
}
Expand Down
4 changes: 2 additions & 2 deletions homeassistant/components/apple_tv/media_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ def state(self) -> MediaPlayerState | None:
self._is_feature_available(FeatureName.PowerState)
and self.atv.power.power_state == PowerState.Off
):
return MediaPlayerState.STANDBY
return MediaPlayerState.OFF
if self._playing:
state = self._playing.device_state
if state in (DeviceState.Idle, DeviceState.Loading):
Expand All @@ -200,7 +200,7 @@ def state(self) -> MediaPlayerState | None:
return MediaPlayerState.PLAYING
if state in (DeviceState.Paused, DeviceState.Seeking, DeviceState.Stopped):
return MediaPlayerState.PAUSED
return MediaPlayerState.STANDBY # Bad or unknown state?
return MediaPlayerState.IDLE # Bad or unknown state?
return None

@callback
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 @@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20250702.0"]
"requirements": ["home-assistant-frontend==20250702.1"]
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

from homeassistant.components.application_credentials import AuthorizationServer
from homeassistant.core import HomeAssistant
from homeassistant.helpers.config_entry_oauth2_flow import (
AUTH_CALLBACK_PATH,
MY_AUTH_CALLBACK_PATH,
)


async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationServer:
Expand All @@ -14,12 +18,14 @@ async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationSe

async def async_get_description_placeholders(hass: HomeAssistant) -> dict[str, str]:
"""Return description placeholders for the credentials dialog."""
if "my" in hass.config.components:
redirect_url = MY_AUTH_CALLBACK_PATH
else:
ha_host = hass.config.external_url or "https://YOUR_DOMAIN:PORT"
redirect_url = f"{ha_host}{AUTH_CALLBACK_PATH}"
return {
"oauth_consent_url": (
"https://console.cloud.google.com/apis/credentials/consent"
),
"more_info_url": (
"https://www.home-assistant.io/integrations/google_assistant_sdk/"
),
"oauth_consent_url": "https://console.cloud.google.com/apis/credentials/consent",
"more_info_url": "https://www.home-assistant.io/integrations/google_assistant_sdk/",
"oauth_creds_url": "https://console.cloud.google.com/apis/credentials",
"redirect_url": redirect_url,
}
2 changes: 1 addition & 1 deletion homeassistant/components/google_assistant_sdk/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
}
},
"application_credentials": {
"description": "Follow the [instructions]({more_info_url}) for [OAuth consent screen]({oauth_consent_url}) to give Home Assistant access to your Google Assistant SDK. You also need to create Application Credentials linked to your account:\n1. Go to [Credentials]({oauth_creds_url}) and select **Create Credentials**.\n1. From the drop-down list select **OAuth client ID**.\n1. Select **Web application** for the Application Type."
"description": "Follow the [instructions]({more_info_url}) for [OAuth consent screen]({oauth_consent_url}) to give Home Assistant access to your Google Assistant SDK. You also need to create Application Credentials linked to your account:\n1. Go to [Credentials]({oauth_creds_url}) and select **Create Credentials**.\n1. From the drop-down list select **OAuth client ID**.\n1. Select **Web application** for the Application Type.\n1. Add `{redirect_url}` under *Authorized redirect URI*."
},
"services": {
"send_text_command": {
Expand Down
21 changes: 8 additions & 13 deletions homeassistant/components/habitica/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,21 +95,16 @@ def get_recurrence_rule(recurrence: rrule) -> str:

'DTSTART:YYYYMMDDTHHMMSS\nRRULE:FREQ=YEARLY;INTERVAL=2'

Parameters
----------
recurrence : rrule
An RRULE object.

Returns
-------
str
Args:
recurrence: An RRULE object.

Returns:
The recurrence rule portion of the RRULE string, starting with 'FREQ='.

Example
-------
>>> rule = get_recurrence_rule(task)
>>> print(rule)
'FREQ=YEARLY;INTERVAL=2'
Example:
>>> rule = get_recurrence_rule(task)
>>> print(rule)
'FREQ=YEARLY;INTERVAL=2'

"""
return str(recurrence).split("RRULE:")[1]
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 @@ -64,7 +64,7 @@ async def ban_startup(app: Application) -> None:
"""Initialize bans when app starts up."""
await app[KEY_BAN_MANAGER].async_load()

app.on_startup.append(ban_startup)
app.on_startup.append(ban_startup) # type: ignore[arg-type]


@middleware
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/husqvarna_automower/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
"iot_class": "cloud_push",
"loggers": ["aioautomower"],
"quality_scale": "silver",
"requirements": ["aioautomower==1.0.1"]
"requirements": ["aioautomower==1.2.0"]
}
2 changes: 1 addition & 1 deletion homeassistant/components/hydrawise/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/hydrawise",
"iot_class": "cloud_polling",
"loggers": ["pydrawise"],
"requirements": ["pydrawise==2025.6.0"]
"requirements": ["pydrawise==2025.7.0"]
}
6 changes: 2 additions & 4 deletions homeassistant/components/lookin/media_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ async def async_mute_volume(self, mute: bool) -> None:
async def async_turn_off(self) -> None:
"""Turn the media player off."""
await self._async_send_command(self._power_off_command)
self._attr_state = MediaPlayerState.STANDBY
self._attr_state = MediaPlayerState.OFF
self.async_write_ha_state()

async def async_turn_on(self) -> None:
Expand All @@ -159,7 +159,5 @@ def _update_from_status(self, status: str) -> None:
state = status[0]
mute = status[2]

self._attr_state = (
MediaPlayerState.ON if state == "1" else MediaPlayerState.STANDBY
)
self._attr_state = MediaPlayerState.ON if state == "1" else MediaPlayerState.OFF
self._attr_is_volume_muted = mute == "0"
Loading
Loading