This commit is contained in:
Franck Nijhof 2024-06-07 21:20:44 +02:00 committed by GitHub
commit b28cdcfc49
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
60 changed files with 780 additions and 332 deletions

View File

@ -134,8 +134,15 @@ COOLDOWN_TIME = 60
DEBUGGER_INTEGRATIONS = {"debugpy"} DEBUGGER_INTEGRATIONS = {"debugpy"}
# Core integrations are unconditionally loaded
CORE_INTEGRATIONS = {"homeassistant", "persistent_notification"} CORE_INTEGRATIONS = {"homeassistant", "persistent_notification"}
LOGGING_INTEGRATIONS = {
# Integrations that are loaded right after the core is set up
LOGGING_AND_HTTP_DEPS_INTEGRATIONS = {
# isal is loaded right away before `http` to ensure if its
# enabled, that `isal` is up to date.
"isal",
# Set log levels # Set log levels
"logger", "logger",
# Error logging # Error logging
@ -214,8 +221,8 @@ CRITICAL_INTEGRATIONS = {
} }
SETUP_ORDER = ( SETUP_ORDER = (
# Load logging as soon as possible # Load logging and http deps as soon as possible
("logging", LOGGING_INTEGRATIONS), ("logging, http deps", LOGGING_AND_HTTP_DEPS_INTEGRATIONS),
# Setup frontend and recorder # Setup frontend and recorder
("frontend, recorder", {*FRONTEND_INTEGRATIONS, *RECORDER_INTEGRATIONS}), ("frontend, recorder", {*FRONTEND_INTEGRATIONS, *RECORDER_INTEGRATIONS}),
# Start up debuggers. Start these first in case they want to wait. # Start up debuggers. Start these first in case they want to wait.

View File

@ -43,6 +43,7 @@ class AgentBaseStation(AlarmControlPanelEntity):
| AlarmControlPanelEntityFeature.ARM_AWAY | AlarmControlPanelEntityFeature.ARM_AWAY
| AlarmControlPanelEntityFeature.ARM_NIGHT | AlarmControlPanelEntityFeature.ARM_NIGHT
) )
_attr_code_arm_required = False
_attr_has_entity_name = True _attr_has_entity_name = True
_attr_name = None _attr_name = None

View File

@ -1,6 +1,6 @@
{ {
"domain": "airgradient", "domain": "airgradient",
"name": "Airgradient", "name": "AirGradient",
"codeowners": ["@airgradienthq", "@joostlek"], "codeowners": ["@airgradienthq", "@joostlek"],
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/airgradient", "documentation": "https://www.home-assistant.io/integrations/airgradient",

View File

@ -103,6 +103,7 @@ SENSOR_TYPES: tuple[AirGradientSensorEntityDescription, ...] = (
AirGradientSensorEntityDescription( AirGradientSensorEntityDescription(
key="pm003", key="pm003",
translation_key="pm003_count", translation_key="pm003_count",
native_unit_of_measurement="particles/dL",
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda status: status.pm003_count, value_fn=lambda status: status.pm003_count,
), ),

View File

@ -48,7 +48,7 @@
"name": "Nitrogen index" "name": "Nitrogen index"
}, },
"pm003_count": { "pm003_count": {
"name": "PM0.3 count" "name": "PM0.3"
}, },
"raw_total_volatile_organic_component": { "raw_total_volatile_organic_component": {
"name": "Raw total VOC" "name": "Raw total VOC"

View File

@ -5,12 +5,13 @@
"title": "Setup your Azure Data Explorer integration", "title": "Setup your Azure Data Explorer integration",
"description": "Enter connection details.", "description": "Enter connection details.",
"data": { "data": {
"clusteringesturi": "Cluster Ingest URI", "cluster_ingest_uri": "Cluster ingest URI",
"database": "Database name", "database": "Database name",
"table": "Table name", "table": "Table name",
"client_id": "Client ID", "client_id": "Client ID",
"client_secret": "Client secret", "client_secret": "Client secret",
"authority_id": "Authority ID" "authority_id": "Authority ID",
"use_queued_ingestion": "Use queued ingestion"
} }
} }
}, },

View File

@ -46,6 +46,7 @@ class BlinkSyncModuleHA(
"""Representation of a Blink Alarm Control Panel.""" """Representation of a Blink Alarm Control Panel."""
_attr_supported_features = AlarmControlPanelEntityFeature.ARM_AWAY _attr_supported_features = AlarmControlPanelEntityFeature.ARM_AWAY
_attr_code_arm_required = False
_attr_has_entity_name = True _attr_has_entity_name = True
_attr_name = None _attr_name = None

View File

@ -4,11 +4,10 @@ from __future__ import annotations
import voluptuous as vol import voluptuous as vol
from homeassistant.core import HomeAssistant, State from homeassistant.core import HomeAssistant
from homeassistant.helpers import intent from homeassistant.helpers import intent
from homeassistant.helpers.entity_component import EntityComponent
from . import DOMAIN, ClimateEntity from . import DOMAIN
INTENT_GET_TEMPERATURE = "HassClimateGetTemperature" INTENT_GET_TEMPERATURE = "HassClimateGetTemperature"
@ -23,7 +22,10 @@ class GetTemperatureIntent(intent.IntentHandler):
intent_type = INTENT_GET_TEMPERATURE intent_type = INTENT_GET_TEMPERATURE
description = "Gets the current temperature of a climate device or entity" description = "Gets the current temperature of a climate device or entity"
slot_schema = {vol.Optional("area"): str, vol.Optional("name"): str} slot_schema = {
vol.Optional("area"): intent.non_empty_string,
vol.Optional("name"): intent.non_empty_string,
}
platforms = {DOMAIN} platforms = {DOMAIN}
async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse: async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
@ -31,74 +33,24 @@ class GetTemperatureIntent(intent.IntentHandler):
hass = intent_obj.hass hass = intent_obj.hass
slots = self.async_validate_slots(intent_obj.slots) slots = self.async_validate_slots(intent_obj.slots)
component: EntityComponent[ClimateEntity] = hass.data[DOMAIN] name: str | None = None
entities: list[ClimateEntity] = list(component.entities) if "name" in slots:
climate_entity: ClimateEntity | None = None name = slots["name"]["value"]
climate_state: State | None = None
if not entities: area: str | None = None
raise intent.IntentHandleError("No climate entities") if "area" in slots:
area = slots["area"]["value"]
name_slot = slots.get("name", {}) match_constraints = intent.MatchTargetsConstraints(
entity_name: str | None = name_slot.get("value") name=name, area_name=area, domains=[DOMAIN], assistant=intent_obj.assistant
entity_text: str | None = name_slot.get("text")
area_slot = slots.get("area", {})
area_id = area_slot.get("value")
if area_id:
# Filter by area and optionally name
area_name = area_slot.get("text")
for maybe_climate in intent.async_match_states(
hass, name=entity_name, area_name=area_id, domains=[DOMAIN]
):
climate_state = maybe_climate
break
if climate_state is None:
raise intent.NoStatesMatchedError(
reason=intent.MatchFailedReason.AREA,
name=entity_text or entity_name,
area=area_name or area_id,
floor=None,
domains={DOMAIN},
device_classes=None,
) )
match_result = intent.async_match_targets(hass, match_constraints)
climate_entity = component.get_entity(climate_state.entity_id) if not match_result.is_match:
elif entity_name: raise intent.MatchFailedError(
# Filter by name result=match_result, constraints=match_constraints
for maybe_climate in intent.async_match_states(
hass, name=entity_name, domains=[DOMAIN]
):
climate_state = maybe_climate
break
if climate_state is None:
raise intent.NoStatesMatchedError(
reason=intent.MatchFailedReason.NAME,
name=entity_name,
area=None,
floor=None,
domains={DOMAIN},
device_classes=None,
) )
climate_entity = component.get_entity(climate_state.entity_id)
else:
# First entity
climate_entity = entities[0]
climate_state = hass.states.get(climate_entity.entity_id)
assert climate_entity is not None
if climate_state is None:
raise intent.IntentHandleError(f"No state for {climate_entity.name}")
assert climate_state is not None
response = intent_obj.create_response() response = intent_obj.create_response()
response.response_type = intent.IntentResponseType.QUERY_ANSWER response.response_type = intent.IntentResponseType.QUERY_ANSWER
response.async_set_states(matched_states=[climate_state]) response.async_set_states(matched_states=match_result.states)
return response return response

View File

@ -429,8 +429,15 @@ class DefaultAgent(ConversationEntity):
intent_context=intent_context, intent_context=intent_context,
language=language, language=language,
): ):
if ("name" in result.entities) and ( # Prioritize results with a "name" slot, but still prefer ones with
not result.entities["name"].is_wildcard # more literal text matched.
if (
("name" in result.entities)
and (not result.entities["name"].is_wildcard)
and (
(name_result is None)
or (result.text_chunks_matched > name_result.text_chunks_matched)
)
): ):
name_result = result name_result = result

View File

