Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor Matter server and client #99

Merged
merged 34 commits into from
Nov 29, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
2d7f3a1
small tweaks and naming
marcelveldt Oct 17, 2022
2f76632
some cleanup/typing
marcelveldt Oct 17, 2022
366214f
wip (not working yet)
marcelveldt Oct 27, 2022
a341934
finish persistent storage controller
marcelveldt Nov 1, 2022
7cd0160
finish subscription logic
marcelveldt Nov 3, 2022
3681db0
full universal handling of (de)serialization
marcelveldt Nov 3, 2022
8fd190e
more wip
marcelveldt Nov 4, 2022
f6cc035
cleanup
marcelveldt Nov 4, 2022
4776f35
cleanup
marcelveldt Nov 4, 2022
d4dcca8
fixes
marcelveldt Nov 5, 2022
56c44b3
plugfest workarounds?
marcelveldt Nov 8, 2022
d220643
temp code
marcelveldt Nov 8, 2022
4c4cd04
switch to orjson
marcelveldt Nov 9, 2022
c1e74f3
client part
marcelveldt Nov 28, 2022
6d5532f
remove ha tests
marcelveldt Nov 28, 2022
a5b00a3
Update matter_server/common/helpers/json.py
marcelveldt Nov 28, 2022
1d834aa
some cleanup
marcelveldt Nov 28, 2022
6dff06c
Merge branch 'refactor-storage' of https://github.com/home-assistant-…
marcelveldt Nov 28, 2022
a224e5b
change version
marcelveldt Nov 28, 2022
9674da7
camel case for our own interfaces
marcelveldt Nov 28, 2022
6cde47b
client part
marcelveldt Nov 29, 2022
d772238
more follow up
marcelveldt Nov 29, 2022
dfffad0
fix events
marcelveldt Nov 29, 2022
0531f04
cleanup
marcelveldt Nov 29, 2022
6851390
fix saving
marcelveldt Nov 29, 2022
f646498
some cleanup
marcelveldt Nov 29, 2022
afb0bcc
some small fixes
marcelveldt Nov 29, 2022
ecdcfb5
some leftover formatting
marcelveldt Nov 29, 2022
5e9c15b
fix example
marcelveldt Nov 29, 2022
5ce47a0
node instance
marcelveldt Nov 29, 2022
1c63b2c
typos
marcelveldt Nov 29, 2022
03b0c26
workaround cluster object
marcelveldt Nov 29, 2022
95e1a18
add do_not_serialize to non dataclass types for now
marcelveldt Nov 29, 2022
7cce90e
formatting
marcelveldt Nov 29, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 76 additions & 33 deletions matter_server/client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,36 @@
from collections import defaultdict
from copy import deepcopy
from datetime import datetime
from enum import Enum
from functools import partial
import json
import logging
from operator import itemgetter
import pprint
from types import TracebackType
from typing import Any, DefaultDict, Dict, List
from typing import TYPE_CHECKING, Any, DefaultDict, Dict, List, Type
import uuid
from enum import Enum
from aiohttp import ClientSession, ClientWebSocketResponse, WSMsgType, client_exceptions

from ..common.helpers.json import json_loads, json_dumps
from ..common.helpers.util import dataclass_from_dict, chip_clusters_version
from aiohttp import ClientSession, ClientWebSocketResponse, WSMsgType, client_exceptions

from ..common.models.server_information import ServerInfo
from ..common.helpers.json import json_dumps, json_loads
from ..common.helpers.util import (
chip_clusters_version,
dataclass_from_dict,
parse_value,
)
from ..common.models.api_command import APICommand
from ..common.models.message import (
CommandMessage,
ErrorResultMessage,
EventMessage,
MessageType,
ResultMessageBase,
EventMessage,
SuccessResultMessage,
parse_message,
)
from ..common.models.node import MatterNode
from ..common.models.server_information import ServerInfo
from .const import MIN_SCHEMA_VERSION
from .exceptions import (
CannotConnect,
Expand All @@ -41,17 +47,8 @@
NotConnected,
)


# Message IDs
SET_API_SCHEMA_MESSAGE_ID = "api-schema-id"
GET_INITIAL_LOG_CONFIG_MESSAGE_ID = "get-initial-log-config"
START_LISTENING_MESSAGE_ID = "listen-id"

LISTEN_MESSAGE_IDS = (
GET_INITIAL_LOG_CONFIG_MESSAGE_ID,
SET_API_SCHEMA_MESSAGE_ID,
START_LISTENING_MESSAGE_ID,
)
if TYPE_CHECKING:
from chip.clusters import ClusterCommand


class MatterClient:
Expand All @@ -66,19 +63,62 @@ def __init__(self, ws_server_url: str, aiohttp_session: ClientSession):
self.server_info: ServerInfo | None = None
self._ws_client: ClientWebSocketResponse | None = None
self._loop = asyncio.get_running_loop()
self._nodes: Dict[int, MatterNode] = {}
self._result_futures: Dict[str, asyncio.Future] = {}

@property
def connected(self) -> bool:
"""Return if we're currently connected."""
return self._ws_client is not None and not self._ws_client.closed

