Compare commits

..

17 Commits

Author SHA1 Message Date
Erik Montnemery
ccbd681d20 Merge branch 'dev' into adjust_sensor_group_behavior 2025-11-27 11:06:14 +01:00
Paulus Schoutsen
2ec5190243 Install requirements_test_all in dev (#157392) 2025-11-27 10:30:50 +01:00
Erik Montnemery
a706db8fdb Minor polish of cover trigger tests (#157397) 2025-11-27 09:57:03 +01:00
starkillerOG
a00923c48b Bump reolink-aio to 0.16.6 (#157399) 2025-11-27 09:53:25 +01:00
Sarah Seidman
7480d59f0f Normalize input for Droplet pairing code (#157361) 2025-11-27 08:36:30 +01:00
Erik Montnemery
4c8d9ed401 Adjust type hints in sensor group (#157373) 2025-11-27 08:34:16 +01:00
Lukas
eef10c59db Pooldose bump api 0.8.0 (new) (#157381) 2025-11-27 08:33:32 +01:00
dependabot[bot]
a1a1f8dd77 Bump docker/metadata-action from 5.5.1 to 5.9.0 (#157395) 2025-11-27 07:26:58 +01:00
dependabot[bot]
c75a5c5151 Bump docker/setup-buildx-action from 3.5.0 to 3.11.1 (#157396) 2025-11-27 07:25:16 +01:00
Allen Porter
cdaaa2bd8f Update fitbit to use new asyncio client library for device list (#157308)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-27 00:23:49 -05:00
Allen Porter
bd84dac8fb Update roborock test typing (#157370) 2025-11-27 00:21:48 -05:00
Allen Porter
42cbeca5b0 Remove old roborock map storage (#157379) 2025-11-27 00:21:04 -05:00
Allen Porter
ad0a498d10 Bump python-roborock to 3.8.1 (#157376) 2025-11-26 16:12:19 -08:00
Jan Bouwhuis
973405822b Move translatable URL out of strings.json for knx integration (#155244) 2025-11-26 23:09:59 +01:00
Franck Nijhof
b883d2f519 Bump version to 2026.1.0dev0 2025-11-26 17:15:29 +00:00
Erik Montnemery
01aa70e249 Merge branch 'dev' into adjust_sensor_group_behavior 2025-09-18 10:56:53 +02:00
Erik
9f6a0c0c77 Adjust sensor group behavior 2025-09-12 14:29:19 +02:00
475 changed files with 3483 additions and 9908 deletions

View File

@@ -393,7 +393,7 @@ jobs:
# 2025.12.0.dev202511250240 -> tags: 2025.12.0.dev202511250240, dev
- name: Generate Docker metadata
id: meta
uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0
with:
images: ${{ matrix.registry }}/home-assistant
sep-tags: ","
@@ -407,7 +407,7 @@ jobs:
type=semver,pattern={{major}}.{{minor}},value=${{ needs.init.outputs.version }},enable=${{ !contains(needs.init.outputs.version, 'd') && !contains(needs.init.outputs.version, 'b') }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@aa33708b10e362ff993539393ff100fa93ed6a27 # v3.7.1
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.7.1
- name: Copy architecture images to DockerHub
if: matrix.registry == 'docker.io/homeassistant'
@@ -418,19 +418,9 @@ jobs:
ARCHS=$(echo '${{ needs.init.outputs.architectures }}' | jq -r '.[]')
for arch in $ARCHS; do
echo "Copying ${arch} image to DockerHub..."
for attempt in 1 2 3; do
if docker buildx imagetools create \
--tag "docker.io/homeassistant/${arch}-homeassistant:${{ needs.init.outputs.version }}" \
"ghcr.io/home-assistant/${arch}-homeassistant:${{ needs.init.outputs.version }}"; then
break
fi
echo "Attempt ${attempt} failed, retrying in 10 seconds..."
sleep 10
if [ "${attempt}" -eq 3 ]; then
echo "Failed after 3 attempts"
exit 1
fi
done
docker buildx imagetools create \
--tag "docker.io/homeassistant/${arch}-homeassistant:${{ needs.init.outputs.version }}" \
"ghcr.io/home-assistant/${arch}-homeassistant:${{ needs.init.outputs.version }}"
cosign sign --yes "docker.io/homeassistant/${arch}-homeassistant:${{ needs.init.outputs.version }}"
done

View File

@@ -40,9 +40,9 @@ env:
CACHE_VERSION: 2
UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 1
HA_SHORT_VERSION: "2025.12"
DEFAULT_PYTHON: "3.13.9"
ALL_PYTHON_VERSIONS: "['3.13.9', '3.14.0']"
HA_SHORT_VERSION: "2026.1"
DEFAULT_PYTHON: "3.13"
ALL_PYTHON_VERSIONS: "['3.13', '3.14']"
# 10.3 is the oldest supported version
# - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022)
# 10.6 is the current long-term-support

4
CODEOWNERS generated
View File

@@ -1354,8 +1354,8 @@ build.json @home-assistant/supervisor
/tests/components/ring/ @sdb9696
/homeassistant/components/risco/ @OnFreund
/tests/components/risco/ @OnFreund
/homeassistant/components/rituals_perfume_genie/ @milanmeu @frenck @quebulm
/tests/components/rituals_perfume_genie/ @milanmeu @frenck @quebulm
/homeassistant/components/rituals_perfume_genie/ @milanmeu @frenck
/tests/components/rituals_perfume_genie/ @milanmeu @frenck
/homeassistant/components/rmvtransport/ @cgtobi
/tests/components/rmvtransport/ @cgtobi
/homeassistant/components/roborock/ @Lash-L @allenporter

View File

@@ -402,8 +402,6 @@ class AuthManager:
if user.is_owner:
raise ValueError("Unable to deactivate the owner")
await self._store.async_deactivate_user(user)
for refresh_token in list(user.refresh_tokens.values()):
self.async_remove_refresh_token(refresh_token)
async def async_remove_credentials(self, credentials: models.Credentials) -> None:
"""Remove credentials."""

View File

@@ -7,7 +7,6 @@ from typing import Any, Final
from homeassistant.const import (
EVENT_COMPONENT_LOADED,
EVENT_CORE_CONFIG_UPDATE,
EVENT_LABS_UPDATED,
EVENT_LOVELACE_UPDATED,
EVENT_PANELS_UPDATED,
EVENT_RECORDER_5MIN_STATISTICS_GENERATED,
@@ -46,7 +45,6 @@ SUBSCRIBE_ALLOWLIST: Final[set[EventType[Any] | str]] = {
EVENT_STATE_CHANGED,
EVENT_THEMES_UPDATED,
EVENT_LABEL_REGISTRY_UPDATED,
EVENT_LABS_UPDATED,
EVENT_CATEGORY_REGISTRY_UPDATED,
EVENT_FLOOR_REGISTRY_UPDATED,
}

View File

@@ -159,74 +159,81 @@
"title": "Alarm control panel",
"triggers": {
"armed": {
"description": "Triggers after one or more alarms become armed, regardless of the mode.",
"description": "Triggers when an alarm is armed.",
"description_configured": "[%key:component::alarm_control_panel::triggers::armed::description%]",
"fields": {
"behavior": {
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
}
},
"name": "Alarm armed"
"name": "When an alarm is armed"
},
"armed_away": {
"description": "Triggers after one or more alarms become armed in away mode.",
"description": "Triggers when an alarm is armed away.",
"description_configured": "[%key:component::alarm_control_panel::triggers::armed_away::description%]",
"fields": {
"behavior": {
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
}
},
"name": "Alarm armed away"
"name": "When an alarm is armed away"
},
"armed_home": {
"description": "Triggers after one or more alarms become armed in home mode.",
"description": "Triggers when an alarm is armed home.",
"description_configured": "[%key:component::alarm_control_panel::triggers::armed_home::description%]",
"fields": {
"behavior": {
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
}
},
"name": "Alarm armed home"
"name": "When an alarm is armed home"
},
"armed_night": {
"description": "Triggers after one or more alarms become armed in night mode.",
"description": "Triggers when an alarm is armed night.",
"description_configured": "[%key:component::alarm_control_panel::triggers::armed_night::description%]",
"fields": {
"behavior": {
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
}
},
"name": "Alarm armed night"
"name": "When an alarm is armed night"
},
"armed_vacation": {
"description": "Triggers after one or more alarms become armed in vacation mode.",
"description": "Triggers when an alarm is armed vacation.",
"description_configured": "[%key:component::alarm_control_panel::triggers::armed_vacation::description%]",
"fields": {
"behavior": {
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
}
},
"name": "Alarm armed vacation"
"name": "When an alarm is armed vacation"
},
"disarmed": {
"description": "Triggers after one or more alarms become disarmed.",
"description": "Triggers when an alarm is disarmed.",
"description_configured": "[%key:component::alarm_control_panel::triggers::disarmed::description%]",
"fields": {
"behavior": {
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
}
},
"name": "Alarm disarmed"
"name": "When an alarm is disarmed"
},
"triggered": {
"description": "Triggers after one or more alarms become triggered.",
"description": "Triggers when an alarm is triggered.",
"description_configured": "[%key:component::alarm_control_panel::triggers::triggered::description%]",
"fields": {
"behavior": {
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
}
},
"name": "Alarm triggered"
"name": "When an alarm is triggered"
}
}
}

View File

@@ -2,7 +2,6 @@
from __future__ import annotations
from aiohttp import CookieJar
from pyanglianwater import AnglianWater
from pyanglianwater.auth import MSOB2CAuth
from pyanglianwater.exceptions import (
@@ -19,7 +18,7 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryError
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import CONF_ACCOUNT_NUMBER, DOMAIN
from .coordinator import AnglianWaterConfigEntry, AnglianWaterUpdateCoordinator
@@ -34,10 +33,7 @@ async def async_setup_entry(
auth = MSOB2CAuth(
username=entry.data[CONF_USERNAME],
password=entry.data[CONF_PASSWORD],
session=async_create_clientsession(
hass,
cookie_jar=CookieJar(quote_cookie=False),
),
session=async_get_clientsession(hass),
refresh_token=entry.data[CONF_ACCESS_TOKEN],
account_number=entry.data[CONF_ACCOUNT_NUMBER],
)

View File

@@ -30,7 +30,6 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
vol.Required(CONF_PASSWORD): selector.TextSelector(
selector.TextSelectorConfig(type=selector.TextSelectorType.PASSWORD)
),
vol.Required(CONF_ACCOUNT_NUMBER): selector.TextSelector(),
}
)
@@ -69,19 +68,34 @@ class AnglianWaterConfigFlow(ConfigFlow, domain=DOMAIN):
self.hass,
cookie_jar=CookieJar(quote_cookie=False),
),
account_number=user_input[CONF_ACCOUNT_NUMBER],
account_number=user_input.get(CONF_ACCOUNT_NUMBER),
)
)
if isinstance(validation_response, BaseAuth):
await self.async_set_unique_id(user_input[CONF_ACCOUNT_NUMBER])
account_number = (
user_input.get(CONF_ACCOUNT_NUMBER)
or validation_response.account_number
)
await self.async_set_unique_id(account_number)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=user_input[CONF_ACCOUNT_NUMBER],
title=account_number,
data={
**user_input,
CONF_ACCESS_TOKEN: validation_response.refresh_token,
CONF_ACCOUNT_NUMBER: account_number,
},
)
if validation_response == "smart_meter_unavailable":
return self.async_show_form(
step_id="user",
data_schema=STEP_USER_DATA_SCHEMA.extend(
{
vol.Required(CONF_ACCOUNT_NUMBER): selector.TextSelector(),
}
),
errors={"base": validation_response},
)
errors["base"] = validation_response
return self.async_show_form(

View File

@@ -18,21 +18,17 @@ _LOGGER = logging.getLogger(__name__)
class AnglianWaterEntity(CoordinatorEntity[AnglianWaterUpdateCoordinator]):
"""Defines a Anglian Water entity."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: AnglianWaterUpdateCoordinator,
smart_meter: SmartMeter,
key: str,
) -> None:
"""Initialize Anglian Water entity."""
super().__init__(coordinator)
self.smart_meter = smart_meter
self._attr_unique_id = f"{smart_meter.serial_number}_{key}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, smart_meter.serial_number)},
name=smart_meter.serial_number,
name="Smart Water Meter",
manufacturer="Anglian Water",
serial_number=smart_meter.serial_number,
)

View File

@@ -5,7 +5,6 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/anglian_water",
"iot_class": "cloud_polling",
"loggers": ["pyanglianwater"],
"quality_scale": "bronze",
"requirements": ["pyanglianwater==2.1.0"]
}

View File

@@ -108,8 +108,9 @@ class AnglianWaterSensorEntity(AnglianWaterEntity, SensorEntity):
description: AnglianWaterSensorEntityDescription,
) -> None:
"""Initialize Anglian Water sensor."""
super().__init__(coordinator, smart_meter, description.key)
super().__init__(coordinator, smart_meter)
self.entity_description = description
self._attr_unique_id = f"{smart_meter.serial_number}_{description.key}"
@property
def native_value(self) -> float | None:

View File

@@ -19,7 +19,7 @@
"data_description": {
"account_number": "Your account number found on your latest bill.",
"password": "Your password",
"username": "Username or email used to log in to the Anglian Water website."
"username": "Username or email used to login to the Anglian Water website."
},
"description": "Enter your Anglian Water account credentials to connect to Home Assistant."
}

View File

@@ -17,7 +17,7 @@ from homeassistant.helpers import (
)
from homeassistant.helpers.typing import ConfigType
from .const import DEFAULT_CONVERSATION_NAME, DOMAIN, LOGGER
from .const import CONF_CHAT_MODEL, DEFAULT, DEFAULT_CONVERSATION_NAME, DOMAIN, LOGGER
PLATFORMS = (Platform.AI_TASK, Platform.CONVERSATION)
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
@@ -37,7 +37,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: AnthropicConfigEntry) ->
partial(anthropic.AsyncAnthropic, api_key=entry.data[CONF_API_KEY])
)
try:
await client.models.list(timeout=10.0)
# Use model from first conversation subentry for validation
subentries = list(entry.subentries.values())
if subentries:
model_id = subentries[0].data.get(CONF_CHAT_MODEL, DEFAULT[CONF_CHAT_MODEL])
else:
model_id = DEFAULT[CONF_CHAT_MODEL]
model = await client.models.retrieve(model_id=model_id, timeout=10.0)
LOGGER.debug("Anthropic model: %s", model.display_name)
except anthropic.AuthenticationError as err:
LOGGER.error("Invalid API key: %s", err)
return False

View File

@@ -5,7 +5,6 @@
"config_flow": true,
"dependencies": ["zeroconf"],
"documentation": "https://www.home-assistant.io/integrations/apple_tv",
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["pyatv", "srptools"],
"requirements": ["pyatv==0.16.1;python_version<'3.14'"],

View File

@@ -1123,6 +1123,63 @@ class PipelineRun:
)
try:
user_input = conversation.ConversationInput(
text=intent_input,
context=self.context,
conversation_id=conversation_id,
device_id=self._device_id,
satellite_id=self._satellite_id,
language=input_language,
agent_id=self.intent_agent.id,
extra_system_prompt=conversation_extra_system_prompt,
)
agent_id = self.intent_agent.id
processed_locally = agent_id == conversation.HOME_ASSISTANT_AGENT
all_targets_in_satellite_area = False
intent_response: intent.IntentResponse | None = None
if not processed_locally and not self._intent_agent_only:
# Sentence triggers override conversation agent
if (
trigger_response_text
:= await conversation.async_handle_sentence_triggers(
self.hass, user_input
)
) is not None:
# Sentence trigger matched
agent_id = "sentence_trigger"
processed_locally = True
intent_response = intent.IntentResponse(
self.pipeline.conversation_language
)
intent_response.async_set_speech(trigger_response_text)
intent_filter: Callable[[RecognizeResult], bool] | None = None
# If the LLM has API access, we filter out some sentences that are
# interfering with LLM operation.
if (
intent_agent_state := self.hass.states.get(self.intent_agent.id)
) and intent_agent_state.attributes.get(
ATTR_SUPPORTED_FEATURES, 0
) & conversation.ConversationEntityFeature.CONTROL:
intent_filter = _async_local_fallback_intent_filter
# Try local intents
if (
intent_response is None
and self.pipeline.prefer_local_intents
and (
intent_response := await conversation.async_handle_intents(
self.hass,
user_input,
intent_filter=intent_filter,
)
)
):
# Local intent matched
agent_id = conversation.HOME_ASSISTANT_AGENT
processed_locally = True
if self.tts_stream and self.tts_stream.supports_streaming_input:
tts_input_stream: asyncio.Queue[str | None] | None = asyncio.Queue()
else:
@@ -1208,17 +1265,6 @@ class PipelineRun:
assert self.tts_stream is not None
self.tts_stream.async_set_message_stream(tts_input_stream_generator())
user_input = conversation.ConversationInput(
text=intent_input,
context=self.context,
conversation_id=conversation_id,
device_id=self._device_id,
satellite_id=self._satellite_id,
language=input_language,
agent_id=self.intent_agent.id,
extra_system_prompt=conversation_extra_system_prompt,
)
with (
chat_session.async_get_chat_session(
self.hass, user_input.conversation_id
@@ -1230,53 +1276,6 @@ class PipelineRun:
chat_log_delta_listener=chat_log_delta_listener,
) as chat_log,
):
agent_id = self.intent_agent.id
processed_locally = agent_id == conversation.HOME_ASSISTANT_AGENT
all_targets_in_satellite_area = False
intent_response: intent.IntentResponse | None = None
if not processed_locally and not self._intent_agent_only:
# Sentence triggers override conversation agent
if (
trigger_response_text
:= await conversation.async_handle_sentence_triggers(
self.hass, user_input, chat_log
)
) is not None:
# Sentence trigger matched
agent_id = "sentence_trigger"
processed_locally = True
intent_response = intent.IntentResponse(
self.pipeline.conversation_language
)
intent_response.async_set_speech(trigger_response_text)
intent_filter: Callable[[RecognizeResult], bool] | None = None
# If the LLM has API access, we filter out some sentences that are
# interfering with LLM operation.
if (
intent_agent_state := self.hass.states.get(self.intent_agent.id)
) and intent_agent_state.attributes.get(
ATTR_SUPPORTED_FEATURES, 0
) & conversation.ConversationEntityFeature.CONTROL:
intent_filter = _async_local_fallback_intent_filter
# Try local intents
if (
intent_response is None
and self.pipeline.prefer_local_intents
and (
intent_response := await conversation.async_handle_intents(
self.hass,
user_input,
chat_log,
intent_filter=intent_filter,
)
)
):
# Local intent matched
agent_id = conversation.HOME_ASSISTANT_AGENT
processed_locally = True
# It was already handled, create response and add to chat history
if intent_response is not None:
speech: str = intent_response.speech.get("plain", {}).get(

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/assist_satellite",
"integration_type": "entity",
"quality_scale": "internal",
"requirements": ["hassil==3.5.0"]
"requirements": ["hassil==3.4.0"]
}

View File

@@ -112,44 +112,48 @@
"title": "Assist satellite",
"triggers": {
"idle": {
"description": "Triggers after one or more voice assistant satellites become idle after having processed a command.",
"description": "Triggers when an Assist satellite becomes idle.",
"description_configured": "[%key:component::assist_satellite::triggers::idle::description%]",
"fields": {
"behavior": {
"description": "[%key:component::assist_satellite::common::trigger_behavior_description%]",
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
}
},
"name": "Satellite became idle"
"name": "When an Assist satellite becomes idle"
},
"listening": {
"description": "Triggers after one or more voice assistant satellites start listening for a command from someone.",
"description": "Triggers when an Assist satellite starts listening.",
"description_configured": "[%key:component::assist_satellite::triggers::listening::description%]",
"fields": {
"behavior": {
"description": "[%key:component::assist_satellite::common::trigger_behavior_description%]",
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
}
},
"name": "Satellite started listening"
"name": "When an Assist satellite starts listening"
},
"processing": {
"description": "Triggers after one or more voice assistant satellites start processing a command after having heard it.",
"description": "Triggers when an Assist satellite is processing.",
"description_configured": "[%key:component::assist_satellite::triggers::processing::description%]",
"fields": {
"behavior": {
"description": "[%key:component::assist_satellite::common::trigger_behavior_description%]",
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
}
},
"name": "Satellite started processing"
"name": "When an Assist satellite is processing"
},
"responding": {
"description": "Triggers after one or more voice assistant satellites start responding to a command after having processed it, or start announcing something.",
"description": "Triggers when an Assist satellite is responding.",
"description_configured": "[%key:component::assist_satellite::triggers::responding::description%]",
"fields": {
"behavior": {
"description": "[%key:component::assist_satellite::common::trigger_behavior_description%]",
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
}
},
"name": "Satellite started responding"
"name": "When an Assist satellite is responding"
}
}
}

View File

@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["aioasuswrt", "asusrouter", "asyncssh"],
"requirements": ["aioasuswrt==1.5.4", "asusrouter==1.21.3"]
"requirements": ["aioasuswrt==1.5.1", "asusrouter==1.21.0"]
}

View File

@@ -124,7 +124,6 @@ _EXPERIMENTAL_CONDITION_PLATFORMS = {
_EXPERIMENTAL_TRIGGER_PLATFORMS = {
"alarm_control_panel",
"assist_satellite",
"binary_sensor",
"climate",
"cover",
"fan",

View File

@@ -8,8 +8,6 @@
"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

@@ -69,10 +69,10 @@
},
"preview_features": {
"new_triggers_conditions": {
"description": "Enables new purpose-specific triggers and conditions that are more user-friendly than technical state-based options.\n\nThese new automation features support targets across your entire home, letting you trigger automations for any entity, device, area, floor, or label (for example, when any light in your living room turned on). Integrations can now also provide their own purpose-specific triggers and conditions, just like actions.\n\nThis preview also includes a new tree view to help you navigate your home when adding triggers, conditions, and actions.",
"disable_confirmation": "Disabling this preview will cause automations and scripts that use the new purpose-specific triggers and conditions to fail.\n\nBefore disabling, ensure that your automations or scripts do not rely on this feature.",
"enable_confirmation": "This feature is still in development and may change. These new purpose-specific triggers and conditions are being refined based on user feedback and are not yet complete.\n\nBy enabling this preview, you'll have early access to these new capabilities, but be aware that they may be modified or updated in future releases.",
"name": "Purpose-specific triggers and conditions"
"description": "Enables new intuitive triggers and conditions that are more user-friendly than technical state-based options.\n\nThese new automation features support targets across your entire home, letting you trigger automations for any entity, device, area, floor, or label (for example, when any light in your living room turned on). Integrations can now also provide their own intuitive triggers and conditions, just like actions.\n\nThis preview also includes a new tree view to help you navigate your home when adding triggers, conditions, and actions.",
"disable_confirmation": "Disabling this preview will cause automations and scripts that use the new intuitive triggers and conditions to fail.\n\nBefore disabling, ensure that your automations or scripts do not rely on this feature.",
"enable_confirmation": "This feature is still in development and may change. These new intuitive triggers and conditions are being refined based on user feedback and are not yet complete.\n\nBy enabling this preview, you'll have early access to these new capabilities, but be aware that they may be modified or updated in future releases.",
"name": "Intuitive triggers and conditions"
}
},
"services": {

View File

@@ -174,13 +174,5 @@
"on": "mdi:window-open"
}
}
},
"triggers": {
"occupancy_cleared": {
"trigger": "mdi:home-outline"
},
"occupancy_detected": {
"trigger": "mdi:home"
}
}
}

View File

@@ -1,8 +1,4 @@
{
"common": {
"trigger_behavior_description_occupancy": "The behavior of the targeted occupancy sensors to trigger on.",
"trigger_behavior_name": "Behavior"
},
"device_automation": {
"condition_type": {
"is_bat_low": "{entity_name} battery is low",
@@ -321,36 +317,5 @@
}
}
},
"selector": {
"trigger_behavior": {
"options": {
"any": "Any",
"first": "First",
"last": "Last"
}
}
},
"title": "Binary sensor",
"triggers": {
"occupancy_cleared": {
"description": "Triggers after one or more occupancy sensors stop detecting occupancy.",
"fields": {
"behavior": {
"description": "[%key:component::binary_sensor::common::trigger_behavior_description_occupancy%]",
"name": "[%key:component::binary_sensor::common::trigger_behavior_name%]"
}
},
"name": "Occupancy cleared"
},
"occupancy_detected": {
"description": "Triggers after one or more occupancy sensors start detecting occupancy.",
"fields": {
"behavior": {
"description": "[%key:component::binary_sensor::common::trigger_behavior_description_occupancy%]",
"name": "[%key:component::binary_sensor::common::trigger_behavior_name%]"
}
},
"name": "Occupancy detected"
}
}
"title": "Binary sensor"
}

View File

@@ -1,67 +0,0 @@
"""Provides triggers for binary sensors."""
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity import get_device_class
from homeassistant.helpers.trigger import EntityStateTriggerBase, Trigger
from homeassistant.helpers.typing import UNDEFINED, UndefinedType
from . import DOMAIN, BinarySensorDeviceClass
def get_device_class_or_undefined(
hass: HomeAssistant, entity_id: str
) -> str | None | UndefinedType:
"""Get the device class of an entity or UNDEFINED if not found."""
try:
return get_device_class(hass, entity_id)
except HomeAssistantError:
return UNDEFINED
class BinarySensorOnOffTrigger(EntityStateTriggerBase):
"""Class for binary sensor on/off triggers."""
_device_class: BinarySensorDeviceClass | None
_domain: str = DOMAIN
def entity_filter(self, entities: set[str]) -> set[str]:
"""Filter entities of this domain."""
entities = super().entity_filter(entities)
return {
entity_id
for entity_id in entities
if get_device_class_or_undefined(self._hass, entity_id)
== self._device_class
}
def make_binary_sensor_trigger(
device_class: BinarySensorDeviceClass | None,
to_state: str,
) -> type[BinarySensorOnOffTrigger]:
"""Create an entity state trigger class."""
class CustomTrigger(BinarySensorOnOffTrigger):
"""Trigger for entity state changes."""
_device_class = device_class
_to_state = to_state
return CustomTrigger
TRIGGERS: dict[str, type[Trigger]] = {
"occupancy_detected": make_binary_sensor_trigger(
BinarySensorDeviceClass.OCCUPANCY, STATE_ON
),
"occupancy_cleared": make_binary_sensor_trigger(
BinarySensorDeviceClass.OCCUPANCY, STATE_OFF
),
}
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
"""Return the triggers for binary sensors."""
return TRIGGERS

View File

@@ -1,25 +0,0 @@
.trigger_common_fields: &trigger_common_fields
behavior:
required: true
default: any
selector:
select:
translation_key: trigger_behavior
options:
- first
- last
- any
occupancy_cleared:
fields: *trigger_common_fields
target:
entity:
domain: binary_sensor
device_class: occupancy
occupancy_detected:
fields: *trigger_common_fields
target:
entity:
domain: binary_sensor
device_class: occupancy

View File

@@ -64,12 +64,6 @@ async def async_migrate_entry(hass: HomeAssistant, entry: BlinkConfigEntry) -> b
if entry.version == 2:
await _reauth_flow_wrapper(hass, entry, data)
return False
if entry.version == 3:
# Migrate device_id to hardware_id for blinkpy 0.25.x OAuth2 compatibility
if "device_id" in data:
data["hardware_id"] = data.pop("device_id")
hass.config_entries.async_update_entry(entry, data=data, version=4)
return True
return True

View File

@@ -21,7 +21,7 @@ from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN, HARDWARE_ID
from .const import DEVICE_ID, DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -43,7 +43,7 @@ async def _send_blink_2fa_pin(blink: Blink, pin: str | None) -> bool:
class BlinkConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a Blink config flow."""
VERSION = 4
VERSION = 3
def __init__(self) -> None:
"""Initialize the blink flow."""
@@ -53,7 +53,7 @@ class BlinkConfigFlow(ConfigFlow, domain=DOMAIN):
async def _handle_user_input(self, user_input: dict[str, Any]):
"""Handle user input."""
self.auth = Auth(
{**user_input, "hardware_id": HARDWARE_ID},
{**user_input, "device_id": DEVICE_ID},
no_prompt=True,
session=async_get_clientsession(self.hass),
)

View File

@@ -3,7 +3,7 @@
from homeassistant.const import Platform
DOMAIN = "blink"
HARDWARE_ID = "Home Assistant"
DEVICE_ID = "Home Assistant"
CONF_MIGRATE = "migrate"
CONF_CAMERA = "camera"

View File

@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/blink",
"iot_class": "cloud_polling",
"loggers": ["blinkpy"],
"requirements": ["blinkpy==0.25.2"]
"requirements": ["blinkpy==0.24.1"]
}

View File

@@ -68,9 +68,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: BoschAlarmConfigEntry) -
config_entry_id=entry.entry_id,
connections={(CONNECTION_NETWORK_MAC, mac)} if mac else set(),
identifiers={(DOMAIN, entry.unique_id or entry.entry_id)},
name=f"Bosch {panel.model.name}",
name=f"Bosch {panel.model}",
manufacturer="Bosch Security Systems",
model=panel.model.name,
model=panel.model,
sw_version=panel.firmware_version,
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

View File

@@ -83,7 +83,7 @@ async def try_connect(
finally:
await panel.disconnect()
return (panel.model.name, panel.serial_number)
return (panel.model, panel.serial_number)
class BoschAlarmConfigFlow(ConfigFlow, domain=DOMAIN):

View File

@@ -20,8 +20,7 @@ async def async_get_config_entry_diagnostics(
return {
"entry_data": async_redact_data(entry.data, TO_REDACT),
"data": {
"model": entry.runtime_data.model.name,
"family": entry.runtime_data.model.family.name,
"model": entry.runtime_data.model,
"serial_number": entry.runtime_data.serial_number,
"protocol_version": entry.runtime_data.protocol_version,
"firmware_version": entry.runtime_data.firmware_version,

View File

@@ -26,7 +26,7 @@ class BoschAlarmEntity(Entity):
self._attr_should_poll = False
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, unique_id)},
name=f"Bosch {panel.model.name}",
name=f"Bosch {panel.model}",
manufacturer="Bosch Security Systems",
)

View File

@@ -12,5 +12,5 @@
"integration_type": "device",
"iot_class": "local_push",
"quality_scale": "platinum",
"requirements": ["bosch-alarm-mode2==0.4.10"]
"requirements": ["bosch-alarm-mode2==0.4.6"]
}

View File

@@ -36,7 +36,6 @@
}
],
"documentation": "https://www.home-assistant.io/integrations/broadlink",
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["broadlink"],
"requirements": ["broadlink==0.19.0"]

View File

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

View File

@@ -98,12 +98,6 @@
}
},
"triggers": {
"started_cooling": {
"trigger": "mdi:snowflake"
},
"started_drying": {
"trigger": "mdi:water-percent"
},
"started_heating": {
"trigger": "mdi:fire"
},

View File

@@ -298,55 +298,38 @@
},
"title": "Climate",
"triggers": {
"started_cooling": {
"description": "Triggers after one or more climate-control devices start cooling.",
"fields": {
"behavior": {
"description": "[%key:component::climate::common::trigger_behavior_description%]",
"name": "[%key:component::climate::common::trigger_behavior_name%]"
}
},
"name": "Climate-control device started cooling"
},
"started_drying": {
"description": "Triggers after one or more climate-control devices start drying.",
"fields": {
"behavior": {
"description": "[%key:component::climate::common::trigger_behavior_description%]",
"name": "[%key:component::climate::common::trigger_behavior_name%]"
}
},
"name": "Climate-control device started drying"
},
"started_heating": {
"description": "Triggers after one or more climate-control devices start heating.",
"description": "Triggers when a climate starts to heat.",
"description_configured": "[%key:component::climate::triggers::started_heating::description%]",
"fields": {
"behavior": {
"description": "[%key:component::climate::common::trigger_behavior_description%]",
"name": "[%key:component::climate::common::trigger_behavior_name%]"
}
},
"name": "Climate-control device started heating"
"name": "When a climate starts to heat"
},
"turned_off": {
"description": "Triggers after one or more climate-control devices turn off.",
"description": "Triggers when a climate is turned off.",
"description_configured": "[%key:component::climate::triggers::turned_off::description%]",
"fields": {
"behavior": {
"description": "[%key:component::climate::common::trigger_behavior_description%]",
"name": "[%key:component::climate::common::trigger_behavior_name%]"
}
},
"name": "Climate-control device turned off"
"name": "When a climate is turned off"
},
"turned_on": {
"description": "Triggers after one or more climate-control devices turn on, regardless of the mode.",
"description": "Triggers when a climate is turned on.",
"description_configured": "[%key:component::climate::triggers::turned_on::description%]",
"fields": {
"behavior": {
"description": "[%key:component::climate::common::trigger_behavior_description%]",
"name": "[%key:component::climate::common::trigger_behavior_name%]"
}
},
"name": "Climate-control device turned on"
"name": "When a climate is turned on"
}
}
}

View File

@@ -11,12 +11,6 @@ from homeassistant.helpers.trigger import (
from .const import ATTR_HVAC_ACTION, DOMAIN, HVACAction, HVACMode
TRIGGERS: dict[str, type[Trigger]] = {
"started_cooling": make_entity_state_attribute_trigger(
DOMAIN, ATTR_HVAC_ACTION, HVACAction.COOLING
),
"started_drying": make_entity_state_attribute_trigger(
DOMAIN, ATTR_HVAC_ACTION, HVACAction.DRYING
),
"turned_off": make_entity_state_trigger(DOMAIN, HVACMode.OFF),
"turned_on": make_conditional_entity_state_trigger(
DOMAIN,

View File

@@ -14,8 +14,6 @@
- last
- any
started_cooling: *trigger_common
started_drying: *trigger_common
started_heating: *trigger_common
turned_off: *trigger_common
turned_on: *trigger_common

View File

@@ -4,13 +4,12 @@ from __future__ import annotations
import asyncio
from collections.abc import Awaitable, Callable
from contextlib import suppress
from datetime import datetime, timedelta
from enum import Enum
import logging
from typing import Any, cast
from hass_nabucasa import Cloud, NabuCasaBaseError
from hass_nabucasa import Cloud
import voluptuous as vol
from homeassistant.components import alexa, google_assistant
@@ -79,16 +78,13 @@ from .subscription import async_subscription_info
DEFAULT_MODE = MODE_PROD
PLATFORMS = [
Platform.AI_TASK,
Platform.BINARY_SENSOR,
Platform.CONVERSATION,
Platform.STT,
Platform.TTS,
]
LLM_PLATFORMS = [
Platform.AI_TASK,
Platform.CONVERSATION,
]
SERVICE_REMOTE_CONNECT = "remote_connect"
SERVICE_REMOTE_DISCONNECT = "remote_disconnect"
@@ -435,14 +431,7 @@ def _handle_prefs_updated(hass: HomeAssistant, cloud: Cloud[CloudClient]) -> Non
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a config entry."""
platforms = PLATFORMS.copy()
if (cloud := hass.data[DATA_CLOUD]).is_logged_in:
with suppress(NabuCasaBaseError):
await cloud.llm.async_ensure_token()
platforms += LLM_PLATFORMS
await hass.config_entries.async_forward_entry_setups(entry, platforms)
entry.runtime_data = {"platforms": platforms}
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
stt_tts_entities_added = hass.data[DATA_PLATFORMS_SETUP]["stt_tts_entities_added"]
stt_tts_entities_added.set()
@@ -451,9 +440,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(
entry, entry.runtime_data["platforms"]
)
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
@callback

View File

@@ -19,7 +19,7 @@ from PIL import Image
from homeassistant.components import ai_task, conversation
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util.json import json_loads
@@ -94,10 +94,15 @@ async def async_setup_entry(
) -> None:
"""Set up Home Assistant Cloud AI Task entity."""
cloud = hass.data[DATA_CLOUD]
async_add_entities([CloudAITaskEntity(cloud, config_entry)])
try:
await cloud.llm.async_ensure_token()
except LLMError:
return
async_add_entities([CloudLLMTaskEntity(cloud, config_entry)])
class CloudAITaskEntity(BaseCloudLLMEntity, ai_task.AITaskEntity):
class CloudLLMTaskEntity(ai_task.AITaskEntity, BaseCloudLLMEntity):
"""Home Assistant Cloud AI Task entity."""
_attr_has_entity_name = True
@@ -174,7 +179,7 @@ class CloudAITaskEntity(BaseCloudLLMEntity, ai_task.AITaskEntity):
attachments=attachments,
)
except LLMAuthenticationError as err:
raise HomeAssistantError("Cloud LLM authentication failed") from err
raise ConfigEntryAuthFailed("Cloud LLM authentication failed") from err
except LLMRateLimitError as err:
raise HomeAssistantError("Cloud LLM is rate limited") from err
except LLMResponseError as err:

View File

@@ -71,7 +71,6 @@ class CloudClient(Interface):
self._google_config_init_lock = asyncio.Lock()
self._relayer_region: str | None = None
self._cloud_ice_servers_listener: Callable[[], None] | None = None
self._ice_servers: list[RTCIceServer] = []
@property
def base_path(self) -> Path:
@@ -118,11 +117,6 @@ class CloudClient(Interface):
"""Return the connected relayer region."""
return self._relayer_region
@property
def ice_servers(self) -> list[RTCIceServer]:
"""Return the current ICE servers."""
return self._ice_servers
async def get_alexa_config(self) -> alexa_config.CloudAlexaConfig:
"""Return Alexa config."""
if self._alexa_config is None:
@@ -209,8 +203,11 @@ class CloudClient(Interface):
ice_servers: list[RTCIceServer],
) -> Callable[[], None]:
"""Register cloud ice server."""
self._ice_servers = ice_servers
return async_register_ice_servers(self._hass, lambda: self._ice_servers)
def get_ice_servers() -> list[RTCIceServer]:
return ice_servers
return async_register_ice_servers(self._hass, get_ice_servers)
async def async_register_cloud_ice_servers_listener(
prefs: CloudPreferences,
@@ -271,7 +268,6 @@ class CloudClient(Interface):
async def logout_cleanups(self) -> None:
"""Cleanup some stuff after logout."""
self._ice_servers = []
await self.prefs.async_set_username(None)
if self._alexa_config:

View File

@@ -4,6 +4,8 @@ from __future__ import annotations
from typing import Literal
from hass_nabucasa.llm import LLMError
from homeassistant.components import conversation
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import MATCH_ALL
@@ -22,12 +24,17 @@ async def async_setup_entry(
) -> None:
"""Set up the Home Assistant Cloud conversation entity."""
cloud = hass.data[DATA_CLOUD]
try:
await cloud.llm.async_ensure_token()
except LLMError:
return
async_add_entities([CloudConversationEntity(cloud, config_entry)])
class CloudConversationEntity(
BaseCloudLLMEntity,
conversation.ConversationEntity,
BaseCloudLLMEntity,
):
"""Home Assistant Cloud conversation agent."""

View File

@@ -8,9 +8,10 @@ import logging
import re
from typing import Any, Literal, cast
from hass_nabucasa import Cloud, NabuCasaBaseError
from hass_nabucasa import Cloud
from hass_nabucasa.llm import (
LLMAuthenticationError,
LLMError,
LLMRateLimitError,
LLMResponseError,
LLMServiceError,
@@ -36,7 +37,7 @@ from voluptuous_openapi import convert
from homeassistant.components import conversation
from homeassistant.config_entries import ConfigEntry
from homeassistant.exceptions import HomeAssistantError
from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError
from homeassistant.helpers import llm
from homeassistant.helpers.entity import Entity
from homeassistant.util import slugify
@@ -561,7 +562,7 @@ class BaseCloudLLMEntity(Entity):
"schema": _format_structured_output(
structure, chat_log.llm_api
),
"strict": False,
"strict": True,
},
}
@@ -600,14 +601,14 @@ class BaseCloudLLMEntity(Entity):
)
except LLMAuthenticationError as err:
raise HomeAssistantError("Cloud LLM authentication failed") from err
raise ConfigEntryAuthFailed("Cloud LLM authentication failed") from err
except LLMRateLimitError as err:
raise HomeAssistantError("Cloud LLM is rate limited") from err
except LLMResponseError as err:
raise HomeAssistantError(str(err)) from err
except LLMServiceError as err:
raise HomeAssistantError("Error talking to Cloud LLM") from err
except NabuCasaBaseError as err:
except LLMError as err:
raise HomeAssistantError(str(err)) from err
if not chat_log.unresponded_tool_results:

View File

@@ -99,7 +99,6 @@ def async_setup(hass: HomeAssistant) -> None:
websocket_api.async_register_command(hass, websocket_hook_delete)
websocket_api.async_register_command(hass, websocket_remote_connect)
websocket_api.async_register_command(hass, websocket_remote_disconnect)
websocket_api.async_register_command(hass, websocket_webrtc_ice_servers)
websocket_api.async_register_command(hass, google_assistant_get)
websocket_api.async_register_command(hass, google_assistant_list)
@@ -1108,7 +1107,6 @@ async def alexa_sync(
@websocket_api.websocket_command({"type": "cloud/tts/info"})
@callback
def tts_info(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
@@ -1136,22 +1134,3 @@ def tts_info(
)
connection.send_result(msg["id"], {"languages": result})
@websocket_api.websocket_command(
{
vol.Required("type"): "cloud/webrtc/ice_servers",
}
)
@_require_cloud_login
@callback
def websocket_webrtc_ice_servers(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
msg: dict[str, Any],
) -> None:
"""Handle get WebRTC ICE servers websocket command."""
connection.send_result(
msg["id"],
[server.to_dict() for server in hass.data[DATA_CLOUD].client.ice_servers],
)

View File

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

View File

@@ -65,10 +65,8 @@ def websocket_create_area(
data.pop("id")
if "aliases" in data:
# Create a set for the aliases without:
# - Empty strings
# - Trailing and leading whitespace characters in the individual aliases
data["aliases"] = {s_strip for s in data["aliases"] if (s_strip := s.strip())}
# Convert aliases to a set
data["aliases"] = set(data["aliases"])
if "labels" in data:
# Convert labels to a set
@@ -135,10 +133,8 @@ def websocket_update_area(
data.pop("id")
if "aliases" in data:
# Create a set for the aliases without:
# - Empty strings
# - Trailing and leading whitespace characters in the individual aliases
data["aliases"] = {s_strip for s in data["aliases"] if (s_strip := s.strip())}
# Convert aliases to a set
data["aliases"] = set(data["aliases"])
if "labels" in data:
# Convert labels to a set

View File

@@ -227,10 +227,8 @@ def websocket_update_entity(
changes[key] = msg[key]
if "aliases" in msg:
# Create a set for the aliases without:
# - Empty strings
# - Trailing and leading whitespace characters in the individual aliases
changes["aliases"] = {s_strip for s in msg["aliases"] if (s_strip := s.strip())}
# Convert aliases to a set
changes["aliases"] = set(msg["aliases"])
if "labels" in msg:
# Convert labels to a set

View File

@@ -61,10 +61,8 @@ def websocket_create_floor(
data.pop("id")
if "aliases" in data:
# Create a set for the aliases without:
# - Empty strings
# - Trailing and leading whitespace characters in the individual aliases
data["aliases"] = {s_strip for s in data["aliases"] if (s_strip := s.strip())}
# Convert aliases to a set
data["aliases"] = set(data["aliases"])
try:
entry = registry.async_create(**data)
@@ -119,10 +117,8 @@ def websocket_update_floor(
data.pop("id")
if "aliases" in data:
# Create a set for the aliases without:
# - Empty strings
# - Trailing and leading whitespace characters in the individual aliases
data["aliases"] = {s_strip for s in data["aliases"] if (s_strip := s.strip())}
# Convert aliases to a set
data["aliases"] = set(data["aliases"])
try:
entry = registry.async_update(**data)

View File

@@ -236,9 +236,7 @@ async def async_prepare_agent(
async def async_handle_sentence_triggers(
hass: HomeAssistant,
user_input: ConversationInput,
chat_log: ChatLog,
hass: HomeAssistant, user_input: ConversationInput
) -> str | None:
"""Try to match input against sentence triggers and return response text.
@@ -247,13 +245,12 @@ async def async_handle_sentence_triggers(
agent = get_agent_manager(hass).default_agent
assert agent is not None
return await agent.async_handle_sentence_triggers(user_input, chat_log)
return await agent.async_handle_sentence_triggers(user_input)
async def async_handle_intents(
hass: HomeAssistant,
user_input: ConversationInput,
chat_log: ChatLog,
*,
intent_filter: Callable[[RecognizeResult], bool] | None = None,
) -> intent.IntentResponse | None:
@@ -264,9 +261,7 @@ async def async_handle_intents(
agent = get_agent_manager(hass).default_agent
assert agent is not None
return await agent.async_handle_intents(
user_input, chat_log, intent_filter=intent_filter
)
return await agent.async_handle_intents(user_input, intent_filter=intent_filter)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:

View File

@@ -66,7 +66,6 @@ from homeassistant.helpers import (
entity_registry as er,
floor_registry as fr,
intent,
llm,
start as ha_start,
template,
translation,
@@ -77,7 +76,7 @@ from homeassistant.util import language as language_util
from homeassistant.util.json import JsonObjectType, json_loads_object
from .agent_manager import get_agent_manager
from .chat_log import AssistantContent, ChatLog, ToolResultContent
from .chat_log import AssistantContent, ChatLog
from .const import (
DOMAIN,
METADATA_CUSTOM_FILE,
@@ -436,7 +435,7 @@ class DefaultAgent(ConversationEntity):
if trigger_result := await self.async_recognize_sentence_trigger(user_input):
# Process callbacks and get response
response_text = await self._handle_trigger_result(
trigger_result, user_input, chat_log
trigger_result, user_input
)
# Convert to conversation result
@@ -448,9 +447,8 @@ class DefaultAgent(ConversationEntity):
if response is None:
# Match intents
intent_result = await self.async_recognize_intent(user_input)
response = await self._async_process_intent_result(
intent_result, user_input, chat_log
intent_result, user_input
)
speech: str = response.speech.get("plain", {}).get("speech", "")
@@ -469,7 +467,6 @@ class DefaultAgent(ConversationEntity):
self,
result: RecognizeResult | None,
user_input: ConversationInput,
chat_log: ChatLog,
) -> intent.IntentResponse:
"""Process user input with intents."""
language = user_input.language or self.hass.config.language
@@ -532,21 +529,12 @@ class DefaultAgent(ConversationEntity):
ConversationTraceEventType.TOOL_CALL,
{
"intent_name": result.intent.name,
"slots": {entity.name: entity.value for entity in result.entities_list},
"slots": {
entity.name: entity.value or entity.text
for entity in result.entities_list
},
},
)
tool_input = llm.ToolInput(
tool_name=result.intent.name,
tool_args={entity.name: entity.value for entity in result.entities_list},
external=True,
)
chat_log.async_add_assistant_content_without_tools(
AssistantContent(
agent_id=user_input.agent_id,
content=None,
tool_calls=[tool_input],
)
)
try:
intent_response = await intent.async_handle(
@@ -609,16 +597,6 @@ class DefaultAgent(ConversationEntity):
)
intent_response.async_set_speech(speech)
tool_result = llm.IntentResponseDict(intent_response)
chat_log.async_add_assistant_content_without_tools(
ToolResultContent(
agent_id=user_input.agent_id,
tool_call_id=tool_input.id,
tool_name=tool_input.tool_name,
tool_result=tool_result,
)
)
return intent_response
def _recognize(
@@ -1545,31 +1523,16 @@ class DefaultAgent(ConversationEntity):
)
async def _handle_trigger_result(
self,
result: SentenceTriggerResult,
user_input: ConversationInput,
chat_log: ChatLog,
self, result: SentenceTriggerResult, user_input: ConversationInput
) -> str:
"""Run sentence trigger callbacks and return response text."""
# Gather callback responses in parallel
trigger_callbacks = [
self._triggers_details[trigger_id].callback(user_input, trigger_result)
for trigger_id, trigger_result in result.matched_triggers.items()
]
tool_input = llm.ToolInput(
tool_name="trigger_sentence",
tool_args={},
external=True,
)
chat_log.async_add_assistant_content_without_tools(
AssistantContent(
agent_id=user_input.agent_id,
content=None,
tool_calls=[tool_input],
)
)
# Use first non-empty result as response.
#
# There may be multiple copies of a trigger running when editing in
@@ -1598,38 +1561,23 @@ class DefaultAgent(ConversationEntity):
f"component.{DOMAIN}.conversation.agent.done", "Done"
)
tool_result: dict[str, Any] = {"response": response_text}
chat_log.async_add_assistant_content_without_tools(
ToolResultContent(
agent_id=user_input.agent_id,
tool_call_id=tool_input.id,
tool_name=tool_input.tool_name,
tool_result=tool_result,
)
)
return response_text
async def async_handle_sentence_triggers(
self,
user_input: ConversationInput,
chat_log: ChatLog,
self, user_input: ConversationInput
) -> str | None:
"""Try to input sentence against sentence triggers and return response text.
Returns None if no match occurred.
"""
if trigger_result := await self.async_recognize_sentence_trigger(user_input):
return await self._handle_trigger_result(
trigger_result, user_input, chat_log
)
return await self._handle_trigger_result(trigger_result, user_input)
return None
async def async_handle_intents(
self,
user_input: ConversationInput,
chat_log: ChatLog,
*,
intent_filter: Callable[[RecognizeResult], bool] | None = None,
) -> intent.IntentResponse | None:
@@ -1645,7 +1593,7 @@ class DefaultAgent(ConversationEntity):
# No error message on failed match
return None
response = await self._async_process_intent_result(result, user_input, chat_log)
response = await self._async_process_intent_result(result, user_input)
if (
response.response_type == intent.IntentResponseType.ERROR
and response.error_code

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/conversation",
"integration_type": "entity",
"quality_scale": "internal",
"requirements": ["hassil==3.5.0", "home-assistant-intents==2025.12.2"]
"requirements": ["hassil==3.4.0", "home-assistant-intents==2025.11.24"]
}

View File

@@ -108,5 +108,34 @@
"toggle_cover_tilt": {
"service": "mdi:arrow-top-right-bottom-left"
}
},
"triggers": {
"awning_opened": {
"trigger": "mdi:awning-outline"
},
"blind_opened": {
"trigger": "mdi:blinds-horizontal"
},
"curtain_opened": {
"trigger": "mdi:curtains"
},
"door_opened": {
"trigger": "mdi:door-open"
},
"garage_opened": {
"trigger": "mdi:garage-open"
},
"gate_opened": {
"trigger": "mdi:gate-open"
},
"shade_opened": {
"trigger": "mdi:roller-shade"
},
"shutter_opened": {
"trigger": "mdi:window-shutter-open"
},
"window_opened": {
"trigger": "mdi:window-open"
}
}
}

View File

@@ -1,4 +1,16 @@
{
"common": {
"trigger_behavior_description_awning": "The behavior of the targeted awnings to trigger on.",
"trigger_behavior_description_blind": "The behavior of the targeted blinds to trigger on.",
"trigger_behavior_description_curtain": "The behavior of the targeted curtains to trigger on.",
"trigger_behavior_description_door": "The behavior of the targeted doors to trigger on.",
"trigger_behavior_description_garage": "The behavior of the targeted garage doors to trigger on.",
"trigger_behavior_description_gate": "The behavior of the targeted gates to trigger on.",
"trigger_behavior_description_shade": "The behavior of the targeted shades to trigger on.",
"trigger_behavior_description_shutter": "The behavior of the targeted shutters to trigger on.",
"trigger_behavior_description_window": "The behavior of the targeted windows to trigger on.",
"trigger_behavior_name": "Behavior"
},
"device_automation": {
"action_type": {
"close": "Close {entity_name}",
@@ -82,6 +94,15 @@
"name": "Window"
}
},
"selector": {
"trigger_behavior": {
"options": {
"any": "Any",
"first": "First",
"last": "Last"
}
}
},
"services": {
"close_cover": {
"description": "Closes a cover.",
@@ -136,5 +157,142 @@
"name": "Toggle tilt"
}
},
"title": "Cover"
"title": "Cover",
"triggers": {
"awning_opened": {
"description": "Triggers when an awning opens.",
"description_configured": "[%key:component::cover::triggers::awning_opened::description%]",
"fields": {
"behavior": {
"description": "[%key:component::cover::common::trigger_behavior_description_awning%]",
"name": "[%key:component::cover::common::trigger_behavior_name%]"
},
"fully_opened": {
"description": "Require the awnings to be fully opened before triggering.",
"name": "Fully opened"
}
},
"name": "When an awning opens"
},
"blind_opened": {
"description": "Triggers when a blind opens.",
"description_configured": "[%key:component::cover::triggers::blind_opened::description%]",
"fields": {
"behavior": {
"description": "[%key:component::cover::common::trigger_behavior_description_blind%]",
"name": "[%key:component::cover::common::trigger_behavior_name%]"
},
"fully_opened": {
"description": "Require the blinds to be fully opened before triggering.",
"name": "Fully opened"
}
},
"name": "When a blind opens"
},
"curtain_opened": {
"description": "Triggers when a curtain opens.",
"description_configured": "[%key:component::cover::triggers::curtain_opened::description%]",
"fields": {
"behavior": {
"description": "[%key:component::cover::common::trigger_behavior_description_curtain%]",
"name": "[%key:component::cover::common::trigger_behavior_name%]"
},
"fully_opened": {
"description": "Require the curtains to be fully opened before triggering.",
"name": "Fully opened"
}
},
"name": "When a curtain opens"
},
"door_opened": {
"description": "Triggers when a door opens.",
"description_configured": "[%key:component::cover::triggers::door_opened::description%]",
"fields": {
"behavior": {
"description": "[%key:component::cover::common::trigger_behavior_description_door%]",
"name": "[%key:component::cover::common::trigger_behavior_name%]"
},
"fully_opened": {
"description": "Require the doors to be fully opened before triggering.",
"name": "Fully opened"
}
},
"name": "When a door opens"
},
"garage_opened": {
"description": "Triggers when a garage door opens.",
"description_configured": "[%key:component::cover::triggers::garage_opened::description%]",
"fields": {
"behavior": {
"description": "[%key:component::cover::common::trigger_behavior_description_garage%]",
"name": "[%key:component::cover::common::trigger_behavior_name%]"
},
"fully_opened": {
"description": "Require the garage doors to be fully opened before triggering.",
"name": "Fully opened"
}
},
"name": "When a garage door opens"
},
"gate_opened": {
"description": "Triggers when a gate opens.",
"description_configured": "[%key:component::cover::triggers::gate_opened::description%]",
"fields": {
"behavior": {
"description": "[%key:component::cover::common::trigger_behavior_description_gate%]",
"name": "[%key:component::cover::common::trigger_behavior_name%]"
},
"fully_opened": {
"description": "Require the gates to be fully opened before triggering.",
"name": "Fully opened"
}
},
"name": "When a gate opens"
},
"shade_opened": {
"description": "Triggers when a shade opens.",
"description_configured": "[%key:component::cover::triggers::shade_opened::description%]",
"fields": {
"behavior": {
"description": "[%key:component::cover::common::trigger_behavior_description_shade%]",
"name": "[%key:component::cover::common::trigger_behavior_name%]"
},
"fully_opened": {
"description": "Require the shades to be fully opened before triggering.",
"name": "Fully opened"
}
},
"name": "When a shade opens"
},
"shutter_opened": {
"description": "Triggers when a shutter opens.",
"description_configured": "[%key:component::cover::triggers::shutter_opened::description%]",
"fields": {
"behavior": {
"description": "[%key:component::cover::common::trigger_behavior_description_shutter%]",
"name": "[%key:component::cover::common::trigger_behavior_name%]"
},
"fully_opened": {
"description": "Require the shutters to be fully opened before triggering.",
"name": "Fully opened"
}
},
"name": "When a shutter opens"
},
"window_opened": {
"description": "Triggers when a window opens.",
"description_configured": "[%key:component::cover::triggers::window_opened::description%]",
"fields": {
"behavior": {
"description": "[%key:component::cover::common::trigger_behavior_description_window%]",
"name": "[%key:component::cover::common::trigger_behavior_name%]"
},
"fully_opened": {
"description": "Require the windows to be fully opened before triggering.",
"name": "Fully opened"
}
},
"name": "When a window opens"
}
}
}

View File

@@ -0,0 +1,116 @@
"""Provides triggers for covers."""
from typing import Final
import voluptuous as vol
from homeassistant.const import CONF_OPTIONS
from homeassistant.core import HomeAssistant, State
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity import get_device_class
from homeassistant.helpers.trigger import (
ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST,
EntityTriggerBase,
Trigger,
TriggerConfig,
)
from homeassistant.helpers.typing import UNDEFINED, UndefinedType
from . import ATTR_CURRENT_POSITION, CoverDeviceClass, CoverState
from .const import DOMAIN
ATTR_FULLY_OPENED: Final = "fully_opened"
COVER_OPENED_TRIGGER_SCHEMA = ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST.extend(
{
vol.Required(CONF_OPTIONS): {
vol.Required(ATTR_FULLY_OPENED, default=False): bool,
},
}
)
def get_device_class_or_undefined(
hass: HomeAssistant, entity_id: str
) -> str | None | UndefinedType:
"""Get the device class of an entity or UNDEFINED if not found."""
try:
return get_device_class(hass, entity_id)
except HomeAssistantError:
return UNDEFINED
class CoverOpenedClosedTrigger(EntityTriggerBase):
"""Class for cover opened and closed triggers."""
_attribute: str = ATTR_CURRENT_POSITION
_attribute_value: int | None = None
_device_class: CoverDeviceClass | None
_domain: str = DOMAIN
_to_states: set[str]
def is_to_state(self, state: State) -> bool:
"""Check if the state matches the target state."""
if state.state not in self._to_states:
return False
if (
self._attribute_value is not None
and (value := state.attributes.get(self._attribute)) is not None
and value != self._attribute_value
):
return False
return True
def entity_filter(self, entities: set[str]) -> set[str]:
"""Filter entities of this domain."""
entities = super().entity_filter(entities)
return {
entity_id
for entity_id in entities
if get_device_class_or_undefined(self._hass, entity_id)
== self._device_class
}
class CoverOpenedTrigger(CoverOpenedClosedTrigger):
"""Class for cover opened triggers."""
_schema = COVER_OPENED_TRIGGER_SCHEMA
_to_states = {CoverState.OPEN, CoverState.OPENING}
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
"""Initialize the state trigger."""
super().__init__(hass, config)
if self._options.get(ATTR_FULLY_OPENED):
self._attribute_value = 100
def make_cover_opened_trigger(
device_class: CoverDeviceClass | None,
) -> type[CoverOpenedTrigger]:
"""Create an entity state attribute trigger class."""
class CustomTrigger(CoverOpenedTrigger):
"""Trigger for entity state changes."""
_device_class = device_class
return CustomTrigger
TRIGGERS: dict[str, type[Trigger]] = {
"awning_opened": make_cover_opened_trigger(CoverDeviceClass.AWNING),
"blind_opened": make_cover_opened_trigger(CoverDeviceClass.BLIND),
"curtain_opened": make_cover_opened_trigger(CoverDeviceClass.CURTAIN),
"door_opened": make_cover_opened_trigger(CoverDeviceClass.DOOR),
"garage_opened": make_cover_opened_trigger(CoverDeviceClass.GARAGE),
"gate_opened": make_cover_opened_trigger(CoverDeviceClass.GATE),
"shade_opened": make_cover_opened_trigger(CoverDeviceClass.SHADE),
"shutter_opened": make_cover_opened_trigger(CoverDeviceClass.SHUTTER),
"window_opened": make_cover_opened_trigger(CoverDeviceClass.WINDOW),
}
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
"""Return the triggers for covers."""
return TRIGGERS

View File

@@ -0,0 +1,79 @@
.trigger_common_fields: &trigger_common_fields
behavior:
required: true
default: any
selector:
select:
translation_key: trigger_behavior
options:
- first
- last
- any
fully_opened:
required: true
default: false
selector:
boolean:
awning_opened:
fields: *trigger_common_fields
target:
entity:
domain: cover
device_class: awning
blind_opened:
fields: *trigger_common_fields
target:
entity:
domain: cover
device_class: blind
curtain_opened:
fields: *trigger_common_fields
target:
entity:
domain: cover
device_class: curtain
door_opened:
fields: *trigger_common_fields
target:
entity:
domain: cover
device_class: door
garage_opened:
fields: *trigger_common_fields
target:
entity:
domain: cover
device_class: garage
gate_opened:
fields: *trigger_common_fields
target:
entity:
domain: cover
device_class: gate
shade_opened:
fields: *trigger_common_fields
target:
entity:
domain: cover
device_class: shade
shutter_opened:
fields: *trigger_common_fields
target:
entity:
domain: cover
device_class: shutter
window_opened:
fields: *trigger_common_fields
target:
entity:
domain: cover
device_class: window

View File

@@ -6,10 +6,9 @@
"config_flow": true,
"dependencies": ["ssdp"],
"documentation": "https://www.home-assistant.io/integrations/dlna_dmr",
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["async_upnp_client"],
"requirements": ["async-upnp-client==0.46.1", "getmac==0.9.5"],
"requirements": ["async-upnp-client==0.46.0", "getmac==0.9.5"],
"ssdp": [
{
"deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1",

View File

@@ -6,9 +6,8 @@
"config_flow": true,
"dependencies": ["ssdp"],
"documentation": "https://www.home-assistant.io/integrations/dlna_dms",
"integration_type": "service",
"iot_class": "local_polling",
"requirements": ["async-upnp-client==0.46.1"],
"requirements": ["async-upnp-client==0.46.0"],
"ssdp": [
{
"deviceType": "urn:schemas-upnp-org:device:MediaServer:1",

View File

@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/dnsip",
"iot_class": "cloud_polling",
"requirements": ["aiodns==3.6.1"]
"requirements": ["aiodns==3.5.0"]
}

View File

@@ -102,12 +102,6 @@ class ConfiguredDoorBird:
"""Get token for device."""
return self._token
def _get_hass_url(self) -> str:
"""Get the Home Assistant URL for this device."""
if custom_url := self.custom_url:
return custom_url
return get_url(self._hass, prefer_external=False)
async def async_register_events(self) -> None:
"""Register events on device."""
if not self.door_station_events:
@@ -152,7 +146,13 @@ class ConfiguredDoorBird:
async def _async_register_events(self) -> dict[str, Any]:
"""Register events on device."""
hass_url = self._get_hass_url()
# Override url if another is specified in the configuration
if custom_url := self.custom_url:
hass_url = custom_url
else:
# Get the URL of this server
hass_url = get_url(self._hass, prefer_external=False)
http_fav = await self._async_get_http_favorites()
if any(
# Note that a list comp is used here to ensure all
@@ -191,14 +191,10 @@ class ConfiguredDoorBird:
self._get_event_name(event): event_type
for event, event_type in DEFAULT_EVENT_TYPES
}
hass_url = self._get_hass_url()
for identifier, data in http_fav.items():
title: str | None = data.get("title")
if not title or not title.startswith("Home Assistant"):
continue
value: str | None = data.get("value")
if not value or not value.startswith(hass_url):
continue # Not our favorite - different HA instance or stale
event = title.partition("(")[2].strip(")")
if input_type := favorite_input_type.get(identifier):
events.append(DoorbirdEvent(event, input_type))

View File

@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
"requirements": ["py-sucks==0.9.11", "deebot-client==17.0.0"]
"requirements": ["py-sucks==0.9.11", "deebot-client==16.4.0"]
}

View File

@@ -5,7 +5,6 @@
"config_flow": true,
"dependencies": ["webhook"],
"documentation": "https://www.home-assistant.io/integrations/ecowitt",
"integration_type": "device",
"iot_class": "local_push",
"requirements": ["aioecowitt==2025.9.2"]
}

View File

@@ -285,14 +285,16 @@ async def async_setup_entry(
name=sensor.name,
)
# Only total rain needs state class for long-term statistics
# Hourly rain doesn't reset to fixed hours, it must be measurement state classes
if sensor.key in (
"totalrainin",
"totalrainmm",
"hrain_piezomm",
"hrain_piezo",
"hourlyrainmm",
"hourlyrainin",
):
description = dataclasses.replace(
description,
state_class=SensorStateClass.TOTAL_INCREASING,
state_class=SensorStateClass.MEASUREMENT,
)
async_add_entities([EcowittSensorEntity(sensor, description)])

View File

@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/ekeybionyx",
"iot_class": "local_push",
"quality_scale": "bronze",
"requirements": ["ekey-bionyxpy==1.0.1"]
"requirements": ["ekey-bionyxpy==1.0.0"]
}

View File

@@ -7,7 +7,7 @@
"iot_class": "local_polling",
"loggers": ["pyenphase"],
"quality_scale": "platinum",
"requirements": ["pyenphase==2.4.2"],
"requirements": ["pyenphase==2.4.0"],
"zeroconf": [
{
"type": "_enphase-envoy._tcp.local."

View File

@@ -17,7 +17,7 @@ DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS = False
DEFAULT_PORT: Final = 6053
STABLE_BLE_VERSION_STR = "2025.11.0"
STABLE_BLE_VERSION_STR = "2025.8.0"
STABLE_BLE_VERSION = AwesomeVersion(STABLE_BLE_VERSION_STR)
PROJECT_URLS = {
"esphome.bluetooth-proxy": "https://esphome.github.io/bluetooth-proxies/",

View File

@@ -17,7 +17,7 @@
"mqtt": ["esphome/discover/#"],
"quality_scale": "platinum",
"requirements": [
"aioesphomeapi==42.9.0",
"aioesphomeapi==42.8.0",
"esphome-dashboard-api==1.3.0",
"bleak-esphome==3.4.0"
],

View File

@@ -157,7 +157,7 @@
"title": "[%key:component::assist_pipeline::issues::assist_in_progress_deprecated::title%]"
},
"ble_firmware_outdated": {
"description": "ESPHome {version} introduces ultra-low latency event processing, reducing BLE event delays from 0-16 milliseconds to approximately 12 microseconds. This resolves stability issues when pairing, connecting, or handshaking with devices that require low latency, and makes Bluetooth proxy operations rival or exceed local adapters. We highly recommend updating {name} to take advantage of these improvements.",
"description": "To improve Bluetooth reliability and performance, we highly recommend updating {name} with ESPHome {version} or later. When updating the device from ESPHome earlier than 2022.12.0, it is recommended to use a serial cable instead of an over-the-air update to take advantage of the new partition scheme.",
"title": "Update {name} with ESPHome {version} or later"
},
"device_conflict": {

View File

@@ -102,7 +102,6 @@ SENSORS: tuple[EssentSensorEntityDescription, ...] = (
key="average_today",
translation_key="average_today",
value_fn=lambda energy_data: energy_data.avg_price,
energy_types=(EnergyType.ELECTRICITY,),
),
EssentSensorEntityDescription(
key="lowest_price_today",

View File

@@ -44,6 +44,9 @@
"electricity_next_price": {
"name": "Next electricity price"
},
"gas_average_today": {
"name": "Average gas price today"
},
"gas_current_price": {
"name": "Current gas price"
},

View File

@@ -6,5 +6,5 @@
"iot_class": "cloud_polling",
"loggers": ["evohome", "evohomeasync", "evohomeasync2"],
"quality_scale": "legacy",
"requirements": ["evohome-async==1.0.6"]
"requirements": ["evohome-async==1.0.5"]
}

View File

@@ -166,24 +166,26 @@
"title": "Fan",
"triggers": {
"turned_off": {
"description": "Triggers after one or more fans turn off.",
"description": "Triggers when a fan is turned off.",
"description_configured": "[%key:component::fan::triggers::turned_off::description%]",
"fields": {
"behavior": {
"description": "[%key:component::fan::common::trigger_behavior_description%]",
"name": "[%key:component::fan::common::trigger_behavior_name%]"
}
},
"name": "Fan turned off"
"name": "When a fan is turned off"
},
"turned_on": {
"description": "Triggers after one or more fans turn on.",
"description": "Triggers when a fan is turned on.",
"description_configured": "[%key:component::fan::triggers::turned_on::description%]",
"fields": {
"behavior": {
"description": "[%key:component::fan::common::trigger_behavior_description%]",
"name": "[%key:component::fan::common::trigger_behavior_name%]"
}
},
"name": "Fan turned on"
"name": "When a fan is turned on"
}
}
}

View File

@@ -1,22 +1,30 @@
"""API for fitbit bound to Home Assistant OAuth."""
from abc import ABC, abstractmethod
from collections.abc import Callable
from collections.abc import Awaitable, Callable
import logging
from typing import Any, cast
from fitbit import Fitbit
from fitbit.exceptions import HTTPException, HTTPUnauthorized
from fitbit_web_api import ApiClient, Configuration, DevicesApi
from fitbit_web_api.exceptions import (
ApiException,
OpenApiException,
UnauthorizedException,
)
from fitbit_web_api.models.device import Device
from requests.exceptions import ConnectionError as RequestsConnectionError
from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_entry_oauth2_flow
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.util.unit_system import METRIC_SYSTEM
from .const import FitbitUnitSystem
from .exceptions import FitbitApiException, FitbitAuthException
from .model import FitbitDevice, FitbitProfile
from .model import FitbitProfile
_LOGGER = logging.getLogger(__name__)
@@ -58,6 +66,14 @@ class FitbitApi(ABC):
expires_at=float(token[CONF_EXPIRES_AT]),
)
async def _async_get_fitbit_web_api(self) -> ApiClient:
"""Create and return an ApiClient configured with the current access token."""
token = await self.async_get_access_token()
configuration = Configuration()
configuration.pool_manager = async_get_clientsession(self._hass)
configuration.access_token = token[CONF_ACCESS_TOKEN]
return ApiClient(configuration)
async def async_get_user_profile(self) -> FitbitProfile:
"""Return the user profile from the API."""
if self._profile is None:
@@ -94,21 +110,13 @@ class FitbitApi(ABC):
return FitbitUnitSystem.METRIC
return FitbitUnitSystem.EN_US
async def async_get_devices(self) -> list[FitbitDevice]:
"""Return available devices."""
client = await self._async_get_client()
devices: list[dict[str, str]] = await self._run(client.get_devices)
async def async_get_devices(self) -> list[Device]:
"""Return available devices using fitbit-web-api."""
client = await self._async_get_fitbit_web_api()
devices_api = DevicesApi(client)
devices: list[Device] = await self._run_async(devices_api.get_devices)
_LOGGER.debug("get_devices=%s", devices)
return [
FitbitDevice(
id=device["id"],
device_version=device["deviceVersion"],
battery_level=int(device["batteryLevel"]),
battery=device["battery"],
type=device["type"],
)
for device in devices
]
return devices
async def async_get_latest_time_series(self, resource_type: str) -> dict[str, Any]:
"""Return the most recent value from the time series for the specified resource type."""
@@ -140,6 +148,20 @@ class FitbitApi(ABC):
_LOGGER.debug("Error from fitbit API: %s", err)
raise FitbitApiException("Error from fitbit API") from err
async def _run_async[_T](self, func: Callable[[], Awaitable[_T]]) -> _T:
"""Run client command."""
try:
return await func()
except UnauthorizedException as err:
_LOGGER.debug("Unauthorized error from fitbit API: %s", err)
raise FitbitAuthException("Authentication error from fitbit API") from err
except ApiException as err:
_LOGGER.debug("Error from fitbit API: %s", err)
raise FitbitApiException("Error from fitbit API") from err
except OpenApiException as err:
_LOGGER.debug("Error communicating with fitbit API: %s", err)
raise FitbitApiException("Communication error from fitbit API") from err
class OAuthFitbitApi(FitbitApi):
"""Provide fitbit authentication tied to an OAuth2 based config entry."""

View File

@@ -6,6 +6,8 @@ import datetime
import logging
from typing import Final
from fitbit_web_api.models.device import Device
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
@@ -13,7 +15,6 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
from .api import FitbitApi
from .exceptions import FitbitApiException, FitbitAuthException
from .model import FitbitDevice
_LOGGER = logging.getLogger(__name__)
@@ -23,7 +24,7 @@ TIMEOUT = 10
type FitbitConfigEntry = ConfigEntry[FitbitData]
class FitbitDeviceCoordinator(DataUpdateCoordinator[dict[str, FitbitDevice]]):
class FitbitDeviceCoordinator(DataUpdateCoordinator[dict[str, Device]]):
"""Coordinator for fetching fitbit devices from the API."""
config_entry: FitbitConfigEntry
@@ -41,7 +42,7 @@ class FitbitDeviceCoordinator(DataUpdateCoordinator[dict[str, FitbitDevice]]):
)
self._api = api
async def _async_update_data(self) -> dict[str, FitbitDevice]:
async def _async_update_data(self) -> dict[str, Device]:
"""Fetch data from API endpoint."""
async with asyncio.timeout(TIMEOUT):
try:
@@ -50,7 +51,7 @@ class FitbitDeviceCoordinator(DataUpdateCoordinator[dict[str, FitbitDevice]]):
raise ConfigEntryAuthFailed(err) from err
except FitbitApiException as err:
raise UpdateFailed(err) from err
return {device.id: device for device in devices}
return {device.id: device for device in devices if device.id is not None}
@dataclass

View File

@@ -6,6 +6,6 @@
"dependencies": ["application_credentials", "http"],
"documentation": "https://www.home-assistant.io/integrations/fitbit",
"iot_class": "cloud_polling",
"loggers": ["fitbit"],
"requirements": ["fitbit==0.3.1"]
"loggers": ["fitbit", "fitbit_web_api"],
"requirements": ["fitbit==0.3.1", "fitbit-web-api==2.13.5"]
}

View File

@@ -21,26 +21,6 @@ class FitbitProfile:
"""The locale defined in the user's Fitbit account settings."""
@dataclass
class FitbitDevice:
"""Device from the Fitbit API response."""
id: str
"""The device ID."""
device_version: str
"""The product name of the device."""
battery_level: int
"""The battery level as a percentage."""
battery: str
"""Returns the battery level of the device."""
type: str
"""The type of the device such as TRACKER or SCALE."""
@dataclass
class FitbitConfig:
"""Information from the fitbit ConfigEntry data."""

View File

@@ -8,6 +8,8 @@ import datetime
import logging
from typing import Any, Final, cast
from fitbit_web_api.models.device import Device
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
@@ -32,7 +34,7 @@ from .api import FitbitApi
from .const import ATTRIBUTION, BATTERY_LEVELS, DOMAIN, FitbitScope, FitbitUnitSystem
from .coordinator import FitbitConfigEntry, FitbitDeviceCoordinator
from .exceptions import FitbitApiException, FitbitAuthException
from .model import FitbitDevice, config_from_entry_data
from .model import config_from_entry_data
_LOGGER: Final = logging.getLogger(__name__)
@@ -657,7 +659,7 @@ class FitbitBatterySensor(CoordinatorEntity[FitbitDeviceCoordinator], SensorEnti
coordinator: FitbitDeviceCoordinator,
user_profile_id: str,
description: FitbitSensorEntityDescription,
device: FitbitDevice,
device: Device,
enable_default_override: bool,
) -> None:
"""Initialize the Fitbit sensor."""
@@ -677,7 +679,9 @@ class FitbitBatterySensor(CoordinatorEntity[FitbitDeviceCoordinator], SensorEnti
@property
def icon(self) -> str | None:
"""Icon to use in the frontend, if any."""
if battery_level := BATTERY_LEVELS.get(self.device.battery):
if self.device.battery is not None and (
battery_level := BATTERY_LEVELS.get(self.device.battery)
):
return icon_for_battery_level(battery_level=battery_level)
return self.entity_description.icon
@@ -697,7 +701,7 @@ class FitbitBatterySensor(CoordinatorEntity[FitbitDeviceCoordinator], SensorEnti
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self.device = self.coordinator.data[self.device.id]
self.device = self.coordinator.data[cast(str, self.device.id)]
self._attr_native_value = self.device.battery
self.async_write_ha_state()
@@ -715,7 +719,7 @@ class FitbitBatteryLevelSensor(
coordinator: FitbitDeviceCoordinator,
user_profile_id: str,
description: FitbitSensorEntityDescription,
device: FitbitDevice,
device: Device,
) -> None:
"""Initialize the Fitbit sensor."""
super().__init__(coordinator)
@@ -736,6 +740,6 @@ class FitbitBatteryLevelSensor(
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self.device = self.coordinator.data[self.device.id]
self.device = self.coordinator.data[cast(str, self.device.id)]
self._attr_native_value = self.device.battery_level
self.async_write_ha_state()

View File

@@ -9,7 +9,6 @@
}
],
"documentation": "https://www.home-assistant.io/integrations/fronius",
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["pyfronius"],
"quality_scale": "platinum",

View File

@@ -23,5 +23,5 @@
"winter_mode": {}
},
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20251203.3"]
"requirements": ["home-assistant-frontend==20251126.0"]
}

View File

@@ -2,7 +2,6 @@
from __future__ import annotations
import asyncio
from collections.abc import Callable, Coroutine
from functools import wraps
from typing import Any
@@ -16,9 +15,7 @@ from homeassistant.helpers import singleton
from homeassistant.helpers.storage import Store
from homeassistant.util.hass_dict import HassKey
DATA_STORAGE: HassKey[dict[str, asyncio.Future[UserStore]]] = HassKey(
"frontend_storage"
)
DATA_STORAGE: HassKey[dict[str, UserStore]] = HassKey("frontend_storage")
DATA_SYSTEM_STORAGE: HassKey[SystemStore] = HassKey("frontend_system_storage")
STORAGE_VERSION_USER_DATA = 1
STORAGE_VERSION_SYSTEM_DATA = 1
@@ -37,18 +34,11 @@ async def async_setup_frontend_storage(hass: HomeAssistant) -> None:
async def async_user_store(hass: HomeAssistant, user_id: str) -> UserStore:
"""Access a user store."""
stores = hass.data.setdefault(DATA_STORAGE, {})
if (future := stores.get(user_id)) is None:
future = stores[user_id] = hass.loop.create_future()
store = UserStore(hass, user_id)
try:
await store.async_load()
except BaseException as ex:
del stores[user_id]
future.set_exception(ex)
raise
future.set_result(store)
if (store := stores.get(user_id)) is None:
store = stores[user_id] = UserStore(hass, user_id)
await store.async_load()
return await future
return store
class UserStore:

View File

@@ -1,9 +1,9 @@
{
"preview_features": {
"winter_mode": {
"description": "Adds falling snowflakes on your screen. Get your home ready for winter! ❄️\n\nIf you have animations disabled in your device accessibility settings, this feature will not work.",
"disable_confirmation": "Snowflakes will no longer fall on your screen. You can re-enable this at any time in Labs settings.",
"enable_confirmation": "Snowflakes will start falling on your screen. You can turn this off at any time in Labs settings.",
"description": "Adds falling snowflakes on your screen. Get your home ready for winter! ❄️",
"disable_confirmation": "Snowflakes will no longer fall on your screen. You can re-enable this at any time in labs settings.",
"enable_confirmation": "Snowflakes will start falling on your screen. You can turn this off at any time in labs settings.",
"name": "Winter mode"
}
},

View File

@@ -6,7 +6,6 @@ from dataclasses import dataclass
import logging
from secrets import token_hex
import shutil
from tempfile import mkdtemp
from aiohttp import BasicAuth, ClientSession, UnixConnector
from aiohttp.client_exceptions import ClientConnectionError, ServerConnectionError
@@ -63,11 +62,11 @@ from .const import (
CONF_DEBUG_UI,
DEBUG_UI_URL_MESSAGE,
DOMAIN,
HA_MANAGED_UNIX_SOCKET,
HA_MANAGED_URL,
RECOMMENDED_VERSION,
)
from .server import Server
from .util import get_go2rtc_unix_socket_path
_LOGGER = logging.getLogger(__name__)
@@ -155,12 +154,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
auth = BasicAuth(username, password)
# HA will manage the binary
temp_dir = mkdtemp(prefix="go2rtc-")
# Manually created session (not using the helper) needs to be closed manually
# See on_stop listener below
session = ClientSession(
connector=UnixConnector(path=get_go2rtc_unix_socket_path(temp_dir)),
auth=auth,
connector=UnixConnector(path=HA_MANAGED_UNIX_SOCKET), auth=auth
)
server = Server(
hass,
@@ -169,7 +166,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
enable_ui=domain_config.get(CONF_DEBUG_UI, False),
username=username,
password=password,
working_dir=temp_dir,
)
try:
await server.start()

View File

@@ -6,6 +6,7 @@ CONF_DEBUG_UI = "debug_ui"
DEBUG_UI_URL_MESSAGE = "Url and debug_ui cannot be set at the same time."
HA_MANAGED_API_PORT = 11984
HA_MANAGED_URL = f"http://localhost:{HA_MANAGED_API_PORT}/"
HA_MANAGED_UNIX_SOCKET = "/run/go2rtc.sock"
# When changing this version, also update the corresponding SHA hash (_GO2RTC_SHA)
# in script/hassfest/docker.py.
RECOMMENDED_VERSION = "1.9.12"

View File

@@ -12,13 +12,13 @@ from go2rtc_client import Go2RtcRestClient
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from .const import HA_MANAGED_API_PORT, HA_MANAGED_URL
from .util import get_go2rtc_unix_socket_path
from .const import HA_MANAGED_API_PORT, HA_MANAGED_UNIX_SOCKET, HA_MANAGED_URL
_LOGGER = logging.getLogger(__name__)
_TERMINATE_TIMEOUT = 5
_SETUP_TIMEOUT = 30
_SUCCESSFUL_BOOT_MESSAGE = "INF [api] listen addr="
_LOCALHOST_IP = "127.0.0.1"
_LOG_BUFFER_SIZE = 512
_RESPAWN_COOLDOWN = 1
@@ -122,9 +122,7 @@ def _format_list_for_yaml(items: tuple[str, ...]) -> str:
return f"[{formatted_items}]"
def _create_temp_file(
enable_ui: bool, username: str, password: str, working_dir: str
) -> str:
def _create_temp_file(enable_ui: bool, username: str, password: str) -> str:
"""Create temporary config file."""
app_modules: tuple[str, ...] = _APP_MODULES
api_paths: tuple[str, ...] = _API_ALLOW_PATHS
@@ -141,13 +139,11 @@ def _create_temp_file(
# Set delete=False to prevent the file from being deleted when the file is closed
# Linux is clearing tmp folder on reboot, so no need to delete it manually
with NamedTemporaryFile(
prefix="go2rtc_", suffix=".yaml", dir=working_dir, delete=False
) as file:
with NamedTemporaryFile(prefix="go2rtc_", suffix=".yaml", delete=False) as file:
file.write(
_GO2RTC_CONFIG_FORMAT.format(
listen_config=listen_config,
unix_socket=get_go2rtc_unix_socket_path(working_dir),
unix_socket=HA_MANAGED_UNIX_SOCKET,
app_modules=_format_list_for_yaml(app_modules),
api_allow_paths=_format_list_for_yaml(api_paths),
username=username,
@@ -169,7 +165,6 @@ class Server:
enable_ui: bool = False,
username: str,
password: str,
working_dir: str,
) -> None:
"""Initialize the server."""
self._hass = hass
@@ -178,7 +173,6 @@ class Server:
self._enable_ui = enable_ui
self._username = username
self._password = password
self._working_dir = working_dir
self._log_buffer: deque[str] = deque(maxlen=_LOG_BUFFER_SIZE)
self._process: asyncio.subprocess.Process | None = None
self._startup_complete = asyncio.Event()
@@ -196,11 +190,7 @@ class Server:
"""Start the server."""
_LOGGER.debug("Starting go2rtc server")
config_file = await self._hass.async_add_executor_job(
_create_temp_file,
self._enable_ui,
self._username,
self._password,
self._working_dir,
_create_temp_file, self._enable_ui, self._username, self._password
)
self._startup_complete.clear()

View File

@@ -1,12 +0,0 @@
"""Go2rtc utility functions."""
from pathlib import Path
_HA_MANAGED_UNIX_SOCKET_FILE = "go2rtc.sock"
def get_go2rtc_unix_socket_path(path: str | Path) -> str:
"""Get the Go2rtc unix socket path."""
if not isinstance(path, Path):
path = Path(path)
return str(path / _HA_MANAGED_UNIX_SOCKET_FILE)

View File

@@ -5,8 +5,7 @@
"config_flow": true,
"dependencies": ["application_credentials"],
"documentation": "https://www.home-assistant.io/integrations/google",
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["googleapiclient"],
"requirements": ["gcal-sync==8.0.0", "oauth2client==4.1.3", "ical==12.1.2"]
"requirements": ["gcal-sync==8.0.0", "oauth2client==4.1.3", "ical==11.1.0"]
}

View File

@@ -51,9 +51,9 @@ async def _validate_input(
description_placeholders: dict[str, str],
) -> bool:
try:
await api.async_get_current_conditions(
await api.async_air_quality(
lat=user_input[CONF_LOCATION][CONF_LATITUDE],
lon=user_input[CONF_LOCATION][CONF_LONGITUDE],
long=user_input[CONF_LOCATION][CONF_LONGITUDE],
)
except GoogleAirQualityApiError as err:
errors["base"] = "cannot_connect"

View File

@@ -7,7 +7,7 @@ from typing import Final
from google_air_quality_api.api import GoogleAirQualityApi
from google_air_quality_api.exceptions import GoogleAirQualityApiError
from google_air_quality_api.model import AirQualityCurrentConditionsData
from google_air_quality_api.model import AirQualityData
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE
@@ -23,9 +23,7 @@ UPDATE_INTERVAL: Final = timedelta(hours=1)
type GoogleAirQualityConfigEntry = ConfigEntry[GoogleAirQualityRuntimeData]
class GoogleAirQualityUpdateCoordinator(
DataUpdateCoordinator[AirQualityCurrentConditionsData]
):
class GoogleAirQualityUpdateCoordinator(DataUpdateCoordinator[AirQualityData]):
"""Coordinator for fetching Google AirQuality data."""
config_entry: GoogleAirQualityConfigEntry
@@ -50,10 +48,10 @@ class GoogleAirQualityUpdateCoordinator(
self.lat = subentry.data[CONF_LATITUDE]
self.long = subentry.data[CONF_LONGITUDE]
async def _async_update_data(self) -> AirQualityCurrentConditionsData:
async def _async_update_data(self) -> AirQualityData:
"""Fetch air quality data for this coordinate."""
try:
return await self.client.async_get_current_conditions(self.lat, self.long)
return await self.client.async_air_quality(self.lat, self.long)
except GoogleAirQualityApiError as ex:
_LOGGER.debug("Cannot fetch air quality data: %s", str(ex))
raise UpdateFailed(

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["google_air_quality_api"],
"quality_scale": "bronze",
"requirements": ["google_air_quality_api==2.0.2"]
"requirements": ["google_air_quality_api==1.1.2"]
}

View File

@@ -4,7 +4,7 @@ from collections.abc import Callable
from dataclasses import dataclass
import logging
from google_air_quality_api.model import AirQualityCurrentConditionsData
from google_air_quality_api.model import AirQualityData
from homeassistant.components.sensor import (
SensorDeviceClass,
@@ -33,17 +33,15 @@ PARALLEL_UPDATES = 0
class AirQualitySensorEntityDescription(SensorEntityDescription):
"""Describes Air Quality sensor entity."""
exists_fn: Callable[[AirQualityCurrentConditionsData], bool] = lambda _: True
options_fn: Callable[[AirQualityCurrentConditionsData], list[str] | None] = (
exists_fn: Callable[[AirQualityData], bool] = lambda _: True
options_fn: Callable[[AirQualityData], list[str] | None] = lambda _: None
value_fn: Callable[[AirQualityData], StateType]
native_unit_of_measurement_fn: Callable[[AirQualityData], str | None] = (
lambda _: None
)
value_fn: Callable[[AirQualityCurrentConditionsData], StateType]
native_unit_of_measurement_fn: Callable[
[AirQualityCurrentConditionsData], str | None
] = lambda _: None
translation_placeholders_fn: (
Callable[[AirQualityCurrentConditionsData], dict[str, str]] | None
) = None
translation_placeholders_fn: Callable[[AirQualityData], dict[str, str]] | None = (
None
)
AIR_QUALITY_SENSOR_TYPES: tuple[AirQualitySensorEntityDescription, ...] = (

View File

@@ -59,14 +59,9 @@
"user": "Add location"
},
"step": {
"location": {
"user": {
"data": {
"location": "[%key:common::config_flow::data::location%]",
"name": "[%key:common::config_flow::data::name%]"
},
"data_description": {
"location": "[%key:component::google_air_quality::config::step::user::data_description::location%]",
"name": "[%key:component::google_air_quality::config::step::user::data_description::name%]"
"location": "[%key:common::config_flow::data::location%]"
},
"description": "Select the coordinates for which you want to create an entry.",
"title": "Air quality data location"
@@ -88,16 +83,16 @@
"1b_good_air_quality": "1B - Good air quality",
"2_cyan": "2 - Cyan",
"2_light_green": "2 - Light green",
"2_orange": "4 - Orange",
"2_red": "5 - Red",
"2_yellow": "3 - Yellow",
"2a_acceptable_air_quality": "2A - Acceptable air quality",
"2b_acceptable_air_quality": "2B - Acceptable air quality",
"3_green": "3 - Green",
"3_yellow": "3 - Yellow",
"3a_aggravated_air_quality": "3A - Aggravated air quality",
"3b_bad_air_quality": "3B - Bad air quality",
"4_orange": "4 - Orange",
"4_yellow_watch": "4 - Yellow/Watch",
"5_orange_alert": "5 - Orange/Alert",
"5_red": "5 - Red",
"6_red_alert": "6 - Red/Alert+",
"10_33": "10-33% of guideline",
"33_66": "33-66% of guideline",
@@ -137,6 +132,7 @@
"heavily_polluted": "Heavily polluted",
"heavy_air_pollution": "Heavy air pollution",
"high_air_pollution": "High air pollution",
"high_air_quality": "High air pollution",
"high_health_risk": "High health risk",
"horrible_air_quality": "Horrible air quality",
"light_air_pollution": "Light air pollution",
@@ -169,18 +165,20 @@
"slightly_polluted": "Slightly polluted",
"sufficient_air_quality": "Sufficient air quality",
"unfavorable_air_quality": "Unfavorable air quality",
"unfavorable_air_quality_for_sensitive_groups": "Unfavorable air quality for sensitive groups",
"unfavorable_sensitive": "Unfavorable air quality for sensitive groups",
"unhealthy_air_quality": "Unhealthy air quality",
"unhealthy_sensitive": "Unhealthy air quality for sensitive groups",
"unsatisfactory_air_quality": "Unsatisfactory air quality",
"very_bad_air_quality": "Very bad air quality",
"very_good_air_quality": "Very good air quality",
"very_high_air_pollution": "Very high air pollution",
"very_high_air_quality": "Very High air pollution",
"very_high_health_risk": "Very high health risk",
"very_low_air_pollution": "Very low air pollution",
"very_polluted": "Very polluted",
"very_poor_air_quality": "Very poor air quality",
"very_unfavorable_air_quality": "Very unfavorable air quality",
"very_unhealthy": "Very unhealthy air quality",
"very_unhealthy_air_quality": "Very unhealthy air quality",
"warning_air_pollution": "Warning level air pollution"
}

View File

@@ -908,21 +908,12 @@ class StartStopTrait(_Trait):
}
if domain in COVER_VALVE_DOMAINS:
assumed_state_or_set_position = bool(
(
self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
& COVER_VALVE_SET_POSITION_FEATURE[domain]
)
or self.state.attributes.get(ATTR_ASSUMED_STATE)
)
return {
"isRunning": state
in (
COVER_VALVE_STATES[domain]["closing"],
COVER_VALVE_STATES[domain]["opening"],
)
or assumed_state_or_set_position
}
raise NotImplementedError(f"Unsupported domain {domain}")
@@ -984,23 +975,11 @@ class StartStopTrait(_Trait):
"""Execute a StartStop command."""
domain = self.state.domain
if command == COMMAND_START_STOP:
assumed_state_or_set_position = bool(
(
self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
& COVER_VALVE_SET_POSITION_FEATURE[domain]
)
or self.state.attributes.get(ATTR_ASSUMED_STATE)
)
if params["start"] is False:
if (
self.state.state
in (
COVER_VALVE_STATES[domain]["closing"],
COVER_VALVE_STATES[domain]["opening"],
)
or assumed_state_or_set_position
):
if self.state.state in (
COVER_VALVE_STATES[domain]["closing"],
COVER_VALVE_STATES[domain]["opening"],
) or self.state.attributes.get(ATTR_ASSUMED_STATE):
await self.hass.services.async_call(
domain,
SERVICE_STOP_COVER_VALVE[domain],
@@ -1013,14 +992,7 @@ class StartStopTrait(_Trait):
ERR_ALREADY_STOPPED,
f"{FRIENDLY_DOMAIN[domain]} is already stopped",
)
elif (
self.state.state
in (
COVER_VALVE_STATES[domain]["open"],
COVER_VALVE_STATES[domain]["closed"],
)
or assumed_state_or_set_position
):
else:
await self.hass.services.async_call(
domain,
SERVICE_TOGGLE_COVER_VALVE[domain],

View File

@@ -149,7 +149,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
}
),
supports_response=SupportsResponse.ONLY,
description_placeholders={"example_image_path": "/config/www/image.jpg"},
)
return True

View File

@@ -23,7 +23,7 @@ CONF_CHAT_MODEL = "chat_model"
RECOMMENDED_CHAT_MODEL = "models/gemini-2.5-flash"
RECOMMENDED_STT_MODEL = RECOMMENDED_CHAT_MODEL
RECOMMENDED_TTS_MODEL = "models/gemini-2.5-flash-preview-tts"
RECOMMENDED_IMAGE_MODEL = "models/gemini-2.5-flash-image"
RECOMMENDED_IMAGE_MODEL = "models/gemini-2.5-flash-image-preview"
CONF_TEMPERATURE = "temperature"
RECOMMENDED_TEMPERATURE = 1.0
CONF_TOP_P = "top_p"

View File

@@ -162,7 +162,7 @@
"fields": {
"filenames": {
"description": "Attachments to add to the prompt (images, PDFs, etc)",
"example": "{example_image_path}",
"example": "/config/www/image.jpg",
"name": "Attachment filenames"
},
"prompt": {

View File

@@ -159,5 +159,4 @@ def async_setup_services(hass: HomeAssistant) -> None:
_async_handle_upload,
schema=UPLOAD_SERVICE_SCHEMA,
supports_response=SupportsResponse.OPTIONAL,
description_placeholders={"example_image_path": "/config/www/image.jpg"},
)

View File

@@ -92,7 +92,7 @@
},
"filename": {
"description": "Path to the image or video to upload.",
"example": "{example_image_path}",
"example": "/config/www/image.jpg",
"name": "Filename"
}
},

View File

@@ -4,7 +4,6 @@
"codeowners": [],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/google_translate",
"integration_type": "service",
"iot_class": "cloud_push",
"loggers": ["gtts"],
"requirements": ["gTTS==2.5.3"]

View File

@@ -18,12 +18,10 @@ from homeassistant.components.notify import (
SERVICE_SEND_MESSAGE,
BaseNotificationService,
NotifyEntity,
NotifyEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_SUPPORTED_FEATURES,
CONF_ACTION,
CONF_ENTITIES,
CONF_SERVICE,
@@ -175,23 +173,14 @@ class NotifyGroup(GroupEntity, NotifyEntity):
async def async_send_message(self, message: str, title: str | None = None) -> None:
"""Send a message to all members of the group."""
data = {
ATTR_MESSAGE: message,
ATTR_ENTITY_ID: self._entity_ids,
}
# add title only if supported and provided
if (
title is not None
and self._attr_supported_features & NotifyEntityFeature.TITLE
):
data[ATTR_TITLE] = title
await self.hass.services.async_call(
NOTIFY_DOMAIN,
SERVICE_SEND_MESSAGE,
data,
{
ATTR_MESSAGE: message,
ATTR_TITLE: title,
ATTR_ENTITY_ID: self._entity_ids,
},
blocking=True,
context=self._context,
)
@@ -205,15 +194,3 @@ class NotifyGroup(GroupEntity, NotifyEntity):
for entity_id in self._entity_ids
if (state := self.hass.states.get(entity_id)) is not None
)
# Support title if all members support it
self._attr_supported_features |= NotifyEntityFeature.TITLE
for entity_id in self._entity_ids:
state = self.hass.states.get(entity_id)
if (
state is None
or not state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
& NotifyEntityFeature.TITLE
):
self._attr_supported_features &= ~NotifyEntityFeature.TITLE
break

Some files were not shown because too many files have changed in this diff Show More