matter-python-client 0.4.1a0.dev20260220__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- matter_python_client-0.4.1a0.dev20260220.dist-info/METADATA +20 -0
- matter_python_client-0.4.1a0.dev20260220.dist-info/RECORD +22 -0
- matter_python_client-0.4.1a0.dev20260220.dist-info/WHEEL +5 -0
- matter_python_client-0.4.1a0.dev20260220.dist-info/top_level.txt +1 -0
- matter_server/__init__.py +1 -0
- matter_server/client/__init__.py +5 -0
- matter_server/client/client.py +806 -0
- matter_server/client/connection.py +177 -0
- matter_server/client/exceptions.py +63 -0
- matter_server/client/models/__init__.py +1 -0
- matter_server/client/models/device_types.py +952 -0
- matter_server/client/models/node.py +414 -0
- matter_server/common/__init__.py +1 -0
- matter_server/common/const.py +8 -0
- matter_server/common/custom_clusters.py +1371 -0
- matter_server/common/errors.py +94 -0
- matter_server/common/helpers/__init__.py +0 -0
- matter_server/common/helpers/api.py +70 -0
- matter_server/common/helpers/json.py +48 -0
- matter_server/common/helpers/util.py +359 -0
- matter_server/common/models.py +273 -0
- matter_server/py.typed +0 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Logic to manage the WebSocket connection to the Matter server."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import pprint
|
|
8
|
+
from typing import Final, cast
|
|
9
|
+
|
|
10
|
+
from aiohttp import ClientSession, ClientWebSocketResponse, WSMsgType, client_exceptions
|
|
11
|
+
|
|
12
|
+
from matter_server.common.helpers.util import dataclass_from_dict
|
|
13
|
+
|
|
14
|
+
from ..common.const import SCHEMA_VERSION
|
|
15
|
+
from ..common.helpers.json import json_dumps, json_loads
|
|
16
|
+
from ..common.models import (
|
|
17
|
+
CommandMessage,
|
|
18
|
+
ErrorResultMessage,
|
|
19
|
+
EventMessage,
|
|
20
|
+
MessageType,
|
|
21
|
+
ServerInfoMessage,
|
|
22
|
+
SuccessResultMessage,
|
|
23
|
+
)
|
|
24
|
+
from .exceptions import (
|
|
25
|
+
CannotConnect,
|
|
26
|
+
ConnectionClosed,
|
|
27
|
+
ConnectionFailed,
|
|
28
|
+
InvalidMessage,
|
|
29
|
+
InvalidState,
|
|
30
|
+
NotConnected,
|
|
31
|
+
ServerVersionTooNew,
|
|
32
|
+
ServerVersionTooOld,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
LOGGER = logging.getLogger(f"{__package__}.connection")
|
|
36
|
+
VERBOSE_LOGGER = os.environ.get("MATTER_VERBOSE_LOGGING")
|
|
37
|
+
SUB_WILDCARD: Final = "*"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class MatterClientConnection:
|
|
41
|
+
"""Manage a Matter server over WebSockets."""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
ws_server_url: str,
|
|
46
|
+
aiohttp_session: ClientSession,
|
|
47
|
+
):
|
|
48
|
+
"""Initialize the Client class."""
|
|
49
|
+
self.ws_server_url = ws_server_url
|
|
50
|
+
# server info is retrieved on connect
|
|
51
|
+
self.server_info: ServerInfoMessage | None = None
|
|
52
|
+
self._aiohttp_session = aiohttp_session
|
|
53
|
+
self._ws_client: ClientWebSocketResponse | None = None
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def connected(self) -> bool:
|
|
57
|
+
"""Return if we're currently connected."""
|
|
58
|
+
return self._ws_client is not None and not self._ws_client.closed
|
|
59
|
+
|
|
60
|
+
async def connect(self) -> None:
|
|
61
|
+
"""Connect to the websocket server."""
|
|
62
|
+
if self._ws_client is not None:
|
|
63
|
+
raise InvalidState("Already connected")
|
|
64
|
+
|
|
65
|
+
LOGGER.debug("Trying to connect")
|
|
66
|
+
try:
|
|
67
|
+
self._ws_client = await self._aiohttp_session.ws_connect(
|
|
68
|
+
self.ws_server_url,
|
|
69
|
+
heartbeat=55,
|
|
70
|
+
compress=15,
|
|
71
|
+
max_msg_size=0,
|
|
72
|
+
)
|
|
73
|
+
except (
|
|
74
|
+
client_exceptions.WSServerHandshakeError,
|
|
75
|
+
client_exceptions.ClientError,
|
|
76
|
+
) as err:
|
|
77
|
+
raise CannotConnect(err) from err
|
|
78
|
+
|
|
79
|
+
# at connect, the server sends a single message with the server info
|
|
80
|
+
info = cast(ServerInfoMessage, await self.receive_message_or_raise())
|
|
81
|
+
self.server_info = info
|
|
82
|
+
|
|
83
|
+
if info.schema_version < SCHEMA_VERSION:
|
|
84
|
+
# The client schema is too new, the server can't handle it yet
|
|
85
|
+
await self._ws_client.close()
|
|
86
|
+
raise ServerVersionTooOld(
|
|
87
|
+
f"Matter schema version is incompatible: {SCHEMA_VERSION}, "
|
|
88
|
+
f"the server supports at most {info.schema_version} "
|
|
89
|
+
"- update the Matter server to a more recent version or downgrade the client."
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if info.min_supported_schema_version > SCHEMA_VERSION:
|
|
93
|
+
# The client schema version is too low and can't be handled by the server anymore
|
|
94
|
+
await self._ws_client.close()
|
|
95
|
+
raise ServerVersionTooNew(
|
|
96
|
+
f"Matter schema version is incompatible: {SCHEMA_VERSION}, "
|
|
97
|
+
f"the server requires at least {info.min_supported_schema_version} "
|
|
98
|
+
"- update the Matter client to a more recent version or downgrade the server."
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
LOGGER.info(
|
|
102
|
+
"Connected to Matter Fabric %s (%s), Schema version %s, CHIP SDK Version %s",
|
|
103
|
+
info.fabric_id,
|
|
104
|
+
info.compressed_fabric_id,
|
|
105
|
+
info.schema_version,
|
|
106
|
+
info.sdk_version,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
async def disconnect(self) -> None:
|
|
110
|
+
"""Disconnect the client."""
|
|
111
|
+
LOGGER.debug("Closing client connection")
|
|
112
|
+
if self._ws_client is not None and not self._ws_client.closed:
|
|
113
|
+
await self._ws_client.close()
|
|
114
|
+
self._ws_client = None
|
|
115
|
+
|
|
116
|
+
async def receive_message_or_raise(self) -> MessageType:
|
|
117
|
+
"""Receive (raw) message or raise."""
|
|
118
|
+
assert self._ws_client
|
|
119
|
+
ws_msg = await self._ws_client.receive()
|
|
120
|
+
|
|
121
|
+
if ws_msg.type in (WSMsgType.CLOSE, WSMsgType.CLOSED, WSMsgType.CLOSING):
|
|
122
|
+
raise ConnectionClosed("Connection was closed.")
|
|
123
|
+
|
|
124
|
+
if ws_msg.type == WSMsgType.ERROR:
|
|
125
|
+
raise ConnectionFailed
|
|
126
|
+
|
|
127
|
+
if ws_msg.type != WSMsgType.TEXT:
|
|
128
|
+
raise InvalidMessage(
|
|
129
|
+
f"Received non-Text message: {ws_msg.type}: {ws_msg.data}"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
msg = parse_message(json_loads(ws_msg.data))
|
|
134
|
+
except TypeError as err:
|
|
135
|
+
raise InvalidMessage(f"Received unsupported JSON: {err}") from err
|
|
136
|
+
except ValueError as err:
|
|
137
|
+
raise InvalidMessage("Received invalid JSON.") from err
|
|
138
|
+
|
|
139
|
+
if VERBOSE_LOGGER and LOGGER.isEnabledFor(logging.DEBUG):
|
|
140
|
+
LOGGER.debug("Received message:\n%s\n", pprint.pformat(ws_msg))
|
|
141
|
+
|
|
142
|
+
return msg
|
|
143
|
+
|
|
144
|
+
async def send_message(self, message: CommandMessage) -> None:
|
|
145
|
+
"""
|
|
146
|
+
Send a CommandMessage to the server.
|
|
147
|
+
|
|
148
|
+
Raises NotConnected if client not connected.
|
|
149
|
+
"""
|
|
150
|
+
if not self.connected:
|
|
151
|
+
raise NotConnected
|
|
152
|
+
|
|
153
|
+
if VERBOSE_LOGGER and LOGGER.isEnabledFor(logging.DEBUG):
|
|
154
|
+
LOGGER.debug("Publishing message:\n%s\n", pprint.pformat(message))
|
|
155
|
+
|
|
156
|
+
assert self._ws_client
|
|
157
|
+
assert isinstance(message, CommandMessage)
|
|
158
|
+
|
|
159
|
+
await self._ws_client.send_json(message, dumps=json_dumps)
|
|
160
|
+
|
|
161
|
+
def __repr__(self) -> str:
|
|
162
|
+
"""Return the representation."""
|
|
163
|
+
prefix = "" if self.connected else "not "
|
|
164
|
+
return f"{type(self).__name__}(ws_server_url={self.ws_server_url!r}, {prefix}connected)"
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def parse_message(raw: dict) -> MessageType:
|
|
168
|
+
"""Parse Message from raw dict object."""
|
|
169
|
+
if "event" in raw:
|
|
170
|
+
return dataclass_from_dict(EventMessage, raw)
|
|
171
|
+
if "error_code" in raw:
|
|
172
|
+
return dataclass_from_dict(ErrorResultMessage, raw)
|
|
173
|
+
if "result" in raw:
|
|
174
|
+
return dataclass_from_dict(SuccessResultMessage, raw)
|
|
175
|
+
if "sdk_version" in raw:
|
|
176
|
+
return dataclass_from_dict(ServerInfoMessage, raw)
|
|
177
|
+
return dataclass_from_dict(CommandMessage, raw)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Client-specific Exceptions for matter-server library."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class MatterClientException(Exception):
|
|
7
|
+
"""Generic Matter exception."""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TransportError(MatterClientException):
|
|
11
|
+
"""Exception raised to represent transport errors."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, message: str, error: Exception | None = None) -> None:
|
|
14
|
+
"""Initialize a transport error."""
|
|
15
|
+
super().__init__(message)
|
|
16
|
+
self.error = error
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ConnectionClosed(TransportError):
|
|
20
|
+
"""Exception raised when the connection is closed."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CannotConnect(TransportError):
|
|
24
|
+
"""Exception raised when failed to connect the client."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, error: Exception) -> None:
|
|
27
|
+
"""Initialize a cannot connect error."""
|
|
28
|
+
super().__init__(f"{error}", error)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ConnectionFailed(TransportError):
|
|
32
|
+
"""Exception raised when an established connection fails."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, error: Exception | None = None) -> None:
|
|
35
|
+
"""Initialize a connection failed error."""
|
|
36
|
+
if error is None:
|
|
37
|
+
super().__init__("Connection failed.")
|
|
38
|
+
return
|
|
39
|
+
super().__init__(f"{error}", error)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class NotConnected(MatterClientException):
|
|
43
|
+
"""Exception raised when not connected to client."""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class InvalidState(MatterClientException):
|
|
47
|
+
"""Exception raised when data gets in invalid state."""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class InvalidMessage(MatterClientException):
|
|
51
|
+
"""Exception raised when an invalid message is received."""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class InvalidServerVersion(MatterClientException):
|
|
55
|
+
"""Exception raised when connected to server with incompatible version."""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class ServerVersionTooOld(InvalidServerVersion):
|
|
59
|
+
"""Exception raised when connected to server with is too old to support this client."""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class ServerVersionTooNew(InvalidServerVersion):
|
|
63
|
+
"""Exception raised when connected to server with is too new for this client."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Client-only models for the Python Matter Server library."""
|