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"}
# Core integrations are unconditionally loaded
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
"logger",
# Error logging
@ -214,8 +221,8 @@ CRITICAL_INTEGRATIONS = {
}
SETUP_ORDER = (
# Load logging as soon as possible
("logging", LOGGING_INTEGRATIONS),
# Load logging and http deps as soon as possible
("logging, http deps", LOGGING_AND_HTTP_DEPS_INTEGRATIONS),
# Setup frontend and recorder
("frontend, recorder", {*FRONTEND_INTEGRATIONS, *RECORDER_INTEGRATIONS}),
# 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_NIGHT
)
_attr_code_arm_required = False
_attr_has_entity_name = True
_attr_name = None

View File

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

View File

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

View File

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

View File

@ -5,12 +5,13 @@
"title": "Setup your Azure Data Explorer integration",
"description": "Enter connection details.",
"data": {
"clusteringesturi": "Cluster Ingest URI",
"cluster_ingest_uri": "Cluster ingest URI",
"database": "Database name",
"table": "Table name",
"client_id": "Client ID",
"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."""
_attr_supported_features = AlarmControlPanelEntityFeature.ARM_AWAY
_attr_code_arm_required = False
_attr_has_entity_name = True
_attr_name = None

View File

@ -4,11 +4,10 @@ from __future__ import annotations
import voluptuous as vol
from homeassistant.core import HomeAssistant, State
from homeassistant.core import HomeAssistant
from homeassistant.helpers import intent
from homeassistant.helpers.entity_component import EntityComponent
from . import DOMAIN, ClimateEntity
from . import DOMAIN
INTENT_GET_TEMPERATURE = "HassClimateGetTemperature"
@ -23,7 +22,10 @@ class GetTemperatureIntent(intent.IntentHandler):
intent_type = INTENT_GET_TEMPERATURE
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}
async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
@ -31,74 +33,24 @@ class GetTemperatureIntent(intent.IntentHandler):
hass = intent_obj.hass
slots = self.async_validate_slots(intent_obj.slots)
component: EntityComponent[ClimateEntity] = hass.data[DOMAIN]
entities: list[ClimateEntity] = list(component.entities)
climate_entity: ClimateEntity | None = None
climate_state: State | None = None
name: str | None = None
if "name" in slots:
name = slots["name"]["value"]
if not entities:
raise intent.IntentHandleError("No climate entities")
area: str | None = None
if "area" in slots:
area = slots["area"]["value"]
name_slot = slots.get("name", {})
entity_name: str | None = name_slot.get("value")
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,
)
climate_entity = component.get_entity(climate_state.entity_id)
elif entity_name:
# Filter by name
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
match_constraints = intent.MatchTargetsConstraints(
name=name, area_name=area, 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
)
response = intent_obj.create_response()
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

View File

@ -429,8 +429,15 @@ class DefaultAgent(ConversationEntity):
intent_context=intent_context,
language=language,
):
if ("name" in result.entities) and (
not result.entities["name"].is_wildcard
# Prioritize results with a "name" slot, but still prefer ones with
# 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

View File

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

View File

@ -165,7 +165,7 @@ class GoogleAssistantConversationAgent(conversation.AbstractConversationAgent):
await session.async_ensure_token_valid()
self.assistant = None
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.assistant = TextAssistant(credentials, self.language)

View File

@ -72,7 +72,7 @@ async def async_send_text_commands(
entry.async_start_reauth(hass)
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))
with TextAssistant(
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:
"""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:
sheet = service.open_by_key(entry.unique_id)
except RefreshError:

View File

@ -61,7 +61,9 @@ class OAuth2FlowHandler(
async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult:
"""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:
_LOGGER.debug("service.open_by_key")

View File

@ -267,15 +267,14 @@ class SupervisorIssues:
placeholders = {PLACEHOLDER_KEY_REFERENCE: issue.reference}
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)
if addons and issue.reference in addons:
placeholders[PLACEHOLDER_KEY_ADDON] = addons[issue.reference][
"name"
]
if "url" in addons[issue.reference]:
placeholders[PLACEHOLDER_KEY_ADDON_URL] = addons[
issue.reference
]["url"]
else:
placeholders[PLACEHOLDER_KEY_ADDON] = issue.reference

