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.
@@ -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."""