mirror of
https://github.com/home-assistant/core.git
synced 2025-07-24 05:37:44 +00:00
2024.6.1 (#119096)
This commit is contained in:
commit
b28cdcfc49
@ -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.
|
||||
|
@ -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
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"domain": "airgradient",
|
||||
"name": "Airgradient",
|
||||
"name": "AirGradient",
|
||||
"codeowners": ["@airgradienthq", "@joostlek"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/airgradient",
|
||||
|
@ -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,
|
||||
),
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
|
||||
|
@ -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."""
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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")
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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."""
|
||||
|
@ -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())
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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."""
|
||||
|
@ -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,
|
||||
|
@ -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."""
|
||||
|
@ -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,
|
||||
|
@ -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."""
|
||||
|
@ -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)
|
||||
)
|
||||
|
@ -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)
|
||||
|
@ -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."""
|
||||
|
@ -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)
|
||||
|
@ -88,6 +88,7 @@ class TuyaAlarmEntity(TuyaEntity, AlarmControlPanelEntity):
|
||||
"""Tuya Alarm Entity."""
|
||||
|
||||
_attr_name = None
|
||||
_attr_code_arm_required = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
@ -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,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
@ -7,5 +7,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["holidays"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["holidays==0.49"]
|
||||
"requirements": ["holidays==0.50"]
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -94,7 +94,7 @@
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
"airgradient": {
|
||||
"name": "Airgradient",
|
||||
"name": "AirGradient",
|
||||
"integration_type": "device",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling"
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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>,
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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(
|
||||
{
|
||||
|
@ -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",
|
||||
},
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
[
|
||||
|
@ -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",
|
||||
],
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user