Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
b5821ef
Update frontend to 20250626.0 (#147601)
bramkragten Jun 26, 2025
b4dd912
Refactor in Google AI TTS in preparation for STT (#147562)
tronikos Jun 26, 2025
69af74a
Improve explanation on how to get API token in Telegram (#147605)
joostlek Jun 26, 2025
35478e3
Set Google AI model as device model (#147582)
joostlek Jun 26, 2025
bf88fcd
Add Manual Charge Switch for Installers for Kostal Plenticore (#146932)
Schlauer-Hax Jun 26, 2025
af7b1a7
Add description placeholders to `SchemaFlowFormStep` (#147544)
HarvsG Jun 26, 2025
1416f0f
Fix meaters not being added after a reload (#147614)
joostlek Jun 26, 2025
aef0809
Fix asset url in Habitica integration (#147612)
tr4nt0r Jun 26, 2025
61a3246
Hide Telegram bot proxy URL behind section (#147613)
joostlek Jun 26, 2025
c2f1e86
Add action exceptions to Alexa Devices (#147546)
chemelli74 Jun 26, 2025
17cd397
Create a new client session for air-Q to fix cookie polution (#147027)
Sibgatulin Jun 26, 2025
babecdf
Add Diagnostics to PlayStation Network (#147607)
JackJPowell Jun 26, 2025
06d04c0
Use non-autospec mock for Reolink's host tests (#147619)
abmantis Jun 26, 2025
b313135
Use non-autospec mock for Reolink's light tests (#147621)
abmantis Jun 26, 2025
7a08edc
Add Claude to gitignore (#147622)
frenck Jun 26, 2025
2655edc
Extend GitHub Copilot instructions and make it suitable for Claude Co…
frenck Jun 26, 2025
1bb653b
Remove unused config regexps (#147631)
scop Jun 26, 2025
9bd0762
Make sure Ollama integration migration is clean (#147630)
joostlek Jun 26, 2025
43535ed
Make sure Anthropic integration migration is clean (#147629)
joostlek Jun 26, 2025
4bdf3d6
Make sure OpenAI integration migration is clean (#147627)
joostlek Jun 26, 2025
1b2be08
Make sure Google Generative AI integration migration is clean (#147625)
joostlek Jun 26, 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
1,102 changes: 1,013 additions & 89 deletions .github/copilot-instructions.md

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -137,4 +137,8 @@ tmp_cache
.ropeproject

# Will be created from script/split_tests.py
pytest_buckets.txt
pytest_buckets.txt

# AI tooling
.claude

1 change: 1 addition & 0 deletions CLAUDE.md
4 changes: 2 additions & 2 deletions homeassistant/components/airq/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator

Expand Down Expand Up @@ -39,7 +39,7 @@ def __init__(
name=DOMAIN,
update_interval=timedelta(seconds=UPDATE_INTERVAL),
)
session = async_get_clientsession(hass)
session = async_create_clientsession(hass)
self.airq = AirQ(
entry.data[CONF_IP_ADDRESS], entry.data[CONF_PASSWORD], session
)
Expand Down
2 changes: 2 additions & 0 deletions homeassistant/components/alexa_devices/notify.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

from .coordinator import AmazonConfigEntry
from .entity import AmazonEntity
from .utils import alexa_api_call

PARALLEL_UPDATES = 1

Expand Down Expand Up @@ -70,6 +71,7 @@ class AmazonNotifyEntity(AmazonEntity, NotifyEntity):

entity_description: AmazonNotifyEntityDescription

@alexa_api_call
async def async_send_message(
self, message: str, title: str | None = None, **kwargs: Any
) -> None:
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/alexa_devices/quality_scale.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ rules:
unique-config-entry: done

# Silver
action-exceptions: todo
action-exceptions: done
config-entry-unloading: done
docs-configuration-parameters: todo
docs-installation-parameters: todo
Expand Down
8 changes: 8 additions & 0 deletions homeassistant/components/alexa_devices/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,5 +70,13 @@
"name": "Do not disturb"
}
}
},
"exceptions": {
"cannot_connect": {
"message": "Error connecting: {error}"
},
"cannot_retrieve_data": {
"message": "Error retrieving data: {error}"
}
}
}
2 changes: 2 additions & 0 deletions homeassistant/components/alexa_devices/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

from .coordinator import AmazonConfigEntry
from .entity import AmazonEntity
from .utils import alexa_api_call

PARALLEL_UPDATES = 1

Expand Down Expand Up @@ -60,6 +61,7 @@ class AmazonSwitchEntity(AmazonEntity, SwitchEntity):

entity_description: AmazonSwitchEntityDescription

@alexa_api_call
async def _switch_set_state(self, state: bool) -> None:
"""Set desired switch state."""
method = getattr(self.coordinator.api, self.entity_description.method)
Expand Down
40 changes: 40 additions & 0 deletions homeassistant/components/alexa_devices/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""Utils for Alexa Devices."""

from collections.abc import Awaitable, Callable, Coroutine
from functools import wraps
from typing import Any, Concatenate

from aioamazondevices.exceptions import CannotConnect, CannotRetrieveData

from homeassistant.exceptions import HomeAssistantError

from .const import DOMAIN
from .entity import AmazonEntity


def alexa_api_call[_T: AmazonEntity, **_P](
func: Callable[Concatenate[_T, _P], Awaitable[None]],
) -> Callable[Concatenate[_T, _P], Coroutine[Any, Any, None]]:
"""Catch Alexa API call exceptions."""

@wraps(func)
async def cmd_wrapper(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> None:
"""Wrap all command methods."""
try:
await func(self, *args, **kwargs)
except CannotConnect as err:
self.coordinator.last_update_success = False
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="cannot_connect",
translation_placeholders={"error": repr(err)},
) from err
except CannotRetrieveData as err:
self.coordinator.last_update_success = False
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="cannot_retrieve_data",
translation_placeholders={"error": repr(err)},
) from err

return cmd_wrapper
6 changes: 6 additions & 0 deletions homeassistant/components/anthropic/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,12 @@ async def async_migrate_integration(hass: HomeAssistant) -> None:
device.id,
remove_config_entry_id=entry.entry_id,
)
else:
device_registry.async_update_device(
device.id,
remove_config_entry_id=entry.entry_id,
remove_config_subentry_id=None,
)

if not use_existing:
await hass.config_entries.async_remove(entry.entry_id)
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==20250625.0"]
"requirements": ["home-assistant-frontend==20250626.0"]
}
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,12 @@ async def async_migrate_integration(hass: HomeAssistant) -> None:
device.id,
remove_config_entry_id=entry.entry_id,
)
else:
device_registry.async_update_device(
device.id,
remove_config_entry_id=entry.entry_id,
remove_config_subentry_id=None,
)

if not use_existing:
await hass.config_entries.async_remove(entry.entry_id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,12 @@ async def _transform_stream(
class GoogleGenerativeAILLMBaseEntity(Entity):
"""Google Generative AI base entity."""

def __init__(self, entry: ConfigEntry, subentry: ConfigSubentry) -> None:
def __init__(
self,
entry: ConfigEntry,
subentry: ConfigSubentry,
default_model: str = RECOMMENDED_CHAT_MODEL,
) -> None:
"""Initialize the agent."""
self.entry = entry
self.subentry = subentry
Expand All @@ -312,7 +317,7 @@ def __init__(self, entry: ConfigEntry, subentry: ConfigSubentry) -> None:
identifiers={(DOMAIN, subentry.subentry_id)},
name=subentry.title,
manufacturer="Google",
model="Generative AI",
model=subentry.data.get(CONF_CHAT_MODEL, default_model).split("/")[-1],
entry_type=dr.DeviceEntryType.SERVICE,
)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"""Helper classes for Google Generative AI integration."""

from __future__ import annotations

from contextlib import suppress
import io
import wave

from homeassistant.exceptions import HomeAssistantError

from .const import LOGGER


def convert_to_wav(audio_data: bytes, mime_type: str) -> bytes:
"""Generate a WAV file header for the given audio data and parameters.

Args:
audio_data: The raw audio data as a bytes object.
mime_type: Mime type of the audio data.

Returns:
A bytes object representing the WAV file header.

"""
parameters = _parse_audio_mime_type(mime_type)

wav_buffer = io.BytesIO()
with wave.open(wav_buffer, "wb") as wf:
wf.setnchannels(1)
wf.setsampwidth(parameters["bits_per_sample"] // 8)
wf.setframerate(parameters["rate"])
wf.writeframes(audio_data)

return wav_buffer.getvalue()


# Below code is from https://aistudio.google.com/app/generate-speech
# when you select "Get SDK code to generate speech".
def _parse_audio_mime_type(mime_type: str) -> dict[str, int]:
"""Parse bits per sample and rate from an audio MIME type string.

Assumes bits per sample is encoded like "L16" and rate as "rate=xxxxx".

Args:
mime_type: The audio MIME type string (e.g., "audio/L16;rate=24000").

Returns:
A dictionary with "bits_per_sample" and "rate" keys. Values will be
integers if found, otherwise None.

"""
if not mime_type.startswith("audio/L"):
LOGGER.warning("Received unexpected MIME type %s", mime_type)
raise HomeAssistantError(f"Unsupported audio MIME type: {mime_type}")

bits_per_sample = 16
rate = 24000

# Extract rate from parameters
parts = mime_type.split(";")
for param in parts: # Skip the main type part
param = param.strip()
if param.lower().startswith("rate="):
# Handle cases like "rate=" with no value or non-integer value and keep rate as default
with suppress(ValueError, IndexError):
rate_str = param.split("=", 1)[1]
rate = int(rate_str)
elif param.startswith("audio/L"):
# Keep bits_per_sample as default if conversion fails
with suppress(ValueError, IndexError):
bits_per_sample = int(param.split("L", 1)[1])

return {"bits_per_sample": bits_per_sample, "rate": rate}
70 changes: 7 additions & 63 deletions homeassistant/components/google_generative_ai_conversation/tts.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@
from __future__ import annotations

from collections.abc import Mapping
from contextlib import suppress
import io
from typing import Any
import wave

from google.genai import types
from google.genai.errors import APIError, ClientError
Expand All @@ -18,13 +15,14 @@
TtsAudioType,
Voice,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.config_entries import ConfigEntry, ConfigSubentry
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback

from .const import CONF_CHAT_MODEL, LOGGER, RECOMMENDED_TTS_MODEL
from .entity import GoogleGenerativeAILLMBaseEntity
from .helpers import convert_to_wav


async def async_setup_entry(
Expand Down Expand Up @@ -116,6 +114,10 @@ class GoogleGenerativeAITextToSpeechEntity(
)
]

def __init__(self, config_entry: ConfigEntry, subentry: ConfigSubentry) -> None:
"""Initialize the TTS entity."""
super().__init__(config_entry, subentry, RECOMMENDED_TTS_MODEL)

@callback
def async_get_supported_voices(self, language: str) -> list[Voice]:
"""Return a list of supported voices for a language."""
Expand Down Expand Up @@ -152,62 +154,4 @@ async def async_get_tts_audio(
except (APIError, ClientError, ValueError) as exc:
LOGGER.error("Error during TTS: %s", exc, exc_info=True)
raise HomeAssistantError(exc) from exc
return "wav", self._convert_to_wav(data, mime_type)

def _convert_to_wav(self, audio_data: bytes, mime_type: str) -> bytes:
"""Generate a WAV file header for the given audio data and parameters.

Args:
audio_data: The raw audio data as a bytes object.
mime_type: Mime type of the audio data.

Returns:
A bytes object representing the WAV file header.

"""
parameters = self._parse_audio_mime_type(mime_type)

wav_buffer = io.BytesIO()
with wave.open(wav_buffer, "wb") as wf:
wf.setnchannels(1)
wf.setsampwidth(parameters["bits_per_sample"] // 8)
wf.setframerate(parameters["rate"])
wf.writeframes(audio_data)

return wav_buffer.getvalue()

def _parse_audio_mime_type(self, mime_type: str) -> dict[str, int]:
"""Parse bits per sample and rate from an audio MIME type string.

Assumes bits per sample is encoded like "L16" and rate as "rate=xxxxx".

Args:
mime_type: The audio MIME type string (e.g., "audio/L16;rate=24000").

Returns:
A dictionary with "bits_per_sample" and "rate" keys. Values will be
integers if found, otherwise None.

"""
if not mime_type.startswith("audio/L"):
LOGGER.warning("Received unexpected MIME type %s", mime_type)
raise HomeAssistantError(f"Unsupported audio MIME type: {mime_type}")

bits_per_sample = 16
rate = 24000

# Extract rate from parameters
parts = mime_type.split(";")
for param in parts: # Skip the main type part
param = param.strip()
if param.lower().startswith("rate="):
# Handle cases like "rate=" with no value or non-integer value and keep rate as default
with suppress(ValueError, IndexError):
rate_str = param.split("=", 1)[1]
rate = int(rate_str)
elif param.startswith("audio/L"):
# Keep bits_per_sample as default if conversion fails
with suppress(ValueError, IndexError):
bits_per_sample = int(param.split("L", 1)[1])

return {"bits_per_sample": bits_per_sample, "rate": rate}
return "wav", convert_to_wav(data, mime_type)
2 changes: 1 addition & 1 deletion homeassistant/components/habitica/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
SITE_DATA_URL = "https://habitica.com/user/settings/siteData"
FORGOT_PASSWORD_URL = "https://habitica.com/forgot-password"
SIGN_UP_URL = "https://habitica.com/register"
HABITICANS_URL = "https://habitica.com/static/img/home-main@3x.ffc32b12.png"
HABITICANS_URL = "https://cdn.habitica.com/assets/home-main@3x-Dwnue45Z.png"

DOMAIN = "habitica"

Expand Down
Loading
Loading