Compare commits

..

12 Commits

Author SHA1 Message Date
farmio
9d979a1d7f review; mark disabled by default 2025-12-03 20:09:09 +01:00
farmio
0a1214256a Add counter for KNX DataSecure undecodable telegrams 2025-12-03 14:52:04 +01:00
Artur Pragacz
1a60c46d67 Bump aioonkyo to 0.4.0 (#157838) 2025-12-03 14:46:52 +01:00
Matthias Alphart
62fba5ca20 Update xknx to 3.12.0 (#157835) 2025-12-03 14:40:40 +01:00
victorigualada
b54cde795c Bump hass-nabucasa from 1.6.2 to 1.7.0 (#157834) 2025-12-03 14:37:45 +01:00
victorigualada
0f456373bf Allow non strict response_format structures for Cloud LLM generation (#157822) 2025-12-03 14:31:09 +01:00
IAmStiven
a5042027b8 Add support for new ElevenLabs model Scribe v2 (#156961) 2025-12-03 14:29:25 +01:00
Franck Nijhof
b15b5ba95c Add final learn more and feedback links for purpose-specific triggers and conditions preview feature (#157830) 2025-12-03 13:14:37 +01:00
Robert Resch
cd6e72798e Prioritize default stun port over alternative (#157829) 2025-12-03 13:14:28 +01:00
Kamil Breguła
739157e59f Simplify availability property in WLED (#157800)
Co-authored-by: mik-laj <12058428+mik-laj@users.noreply.github.com>
2025-12-03 13:00:21 +01:00
torben-iometer
267aa1af42 bump iometer to v0.3.0 (#157826) 2025-12-03 12:47:05 +01:00
Michael
7328b61a69 Add integration_type to Oralb (#157828) 2025-12-03 12:46:50 +01:00
29 changed files with 129 additions and 161 deletions

View File

@@ -8,6 +8,8 @@
"integration_type": "system",
"preview_features": {
"new_triggers_conditions": {
"feedback_url": "https://forms.gle/fWFZqf5MzuwWTsCH8",
"learn_more_url": "https://www.home-assistant.io/blog/2025/12/03/release-202512/#purpose-specific-triggers-and-conditions",
"report_issue_url": "https://github.com/home-assistant/core/issues/new?template=bug_report.yml&integration_link=https://www.home-assistant.io/integrations/automation&integration_name=Automation"
}
},

View File

@@ -407,8 +407,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
return [
RTCIceServer(
urls=[
"stun:stun.home-assistant.io:80",
"stun:stun.home-assistant.io:3478",
"stun:stun.home-assistant.io:80",
]
),
]

View File

@@ -561,7 +561,7 @@ class BaseCloudLLMEntity(Entity):
"schema": _format_structured_output(
structure, chat_log.llm_api
),
"strict": True,
"strict": False,
},
}

View File

@@ -13,6 +13,6 @@
"integration_type": "system",
"iot_class": "cloud_push",
"loggers": ["acme", "hass_nabucasa", "snitun"],
"requirements": ["hass-nabucasa==1.6.2"],
"requirements": ["hass-nabucasa==1.7.0"],
"single_config_entry": true
}

View File

@@ -17,7 +17,7 @@ DEFAULT_TTS_MODEL = "eleven_multilingual_v2"
DEFAULT_STABILITY = 0.5
DEFAULT_SIMILARITY = 0.75
DEFAULT_STT_AUTO_LANGUAGE = False
DEFAULT_STT_MODEL = "scribe_v1"
DEFAULT_STT_MODEL = "scribe_v2"
DEFAULT_STYLE = 0
DEFAULT_USE_SPEAKER_BOOST = True
@@ -129,4 +129,5 @@ STT_LANGUAGES = [
STT_MODELS = {
"scribe_v1": "Scribe v1",
"scribe_v1_experimental": "Scribe v1 Experimental",
"scribe_v2": "Scribe v2 Realtime",
}

View File

@@ -7,6 +7,6 @@
"integration_type": "device",
"iot_class": "local_polling",
"quality_scale": "bronze",
"requirements": ["iometer==0.2.0"],
"requirements": ["iometer==0.3.0"],
"zeroconf": ["_iometer._tcp.local."]
}

View File

@@ -21,6 +21,9 @@
"telegram_count": {
"default": "mdi:plus-network"
},
"telegrams_data_secure_undecodable": {
"default": "mdi:lock-alert"
},
"telegrams_incoming": {
"default": "mdi:upload-network"
},

View File

@@ -11,7 +11,7 @@
"loggers": ["xknx", "xknxproject"],
"quality_scale": "silver",
"requirements": [
"xknx==3.11.0",
"xknx==3.12.0",
"xknxproject==3.8.2",
"knx-frontend==2025.10.31.195356"
],

View File

@@ -108,6 +108,12 @@ SYSTEM_ENTITY_DESCRIPTIONS = (
+ knx.xknx.connection_manager.cemi_count_incoming
+ knx.xknx.connection_manager.cemi_count_incoming_error,
),
KNXSystemEntityDescription(
key="telegrams_data_secure_undecodable",
entity_registry_enabled_default=False,
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=lambda knx: knx.xknx.connection_manager.undecoded_data_secure,
),
)

View File

@@ -639,6 +639,10 @@
"name": "Telegrams",
"unit_of_measurement": "telegrams"
},
"telegrams_data_secure_undecodable": {
"name": "Undecodable Data Secure telegrams",
"unit_of_measurement": "[%key:component::knx::entity::sensor::telegrams_incoming_error::unit_of_measurement%]"
},
"telegrams_incoming": {
"name": "Incoming telegrams",
"unit_of_measurement": "[%key:component::knx::entity::sensor::telegram_count::unit_of_measurement%]"

View File

@@ -9,7 +9,7 @@
"iot_class": "local_push",
"loggers": ["aioonkyo"],
"quality_scale": "bronze",
"requirements": ["aioonkyo==0.3.0"],
"requirements": ["aioonkyo==0.4.0"],
"ssdp": [
{
"deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1",

View File

@@ -10,6 +10,7 @@
"config_flow": true,
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/oralb",
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["oralb_ble"],
"requirements": ["oralb-ble==0.17.6"]

View File

@@ -24,14 +24,9 @@ from homeassistant.helpers.trigger import (
async_get_all_descriptions as async_get_all_trigger_descriptions,
)
from homeassistant.helpers.typing import ConfigType
from homeassistant.util.hass_dict import HassKey
_LOGGER = logging.getLogger(__name__)
FLATTENED_SERVICE_DESCRIPTIONS_CACHE: HassKey[
tuple[dict[str, dict[str, Any]], dict[str, dict[str, Any] | None]]
] = HassKey("websocket_automation_flat_service_description_cache")
@dataclass(slots=True, kw_only=True)
class _EntityFilter:
@@ -222,29 +217,12 @@ async def async_get_services_for_target(
) -> set[str]:
"""Get services for a target."""
descriptions = await async_get_all_service_descriptions(hass)
def get_flattened_service_descriptions() -> dict[str, dict[str, Any] | None]:
"""Get flattened service descriptions, with caching."""
if FLATTENED_SERVICE_DESCRIPTIONS_CACHE in hass.data:
cached_descriptions, cached_flat_descriptions = hass.data[
FLATTENED_SERVICE_DESCRIPTIONS_CACHE
]
# If the descriptions are the same, return the cached flattened version
if cached_descriptions is descriptions:
return cached_flat_descriptions
# Flatten dicts to be keyed by domain.name to match trigger/condition format
flat_descriptions = {
f"{domain}.{service_name}": desc
for domain, services in descriptions.items()
for service_name, desc in services.items()
}
hass.data[FLATTENED_SERVICE_DESCRIPTIONS_CACHE] = (
descriptions,
flat_descriptions,
)
return flat_descriptions
# Flatten dicts to be keyed by domain.name to match trigger/condition format
descriptions_flatten = {
f"{domain}.{service_name}": desc
for domain, services in descriptions.items()
for service_name, desc in services.items()
}
return _async_get_automation_components_for_target(
hass, target_selector, expand_group, get_flattened_service_descriptions()
hass, target_selector, expand_group, descriptions_flatten
)

View File

@@ -150,12 +150,9 @@ class WLEDSegmentLight(WLEDEntity, LightEntity):
@property
def available(self) -> bool:
"""Return True if entity is available."""
try:
self.coordinator.data.state.segments[self._segment]
except KeyError:
return False
return super().available
return (
super().available and self._segment in self.coordinator.data.state.segments
)
@property
def rgb_color(self) -> tuple[int, int, int] | None:

View File

@@ -97,12 +97,9 @@ class WLEDNumber(WLEDEntity, NumberEntity):
@property
def available(self) -> bool:
"""Return True if entity is available."""
try:
self.coordinator.data.state.segments[self._segment]
except KeyError:
return False
return super().available
return (
super().available and self._segment in self.coordinator.data.state.segments
)
@property
def native_value(self) -> float | None:

View File

@@ -31,10 +31,6 @@ rules:
config-entry-unloading: done
docs-configuration-parameters: done
docs-installation-parameters: todo
entity-unavailable:
status: todo
comment: |
The WLEDSegmentLight.available property can just be an if .. in .. check
integration-owner: done
log-when-unavailable: done
parallel-updates: done

View File

@@ -173,12 +173,9 @@ class WLEDPaletteSelect(WLEDEntity, SelectEntity):
@property
def available(self) -> bool:
"""Return True if entity is available."""
try:
self.coordinator.data.state.segments[self._segment]
except KeyError:
return False
return super().available
return (
super().available and self._segment in self.coordinator.data.state.segments
)
@property
def current_option(self) -> str | None:

View File

@@ -167,12 +167,9 @@ class WLEDReverseSwitch(WLEDEntity, SwitchEntity):
@property
def available(self) -> bool:
"""Return True if entity is available."""
try:
self.coordinator.data.state.segments[self._segment]
except KeyError:
return False
return super().available
return (
super().available and self._segment in self.coordinator.data.state.segments
)
@property
def is_on(self) -> bool:

View File

@@ -4832,7 +4832,7 @@
},
"oralb": {
"name": "Oral-B",
"integration_type": "hub",
"integration_type": "device",
"config_flow": true,
"iot_class": "local_push"
},

View File

@@ -6,8 +6,8 @@ To update, run python3 -m script.hassfest
LABS_PREVIEW_FEATURES = {
"automation": {
"new_triggers_conditions": {
"feedback_url": "",
"learn_more_url": "",
"feedback_url": "https://forms.gle/fWFZqf5MzuwWTsCH8",
"learn_more_url": "https://www.home-assistant.io/blog/2025/12/03/release-202512/#purpose-specific-triggers-and-conditions",
"report_issue_url": "https://github.com/home-assistant/core/issues/new?template=bug_report.yml&integration_link=https://www.home-assistant.io/integrations/automation&integration_name=Automation",
},
},

View File

@@ -36,7 +36,7 @@ fnv-hash-fast==1.6.0
go2rtc-client==0.3.0
ha-ffmpeg==3.2.2
habluetooth==5.8.0
hass-nabucasa==1.6.2
hass-nabucasa==1.7.0
hassil==3.5.0
home-assistant-bluetooth==1.13.1
home-assistant-frontend==20251202.0

View File

@@ -48,7 +48,7 @@ dependencies = [
"fnv-hash-fast==1.6.0",
# hass-nabucasa is imported by helpers which don't depend on the cloud
# integration
"hass-nabucasa==1.6.2",
"hass-nabucasa==1.7.0",
# When bumping httpx, please check the version pins of
# httpcore, anyio, and h11 in gen_requirements_all
"httpx==0.28.1",

2
requirements.txt generated
View File

@@ -22,7 +22,7 @@ certifi>=2021.5.30
ciso8601==2.3.3
cronsim==2.7
fnv-hash-fast==1.6.0
hass-nabucasa==1.6.2
hass-nabucasa==1.7.0
httpx==0.28.1
home-assistant-bluetooth==1.13.1
ifaddr==0.2.0

8
requirements_all.txt generated
View File

@@ -340,7 +340,7 @@ aiontfy==0.6.1
aionut==4.3.4
# homeassistant.components.onkyo
aioonkyo==0.3.0
aioonkyo==0.4.0
# homeassistant.components.openexchangerates
aioopenexchangerates==0.6.8
@@ -1163,7 +1163,7 @@ habluetooth==5.8.0
hanna-cloud==0.0.6
# homeassistant.components.cloud
hass-nabucasa==1.6.2
hass-nabucasa==1.7.0
# homeassistant.components.splunk
hass-splunk==0.1.1
@@ -1288,7 +1288,7 @@ insteon-frontend-home-assistant==0.5.0
intellifire4py==4.2.1
# homeassistant.components.iometer
iometer==0.2.0
iometer==0.3.0
# homeassistant.components.iotty
iottycloud==0.3.0
@@ -3191,7 +3191,7 @@ wyoming==1.7.2
xiaomi-ble==1.2.0
# homeassistant.components.knx
xknx==3.11.0
xknx==3.12.0
# homeassistant.components.knx
xknxproject==3.8.2

View File

@@ -325,7 +325,7 @@ aiontfy==0.6.1
aionut==4.3.4
# homeassistant.components.onkyo
aioonkyo==0.3.0
aioonkyo==0.4.0
# homeassistant.components.openexchangerates
aioopenexchangerates==0.6.8
@@ -1033,7 +1033,7 @@ habluetooth==5.8.0
hanna-cloud==0.0.6
# homeassistant.components.cloud
hass-nabucasa==1.6.2
hass-nabucasa==1.7.0
# homeassistant.components.assist_satellite
# homeassistant.components.conversation
@@ -1134,7 +1134,7 @@ insteon-frontend-home-assistant==0.5.0
intellifire4py==4.2.1
# homeassistant.components.iometer
iometer==0.2.0
iometer==0.3.0
# homeassistant.components.iotty
iottycloud==0.3.0
@@ -2658,7 +2658,7 @@ wyoming==1.7.2
xiaomi-ble==1.2.0
# homeassistant.components.knx
xknx==3.11.0
xknx==3.12.0
# homeassistant.components.knx
xknxproject==3.8.2

View File

@@ -205,8 +205,8 @@ async def test_ws_get_client_config(
"iceServers": [
{
"urls": [
"stun:stun.home-assistant.io:80",
"stun:stun.home-assistant.io:3478",
"stun:stun.home-assistant.io:80",
]
},
],
@@ -238,8 +238,8 @@ async def test_ws_get_client_config(
"iceServers": [
{
"urls": [
"stun:stun.home-assistant.io:80",
"stun:stun.home-assistant.io:3478",
"stun:stun.home-assistant.io:80",
]
},
{

View File

@@ -11,6 +11,7 @@ import pytest
import voluptuous as vol
from homeassistant.components import conversation
from homeassistant.components.cloud.const import AI_TASK_ENTITY_UNIQUE_ID, DOMAIN
from homeassistant.components.cloud.entity import (
BaseCloudLLMEntity,
_convert_content_to_param,
@@ -18,7 +19,8 @@ from homeassistant.components.cloud.entity import (
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import llm, selector
from homeassistant.helpers import entity_registry as er, llm, selector
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry
@@ -219,3 +221,66 @@ async def test_prepare_chat_for_generation_passes_messages_through(
assert response["messages"] == messages
assert response["conversation_id"] == "conversation-id"
async def test_async_handle_chat_log_service_sets_structured_output_non_strict(
hass: HomeAssistant,
cloud: MagicMock,
entity_registry: er.EntityRegistry,
mock_cloud_login: None,
) -> None:
"""Ensure structured output requests always disable strict validation via service."""
assert await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
on_start_callback = cloud.register_on_start.call_args[0][0]
await on_start_callback()
await hass.async_block_till_done()
entity_id = entity_registry.async_get_entity_id(
"ai_task", DOMAIN, AI_TASK_ENTITY_UNIQUE_ID
)
assert entity_id is not None
async def _empty_stream():
return
async def _fake_delta_stream(
self: conversation.ChatLog,
agent_id: str,
stream,
):
content = conversation.AssistantContent(
agent_id=agent_id, content='{"value": "ok"}'
)
self.async_add_assistant_content_without_tools(content)
yield content
cloud.llm.async_generate_data = AsyncMock(return_value=_empty_stream())
with patch(
"homeassistant.components.conversation.chat_log.ChatLog.async_add_delta_content_stream",
_fake_delta_stream,
):
await hass.services.async_call(
"ai_task",
"generate_data",
{
"entity_id": entity_id,
"task_name": "Device Report",
"instructions": "Provide value.",
"structure": {
"value": {
"selector": {"text": None},
"required": True,
}
},
},
blocking=True,
return_response=True,
)
cloud.llm.async_generate_data.assert_awaited_once()
_, kwargs = cloud.llm.async_generate_data.call_args
assert kwargs["response_format"]["json_schema"]["strict"] is False

View File

@@ -36,6 +36,7 @@ async def test_diagnostic_entities(
"sensor.knx_interface_outgoing_telegrams",
"sensor.knx_interface_outgoing_telegram_errors",
"sensor.knx_interface_telegrams",
"sensor.knx_interface_undecodable_data_secure_telegrams",
):
entity = entity_registry.async_get(entity_id)
assert entity.entity_category is EntityCategory.DIAGNOSTIC
@@ -43,6 +44,7 @@ async def test_diagnostic_entities(
for entity_id in (
"sensor.knx_interface_incoming_telegrams",
"sensor.knx_interface_outgoing_telegrams",
"sensor.knx_interface_undecodable_data_secure_telegrams",
):
entity = entity_registry.async_get(entity_id)
assert entity.disabled is True
@@ -57,7 +59,7 @@ async def test_diagnostic_entities(
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert len(events) == 3 # 5 polled sensors - 2 disabled
assert len(events) == 3 # 6 polled sensors - 3 disabled
events.clear()
for entity_id, test_state in (
@@ -74,7 +76,7 @@ async def test_diagnostic_entities(
state=XknxConnectionState.DISCONNECTED
)
await hass.async_block_till_done()
assert len(events) == 4 # 3 not always_available + 3 force_update - 2 disabled
assert len(events) == 4
events.clear()
knx.xknx.current_address = IndividualAddress("1.1.1")

View File

@@ -4162,81 +4162,3 @@ async def test_get_services_for_target(
"switch.turn_on",
],
)
@patch("annotatedyaml.loader.load_yaml")
@patch.object(Integration, "has_services", return_value=True)
async def test_get_services_for_target_caching(
mock_has_services: Mock,
mock_load_yaml: Mock,
hass: HomeAssistant,
websocket_client: MockHAClientWebSocket,
) -> None:
"""Test that flattened service descriptions are cached and reused."""
def get_common_service_descriptions(domain: str):
return f"""
turn_on:
target:
entity:
domain: {domain}
"""
def _load_yaml(fname, secrets=None):
domain = fname.split("/")[-2]
with io.StringIO(get_common_service_descriptions(domain)) as file:
return parse_yaml(file)
mock_load_yaml.side_effect = _load_yaml
await hass.async_block_till_done()
hass.services.async_register("light", "turn_on", lambda call: None)
hass.services.async_register("switch", "turn_on", lambda call: None)
await hass.async_block_till_done()
async def call_command():
await websocket_client.send_json_auto_id(
{
"type": "get_services_for_target",
"target": {"entity_id": ["light.test1"]},
}
)
msg = await websocket_client.receive_json()
assert msg["success"]
with patch(
"homeassistant.components.websocket_api.automation._async_get_automation_components_for_target",
return_value=set(),
) as mock_get_components:
# First call: should create and cache flat descriptions
await call_command()
assert mock_get_components.call_count == 1
first_flat_descriptions = mock_get_components.call_args_list[0][0][3]
assert first_flat_descriptions == {
"light.turn_on": {
"fields": {},
"target": {"entity": [{"domain": ["light"]}]},
},
"switch.turn_on": {
"fields": {},
"target": {"entity": [{"domain": ["switch"]}]},
},
}
# Second call: should reuse cached flat descriptions
await call_command()
assert mock_get_components.call_count == 2
second_flat_descriptions = mock_get_components.call_args_list[1][0][3]
assert first_flat_descriptions is second_flat_descriptions
# Register a new service to invalidate cache
hass.services.async_register("new_domain", "new_service", lambda call: None)
await hass.async_block_till_done()
# Third call: cache should be rebuilt
await call_command()
assert mock_get_components.call_count == 3
third_flat_descriptions = mock_get_components.call_args_list[2][0][3]
assert "new_domain.new_service" in third_flat_descriptions
assert third_flat_descriptions is not first_flat_descriptions