python-swidget 1.2.4__tar.gz → 1.3.0__tar.gz
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.
- {python_swidget-1.2.4 → python_swidget-1.3.0}/PKG-INFO +3 -2
- {python_swidget-1.2.4 → python_swidget-1.3.0}/pyproject.toml +1 -1
- {python_swidget-1.2.4 → python_swidget-1.3.0}/swidget/__init__.py +2 -0
- {python_swidget-1.2.4 → python_swidget-1.3.0}/swidget/discovery.py +2 -3
- {python_swidget-1.2.4 → python_swidget-1.3.0}/swidget/swidgetdevice.py +18 -24
- {python_swidget-1.2.4 → python_swidget-1.3.0}/swidget/swidgetdimmer.py +1 -1
- python_swidget-1.3.0/swidget/websocket.py +207 -0
- python_swidget-1.2.4/swidget/websocket.py +0 -188
- {python_swidget-1.2.4 → python_swidget-1.3.0}/README.md +0 -0
- {python_swidget-1.2.4 → python_swidget-1.3.0}/swidget/cli.py +0 -0
- {python_swidget-1.2.4 → python_swidget-1.3.0}/swidget/exceptions.py +0 -0
- {python_swidget-1.2.4 → python_swidget-1.3.0}/swidget/provision.py +0 -0
- {python_swidget-1.2.4 → python_swidget-1.3.0}/swidget/py.typed +0 -0
- {python_swidget-1.2.4 → python_swidget-1.3.0}/swidget/swidgetoutlet.py +0 -0
- {python_swidget-1.2.4 → python_swidget-1.3.0}/swidget/swidgetswitch.py +0 -0
- {python_swidget-1.2.4 → python_swidget-1.3.0}/swidget/swidgettimerswitch.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
2
|
Name: python-swidget
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: Python API for Swidget smart devices
|
|
5
5
|
Home-page: https://github.com/swidget/python-swidget
|
|
6
6
|
License: GPL-3.0-or-later
|
|
@@ -13,6 +13,7 @@ Classifier: Programming Language :: Python :: 3.9
|
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.10
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.11
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
17
|
Provides-Extra: docs
|
|
17
18
|
Requires-Dist: aiohttp (>=3.8.1)
|
|
18
19
|
Requires-Dist: anyio
|
|
@@ -19,6 +19,7 @@ from swidget.exceptions import SwidgetException
|
|
|
19
19
|
from swidget.provision import provision_wifi
|
|
20
20
|
from swidget.swidgetdevice import (
|
|
21
21
|
DeviceType,
|
|
22
|
+
InsertType,
|
|
22
23
|
SwidgetAssembly,
|
|
23
24
|
SwidgetComponent,
|
|
24
25
|
SwidgetDevice,
|
|
@@ -38,6 +39,7 @@ __all__ = [
|
|
|
38
39
|
"SwidgetDiscoveredDevice",
|
|
39
40
|
"SwidgetException",
|
|
40
41
|
"DeviceType",
|
|
42
|
+
"InsertType",
|
|
41
43
|
"SwidgetAssembly",
|
|
42
44
|
"SwidgetDevice",
|
|
43
45
|
"SwidgetComponent",
|
|
@@ -59,7 +59,7 @@ class SwidgetProtocol(ssdp.SimpleServiceDiscoveryProtocol):
|
|
|
59
59
|
insert_type=insert_type,
|
|
60
60
|
)
|
|
61
61
|
_LOGGER.debug(
|
|
62
|
-
f"Swidget device '{friendly_name}' at {ip_address}
|
|
62
|
+
f"Discovered Swidget device via SSDP: '{friendly_name}' at {ip_address} Type:{device_type}/{insert_type}"
|
|
63
63
|
)
|
|
64
64
|
|
|
65
65
|
|
|
@@ -108,9 +108,8 @@ async def discover_single(
|
|
|
108
108
|
|
|
109
109
|
_LOGGER.debug(f"Creating new device class of type: {device_type}")
|
|
110
110
|
device_class = _get_device_class(device_type)
|
|
111
|
-
_LOGGER.debug(f"{device_class}")
|
|
111
|
+
_LOGGER.debug(f"{device_class} created")
|
|
112
112
|
dev = device_class(host, token_name, password, use_https, use_websockets)
|
|
113
|
-
await dev.start()
|
|
114
113
|
return dev
|
|
115
114
|
|
|
116
115
|
|
|
@@ -115,10 +115,10 @@ class SwidgetDevice:
|
|
|
115
115
|
async def start(self) -> None:
|
|
116
116
|
"""Start the websocket."""
|
|
117
117
|
_LOGGER.debug("SwidgetDevice.start()")
|
|
118
|
-
if self.use_websockets:
|
|
118
|
+
if self.use_websockets and not self.connected:
|
|
119
119
|
_LOGGER.debug("Calling self._websocket.connect()")
|
|
120
120
|
await self._websocket.connect()
|
|
121
|
-
_LOGGER.debug("Calling self.update()
|
|
121
|
+
_LOGGER.debug("Calling self.update()")
|
|
122
122
|
await self.update()
|
|
123
123
|
|
|
124
124
|
async def stop(self) -> bool:
|
|
@@ -143,18 +143,18 @@ class SwidgetDevice:
|
|
|
143
143
|
"""Wrapper for the stop() function."""
|
|
144
144
|
await self.stop()
|
|
145
145
|
|
|
146
|
-
def add_event_callback(self, callback: Callable[[Any], Any]):
|
|
146
|
+
def add_event_callback(self, callback: Callable[[Any], Any]) -> bool:
|
|
147
147
|
"""Register a function to be called when a new websocket message is recieved."""
|
|
148
148
|
for c in self._subscribers:
|
|
149
149
|
if c == callback:
|
|
150
|
-
_LOGGER.
|
|
150
|
+
_LOGGER.warning(
|
|
151
151
|
"Callback has already been added, not adding the same callback function again"
|
|
152
152
|
)
|
|
153
153
|
return False
|
|
154
154
|
self._subscribers.append(callback)
|
|
155
155
|
return True
|
|
156
156
|
|
|
157
|
-
def remove_event_callback(self, callback: Callable[[Any], Any]):
|
|
157
|
+
def remove_event_callback(self, callback: Callable[[Any], Any]) -> bool:
|
|
158
158
|
"""Remove a registered callback function."""
|
|
159
159
|
if callback in self._subscribers:
|
|
160
160
|
self._subscribers.remove(callback)
|
|
@@ -181,7 +181,7 @@ class SwidgetDevice:
|
|
|
181
181
|
)
|
|
182
182
|
await self.signal_callbacks(message)
|
|
183
183
|
|
|
184
|
-
async def signal_callbacks(self, message):
|
|
184
|
+
async def signal_callbacks(self, message) -> None:
|
|
185
185
|
"""Call any available registered callback functions."""
|
|
186
186
|
_LOGGER.debug("SwidgetDevice.signal_callsbacks() called")
|
|
187
187
|
for callback in self._subscribers:
|
|
@@ -190,20 +190,14 @@ class SwidgetDevice:
|
|
|
190
190
|
async def get_device_config(self) -> Any:
|
|
191
191
|
"""Get the config of the device."""
|
|
192
192
|
_LOGGER.debug("SwidgetDevice.get_device_config() called")
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
url=f"{self.uri_scheme}://{self.ip_address}/api/v1/device_config",
|
|
202
|
-
ssl=False,
|
|
203
|
-
) as response:
|
|
204
|
-
config = await response.json()
|
|
205
|
-
self.device_config = DeviceConfiguration(config)
|
|
206
|
-
self._last_update = int(time.time())
|
|
193
|
+
_LOGGER.debug("Sending get_summary() command over http")
|
|
194
|
+
async with self._session.get(
|
|
195
|
+
url=f"{self.uri_scheme}://{self.ip_address}/api/v1/device_config",
|
|
196
|
+
ssl=False,
|
|
197
|
+
) as response:
|
|
198
|
+
config = await response.json()
|
|
199
|
+
self.device_config = DeviceConfiguration(config)
|
|
200
|
+
self._last_update = int(time.time())
|
|
207
201
|
|
|
208
202
|
async def get_summary(self) -> None:
|
|
209
203
|
"""Get a summary of the device over HTTP."""
|
|
@@ -325,7 +319,7 @@ class SwidgetDevice:
|
|
|
325
319
|
_LOGGER.debug("SwidgetDevice.send_command() called")
|
|
326
320
|
data = {assembly: {"components": {component: {function: command}}}}
|
|
327
321
|
_LOGGER.debug(f"Command to send: {data}")
|
|
328
|
-
if self.use_websockets:
|
|
322
|
+
if self.use_websockets and self.connected is True:
|
|
329
323
|
_LOGGER.debug("In websocket mode. Sending command over websocket")
|
|
330
324
|
command_data = json.dumps(
|
|
331
325
|
{"type": "command", "request_id": "command", "payload": data}
|
|
@@ -653,12 +647,12 @@ class SwidgetDevice:
|
|
|
653
647
|
"""Return True if the device is dimmable."""
|
|
654
648
|
return self.is_dimmer
|
|
655
649
|
|
|
656
|
-
@property
|
|
650
|
+
@property
|
|
657
651
|
def friendly_name(self) -> str:
|
|
658
652
|
"""Return a friendly description of the device."""
|
|
659
653
|
return self._friendly_name
|
|
660
654
|
|
|
661
|
-
@property
|
|
655
|
+
@property
|
|
662
656
|
def is_on(self) -> bool:
|
|
663
657
|
"""Return whether device is on."""
|
|
664
658
|
_LOGGER.debug("SwidgetDevice.is_on called")
|
|
@@ -703,7 +697,7 @@ class SwidgetDevice:
|
|
|
703
697
|
command={"state": "off"},
|
|
704
698
|
)
|
|
705
699
|
|
|
706
|
-
@property
|
|
700
|
+
@property
|
|
707
701
|
def usb_is_on(self) -> bool:
|
|
708
702
|
"""Return whether USB is on."""
|
|
709
703
|
_LOGGER.debug("SwidgetDevice.usb_is_on called")
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""Module to handle websocket connections to Swidget devices."""
|
|
2
|
+
import asyncio
|
|
3
|
+
import logging
|
|
4
|
+
import socket
|
|
5
|
+
from typing import Any, Awaitable, Callable, Union
|
|
6
|
+
|
|
7
|
+
import aiohttp
|
|
8
|
+
from aiohttp import (
|
|
9
|
+
ClientConnectionError,
|
|
10
|
+
ClientWebSocketResponse,
|
|
11
|
+
WSMsgType,
|
|
12
|
+
WSServerHandshakeError,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
_LOGGER = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SwidgetWebsocket:
|
|
19
|
+
"""A websocket connection to a Swidget Device."""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
host: str,
|
|
24
|
+
token_name: str,
|
|
25
|
+
secret_key: str,
|
|
26
|
+
callback: Union[Callable[[Any], None], Callable[[Any], Awaitable[None]]],
|
|
27
|
+
session: aiohttp.ClientSession | None = None,
|
|
28
|
+
use_security: bool = True,
|
|
29
|
+
retry_interval: int = 30, # Initial retry interval in seconds
|
|
30
|
+
max_retries: int = 200, # Maximum number of reconnection attempts
|
|
31
|
+
):
|
|
32
|
+
"""Initialize the SwidgetWebsocket.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
host: The hostname or IP address of the Swidget device.
|
|
36
|
+
token_name: The name of the authentication token.
|
|
37
|
+
secret_key: The secret key for authentication.
|
|
38
|
+
callback: A callable that will be called with received messages.
|
|
39
|
+
session: An optional aiohttp.ClientSession to use.
|
|
40
|
+
use_security: Whether to use wss:// (True) or ws:// (False).
|
|
41
|
+
retry_interval: Initial interval for reconnection attempts.
|
|
42
|
+
max_retries: Maximum number of reconnection attempts.
|
|
43
|
+
"""
|
|
44
|
+
self.host = host
|
|
45
|
+
self.token_name = token_name or "x-secret-key"
|
|
46
|
+
self.secret_key = secret_key or ""
|
|
47
|
+
self.session = session or aiohttp.ClientSession()
|
|
48
|
+
self.use_security = use_security
|
|
49
|
+
self.callback = callback
|
|
50
|
+
self.retry_interval = retry_interval
|
|
51
|
+
self.max_retries = max_retries
|
|
52
|
+
self.retry_count = 0
|
|
53
|
+
self._verify_ssl = False
|
|
54
|
+
self.uri = self._get_uri()
|
|
55
|
+
|
|
56
|
+
# self._client= None
|
|
57
|
+
self.is_running = True
|
|
58
|
+
self._closing = False
|
|
59
|
+
self._client: ClientWebSocketResponse | None = None
|
|
60
|
+
self._receiver_task: asyncio.Task | None = None
|
|
61
|
+
self._closing = False
|
|
62
|
+
|
|
63
|
+
def _get_uri(self) -> str:
|
|
64
|
+
"""Generate the websocket URI."""
|
|
65
|
+
protocol = "wss" if self.use_security else "ws"
|
|
66
|
+
return (
|
|
67
|
+
f"{protocol}://{self.host}/api/v1/sock?{self.token_name}={self.secret_key}"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
async def connect(self) -> None:
|
|
71
|
+
"""Connect to the websocket server."""
|
|
72
|
+
_LOGGER.debug("websocket.connect() called")
|
|
73
|
+
|
|
74
|
+
if self.connected:
|
|
75
|
+
_LOGGER.debug("Websocket already connected")
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
if self._client is not None:
|
|
79
|
+
raise ConnectionError("Already connected")
|
|
80
|
+
|
|
81
|
+
if not self.session:
|
|
82
|
+
raise ConnectionError("No aiohttp session available")
|
|
83
|
+
|
|
84
|
+
headers = {"Connection": "Upgrade", self.token_name: self.secret_key}
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
_LOGGER.debug(f"Trying to connect to {self.host}")
|
|
88
|
+
self._client = await self.session.ws_connect(
|
|
89
|
+
url=self.uri,
|
|
90
|
+
headers=headers,
|
|
91
|
+
verify_ssl=self._verify_ssl,
|
|
92
|
+
heartbeat=30,
|
|
93
|
+
)
|
|
94
|
+
self.retry_count = 0
|
|
95
|
+
_LOGGER.debug("Websocket now connected")
|
|
96
|
+
except (ClientConnectionError, WSServerHandshakeError) as e:
|
|
97
|
+
_LOGGER.error(f"Error connecting to websocket: {e}")
|
|
98
|
+
self._client = None
|
|
99
|
+
except socket.gaierror as e:
|
|
100
|
+
_LOGGER.error(f"Error resolving host: {e}")
|
|
101
|
+
self._client = None
|
|
102
|
+
except Exception as e:
|
|
103
|
+
_LOGGER.error(f"An unexpected error occurred: {e}")
|
|
104
|
+
self._client = None
|
|
105
|
+
|
|
106
|
+
async def send_str(self, message: str) -> None:
|
|
107
|
+
"""Send a string message through the websocket with retry attempts."""
|
|
108
|
+
_LOGGER.debug("websocket.send_str() called")
|
|
109
|
+
max_send_retries = 3
|
|
110
|
+
for attempt in range(max_send_retries):
|
|
111
|
+
try:
|
|
112
|
+
if self._client is not None:
|
|
113
|
+
await self._client.send_str(message)
|
|
114
|
+
return
|
|
115
|
+
else:
|
|
116
|
+
_LOGGER.warning("Websocket is not connected, not sending")
|
|
117
|
+
return
|
|
118
|
+
except Exception as e:
|
|
119
|
+
_LOGGER.warning(
|
|
120
|
+
f"Error sending message, attempt {attempt}/{max_send_retries}: {e}"
|
|
121
|
+
)
|
|
122
|
+
if attempt < max_send_retries - 1:
|
|
123
|
+
await asyncio.sleep(5**attempt)
|
|
124
|
+
else:
|
|
125
|
+
_LOGGER.error(
|
|
126
|
+
f"Failed to send message after {max_send_retries} attempts."
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
async def receive(self):
|
|
130
|
+
"""Receive a message from the WebSocket server."""
|
|
131
|
+
_LOGGER.debug("websocket.receive() called")
|
|
132
|
+
try:
|
|
133
|
+
if self._client is not None:
|
|
134
|
+
message = await self._client.receive()
|
|
135
|
+
if message.type == WSMsgType.TEXT:
|
|
136
|
+
message_data = message.json()
|
|
137
|
+
_LOGGER.debug(f"[{self.host}] Received message: {message_data}")
|
|
138
|
+
return message_data
|
|
139
|
+
elif message.type == WSMsgType.CLOSED:
|
|
140
|
+
_LOGGER.error("Websocket client is closed")
|
|
141
|
+
self._client = None
|
|
142
|
+
elif message.type == WSMsgType.ERROR:
|
|
143
|
+
_LOGGER.error("WebSocket error.")
|
|
144
|
+
self._client = None
|
|
145
|
+
except Exception as e:
|
|
146
|
+
_LOGGER.error(f"Error receiving message: {e}")
|
|
147
|
+
|
|
148
|
+
async def close(self):
|
|
149
|
+
"""Close the WebSocket connection."""
|
|
150
|
+
_LOGGER.debug("websocket.close() called")
|
|
151
|
+
self.is_running = False
|
|
152
|
+
self._closing = True
|
|
153
|
+
if self._client is not None:
|
|
154
|
+
await self._client.close()
|
|
155
|
+
self._client = None
|
|
156
|
+
if self.session and not self.session.closed:
|
|
157
|
+
await self.session.close()
|
|
158
|
+
|
|
159
|
+
async def reconnect(self):
|
|
160
|
+
"""Reconnect to the WebSocket server after a delay."""
|
|
161
|
+
_LOGGER.debug("websocket.reconnect() called")
|
|
162
|
+
if self.max_retries is not None and self.retry_count >= self.max_retries:
|
|
163
|
+
_LOGGER.warning("Max retries reached. Stopping reconnect attempts.")
|
|
164
|
+
self.is_running = False
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
self.retry_count += 1
|
|
168
|
+
delay = self.retry_interval * (2 ** (self.retry_count - 1))
|
|
169
|
+
_LOGGER.warning(
|
|
170
|
+
f"Reconnecting in {delay} seconds (attempt {self.retry_count})..."
|
|
171
|
+
)
|
|
172
|
+
await asyncio.sleep(delay)
|
|
173
|
+
await self.connect()
|
|
174
|
+
|
|
175
|
+
async def run(self):
|
|
176
|
+
"""Run the WebSocket client to handle messages and reconnections."""
|
|
177
|
+
while self.is_running:
|
|
178
|
+
if self._client is None:
|
|
179
|
+
await self.reconnect()
|
|
180
|
+
if self._client is not None:
|
|
181
|
+
message = await self.receive()
|
|
182
|
+
if message and self.callback:
|
|
183
|
+
if asyncio.iscoroutinefunction(self.callback):
|
|
184
|
+
await self.callback(message)
|
|
185
|
+
else:
|
|
186
|
+
self.callback(message)
|
|
187
|
+
|
|
188
|
+
def status(self) -> dict:
|
|
189
|
+
"""Return the current status of the websocket connection."""
|
|
190
|
+
_LOGGER.debug("websocket.status() called")
|
|
191
|
+
return {
|
|
192
|
+
"host": self.host,
|
|
193
|
+
"connected": self._client is not None,
|
|
194
|
+
"closing": self._closing,
|
|
195
|
+
"retry_interval": self.retry_interval,
|
|
196
|
+
"max_retries": self.max_retries,
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
@property
|
|
200
|
+
def connected(self) -> bool:
|
|
201
|
+
"""Return the status of the connection."""
|
|
202
|
+
return self._client is not None and not self._client.closed
|
|
203
|
+
|
|
204
|
+
def __repr__(self) -> str:
|
|
205
|
+
"""Return the representation."""
|
|
206
|
+
prefix = "" if self.connected else "not "
|
|
207
|
+
return f"{type(self).__name__}(ws_server_url={self.host}, {prefix}connected)"
|
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
"""Module to handle websocket connections to Swidget devices."""
|
|
2
|
-
import asyncio
|
|
3
|
-
import logging
|
|
4
|
-
import socket
|
|
5
|
-
from typing import Any
|
|
6
|
-
|
|
7
|
-
import aiohttp
|
|
8
|
-
from aiohttp import ClientWebSocketResponse, WSMsgType
|
|
9
|
-
|
|
10
|
-
_LOGGER = logging.getLogger(__name__)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
async def cancel_task(*tasks: asyncio.Task | None) -> None:
|
|
14
|
-
"""Cancel task(s)."""
|
|
15
|
-
for task in tasks:
|
|
16
|
-
if task is not None and not task.done():
|
|
17
|
-
task.cancel()
|
|
18
|
-
try:
|
|
19
|
-
await task
|
|
20
|
-
except asyncio.CancelledError:
|
|
21
|
-
pass
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class SwidgetWebsocket:
|
|
25
|
-
"""A websocket connection to a Swidget Device."""
|
|
26
|
-
|
|
27
|
-
# pylint: disable=too-many-instance-attributes
|
|
28
|
-
_client: aiohttp.ClientWebSocketResponse | None = None
|
|
29
|
-
|
|
30
|
-
def __init__(
|
|
31
|
-
self,
|
|
32
|
-
host,
|
|
33
|
-
token_name,
|
|
34
|
-
secret_key,
|
|
35
|
-
callback,
|
|
36
|
-
session=None,
|
|
37
|
-
use_security=True,
|
|
38
|
-
):
|
|
39
|
-
self.host = host
|
|
40
|
-
self.session = session or aiohttp.ClientSession()
|
|
41
|
-
self.use_security = use_security
|
|
42
|
-
self.uri = self.get_uri(host, token_name, secret_key)
|
|
43
|
-
self.callback = callback
|
|
44
|
-
self._verify_ssl = False
|
|
45
|
-
self._state = None
|
|
46
|
-
self.failed_attempts = 0
|
|
47
|
-
self._error_reason = None
|
|
48
|
-
self.headers = {"Connection": "Upgrade"}
|
|
49
|
-
self._receiver_task: asyncio.Task | None = None
|
|
50
|
-
|
|
51
|
-
@property
|
|
52
|
-
def connected(self) -> bool:
|
|
53
|
-
"""Return of status of whether the device is currently connected."""
|
|
54
|
-
return self._client is not None and not self._client.closed
|
|
55
|
-
|
|
56
|
-
@property
|
|
57
|
-
def websocket(self) -> ClientWebSocketResponse | None:
|
|
58
|
-
"""Return the web socket."""
|
|
59
|
-
return self._client
|
|
60
|
-
|
|
61
|
-
def get_uri(self, host, token_name, secret_key):
|
|
62
|
-
"""Generate the websocket URI."""
|
|
63
|
-
if self.use_security:
|
|
64
|
-
return f"wss://{host}/api/v1/sock?{token_name}={secret_key}"
|
|
65
|
-
else:
|
|
66
|
-
return f"ws://{host}/api/v1/sock?{token_name}={secret_key}"
|
|
67
|
-
|
|
68
|
-
async def connect(self) -> None:
|
|
69
|
-
"""Create a new connection and, optionally, start the monitor."""
|
|
70
|
-
_LOGGER.debug("websocket.connect() called")
|
|
71
|
-
await cancel_task(self._receiver_task)
|
|
72
|
-
if self.connected:
|
|
73
|
-
_LOGGER.debug("Websocket already connected")
|
|
74
|
-
return
|
|
75
|
-
|
|
76
|
-
if self._client is not None:
|
|
77
|
-
raise ConnectionError("Already connected")
|
|
78
|
-
|
|
79
|
-
if not self.session:
|
|
80
|
-
raise ConnectionError("No aiohttp session available")
|
|
81
|
-
|
|
82
|
-
try:
|
|
83
|
-
_LOGGER.debug("Trying to connect")
|
|
84
|
-
self._client = await self.session.ws_connect(
|
|
85
|
-
url=self.uri,
|
|
86
|
-
headers=self.headers,
|
|
87
|
-
verify_ssl=self._verify_ssl,
|
|
88
|
-
heartbeat=30,
|
|
89
|
-
)
|
|
90
|
-
_LOGGER.debug("Websocket now connected")
|
|
91
|
-
except aiohttp.WSServerHandshakeError as handshake_error:
|
|
92
|
-
_LOGGER.error(
|
|
93
|
-
f"Error occurred during websocket handshake: {handshake_error}"
|
|
94
|
-
)
|
|
95
|
-
raise
|
|
96
|
-
except aiohttp.ClientConnectionError as connection_error:
|
|
97
|
-
_LOGGER.error(
|
|
98
|
-
f"Error connecting to the websocket server: {connection_error}"
|
|
99
|
-
)
|
|
100
|
-
raise
|
|
101
|
-
except socket.gaierror as gai_error:
|
|
102
|
-
_LOGGER.error(f"Error resolving host: {gai_error}")
|
|
103
|
-
raise
|
|
104
|
-
except Exception as e:
|
|
105
|
-
_LOGGER.error(f"An unexpected error occurred: {e}")
|
|
106
|
-
raise
|
|
107
|
-
self._receiver_task = asyncio.ensure_future(self.listen())
|
|
108
|
-
|
|
109
|
-
async def close(self) -> None:
|
|
110
|
-
"""Close the websocket."""
|
|
111
|
-
_LOGGER.debug("websocket.close() called")
|
|
112
|
-
if self._client is not None and not self._client.closed:
|
|
113
|
-
await self._client.close()
|
|
114
|
-
self._client = None
|
|
115
|
-
|
|
116
|
-
async def disconnect(self) -> None:
|
|
117
|
-
"""Wrapper of the close() function."""
|
|
118
|
-
await self.close()
|
|
119
|
-
|
|
120
|
-
async def send_str(self, message):
|
|
121
|
-
"""Send a message through the websocket."""
|
|
122
|
-
_LOGGER.debug("websocket.send_str() called")
|
|
123
|
-
if not self.connected:
|
|
124
|
-
raise ConnectionError
|
|
125
|
-
message = str(message)
|
|
126
|
-
_LOGGER.debug(f"Sending messsage over websocket: {message}")
|
|
127
|
-
await self._client.send_str(f"{message}")
|
|
128
|
-
|
|
129
|
-
async def listen(self):
|
|
130
|
-
"""Ask the client to listen to websocket events."""
|
|
131
|
-
_LOGGER.debug("websocket.listen() called")
|
|
132
|
-
# Check if the websocket is connected
|
|
133
|
-
if not self._client or not self.connected:
|
|
134
|
-
_LOGGER.error("Websocket is not connected")
|
|
135
|
-
raise ConnectionError("Websocket is not connected")
|
|
136
|
-
|
|
137
|
-
while not self._client.closed:
|
|
138
|
-
try:
|
|
139
|
-
message = await self._client.receive()
|
|
140
|
-
except StopAsyncIteration:
|
|
141
|
-
break
|
|
142
|
-
|
|
143
|
-
if message.type == aiohttp.WSMsgType.ERROR:
|
|
144
|
-
raise ConnectionError("Websocket error occurred")
|
|
145
|
-
|
|
146
|
-
if message.type == aiohttp.WSMsgType.TEXT:
|
|
147
|
-
message_data = message.json()
|
|
148
|
-
_LOGGER.debug(f"Received from websocket: {message_data}")
|
|
149
|
-
await self.callback(message_data)
|
|
150
|
-
|
|
151
|
-
if message.type in (
|
|
152
|
-
aiohttp.WSMsgType.CLOSE,
|
|
153
|
-
aiohttp.WSMsgType.CLOSED,
|
|
154
|
-
aiohttp.WSMsgType.CLOSING,
|
|
155
|
-
):
|
|
156
|
-
_LOGGER.error("Connection to the Swidget WebSocket has been closed")
|
|
157
|
-
break
|
|
158
|
-
|
|
159
|
-
self._client = None # Ensure client is set to None when closed
|
|
160
|
-
|
|
161
|
-
async def receive_message_or_raise(self) -> Any:
|
|
162
|
-
"""Receive ONE (raw) message or raise."""
|
|
163
|
-
assert self._client
|
|
164
|
-
ws_msg = await self._client.receive()
|
|
165
|
-
|
|
166
|
-
if ws_msg.type in (WSMsgType.CLOSE, WSMsgType.CLOSED, WSMsgType.CLOSING):
|
|
167
|
-
raise ConnectionError("Connection was closed.")
|
|
168
|
-
|
|
169
|
-
if ws_msg.type == WSMsgType.ERROR:
|
|
170
|
-
raise ConnectionError
|
|
171
|
-
|
|
172
|
-
if ws_msg.type != WSMsgType.TEXT:
|
|
173
|
-
raise ValueError(f"Received non-Text message: {ws_msg.type}: {ws_msg.data}")
|
|
174
|
-
|
|
175
|
-
try:
|
|
176
|
-
msg = ws_msg.json()
|
|
177
|
-
except TypeError as err:
|
|
178
|
-
raise TypeError(f"Received unsupported JSON: {err}") from err
|
|
179
|
-
except ValueError as err:
|
|
180
|
-
raise ValueError("Received invalid JSON.") from err
|
|
181
|
-
|
|
182
|
-
_LOGGER.debug(f"Received message:\n{msg}\n")
|
|
183
|
-
return msg
|
|
184
|
-
|
|
185
|
-
def __repr__(self) -> str:
|
|
186
|
-
"""Return the representation."""
|
|
187
|
-
prefix = "" if self.connected else "not "
|
|
188
|
-
return f"{type(self).__name__}(ws_server_url={self.host}, {prefix}connected)"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|