Add get/set/config messages for voice assistant (#956)

This commit is contained in:
Michael Hansen 2024-09-12 19:49:39 -05:00 committed by GitHub
parent bf6aff7ab0
commit 709cb3fd15
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 332 additions and 109 deletions

View File

@ -1589,6 +1589,36 @@ message VoiceAssistantAnnounceFinished {
bool success = 1; bool success = 1;
} }
message VoiceAssistantWakeWord {
uint32 id = 1;
string wake_word = 2;
repeated string trained_languages = 3;
}
message VoiceAssistantConfigurationRequest {
option (id) = 121;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_VOICE_ASSISTANT";
}
message VoiceAssistantConfigurationResponse {
option (id) = 122;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_VOICE_ASSISTANT";
repeated VoiceAssistantWakeWord available_wake_words = 1;
repeated uint32 active_wake_words = 2;
uint32 max_active_wake_words = 3;
}
message VoiceAssistantSetConfiguration {
option (id) = 123;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_VOICE_ASSISTANT";
repeated uint32 active_wake_words = 1;
}
// ==================== ALARM CONTROL PANEL ==================== // ==================== ALARM CONTROL PANEL ====================
enum AlarmControlPanelState { enum AlarmControlPanelState {
ALARM_STATE_DISARMED = 0; ALARM_STATE_DISARMED = 0;

File diff suppressed because one or more lines are too long

View File

@ -73,10 +73,13 @@ from .api_pb2 import ( # type: ignore
VoiceAssistantAnnounceFinished, VoiceAssistantAnnounceFinished,
VoiceAssistantAnnounceRequest, VoiceAssistantAnnounceRequest,
VoiceAssistantAudio, VoiceAssistantAudio,
VoiceAssistantConfigurationRequest,
VoiceAssistantConfigurationResponse,
VoiceAssistantEventData, VoiceAssistantEventData,
VoiceAssistantEventResponse, VoiceAssistantEventResponse,
VoiceAssistantRequest, VoiceAssistantRequest,
VoiceAssistantResponse, VoiceAssistantResponse,
VoiceAssistantSetConfiguration,
VoiceAssistantTimerEventResponse, VoiceAssistantTimerEventResponse,
) )
from .client_callbacks import ( from .client_callbacks import (
@ -133,6 +136,7 @@ from .model import (
VoiceAssistantAudioData, VoiceAssistantAudioData,
VoiceAssistantAudioSettings as VoiceAssistantAudioSettingsModel, VoiceAssistantAudioSettings as VoiceAssistantAudioSettingsModel,
VoiceAssistantCommand, VoiceAssistantCommand,
VoiceAssistantConfigurationResponse as VoiceAssistantConfigurationResponseModel,
VoiceAssistantEventType, VoiceAssistantEventType,
VoiceAssistantSubscriptionFlag, VoiceAssistantSubscriptionFlag,
VoiceAssistantTimerEventType, VoiceAssistantTimerEventType,
@ -1459,6 +1463,22 @@ class APIClient:
) )
return VoiceAssistantAnnounceFinishedModel.from_pb(resp) return VoiceAssistantAnnounceFinishedModel.from_pb(resp)
async def get_voice_assistant_configuration(
self, timeout: float
) -> VoiceAssistantConfigurationResponseModel:
resp = await self._get_connection().send_message_await_response(
VoiceAssistantConfigurationRequest(),
VoiceAssistantConfigurationResponse,
timeout,
)
return VoiceAssistantConfigurationResponseModel.from_pb(resp)
async def set_voice_assistant_configuration(
self, active_wake_words: list[int]
) -> None:
req = VoiceAssistantSetConfiguration(active_wake_words=active_wake_words)
self._get_connection().send_message(req)
def alarm_control_panel_command( def alarm_control_panel_command(
self, self,
key: int, key: int,

View File

@ -121,9 +121,12 @@ from .api_pb2 import ( # type: ignore
VoiceAssistantAnnounceFinished, VoiceAssistantAnnounceFinished,
VoiceAssistantAnnounceRequest, VoiceAssistantAnnounceRequest,
VoiceAssistantAudio, VoiceAssistantAudio,
VoiceAssistantConfigurationRequest,
VoiceAssistantConfigurationResponse,
VoiceAssistantEventResponse, VoiceAssistantEventResponse,
VoiceAssistantRequest, VoiceAssistantRequest,
VoiceAssistantResponse, VoiceAssistantResponse,
VoiceAssistantSetConfiguration,
VoiceAssistantTimerEventResponse, VoiceAssistantTimerEventResponse,
) )
@ -396,6 +399,9 @@ MESSAGE_TYPE_TO_PROTO = {
118: UpdateCommandRequest, 118: UpdateCommandRequest,
119: VoiceAssistantAnnounceRequest, 119: VoiceAssistantAnnounceRequest,
120: VoiceAssistantAnnounceFinished, 120: VoiceAssistantAnnounceFinished,
121: VoiceAssistantConfigurationRequest,
122: VoiceAssistantConfigurationResponse,
123: VoiceAssistantSetConfiguration,
} }
MESSAGE_NUMBER_TO_PROTO = tuple(MESSAGE_TYPE_TO_PROTO.values()) MESSAGE_NUMBER_TO_PROTO = tuple(MESSAGE_TYPE_TO_PROTO.values())

View File

@ -1298,6 +1298,42 @@ class VoiceAssistantAnnounceFinished(APIModelBase):
success: bool = False success: bool = False
@_frozen_dataclass_decorator
class VoiceAssistantWakeWord(APIModelBase):
id: int
wake_word: str
trained_languages: list[str]
@classmethod
def convert_list(cls, value: list[Any]) -> list[VoiceAssistantWakeWord]:
ret = []
for x in value:
if isinstance(x, dict):
ret.append(VoiceAssistantWakeWord.from_dict(x))
else:
ret.append(VoiceAssistantWakeWord.from_pb(x))
return ret
@_frozen_dataclass_decorator
class VoiceAssistantConfigurationResponse(APIModelBase):
available_wake_words: list[VoiceAssistantWakeWord] = converter_field(
default_factory=list, converter=VoiceAssistantWakeWord.convert_list
)
active_wake_words: list[int] = converter_field(default_factory=list, converter=list)
max_active_wake_words: int = 0
@_frozen_dataclass_decorator
class VoiceAssistantConfigurationRequest(APIModelBase):
pass
@_frozen_dataclass_decorator
class VoiceAssistantSetConfiguration(APIModelBase):
active_wake_words: list[int] = converter_field(default_factory=list, converter=list)
class LogLevel(APIIntEnum): class LogLevel(APIIntEnum):
LOG_LEVEL_NONE = 0 LOG_LEVEL_NONE = 0
LOG_LEVEL_ERROR = 1 LOG_LEVEL_ERROR = 1

View File

@ -71,11 +71,15 @@ from aioesphomeapi.api_pb2 import (
VoiceAssistantAnnounceRequest, VoiceAssistantAnnounceRequest,
VoiceAssistantAudio, VoiceAssistantAudio,
VoiceAssistantAudioSettings, VoiceAssistantAudioSettings,
VoiceAssistantConfigurationRequest,
VoiceAssistantConfigurationResponse,
VoiceAssistantEventData, VoiceAssistantEventData,
VoiceAssistantEventResponse, VoiceAssistantEventResponse,
VoiceAssistantRequest, VoiceAssistantRequest,
VoiceAssistantResponse, VoiceAssistantResponse,
VoiceAssistantSetConfiguration,
VoiceAssistantTimerEventResponse, VoiceAssistantTimerEventResponse,
VoiceAssistantWakeWord,
) )
from aioesphomeapi.client import APIClient, BluetoothConnectionDroppedError from aioesphomeapi.client import APIClient, BluetoothConnectionDroppedError
from aioesphomeapi.connection import APIConnection from aioesphomeapi.connection import APIConnection
@ -113,6 +117,7 @@ from aioesphomeapi.model import (
UserServiceArgType, UserServiceArgType,
VoiceAssistantAnnounceFinished as VoiceAssistantAnnounceFinishedModel, VoiceAssistantAnnounceFinished as VoiceAssistantAnnounceFinishedModel,
VoiceAssistantAudioSettings as VoiceAssistantAudioSettingsModel, VoiceAssistantAudioSettings as VoiceAssistantAudioSettingsModel,
VoiceAssistantConfigurationResponse as VoiceAssistantConfigurationResponseModel,
VoiceAssistantEventType as VoiceAssistantEventModelType, VoiceAssistantEventType as VoiceAssistantEventModelType,
VoiceAssistantTimerEventType as VoiceAssistantTimerEventModelType, VoiceAssistantTimerEventType as VoiceAssistantTimerEventModelType,
) )
@ -2660,6 +2665,57 @@ async def test_subscribe_voice_assistant_announcement_finished(
assert len(send.mock_calls) == 0 assert len(send.mock_calls) == 0
@pytest.mark.asyncio
async def test_get_voice_assistant_configuration(
api_client: tuple[
APIClient, APIConnection, asyncio.Transport, APIPlaintextFrameHelper
],
) -> None:
client, connection, _transport, protocol = api_client
original_send_message = connection.send_message
def send_message(msg):
assert msg == VoiceAssistantConfigurationRequest()
original_send_message(msg)
with patch.object(connection, "send_message", new=send_message):
config_task = asyncio.create_task(
client.get_voice_assistant_configuration(timeout=1.0)
)
await asyncio.sleep(0)
response: message.Message = VoiceAssistantConfigurationResponse(
available_wake_words=[
VoiceAssistantWakeWord(
id=1,
wake_word="okay nabu",
trained_languages=["en"],
)
],
active_wake_words=[1],
max_active_wake_words=1,
)
mock_data_received(protocol, generate_plaintext_packet(response))
config = await config_task
assert isinstance(config, VoiceAssistantConfigurationResponseModel)
@pytest.mark.asyncio
async def test_set_voice_assistant_configuration(
api_client: tuple[
APIClient, APIConnection, asyncio.Transport, APIPlaintextFrameHelper
],
) -> None:
client, connection, _transport, protocol = api_client
original_send_message = connection.send_message
def send_message(msg):
assert msg == VoiceAssistantSetConfiguration(active_wake_words=[1])
original_send_message(msg)
with patch.object(connection, "send_message", new=send_message):
await client.set_voice_assistant_configuration([1])
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_api_version_after_connection_closed( async def test_api_version_after_connection_closed(
api_client: tuple[ api_client: tuple[

View File

@ -118,7 +118,9 @@ from aioesphomeapi.model import (
UserServiceArgType, UserServiceArgType,
ValveInfo, ValveInfo,
ValveState, ValveState,
VoiceAssistantConfigurationResponse,
VoiceAssistantFeature, VoiceAssistantFeature,
VoiceAssistantWakeWord,
build_unique_id, build_unique_id,
converter_field, converter_field,
) )
@ -681,3 +683,30 @@ def test_media_player_supported_format_convert_list() -> None:
) )
], ],
) )
def test_voice_assistant_wake_word_convert_list() -> None:
"""Test list conversion for VoiceAssistantWakeWord."""
assert VoiceAssistantConfigurationResponse.from_dict(
{
"available_wake_words": [
{
"id": 1,
"wake_word": "okay nabu",
"trained_languages": ["en"],
}
],
"active_wake_words": [1],
"max_active_wake_words": 1,
}
) == VoiceAssistantConfigurationResponse(
available_wake_words=[
VoiceAssistantWakeWord(
id=1,
wake_word="okay nabu",
trained_languages=["en"],
)
],
active_wake_words=[1],
max_active_wake_words=1,
)