View File

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

View File

@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/holiday",
"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",
"name": "HTTP",
"after_dependencies": ["isal"],
"codeowners": ["@home-assistant/core"],
"documentation": "https://www.home-assistant.io/integrations/http",
"integration_type": "system",

View File

@ -35,7 +35,7 @@ class HumidityHandler(intent.IntentHandler):
intent_type = INTENT_HUMIDITY
description = "Set desired humidity level"
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)),
}
platforms = {DOMAIN}
@ -44,18 +44,19 @@ class HumidityHandler(intent.IntentHandler):
"""Handle the hass intent."""
hass = intent_obj.hass
slots = self.async_validate_slots(intent_obj.slots)
states = list(
intent.async_match_states(
hass,
name=slots["name"]["value"],
states=hass.states.async_all(DOMAIN),
)
match_constraints = intent.MatchTargetsConstraints(
name=slots["name"]["value"],
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:
raise intent.IntentHandleError("No entities matched")
state = states[0]
state = match_result.states[0]
service_data = {ATTR_ENTITY_ID: state.entity_id}
humidity = slots["humidity"]["value"]
@ -89,7 +90,7 @@ class SetModeHandler(intent.IntentHandler):
intent_type = INTENT_MODE
description = "Set humidifier mode"
slot_schema = {
vol.Required("name"): cv.string,
vol.Required("name"): intent.non_empty_string,
vol.Required("mode"): cv.string,
}
platforms = {DOMAIN}
@ -98,18 +99,18 @@ class SetModeHandler(intent.IntentHandler):
"""Handle the hass intent."""
hass = intent_obj.hass
slots = self.async_validate_slots(intent_obj.slots)
states = list(
intent.async_match_states(
hass,
name=slots["name"]["value"],
states=hass.states.async_all(DOMAIN),
)
match_constraints = intent.MatchTargetsConstraints(
name=slots["name"]["value"],
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:
raise intent.IntentHandleError("No entities matched")
state = states[0]
state = match_result.states[0]
service_data = {ATTR_ENTITY_ID: state.entity_id}
intent.async_test_feature(state, HumidifierEntityFeature.MODES, "modes")

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/hydrawise",
"iot_class": "cloud_polling",
"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_AWAY
)
_attr_code_arm_required = False
def __init__(self, coordinator: IAlarmDataUpdateCoordinator) -> None:
"""Create the entity with a DataUpdateCoordinator."""

View File

@ -195,13 +195,13 @@ class ImapMessage:
):
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
if message_html is not None:
if message_html:
return message_html
if message_untyped_text is not None:
if message_untyped_text:
return message_untyped_text
return str(self.email_message.get_payload())

View File

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

View File

@ -283,16 +283,13 @@ class KNXClimate(KnxEntity, ClimateEntity):
)
if knx_controller_mode in self._device.mode.controller_modes:
await self._device.mode.set_controller_mode(knx_controller_mode)
self.async_write_ha_state()
return
if self._device.supports_on_off:
if hvac_mode == HVACMode.OFF:
await self._device.turn_off()
elif not self._device.is_on:
# for default hvac mode, otherwise above would have triggered
await self._device.turn_on()
self.async_write_ha_state()
self.async_write_ha_state()
@property
def preset_mode(self) -> str | None:

View File

@ -48,6 +48,7 @@ class LupusecAlarm(LupusecDevice, AlarmControlPanelEntity):
AlarmControlPanelEntityFeature.ARM_HOME
| AlarmControlPanelEntityFeature.ARM_AWAY
)
_attr_code_arm_required = False
def __init__(
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
# know it is working at startup (then if not, fail loudly).
token = self._oauth_session.token
creds = Credentials(
creds = Credentials( # type: ignore[no-untyped-call]
token=token["access_token"],
refresh_token=token["refresh_token"],
token_uri=OAUTH2_TOKEN,
@ -92,7 +92,7 @@ class AccessTokenAuthImpl(AbstractAuth):
async def async_get_creds(self) -> Credentials:
"""Return an OAuth credential for Pub/Sub Subscriber."""
return Credentials(
return Credentials( # type: ignore[no-untyped-call]
token=self._access_token,
token_uri=OAUTH2_TOKEN,
scopes=SDM_SCOPES,

View File

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

View File

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

View File

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

View File

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

View File

@ -584,11 +584,13 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]):
raise UpdateFailed(
f"Sleeping device did not update within {self.sleep_period} seconds interval"
)
if self.device.connected:
return
if not await self._async_device_connect_task():
raise UpdateFailed("Device reconnect error")
async with self._connection_lock:
if self.device.connected: # Already connected
return
if not await self._async_device_connect_task():
raise UpdateFailed("Device reconnect error")
async def _async_disconnected(self, reconnect: bool) -> None:
"""Handle device disconnected."""

View File

@ -8,6 +8,8 @@ from typing import Any
import pysnmp.hlapi.asyncio as hlapi
from pysnmp.hlapi.asyncio import (
CommunityData,
ObjectIdentity,
ObjectType,
UdpTransportTarget,
UsmUserData,
getCmd,
@ -63,7 +65,12 @@ from .const import (
MAP_PRIV_PROTOCOLS,
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__)
@ -125,23 +132,23 @@ async def async_setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the SNMP switch."""
name = config.get(CONF_NAME)
host = config.get(CONF_HOST)
port = config.get(CONF_PORT)
name: str = config[CONF_NAME]
host: str = config[CONF_HOST]
port: int = config[CONF_PORT]
community = config.get(CONF_COMMUNITY)
baseoid: str = config[CONF_BASEOID]
command_oid = config.get(CONF_COMMAND_OID)
command_payload_on = config.get(CONF_COMMAND_PAYLOAD_ON)
command_payload_off = config.get(CONF_COMMAND_PAYLOAD_OFF)
command_oid: str | None = config.get(CONF_COMMAND_OID)
command_payload_on: str | None = config.get(CONF_COMMAND_PAYLOAD_ON)
command_payload_off: str | None = config.get(CONF_COMMAND_PAYLOAD_OFF)
version: str = config[CONF_VERSION]
username = config.get(CONF_USERNAME)
authkey = config.get(CONF_AUTH_KEY)
authproto: str = config[CONF_AUTH_PROTOCOL]
privkey = config.get(CONF_PRIV_KEY)
privproto: str = config[CONF_PRIV_PROTOCOL]
payload_on = config.get(CONF_PAYLOAD_ON)
payload_off = config.get(CONF_PAYLOAD_OFF)
vartype = config.get(CONF_VARTYPE)
payload_on: str = config[CONF_PAYLOAD_ON]
payload_off: str = config[CONF_PAYLOAD_OFF]
vartype: str = config[CONF_VARTYPE]
if version == "3":
if not authkey:
@ -159,9 +166,11 @@ async def async_setup_platform(
else:
auth_data = CommunityData(community, mpModel=SNMP_VERSIONS[version])
transport = UdpTransportTarget((host, port))
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(
[
@ -177,6 +186,7 @@ async def async_setup_platform(
command_payload_off,
vartype,
request_args,
command_args,
)
],
True,
@ -188,21 +198,22 @@ class SnmpSwitch(SwitchEntity):
def __init__(
self,
name,
host,
port,
baseoid,
commandoid,
payload_on,
payload_off,
command_payload_on,
command_payload_off,
vartype,
request_args,
name: str,
host: str,
port: int,
baseoid: str,
commandoid: str | None,
payload_on: str,
payload_off: str,
command_payload_on: str | None,
command_payload_off: str | None,
vartype: str,
request_args: RequestArgsType,
command_args: CommandArgsType,
) -> None:
"""Initialize the switch."""
self._name = name
self._attr_name = name
self._baseoid = baseoid
self._vartype = vartype
@ -215,7 +226,8 @@ class SnmpSwitch(SwitchEntity):
self._payload_on = payload_on
self._payload_off = payload_off
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:
"""Turn on the switch."""
@ -226,7 +238,7 @@ class SnmpSwitch(SwitchEntity):
"""Turn off the switch."""
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
if self._vartype == "none" and not self._command_payload_on.isdigit():
await self._set(command)
@ -265,14 +277,12 @@ class SnmpSwitch(SwitchEntity):
self._state = None
@property
def name(self):
"""Return the switch's name."""
return self._name
@property
def is_on(self):
def is_on(self) -> bool | None:
"""Return true if switch is on; False if off. None if unknown."""
return self._state
async def _set(self, value):
await setCmd(*self._request_args, value)
async def _set(self, value: Any) -> None:
"""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__)
type CommandArgsType = tuple[
SnmpEngine,
UsmUserData | CommunityData,
UdpTransportTarget | Udp6TransportTarget,
ContextData,
]
type RequestArgsType = tuple[
SnmpEngine,
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(
hass: HomeAssistant,
auth_data: UsmUserData | CommunityData,
target: UdpTransportTarget | Udp6TransportTarget,
object_id: str,
) -> RequestArgsType:
"""Create request arguments."""
return (
await async_get_snmp_engine(hass),
auth_data,
target,
ContextData(),
ObjectType(ObjectIdentity(object_id)),
"""Create request arguments.
The same ObjectType is used for all requests.
"""
engine, auth_data, target, context_data = await async_create_command_cmd_args(
hass, auth_data, target
)
object_type = ObjectType(ObjectIdentity(object_id))
return (engine, auth_data, target, context_data, object_type)
@singleton(DATA_SNMP_ENGINE)

View File

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

View File

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

View File

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

View File

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

View File

@ -7,5 +7,5 @@
"iot_class": "local_polling",
"loggers": ["holidays"],
"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_supported_features = AlarmControlPanelEntityFeature.ARM_AWAY
_attr_code_arm_required = False
def __init__(
self, gateway_device, gateway_name, model, mac_address, gateway_device_id

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,21 +1,23 @@
"""Test climate intents."""
from collections.abc import Generator
from unittest.mock import patch
import pytest
from homeassistant.components import conversation
from homeassistant.components.climate import (
DOMAIN,
ClimateEntity,
HVACMode,
intent as climate_intent,
)
from homeassistant.components.homeassistant.exposed_entities import async_expose_entity
from homeassistant.config_entries import ConfigEntry, ConfigFlow
from homeassistant.const import Platform, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.helpers import area_registry as ar, entity_registry as er, intent
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.setup import async_setup_component
from tests.common import (
MockConfigEntry,
@ -113,6 +115,7 @@ async def test_get_temperature(
entity_registry: er.EntityRegistry,
) -> None:
"""Test HassClimateGetTemperature intent."""
assert await async_setup_component(hass, "homeassistant", {})
await climate_intent.async_setup_intents(hass)
climate_1 = MockClimateEntity()
@ -148,10 +151,14 @@ async def test_get_temperature(
# First climate entity will be selected (no area)
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 len(response.matched_states) == 1
assert response.matched_states
assert response.matched_states[0].entity_id == climate_1.entity_id
state = response.matched_states[0]
assert state.attributes["current_temperature"] == 10.0
@ -162,6 +169,7 @@ async def test_get_temperature(
"test",
climate_intent.INTENT_GET_TEMPERATURE,
{"area": {"value": bedroom_area.name}},
assistant=conversation.DOMAIN,
)
assert response.response_type == intent.IntentResponseType.QUERY_ANSWER
assert len(response.matched_states) == 1
@ -175,6 +183,7 @@ async def test_get_temperature(
"test",
climate_intent.INTENT_GET_TEMPERATURE,
{"name": {"value": "Climate 2"}},
assistant=conversation.DOMAIN,
)
assert response.response_type == intent.IntentResponseType.QUERY_ANSWER
assert len(response.matched_states) == 1
@ -189,6 +198,7 @@ async def test_get_temperature(
"test",
climate_intent.INTENT_GET_TEMPERATURE,
{"area": {"value": office_area.name}},
assistant=conversation.DOMAIN,
)
# Exception should contain details of what we tried to match
@ -197,7 +207,7 @@ async def test_get_temperature(
constraints = error.value.constraints
assert constraints.name is None
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
# Check wrong name
@ -214,7 +224,7 @@ async def test_get_temperature(
constraints = error.value.constraints
assert constraints.name == "Does not exist"
assert constraints.area_name is None
assert constraints.domains == {DOMAIN}
assert constraints.domains and (set(constraints.domains) == {DOMAIN})
assert constraints.device_classes is None
# Check wrong name with area
@ -231,7 +241,7 @@ async def test_get_temperature(
constraints = error.value.constraints
assert constraints.name == "Climate 1"
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
@ -239,62 +249,190 @@ async def test_get_temperature_no_entities(
hass: HomeAssistant,
) -> None:
"""Test HassClimateGetTemperature intent with no climate entities."""
assert await async_setup_component(hass, "homeassistant", {})
await climate_intent.async_setup_intents(hass)
await create_mock_platform(hass, [])
with pytest.raises(intent.IntentHandleError):
with pytest.raises(intent.MatchFailedError) as err:
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,
area_registry: ar.AreaRegistry,
entity_registry: er.EntityRegistry,
) -> 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)
climate_1 = MockClimateEntity()
climate_1._attr_name = "Climate 1"
climate_1._attr_unique_id = "1234"
climate_1._attr_current_temperature = 10.0
entity_registry.async_get_or_create(
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")
bedroom_area = area_registry.async_create(name="Bedroom")
entity_registry.async_update_entity(
climate_1.entity_id, area_id=living_room_area.id
)
entity_registry.async_update_entity(
climate_2.entity_id, area_id=living_room_area.id
)
with (
patch("homeassistant.core.StateMachine.get", return_value=None),
pytest.raises(intent.IntentHandleError),
):
await intent.async_handle(
hass, "test", climate_intent.INTENT_GET_TEMPERATURE, {}
)
with (
patch("homeassistant.core.StateMachine.async_all", return_value=[]),
pytest.raises(intent.MatchFailedError) as error,
):
# Should fail with empty name
with pytest.raises(intent.InvalidSlotInfo):
await intent.async_handle(
hass,
"test",
climate_intent.INTENT_GET_TEMPERATURE,
{"area": {"value": "Living Room"}},
{"name": {"value": ""}},
assistant=conversation.DOMAIN,
)
# Exception should contain details of what we tried to match
assert isinstance(error.value, intent.MatchFailedError)
assert error.value.result.no_match_reason == intent.MatchFailedReason.AREA
constraints = error.value.constraints
assert constraints.name is None
assert constraints.area_name == "Living Room"
assert constraints.domains == {DOMAIN}
assert constraints.device_classes is None
# Should fail with empty area
with pytest.raises(intent.InvalidSlotInfo):
await intent.async_handle(
hass,
"test",
climate_intent.INTENT_GET_TEMPERATURE,
{"area": {"value": ""}},
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."""
from unittest.mock import patch
import pytest
from homeassistant.components import (
@ -7,6 +9,7 @@ from homeassistant.components import (
cover,
light,
media_player,
todo,
vacuum,
valve,
)
@ -35,6 +38,27 @@ from homeassistant.setup import async_setup_component
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
async def init_components(hass: HomeAssistant):
"""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} == {
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"]
await hass.async_stop()
VALID_STORE_DATA = json.dumps(
{

View File

@ -878,6 +878,6 @@ async def test_supervisor_issues_detached_addon_missing(
placeholders={
"reference": "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
from homeassistant.components import conversation
from homeassistant.components.homeassistant.exposed_entities import async_expose_entity
from homeassistant.components.humidifier import (
ATTR_AVAILABLE_MODES,
ATTR_HUMIDITY,
@ -19,13 +21,22 @@ from homeassistant.const import (
STATE_ON,
)
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
async def test_intent_set_humidity(hass: HomeAssistant) -> None:
"""Test the set humidity intent."""
assert await async_setup_component(hass, "homeassistant", {})
hass.states.async_set(
"humidifier.bedroom_humidifier", STATE_ON, {ATTR_HUMIDITY: 40}
)
@ -38,6 +49,7 @@ async def test_intent_set_humidity(hass: HomeAssistant) -> None:
"test",
intent.INTENT_HUMIDITY,
{"name": {"value": "Bedroom humidifier"}, "humidity": {"value": "50"}},
assistant=conversation.DOMAIN,
)
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:
"""Test the set humidity intent for turned off humidifier."""
assert await async_setup_component(hass, "homeassistant", {})
hass.states.async_set(
"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",
intent.INTENT_HUMIDITY,
{"name": {"value": "Bedroom humidifier"}, "humidity": {"value": "50"}},
assistant=conversation.DOMAIN,
)
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:
"""Test the set mode intent."""
assert await async_setup_component(hass, "homeassistant", {})
hass.states.async_set(
"humidifier.bedroom_humidifier",
STATE_ON,
@ -108,6 +123,7 @@ async def test_intent_set_mode(hass: HomeAssistant) -> None:
"test",
intent.INTENT_MODE,
{"name": {"value": "Bedroom humidifier"}, "mode": {"value": "away"}},
assistant=conversation.DOMAIN,
)
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:
"""Test the set mode intent."""
assert await async_setup_component(hass, "homeassistant", {})
hass.states.async_set(
"humidifier.bedroom_humidifier",
STATE_OFF,
@ -146,6 +163,7 @@ async def test_intent_set_mode_and_turn_on(hass: HomeAssistant) -> None:
"test",
intent.INTENT_MODE,
{"name": {"value": "Bedroom humidifier"}, "mode": {"value": "away"}},
assistant=conversation.DOMAIN,
)
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:
"""Test the set mode intent where modes are not supported."""
assert await async_setup_component(hass, "homeassistant", {})
hass.states.async_set(
"humidifier.bedroom_humidifier", STATE_ON, {ATTR_HUMIDITY: 40}
)
@ -181,6 +200,7 @@ async def test_intent_set_mode_tests_feature(hass: HomeAssistant) -> None:
"test",
intent.INTENT_MODE,
{"name": {"value": "Bedroom humidifier"}, "mode": {"value": "away"}},
assistant=conversation.DOMAIN,
)
assert str(excinfo.value) == "Entity bedroom humidifier does not support modes"
assert len(mode_calls) == 0
@ -191,6 +211,7 @@ async def test_intent_set_unknown_mode(
hass: HomeAssistant, available_modes: list[str] | None
) -> None:
"""Test the set mode intent for unsupported mode."""
assert await async_setup_component(hass, "homeassistant", {})
hass.states.async_set(
"humidifier.bedroom_humidifier",
STATE_ON,
@ -210,6 +231,111 @@ async def test_intent_set_unknown_mode(
"test",
intent.INTENT_MODE,
{"name": {"value": "Bedroom humidifier"}, "mode": {"value": "eco"}},
assistant=conversation.DOMAIN,
)
assert str(excinfo.value) == "Entity bedroom humidifier does not support eco mode"
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"
)
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 = (
b'Content-Type: text/plain; charset="utf-8"\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"
)
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 = (
b"\r\nThis is a multi-part message in MIME format.\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 = (
"OK",
[
@ -249,6 +275,19 @@ TEST_FETCH_RESPONSE_MULTIPART = (
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 = (
"OK",
[

View File

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

View File

@ -54,11 +54,12 @@ async def test_climate_basic_temperature_set(
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(
hass: HomeAssistant, knx: KNXTestKit, heat_cool: bool
hass: HomeAssistant, knx: KNXTestKit, heat_cool_ga: str | None
) -> None:
"""Test KNX climate on/off."""
on_off_ga = "3/3/3"
await knx.setup_integration(
{
ClimateSchema.PLATFORM: {
@ -66,15 +67,15 @@ async def test_climate_on_off(
ClimateSchema.CONF_TEMPERATURE_ADDRESS: "1/2/3",
ClimateSchema.CONF_TARGET_TEMPERATURE_ADDRESS: "1/2/4",
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_HEAT_COOL_ADDRESS: "1/2/10",
ClimateSchema.CONF_HEAT_COOL_ADDRESS: heat_cool_ga,
ClimateSchema.CONF_HEAT_COOL_STATE_ADDRESS: "1/2/11",
}
if heat_cool
if heat_cool_ga
else {}
)
}
@ -82,7 +83,7 @@ async def test_climate_on_off(
await hass.async_block_till_done()
# read heat/cool state
if heat_cool:
if heat_cool_ga:
await knx.assert_read("1/2/11")
await knx.receive_response("1/2/11", 0) # cool
# read temperature state
@ -102,7 +103,7 @@ async def test_climate_on_off(
{"entity_id": "climate.test"},
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"
# turn on
@ -112,8 +113,8 @@ async def test_climate_on_off(
{"entity_id": "climate.test"},
blocking=True,
)
await knx.assert_write("1/2/8", 1)
if heat_cool:
await knx.assert_write(on_off_ga, 1)
if heat_cool_ga:
# does not fall back to default hvac mode after turn_on
assert hass.states.get("climate.test").state == "cool"
else:
@ -126,7 +127,8 @@ async def test_climate_on_off(
{"entity_id": "climate.test", "hvac_mode": HVACMode.OFF},
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
await hass.services.async_call(
@ -135,15 +137,20 @@ async def test_climate_on_off(
{"entity_id": "climate.test", "hvac_mode": HVACMode.HEAT},
blocking=True,
)
if heat_cool:
# only set new hvac_mode without changing on/off - actuator shall handle that
await knx.assert_write("1/2/10", 1)
if heat_cool_ga:
await knx.assert_write(heat_cool_ga, 1)
await knx.assert_write(on_off_ga, 1)
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."""
controller_mode_ga = "3/3/3"
await knx.setup_integration(
{
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_TARGET_TEMPERATURE_ADDRESS: "1/2/4",
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_ON_OFF_ADDRESS: "1/2/8",
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.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(
"climate",
"set_hvac_mode",
{"entity_id": "climate.test", "hvac_mode": HVACMode.OFF},
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(
"climate",
"set_hvac_mode",
{"entity_id": "climate.test", "hvac_mode": HVACMode.HEAT},
{"entity_id": "climate.test", "hvac_mode": HVACMode.COOL},
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(

View File

@ -9,6 +9,8 @@ import zoneinfo
import pytest
import voluptuous as vol
from homeassistant.components import conversation
from homeassistant.components.homeassistant.exposed_entities import async_expose_entity
from homeassistant.components.todo import (
DOMAIN,
TodoItem,
@ -23,6 +25,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import intent
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.setup import async_setup_component
from tests.common import (
MockConfigEntry,
@ -1110,6 +1113,7 @@ async def test_add_item_intent(
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test adding items to lists using an intent."""
assert await async_setup_component(hass, "homeassistant", {})
await todo_intent.async_setup_intents(hass)
entity1 = MockTodoListEntity()
@ -1128,6 +1132,7 @@ async def test_add_item_intent(
"test",
todo_intent.INTENT_LIST_ADD_ITEM,
{"item": {"value": "beer"}, "name": {"value": "list 1"}},
assistant=conversation.DOMAIN,
)
assert response.response_type == intent.IntentResponseType.ACTION_DONE
@ -1143,6 +1148,7 @@ async def test_add_item_intent(
"test",
todo_intent.INTENT_LIST_ADD_ITEM,
{"item": {"value": "cheese"}, "name": {"value": "List 2"}},
assistant=conversation.DOMAIN,
)
assert response.response_type == intent.IntentResponseType.ACTION_DONE
@ -1157,6 +1163,7 @@ async def test_add_item_intent(
"test",
todo_intent.INTENT_LIST_ADD_ITEM,
{"item": {"value": "wine"}, "name": {"value": "lIST 2"}},
assistant=conversation.DOMAIN,
)
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].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
with pytest.raises(intent.IntentHandleError):
with pytest.raises(intent.MatchFailedError):
await intent.async_handle(
hass,
"test",
todo_intent.INTENT_LIST_ADD_ITEM,
{"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."""
from unittest.mock import patch
import pytest
from homeassistant.components import conversation
from homeassistant.components.homeassistant.exposed_entities import async_expose_entity
from homeassistant.components.weather import (
DOMAIN,
WeatherEntity,
@ -16,15 +16,18 @@ from homeassistant.setup import async_setup_component
async def test_get_weather(hass: HomeAssistant) -> None:
"""Test get weather for first entity and by name."""
assert await async_setup_component(hass, "homeassistant", {})
assert await async_setup_component(hass, "weather", {"weather": {}})
entity1 = WeatherEntity()
entity1._attr_name = "Weather 1"
entity1.entity_id = "weather.test_1"
async_expose_entity(hass, conversation.DOMAIN, entity1.entity_id, True)
entity2 = WeatherEntity()
entity2._attr_name = "Weather 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])
@ -45,15 +48,31 @@ async def test_get_weather(hass: HomeAssistant) -> None:
"test",
weather_intent.INTENT_GET_WEATHER,
{"name": {"value": "Weather 2"}},
assistant=conversation.DOMAIN,
)
assert response.response_type == intent.IntentResponseType.QUERY_ANSWER
assert len(response.matched_states) == 1
state = response.matched_states[0]
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:
"""Test get weather with the wrong name."""
assert await async_setup_component(hass, "homeassistant", {})
assert await async_setup_component(hass, "weather", {"weather": {}})
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 weather_intent.async_setup_intents(hass)
async_expose_entity(hass, conversation.DOMAIN, entity1.entity_id, True)
# Incorrect name
with pytest.raises(intent.IntentHandleError):
with pytest.raises(intent.MatchFailedError) as err:
await intent.async_handle(
hass,
"test",
weather_intent.INTENT_GET_WEATHER,
{"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:
"""Test get weather with no weather entities."""
assert await async_setup_component(hass, "homeassistant", {})
assert await async_setup_component(hass, "weather", {"weather": {}})
await weather_intent.async_setup_intents(hass)
# No weather entities
with pytest.raises(intent.IntentHandleError):
await intent.async_handle(hass, "test", weather_intent.INTENT_GET_WEATHER, {})
async def test_get_weather_no_state(hass: HomeAssistant) -> None:
"""Test get weather when state is not returned."""
assert await async_setup_component(hass, "weather", {"weather": {}})
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
# 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, {})
with pytest.raises(intent.MatchFailedError) as err:
await intent.async_handle(
hass,
"test",
weather_intent.INTENT_GET_WEATHER,
{},
assistant=conversation.DOMAIN,
)
assert err.value.result.no_match_reason == intent.MatchFailedReason.DOMAIN

View File

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

View File

@ -591,7 +591,7 @@ async def test_discovery_requirements_mqtt(hass: HomeAssistant) -> None:
) as mock_process:
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
@ -608,13 +608,12 @@ async def test_discovery_requirements_ssdp(hass: HomeAssistant) -> None:
) as mock_process:
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[1][1][0],
mock_process.mock_calls[2][1][0],
mock_process.mock_calls[3][1][0],
} == {"network", "recorder", "isal"}
} == {"network", "recorder"}
@pytest.mark.parametrize(
@ -638,7 +637,7 @@ async def test_discovery_requirements_zeroconf(
) as mock_process:
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