Updated fork of PR for Text input components (#532)

Co-authored-by: Maurits <maurits@vloop.nl>
Co-authored-by: Daniel Dunn <dannydunn@eternityforest.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
Daniel Dunn 2023-10-24 20:35:04 -06:00 committed by GitHub
parent ae03a831b9
commit 5a8c0d8e23
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 208 additions and 55 deletions

View File

@ -40,6 +40,7 @@ service APIConnection {
rpc climate_command (ClimateCommandRequest) returns (void) {}
rpc number_command (NumberCommandRequest) returns (void) {}
rpc select_command (SelectCommandRequest) returns (void) {}
rpc text_command (TextCommandRequest) returns (void) {}
rpc siren_command (SirenCommandRequest) returns (void) {}
rpc button_command (ButtonCommandRequest) returns (void) {}
rpc lock_command (LockCommandRequest) returns (void) {}
@ -1555,3 +1556,48 @@ message AlarmControlPanelCommandRequest {
AlarmControlPanelStateCommand command = 2;
string code = 3;
}
// ===================== TEXT =====================
enum TextMode {
TEXT_MODE_TEXT = 0;
TEXT_MODE_PASSWORD = 1;
}
message ListEntitiesTextResponse {
option (id) = 97;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_TEXT";
string object_id = 1;
fixed32 key = 2;
string name = 3;
string unique_id = 4;
string icon = 5;
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
uint32 min_length = 8;
uint32 max_length = 9;
string pattern = 10;
TextMode mode = 11;
}
message TextStateResponse {
option (id) = 98;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_TEXT";
option (no_delay) = true;
fixed32 key = 1;
string state = 2;
// If the Text does not have a valid state yet.
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
bool missing_state = 3;
}
message TextCommandRequest {
option (id) = 99;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_TEXT";
option (no_delay) = true;
fixed32 key = 1;
string state = 2;
}

File diff suppressed because one or more lines are too long

View File

@ -2,9 +2,8 @@ from __future__ import annotations
import asyncio
import logging
from collections.abc import Awaitable, Coroutine
from functools import partial
from typing import TYPE_CHECKING, Any, Callable, Union, cast
from typing import TYPE_CHECKING, Any, Awaitable, Callable, Coroutine, Union, cast
from google.protobuf import message
@ -69,6 +68,7 @@ from .api_pb2 import ( # type: ignore
ListEntitiesServicesResponse,
ListEntitiesSirenResponse,
ListEntitiesSwitchResponse,
ListEntitiesTextResponse,
ListEntitiesTextSensorResponse,
LockCommandRequest,
LockStateResponse,
@ -92,7 +92,9 @@ from .api_pb2 import ( # type: ignore
SubscribeVoiceAssistantRequest,
SwitchCommandRequest,
SwitchStateResponse,
TextCommandRequest,
TextSensorStateResponse,
TextStateResponse,
UnsubscribeBluetoothLEAdvertisementsRequest,
VoiceAssistantAudioSettings,
VoiceAssistantEventData,
@ -165,8 +167,10 @@ from .model import (
SirenState,
SwitchInfo,
SwitchState,
TextInfo,
TextSensorInfo,
TextSensorState,
TextState,
UserService,
UserServiceArgType,
VoiceAssistantCommand,
@ -198,6 +202,7 @@ SUBSCRIBE_STATES_RESPONSE_TYPES: dict[Any, type[EntityState]] = {
SensorStateResponse: SensorState,
SirenStateResponse: SirenState,
SwitchStateResponse: SwitchState,
TextStateResponse: TextState,
TextSensorStateResponse: TextSensorState,
ClimateStateResponse: ClimateState,
LockStateResponse: LockEntityState,
@ -217,6 +222,7 @@ LIST_ENTITIES_SERVICES_RESPONSE_TYPES: dict[Any, type[EntityInfo] | None] = {
ListEntitiesSensorResponse: SensorInfo,
ListEntitiesSirenResponse: SirenInfo,
ListEntitiesSwitchResponse: SwitchInfo,
ListEntitiesTextResponse: TextInfo,
ListEntitiesTextSensorResponse: TextSensorInfo,
ListEntitiesServicesResponse: None,
ListEntitiesCameraResponse: CameraInfo,
@ -1338,6 +1344,15 @@ class APIClient:
assert self._connection is not None
self._connection.send_message(req)
async def text_command(self, key: int, state: str) -> None:
self._check_authenticated()
req = TextCommandRequest()
req.key = key
req.state = state
assert self._connection is not None
self._connection.send_message(req)
async def execute_service(
self, service: UserService, data: ExecuteServiceDataType
) -> None:

View File

@ -71,6 +71,7 @@ from .api_pb2 import ( # type: ignore
ListEntitiesServicesResponse,
ListEntitiesSirenResponse,
ListEntitiesSwitchResponse,
ListEntitiesTextResponse,
ListEntitiesTextSensorResponse,
LockCommandRequest,
LockStateResponse,
@ -96,7 +97,9 @@ from .api_pb2 import ( # type: ignore
SubscribeVoiceAssistantRequest,
SwitchCommandRequest,
SwitchStateResponse,
TextCommandRequest,
TextSensorStateResponse,
TextStateResponse,
UnsubscribeBluetoothLEAdvertisementsRequest,
VoiceAssistantEventResponse,
VoiceAssistantRequest,
@ -340,4 +343,7 @@ MESSAGE_TYPE_TO_PROTO = {
94: ListEntitiesAlarmControlPanelResponse,
95: AlarmControlPanelStateResponse,
96: AlarmControlPanelCommandRequest,
97: ListEntitiesTextResponse,
98: TextStateResponse,
99: TextCommandRequest,
}

View File

@ -2,10 +2,9 @@ from __future__ import annotations
import enum
import sys
from collections.abc import Iterable
from dataclasses import asdict, dataclass, field, fields
from functools import cache, lru_cache, partial
from typing import TYPE_CHECKING, Any, Callable, TypeVar, cast
from typing import TYPE_CHECKING, Any, Callable, Iterable, Optional, TypeVar, cast
from uuid import UUID
from .util import fix_float_single_double_conversion
@ -757,6 +756,28 @@ class AlarmControlPanelEntityState(EntityState):
)
# ==================== TEXT ====================
class TextMode(APIIntEnum):
TEXT = 0
PASSWORD = 1
@_frozen_dataclass_decorator
class TextInfo(EntityInfo):
min_length: int = 0
max_length: int = 255
pattern: str = ""
mode: Optional[TextMode] = converter_field(
default=TextMode.TEXT, converter=TextMode.convert
)
@_frozen_dataclass_decorator
class TextState(EntityState):
state: str = ""
missing_state: bool = False
# ==================== INFO MAP ====================
COMPONENT_TYPE_TO_INFO: dict[str, type[EntityInfo]] = {
@ -776,6 +797,7 @@ COMPONENT_TYPE_TO_INFO: dict[str, type[EntityInfo]] = {
"lock": LockInfo,
"media_player": MediaPlayerInfo,
"alarm_control_panel": AlarmControlPanelInfo,
"text": TextInfo,
}
@ -1126,6 +1148,7 @@ _TYPE_TO_NAME = {
LockInfo: "lock",
MediaPlayerInfo: "media_player",
AlarmControlPanelInfo: "alarm_control_panel",
TextInfo: "text_info",
}

View File

@ -20,6 +20,7 @@ from aioesphomeapi.api_pb2 import (
NumberCommandRequest,
SelectCommandRequest,
SwitchCommandRequest,
TextCommandRequest,
)
from aioesphomeapi.client import APIClient
from aioesphomeapi.model import (
@ -559,3 +560,18 @@ async def test_alarm_panel_command(auth_client, cmd, req):
await auth_client.alarm_control_panel_command(**cmd)
send.assert_called_once_with(AlarmControlPanelCommandRequest(**req))
@pytest.mark.asyncio
@pytest.mark.parametrize(
"cmd, req",
[
(dict(key=1, state="hello world"), dict(key=1, state="hello world")),
(dict(key=1, state="goodbye"), dict(key=1, state="goodbye")),
],
)
async def test_text_command(auth_client, cmd, req):
send = patch_send(auth_client)
await auth_client.text_command(**cmd)
send.assert_called_once_with(TextCommandRequest(**req))

View File

@ -37,6 +37,7 @@ from aioesphomeapi.api_pb2 import (
ServiceArgType,
SwitchStateResponse,
TextSensorStateResponse,
TextStateResponse,
)
from aioesphomeapi.model import (
_TYPE_TO_NAME,
@ -74,8 +75,10 @@ from aioesphomeapi.model import (
SirenInfo,
SwitchInfo,
SwitchState,
TextInfo,
TextSensorInfo,
TextSensorState,
TextState,
UserService,
UserServiceArg,
UserServiceArgType,
@ -236,6 +239,7 @@ def test_api_version_ord():
(MediaPlayerEntityState, MediaPlayerStateResponse),
(AlarmControlPanelInfo, ListEntitiesAlarmControlPanelResponse),
(AlarmControlPanelEntityState, AlarmControlPanelStateResponse),
(TextState, TextStateResponse),
],
)
def test_basic_pb_conversions(model, pb):
@ -346,6 +350,7 @@ def test_user_service_conversion():
LockInfo,
MediaPlayerInfo,
AlarmControlPanelInfo,
TextInfo,
],
)
def test_build_unique_id(model):