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 climate_command (ClimateCommandRequest) returns (void) {}
rpc number_command (NumberCommandRequest) returns (void) {} rpc number_command (NumberCommandRequest) returns (void) {}
rpc select_command (SelectCommandRequest) returns (void) {} rpc select_command (SelectCommandRequest) returns (void) {}
rpc text_command (TextCommandRequest) returns (void) {}
rpc siren_command (SirenCommandRequest) returns (void) {} rpc siren_command (SirenCommandRequest) returns (void) {}
rpc button_command (ButtonCommandRequest) returns (void) {} rpc button_command (ButtonCommandRequest) returns (void) {}
rpc lock_command (LockCommandRequest) returns (void) {} rpc lock_command (LockCommandRequest) returns (void) {}
@ -1555,3 +1556,48 @@ message AlarmControlPanelCommandRequest {
AlarmControlPanelStateCommand command = 2; AlarmControlPanelStateCommand command = 2;
string code = 3; 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 asyncio
import logging import logging
from collections.abc import Awaitable, Coroutine
from functools import partial 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 from google.protobuf import message
@ -69,6 +68,7 @@ from .api_pb2 import ( # type: ignore
ListEntitiesServicesResponse, ListEntitiesServicesResponse,
ListEntitiesSirenResponse, ListEntitiesSirenResponse,
ListEntitiesSwitchResponse, ListEntitiesSwitchResponse,
ListEntitiesTextResponse,
ListEntitiesTextSensorResponse, ListEntitiesTextSensorResponse,
LockCommandRequest, LockCommandRequest,
LockStateResponse, LockStateResponse,
@ -92,7 +92,9 @@ from .api_pb2 import ( # type: ignore
SubscribeVoiceAssistantRequest, SubscribeVoiceAssistantRequest,
SwitchCommandRequest, SwitchCommandRequest,
SwitchStateResponse, SwitchStateResponse,
TextCommandRequest,
TextSensorStateResponse, TextSensorStateResponse,
TextStateResponse,
UnsubscribeBluetoothLEAdvertisementsRequest, UnsubscribeBluetoothLEAdvertisementsRequest,
VoiceAssistantAudioSettings, VoiceAssistantAudioSettings,
VoiceAssistantEventData, VoiceAssistantEventData,
@ -165,8 +167,10 @@ from .model import (
SirenState, SirenState,
SwitchInfo, SwitchInfo,
SwitchState, SwitchState,
TextInfo,
TextSensorInfo, TextSensorInfo,
TextSensorState, TextSensorState,
TextState,
UserService, UserService,
UserServiceArgType, UserServiceArgType,
VoiceAssistantCommand, VoiceAssistantCommand,
@ -198,6 +202,7 @@ SUBSCRIBE_STATES_RESPONSE_TYPES: dict[Any, type[EntityState]] = {
SensorStateResponse: SensorState, SensorStateResponse: SensorState,
SirenStateResponse: SirenState, SirenStateResponse: SirenState,
SwitchStateResponse: SwitchState, SwitchStateResponse: SwitchState,
TextStateResponse: TextState,
TextSensorStateResponse: TextSensorState, TextSensorStateResponse: TextSensorState,
ClimateStateResponse: ClimateState, ClimateStateResponse: ClimateState,
LockStateResponse: LockEntityState, LockStateResponse: LockEntityState,
@ -217,6 +222,7 @@ LIST_ENTITIES_SERVICES_RESPONSE_TYPES: dict[Any, type[EntityInfo] | None] = {
ListEntitiesSensorResponse: SensorInfo, ListEntitiesSensorResponse: SensorInfo,
ListEntitiesSirenResponse: SirenInfo, ListEntitiesSirenResponse: SirenInfo,
ListEntitiesSwitchResponse: SwitchInfo, ListEntitiesSwitchResponse: SwitchInfo,
ListEntitiesTextResponse: TextInfo,
ListEntitiesTextSensorResponse: TextSensorInfo, ListEntitiesTextSensorResponse: TextSensorInfo,
ListEntitiesServicesResponse: None, ListEntitiesServicesResponse: None,
ListEntitiesCameraResponse: CameraInfo, ListEntitiesCameraResponse: CameraInfo,
@ -1338,6 +1344,15 @@ class APIClient:
assert self._connection is not None assert self._connection is not None
self._connection.send_message(req) 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( async def execute_service(
self, service: UserService, data: ExecuteServiceDataType self, service: UserService, data: ExecuteServiceDataType
) -> None: ) -> None:

View File

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

View File

@ -2,10 +2,9 @@ from __future__ import annotations
import enum import enum
import sys import sys
from collections.abc import Iterable
from dataclasses import asdict, dataclass, field, fields from dataclasses import asdict, dataclass, field, fields
from functools import cache, lru_cache, partial 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 uuid import UUID
from .util import fix_float_single_double_conversion 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 ==================== # ==================== INFO MAP ====================
COMPONENT_TYPE_TO_INFO: dict[str, type[EntityInfo]] = { COMPONENT_TYPE_TO_INFO: dict[str, type[EntityInfo]] = {
@ -776,6 +797,7 @@ COMPONENT_TYPE_TO_INFO: dict[str, type[EntityInfo]] = {
"lock": LockInfo, "lock": LockInfo,
"media_player": MediaPlayerInfo, "media_player": MediaPlayerInfo,
"alarm_control_panel": AlarmControlPanelInfo, "alarm_control_panel": AlarmControlPanelInfo,
"text": TextInfo,
} }
@ -1126,6 +1148,7 @@ _TYPE_TO_NAME = {
LockInfo: "lock", LockInfo: "lock",
MediaPlayerInfo: "media_player", MediaPlayerInfo: "media_player",
AlarmControlPanelInfo: "alarm_control_panel", AlarmControlPanelInfo: "alarm_control_panel",
TextInfo: "text_info",
} }

View File

@ -20,6 +20,7 @@ from aioesphomeapi.api_pb2 import (
NumberCommandRequest, NumberCommandRequest,
SelectCommandRequest, SelectCommandRequest,
SwitchCommandRequest, SwitchCommandRequest,
TextCommandRequest,
) )
from aioesphomeapi.client import APIClient from aioesphomeapi.client import APIClient
from aioesphomeapi.model import ( 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) await auth_client.alarm_control_panel_command(**cmd)
send.assert_called_once_with(AlarmControlPanelCommandRequest(**req)) 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, ServiceArgType,
SwitchStateResponse, SwitchStateResponse,
TextSensorStateResponse, TextSensorStateResponse,
TextStateResponse,
) )
from aioesphomeapi.model import ( from aioesphomeapi.model import (
_TYPE_TO_NAME, _TYPE_TO_NAME,
@ -74,8 +75,10 @@ from aioesphomeapi.model import (
SirenInfo, SirenInfo,
SwitchInfo, SwitchInfo,
SwitchState, SwitchState,
TextInfo,
TextSensorInfo, TextSensorInfo,
TextSensorState, TextSensorState,
TextState,
UserService, UserService,
UserServiceArg, UserServiceArg,
UserServiceArgType, UserServiceArgType,
@ -236,6 +239,7 @@ def test_api_version_ord():
(MediaPlayerEntityState, MediaPlayerStateResponse), (MediaPlayerEntityState, MediaPlayerStateResponse),
(AlarmControlPanelInfo, ListEntitiesAlarmControlPanelResponse), (AlarmControlPanelInfo, ListEntitiesAlarmControlPanelResponse),
(AlarmControlPanelEntityState, AlarmControlPanelStateResponse), (AlarmControlPanelEntityState, AlarmControlPanelStateResponse),
(TextState, TextStateResponse),
], ],
) )
def test_basic_pb_conversions(model, pb): def test_basic_pb_conversions(model, pb):
@ -346,6 +350,7 @@ def test_user_service_conversion():
LockInfo, LockInfo,
MediaPlayerInfo, MediaPlayerInfo,
AlarmControlPanelInfo, AlarmControlPanelInfo,
TextInfo,
], ],
) )
def test_build_unique_id(model): def test_build_unique_id(model):