@ -67,6 +67,7 @@ class EgardiaAlarm(AlarmControlPanelEntity):
"""Representation of a Egardia alarm.""" """Representation of a Egardia alarm."""
_attr_state: str | None _attr_state: str | None
_attr_code_arm_required = False
_attr_supported_features = ( _attr_supported_features = (
AlarmControlPanelEntityFeature.ARM_HOME AlarmControlPanelEntityFeature.ARM_HOME
| AlarmControlPanelEntityFeature.ARM_AWAY | AlarmControlPanelEntityFeature.ARM_AWAY

View File

@ -165,7 +165,7 @@ class GoogleAssistantConversationAgent(conversation.AbstractConversationAgent):
await session.async_ensure_token_valid() await session.async_ensure_token_valid()
self.assistant = None self.assistant = None
if not self.assistant or user_input.language != self.language: if not self.assistant or user_input.language != self.language:
credentials = Credentials(session.token[CONF_ACCESS_TOKEN]) credentials = Credentials(session.token[CONF_ACCESS_TOKEN]) # type: ignore[no-untyped-call]
self.language = user_input.language self.language = user_input.language
self.assistant = TextAssistant(credentials, self.language) self.assistant = TextAssistant(credentials, self.language)

View File

@ -72,7 +72,7 @@ async def async_send_text_commands(
entry.async_start_reauth(hass) entry.async_start_reauth(hass)
raise raise
credentials = Credentials(session.token[CONF_ACCESS_TOKEN]) credentials = Credentials(session.token[CONF_ACCESS_TOKEN]) # type: ignore[no-untyped-call]
language_code = entry.options.get(CONF_LANGUAGE_CODE, default_language_code(hass)) language_code = entry.options.get(CONF_LANGUAGE_CODE, default_language_code(hass))
with TextAssistant( with TextAssistant(
credentials, language_code, audio_out=bool(media_players) credentials, language_code, audio_out=bool(media_players)

View File

@ -93,7 +93,9 @@ async def async_setup_service(hass: HomeAssistant) -> None:
def _append_to_sheet(call: ServiceCall, entry: ConfigEntry) -> None: def _append_to_sheet(call: ServiceCall, entry: ConfigEntry) -> None:
"""Run append in the executor.""" """Run append in the executor."""
service = Client(Credentials(entry.data[CONF_TOKEN][CONF_ACCESS_TOKEN])) service = Client(
Credentials(entry.data[CONF_TOKEN][CONF_ACCESS_TOKEN]) # type: ignore[no-untyped-call]
)
try: try:
sheet = service.open_by_key(entry.unique_id) sheet = service.open_by_key(entry.unique_id)
except RefreshError: except RefreshError:

View File

@ -61,7 +61,9 @@ class OAuth2FlowHandler(
async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult: async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult:
"""Create an entry for the flow, or update existing entry.""" """Create an entry for the flow, or update existing entry."""
service = Client(Credentials(data[CONF_TOKEN][CONF_ACCESS_TOKEN])) service = Client(
Credentials(data[CONF_TOKEN][CONF_ACCESS_TOKEN]) # type: ignore[no-untyped-call]
)
if self.reauth_entry: if self.reauth_entry:
_LOGGER.debug("service.open_by_key") _LOGGER.debug("service.open_by_key")

View File

@ -267,15 +267,14 @@ class SupervisorIssues:
placeholders = {PLACEHOLDER_KEY_REFERENCE: issue.reference} placeholders = {PLACEHOLDER_KEY_REFERENCE: issue.reference}
if issue.key == ISSUE_KEY_ADDON_DETACHED_ADDON_MISSING: if issue.key == ISSUE_KEY_ADDON_DETACHED_ADDON_MISSING:
placeholders[PLACEHOLDER_KEY_ADDON_URL] = (
f"/hassio/addon/{issue.reference}"
)
addons = get_addons_info(self._hass) addons = get_addons_info(self._hass)
if addons and issue.reference in addons: if addons and issue.reference in addons:
placeholders[PLACEHOLDER_KEY_ADDON] = addons[issue.reference][ placeholders[PLACEHOLDER_KEY_ADDON] = addons[issue.reference][
"name" "name"
] ]
if "url" in addons[issue.reference]:
placeholders[PLACEHOLDER_KEY_ADDON_URL] = addons[
issue.reference
]["url"]
else: else:
placeholders[PLACEHOLDER_KEY_ADDON] = issue.reference placeholders[PLACEHOLDER_KEY_ADDON] = issue.reference

View File

@ -51,6 +51,7 @@ class HiveAlarmControlPanelEntity(HiveEntity, AlarmControlPanelEntity):
| AlarmControlPanelEntityFeature.ARM_AWAY | AlarmControlPanelEntityFeature.ARM_AWAY
| AlarmControlPanelEntityFeature.TRIGGER | AlarmControlPanelEntityFeature.TRIGGER
) )
_attr_code_arm_required = False
async def async_alarm_disarm(self, code: str | None = None) -> None: async def async_alarm_disarm(self, code: str | None = None) -> None:
"""Send disarm command.""" """Send disarm command."""

View File

@ -5,5 +5,5 @@
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/holiday", "documentation": "https://www.home-assistant.io/integrations/holiday",
"iot_class": "local_polling", "iot_class": "local_polling",
"requirements": ["holidays==0.49", "babel==2.13.1"] "requirements": ["holidays==0.50", "babel==2.13.1"]
} }

View File

@ -1,7 +1,6 @@
{ {
"domain": "http", "domain": "http",
"name": "HTTP", "name": "HTTP",
"after_dependencies": ["isal"],
"codeowners": ["@home-assistant/core"], "codeowners": ["@home-assistant/core"],
"documentation": "https://www.home-assistant.io/integrations/http", "documentation": "https://www.home-assistant.io/integrations/http",
"integration_type": "system", "integration_type": "system",

View File

@ -35,7 +35,7 @@ class HumidityHandler(intent.IntentHandler):
intent_type = INTENT_HUMIDITY intent_type = INTENT_HUMIDITY
description = "Set desired humidity level" description = "Set desired humidity level"
slot_schema = { slot_schema = {
vol.Required("name"): cv.string, vol.Required("name"): intent.non_empty_string,
vol.Required("humidity"): vol.All(vol.Coerce(int), vol.Range(0, 100)), vol.Required("humidity"): vol.All(vol.Coerce(int), vol.Range(0, 100)),
} }
platforms = {DOMAIN} platforms = {DOMAIN}
@ -44,18 +44,19 @@ class HumidityHandler(intent.IntentHandler):
"""Handle the hass intent.""" """Handle the hass intent."""
hass = intent_obj.hass hass = intent_obj.hass
slots = self.async_validate_slots(intent_obj.slots) slots = self.async_validate_slots(intent_obj.slots)
states = list(
intent.async_match_states( match_constraints = intent.MatchTargetsConstraints(
hass,
name=slots["name"]["value"], name=slots["name"]["value"],
states=hass.states.async_all(DOMAIN), domains=[DOMAIN],
assistant=intent_obj.assistant,
) )
match_result = intent.async_match_targets(hass, match_constraints)
if not match_result.is_match:
raise intent.MatchFailedError(
result=match_result, constraints=match_constraints
) )
if not states: state = match_result.states[0]
raise intent.IntentHandleError("No entities matched")
state = states[0]
service_data = {ATTR_ENTITY_ID: state.entity_id} service_data = {ATTR_ENTITY_ID: state.entity_id}
humidity = slots["humidity"]["value"] humidity = slots["humidity"]["value"]
@ -89,7 +90,7 @@ class SetModeHandler(intent.IntentHandler):
intent_type = INTENT_MODE intent_type = INTENT_MODE
description = "Set humidifier mode" description = "Set humidifier mode"
slot_schema = { slot_schema = {
vol.Required("name"): cv.string, vol.Required("name"): intent.non_empty_string,
vol.Required("mode"): cv.string, vol.Required("mode"): cv.string,
} }
platforms = {DOMAIN} platforms = {DOMAIN}
@ -98,18 +99,18 @@ class SetModeHandler(intent.IntentHandler):
"""Handle the hass intent.""" """Handle the hass intent."""
hass = intent_obj.hass hass = intent_obj.hass
slots = self.async_validate_slots(intent_obj.slots) slots = self.async_validate_slots(intent_obj.slots)
states = list( match_constraints = intent.MatchTargetsConstraints(
intent.async_match_states(
hass,
name=slots["name"]["value"], name=slots["name"]["value"],
states=hass.states.async_all(DOMAIN), domains=[DOMAIN],
assistant=intent_obj.assistant,
) )
match_result = intent.async_match_targets(hass, match_constraints)
if not match_result.is_match:
raise intent.MatchFailedError(
result=match_result, constraints=match_constraints
) )
if not states: state = match_result.states[0]
raise intent.IntentHandleError("No entities matched")
state = states[0]
service_data = {ATTR_ENTITY_ID: state.entity_id} service_data = {ATTR_ENTITY_ID: state.entity_id}
intent.async_test_feature(state, HumidifierEntityFeature.MODES, "modes") intent.async_test_feature(state, HumidifierEntityFeature.MODES, "modes")

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/hydrawise", "documentation": "https://www.home-assistant.io/integrations/hydrawise",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["pydrawise"], "loggers": ["pydrawise"],
"requirements": ["pydrawise==2024.6.2"] "requirements": ["pydrawise==2024.6.3"]
} }

View File

@ -37,6 +37,7 @@ class IAlarmPanel(
AlarmControlPanelEntityFeature.ARM_HOME AlarmControlPanelEntityFeature.ARM_HOME
| AlarmControlPanelEntityFeature.ARM_AWAY | AlarmControlPanelEntityFeature.ARM_AWAY
) )
_attr_code_arm_required = False
def __init__(self, coordinator: IAlarmDataUpdateCoordinator) -> None: def __init__(self, coordinator: IAlarmDataUpdateCoordinator) -> None:
"""Create the entity with a DataUpdateCoordinator.""" """Create the entity with a DataUpdateCoordinator."""

View File

@ -195,13 +195,13 @@ class ImapMessage:
): ):
message_untyped_text = str(part.get_payload()) message_untyped_text = str(part.get_payload())
if message_text is not None: if message_text is not None and message_text.strip():
return message_text return message_text
if message_html is not None: if message_html:
return message_html return message_html
if message_untyped_text is not None: if message_untyped_text:
return message_untyped_text return message_untyped_text
return str(self.email_message.get_payload()) return str(self.email_message.get_payload())

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/imgw_pib", "documentation": "https://www.home-assistant.io/integrations/imgw_pib",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": ["imgw_pib==1.0.1"] "requirements": ["imgw_pib==1.0.4"]
} }

View File

@ -283,14 +283,11 @@ class KNXClimate(KnxEntity, ClimateEntity):
) )
if knx_controller_mode in self._device.mode.controller_modes: if knx_controller_mode in self._device.mode.controller_modes:
await self._device.mode.set_controller_mode(knx_controller_mode) await self._device.mode.set_controller_mode(knx_controller_mode)
self.async_write_ha_state()
return
if self._device.supports_on_off: if self._device.supports_on_off:
if hvac_mode == HVACMode.OFF: if hvac_mode == HVACMode.OFF:
await self._device.turn_off() await self._device.turn_off()
elif not self._device.is_on: elif not self._device.is_on:
# for default hvac mode, otherwise above would have triggered
await self._device.turn_on() await self._device.turn_on()
self.async_write_ha_state() self.async_write_ha_state()

View File

