mirror of
https://github.com/home-assistant/core.git
synced 2026-05-24 09:45:13 +02:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f18291259e | |||
| c056242390 | |||
| 9cbb14bbde | |||
| 6634c4ce78 | |||
| ae1355666b | |||
| 2d0d202b80 | |||
| 9fd48344f8 | |||
| 7b4ed59861 | |||
| 296d625121 |
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/github-cli:1": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "ghcr.io/devcontainers/features/github-cli@sha256:d22f50b70ed75339b4eed1ba9ecde3a1791f90e88d37936517e3bace0bbad671",
|
||||
"integrity": "sha256:d22f50b70ed75339b4eed1ba9ecde3a1791f90e88d37936517e3bace0bbad671"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,7 +70,6 @@ from .api import (
|
||||
async_register_callback,
|
||||
async_register_scanner,
|
||||
async_remove_scanner,
|
||||
async_request_active_scan,
|
||||
async_scanner_by_source,
|
||||
async_scanner_count,
|
||||
async_scanner_devices_by_address,
|
||||
@@ -129,7 +128,6 @@ __all__ = [
|
||||
"async_register_callback",
|
||||
"async_register_scanner",
|
||||
"async_remove_scanner",
|
||||
"async_request_active_scan",
|
||||
"async_scanner_by_source",
|
||||
"async_scanner_count",
|
||||
"async_scanner_devices_by_address",
|
||||
|
||||
@@ -284,19 +284,3 @@ def async_set_fallback_availability_interval(
|
||||
) -> None:
|
||||
"""Override the fallback availability timeout for a MAC address."""
|
||||
_get_manager(hass).async_set_fallback_availability_interval(address, interval)
|
||||
|
||||
|
||||
async def async_request_active_scan(
|
||||
hass: HomeAssistant, duration: float | None = None
|
||||
) -> None:
|
||||
"""Run an on-demand active sweep across every AUTO scanner.
|
||||
|
||||
Intended for config-flow discovery and other one-shot probes that
|
||||
need fresh advertisements without waiting for the periodic
|
||||
rediscovery cadence. Awaits ``duration`` seconds so the caller can
|
||||
then read newly discovered advertisements. Defaults to habluetooth's
|
||||
on-demand sweep duration when ``duration`` is not provided; the
|
||||
scheduler clamps the value to its allowed range. Concurrent callers
|
||||
dedupe to a single bus-wide window.
|
||||
"""
|
||||
await _get_manager(hass).async_request_active_scan(duration)
|
||||
|
||||
@@ -1,16 +1,33 @@
|
||||
"""Bluetooth support for esphome."""
|
||||
|
||||
from functools import partial
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from aioesphomeapi import APIClient, DeviceInfo
|
||||
from aioesphomeapi import (
|
||||
APIClient,
|
||||
APIVersion,
|
||||
BluetoothProxyFeature,
|
||||
BluetoothScannerMode,
|
||||
BluetoothScannerStateResponse,
|
||||
DeviceInfo,
|
||||
)
|
||||
from bleak_esphome import connect_scanner
|
||||
|
||||
from homeassistant.components.bluetooth import async_register_scanner
|
||||
from homeassistant.components.bluetooth import (
|
||||
BluetoothScanningMode,
|
||||
async_register_scanner,
|
||||
)
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .entry_data import RuntimeEntryData
|
||||
from .const import CONF_BLUETOOTH_SCANNING_MODE, DEFAULT_BLUETOOTH_SCANNING_MODE, DOMAIN
|
||||
from .entry_data import ESPHomeConfigEntry, RuntimeEntryData
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bleak_esphome.backend.scanner import ESPHomeScanner
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_VALID_SCANNING_MODES = {mode.value for mode in BluetoothScanningMode}
|
||||
|
||||
|
||||
@hass_callback
|
||||
@@ -23,6 +40,7 @@ def _async_unload(unload_callbacks: list[CALLBACK_TYPE]) -> None:
|
||||
@hass_callback
|
||||
def async_connect_scanner(
|
||||
hass: HomeAssistant,
|
||||
entry: ESPHomeConfigEntry,
|
||||
entry_data: RuntimeEntryData,
|
||||
cli: APIClient,
|
||||
device_info: DeviceInfo,
|
||||
@@ -35,17 +53,75 @@ def async_connect_scanner(
|
||||
scanner = client_data.scanner
|
||||
if TYPE_CHECKING:
|
||||
assert scanner is not None
|
||||
return partial(
|
||||
_async_unload,
|
||||
[
|
||||
async_register_scanner(
|
||||
hass,
|
||||
scanner,
|
||||
source_domain=DOMAIN,
|
||||
source_model=device_info.model,
|
||||
source_config_entry_id=entry_data.entry_id,
|
||||
source_device_id=device_id,
|
||||
),
|
||||
scanner.async_setup(),
|
||||
],
|
||||
)
|
||||
api_version = cli.api_version or APIVersion()
|
||||
feature_flags = device_info.bluetooth_proxy_feature_flags_compat(api_version)
|
||||
state_and_mode = bool(feature_flags & BluetoothProxyFeature.FEATURE_STATE_AND_MODE)
|
||||
# Pin mode before async_register_scanner so habluetooth spawns the AUTO worker.
|
||||
deferred_migration: CALLBACK_TYPE | None = None
|
||||
if state_and_mode:
|
||||
deferred_migration = _async_apply_scanning_mode(hass, entry, scanner, cli)
|
||||
callbacks: list[CALLBACK_TYPE] = [
|
||||
async_register_scanner(
|
||||
hass,
|
||||
scanner,
|
||||
source_domain=DOMAIN,
|
||||
source_model=device_info.model,
|
||||
source_config_entry_id=entry_data.entry_id,
|
||||
source_device_id=device_id,
|
||||
),
|
||||
scanner.async_setup(),
|
||||
]
|
||||
if deferred_migration is not None:
|
||||
callbacks.append(deferred_migration)
|
||||
return partial(_async_unload, callbacks)
|
||||
|
||||
|
||||
@hass_callback
|
||||
def _async_apply_scanning_mode(
|
||||
hass: HomeAssistant,
|
||||
entry: ESPHomeConfigEntry,
|
||||
scanner: ESPHomeScanner,
|
||||
cli: APIClient,
|
||||
) -> CALLBACK_TYPE | None:
|
||||
"""Apply saved scanning mode synchronously; migrate from configured_mode later."""
|
||||
saved = entry.options.get(CONF_BLUETOOTH_SCANNING_MODE)
|
||||
if saved is not None and saved not in _VALID_SCANNING_MODES:
|
||||
_LOGGER.warning("%s: unknown scanning mode %r", entry.title, saved)
|
||||
saved = None
|
||||
initial_value = saved if saved is not None else DEFAULT_BLUETOOTH_SCANNING_MODE
|
||||
scanner.async_set_scanning_mode(BluetoothScanningMode(initial_value))
|
||||
if saved is not None:
|
||||
return None
|
||||
|
||||
unsub_holder: list[CALLBACK_TYPE] = []
|
||||
|
||||
@hass_callback
|
||||
def _migrate(state: BluetoothScannerStateResponse) -> None:
|
||||
# proto3 unset enums decode to None; wait for a real value.
|
||||
if (configured_pb := state.configured_mode) is None:
|
||||
return
|
||||
if unsub_holder:
|
||||
unsub_holder.pop()()
|
||||
if configured_pb is BluetoothScannerMode.PASSIVE:
|
||||
new_mode = BluetoothScanningMode.PASSIVE
|
||||
else:
|
||||
new_mode = BluetoothScanningMode(DEFAULT_BLUETOOTH_SCANNING_MODE)
|
||||
hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
options={
|
||||
**entry.options,
|
||||
CONF_BLUETOOTH_SCANNING_MODE: new_mode.value,
|
||||
},
|
||||
)
|
||||
# AUTO -> AUTO is already pinned; only re-apply on a downgrade.
|
||||
if new_mode is not BluetoothScanningMode(DEFAULT_BLUETOOTH_SCANNING_MODE):
|
||||
scanner.async_set_scanning_mode(new_mode)
|
||||
|
||||
unsub_holder.append(cli.subscribe_bluetooth_scanner_state(_migrate))
|
||||
|
||||
@hass_callback
|
||||
def _unsubscribe() -> None:
|
||||
if unsub_holder:
|
||||
unsub_holder.pop()()
|
||||
|
||||
return _unsubscribe
|
||||
|
||||
@@ -9,6 +9,7 @@ from typing import Any, cast
|
||||
from aioesphomeapi import (
|
||||
APIClient,
|
||||
APIConnectionError,
|
||||
BluetoothProxyFeature,
|
||||
DeviceInfo,
|
||||
InvalidAuthAPIError,
|
||||
InvalidEncryptionKeyAPIError,
|
||||
@@ -20,6 +21,7 @@ import aiohttp
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import zeroconf
|
||||
from homeassistant.components.bluetooth import BluetoothScanningMode
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_ESPHOME,
|
||||
SOURCE_IGNORE,
|
||||
@@ -38,6 +40,11 @@ from homeassistant.data_entry_flow import AbortFlow, FlowResultType
|
||||
from homeassistant.helpers import discovery_flow
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
from homeassistant.helpers.importlib import async_import_module
|
||||
from homeassistant.helpers.selector import (
|
||||
SelectSelector,
|
||||
SelectSelectorConfig,
|
||||
SelectSelectorMode,
|
||||
)
|
||||
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
|
||||
from homeassistant.helpers.service_info.esphome import ESPHomeServiceInfo
|
||||
from homeassistant.helpers.service_info.hassio import HassioServiceInfo
|
||||
@@ -47,10 +54,12 @@ from homeassistant.util.json import json_loads_object
|
||||
|
||||
from .const import (
|
||||
CONF_ALLOW_SERVICE_CALLS,
|
||||
CONF_BLUETOOTH_SCANNING_MODE,
|
||||
CONF_DEVICE_NAME,
|
||||
CONF_NOISE_PSK,
|
||||
CONF_SUBSCRIBE_LOGS,
|
||||
DEFAULT_ALLOW_SERVICE_CALLS,
|
||||
DEFAULT_BLUETOOTH_SCANNING_MODE,
|
||||
DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS,
|
||||
DEFAULT_PORT,
|
||||
DOMAIN,
|
||||
@@ -68,6 +77,18 @@ _LOGGER = logging.getLogger(__name__)
|
||||
ZERO_NOISE_PSK = "MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA="
|
||||
DEFAULT_NAME = "ESPHome"
|
||||
|
||||
_BLUETOOTH_SCANNING_MODE_SELECTOR = SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=[
|
||||
BluetoothScanningMode.AUTO.value,
|
||||
BluetoothScanningMode.ACTIVE.value,
|
||||
BluetoothScanningMode.PASSIVE.value,
|
||||
],
|
||||
translation_key="bluetooth_scanning_mode",
|
||||
mode=SelectSelectorMode.DROPDOWN,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a esphome config flow."""
|
||||
@@ -936,18 +957,44 @@ class OptionsFlowHandler(OptionsFlowWithReload):
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
|
||||
data_schema = vol.Schema(
|
||||
{
|
||||
options = self.config_entry.options
|
||||
schema: dict[Any, Any] = {
|
||||
vol.Required(
|
||||
CONF_ALLOW_SERVICE_CALLS,
|
||||
default=options.get(
|
||||
CONF_ALLOW_SERVICE_CALLS, DEFAULT_ALLOW_SERVICE_CALLS
|
||||
),
|
||||
): bool,
|
||||
vol.Required(
|
||||
CONF_SUBSCRIBE_LOGS,
|
||||
default=options.get(CONF_SUBSCRIBE_LOGS, False),
|
||||
): bool,
|
||||
}
|
||||
if _entry_has_bluetooth_scanner(self.config_entry):
|
||||
schema[
|
||||
vol.Required(
|
||||
CONF_ALLOW_SERVICE_CALLS,
|
||||
default=self.config_entry.options.get(
|
||||
CONF_ALLOW_SERVICE_CALLS, DEFAULT_ALLOW_SERVICE_CALLS
|
||||
CONF_BLUETOOTH_SCANNING_MODE,
|
||||
default=options.get(
|
||||
CONF_BLUETOOTH_SCANNING_MODE, DEFAULT_BLUETOOTH_SCANNING_MODE
|
||||
),
|
||||
): bool,
|
||||
vol.Required(
|
||||
CONF_SUBSCRIBE_LOGS,
|
||||
default=self.config_entry.options.get(CONF_SUBSCRIBE_LOGS, False),
|
||||
): bool,
|
||||
}
|
||||
)
|
||||
] = _BLUETOOTH_SCANNING_MODE_SELECTOR
|
||||
return self.async_show_form(step_id="init", data_schema=vol.Schema(schema))
|
||||
|
||||
|
||||
@callback
|
||||
def _entry_has_bluetooth_scanner(entry: ESPHomeConfigEntry) -> bool:
|
||||
"""Return True if the entry exposes a bluetooth proxy scanner or has one saved."""
|
||||
# Keep showing the option if it was previously saved, even when the
|
||||
# device is offline or stops advertising the feature flag, so the
|
||||
# saved value isn't silently dropped on the next options save.
|
||||
if CONF_BLUETOOTH_SCANNING_MODE in entry.options:
|
||||
return True
|
||||
if entry.state is ConfigEntryState.LOADED and (
|
||||
device_info := entry.runtime_data.device_info
|
||||
):
|
||||
flags = device_info.bluetooth_proxy_feature_flags_compat(
|
||||
entry.runtime_data.api_version
|
||||
)
|
||||
return self.async_show_form(step_id="init", data_schema=data_schema)
|
||||
return bool(flags & BluetoothProxyFeature.FEATURE_STATE_AND_MODE)
|
||||
return False
|
||||
|
||||
@@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, Final
|
||||
|
||||
from awesomeversion import AwesomeVersion
|
||||
|
||||
from homeassistant.components.bluetooth import BluetoothScanningMode
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -18,9 +19,11 @@ CONF_SUBSCRIBE_LOGS = "subscribe_logs"
|
||||
CONF_DEVICE_NAME = "device_name"
|
||||
CONF_NOISE_PSK = "noise_psk"
|
||||
CONF_BLUETOOTH_MAC_ADDRESS = "bluetooth_mac_address"
|
||||
CONF_BLUETOOTH_SCANNING_MODE = "bluetooth_scanning_mode"
|
||||
|
||||
DEFAULT_ALLOW_SERVICE_CALLS = True
|
||||
DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS = False
|
||||
DEFAULT_BLUETOOTH_SCANNING_MODE = BluetoothScanningMode.AUTO.value
|
||||
|
||||
DEFAULT_PORT: Final = 6053
|
||||
|
||||
|
||||
@@ -669,7 +669,7 @@ class ESPHomeManager:
|
||||
if device_info.bluetooth_proxy_feature_flags_compat(api_version):
|
||||
entry_data.disconnect_callbacks.add(
|
||||
async_connect_scanner(
|
||||
hass, entry_data, cli, device_info, self.device_id
|
||||
hass, self.entry, entry_data, cli, device_info, self.device_id
|
||||
)
|
||||
)
|
||||
else:
|
||||
|
||||
@@ -209,13 +209,24 @@
|
||||
"init": {
|
||||
"data": {
|
||||
"allow_service_calls": "Allow the device to perform Home Assistant actions.",
|
||||
"bluetooth_scanning_mode": "Bluetooth scanning mode",
|
||||
"subscribe_logs": "Subscribe to logs from the device."
|
||||
},
|
||||
"data_description": {
|
||||
"allow_service_calls": "When enabled, ESPHome devices can perform Home Assistant actions or send events. Only enable this if you trust the device.",
|
||||
"bluetooth_scanning_mode": "Auto is recommended for most setups. It saves battery on your Bluetooth devices while still catching new devices and updates quickly.",
|
||||
"subscribe_logs": "When enabled, the device will send logs to Home Assistant and you can view them in the logs panel."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"bluetooth_scanning_mode": {
|
||||
"options": {
|
||||
"active": "Active (uses more device battery, fastest updates)",
|
||||
"auto": "Auto (recommended, saves device battery)",
|
||||
"passive": "Passive (lowest device battery use, some details may be missing)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,6 @@ ATTR_LAST_SAVED_AT: Final = "last_saved_at"
|
||||
|
||||
ATTR_DURATION: Final = "duration"
|
||||
ATTR_DISTANCE: Final = "distance"
|
||||
# pylint: disable-next=home-assistant-duplicate-const
|
||||
ATTR_ELEVATION: Final = "elevation"
|
||||
ATTR_HEIGHT: Final = "height"
|
||||
ATTR_WEIGHT: Final = "weight"
|
||||
ATTR_BODY: Final = "body"
|
||||
|
||||
@@ -63,5 +63,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/inkbird",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["inkbird-ble==1.1.1"]
|
||||
"requirements": ["inkbird-ble==1.1.2"]
|
||||
}
|
||||
|
||||
@@ -95,9 +95,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: RoborockConfigEntry) ->
|
||||
prefer_cache=False,
|
||||
)
|
||||
except RoborockInvalidCredentials as err:
|
||||
# pylint: disable-next=home-assistant-exception-message-with-translation
|
||||
raise ConfigEntryAuthFailed(
|
||||
"Invalid credentials",
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_credentials",
|
||||
) from err
|
||||
@@ -118,9 +116,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: RoborockConfigEntry) ->
|
||||
) from err
|
||||
except RoborockException as err:
|
||||
_LOGGER.debug("Failed to get Roborock home data: %s", err)
|
||||
# pylint: disable-next=home-assistant-exception-message-with-translation
|
||||
raise ConfigEntryNotReady(
|
||||
"Failed to get Roborock home data",
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="home_data_fail",
|
||||
) from err
|
||||
@@ -178,9 +174,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: RoborockConfigEntry) ->
|
||||
len(v1_coords) + len(a01_coords) + len(b01_q7_coords) + len(b01_q10_coords) == 0
|
||||
and enabled_devices
|
||||
):
|
||||
# pylint: disable-next=home-assistant-exception-message-with-translation
|
||||
raise ConfigEntryNotReady(
|
||||
"No devices were able to successfully setup",
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="no_coordinators",
|
||||
)
|
||||
|
||||
@@ -161,11 +161,9 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceState | None]):
|
||||
_LOGGER.info("Home discovery skipped while device is busy/cleaning")
|
||||
except RoborockException as err:
|
||||
_LOGGER.debug("Failed to get maps: %s", err)
|
||||
# pylint: disable-next=home-assistant-exception-placeholder-mismatch
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="map_failure",
|
||||
translation_placeholders={"error": str(err)},
|
||||
) from err
|
||||
else:
|
||||
# Force a map refresh on first setup
|
||||
|
||||
@@ -71,6 +71,13 @@ NO_ENTITIES_PROMPT = (
|
||||
"to their voice assistant in Home Assistant."
|
||||
)
|
||||
|
||||
DEVICE_CONTROL_TOOL_USAGE_PROMPT = (
|
||||
"When controlling Home Assistant always call the intent tools. "
|
||||
"Use HassTurnOn to lock and HassTurnOff to unlock a lock. "
|
||||
"When controlling a device, prefer passing just name and domain. "
|
||||
"When controlling an area, prefer passing just area name and domain."
|
||||
)
|
||||
|
||||
DYNAMIC_CONTEXT_PROMPT = (
|
||||
"You ARE equipped to answer questions about the"
|
||||
" current state of\n"
|
||||
@@ -496,27 +503,33 @@ class AssistAPI(API):
|
||||
) -> str:
|
||||
if not exposed_entities or not exposed_entities["entities"]:
|
||||
return NO_ENTITIES_PROMPT
|
||||
return "\n".join(
|
||||
[
|
||||
*self._async_get_preable(llm_context),
|
||||
*self._async_get_exposed_entities_prompt(llm_context, exposed_entities),
|
||||
]
|
||||
)
|
||||
|
||||
# Collect all parts, filtering out any None values
|
||||
prompt_parts = [
|
||||
DEVICE_CONTROL_TOOL_USAGE_PROMPT,
|
||||
DYNAMIC_CONTEXT_PROMPT,
|
||||
*self._async_get_exposed_entities_prompt(exposed_entities),
|
||||
self._async_get_voice_satellite_area_prompt(llm_context),
|
||||
self._async_get_no_timer_prompt(llm_context),
|
||||
]
|
||||
|
||||
# Filter out None and empty strings before joining
|
||||
return "\n".join([part for part in prompt_parts if part])
|
||||
|
||||
@callback
|
||||
def _async_get_preable(self, llm_context: LLMContext) -> list[str]:
|
||||
"""Return the prompt for the API."""
|
||||
def _async_get_no_timer_prompt(self, llm_context: LLMContext) -> str | None:
|
||||
if not llm_context.device_id or not async_device_supports_timers(
|
||||
self.hass, llm_context.device_id
|
||||
):
|
||||
return "This device is not able to start timers."
|
||||
return None
|
||||
|
||||
prompt = [
|
||||
(
|
||||
"When controlling Home Assistant always call the intent tools. "
|
||||
"Use HassTurnOn to lock and HassTurnOff to unlock a lock. "
|
||||
"When controlling a device, prefer passing just name and domain. "
|
||||
"When controlling an area, prefer passing just area name and domain."
|
||||
)
|
||||
]
|
||||
area: ar.AreaEntry | None = None
|
||||
@callback
|
||||
def _async_get_voice_satellite_area_prompt(self, llm_context: LLMContext) -> str:
|
||||
"""Return the area prompt for the voice satellite."""
|
||||
floor: fr.FloorEntry | None = None
|
||||
area: ar.AreaEntry | None = None
|
||||
extra = ""
|
||||
if llm_context.device_id:
|
||||
device_reg = dr.async_get(self.hass)
|
||||
device = device_reg.async_get(llm_context.device_id)
|
||||
@@ -535,28 +548,18 @@ class AssistAPI(API):
|
||||
)
|
||||
|
||||
if floor and area:
|
||||
prompt.append(f"You are in area {area.name} (floor {floor.name}) {extra}")
|
||||
elif area:
|
||||
prompt.append(f"You are in area {area.name} {extra}")
|
||||
else:
|
||||
prompt.append(
|
||||
"When a user asks to turn on all devices of a specific type, "
|
||||
"ask user to specify an area, unless there"
|
||||
" is only one device of that type."
|
||||
)
|
||||
|
||||
if not llm_context.device_id or not async_device_supports_timers(
|
||||
self.hass, llm_context.device_id
|
||||
):
|
||||
prompt.append("This device is not able to start timers.")
|
||||
|
||||
prompt.append(DYNAMIC_CONTEXT_PROMPT)
|
||||
|
||||
return prompt
|
||||
return f"You are in area {area.name} (floor {floor.name}) {extra}".strip()
|
||||
if area:
|
||||
return f"You are in area {area.name} {extra}".strip()
|
||||
return (
|
||||
"When a user asks to turn on all devices of a specific type, "
|
||||
"ask the user to specify an area, unless there"
|
||||
" is only one device of that type."
|
||||
)
|
||||
|
||||
@callback
|
||||
def _async_get_exposed_entities_prompt(
|
||||
self, llm_context: LLMContext, exposed_entities: dict | None
|
||||
self, exposed_entities: dict | None
|
||||
) -> list[str]:
|
||||
"""Return the prompt for the API for exposed entities."""
|
||||
prompt = []
|
||||
|
||||
Generated
+1
-1
@@ -1359,7 +1359,7 @@ influxdb==5.3.1
|
||||
infrared-protocols==5.4.0
|
||||
|
||||
# homeassistant.components.inkbird
|
||||
inkbird-ble==1.1.1
|
||||
inkbird-ble==1.1.2
|
||||
|
||||
# homeassistant.components.insteon
|
||||
insteon-frontend-home-assistant==0.6.2
|
||||
|
||||
@@ -14,7 +14,6 @@ from homeassistant.components.bluetooth import (
|
||||
BluetoothServiceInfo,
|
||||
HaBluetoothConnector,
|
||||
async_clear_advertisement_history,
|
||||
async_request_active_scan,
|
||||
async_scanner_by_source,
|
||||
async_scanner_devices_by_address,
|
||||
)
|
||||
@@ -279,12 +278,3 @@ async def test_clear_advertisement_history(hass: HomeAssistant) -> None:
|
||||
assert len(callbacks) == 2
|
||||
|
||||
cancel()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_bluetooth")
|
||||
async def test_async_request_active_scan(hass: HomeAssistant) -> None:
|
||||
"""Test async_request_active_scan completes without error."""
|
||||
await async_request_active_scan(hass, 0.01)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
await async_request_active_scan(hass, 0)
|
||||
|
||||
@@ -1,12 +1,30 @@
|
||||
"""Test the ESPHome bluetooth integration."""
|
||||
|
||||
from unittest.mock import patch
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from aioesphomeapi import (
|
||||
BluetoothProxyFeature,
|
||||
BluetoothScannerMode,
|
||||
BluetoothScannerState,
|
||||
BluetoothScannerStateResponse,
|
||||
)
|
||||
|
||||
from homeassistant.components import bluetooth
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.components.bluetooth import BluetoothScanningMode
|
||||
from homeassistant.components.esphome.const import CONF_BLUETOOTH_SCANNING_MODE
|
||||
from homeassistant.core import HomeAssistant, callback as hass_callback
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from .conftest import MockESPHomeDevice
|
||||
from .conftest import MockBluetoothEntryType, MockESPHomeDevice
|
||||
|
||||
_PROXY_WITH_STATE_AND_MODE = (
|
||||
BluetoothProxyFeature.PASSIVE_SCAN
|
||||
| BluetoothProxyFeature.ACTIVE_CONNECTIONS
|
||||
| BluetoothProxyFeature.RAW_ADVERTISEMENTS
|
||||
| BluetoothProxyFeature.FEATURE_STATE_AND_MODE
|
||||
)
|
||||
|
||||
|
||||
async def test_bluetooth_connect_with_raw_adv(
|
||||
@@ -94,3 +112,235 @@ async def test_bluetooth_cleanup_on_remove_entry(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
remove_mock.assert_called_once_with(hass, scanner.source)
|
||||
|
||||
|
||||
async def test_scanning_mode_saved_option_applied(
|
||||
hass: HomeAssistant,
|
||||
mock_bluetooth_entry: MockBluetoothEntryType,
|
||||
) -> None:
|
||||
"""A saved CONF_BLUETOOTH_SCANNING_MODE is applied immediately to the proxy."""
|
||||
device = await mock_bluetooth_entry(
|
||||
bluetooth_proxy_feature_flags=_PROXY_WITH_STATE_AND_MODE
|
||||
)
|
||||
hass.config_entries.async_update_entry(
|
||||
device.entry,
|
||||
options={**device.entry.options, CONF_BLUETOOTH_SCANNING_MODE: "passive"},
|
||||
)
|
||||
set_mode_mock = MagicMock()
|
||||
device.client.bluetooth_scanner_set_mode = set_mode_mock
|
||||
await hass.config_entries.async_reload(device.entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
set_mode_mock.assert_any_call(BluetoothScannerMode.PASSIVE)
|
||||
|
||||
|
||||
async def test_scanning_mode_invalid_option_falls_back_to_default(
|
||||
hass: HomeAssistant,
|
||||
mock_bluetooth_entry: MockBluetoothEntryType,
|
||||
) -> None:
|
||||
"""A malformed saved value falls back to the AUTO default instead of raising."""
|
||||
device = await mock_bluetooth_entry(
|
||||
bluetooth_proxy_feature_flags=_PROXY_WITH_STATE_AND_MODE
|
||||
)
|
||||
hass.config_entries.async_update_entry(
|
||||
device.entry,
|
||||
options={**device.entry.options, CONF_BLUETOOTH_SCANNING_MODE: "bogus"},
|
||||
)
|
||||
set_mode_mock = MagicMock()
|
||||
device.client.bluetooth_scanner_set_mode = set_mode_mock
|
||||
await hass.config_entries.async_reload(device.entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# AUTO maps to PASSIVE on the firmware.
|
||||
set_mode_mock.assert_any_call(BluetoothScannerMode.PASSIVE)
|
||||
|
||||
|
||||
async def test_scanning_mode_migration_passive_is_honored(
|
||||
hass: HomeAssistant,
|
||||
mock_bluetooth_entry: MockBluetoothEntryType,
|
||||
) -> None:
|
||||
"""Proxy configured PASSIVE in YAML is honored on first state update."""
|
||||
set_mode_mock = MagicMock()
|
||||
state_subscriptions: list[Callable[[BluetoothScannerStateResponse], None]] = []
|
||||
|
||||
def _subscribe(
|
||||
callback: Callable[[BluetoothScannerStateResponse], None],
|
||||
) -> Callable[[], None]:
|
||||
state_subscriptions.append(callback)
|
||||
return lambda: state_subscriptions.remove(callback)
|
||||
|
||||
device = await mock_bluetooth_entry(
|
||||
bluetooth_proxy_feature_flags=_PROXY_WITH_STATE_AND_MODE
|
||||
)
|
||||
device.client.bluetooth_scanner_set_mode = set_mode_mock
|
||||
device.client.subscribe_bluetooth_scanner_state = _subscribe
|
||||
await hass.config_entries.async_reload(device.entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert state_subscriptions
|
||||
for callback in state_subscriptions[:]:
|
||||
callback(
|
||||
BluetoothScannerStateResponse(
|
||||
state=BluetoothScannerState.RUNNING,
|
||||
mode=BluetoothScannerMode.PASSIVE,
|
||||
configured_mode=BluetoothScannerMode.PASSIVE,
|
||||
)
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert device.entry.options[CONF_BLUETOOTH_SCANNING_MODE] == "passive"
|
||||
set_mode_mock.assert_any_call(BluetoothScannerMode.PASSIVE)
|
||||
|
||||
|
||||
async def test_scanning_mode_migration_waits_for_known_configured_mode(
|
||||
hass: HomeAssistant,
|
||||
mock_bluetooth_entry: MockBluetoothEntryType,
|
||||
) -> None:
|
||||
"""An initial state with configured_mode=None must not commit a migration."""
|
||||
state_subscriptions: list[Callable[[BluetoothScannerStateResponse], None]] = []
|
||||
set_mode_mock = MagicMock()
|
||||
|
||||
def _subscribe(
|
||||
callback: Callable[[BluetoothScannerStateResponse], None],
|
||||
) -> Callable[[], None]:
|
||||
state_subscriptions.append(callback)
|
||||
return lambda: state_subscriptions.remove(callback)
|
||||
|
||||
device = await mock_bluetooth_entry(
|
||||
bluetooth_proxy_feature_flags=_PROXY_WITH_STATE_AND_MODE
|
||||
)
|
||||
device.client.bluetooth_scanner_set_mode = set_mode_mock
|
||||
device.client.subscribe_bluetooth_scanner_state = _subscribe
|
||||
await hass.config_entries.async_reload(device.entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert state_subscriptions
|
||||
for callback in state_subscriptions[:]:
|
||||
callback(
|
||||
BluetoothScannerStateResponse(
|
||||
state=BluetoothScannerState.RUNNING,
|
||||
mode=None,
|
||||
configured_mode=None,
|
||||
)
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert CONF_BLUETOOTH_SCANNING_MODE not in device.entry.options
|
||||
# A second response with a real configured_mode commits the migration.
|
||||
for callback in state_subscriptions[:]:
|
||||
callback(
|
||||
BluetoothScannerStateResponse(
|
||||
state=BluetoothScannerState.RUNNING,
|
||||
mode=BluetoothScannerMode.PASSIVE,
|
||||
configured_mode=BluetoothScannerMode.PASSIVE,
|
||||
)
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert device.entry.options[CONF_BLUETOOTH_SCANNING_MODE] == "passive"
|
||||
|
||||
|
||||
async def test_scanning_mode_pending_subscription_unsubscribes_on_unload(
|
||||
hass: HomeAssistant,
|
||||
mock_bluetooth_entry: MockBluetoothEntryType,
|
||||
) -> None:
|
||||
"""Unloading before the first state update cancels the migration subscription."""
|
||||
state_subscriptions: list[Callable[[BluetoothScannerStateResponse], None]] = []
|
||||
unsub_calls: list[Callable[[BluetoothScannerStateResponse], None]] = []
|
||||
|
||||
def _subscribe(
|
||||
callback: Callable[[BluetoothScannerStateResponse], None],
|
||||
) -> Callable[[], None]:
|
||||
state_subscriptions.append(callback)
|
||||
|
||||
def _unsub() -> None:
|
||||
unsub_calls.append(callback)
|
||||
state_subscriptions.remove(callback)
|
||||
|
||||
return _unsub
|
||||
|
||||
device = await mock_bluetooth_entry(
|
||||
bluetooth_proxy_feature_flags=_PROXY_WITH_STATE_AND_MODE
|
||||
)
|
||||
device.client.subscribe_bluetooth_scanner_state = _subscribe
|
||||
await hass.config_entries.async_reload(device.entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
# The migration subscription is pending; tear the entry down without
|
||||
# firing a state update so _unsubscribe in bluetooth.py runs the
|
||||
# cancellation arm.
|
||||
assert state_subscriptions
|
||||
await hass.config_entries.async_unload(device.entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert unsub_calls
|
||||
|
||||
|
||||
async def test_scanning_mode_migration_active_becomes_auto(
|
||||
hass: HomeAssistant,
|
||||
mock_bluetooth_entry: MockBluetoothEntryType,
|
||||
) -> None:
|
||||
"""Proxy configured ACTIVE migrates to AUTO on first state update."""
|
||||
set_mode_mock = MagicMock()
|
||||
state_subscriptions: list[Callable[[BluetoothScannerStateResponse], None]] = []
|
||||
|
||||
def _subscribe(
|
||||
callback: Callable[[BluetoothScannerStateResponse], None],
|
||||
) -> Callable[[], None]:
|
||||
state_subscriptions.append(callback)
|
||||
return lambda: state_subscriptions.remove(callback)
|
||||
|
||||
device = await mock_bluetooth_entry(
|
||||
bluetooth_proxy_feature_flags=_PROXY_WITH_STATE_AND_MODE
|
||||
)
|
||||
device.client.bluetooth_scanner_set_mode = set_mode_mock
|
||||
device.client.subscribe_bluetooth_scanner_state = _subscribe
|
||||
await hass.config_entries.async_reload(device.entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# AUTO was applied at setup before async_register_scanner so habluetooth's
|
||||
# scheduler spawns a worker; AUTO maps to PASSIVE on the firmware.
|
||||
assert set_mode_mock.call_args_list == [((BluetoothScannerMode.PASSIVE,), {})]
|
||||
set_mode_mock.reset_mock()
|
||||
assert state_subscriptions
|
||||
for callback in state_subscriptions[:]:
|
||||
callback(
|
||||
BluetoothScannerStateResponse(
|
||||
state=BluetoothScannerState.RUNNING,
|
||||
mode=BluetoothScannerMode.ACTIVE,
|
||||
configured_mode=BluetoothScannerMode.ACTIVE,
|
||||
)
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert device.entry.options[CONF_BLUETOOTH_SCANNING_MODE] == "auto"
|
||||
# AUTO -> AUTO does not re-send a firmware command.
|
||||
set_mode_mock.assert_not_called()
|
||||
|
||||
|
||||
async def test_scanning_mode_default_pinned_before_register(
|
||||
hass: HomeAssistant,
|
||||
mock_bluetooth_entry: MockBluetoothEntryType,
|
||||
) -> None:
|
||||
"""The default AUTO is applied immediately so the AUTO worker spawns at register."""
|
||||
set_mode_mock = MagicMock()
|
||||
requested_at_register: list[BluetoothScanningMode | None] = []
|
||||
real_register = bluetooth.async_register_scanner
|
||||
|
||||
@hass_callback
|
||||
def _spy_register(*args: Any, **kwargs: Any) -> Callable[[], None]:
|
||||
requested_at_register.append(args[1].requested_mode)
|
||||
return real_register(*args, **kwargs)
|
||||
|
||||
device = await mock_bluetooth_entry(
|
||||
bluetooth_proxy_feature_flags=_PROXY_WITH_STATE_AND_MODE
|
||||
)
|
||||
device.client.bluetooth_scanner_set_mode = set_mode_mock
|
||||
with patch(
|
||||
"homeassistant.components.esphome.bluetooth.async_register_scanner",
|
||||
_spy_register,
|
||||
):
|
||||
await hass.config_entries.async_reload(device.entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# AUTO -> PASSIVE is sent before async_register_scanner, so the
|
||||
# habluetooth auto-mode worker is spawned at registration time.
|
||||
set_mode_mock.assert_called_once_with(BluetoothScannerMode.PASSIVE)
|
||||
assert requested_at_register == [BluetoothScanningMode.AUTO]
|
||||
|
||||
@@ -8,6 +8,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from aioesphomeapi import (
|
||||
APIClient,
|
||||
APIConnectionError,
|
||||
BluetoothProxyFeature,
|
||||
DeviceInfo,
|
||||
InvalidAuthAPIError,
|
||||
InvalidEncryptionKeyAPIError,
|
||||
@@ -22,9 +23,11 @@ from homeassistant import config_entries
|
||||
from homeassistant.components.esphome import dashboard
|
||||
from homeassistant.components.esphome.const import (
|
||||
CONF_ALLOW_SERVICE_CALLS,
|
||||
CONF_BLUETOOTH_SCANNING_MODE,
|
||||
CONF_DEVICE_NAME,
|
||||
CONF_NOISE_PSK,
|
||||
CONF_SUBSCRIBE_LOGS,
|
||||
DEFAULT_BLUETOOTH_SCANNING_MODE,
|
||||
DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS,
|
||||
DOMAIN,
|
||||
)
|
||||
@@ -43,7 +46,11 @@ from homeassistant.helpers.service_info.mqtt import MqttServiceInfo
|
||||
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||
|
||||
from . import VALID_NOISE_PSK
|
||||
from .conftest import MockESPHomeDeviceType, MockGenericDeviceEntryType
|
||||
from .conftest import (
|
||||
MockBluetoothEntryType,
|
||||
MockESPHomeDeviceType,
|
||||
MockGenericDeviceEntryType,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
@@ -2078,6 +2085,105 @@ async def test_option_flow_subscribe_logs(
|
||||
assert len(mock_reload.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_option_flow_shows_saved_scanning_mode_when_proxy_unavailable(
|
||||
hass: HomeAssistant,
|
||||
mock_client: APIClient,
|
||||
mock_generic_device_entry: MockGenericDeviceEntryType,
|
||||
) -> None:
|
||||
"""A previously-saved mode keeps surfacing even if the proxy feature flag is gone."""
|
||||
entry = await mock_generic_device_entry(mock_client=mock_client)
|
||||
hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
options={**entry.options, CONF_BLUETOOTH_SCANNING_MODE: "passive"},
|
||||
)
|
||||
result = await hass.config_entries.options.async_init(entry.entry_id)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert CONF_BLUETOOTH_SCANNING_MODE in result["data_schema"].schema
|
||||
with patch("homeassistant.components.esphome.async_setup_entry", return_value=True):
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_ALLOW_SERVICE_CALLS: False,
|
||||
CONF_SUBSCRIBE_LOGS: False,
|
||||
CONF_BLUETOOTH_SCANNING_MODE: "auto",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["data"][CONF_BLUETOOTH_SCANNING_MODE] == "auto"
|
||||
|
||||
|
||||
async def test_option_flow_unloaded_entry_without_saved_mode(
|
||||
hass: HomeAssistant,
|
||||
mock_client: APIClient,
|
||||
mock_generic_device_entry: MockGenericDeviceEntryType,
|
||||
) -> None:
|
||||
"""An unloaded entry without a saved scanning mode hides the option."""
|
||||
entry = await mock_generic_device_entry(mock_client=mock_client)
|
||||
await hass.config_entries.async_unload(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
result = await hass.config_entries.options.async_init(entry.entry_id)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert CONF_BLUETOOTH_SCANNING_MODE not in result["data_schema"].schema
|
||||
|
||||
|
||||
async def test_option_flow_hides_bluetooth_scanning_mode_without_proxy(
|
||||
hass: HomeAssistant,
|
||||
mock_client: APIClient,
|
||||
mock_generic_device_entry: MockGenericDeviceEntryType,
|
||||
) -> None:
|
||||
"""Devices without a bluetooth proxy must not see the scanning mode option."""
|
||||
entry = await mock_generic_device_entry(mock_client=mock_client)
|
||||
|
||||
result = await hass.config_entries.options.async_init(entry.entry_id)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert CONF_BLUETOOTH_SCANNING_MODE not in result["data_schema"].schema
|
||||
with patch("homeassistant.components.esphome.async_setup_entry", return_value=True):
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_ALLOW_SERVICE_CALLS: False, CONF_SUBSCRIBE_LOGS: False},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert CONF_BLUETOOTH_SCANNING_MODE not in result["data"]
|
||||
|
||||
|
||||
async def test_option_flow_bluetooth_scanning_mode(
|
||||
hass: HomeAssistant,
|
||||
mock_bluetooth_entry: MockBluetoothEntryType,
|
||||
) -> None:
|
||||
"""Bluetooth proxy devices with FEATURE_STATE_AND_MODE expose the option."""
|
||||
device = await mock_bluetooth_entry(
|
||||
bluetooth_proxy_feature_flags=BluetoothProxyFeature.PASSIVE_SCAN
|
||||
| BluetoothProxyFeature.ACTIVE_CONNECTIONS
|
||||
| BluetoothProxyFeature.RAW_ADVERTISEMENTS
|
||||
| BluetoothProxyFeature.FEATURE_STATE_AND_MODE
|
||||
)
|
||||
entry = device.entry
|
||||
|
||||
result = await hass.config_entries.options.async_init(entry.entry_id)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["data_schema"]({}) == {
|
||||
CONF_ALLOW_SERVICE_CALLS: DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS,
|
||||
CONF_SUBSCRIBE_LOGS: False,
|
||||
CONF_BLUETOOTH_SCANNING_MODE: DEFAULT_BLUETOOTH_SCANNING_MODE,
|
||||
}
|
||||
with patch("homeassistant.components.esphome.async_setup_entry", return_value=True):
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_ALLOW_SERVICE_CALLS: False,
|
||||
CONF_SUBSCRIBE_LOGS: False,
|
||||
CONF_BLUETOOTH_SCANNING_MODE: "passive",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["data"][CONF_BLUETOOTH_SCANNING_MODE] == "passive"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_setup_entry", "mock_zeroconf")
|
||||
async def test_user_discovers_name_no_dashboard(
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -61,7 +61,7 @@ INITIALIZE_MESSAGE = {
|
||||
}
|
||||
EVENT_PREFIX = "event: "
|
||||
DATA_PREFIX = "data: "
|
||||
EXPECTED_PROMPT_SUFFIX = """
|
||||
EXPECTED_PROMPT_ENTITY_DEFINITION = """
|
||||
- names: Kitchen Light
|
||||
domain: light
|
||||
areas: Kitchen
|
||||
@@ -481,7 +481,7 @@ async def test_prompt_get(
|
||||
assert result.messages[0].role == "assistant"
|
||||
assert result.messages[0].content.type == "text"
|
||||
assert "When controlling Home Assistant" in result.messages[0].content.text
|
||||
assert result.messages[0].content.text.endswith(EXPECTED_PROMPT_SUFFIX)
|
||||
assert EXPECTED_PROMPT_ENTITY_DEFINITION in result.messages[0].content.text
|
||||
|
||||
|
||||
async def test_get_unknown_prompt(
|
||||
|
||||
+13
-12
@@ -419,6 +419,7 @@ async def test_assist_api_prompt(
|
||||
device_id=None,
|
||||
)
|
||||
api = await llm.async_get_api(hass, "assist", llm_context)
|
||||
|
||||
assert api.api_prompt == (
|
||||
"Only if the user wants to control a device, tell them to expose"
|
||||
" entities to their "
|
||||
@@ -677,7 +678,7 @@ Static Context: An overview of the areas and the devices in this smart home:
|
||||
|
||||
area_prompt = (
|
||||
"When a user asks to turn on all devices of a specific type, "
|
||||
"ask user to specify an area, unless there is only one device of that type."
|
||||
"ask the user to specify an area, unless there is only one device of that type."
|
||||
)
|
||||
dynamic_context_prompt = (
|
||||
"You ARE equipped to answer questions about the"
|
||||
@@ -708,10 +709,10 @@ Static Context: An overview of the areas and the devices in this smart home:
|
||||
api = await llm.async_get_api(hass, "assist", llm_context)
|
||||
assert api.api_prompt == (
|
||||
f"""{first_part_prompt}
|
||||
{area_prompt}
|
||||
{no_timer_prompt}
|
||||
{dynamic_context_prompt}
|
||||
{stateless_exposed_entities_prompt}"""
|
||||
{stateless_exposed_entities_prompt}
|
||||
{area_prompt}
|
||||
{no_timer_prompt}"""
|
||||
)
|
||||
|
||||
# Verify that the GetLiveContext tool returns the same results
|
||||
@@ -733,10 +734,10 @@ Static Context: An overview of the areas and the devices in this smart home:
|
||||
api = await llm.async_get_api(hass, "assist", llm_context)
|
||||
assert api.api_prompt == (
|
||||
f"""{first_part_prompt}
|
||||
{area_prompt}
|
||||
{no_timer_prompt}
|
||||
{dynamic_context_prompt}
|
||||
{stateless_exposed_entities_prompt}"""
|
||||
{stateless_exposed_entities_prompt}
|
||||
{area_prompt}
|
||||
{no_timer_prompt}"""
|
||||
)
|
||||
|
||||
# Add floor
|
||||
@@ -750,10 +751,10 @@ Static Context: An overview of the areas and the devices in this smart home:
|
||||
api = await llm.async_get_api(hass, "assist", llm_context)
|
||||
assert api.api_prompt == (
|
||||
f"""{first_part_prompt}
|
||||
{area_prompt}
|
||||
{no_timer_prompt}
|
||||
{dynamic_context_prompt}
|
||||
{stateless_exposed_entities_prompt}"""
|
||||
{stateless_exposed_entities_prompt}
|
||||
{area_prompt}
|
||||
{no_timer_prompt}"""
|
||||
)
|
||||
|
||||
# Register device for timers
|
||||
@@ -763,9 +764,9 @@ Static Context: An overview of the areas and the devices in this smart home:
|
||||
# The no_timer_prompt is gone
|
||||
assert api.api_prompt == (
|
||||
f"""{first_part_prompt}
|
||||
{area_prompt}
|
||||
{dynamic_context_prompt}
|
||||
{stateless_exposed_entities_prompt}"""
|
||||
{stateless_exposed_entities_prompt}
|
||||
{area_prompt}"""
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user