Skip to content

Commit

Permalink
Add support for alarm_control_panel entities (#427)
Browse files Browse the repository at this point in the history
  • Loading branch information
grahambrown11 committed Jun 11, 2023
1 parent ac3644a commit a79da42
Show file tree
Hide file tree
Showing 7 changed files with 272 additions and 47 deletions.
61 changes: 61 additions & 0 deletions aioesphomeapi/api.proto
Expand Up @@ -57,6 +57,7 @@ service APIConnection {

rpc subscribe_voice_assistant(SubscribeVoiceAssistantRequest) returns (void) {}

rpc alarm_control_panel_command (AlarmControlPanelCommandRequest) returns (void) {}
}


Expand Down Expand Up @@ -1481,3 +1482,63 @@ message VoiceAssistantEventResponse {
VoiceAssistantEvent event_type = 1;
repeated VoiceAssistantEventData data = 2;
}

// ==================== ALARM CONTROL PANEL ====================
enum AlarmControlPanelState {
ALARM_STATE_DISARMED = 0;
ALARM_STATE_ARMED_HOME = 1;
ALARM_STATE_ARMED_AWAY = 2;
ALARM_STATE_ARMED_NIGHT = 3;
ALARM_STATE_ARMED_VACATION = 4;
ALARM_STATE_ARMED_CUSTOM_BYPASS = 5;
ALARM_STATE_PENDING = 6;
ALARM_STATE_ARMING = 7;
ALARM_STATE_DISARMING = 8;
ALARM_STATE_TRIGGERED = 9;
}

enum AlarmControlPanelStateCommand {
ALARM_CONTROL_PANEL_DISARM = 0;
ALARM_CONTROL_PANEL_ARM_AWAY = 1;
ALARM_CONTROL_PANEL_ARM_HOME = 2;
ALARM_CONTROL_PANEL_ARM_NIGHT = 3;
ALARM_CONTROL_PANEL_ARM_VACATION = 4;
ALARM_CONTROL_PANEL_ARM_CUSTOM_BYPASS = 5;
ALARM_CONTROL_PANEL_TRIGGER = 6;
}

message ListEntitiesAlarmControlPanelResponse {
option (id) = 94;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_ALARM_CONTROL_PANEL";

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 supported_features = 8;
bool requires_code = 9;
bool requires_code_to_arm = 10;
}

message AlarmControlPanelStateResponse {
option (id) = 95;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_ALARM_CONTROL_PANEL";
option (no_delay) = true;
fixed32 key = 1;
AlarmControlPanelState state = 2;
}

message AlarmControlPanelCommandRequest {
option (id) = 96;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_ALARM_CONTROL_PANEL";
option (no_delay) = true;
fixed32 key = 1;
AlarmControlPanelStateCommand command = 2;
string code = 3;
}
155 changes: 108 additions & 47 deletions aioesphomeapi/api_pb2.py

Large diffs are not rendered by default.

24 changes: 24 additions & 0 deletions aioesphomeapi/client.py
Expand Up @@ -18,6 +18,8 @@
from google.protobuf import message

from .api_pb2 import ( # type: ignore
AlarmControlPanelCommandRequest,
AlarmControlPanelStateResponse,
BinarySensorStateResponse,
BluetoothConnectionsFreeResponse,
BluetoothDeviceClearCacheResponse,
Expand Down Expand Up @@ -57,6 +59,7 @@
HomeAssistantStateResponse,
LightCommandRequest,
LightStateResponse,
ListEntitiesAlarmControlPanelResponse,
ListEntitiesBinarySensorResponse,
ListEntitiesButtonResponse,
ListEntitiesCameraResponse,
Expand Down Expand Up @@ -113,6 +116,9 @@
)
from .host_resolver import ZeroconfInstanceType
from .model import (
AlarmControlPanelCommand,
AlarmControlPanelEntityState,
AlarmControlPanelInfo,
APIVersion,
BinarySensorInfo,
BinarySensorState,
Expand Down Expand Up @@ -349,6 +355,7 @@ async def list_entities_services(
ListEntitiesClimateResponse: ClimateInfo,
ListEntitiesLockResponse: LockInfo,
ListEntitiesMediaPlayerResponse: MediaPlayerInfo,
ListEntitiesAlarmControlPanelResponse: AlarmControlPanelInfo,
}
msg_types = (ListEntitiesDoneResponse, *response_types)

Expand Down Expand Up @@ -390,6 +397,7 @@ async def subscribe_states(self, on_state: Callable[[EntityState], None]) -> Non
ClimateStateResponse: ClimateState,
LockStateResponse: LockEntityState,
MediaPlayerStateResponse: MediaPlayerEntityState,
AlarmControlPanelStateResponse: AlarmControlPanelEntityState,
}
msg_types = (*response_types, CameraImageResponse)

Expand Down Expand Up @@ -1380,3 +1388,19 @@ def send_voice_assistant_event(

assert self._connection is not None
self._connection.send_message(req)

async def alarm_control_panel_command(
self,
key: int,
command: AlarmControlPanelCommand,
code: Optional[str] = None,
) -> None:
self._check_authenticated()

req = AlarmControlPanelCommandRequest()
req.key = key
req.command = command
if code is not None:
req.code = code
assert self._connection is not None
self._connection.send_message(req)
6 changes: 6 additions & 0 deletions aioesphomeapi/core.py
Expand Up @@ -3,6 +3,8 @@
from aioesphomeapi.model import BluetoothGATTError

from .api_pb2 import ( # type: ignore
AlarmControlPanelCommandRequest,
AlarmControlPanelStateResponse,
BinarySensorStateResponse,
BluetoothConnectionsFreeResponse,
BluetoothDeviceClearCacheResponse,
Expand Down Expand Up @@ -49,6 +51,7 @@
HomeAssistantStateResponse,
LightCommandRequest,
LightStateResponse,
ListEntitiesAlarmControlPanelResponse,
ListEntitiesBinarySensorResponse,
ListEntitiesButtonResponse,
ListEntitiesCameraResponse,
Expand Down Expand Up @@ -320,4 +323,7 @@ def __init__(self, error: BluetoothGATTError) -> None:
91: VoiceAssistantResponse,
92: VoiceAssistantEventResponse,
93: BluetoothLERawAdvertisementsResponse,
94: ListEntitiesAlarmControlPanelResponse,
95: AlarmControlPanelStateResponse,
96: AlarmControlPanelCommandRequest,
}
40 changes: 40 additions & 0 deletions aioesphomeapi/model.py
Expand Up @@ -725,6 +725,45 @@ class MediaPlayerEntityState(EntityState):
muted: bool = False


# ==================== ALARM CONTROL PANEL ====================
class AlarmControlPanelState(APIIntEnum):
DISARMED = 0
ARMED_HOME = 1
ARMED_AWAY = 2
ARMED_NIGHT = 3
ARMED_VACATION = 4
ARMED_CUSTOM_BYPASS = 5
PENDING = 6
ARMING = 7
DISARMING = 8
TRIGGERED = 9


class AlarmControlPanelCommand(APIIntEnum):
DISARM = 0
ARM_AWAY = 1
ARM_HOME = 2
ARM_NIGHT = 3
ARM_VACATION = 4
ARM_CUSTOM_BYPASS = 5
TRIGGER = 6


@dataclass(frozen=True)
class AlarmControlPanelInfo(EntityInfo):
supported_features: int = 0
requires_code: bool = False
requires_code_to_arm: bool = False


@dataclass(frozen=True)
class AlarmControlPanelEntityState(EntityState):
state: Optional[AlarmControlPanelState] = converter_field(
default=AlarmControlPanelState.DISARMED,
converter=AlarmControlPanelState.convert,
)


# ==================== INFO MAP ====================

COMPONENT_TYPE_TO_INFO: Dict[str, Type[EntityInfo]] = {
Expand All @@ -743,6 +782,7 @@ class MediaPlayerEntityState(EntityState):
"button": ButtonInfo,
"lock": LockInfo,
"media_player": MediaPlayerInfo,
"alarm_control_panel": AlarmControlPanelInfo,
}


Expand Down
27 changes: 27 additions & 0 deletions tests/test_client.py
Expand Up @@ -4,6 +4,7 @@
from mock import AsyncMock, MagicMock, call, patch

from aioesphomeapi.api_pb2 import (
AlarmControlPanelCommandRequest,
BinarySensorStateResponse,
CameraImageRequest,
CameraImageResponse,
Expand All @@ -24,6 +25,7 @@
)
from aioesphomeapi.client import APIClient
from aioesphomeapi.model import (
AlarmControlPanelCommand,
APIVersion,
BinarySensorInfo,
BinarySensorState,
Expand Down Expand Up @@ -535,3 +537,28 @@ async def test_request_image_stream(auth_client):

await auth_client.request_image_stream()
send.assert_called_once_with(CameraImageRequest(single=False, stream=True))


@pytest.mark.asyncio
@pytest.mark.parametrize(
"cmd, req",
[
(
dict(key=1, command=AlarmControlPanelCommand.ARM_AWAY),
dict(key=1, command=AlarmControlPanelCommand.ARM_AWAY, code=None),
),
(
dict(key=1, command=AlarmControlPanelCommand.ARM_HOME),
dict(key=1, command=AlarmControlPanelCommand.ARM_HOME, code=None),
),
(
dict(key=1, command=AlarmControlPanelCommand.DISARM, code="1234"),
dict(key=1, command=AlarmControlPanelCommand.DISARM, code="1234"),
),
],
)
async def test_alarm_panel_command(auth_client, cmd, req):
send = patch_send(auth_client)

await auth_client.alarm_control_panel_command(**cmd)
send.assert_called_once_with(AlarmControlPanelCommandRequest(**req))
6 changes: 6 additions & 0 deletions tests/test_model.py
Expand Up @@ -4,6 +4,7 @@
import pytest

from aioesphomeapi.api_pb2 import (
AlarmControlPanelStateResponse,
BinarySensorStateResponse,
ClimateStateResponse,
CoverStateResponse,
Expand All @@ -12,6 +13,7 @@
HomeassistantServiceMap,
HomeassistantServiceResponse,
LightStateResponse,
ListEntitiesAlarmControlPanelResponse,
ListEntitiesBinarySensorResponse,
ListEntitiesButtonResponse,
ListEntitiesClimateResponse,
Expand All @@ -37,6 +39,8 @@
TextSensorStateResponse,
)
from aioesphomeapi.model import (
AlarmControlPanelEntityState,
AlarmControlPanelInfo,
APIIntEnum,
APIModelBase,
APIVersion,
Expand Down Expand Up @@ -226,6 +230,8 @@ def test_api_version_ord():
(LockEntityState, LockStateResponse),
(MediaPlayerInfo, ListEntitiesMediaPlayerResponse),
(MediaPlayerEntityState, MediaPlayerStateResponse),
(AlarmControlPanelInfo, ListEntitiesAlarmControlPanelResponse),
(AlarmControlPanelEntityState, AlarmControlPanelStateResponse),
],
)
def test_basic_pb_conversions(model, pb):
Expand Down

0 comments on commit a79da42

Please sign in to comment.