@ -48,6 +48,7 @@ class LupusecAlarm(LupusecDevice, AlarmControlPanelEntity):
AlarmControlPanelEntityFeature.ARM_HOME AlarmControlPanelEntityFeature.ARM_HOME
| AlarmControlPanelEntityFeature.ARM_AWAY | AlarmControlPanelEntityFeature.ARM_AWAY
) )
_attr_code_arm_required = False
def __init__( def __init__(
self, data: lupupy.Lupusec, device: lupupy.devices.LupusecAlarm, entry_id: str self, data: lupupy.Lupusec, device: lupupy.devices.LupusecAlarm, entry_id: str

View File

@ -57,7 +57,7 @@ class AsyncConfigEntryAuth(AbstractAuth):
# even when it is expired to fully hand off this responsibility and # even when it is expired to fully hand off this responsibility and
# know it is working at startup (then if not, fail loudly). # know it is working at startup (then if not, fail loudly).
token = self._oauth_session.token token = self._oauth_session.token
creds = Credentials( creds = Credentials( # type: ignore[no-untyped-call]
token=token["access_token"], token=token["access_token"],
refresh_token=token["refresh_token"], refresh_token=token["refresh_token"],
token_uri=OAUTH2_TOKEN, token_uri=OAUTH2_TOKEN,
@ -92,7 +92,7 @@ class AccessTokenAuthImpl(AbstractAuth):
async def async_get_creds(self) -> Credentials: async def async_get_creds(self) -> Credentials:
"""Return an OAuth credential for Pub/Sub Subscriber.""" """Return an OAuth credential for Pub/Sub Subscriber."""
return Credentials( return Credentials( # type: ignore[no-untyped-call]
token=self._access_token, token=self._access_token,
token_uri=OAUTH2_TOKEN, token_uri=OAUTH2_TOKEN,
scopes=SDM_SCOPES, scopes=SDM_SCOPES,

View File

@ -100,6 +100,7 @@ class NX584Alarm(AlarmControlPanelEntity):
AlarmControlPanelEntityFeature.ARM_HOME AlarmControlPanelEntityFeature.ARM_HOME
| AlarmControlPanelEntityFeature.ARM_AWAY | AlarmControlPanelEntityFeature.ARM_AWAY
) )
_attr_code_arm_required = False
def __init__(self, name: str, alarm_client: client.Client, url: str) -> None: def __init__(self, name: str, alarm_client: client.Client, url: str) -> None:
"""Init the nx584 alarm panel.""" """Init the nx584 alarm panel."""

View File

@ -240,6 +240,7 @@ class OverkizAlarmControlPanel(OverkizDescriptiveEntity, AlarmControlPanelEntity
"""Representation of an Overkiz Alarm Control Panel.""" """Representation of an Overkiz Alarm Control Panel."""
entity_description: OverkizAlarmDescription entity_description: OverkizAlarmDescription
_attr_code_arm_required = False
def __init__( def __init__(
self, self,

View File

@ -55,6 +55,7 @@ class MinutPointAlarmControl(AlarmControlPanelEntity):
"""The platform class required by Home Assistant.""" """The platform class required by Home Assistant."""
_attr_supported_features = AlarmControlPanelEntityFeature.ARM_AWAY _attr_supported_features = AlarmControlPanelEntityFeature.ARM_AWAY
_attr_code_arm_required = False
def __init__(self, point_client: MinutPointClient, home_id: str) -> None: def __init__(self, point_client: MinutPointClient, home_id: str) -> None:
"""Initialize the entity.""" """Initialize the entity."""

View File

@ -137,7 +137,7 @@ def _register_new_account(
configurator.request_done(hass, request_id) configurator.request_done(hass, request_id)
request_id = configurator.async_request_config( request_id = configurator.request_config(
hass, hass,
f"{DOMAIN} - {account_name}", f"{DOMAIN} - {account_name}",
callback=register_account_callback, callback=register_account_callback,

View File

@ -584,7 +584,9 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]):
raise UpdateFailed( raise UpdateFailed(
f"Sleeping device did not update within {self.sleep_period} seconds interval" f"Sleeping device did not update within {self.sleep_period} seconds interval"
) )
if self.device.connected:
async with self._connection_lock:
if self.device.connected: # Already connected
return return
if not await self._async_device_connect_task(): if not await self._async_device_connect_task():

View File

@ -8,6 +8,8 @@ from typing import Any
import pysnmp.hlapi.asyncio as hlapi import pysnmp.hlapi.asyncio as hlapi
from pysnmp.hlapi.asyncio import ( from pysnmp.hlapi.asyncio import (
CommunityData, CommunityData,
ObjectIdentity,
ObjectType,
UdpTransportTarget, UdpTransportTarget,
UsmUserData, UsmUserData,
getCmd, getCmd,
@ -63,7 +65,12 @@ from .const import (
MAP_PRIV_PROTOCOLS, MAP_PRIV_PROTOCOLS,
SNMP_VERSIONS, SNMP_VERSIONS,
) )
from .util import RequestArgsType, async_create_request_cmd_args from .util import (
CommandArgsType,
RequestArgsType,
async_create_command_cmd_args,
async_create_request_cmd_args,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -125,23 +132,23 @@ async def async_setup_platform(
discovery_info: DiscoveryInfoType | None = None, discovery_info: DiscoveryInfoType | None = None,
) -> None: ) -> None:
"""Set up the SNMP switch.""" """Set up the SNMP switch."""
name = config.get(CONF_NAME) name: str = config[CONF_NAME]
host = config.get(CONF_HOST) host: str = config[CONF_HOST]
port = config.get(CONF_PORT) port: int = config[CONF_PORT]
community = config.get(CONF_COMMUNITY) community = config.get(CONF_COMMUNITY)
baseoid: str = config[CONF_BASEOID] baseoid: str = config[CONF_BASEOID]
command_oid = config.get(CONF_COMMAND_OID) command_oid: str | None = config.get(CONF_COMMAND_OID)
command_payload_on = config.get(CONF_COMMAND_PAYLOAD_ON) command_payload_on: str | None = config.get(CONF_COMMAND_PAYLOAD_ON)
command_payload_off = config.get(CONF_COMMAND_PAYLOAD_OFF) command_payload_off: str | None = config.get(CONF_COMMAND_PAYLOAD_OFF)
version: str = config[CONF_VERSION] version: str = config[CONF_VERSION]
username = config.get(CONF_USERNAME) username = config.get(CONF_USERNAME)
authkey = config.get(CONF_AUTH_KEY) authkey = config.get(CONF_AUTH_KEY)
authproto: str = config[CONF_AUTH_PROTOCOL] authproto: str = config[CONF_AUTH_PROTOCOL]
privkey = config.get(CONF_PRIV_KEY) privkey = config.get(CONF_PRIV_KEY)
privproto: str = config[CONF_PRIV_PROTOCOL] privproto: str = config[CONF_PRIV_PROTOCOL]
payload_on = config.get(CONF_PAYLOAD_ON) payload_on: str = config[CONF_PAYLOAD_ON]
payload_off = config.get(CONF_PAYLOAD_OFF) payload_off: str = config[CONF_PAYLOAD_OFF]
vartype = config.get(CONF_VARTYPE) vartype: str = config[CONF_VARTYPE]
if version == "3": if version == "3":
if not authkey: if not authkey:
@ -159,9 +166,11 @@ async def async_setup_platform(
else: else:
auth_data = CommunityData(community, mpModel=SNMP_VERSIONS[version]) auth_data = CommunityData(community, mpModel=SNMP_VERSIONS[version])
transport = UdpTransportTarget((host, port))
request_args = await async_create_request_cmd_args( request_args = await async_create_request_cmd_args(
hass, auth_data, UdpTransportTarget((host, port)), baseoid hass, auth_data, transport, baseoid
) )
command_args = await async_create_command_cmd_args(hass, auth_data, transport)
async_add_entities( async_add_entities(
[ [
@ -177,6 +186,7 @@ async def async_setup_platform(
command_payload_off, command_payload_off,
vartype, vartype,
request_args, request_args,
command_args,
) )
], ],
True, True,
@ -188,21 +198,22 @@ class SnmpSwitch(SwitchEntity):
def __init__( def __init__(
self, self,
name, name: str,
host, host: str,
port, port: int,
baseoid, baseoid: str,
commandoid, commandoid: str | None,
payload_on, payload_on: str,
payload_off, payload_off: str,
command_payload_on, command_payload_on: str | None,
command_payload_off, command_payload_off: str | None,
vartype, vartype: str,
request_args, request_args: RequestArgsType,
command_args: CommandArgsType,
) -> None: ) -> None:
"""Initialize the switch.""" """Initialize the switch."""
self._name = name self._attr_name = name
self._baseoid = baseoid self._baseoid = baseoid
self._vartype = vartype self._vartype = vartype
@ -215,7 +226,8 @@ class SnmpSwitch(SwitchEntity):
self._payload_on = payload_on self._payload_on = payload_on
self._payload_off = payload_off self._payload_off = payload_off
self._target = UdpTransportTarget((host, port)) self._target = UdpTransportTarget((host, port))
self._request_args: RequestArgsType = request_args self._request_args = request_args
self._command_args = command_args
async def async_turn_on(self, **kwargs: Any) -> None: async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on the switch.""" """Turn on the switch."""
@ -226,7 +238,7 @@ class SnmpSwitch(SwitchEntity):
"""Turn off the switch.""" """Turn off the switch."""
await self._execute_command(self._command_payload_off) await self._execute_command(self._command_payload_off)
async def _execute_command(self, command): async def _execute_command(self, command: str) -> None:
# User did not set vartype and command is not a digit # User did not set vartype and command is not a digit
if self._vartype == "none" and not self._command_payload_on.isdigit(): if self._vartype == "none" and not self._command_payload_on.isdigit():
await self._set(command) await self._set(command)
@ -265,14 +277,12 @@ class SnmpSwitch(SwitchEntity):
self._state = None self._state = None
@property @property
def name(self): def is_on(self) -> bool | None:
"""Return the switch's name."""
return self._name
@property
def is_on(self):
"""Return true if switch is on; False if off. None if unknown.""" """Return true if switch is on; False if off. None if unknown."""
return self._state return self._state
async def _set(self, value): async def _set(self, value: Any) -> None:
await setCmd(*self._request_args, value) """Set the state of the switch."""
await setCmd(
*self._command_args, ObjectType(ObjectIdentity(self._commandoid), value)
)

View File

@ -25,6 +25,14 @@ DATA_SNMP_ENGINE = "snmp_engine"
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
type CommandArgsType = tuple[
SnmpEngine,
UsmUserData | CommunityData,
UdpTransportTarget | Udp6TransportTarget,
ContextData,
]
type RequestArgsType = tuple[ type RequestArgsType = tuple[
SnmpEngine, SnmpEngine,
UsmUserData | CommunityData, UsmUserData | CommunityData,
@ -34,20 +42,34 @@ type RequestArgsType = tuple[
] ]
async def async_create_command_cmd_args(
hass: HomeAssistant,
auth_data: UsmUserData | CommunityData,
target: UdpTransportTarget | Udp6TransportTarget,
) -> CommandArgsType:
"""Create command arguments.
The ObjectType needs to be created dynamically by the caller.
"""
engine = await async_get_snmp_engine(hass)
return (engine, auth_data, target, ContextData())
async def async_create_request_cmd_args( async def async_create_request_cmd_args(
hass: HomeAssistant, hass: HomeAssistant,
auth_data: UsmUserData | CommunityData, auth_data: UsmUserData | CommunityData,
target: UdpTransportTarget | Udp6TransportTarget, target: UdpTransportTarget | Udp6TransportTarget,
object_id: str, object_id: str,
) -> RequestArgsType: ) -> RequestArgsType:
"""Create request arguments.""" """Create request arguments.
return (
await async_get_snmp_engine(hass), The same ObjectType is used for all requests.
auth_data, """
target, engine, auth_data, target, context_data = await async_create_command_cmd_args(
ContextData(), hass, auth_data, target
ObjectType(ObjectIdentity(object_id)),
) )
object_type = ObjectType(ObjectIdentity(object_id))
return (engine, auth_data, target, context_data, object_type)
@singleton(DATA_SNMP_ENGINE) @singleton(DATA_SNMP_ENGINE)

View File

@ -62,6 +62,7 @@ class SpcAlarm(AlarmControlPanelEntity):
| AlarmControlPanelEntityFeature.ARM_AWAY | AlarmControlPanelEntityFeature.ARM_AWAY
| AlarmControlPanelEntityFeature.ARM_NIGHT | AlarmControlPanelEntityFeature.ARM_NIGHT
) )
_attr_code_arm_required = False
def __init__(self, area: Area, api: SpcWebGateway) -> None: def __init__(self, area: Area, api: SpcWebGateway) -> None:
"""Initialize the SPC alarm panel.""" """Initialize the SPC alarm panel."""

View File

@ -4,7 +4,6 @@ from __future__ import annotations
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import intent from homeassistant.helpers import intent
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from . import DOMAIN, TodoItem, TodoItemStatus, TodoListEntity from . import DOMAIN, TodoItem, TodoItemStatus, TodoListEntity
@ -22,7 +21,7 @@ class ListAddItemIntent(intent.IntentHandler):
intent_type = INTENT_LIST_ADD_ITEM intent_type = INTENT_LIST_ADD_ITEM
description = "Add item to a todo list" description = "Add item to a todo list"
slot_schema = {"item": cv.string, "name": cv.string} slot_schema = {"item": intent.non_empty_string, "name": intent.non_empty_string}
platforms = {DOMAIN} platforms = {DOMAIN}
async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse: async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
@ -37,18 +36,19 @@ class ListAddItemIntent(intent.IntentHandler):
target_list: TodoListEntity | None = None target_list: TodoListEntity | None = None
# Find matching list # Find matching list
for list_state in intent.async_match_states( match_constraints = intent.MatchTargetsConstraints(
hass, name=list_name, domains=[DOMAIN] name=list_name, domains=[DOMAIN], assistant=intent_obj.assistant
): )
target_list = component.get_entity(list_state.entity_id) match_result = intent.async_match_targets(hass, match_constraints)
if target_list is not None: if not match_result.is_match:
break raise intent.MatchFailedError(
result=match_result, constraints=match_constraints
)
target_list = component.get_entity(match_result.states[0].entity_id)
if target_list is None: if target_list is None:
raise intent.IntentHandleError(f"No to-do list: {list_name}") raise intent.IntentHandleError(f"No to-do list: {list_name}")
assert target_list is not None
# Add to list # Add to list
await target_list.async_create_todo_item( await target_list.async_create_todo_item(
TodoItem(summary=item, status=TodoItemStatus.NEEDS_ACTION) TodoItem(summary=item, status=TodoItemStatus.NEEDS_ACTION)

View File

@ -88,6 +88,7 @@ class TuyaAlarmEntity(TuyaEntity, AlarmControlPanelEntity):
"""Tuya Alarm Entity.""" """Tuya Alarm Entity."""
_attr_name = None _attr_name = None
_attr_code_arm_required = False
def __init__( def __init__(
self, self,

View File

@ -6,10 +6,8 @@ import voluptuous as vol
from homeassistant.core import HomeAssistant, State from homeassistant.core import HomeAssistant, State
from homeassistant.helpers import intent from homeassistant.helpers import intent
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_component import EntityComponent
from . import DOMAIN, WeatherEntity from . import DOMAIN
INTENT_GET_WEATHER = "HassGetWeather" INTENT_GET_WEATHER = "HassGetWeather"
@ -24,7 +22,7 @@ class GetWeatherIntent(intent.IntentHandler):
intent_type = INTENT_GET_WEATHER intent_type = INTENT_GET_WEATHER
description = "Gets the current weather" description = "Gets the current weather"
slot_schema = {vol.Optional("name"): cv.string} slot_schema = {vol.Optional("name"): intent.non_empty_string}
platforms = {DOMAIN} platforms = {DOMAIN}
async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse: async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
@ -32,43 +30,21 @@ class GetWeatherIntent(intent.IntentHandler):
hass = intent_obj.hass hass = intent_obj.hass
slots = self.async_validate_slots(intent_obj.slots) slots = self.async_validate_slots(intent_obj.slots)
weather: WeatherEntity | None = None
weather_state: State | None = None weather_state: State | None = None
component: EntityComponent[WeatherEntity] = hass.data[DOMAIN] name: str | None = None
entities = list(component.entities)
if "name" in slots: if "name" in slots:
# Named weather entity name = slots["name"]["value"]
weather_name = slots["name"]["value"]
# Find matching weather entity match_constraints = intent.MatchTargetsConstraints(
matching_states = intent.async_match_states( name=name, domains=[DOMAIN], assistant=intent_obj.assistant
hass, name=weather_name, domains=[DOMAIN]
) )
for maybe_weather_state in matching_states: match_result = intent.async_match_targets(hass, match_constraints)
weather = component.get_entity(maybe_weather_state.entity_id) if not match_result.is_match:
if weather is not None: raise intent.MatchFailedError(
weather_state = maybe_weather_state result=match_result, constraints=match_constraints
break
if weather is None:
raise intent.IntentHandleError(
f"No weather entity named {weather_name}"
) )
elif entities:
# First weather entity
weather = entities[0]
weather_name = weather.name
weather_state = hass.states.get(weather.entity_id)
if weather is None: weather_state = match_result.states[0]
raise intent.IntentHandleError("No weather entity")
if weather_state is None:
raise intent.IntentHandleError(f"No state for weather: {weather.name}")
assert weather is not None
assert weather_state is not None
# Create response # Create response
response = intent_obj.create_response() response = intent_obj.create_response()
@ -77,8 +53,8 @@ class GetWeatherIntent(intent.IntentHandler):
success_results=[ success_results=[
intent.IntentResponseTarget( intent.IntentResponseTarget(
type=intent.IntentResponseTargetType.ENTITY, type=intent.IntentResponseTargetType.ENTITY,
name=weather_name, name=weather_state.name,
id=weather.entity_id, id=weather_state.entity_id,
) )
] ]
) )

View File

@ -7,5 +7,5 @@
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["holidays"], "loggers": ["holidays"],
"quality_scale": "internal", "quality_scale": "internal",
"requirements": ["holidays==0.49"] "requirements": ["holidays==0.50"]
} }

View File

@ -54,6 +54,7 @@ class XiaomiGatewayAlarm(AlarmControlPanelEntity):
_attr_icon = "mdi:shield-home" _attr_icon = "mdi:shield-home"
_attr_supported_features = AlarmControlPanelEntityFeature.ARM_AWAY _attr_supported_features = AlarmControlPanelEntityFeature.ARM_AWAY
_attr_code_arm_required = False
def __init__( def __init__(
self, gateway_device, gateway_name, model, mac_address, gateway_device_id self, gateway_device, gateway_name, model, mac_address, gateway_device_id

View File

@ -24,7 +24,7 @@ if TYPE_CHECKING:
APPLICATION_NAME: Final = "HomeAssistant" APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2024 MAJOR_VERSION: Final = 2024
MINOR_VERSION: Final = 6 MINOR_VERSION: Final = 6
PATCH_VERSION: Final = "0" PATCH_VERSION: Final = "1"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0)

View File

@ -94,7 +94,7 @@
"iot_class": "local_polling" "iot_class": "local_polling"
}, },
"airgradient": { "airgradient": {
"name": "Airgradient", "name": "AirGradient",
"integration_type": "device", "integration_type": "device",
"config_flow": true, "config_flow": true,
"iot_class": "local_polling" "iot_class": "local_polling"

View File

@ -712,6 +712,7 @@ def async_match_states(
domains: Collection[str] | None = None, domains: Collection[str] | None = None,
device_classes: Collection[str] | None = None, device_classes: Collection[str] | None = None,
states: list[State] | None = None, states: list[State] | None = None,
assistant: str | None = None,
) -> Iterable[State]: ) -> Iterable[State]:
"""Simplified interface to async_match_targets that returns states matching the constraints.""" """Simplified interface to async_match_targets that returns states matching the constraints."""
result = async_match_targets( result = async_match_targets(
@ -722,6 +723,7 @@ def async_match_states(
floor_name=floor_name, floor_name=floor_name,
domains=domains, domains=domains,
device_classes=device_classes, device_classes=device_classes,
assistant=assistant,
), ),
states=states, states=states,
) )

View File

@ -39,7 +39,7 @@ ifaddr==0.2.0
Jinja2==3.1.4 Jinja2==3.1.4
lru-dict==1.3.0 lru-dict==1.3.0
mutagen==1.47.0 mutagen==1.47.0
orjson==3.10.3 orjson==3.9.15
packaging>=23.1 packaging>=23.1
paho-mqtt==1.6.1 paho-mqtt==1.6.1
Pillow==10.3.0 Pillow==10.3.0
@ -53,7 +53,7 @@ python-slugify==8.0.4
PyTurboJPEG==1.7.1 PyTurboJPEG==1.7.1
pyudev==0.24.1 pyudev==0.24.1
PyYAML==6.0.1 PyYAML==6.0.1
requests==2.31.0 requests==2.32.3
SQLAlchemy==2.0.30 SQLAlchemy==2.0.30
typing-extensions>=4.12.0,<5.0 typing-extensions>=4.12.0,<5.0
ulid-transform==0.9.0 ulid-transform==0.9.0

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "homeassistant" name = "homeassistant"
version = "2024.6.0" version = "2024.6.1"
license = {text = "Apache-2.0"} license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3." description = "Open-source home automation platform running on Python 3."
readme = "README.rst" readme = "README.rst"
@ -53,13 +53,13 @@ dependencies = [
"cryptography==42.0.5", "cryptography==42.0.5",
"Pillow==10.3.0", "Pillow==10.3.0",
"pyOpenSSL==24.1.0", "pyOpenSSL==24.1.0",
"orjson==3.10.3", "orjson==3.9.15",
"packaging>=23.1", "packaging>=23.1",
"pip>=21.3.1", "pip>=21.3.1",
"psutil-home-assistant==0.0.1", "psutil-home-assistant==0.0.1",
"python-slugify==8.0.4", "python-slugify==8.0.4",
"PyYAML==6.0.1", "PyYAML==6.0.1",
"requests==2.31.0", "requests==2.32.3",
"SQLAlchemy==2.0.30", "SQLAlchemy==2.0.30",
"typing-extensions>=4.12.0,<5.0", "typing-extensions>=4.12.0,<5.0",
"ulid-transform==0.9.0", "ulid-transform==0.9.0",

View File

@ -28,13 +28,13 @@ PyJWT==2.8.0
cryptography==42.0.5 cryptography==42.0.5
Pillow==10.3.0 Pillow==10.3.0
pyOpenSSL==24.1.0 pyOpenSSL==24.1.0
orjson==3.10.3 orjson==3.9.15
packaging>=23.1 packaging>=23.1
pip>=21.3.1 pip>=21.3.1
psutil-home-assistant==0.0.1 psutil-home-assistant==0.0.1
python-slugify==8.0.4 python-slugify==8.0.4
PyYAML==6.0.1 PyYAML==6.0.1
requests==2.31.0 requests==2.32.3
SQLAlchemy==2.0.30 SQLAlchemy==2.0.30
typing-extensions>=4.12.0,<5.0 typing-extensions>=4.12.0,<5.0
ulid-transform==0.9.0 ulid-transform==0.9.0

View File

@ -1084,7 +1084,7 @@ hole==0.8.0
# homeassistant.components.holiday # homeassistant.components.holiday
# homeassistant.components.workday # homeassistant.components.workday
holidays==0.49 holidays==0.50
# homeassistant.components.frontend # homeassistant.components.frontend
home-assistant-frontend==20240605.0 home-assistant-frontend==20240605.0
@ -1146,7 +1146,7 @@ iglo==1.2.7
ihcsdk==2.8.5 ihcsdk==2.8.5
# homeassistant.components.imgw_pib # homeassistant.components.imgw_pib
imgw_pib==1.0.1 imgw_pib==1.0.4
# homeassistant.components.incomfort # homeassistant.components.incomfort
incomfort-client==0.5.0 incomfort-client==0.5.0
@ -1794,7 +1794,7 @@ pydiscovergy==3.0.1
pydoods==1.0.2 pydoods==1.0.2
# homeassistant.components.hydrawise # homeassistant.components.hydrawise
pydrawise==2024.6.2 pydrawise==2024.6.3
# homeassistant.components.android_ip_webcam # homeassistant.components.android_ip_webcam
pydroid-ipcam==2.0.0 pydroid-ipcam==2.0.0

View File

@ -886,7 +886,7 @@ hole==0.8.0
# homeassistant.components.holiday # homeassistant.components.holiday
# homeassistant.components.workday # homeassistant.components.workday
holidays==0.49 holidays==0.50
# homeassistant.components.frontend # homeassistant.components.frontend
home-assistant-frontend==20240605.0 home-assistant-frontend==20240605.0
@ -933,7 +933,7 @@ idasen-ha==2.5.3
ifaddr==0.2.0 ifaddr==0.2.0
# homeassistant.components.imgw_pib # homeassistant.components.imgw_pib
imgw_pib==1.0.1 imgw_pib==1.0.4
# homeassistant.components.influxdb # homeassistant.components.influxdb
influxdb-client==1.24.0 influxdb-client==1.24.0
@ -1405,7 +1405,7 @@ pydexcom==0.2.3
pydiscovergy==3.0.1 pydiscovergy==3.0.1
# homeassistant.components.hydrawise # homeassistant.components.hydrawise
pydrawise==2024.6.2 pydrawise==2024.6.3
# homeassistant.components.android_ip_webcam # homeassistant.components.android_ip_webcam
pydroid-ipcam==2.0.0 pydroid-ipcam==2.0.0

View File

@ -150,7 +150,7 @@
'state': '1', 'state': '1',
}) })
# --- # ---
# name: test_all_entities[sensor.airgradient_pm0_3_count-entry] # name: test_all_entities[sensor.airgradient_pm0_3-entry]
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({
}), }),
@ -164,7 +164,7 @@
'disabled_by': None, 'disabled_by': None,
'domain': 'sensor', 'domain': 'sensor',
'entity_category': None, 'entity_category': None,
'entity_id': 'sensor.airgradient_pm0_3_count', 'entity_id': 'sensor.airgradient_pm0_3',
'has_entity_name': True, 'has_entity_name': True,
'hidden_by': None, 'hidden_by': None,
'icon': None, 'icon': None,
@ -176,23 +176,24 @@
}), }),
'original_device_class': None, 'original_device_class': None,
'original_icon': None, 'original_icon': None,
'original_name': 'PM0.3 count', 'original_name': 'PM0.3',
'platform': 'airgradient', 'platform': 'airgradient',
'previous_unique_id': None, 'previous_unique_id': None,
'supported_features': 0, 'supported_features': 0,
'translation_key': 'pm003_count', 'translation_key': 'pm003_count',
'unique_id': '84fce612f5b8-pm003', 'unique_id': '84fce612f5b8-pm003',
'unit_of_measurement': None, 'unit_of_measurement': 'particles/dL',
}) })
# --- # ---
# name: test_all_entities[sensor.airgradient_pm0_3_count-state] # name: test_all_entities[sensor.airgradient_pm0_3-state]
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'friendly_name': 'Airgradient PM0.3 count', 'friendly_name': 'Airgradient PM0.3',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'particles/dL',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.airgradient_pm0_3_count', 'entity_id': 'sensor.airgradient_pm0_3',
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,