async def async_send_command(
async def get_nodes(self) -> list[MatterNode]:
"""Return all Matter nodes."""
if self._nodes:
# if start_listening is called this dict will be kept up to date
return list(self._nodes.values())
data = await self.send_command(APICommand.GET_NODES)
return [MatterNode.from_dict(x) for x in data]

async def get_node(self, node_id: int) -> MatterNode:
"""Return Matter node by id."""
if node_id in self._nodes:
# if start_listening is called this dict will be kept up to date
return self._nodes[node_id]
data = await self.send_command(APICommand.GET_NODE, node_id=node_id)
return MatterNode.from_dict(data)

async def commission_with_code(self, code: str) -> MatterNode:
"""
Commission a device using QRCode or ManualPairingCode.

Returns full NodeInfo once complete.
"""
data = await self.send_command(APICommand.COMMISSION_WITH_CODE, code=code)
return MatterNode.from_dict(data)

async def commission_on_network(self, setup_pin_code: int) -> MatterNode:
"""
Commission a device already connected to the network.

Returns full NodeInfo once complete.
"""
data = await self.send_command(APICommand.COMMISSION_ON_NETWORK, setup_pin_code=setup_pin_code)
return MatterNode.from_dict(data)

async def set_wifi_credentials(self, setup_pin_code: int) -> None:
"""Set WiFi credentials for commissioning to a (new) device."""
await self.send_command(APICommand.SET_WIFI_CREDENTIALS, setup_pin_code=setup_pin_code)

async def send_device_command(self, node_id: int, endpoint: int, payload: ClusterCommand) -> Any:
"""Send a command to a Matter node/device."""
return await self.send_command(APICommand.DEVICE_COMMAND, node_id=node_id, endpoint=endpoint, payload=payload)

async def send_command(
self,
command: str,
args: dict[str, Any],
require_schema: int | None = None,
) -> dict:
**kwargs
) -> Any:
"""Send a command and get a response."""
if require_schema is not None and require_schema > self.server_info.schema_version:
raise InvalidServerVersion(
Expand All @@ -87,20 +127,20 @@ async def async_send_command(
)

message = CommandMessage(
messageId=uuid.uuid4().hex,
message_id=uuid.uuid4().hex,
command=command,
args=args,
args=kwargs,
)
future: asyncio.Future[dict] = self._loop.create_future()
self._result_futures[message.messageId] = future
self._result_futures[message.message_id] = future
await self._send_message(message)
try:
return await future
finally:
self._result_futures.pop(message.messageId)
self._result_futures.pop(message.message_id)

async def async_send_command_no_wait(
self, command: str, args: dict[str, Any], require_schema: int | None = None
async def send_command_no_wait(
self, command: str, require_schema: int | None = None, **kwargs
) -> None:
"""Send a command without waiting for the response."""
if require_schema is not None and require_schema > self.server_info.schema_version:
Expand All @@ -109,9 +149,9 @@ async def async_send_command_no_wait(
f"Server to a version that supports at least api schema {require_schema}."
)
message = CommandMessage(
messageId=uuid.uuid4().hex,
message_id=uuid.uuid4().hex,
command=command,
args=args,
args=kwargs,
)
await self._send_message(message)

Expand Down Expand Up @@ -171,12 +211,15 @@ async def connect(self) -> None:
info.sdk_version
)

async def listen(self, driver_ready: asyncio.Event) -> None:
async def start_listening(self, driver_ready: asyncio.Event) -> None:
"""Start listening to the websocket (and receive initial state)."""
if not self.connected:
raise InvalidState("Not connected when start listening")

try:
nodes = await self.send_command(APICommand.START_LISTENING)
self._nodes = nodes

self.logger.info("Matter client initialized.")
driver_ready.set()

Expand Down Expand Up @@ -233,7 +276,7 @@ def _handle_incoming_message(self, msg: MessageType) -> None:
# handle result message
if isinstance(msg, ResultMessageBase):

future = self._result_futures.get(msg.messageId)
future = self._result_futures.get(msg.message_id)

if future is None:
# no listener for this result
Expand All @@ -245,7 +288,7 @@ def _handle_incoming_message(self, msg: MessageType) -> None:
return
if isinstance(msg, ErrorResultMessage):
msg: ErrorResultMessage = msg
future.set_exception(FailedCommand(msg.messageId, msg.errorCode))
future.set_exception(FailedCommand(msg.message_id, msg.errorCode))
return

# handle EventMessage
Expand Down Expand Up @@ -291,4 +334,4 @@ async def __aexit__(
def __repr__(self) -> str:
"""Return the representation."""
prefix = "" if self.connected else "not "
return f"{type(self).__name__}(ws_server_url={self.ws_server_url!r}, {prefix}connected)"
return f"{type(self).__name__}(ws_server_url={self.ws_server_url!r}, {prefix}connected)"
17 changes: 8 additions & 9 deletions matter_server/client/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
"""Exceptions for matter-server."""
from __future__ import annotations

from ..common.models.error import MatterError

class BaseMatterServerError(Exception):
"""Base Matter exception."""
# TODO: merge these exceptions with the common ones


class TransportError(BaseMatterServerError):
class TransportError(MatterError):
"""Exception raised to represent transport errors."""

def __init__(self, message: str, error: Exception | None = None) -> None:
Expand Down Expand Up @@ -38,23 +37,23 @@ def __init__(self, error: Exception | None = None) -> None:
super().__init__(f"{error}", error)


class NotConnected(BaseMatterServerError):
class NotConnected(MatterError):
"""Exception raised when not connected to client."""


class InvalidState(BaseMatterServerError):
class InvalidState(MatterError):
"""Exception raised when data gets in invalid state."""


class InvalidMessage(BaseMatterServerError):
class InvalidMessage(MatterError):
"""Exception raised when an invalid message is received."""


class InvalidServerVersion(BaseMatterServerError):
class InvalidServerVersion(MatterError):
"""Exception raised when connected to server with incompatible version."""


class FailedCommand(BaseMatterServerError):
class FailedCommand(MatterError):
"""When a command has failed."""

def __init__(self, message_id: str, error_code: str, msg: str | None = None):
Expand Down
Loading