View File

@ -1,21 +1,23 @@
"""Test climate intents.""" """Test climate intents."""
from collections.abc import Generator from collections.abc import Generator
from unittest.mock import patch
import pytest import pytest
from homeassistant.components import conversation
from homeassistant.components.climate import ( from homeassistant.components.climate import (
DOMAIN, DOMAIN,
ClimateEntity, ClimateEntity,
HVACMode, HVACMode,
intent as climate_intent, intent as climate_intent,
) )
from homeassistant.components.homeassistant.exposed_entities import async_expose_entity
from homeassistant.config_entries import ConfigEntry, ConfigFlow from homeassistant.config_entries import ConfigEntry, ConfigFlow
from homeassistant.const import Platform, UnitOfTemperature from homeassistant.const import Platform, UnitOfTemperature
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import area_registry as ar, entity_registry as er, intent from homeassistant.helpers import area_registry as ar, entity_registry as er, intent
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.setup import async_setup_component
from tests.common import ( from tests.common import (
MockConfigEntry, MockConfigEntry,
@ -113,6 +115,7 @@ async def test_get_temperature(
entity_registry: er.EntityRegistry, entity_registry: er.EntityRegistry,
) -> None: ) -> None:
"""Test HassClimateGetTemperature intent.""" """Test HassClimateGetTemperature intent."""
assert await async_setup_component(hass, "homeassistant", {})
await climate_intent.async_setup_intents(hass) await climate_intent.async_setup_intents(hass)
climate_1 = MockClimateEntity() climate_1 = MockClimateEntity()
@ -148,10 +151,14 @@ async def test_get_temperature(
# First climate entity will be selected (no area) # First climate entity will be selected (no area)
response = await intent.async_handle( response = await intent.async_handle(
hass, "test", climate_intent.INTENT_GET_TEMPERATURE, {} hass,
"test",
climate_intent.INTENT_GET_TEMPERATURE,
{},
assistant=conversation.DOMAIN,
) )
assert response.response_type == intent.IntentResponseType.QUERY_ANSWER assert response.response_type == intent.IntentResponseType.QUERY_ANSWER
assert len(response.matched_states) == 1 assert response.matched_states
assert response.matched_states[0].entity_id == climate_1.entity_id assert response.matched_states[0].entity_id == climate_1.entity_id
state = response.matched_states[0] state = response.matched_states[0]
assert state.attributes["current_temperature"] == 10.0 assert state.attributes["current_temperature"] == 10.0
@ -162,6 +169,7 @@ async def test_get_temperature(
"test", "test",
climate_intent.INTENT_GET_TEMPERATURE, climate_intent.INTENT_GET_TEMPERATURE,
{"area": {"value": bedroom_area.name}}, {"area": {"value": bedroom_area.name}},
assistant=conversation.DOMAIN,
) )
assert response.response_type == intent.IntentResponseType.QUERY_ANSWER assert response.response_type == intent.IntentResponseType.QUERY_ANSWER
assert len(response.matched_states) == 1 assert len(response.matched_states) == 1
@ -175,6 +183,7 @@ async def test_get_temperature(
"test", "test",
climate_intent.INTENT_GET_TEMPERATURE, climate_intent.INTENT_GET_TEMPERATURE,
{"name": {"value": "Climate 2"}}, {"name": {"value": "Climate 2"}},
assistant=conversation.DOMAIN,
) )
assert response.response_type == intent.IntentResponseType.QUERY_ANSWER assert response.response_type == intent.IntentResponseType.QUERY_ANSWER
assert len(response.matched_states) == 1 assert len(response.matched_states) == 1
@ -189,6 +198,7 @@ async def test_get_temperature(
"test", "test",
climate_intent.INTENT_GET_TEMPERATURE, climate_intent.INTENT_GET_TEMPERATURE,
{"area": {"value": office_area.name}}, {"area": {"value": office_area.name}},
assistant=conversation.DOMAIN,
) )
# Exception should contain details of what we tried to match # Exception should contain details of what we tried to match
@ -197,7 +207,7 @@ async def test_get_temperature(
constraints = error.value.constraints constraints = error.value.constraints
assert constraints.name is None assert constraints.name is None
assert constraints.area_name == office_area.name assert constraints.area_name == office_area.name
assert constraints.domains == {DOMAIN} assert constraints.domains and (set(constraints.domains) == {DOMAIN})
assert constraints.device_classes is None assert constraints.device_classes is None
# Check wrong name # Check wrong name
@ -214,7 +224,7 @@ async def test_get_temperature(
constraints = error.value.constraints constraints = error.value.constraints
assert constraints.name == "Does not exist" assert constraints.name == "Does not exist"
assert constraints.area_name is None assert constraints.area_name is None
assert constraints.domains == {DOMAIN} assert constraints.domains and (set(constraints.domains) == {DOMAIN})
assert constraints.device_classes is None assert constraints.device_classes is None
# Check wrong name with area # Check wrong name with area
@ -231,7 +241,7 @@ async def test_get_temperature(
constraints = error.value.constraints constraints = error.value.constraints
assert constraints.name == "Climate 1" assert constraints.name == "Climate 1"
assert constraints.area_name == bedroom_area.name assert constraints.area_name == bedroom_area.name
assert constraints.domains == {DOMAIN} assert constraints.domains and (set(constraints.domains) == {DOMAIN})
assert constraints.device_classes is None assert constraints.device_classes is None
@ -239,62 +249,190 @@ async def test_get_temperature_no_entities(
hass: HomeAssistant, hass: HomeAssistant,
) -> None: ) -> None:
"""Test HassClimateGetTemperature intent with no climate entities.""" """Test HassClimateGetTemperature intent with no climate entities."""
assert await async_setup_component(hass, "homeassistant", {})
await climate_intent.async_setup_intents(hass) await climate_intent.async_setup_intents(hass)
await create_mock_platform(hass, []) await create_mock_platform(hass, [])
with pytest.raises(intent.IntentHandleError): with pytest.raises(intent.MatchFailedError) as err:
await intent.async_handle( await intent.async_handle(
hass, "test", climate_intent.INTENT_GET_TEMPERATURE, {} hass,
"test",
climate_intent.INTENT_GET_TEMPERATURE,
{},
assistant=conversation.DOMAIN,
) )
assert err.value.result.no_match_reason == intent.MatchFailedReason.DOMAIN
async def test_get_temperature_no_state( async def test_not_exposed(
hass: HomeAssistant, hass: HomeAssistant,
area_registry: ar.AreaRegistry, area_registry: ar.AreaRegistry,
entity_registry: er.EntityRegistry, entity_registry: er.EntityRegistry,
) -> None: ) -> None:
"""Test HassClimateGetTemperature intent when states are missing.""" """Test HassClimateGetTemperature intent when entities aren't exposed."""
assert await async_setup_component(hass, "homeassistant", {})
await climate_intent.async_setup_intents(hass) await climate_intent.async_setup_intents(hass)
climate_1 = MockClimateEntity() climate_1 = MockClimateEntity()
climate_1._attr_name = "Climate 1" climate_1._attr_name = "Climate 1"
climate_1._attr_unique_id = "1234" climate_1._attr_unique_id = "1234"
climate_1._attr_current_temperature = 10.0
entity_registry.async_get_or_create( entity_registry.async_get_or_create(
DOMAIN, "test", "1234", suggested_object_id="climate_1" DOMAIN, "test", "1234", suggested_object_id="climate_1"
) )
await create_mock_platform(hass, [climate_1]) climate_2 = MockClimateEntity()
climate_2._attr_name = "Climate 2"
climate_2._attr_unique_id = "5678"
climate_2._attr_current_temperature = 22.0
entity_registry.async_get_or_create(
DOMAIN, "test", "5678", suggested_object_id="climate_2"
)
await create_mock_platform(hass, [climate_1, climate_2])
# Add climate entities to same area
living_room_area = area_registry.async_create(name="Living Room") living_room_area = area_registry.async_create(name="Living Room")
bedroom_area = area_registry.async_create(name="Bedroom")
entity_registry.async_update_entity( entity_registry.async_update_entity(
climate_1.entity_id, area_id=living_room_area.id climate_1.entity_id, area_id=living_room_area.id
) )
entity_registry.async_update_entity(
with ( climate_2.entity_id, area_id=living_room_area.id
patch("homeassistant.core.StateMachine.get", return_value=None),
pytest.raises(intent.IntentHandleError),
):
await intent.async_handle(
hass, "test", climate_intent.INTENT_GET_TEMPERATURE, {}
) )
with ( # Should fail with empty name
patch("homeassistant.core.StateMachine.async_all", return_value=[]), with pytest.raises(intent.InvalidSlotInfo):
pytest.raises(intent.MatchFailedError) as error,
):
await intent.async_handle( await intent.async_handle(
hass, hass,
"test", "test",
climate_intent.INTENT_GET_TEMPERATURE, climate_intent.INTENT_GET_TEMPERATURE,
{"area": {"value": "Living Room"}}, {"name": {"value": ""}},
assistant=conversation.DOMAIN,
) )
# Exception should contain details of what we tried to match # Should fail with empty area
assert isinstance(error.value, intent.MatchFailedError) with pytest.raises(intent.InvalidSlotInfo):
assert error.value.result.no_match_reason == intent.MatchFailedReason.AREA await intent.async_handle(
constraints = error.value.constraints hass,
assert constraints.name is None "test",
assert constraints.area_name == "Living Room" climate_intent.INTENT_GET_TEMPERATURE,
assert constraints.domains == {DOMAIN} {"area": {"value": ""}},
assert constraints.device_classes is None assistant=conversation.DOMAIN,
)
# Expose second, hide first
async_expose_entity(hass, conversation.DOMAIN, climate_1.entity_id, False)
async_expose_entity(hass, conversation.DOMAIN, climate_2.entity_id, True)
# Second climate entity is exposed
response = await intent.async_handle(
hass,
"test",
climate_intent.INTENT_GET_TEMPERATURE,
{},
assistant=conversation.DOMAIN,
)
assert response.response_type == intent.IntentResponseType.QUERY_ANSWER
assert len(response.matched_states) == 1
assert response.matched_states[0].entity_id == climate_2.entity_id
# Using the area should work
response = await intent.async_handle(
hass,
"test",
climate_intent.INTENT_GET_TEMPERATURE,
{"area": {"value": living_room_area.name}},
assistant=conversation.DOMAIN,
)
assert response.response_type == intent.IntentResponseType.QUERY_ANSWER
assert len(response.matched_states) == 1
assert response.matched_states[0].entity_id == climate_2.entity_id
# Using the name of the exposed entity should work
response = await intent.async_handle(
hass,
"test",
climate_intent.INTENT_GET_TEMPERATURE,
{"name": {"value": climate_2.name}},
assistant=conversation.DOMAIN,
)
assert response.response_type == intent.IntentResponseType.QUERY_ANSWER
assert len(response.matched_states) == 1
assert response.matched_states[0].entity_id == climate_2.entity_id
# Using the name of the *unexposed* entity should fail
with pytest.raises(intent.MatchFailedError) as err:
await intent.async_handle(
hass,
"test",
climate_intent.INTENT_GET_TEMPERATURE,
{"name": {"value": climate_1.name}},
assistant=conversation.DOMAIN,
)
assert err.value.result.no_match_reason == intent.MatchFailedReason.NAME
# Expose first, hide second
async_expose_entity(hass, conversation.DOMAIN, climate_1.entity_id, True)
async_expose_entity(hass, conversation.DOMAIN, climate_2.entity_id, False)
# Second climate entity is exposed
response = await intent.async_handle(
hass,
"test",
climate_intent.INTENT_GET_TEMPERATURE,
{},
assistant=conversation.DOMAIN,
)
assert response.response_type == intent.IntentResponseType.QUERY_ANSWER
assert len(response.matched_states) == 1
assert response.matched_states[0].entity_id == climate_1.entity_id
# Wrong area name
with pytest.raises(intent.MatchFailedError) as err:
await intent.async_handle(
hass,
"test",
climate_intent.INTENT_GET_TEMPERATURE,
{"area": {"value": bedroom_area.name}},
assistant=conversation.DOMAIN,
)
assert err.value.result.no_match_reason == intent.MatchFailedReason.AREA
# Neither are exposed
async_expose_entity(hass, conversation.DOMAIN, climate_1.entity_id, False)
async_expose_entity(hass, conversation.DOMAIN, climate_2.entity_id, False)
with pytest.raises(intent.MatchFailedError) as err:
await intent.async_handle(
hass,
"test",
climate_intent.INTENT_GET_TEMPERATURE,
{},
assistant=conversation.DOMAIN,
)
assert err.value.result.no_match_reason == intent.MatchFailedReason.ASSISTANT
# Should fail with area
with pytest.raises(intent.MatchFailedError) as err:
await intent.async_handle(
hass,
"test",
climate_intent.INTENT_GET_TEMPERATURE,
{"area": {"value": living_room_area.name}},
assistant=conversation.DOMAIN,
)
assert err.value.result.no_match_reason == intent.MatchFailedReason.ASSISTANT
# Should fail with both names
for name in (climate_1.name, climate_2.name):
with pytest.raises(intent.MatchFailedError) as err:
await intent.async_handle(
hass,
"test",
climate_intent.INTENT_GET_TEMPERATURE,
{"name": {"value": name}},
assistant=conversation.DOMAIN,
)
assert err.value.result.no_match_reason == intent.MatchFailedReason.ASSISTANT

View File

@ -1,5 +1,7 @@
"""Test intents for the default agent.""" """Test intents for the default agent."""
from unittest.mock import patch
import pytest import pytest
from homeassistant.components import ( from homeassistant.components import (
@ -7,6 +9,7 @@ from homeassistant.components import (
cover, cover,
light, light,
media_player, media_player,
todo,
vacuum, vacuum,
valve, valve,
) )
@ -35,6 +38,27 @@ from homeassistant.setup import async_setup_component
from tests.common import async_mock_service from tests.common import async_mock_service
class MockTodoListEntity(todo.TodoListEntity):
"""Test todo list entity."""
def __init__(self, items: list[todo.TodoItem] | None = None) -> None:
"""Initialize entity."""
self._attr_todo_items = items or []
@property
def items(self) -> list[todo.TodoItem]:
"""Return the items in the To-do list."""
return self._attr_todo_items
async def async_create_todo_item(self, item: todo.TodoItem) -> None:
"""Add an item to the To-do list."""
self._attr_todo_items.append(item)
async def async_delete_todo_items(self, uids: list[str]) -> None:
"""Delete an item in the To-do list."""
self._attr_todo_items = [item for item in self.items if item.uid not in uids]
@pytest.fixture @pytest.fixture
async def init_components(hass: HomeAssistant): async def init_components(hass: HomeAssistant):
"""Initialize relevant components with empty configs.""" """Initialize relevant components with empty configs."""
@ -365,3 +389,27 @@ async def test_turn_floor_lights_on_off(
assert {s.entity_id for s in result.response.matched_states} == { assert {s.entity_id for s in result.response.matched_states} == {
bedroom_light.entity_id bedroom_light.entity_id
} }
async def test_todo_add_item_fr(
hass: HomeAssistant,
init_components,
) -> None:
"""Test that wildcard matches prioritize results with more literal text matched."""
assert await async_setup_component(hass, todo.DOMAIN, {})
hass.states.async_set("todo.liste_des_courses", 0, {})
with (
patch.object(hass.config, "language", "fr"),
patch(
"homeassistant.components.todo.intent.ListAddItemIntent.async_handle",
return_value=intent.IntentResponse(hass.config.language),
) as mock_handle,
):
await conversation.async_converse(
hass, "Ajoute de la farine a la liste des courses", None, Context(), None
)
mock_handle.assert_called_once()
assert mock_handle.call_args.args
intent_obj = mock_handle.call_args.args[0]
assert intent_obj.slots.get("item", {}).get("value", "").strip() == "farine"

View File

@ -577,6 +577,8 @@ async def test_async_get_users_from_store(tmpdir: py.path.local) -> None:
assert await async_get_users(hass) == ["agent_1"] assert await async_get_users(hass) == ["agent_1"]
await hass.async_stop()
VALID_STORE_DATA = json.dumps( VALID_STORE_DATA = json.dumps(
{ {

View File

@ -878,6 +878,6 @@ async def test_supervisor_issues_detached_addon_missing(
placeholders={ placeholders={
"reference": "test", "reference": "test",
"addon": "test", "addon": "test",
"addon_url": "https://github.com/home-assistant/addons/test", "addon_url": "/hassio/addon/test",
}, },
) )

View File

@ -2,6 +2,8 @@
import pytest import pytest
from homeassistant.components import conversation
from homeassistant.components.homeassistant.exposed_entities import async_expose_entity
from homeassistant.components.humidifier import ( from homeassistant.components.humidifier import (
ATTR_AVAILABLE_MODES, ATTR_AVAILABLE_MODES,
ATTR_HUMIDITY, ATTR_HUMIDITY,
@ -19,13 +21,22 @@ from homeassistant.const import (
STATE_ON, STATE_ON,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.intent import IntentHandleError, async_handle from homeassistant.helpers.intent import (
IntentHandleError,
IntentResponseType,
InvalidSlotInfo,
MatchFailedError,
MatchFailedReason,
async_handle,
)
from homeassistant.setup import async_setup_component
from tests.common import async_mock_service from tests.common import async_mock_service
async def test_intent_set_humidity(hass: HomeAssistant) -> None: async def test_intent_set_humidity(hass: HomeAssistant) -> None:
"""Test the set humidity intent.""" """Test the set humidity intent."""
assert await async_setup_component(hass, "homeassistant", {})
hass.states.async_set( hass.states.async_set(
"humidifier.bedroom_humidifier", STATE_ON, {ATTR_HUMIDITY: 40} "humidifier.bedroom_humidifier", STATE_ON, {ATTR_HUMIDITY: 40}
) )
@ -38,6 +49,7 @@ async def test_intent_set_humidity(hass: HomeAssistant) -> None:
"test", "test",
intent.INTENT_HUMIDITY, intent.INTENT_HUMIDITY,
{"name": {"value": "Bedroom humidifier"}, "humidity": {"value": "50"}}, {"name": {"value": "Bedroom humidifier"}, "humidity": {"value": "50"}},
assistant=conversation.DOMAIN,
) )
await hass.async_block_till_done() await hass.async_block_till_done()
@ -54,6 +66,7 @@ async def test_intent_set_humidity(hass: HomeAssistant) -> None:
async def test_intent_set_humidity_and_turn_on(hass: HomeAssistant) -> None: async def test_intent_set_humidity_and_turn_on(hass: HomeAssistant) -> None:
"""Test the set humidity intent for turned off humidifier.""" """Test the set humidity intent for turned off humidifier."""
assert await async_setup_component(hass, "homeassistant", {})
hass.states.async_set( hass.states.async_set(
"humidifier.bedroom_humidifier", STATE_OFF, {ATTR_HUMIDITY: 40} "humidifier.bedroom_humidifier", STATE_OFF, {ATTR_HUMIDITY: 40}
) )
@ -66,6 +79,7 @@ async def test_intent_set_humidity_and_turn_on(hass: HomeAssistant) -> None:
"test", "test",
intent.INTENT_HUMIDITY, intent.INTENT_HUMIDITY,
{"name": {"value": "Bedroom humidifier"}, "humidity": {"value": "50"}}, {"name": {"value": "Bedroom humidifier"}, "humidity": {"value": "50"}},
assistant=conversation.DOMAIN,
) )
await hass.async_block_till_done() await hass.async_block_till_done()
@ -89,6 +103,7 @@ async def test_intent_set_humidity_and_turn_on(hass: HomeAssistant) -> None:
async def test_intent_set_mode(hass: HomeAssistant) -> None: async def test_intent_set_mode(hass: HomeAssistant) -> None:
"""Test the set mode intent.""" """Test the set mode intent."""
assert await async_setup_component(hass, "homeassistant", {})
hass.states.async_set( hass.states.async_set(
"humidifier.bedroom_humidifier", "humidifier.bedroom_humidifier",
STATE_ON, STATE_ON,
@ -108,6 +123,7 @@ async def test_intent_set_mode(hass: HomeAssistant) -> None:
"test", "test",
intent.INTENT_MODE, intent.INTENT_MODE,
{"name": {"value": "Bedroom humidifier"}, "mode": {"value": "away"}}, {"name": {"value": "Bedroom humidifier"}, "mode": {"value": "away"}},
assistant=conversation.DOMAIN,
) )
await hass.async_block_till_done() await hass.async_block_till_done()
@ -127,6 +143,7 @@ async def test_intent_set_mode(hass: HomeAssistant) -> None:
async def test_intent_set_mode_and_turn_on(hass: HomeAssistant) -> None: async def test_intent_set_mode_and_turn_on(hass: HomeAssistant) -> None:
"""Test the set mode intent.""" """Test the set mode intent."""
assert await async_setup_component(hass, "homeassistant", {})
hass.states.async_set( hass.states.async_set(
"humidifier.bedroom_humidifier", "humidifier.bedroom_humidifier",
STATE_OFF, STATE_OFF,
@ -146,6 +163,7 @@ async def test_intent_set_mode_and_turn_on(hass: HomeAssistant) -> None:
"test", "test",
intent.INTENT_MODE, intent.INTENT_MODE,
{"name": {"value": "Bedroom humidifier"}, "mode": {"value": "away"}}, {"name": {"value": "Bedroom humidifier"}, "mode": {"value": "away"}},
assistant=conversation.DOMAIN,
) )
await hass.async_block_till_done() await hass.async_block_till_done()
@ -169,6 +187,7 @@ async def test_intent_set_mode_and_turn_on(hass: HomeAssistant) -> None:
async def test_intent_set_mode_tests_feature(hass: HomeAssistant) -> None: async def test_intent_set_mode_tests_feature(hass: HomeAssistant) -> None:
"""Test the set mode intent where modes are not supported.""" """Test the set mode intent where modes are not supported."""
assert await async_setup_component(hass, "homeassistant", {})
hass.states.async_set( hass.states.async_set(
"humidifier.bedroom_humidifier", STATE_ON, {ATTR_HUMIDITY: 40} "humidifier.bedroom_humidifier", STATE_ON, {ATTR_HUMIDITY: 40}
) )
@ -181,6 +200,7 @@ async def test_intent_set_mode_tests_feature(hass: HomeAssistant) -> None:
"test", "test",
intent.INTENT_MODE, intent.INTENT_MODE,
{"name": {"value": "Bedroom humidifier"}, "mode": {"value": "away"}}, {"name": {"value": "Bedroom humidifier"}, "mode": {"value": "away"}},
assistant=conversation.DOMAIN,
) )
assert str(excinfo.value) == "Entity bedroom humidifier does not support modes" assert str(excinfo.value) == "Entity bedroom humidifier does not support modes"
assert len(mode_calls) == 0 assert len(mode_calls) == 0
@ -191,6 +211,7 @@ async def test_intent_set_unknown_mode(
hass: HomeAssistant, available_modes: list[str] | None hass: HomeAssistant, available_modes: list[str] | None
) -> None: ) -> None:
"""Test the set mode intent for unsupported mode.""" """Test the set mode intent for unsupported mode."""
assert await async_setup_component(hass, "homeassistant", {})
hass.states.async_set( hass.states.async_set(
"humidifier.bedroom_humidifier", "humidifier.bedroom_humidifier",
STATE_ON, STATE_ON,
@ -210,6 +231,111 @@ async def test_intent_set_unknown_mode(
"test", "test",
intent.INTENT_MODE, intent.INTENT_MODE,
{"name": {"value": "Bedroom humidifier"}, "mode": {"value": "eco"}}, {"name": {"value": "Bedroom humidifier"}, "mode": {"value": "eco"}},
assistant=conversation.DOMAIN,
) )
assert str(excinfo.value) == "Entity bedroom humidifier does not support eco mode" assert str(excinfo.value) == "Entity bedroom humidifier does not support eco mode"
assert len(mode_calls) == 0 assert len(mode_calls) == 0
async def test_intent_errors(hass: HomeAssistant) -> None:
"""Test the error conditions for set humidity and set mode intents."""
assert await async_setup_component(hass, "homeassistant", {})
entity_id = "humidifier.bedroom_humidifier"
hass.states.async_set(
entity_id,
STATE_ON,
{
ATTR_HUMIDITY: 40,
ATTR_SUPPORTED_FEATURES: 1,
ATTR_AVAILABLE_MODES: ["home", "away"],
ATTR_MODE: None,
},
)
async_mock_service(hass, DOMAIN, SERVICE_SET_HUMIDITY)
async_mock_service(hass, DOMAIN, SERVICE_SET_MODE)
await intent.async_setup_intents(hass)
# Humidifiers are exposed by default
result = await async_handle(
hass,
"test",
intent.INTENT_HUMIDITY,
{"name": {"value": "Bedroom humidifier"}, "humidity": {"value": "50"}},
assistant=conversation.DOMAIN,
)
assert result.response_type == IntentResponseType.ACTION_DONE
result = await async_handle(
hass,
"test",
intent.INTENT_MODE,
{"name": {"value": "Bedroom humidifier"}, "mode": {"value": "away"}},
assistant=conversation.DOMAIN,
)
assert result.response_type == IntentResponseType.ACTION_DONE
# Unexposing it should fail
async_expose_entity(hass, conversation.DOMAIN, entity_id, False)
with pytest.raises(MatchFailedError) as err:
await async_handle(
hass,
"test",
intent.INTENT_HUMIDITY,
{"name": {"value": "Bedroom humidifier"}, "humidity": {"value": "50"}},
assistant=conversation.DOMAIN,
)
assert err.value.result.no_match_reason == MatchFailedReason.ASSISTANT
with pytest.raises(MatchFailedError) as err:
await async_handle(
hass,
"test",
intent.INTENT_MODE,
{"name": {"value": "Bedroom humidifier"}, "mode": {"value": "away"}},
assistant=conversation.DOMAIN,
)
assert err.value.result.no_match_reason == MatchFailedReason.ASSISTANT
# Expose again to test other errors
async_expose_entity(hass, conversation.DOMAIN, entity_id, True)
# Empty name should fail
with pytest.raises(InvalidSlotInfo):
await async_handle(
hass,
"test",
intent.INTENT_HUMIDITY,
{"name": {"value": ""}, "humidity": {"value": "50"}},
assistant=conversation.DOMAIN,
)
with pytest.raises(InvalidSlotInfo):
await async_handle(
hass,
"test",
intent.INTENT_MODE,
{"name": {"value": ""}, "mode": {"value": "away"}},
assistant=conversation.DOMAIN,
)
# Wrong name should fail
with pytest.raises(MatchFailedError) as err:
await async_handle(
hass,
"test",
intent.INTENT_HUMIDITY,
{"name": {"value": "does not exist"}, "humidity": {"value": "50"}},
assistant=conversation.DOMAIN,
)
assert err.value.result.no_match_reason == MatchFailedReason.NAME
with pytest.raises(MatchFailedError) as err:
await async_handle(
hass,
"test",
intent.INTENT_MODE,
{"name": {"value": "does not exist"}, "mode": {"value": "away"}},
assistant=conversation.DOMAIN,
)
assert err.value.result.no_match_reason == MatchFailedReason.NAME

View File

@ -59,6 +59,11 @@ TEST_CONTENT_TEXT_PLAIN = (
b"Content-Transfer-Encoding: 7bit\r\n\r\nTest body\r\n" b"Content-Transfer-Encoding: 7bit\r\n\r\nTest body\r\n"
) )
TEST_CONTENT_TEXT_PLAIN_EMPTY = (
b'Content-Type: text/plain; charset="utf-8"\r\n'
b"Content-Transfer-Encoding: 7bit\r\n\r\n \r\n"
)
TEST_CONTENT_TEXT_BASE64 = ( TEST_CONTENT_TEXT_BASE64 = (
b'Content-Type: text/plain; charset="utf-8"\r\n' b'Content-Type: text/plain; charset="utf-8"\r\n'
b"Content-Transfer-Encoding: base64\r\n\r\nVGVzdCBib2R5\r\n" b"Content-Transfer-Encoding: base64\r\n\r\nVGVzdCBib2R5\r\n"
@ -108,6 +113,15 @@ TEST_CONTENT_MULTIPART = (
+ b"\r\n--Mark=_100584970350292485166--\r\n" + b"\r\n--Mark=_100584970350292485166--\r\n"
) )
TEST_CONTENT_MULTIPART_EMPTY_PLAIN = (
b"\r\nThis is a multi-part message in MIME format.\r\n"
b"\r\n--Mark=_100584970350292485166\r\n"
+ TEST_CONTENT_TEXT_PLAIN_EMPTY
+ b"\r\n--Mark=_100584970350292485166\r\n"
+ TEST_CONTENT_HTML
+ b"\r\n--Mark=_100584970350292485166--\r\n"
)
TEST_CONTENT_MULTIPART_BASE64 = ( TEST_CONTENT_MULTIPART_BASE64 = (
b"\r\nThis is a multi-part message in MIME format.\r\n" b"\r\nThis is a multi-part message in MIME format.\r\n"
b"\r\n--Mark=_100584970350292485166\r\n" b"\r\n--Mark=_100584970350292485166\r\n"
@ -155,6 +169,18 @@ TEST_FETCH_RESPONSE_TEXT_PLAIN = (
], ],
) )
TEST_FETCH_RESPONSE_TEXT_PLAIN_EMPTY = (
"OK",
[
b"1 FETCH (BODY[] {"
+ str(len(TEST_MESSAGE + TEST_CONTENT_TEXT_PLAIN_EMPTY)).encode("utf-8")
+ b"}",
bytearray(TEST_MESSAGE + TEST_CONTENT_TEXT_PLAIN_EMPTY),
b")",
b"Fetch completed (0.0001 + 0.000 secs).",
],
)
TEST_FETCH_RESPONSE_TEXT_PLAIN_ALT = ( TEST_FETCH_RESPONSE_TEXT_PLAIN_ALT = (
"OK", "OK",
[ [
@ -249,6 +275,19 @@ TEST_FETCH_RESPONSE_MULTIPART = (
b"Fetch completed (0.0001 + 0.000 secs).", b"Fetch completed (0.0001 + 0.000 secs).",
], ],
) )
TEST_FETCH_RESPONSE_MULTIPART_EMPTY_PLAIN = (
"OK",
[
b"1 FETCH (BODY[] {"
+ str(len(TEST_MESSAGE_MULTIPART + TEST_CONTENT_MULTIPART_EMPTY_PLAIN)).encode(
"utf-8"
)
+ b"}",
bytearray(TEST_MESSAGE_MULTIPART + TEST_CONTENT_MULTIPART_EMPTY_PLAIN),
b")",
b"Fetch completed (0.0001 + 0.000 secs).",
],
)
TEST_FETCH_RESPONSE_MULTIPART_BASE64 = ( TEST_FETCH_RESPONSE_MULTIPART_BASE64 = (
"OK", "OK",
[ [

View File

@ -29,6 +29,7 @@ from .const import (
TEST_FETCH_RESPONSE_MULTIPART, TEST_FETCH_RESPONSE_MULTIPART,
TEST_FETCH_RESPONSE_MULTIPART_BASE64, TEST_FETCH_RESPONSE_MULTIPART_BASE64,
TEST_FETCH_RESPONSE_MULTIPART_BASE64_INVALID, TEST_FETCH_RESPONSE_MULTIPART_BASE64_INVALID,
TEST_FETCH_RESPONSE_MULTIPART_EMPTY_PLAIN,
TEST_FETCH_RESPONSE_NO_SUBJECT_TO_FROM, TEST_FETCH_RESPONSE_NO_SUBJECT_TO_FROM,
TEST_FETCH_RESPONSE_TEXT_BARE, TEST_FETCH_RESPONSE_TEXT_BARE,
TEST_FETCH_RESPONSE_TEXT_OTHER, TEST_FETCH_RESPONSE_TEXT_OTHER,
@ -116,6 +117,7 @@ async def test_entry_startup_fails(
(TEST_FETCH_RESPONSE_TEXT_OTHER, True), (TEST_FETCH_RESPONSE_TEXT_OTHER, True),
(TEST_FETCH_RESPONSE_HTML, True), (TEST_FETCH_RESPONSE_HTML, True),
(TEST_FETCH_RESPONSE_MULTIPART, True), (TEST_FETCH_RESPONSE_MULTIPART, True),
(TEST_FETCH_RESPONSE_MULTIPART_EMPTY_PLAIN, True),
(TEST_FETCH_RESPONSE_MULTIPART_BASE64, True), (TEST_FETCH_RESPONSE_MULTIPART_BASE64, True),
(TEST_FETCH_RESPONSE_BINARY, True), (TEST_FETCH_RESPONSE_BINARY, True),
], ],
@ -129,6 +131,7 @@ async def test_entry_startup_fails(
"other", "other",
"html", "html",
"multipart", "multipart",
"multipart_empty_plain",
"multipart_base64", "multipart_base64",
"binary", "binary",
], ],

View File

@ -54,11 +54,12 @@ async def test_climate_basic_temperature_set(
assert len(events) == 1 assert len(events) == 1
@pytest.mark.parametrize("heat_cool", [False, True]) @pytest.mark.parametrize("heat_cool_ga", [None, "4/4/4"])
async def test_climate_on_off( async def test_climate_on_off(
hass: HomeAssistant, knx: KNXTestKit, heat_cool: bool hass: HomeAssistant, knx: KNXTestKit, heat_cool_ga: str | None
) -> None: ) -> None:
"""Test KNX climate on/off.""" """Test KNX climate on/off."""
on_off_ga = "3/3/3"
await knx.setup_integration( await knx.setup_integration(
{ {
ClimateSchema.PLATFORM: { ClimateSchema.PLATFORM: {
@ -66,15 +67,15 @@ async def test_climate_on_off(
ClimateSchema.CONF_TEMPERATURE_ADDRESS: "1/2/3", ClimateSchema.CONF_TEMPERATURE_ADDRESS: "1/2/3",
ClimateSchema.CONF_TARGET_TEMPERATURE_ADDRESS: "1/2/4", ClimateSchema.CONF_TARGET_TEMPERATURE_ADDRESS: "1/2/4",
ClimateSchema.CONF_TARGET_TEMPERATURE_STATE_ADDRESS: "1/2/5", ClimateSchema.CONF_TARGET_TEMPERATURE_STATE_ADDRESS: "1/2/5",
ClimateSchema.CONF_ON_OFF_ADDRESS: "1/2/8", ClimateSchema.CONF_ON_OFF_ADDRESS: on_off_ga,
ClimateSchema.CONF_ON_OFF_STATE_ADDRESS: "1/2/9", ClimateSchema.CONF_ON_OFF_STATE_ADDRESS: "1/2/9",
} }
| ( | (
{ {
ClimateSchema.CONF_HEAT_COOL_ADDRESS: "1/2/10", ClimateSchema.CONF_HEAT_COOL_ADDRESS: heat_cool_ga,
ClimateSchema.CONF_HEAT_COOL_STATE_ADDRESS: "1/2/11", ClimateSchema.CONF_HEAT_COOL_STATE_ADDRESS: "1/2/11",
} }
if heat_cool if heat_cool_ga
else {} else {}
) )
} }
@ -82,7 +83,7 @@ async def test_climate_on_off(
await hass.async_block_till_done() await hass.async_block_till_done()
# read heat/cool state # read heat/cool state
if heat_cool: if heat_cool_ga:
await knx.assert_read("1/2/11") await knx.assert_read("1/2/11")
await knx.receive_response("1/2/11", 0) # cool await knx.receive_response("1/2/11", 0) # cool
# read temperature state # read temperature state
@ -102,7 +103,7 @@ async def test_climate_on_off(
{"entity_id": "climate.test"}, {"entity_id": "climate.test"},
blocking=True, blocking=True,
) )
await knx.assert_write("1/2/8", 0) await knx.assert_write(on_off_ga, 0)
assert hass.states.get("climate.test").state == "off" assert hass.states.get("climate.test").state == "off"
# turn on # turn on
@ -112,8 +113,8 @@ async def test_climate_on_off(
{"entity_id": "climate.test"}, {"entity_id": "climate.test"},
blocking=True, blocking=True,
) )
await knx.assert_write("1/2/8", 1) await knx.assert_write(on_off_ga, 1)
if heat_cool: if heat_cool_ga:
# does not fall back to default hvac mode after turn_on # does not fall back to default hvac mode after turn_on
assert hass.states.get("climate.test").state == "cool" assert hass.states.get("climate.test").state == "cool"
else: else:
@ -126,7 +127,8 @@ async def test_climate_on_off(
{"entity_id": "climate.test", "hvac_mode": HVACMode.OFF}, {"entity_id": "climate.test", "hvac_mode": HVACMode.OFF},
blocking=True, blocking=True,
) )
await knx.assert_write("1/2/8", 0) await knx.assert_write(on_off_ga, 0)
assert hass.states.get("climate.test").state == "off"
# set hvac mode to heat # set hvac mode to heat
await hass.services.async_call( await hass.services.async_call(
@ -135,15 +137,20 @@ async def test_climate_on_off(
{"entity_id": "climate.test", "hvac_mode": HVACMode.HEAT}, {"entity_id": "climate.test", "hvac_mode": HVACMode.HEAT},
blocking=True, blocking=True,
) )
if heat_cool: if heat_cool_ga:
# only set new hvac_mode without changing on/off - actuator shall handle that await knx.assert_write(heat_cool_ga, 1)
await knx.assert_write("1/2/10", 1) await knx.assert_write(on_off_ga, 1)
else: else:
await knx.assert_write("1/2/8", 1) await knx.assert_write(on_off_ga, 1)
assert hass.states.get("climate.test").state == "heat"
async def test_climate_hvac_mode(hass: HomeAssistant, knx: KNXTestKit) -> None: @pytest.mark.parametrize("on_off_ga", [None, "4/4/4"])
async def test_climate_hvac_mode(
hass: HomeAssistant, knx: KNXTestKit, on_off_ga: str | None
) -> None:
"""Test KNX climate hvac mode.""" """Test KNX climate hvac mode."""
controller_mode_ga = "3/3/3"
await knx.setup_integration( await knx.setup_integration(
{ {
ClimateSchema.PLATFORM: { ClimateSchema.PLATFORM: {
@ -151,11 +158,17 @@ async def test_climate_hvac_mode(hass: HomeAssistant, knx: KNXTestKit) -> None:
ClimateSchema.CONF_TEMPERATURE_ADDRESS: "1/2/3", ClimateSchema.CONF_TEMPERATURE_ADDRESS: "1/2/3",
ClimateSchema.CONF_TARGET_TEMPERATURE_ADDRESS: "1/2/4", ClimateSchema.CONF_TARGET_TEMPERATURE_ADDRESS: "1/2/4",
ClimateSchema.CONF_TARGET_TEMPERATURE_STATE_ADDRESS: "1/2/5", ClimateSchema.CONF_TARGET_TEMPERATURE_STATE_ADDRESS: "1/2/5",
ClimateSchema.CONF_CONTROLLER_MODE_ADDRESS: "1/2/6", ClimateSchema.CONF_CONTROLLER_MODE_ADDRESS: controller_mode_ga,
ClimateSchema.CONF_CONTROLLER_MODE_STATE_ADDRESS: "1/2/7", ClimateSchema.CONF_CONTROLLER_MODE_STATE_ADDRESS: "1/2/7",
ClimateSchema.CONF_ON_OFF_ADDRESS: "1/2/8",
ClimateSchema.CONF_OPERATION_MODES: ["Auto"], ClimateSchema.CONF_OPERATION_MODES: ["Auto"],
} }
| (
{
ClimateSchema.CONF_ON_OFF_ADDRESS: on_off_ga,
}
if on_off_ga
else {}
)
} }
) )
@ -171,23 +184,56 @@ async def test_climate_hvac_mode(hass: HomeAssistant, knx: KNXTestKit) -> None:
await knx.assert_read("1/2/5") await knx.assert_read("1/2/5")
await knx.receive_response("1/2/5", RAW_FLOAT_22_0) await knx.receive_response("1/2/5", RAW_FLOAT_22_0)
# turn hvac mode to off # turn hvac mode to off - set_hvac_mode() doesn't send to on_off if dedicated hvac mode is available
await hass.services.async_call( await hass.services.async_call(
"climate", "climate",
"set_hvac_mode", "set_hvac_mode",
{"entity_id": "climate.test", "hvac_mode": HVACMode.OFF}, {"entity_id": "climate.test", "hvac_mode": HVACMode.OFF},
blocking=True, blocking=True,
) )
await knx.assert_write("1/2/6", (0x06,)) await knx.assert_write(controller_mode_ga, (0x06,))
if on_off_ga:
await knx.assert_write(on_off_ga, 0)
assert hass.states.get("climate.test").state == "off"
# turn hvac on # set hvac to non default mode
await hass.services.async_call( await hass.services.async_call(
"climate", "climate",
"set_hvac_mode", "set_hvac_mode",
{"entity_id": "climate.test", "hvac_mode": HVACMode.HEAT}, {"entity_id": "climate.test", "hvac_mode": HVACMode.COOL},
blocking=True, blocking=True,
) )
await knx.assert_write("1/2/6", (0x01,)) await knx.assert_write(controller_mode_ga, (0x03,))
if on_off_ga:
await knx.assert_write(on_off_ga, 1)
assert hass.states.get("climate.test").state == "cool"
# turn off
await hass.services.async_call(
"climate",
"turn_off",
{"entity_id": "climate.test"},
blocking=True,
)
if on_off_ga:
await knx.assert_write(on_off_ga, 0)
else:
await knx.assert_write(controller_mode_ga, (0x06,))
assert hass.states.get("climate.test").state == "off"
# turn on
await hass.services.async_call(
"climate",
"turn_on",
{"entity_id": "climate.test"},
blocking=True,
)
if on_off_ga:
await knx.assert_write(on_off_ga, 1)
else:
# restore last hvac mode
await knx.assert_write(controller_mode_ga, (0x03,))
assert hass.states.get("climate.test").state == "cool"
async def test_climate_preset_mode( async def test_climate_preset_mode(

View File

@ -9,6 +9,8 @@ import zoneinfo
import pytest import pytest
import voluptuous as vol import voluptuous as vol
from homeassistant.components import conversation
from homeassistant.components.homeassistant.exposed_entities import async_expose_entity
from homeassistant.components.todo import ( from homeassistant.components.todo import (
DOMAIN, DOMAIN,
TodoItem, TodoItem,
@ -23,6 +25,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import intent from homeassistant.helpers import intent
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.setup import async_setup_component
from tests.common import ( from tests.common import (
MockConfigEntry, MockConfigEntry,
@ -1110,6 +1113,7 @@ async def test_add_item_intent(
hass_ws_client: WebSocketGenerator, hass_ws_client: WebSocketGenerator,
) -> None: ) -> None:
"""Test adding items to lists using an intent.""" """Test adding items to lists using an intent."""
assert await async_setup_component(hass, "homeassistant", {})
await todo_intent.async_setup_intents(hass) await todo_intent.async_setup_intents(hass)
entity1 = MockTodoListEntity() entity1 = MockTodoListEntity()
@ -1128,6 +1132,7 @@ async def test_add_item_intent(
"test", "test",
todo_intent.INTENT_LIST_ADD_ITEM, todo_intent.INTENT_LIST_ADD_ITEM,
{"item": {"value": "beer"}, "name": {"value": "list 1"}}, {"item": {"value": "beer"}, "name": {"value": "list 1"}},
assistant=conversation.DOMAIN,
) )
assert response.response_type == intent.IntentResponseType.ACTION_DONE assert response.response_type == intent.IntentResponseType.ACTION_DONE
@ -1143,6 +1148,7 @@ async def test_add_item_intent(
"test", "test",
todo_intent.INTENT_LIST_ADD_ITEM, todo_intent.INTENT_LIST_ADD_ITEM,
{"item": {"value": "cheese"}, "name": {"value": "List 2"}}, {"item": {"value": "cheese"}, "name": {"value": "List 2"}},
assistant=conversation.DOMAIN,
) )
assert response.response_type == intent.IntentResponseType.ACTION_DONE assert response.response_type == intent.IntentResponseType.ACTION_DONE
@ -1157,6 +1163,7 @@ async def test_add_item_intent(
"test", "test",
todo_intent.INTENT_LIST_ADD_ITEM, todo_intent.INTENT_LIST_ADD_ITEM,
{"item": {"value": "wine"}, "name": {"value": "lIST 2"}}, {"item": {"value": "wine"}, "name": {"value": "lIST 2"}},
assistant=conversation.DOMAIN,
) )
assert response.response_type == intent.IntentResponseType.ACTION_DONE assert response.response_type == intent.IntentResponseType.ACTION_DONE
@ -1165,13 +1172,46 @@ async def test_add_item_intent(
assert entity2.items[1].summary == "wine" assert entity2.items[1].summary == "wine"
assert entity2.items[1].status == TodoItemStatus.NEEDS_ACTION assert entity2.items[1].status == TodoItemStatus.NEEDS_ACTION
# Should fail if lists are not exposed
async_expose_entity(hass, conversation.DOMAIN, entity1.entity_id, False)
async_expose_entity(hass, conversation.DOMAIN, entity2.entity_id, False)
with pytest.raises(intent.MatchFailedError) as err:
await intent.async_handle(
hass,
"test",
todo_intent.INTENT_LIST_ADD_ITEM,
{"item": {"value": "cookies"}, "name": {"value": "list 1"}},
assistant=conversation.DOMAIN,
)
assert err.value.result.no_match_reason == intent.MatchFailedReason.ASSISTANT
# Missing list # Missing list
with pytest.raises(intent.IntentHandleError): with pytest.raises(intent.MatchFailedError):
await intent.async_handle( await intent.async_handle(
hass, hass,
"test", "test",
todo_intent.INTENT_LIST_ADD_ITEM, todo_intent.INTENT_LIST_ADD_ITEM,
{"item": {"value": "wine"}, "name": {"value": "This list does not exist"}}, {"item": {"value": "wine"}, "name": {"value": "This list does not exist"}},
assistant=conversation.DOMAIN,
)
# Fail with empty name/item
with pytest.raises(intent.InvalidSlotInfo):
await intent.async_handle(
hass,
"test",
todo_intent.INTENT_LIST_ADD_ITEM,
{"item": {"value": "wine"}, "name": {"value": ""}},
assistant=conversation.DOMAIN,
)
with pytest.raises(intent.InvalidSlotInfo):
await intent.async_handle(
hass,
"test",
todo_intent.INTENT_LIST_ADD_ITEM,
{"item": {"value": ""}, "name": {"value": "list 1"}},
assistant=conversation.DOMAIN,
) )

View File

@ -1,9 +1,9 @@
"""Test weather intents.""" """Test weather intents."""
from unittest.mock import patch
import pytest import pytest
from homeassistant.components import conversation
from homeassistant.components.homeassistant.exposed_entities import async_expose_entity
from homeassistant.components.weather import ( from homeassistant.components.weather import (
DOMAIN, DOMAIN,
WeatherEntity, WeatherEntity,
@ -16,15 +16,18 @@ from homeassistant.setup import async_setup_component
async def test_get_weather(hass: HomeAssistant) -> None: async def test_get_weather(hass: HomeAssistant) -> None:
"""Test get weather for first entity and by name.""" """Test get weather for first entity and by name."""
assert await async_setup_component(hass, "homeassistant", {})
assert await async_setup_component(hass, "weather", {"weather": {}}) assert await async_setup_component(hass, "weather", {"weather": {}})
entity1 = WeatherEntity() entity1 = WeatherEntity()
entity1._attr_name = "Weather 1" entity1._attr_name = "Weather 1"
entity1.entity_id = "weather.test_1" entity1.entity_id = "weather.test_1"
async_expose_entity(hass, conversation.DOMAIN, entity1.entity_id, True)
entity2 = WeatherEntity() entity2 = WeatherEntity()
entity2._attr_name = "Weather 2" entity2._attr_name = "Weather 2"
entity2.entity_id = "weather.test_2" entity2.entity_id = "weather.test_2"
async_expose_entity(hass, conversation.DOMAIN, entity2.entity_id, True)
await hass.data[DOMAIN].async_add_entities([entity1, entity2]) await hass.data[DOMAIN].async_add_entities([entity1, entity2])
@ -45,15 +48,31 @@ async def test_get_weather(hass: HomeAssistant) -> None:
"test", "test",
weather_intent.INTENT_GET_WEATHER, weather_intent.INTENT_GET_WEATHER,
{"name": {"value": "Weather 2"}}, {"name": {"value": "Weather 2"}},
assistant=conversation.DOMAIN,
) )
assert response.response_type == intent.IntentResponseType.QUERY_ANSWER assert response.response_type == intent.IntentResponseType.QUERY_ANSWER
assert len(response.matched_states) == 1 assert len(response.matched_states) == 1
state = response.matched_states[0] state = response.matched_states[0]
assert state.entity_id == entity2.entity_id assert state.entity_id == entity2.entity_id
# Should fail if not exposed
async_expose_entity(hass, conversation.DOMAIN, entity1.entity_id, False)
async_expose_entity(hass, conversation.DOMAIN, entity2.entity_id, False)
for name in (entity1.name, entity2.name):
with pytest.raises(intent.MatchFailedError) as err:
await intent.async_handle(
hass,
"test",
weather_intent.INTENT_GET_WEATHER,
{"name": {"value": name}},
assistant=conversation.DOMAIN,
)
assert err.value.result.no_match_reason == intent.MatchFailedReason.ASSISTANT
async def test_get_weather_wrong_name(hass: HomeAssistant) -> None: async def test_get_weather_wrong_name(hass: HomeAssistant) -> None:
"""Test get weather with the wrong name.""" """Test get weather with the wrong name."""
assert await async_setup_component(hass, "homeassistant", {})
assert await async_setup_component(hass, "weather", {"weather": {}}) assert await async_setup_component(hass, "weather", {"weather": {}})
entity1 = WeatherEntity() entity1 = WeatherEntity()
@ -63,48 +82,43 @@ async def test_get_weather_wrong_name(hass: HomeAssistant) -> None:
await hass.data[DOMAIN].async_add_entities([entity1]) await hass.data[DOMAIN].async_add_entities([entity1])
await weather_intent.async_setup_intents(hass) await weather_intent.async_setup_intents(hass)
async_expose_entity(hass, conversation.DOMAIN, entity1.entity_id, True)
# Incorrect name # Incorrect name
with pytest.raises(intent.IntentHandleError): with pytest.raises(intent.MatchFailedError) as err:
await intent.async_handle( await intent.async_handle(
hass, hass,
"test", "test",
weather_intent.INTENT_GET_WEATHER, weather_intent.INTENT_GET_WEATHER,
{"name": {"value": "not the right name"}}, {"name": {"value": "not the right name"}},
assistant=conversation.DOMAIN,
)
assert err.value.result.no_match_reason == intent.MatchFailedReason.NAME
# Empty name
with pytest.raises(intent.InvalidSlotInfo):
await intent.async_handle(
hass,
"test",
weather_intent.INTENT_GET_WEATHER,
{"name": {"value": ""}},
assistant=conversation.DOMAIN,
) )
async def test_get_weather_no_entities(hass: HomeAssistant) -> None: async def test_get_weather_no_entities(hass: HomeAssistant) -> None:
"""Test get weather with no weather entities.""" """Test get weather with no weather entities."""
assert await async_setup_component(hass, "homeassistant", {})
assert await async_setup_component(hass, "weather", {"weather": {}}) assert await async_setup_component(hass, "weather", {"weather": {}})
await weather_intent.async_setup_intents(hass) await weather_intent.async_setup_intents(hass)
# No weather entities # No weather entities
with pytest.raises(intent.IntentHandleError): with pytest.raises(intent.MatchFailedError) as err:
await intent.async_handle(hass, "test", weather_intent.INTENT_GET_WEATHER, {}) await intent.async_handle(
hass,
"test",
async def test_get_weather_no_state(hass: HomeAssistant) -> None: weather_intent.INTENT_GET_WEATHER,
"""Test get weather when state is not returned.""" {},
assert await async_setup_component(hass, "weather", {"weather": {}}) assistant=conversation.DOMAIN,
entity1 = WeatherEntity()
entity1._attr_name = "Weather 1"
entity1.entity_id = "weather.test_1"
await hass.data[DOMAIN].async_add_entities([entity1])
await weather_intent.async_setup_intents(hass)
# Success with state
response = await intent.async_handle(
hass, "test", weather_intent.INTENT_GET_WEATHER, {}
) )
assert response.response_type == intent.IntentResponseType.QUERY_ANSWER assert err.value.result.no_match_reason == intent.MatchFailedReason.DOMAIN
# Failure without state
with (
patch("homeassistant.core.StateMachine.get", return_value=None),
pytest.raises(intent.IntentHandleError),
):
await intent.async_handle(hass, "test", weather_intent.INTENT_GET_WEATHER, {})

View File

@ -10,7 +10,7 @@ from homeassistant.bootstrap import (
DEBUGGER_INTEGRATIONS, DEBUGGER_INTEGRATIONS,
DEFAULT_INTEGRATIONS, DEFAULT_INTEGRATIONS,
FRONTEND_INTEGRATIONS, FRONTEND_INTEGRATIONS,
LOGGING_INTEGRATIONS, LOGGING_AND_HTTP_DEPS_INTEGRATIONS,
RECORDER_INTEGRATIONS, RECORDER_INTEGRATIONS,
STAGE_1_INTEGRATIONS, STAGE_1_INTEGRATIONS,
) )
@ -23,7 +23,7 @@ from homeassistant.bootstrap import (
{ {
*DEBUGGER_INTEGRATIONS, *DEBUGGER_INTEGRATIONS,
*CORE_INTEGRATIONS, *CORE_INTEGRATIONS,
*LOGGING_INTEGRATIONS, *LOGGING_AND_HTTP_DEPS_INTEGRATIONS,
*FRONTEND_INTEGRATIONS, *FRONTEND_INTEGRATIONS,
*RECORDER_INTEGRATIONS, *RECORDER_INTEGRATIONS,
*STAGE_1_INTEGRATIONS, *STAGE_1_INTEGRATIONS,

View File

@ -591,7 +591,7 @@ async def test_discovery_requirements_mqtt(hass: HomeAssistant) -> None:
) as mock_process: ) as mock_process:
await async_get_integration_with_requirements(hass, "mqtt_comp") await async_get_integration_with_requirements(hass, "mqtt_comp")
assert len(mock_process.mock_calls) == 2 assert len(mock_process.mock_calls) == 1
assert mock_process.mock_calls[0][1][1] == mqtt.requirements assert mock_process.mock_calls[0][1][1] == mqtt.requirements
@ -608,13 +608,12 @@ async def test_discovery_requirements_ssdp(hass: HomeAssistant) -> None:
) as mock_process: ) as mock_process:
await async_get_integration_with_requirements(hass, "ssdp_comp") await async_get_integration_with_requirements(hass, "ssdp_comp")
assert len(mock_process.mock_calls) == 4 assert len(mock_process.mock_calls) == 3
assert mock_process.mock_calls[0][1][1] == ssdp.requirements assert mock_process.mock_calls[0][1][1] == ssdp.requirements
assert { assert {
mock_process.mock_calls[1][1][0], mock_process.mock_calls[1][1][0],
mock_process.mock_calls[2][1][0], mock_process.mock_calls[2][1][0],
mock_process.mock_calls[3][1][0], } == {"network", "recorder"}
} == {"network", "recorder", "isal"}
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -638,7 +637,7 @@ async def test_discovery_requirements_zeroconf(
) as mock_process: ) as mock_process:
await async_get_integration_with_requirements(hass, "comp") await async_get_integration_with_requirements(hass, "comp")
assert len(mock_process.mock_calls) == 4 assert len(mock_process.mock_calls) == 3
assert mock_process.mock_calls[0][1][1] == zeroconf.requirements assert mock_process.mock_calls[0][1][1] == zeroconf.requirements