python-swidget 1.3.2__tar.gz → 1.4.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.4.0/PKG-INFO +270 -0
- python_swidget-1.4.0/README.md +235 -0
- {python_swidget-1.3.2 → python_swidget-1.4.0}/pyproject.toml +10 -4
- {python_swidget-1.3.2 → python_swidget-1.4.0}/swidget/__init__.py +1 -1
- {python_swidget-1.3.2 → python_swidget-1.4.0}/swidget/cli.py +2 -2
- {python_swidget-1.3.2 → python_swidget-1.4.0}/swidget/discovery.py +3 -1
- python_swidget-1.4.0/swidget/exceptions.py +13 -0
- {python_swidget-1.3.2 → python_swidget-1.4.0}/swidget/swidgetdevice.py +174 -87
- {python_swidget-1.3.2 → python_swidget-1.4.0}/swidget/swidgetdimmer.py +2 -2
- {python_swidget-1.3.2 → python_swidget-1.4.0}/swidget/swidgettimerswitch.py +2 -2
- {python_swidget-1.3.2 → python_swidget-1.4.0}/swidget/websocket.py +21 -16
- python_swidget-1.3.2/PKG-INFO +0 -55
- python_swidget-1.3.2/README.md +0 -21
- python_swidget-1.3.2/swidget/exceptions.py +0 -2
- {python_swidget-1.3.2 → python_swidget-1.4.0}/swidget/provision.py +0 -0
- {python_swidget-1.3.2 → python_swidget-1.4.0}/swidget/py.typed +0 -0
- {python_swidget-1.3.2 → python_swidget-1.4.0}/swidget/swidgetoutlet.py +0 -0
- {python_swidget-1.3.2 → python_swidget-1.4.0}/swidget/swidgetswitch.py +0 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-swidget
|
|
3
|
+
Version: 1.4.0
|
|
4
|
+
Summary: Python API for Swidget smart devices
|
|
5
|
+
License: GPL-3.0-or-later
|
|
6
|
+
Author: Swidget
|
|
7
|
+
Requires-Python: >=3.8,<4.0
|
|
8
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Provides-Extra: docs
|
|
18
|
+
Requires-Dist: aiohttp (>=3.8.1)
|
|
19
|
+
Requires-Dist: anyio
|
|
20
|
+
Requires-Dist: asyncclick (>=8)
|
|
21
|
+
Requires-Dist: importlib-metadata
|
|
22
|
+
Requires-Dist: m2r (>=0,<1) ; extra == "docs"
|
|
23
|
+
Requires-Dist: mistune (<2.0.0) ; extra == "docs"
|
|
24
|
+
Requires-Dist: pydantic (>=2,<3)
|
|
25
|
+
Requires-Dist: requests (==2.32.5)
|
|
26
|
+
Requires-Dist: sphinx (>=4,<5) ; extra == "docs"
|
|
27
|
+
Requires-Dist: sphinx_rtd_theme (>=0,<1) ; extra == "docs"
|
|
28
|
+
Requires-Dist: sphinxcontrib-programoutput (>=0,<1) ; extra == "docs"
|
|
29
|
+
Requires-Dist: ssdp (==1.1.1)
|
|
30
|
+
Requires-Dist: types-requests (>=2.32.0,<3.0.0)
|
|
31
|
+
Requires-Dist: urllib3 (==2.5.0)
|
|
32
|
+
Project-URL: Repository, https://github.com/swidget/python-swidget
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
python-swidget
|
|
36
|
+
==============
|
|
37
|
+
|
|
38
|
+

|
|
39
|
+

|
|
40
|
+

|
|
41
|
+
|
|
42
|
+
Python SDK for Swidget smart devices. It supports local HTTP control and websockets for realtime updates, along with SSDP discovery, Wi-Fi provisioning helpers, and a CLI tool (`swidget`).
|
|
43
|
+
|
|
44
|
+
Table of contents
|
|
45
|
+
-----------------
|
|
46
|
+
- Installation
|
|
47
|
+
- Quickstart
|
|
48
|
+
- Device types and helpers
|
|
49
|
+
- HTTP vs websocket modes
|
|
50
|
+
- Discovery
|
|
51
|
+
- Provisioning (AP mode)
|
|
52
|
+
- CLI usage
|
|
53
|
+
- Development (tests, lint, hooks)
|
|
54
|
+
|
|
55
|
+
Installation
|
|
56
|
+
------------
|
|
57
|
+
Using Poetry (preferred):
|
|
58
|
+
```
|
|
59
|
+
poetry install --with dev
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Using pip:
|
|
63
|
+
```
|
|
64
|
+
pip install python-swidget
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Quickstart
|
|
68
|
+
----------
|
|
69
|
+
```python
|
|
70
|
+
from swidget import SwidgetDimmer
|
|
71
|
+
|
|
72
|
+
dev = SwidgetDimmer(
|
|
73
|
+
host="192.168.1.50",
|
|
74
|
+
token_name="x-secret-key",
|
|
75
|
+
secret_key="password",
|
|
76
|
+
use_https=True,
|
|
77
|
+
use_websockets=False, # set True for realtime updates
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# HTTP-only mode
|
|
81
|
+
await dev.update()
|
|
82
|
+
await dev.turn_on()
|
|
83
|
+
await dev.turn_off()
|
|
84
|
+
await dev.close()
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Websocket mode (realtime)
|
|
88
|
+
```python
|
|
89
|
+
dev = SwidgetDimmer(
|
|
90
|
+
host="192.168.1.50",
|
|
91
|
+
token_name="x-secret-key",
|
|
92
|
+
secret_key="password",
|
|
93
|
+
use_https=True,
|
|
94
|
+
use_websockets=True,
|
|
95
|
+
)
|
|
96
|
+
await dev.start() # opens websocket and updates state
|
|
97
|
+
await dev.turn_on()
|
|
98
|
+
await dev.close()
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Device types and helpers
|
|
102
|
+
------------------------
|
|
103
|
+
- `SwidgetDevice` – base class (shared HTTP/websocket helpers)
|
|
104
|
+
- `SwidgetDimmer` – brightness control (`brightness`, `set_brightness`)
|
|
105
|
+
- `SwidgetOutlet` – outlet control (`turn_on`, `turn_off`, power readings)
|
|
106
|
+
- `SwidgetSwitch` – generic switch control (`turn_on`, `turn_off`)
|
|
107
|
+
- `SwidgetTimerSwitch` – timer-capable switch (`set_countdown_timer`)
|
|
108
|
+
|
|
109
|
+
Common properties:
|
|
110
|
+
- `device_type`, `insert_type`, `friendly_name`, `hw_info`
|
|
111
|
+
- `realtime_values`: convenience dict of sensor/power values
|
|
112
|
+
|
|
113
|
+
Common actions:
|
|
114
|
+
- `turn_on()`, `turn_off()`, `blink()`, `ping()`
|
|
115
|
+
- `update()` to refresh summary/state/config
|
|
116
|
+
- `send_command(assembly, component, function, command_dict)` for raw control
|
|
117
|
+
|
|
118
|
+
HTTP vs websocket modes
|
|
119
|
+
-----------------------
|
|
120
|
+
- HTTP-only: set `use_websockets=False`. All operations go over REST (`/api/v1/...`).
|
|
121
|
+
- Websocket mode: set `use_websockets=True`. Summary/state updates and commands use the socket when connected; HTTP is used as fallback where applicable.
|
|
122
|
+
- TLS: `use_https=True` disables certificate verification by default; pass `verify_ssl=True` when constructing `SwidgetDevice` if you have valid certs.
|
|
123
|
+
|
|
124
|
+
Discovery
|
|
125
|
+
---------
|
|
126
|
+
Discover devices via SSDP:
|
|
127
|
+
```python
|
|
128
|
+
from swidget import discover_devices
|
|
129
|
+
devices = await discover_devices(timeout=5)
|
|
130
|
+
for mac, dev in devices.items():
|
|
131
|
+
print(dev.host, dev.friendly_name, dev.host_type, dev.insert_type)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Discover a single device when you know the IP:
|
|
135
|
+
```python
|
|
136
|
+
from swidget import discover_single
|
|
137
|
+
dev = await discover_single(
|
|
138
|
+
host="192.168.1.50",
|
|
139
|
+
token_name="x-secret-key",
|
|
140
|
+
password="device_password",
|
|
141
|
+
use_https=True,
|
|
142
|
+
use_websockets=False,
|
|
143
|
+
)
|
|
144
|
+
await dev.update()
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Provisioning (AP mode)
|
|
148
|
+
----------------------
|
|
149
|
+
Provision a device while connected to its AP (`Swidget-...` SSID):
|
|
150
|
+
```python
|
|
151
|
+
from swidget import provision_wifi
|
|
152
|
+
provision_wifi(
|
|
153
|
+
device_name="My Swidget",
|
|
154
|
+
ssid="HomeWiFi",
|
|
155
|
+
network_password="wifi-pass",
|
|
156
|
+
secret_key="factory-secret",
|
|
157
|
+
)
|
|
158
|
+
```
|
|
159
|
+
Notes:
|
|
160
|
+
- Provisioning disables TLS verification; run only on the device’s AP network.
|
|
161
|
+
- The helper waits for connection success and triggers setup completion.
|
|
162
|
+
|
|
163
|
+
CLI usage
|
|
164
|
+
---------
|
|
165
|
+
Install the console entrypoint via Poetry/pip and run:
|
|
166
|
+
```
|
|
167
|
+
swidget --host 192.168.1.50 --password device_password --type dimmer state
|
|
168
|
+
swidget --host 192.168.1.50 --password device_password on
|
|
169
|
+
swidget --host 192.168.1.50 --password device_password brightness 80
|
|
170
|
+
swidget discover
|
|
171
|
+
```
|
|
172
|
+
Key options:
|
|
173
|
+
- `--http_only` (flag) to force HTTP-only (no websockets)
|
|
174
|
+
- `--type` to skip discovery when you know the device type
|
|
175
|
+
- `wifi join` to provision via AP (prompts for SSID/password)
|
|
176
|
+
|
|
177
|
+
Development
|
|
178
|
+
-----------
|
|
179
|
+
Run tests and lint (hook runs both):
|
|
180
|
+
```
|
|
181
|
+
pytest
|
|
182
|
+
black --check .
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
If you install via plain pip and want the dev toolchain (tests, typing, docs, formatting):
|
|
186
|
+
```
|
|
187
|
+
pip install pytest pytest-asyncio aioresponses mypy black sphinx sphinx-rtd-theme sphinx-autobuild
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Enable git hook (optional):
|
|
191
|
+
```
|
|
192
|
+
git config core.hooksPath githooks
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Code structure:
|
|
196
|
+
- `swidget/` core library: device models, discovery, provisioning, websocket client, CLI
|
|
197
|
+
- `tests/` unit tests (network mocked)
|
|
198
|
+
- `devtools/synthetic_test.py` optional integration script for real devices (kept separate from unit tests)
|
|
199
|
+
- `examples/` runnable samples (dimmer HTTP, outlet power, timer switch, snapshot)
|
|
200
|
+
|
|
201
|
+
Additional examples
|
|
202
|
+
-------------------
|
|
203
|
+
|
|
204
|
+
Outlet (power reading + toggle)
|
|
205
|
+
```
|
|
206
|
+
from swidget import SwidgetOutlet
|
|
207
|
+
|
|
208
|
+
dev = SwidgetOutlet(
|
|
209
|
+
host="192.168.1.60",
|
|
210
|
+
token_name="x-secret-key",
|
|
211
|
+
secret_key="password",
|
|
212
|
+
use_https=True,
|
|
213
|
+
use_websockets=False,
|
|
214
|
+
)
|
|
215
|
+
await dev.update()
|
|
216
|
+
print("Host features:", dev.host_features)
|
|
217
|
+
print("Realtime values:", dev.realtime_values)
|
|
218
|
+
await dev.turn_on()
|
|
219
|
+
await dev.turn_off()
|
|
220
|
+
await dev.close()
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Timer switch
|
|
224
|
+
```
|
|
225
|
+
from swidget import SwidgetTimerSwitch
|
|
226
|
+
|
|
227
|
+
dev = SwidgetTimerSwitch(
|
|
228
|
+
host="192.168.1.61",
|
|
229
|
+
token_name="x-secret-key",
|
|
230
|
+
secret_key="password",
|
|
231
|
+
use_https=True,
|
|
232
|
+
use_websockets=False,
|
|
233
|
+
)
|
|
234
|
+
await dev.set_countdown_timer(20) # minutes
|
|
235
|
+
await dev.close()
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Snapshots (video-capable insert)
|
|
239
|
+
```
|
|
240
|
+
from swidget import SwidgetDevice
|
|
241
|
+
|
|
242
|
+
dev = SwidgetDevice(
|
|
243
|
+
host="192.168.1.62",
|
|
244
|
+
token_name="x-secret-key",
|
|
245
|
+
secret_key="password",
|
|
246
|
+
use_https=True,
|
|
247
|
+
use_websockets=False,
|
|
248
|
+
)
|
|
249
|
+
await dev.update()
|
|
250
|
+
img = await dev.get_snapshot_bytes(width=640, height=360)
|
|
251
|
+
with open("snapshot.jpg", "wb") as f:
|
|
252
|
+
f.write(img)
|
|
253
|
+
await dev.close()
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Type checking
|
|
257
|
+
-------------
|
|
258
|
+
Run mypy in strict mode:
|
|
259
|
+
```
|
|
260
|
+
poetry run mypy --strict swidget
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Docs build
|
|
264
|
+
----------
|
|
265
|
+
Generate HTML docs with Sphinx:
|
|
266
|
+
```
|
|
267
|
+
cd docs
|
|
268
|
+
poetry run sphinx-build -b html . _build
|
|
269
|
+
```
|
|
270
|
+
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
python-swidget
|
|
2
|
+
==============
|
|
3
|
+
|
|
4
|
+

|
|
5
|
+

|
|
6
|
+

|
|
7
|
+
|
|
8
|
+
Python SDK for Swidget smart devices. It supports local HTTP control and websockets for realtime updates, along with SSDP discovery, Wi-Fi provisioning helpers, and a CLI tool (`swidget`).
|
|
9
|
+
|
|
10
|
+
Table of contents
|
|
11
|
+
-----------------
|
|
12
|
+
- Installation
|
|
13
|
+
- Quickstart
|
|
14
|
+
- Device types and helpers
|
|
15
|
+
- HTTP vs websocket modes
|
|
16
|
+
- Discovery
|
|
17
|
+
- Provisioning (AP mode)
|
|
18
|
+
- CLI usage
|
|
19
|
+
- Development (tests, lint, hooks)
|
|
20
|
+
|
|
21
|
+
Installation
|
|
22
|
+
------------
|
|
23
|
+
Using Poetry (preferred):
|
|
24
|
+
```
|
|
25
|
+
poetry install --with dev
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Using pip:
|
|
29
|
+
```
|
|
30
|
+
pip install python-swidget
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Quickstart
|
|
34
|
+
----------
|
|
35
|
+
```python
|
|
36
|
+
from swidget import SwidgetDimmer
|
|
37
|
+
|
|
38
|
+
dev = SwidgetDimmer(
|
|
39
|
+
host="192.168.1.50",
|
|
40
|
+
token_name="x-secret-key",
|
|
41
|
+
secret_key="password",
|
|
42
|
+
use_https=True,
|
|
43
|
+
use_websockets=False, # set True for realtime updates
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# HTTP-only mode
|
|
47
|
+
await dev.update()
|
|
48
|
+
await dev.turn_on()
|
|
49
|
+
await dev.turn_off()
|
|
50
|
+
await dev.close()
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Websocket mode (realtime)
|
|
54
|
+
```python
|
|
55
|
+
dev = SwidgetDimmer(
|
|
56
|
+
host="192.168.1.50",
|
|
57
|
+
token_name="x-secret-key",
|
|
58
|
+
secret_key="password",
|
|
59
|
+
use_https=True,
|
|
60
|
+
use_websockets=True,
|
|
61
|
+
)
|
|
62
|
+
await dev.start() # opens websocket and updates state
|
|
63
|
+
await dev.turn_on()
|
|
64
|
+
await dev.close()
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Device types and helpers
|
|
68
|
+
------------------------
|
|
69
|
+
- `SwidgetDevice` – base class (shared HTTP/websocket helpers)
|
|
70
|
+
- `SwidgetDimmer` – brightness control (`brightness`, `set_brightness`)
|
|
71
|
+
- `SwidgetOutlet` – outlet control (`turn_on`, `turn_off`, power readings)
|
|
72
|
+
- `SwidgetSwitch` – generic switch control (`turn_on`, `turn_off`)
|
|
73
|
+
- `SwidgetTimerSwitch` – timer-capable switch (`set_countdown_timer`)
|
|
74
|
+
|
|
75
|
+
Common properties:
|
|
76
|
+
- `device_type`, `insert_type`, `friendly_name`, `hw_info`
|
|
77
|
+
- `realtime_values`: convenience dict of sensor/power values
|
|
78
|
+
|
|
79
|
+
Common actions:
|
|
80
|
+
- `turn_on()`, `turn_off()`, `blink()`, `ping()`
|
|
81
|
+
- `update()` to refresh summary/state/config
|
|
82
|
+
- `send_command(assembly, component, function, command_dict)` for raw control
|
|
83
|
+
|
|
84
|
+
HTTP vs websocket modes
|
|
85
|
+
-----------------------
|
|
86
|
+
- HTTP-only: set `use_websockets=False`. All operations go over REST (`/api/v1/...`).
|
|
87
|
+
- Websocket mode: set `use_websockets=True`. Summary/state updates and commands use the socket when connected; HTTP is used as fallback where applicable.
|
|
88
|
+
- TLS: `use_https=True` disables certificate verification by default; pass `verify_ssl=True` when constructing `SwidgetDevice` if you have valid certs.
|
|
89
|
+
|
|
90
|
+
Discovery
|
|
91
|
+
---------
|
|
92
|
+
Discover devices via SSDP:
|
|
93
|
+
```python
|
|
94
|
+
from swidget import discover_devices
|
|
95
|
+
devices = await discover_devices(timeout=5)
|
|
96
|
+
for mac, dev in devices.items():
|
|
97
|
+
print(dev.host, dev.friendly_name, dev.host_type, dev.insert_type)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Discover a single device when you know the IP:
|
|
101
|
+
```python
|
|
102
|
+
from swidget import discover_single
|
|
103
|
+
dev = await discover_single(
|
|
104
|
+
host="192.168.1.50",
|
|
105
|
+
token_name="x-secret-key",
|
|
106
|
+
password="device_password",
|
|
107
|
+
use_https=True,
|
|
108
|
+
use_websockets=False,
|
|
109
|
+
)
|
|
110
|
+
await dev.update()
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Provisioning (AP mode)
|
|
114
|
+
----------------------
|
|
115
|
+
Provision a device while connected to its AP (`Swidget-...` SSID):
|
|
116
|
+
```python
|
|
117
|
+
from swidget import provision_wifi
|
|
118
|
+
provision_wifi(
|
|
119
|
+
device_name="My Swidget",
|
|
120
|
+
ssid="HomeWiFi",
|
|
121
|
+
network_password="wifi-pass",
|
|
122
|
+
secret_key="factory-secret",
|
|
123
|
+
)
|
|
124
|
+
```
|
|
125
|
+
Notes:
|
|
126
|
+
- Provisioning disables TLS verification; run only on the device’s AP network.
|
|
127
|
+
- The helper waits for connection success and triggers setup completion.
|
|
128
|
+
|
|
129
|
+
CLI usage
|
|
130
|
+
---------
|
|
131
|
+
Install the console entrypoint via Poetry/pip and run:
|
|
132
|
+
```
|
|
133
|
+
swidget --host 192.168.1.50 --password device_password --type dimmer state
|
|
134
|
+
swidget --host 192.168.1.50 --password device_password on
|
|
135
|
+
swidget --host 192.168.1.50 --password device_password brightness 80
|
|
136
|
+
swidget discover
|
|
137
|
+
```
|
|
138
|
+
Key options:
|
|
139
|
+
- `--http_only` (flag) to force HTTP-only (no websockets)
|
|
140
|
+
- `--type` to skip discovery when you know the device type
|
|
141
|
+
- `wifi join` to provision via AP (prompts for SSID/password)
|
|
142
|
+
|
|
143
|
+
Development
|
|
144
|
+
-----------
|
|
145
|
+
Run tests and lint (hook runs both):
|
|
146
|
+
```
|
|
147
|
+
pytest
|
|
148
|
+
black --check .
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
If you install via plain pip and want the dev toolchain (tests, typing, docs, formatting):
|
|
152
|
+
```
|
|
153
|
+
pip install pytest pytest-asyncio aioresponses mypy black sphinx sphinx-rtd-theme sphinx-autobuild
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Enable git hook (optional):
|
|
157
|
+
```
|
|
158
|
+
git config core.hooksPath githooks
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Code structure:
|
|
162
|
+
- `swidget/` core library: device models, discovery, provisioning, websocket client, CLI
|
|
163
|
+
- `tests/` unit tests (network mocked)
|
|
164
|
+
- `devtools/synthetic_test.py` optional integration script for real devices (kept separate from unit tests)
|
|
165
|
+
- `examples/` runnable samples (dimmer HTTP, outlet power, timer switch, snapshot)
|
|
166
|
+
|
|
167
|
+
Additional examples
|
|
168
|
+
-------------------
|
|
169
|
+
|
|
170
|
+
Outlet (power reading + toggle)
|
|
171
|
+
```
|
|
172
|
+
from swidget import SwidgetOutlet
|
|
173
|
+
|
|
174
|
+
dev = SwidgetOutlet(
|
|
175
|
+
host="192.168.1.60",
|
|
176
|
+
token_name="x-secret-key",
|
|
177
|
+
secret_key="password",
|
|
178
|
+
use_https=True,
|
|
179
|
+
use_websockets=False,
|
|
180
|
+
)
|
|
181
|
+
await dev.update()
|
|
182
|
+
print("Host features:", dev.host_features)
|
|
183
|
+
print("Realtime values:", dev.realtime_values)
|
|
184
|
+
await dev.turn_on()
|
|
185
|
+
await dev.turn_off()
|
|
186
|
+
await dev.close()
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Timer switch
|
|
190
|
+
```
|
|
191
|
+
from swidget import SwidgetTimerSwitch
|
|
192
|
+
|
|
193
|
+
dev = SwidgetTimerSwitch(
|
|
194
|
+
host="192.168.1.61",
|
|
195
|
+
token_name="x-secret-key",
|
|
196
|
+
secret_key="password",
|
|
197
|
+
use_https=True,
|
|
198
|
+
use_websockets=False,
|
|
199
|
+
)
|
|
200
|
+
await dev.set_countdown_timer(20) # minutes
|
|
201
|
+
await dev.close()
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Snapshots (video-capable insert)
|
|
205
|
+
```
|
|
206
|
+
from swidget import SwidgetDevice
|
|
207
|
+
|
|
208
|
+
dev = SwidgetDevice(
|
|
209
|
+
host="192.168.1.62",
|
|
210
|
+
token_name="x-secret-key",
|
|
211
|
+
secret_key="password",
|
|
212
|
+
use_https=True,
|
|
213
|
+
use_websockets=False,
|
|
214
|
+
)
|
|
215
|
+
await dev.update()
|
|
216
|
+
img = await dev.get_snapshot_bytes(width=640, height=360)
|
|
217
|
+
with open("snapshot.jpg", "wb") as f:
|
|
218
|
+
f.write(img)
|
|
219
|
+
await dev.close()
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Type checking
|
|
223
|
+
-------------
|
|
224
|
+
Run mypy in strict mode:
|
|
225
|
+
```
|
|
226
|
+
poetry run mypy --strict swidget
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Docs build
|
|
230
|
+
----------
|
|
231
|
+
Generate HTML docs with Sphinx:
|
|
232
|
+
```
|
|
233
|
+
cd docs
|
|
234
|
+
poetry run sphinx-build -b html . _build
|
|
235
|
+
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "python-swidget"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.4.0"
|
|
4
4
|
description = "Python API for Swidget smart devices"
|
|
5
5
|
license = "GPL-3.0-or-later"
|
|
6
6
|
authors = ["Swidget"]
|
|
@@ -22,9 +22,9 @@ importlib-metadata = "*"
|
|
|
22
22
|
asyncclick = ">=8"
|
|
23
23
|
pydantic = "^2"
|
|
24
24
|
ssdp = "1.1.1"
|
|
25
|
-
requests = "2.32.
|
|
26
|
-
types-requests = "^2.
|
|
27
|
-
urllib3 = '
|
|
25
|
+
requests = "2.32.5"
|
|
26
|
+
types-requests = "^2.32.0"
|
|
27
|
+
urllib3 = '2.5.0'
|
|
28
28
|
|
|
29
29
|
# required only for docs
|
|
30
30
|
sphinx = { version = "^4", optional = true }
|
|
@@ -38,6 +38,7 @@ pytest = ">=6.2.5"
|
|
|
38
38
|
pytest-cov = "^2"
|
|
39
39
|
pytest-asyncio = "^0"
|
|
40
40
|
pytest-sugar = "*"
|
|
41
|
+
aioresponses = "^0.7.6"
|
|
41
42
|
pre-commit = "*"
|
|
42
43
|
voluptuous = "*"
|
|
43
44
|
toml = "*"
|
|
@@ -46,6 +47,11 @@ pytest-mock = "^3"
|
|
|
46
47
|
codecov = "^2"
|
|
47
48
|
xdoctest = "^0"
|
|
48
49
|
coverage = {version = "^6", extras = ["toml"]}
|
|
50
|
+
sphinx = "^4"
|
|
51
|
+
sphinx-autobuild = "^2021.3.14"
|
|
52
|
+
sphinx-rtd-theme = "^1.2"
|
|
53
|
+
mypy = "^1.11"
|
|
54
|
+
black = "^24.4"
|
|
49
55
|
|
|
50
56
|
[tool.poetry.extras]
|
|
51
57
|
docs = ["sphinx", "sphinx_rtd_theme", "m2r", "mistune", "sphinxcontrib-programoutput"]
|
|
@@ -12,7 +12,7 @@ Module-specific errors are raised as `SwidgetException` and are expected
|
|
|
12
12
|
to be handled by the user of the library.
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
|
-
from importlib_metadata import version
|
|
15
|
+
from importlib_metadata import version
|
|
16
16
|
|
|
17
17
|
from swidget.discovery import SwidgetDiscoveredDevice, discover_devices, discover_single
|
|
18
18
|
from swidget.exceptions import SwidgetException
|
|
@@ -236,10 +236,10 @@ async def ping(dev):
|
|
|
236
236
|
click.echo("Pinging the device")
|
|
237
237
|
try:
|
|
238
238
|
result = await dev.ping()
|
|
239
|
-
if result
|
|
239
|
+
if result:
|
|
240
240
|
click.echo("Successfully pinged device")
|
|
241
241
|
else:
|
|
242
|
-
click.echo(
|
|
242
|
+
click.echo("Unable to ping device")
|
|
243
243
|
except Exception:
|
|
244
244
|
click.echo("Unable to ping device")
|
|
245
245
|
|
|
@@ -5,7 +5,7 @@ import socket
|
|
|
5
5
|
from typing import Any, Type
|
|
6
6
|
from urllib.parse import urlparse
|
|
7
7
|
|
|
8
|
-
import ssdp
|
|
8
|
+
import ssdp
|
|
9
9
|
|
|
10
10
|
from swidget.swidgetdevice import DeviceType, SwidgetDevice
|
|
11
11
|
|
|
@@ -31,12 +31,14 @@ class SwidgetDiscoveredDevice:
|
|
|
31
31
|
host_type: str,
|
|
32
32
|
insert_type: str,
|
|
33
33
|
friendly_name: str = "Swidget Discovered Device",
|
|
34
|
+
host_id: str = "",
|
|
34
35
|
):
|
|
35
36
|
self.mac = mac
|
|
36
37
|
self.host = host
|
|
37
38
|
self.friendly_name = friendly_name
|
|
38
39
|
self.host_type = host_type
|
|
39
40
|
self.insert_type = insert_type
|
|
41
|
+
self.host_id = host_id
|
|
40
42
|
|
|
41
43
|
|
|
42
44
|
class SwidgetProtocol(ssdp.SimpleServiceDiscoveryProtocol):
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Exception types for the python-swidget library."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class SwidgetException(Exception):
|
|
5
|
+
"""Base exception for Swidget-related errors."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SwidgetAuthenticationException(SwidgetException):
|
|
9
|
+
"""Raised when device authentication fails (HTTP 403)."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SwidgetConnectionException(SwidgetException):
|
|
13
|
+
"""Raised when a connection to the device cannot be established."""
|
|
@@ -9,8 +9,13 @@ from types import TracebackType
|
|
|
9
9
|
from typing import Any, Dict, List, Optional
|
|
10
10
|
|
|
11
11
|
from aiohttp import ClientSession, TCPConnector
|
|
12
|
+
from aiohttp.client_exceptions import ClientConnectorError
|
|
12
13
|
|
|
13
|
-
from .exceptions import
|
|
14
|
+
from .exceptions import (
|
|
15
|
+
SwidgetAuthenticationException,
|
|
16
|
+
SwidgetConnectionException,
|
|
17
|
+
SwidgetException,
|
|
18
|
+
)
|
|
14
19
|
from .websocket import SwidgetWebsocket
|
|
15
20
|
|
|
16
21
|
_LOGGER = logging.getLogger(__name__)
|
|
@@ -37,6 +42,8 @@ class InsertType(Enum):
|
|
|
37
42
|
GL = "GUIDE LIGHT"
|
|
38
43
|
PO = "POWER OUT"
|
|
39
44
|
VIDEO = "video" # This is not a mistake.
|
|
45
|
+
USBC = "USBC"
|
|
46
|
+
WD = "WATER DETECTOR"
|
|
40
47
|
Unknown = -1
|
|
41
48
|
|
|
42
49
|
|
|
@@ -61,10 +68,16 @@ class SelfDiagnosticErrorCodes(Enum):
|
|
|
61
68
|
|
|
62
69
|
|
|
63
70
|
class SwidgetDevice:
|
|
64
|
-
"""
|
|
71
|
+
"""Core representation of a Swidget device (base class for all device types)."""
|
|
65
72
|
|
|
66
73
|
def __init__(
|
|
67
|
-
self,
|
|
74
|
+
self,
|
|
75
|
+
host,
|
|
76
|
+
token_name,
|
|
77
|
+
secret_key,
|
|
78
|
+
use_https=True,
|
|
79
|
+
use_websockets=True,
|
|
80
|
+
verify_ssl: bool = False,
|
|
68
81
|
) -> None:
|
|
69
82
|
self.token_name = token_name
|
|
70
83
|
self.ip_address = host
|
|
@@ -72,13 +85,16 @@ class SwidgetDevice:
|
|
|
72
85
|
self.uri_scheme = "https" if self.use_https is True else "http"
|
|
73
86
|
self.secret_key = secret_key
|
|
74
87
|
self.use_websockets = use_websockets
|
|
88
|
+
self.verify_ssl = verify_ssl
|
|
75
89
|
self.device_type = DeviceType.Unknown
|
|
76
90
|
self._friendly_name = "Unknown Swidget Device"
|
|
77
91
|
self.assemblies: Dict[Any, Any] = dict()
|
|
78
92
|
self.device_config: DeviceConfiguration = DeviceConfiguration({})
|
|
79
93
|
self._subscribers: List[Any] = list()
|
|
80
94
|
headers = {self.token_name: self.secret_key, "Connection": "keep-alive"}
|
|
81
|
-
|
|
95
|
+
# aiohttp recommends using ssl context; verify_ssl is deprecated.
|
|
96
|
+
ssl_flag = verify_ssl if use_https else False
|
|
97
|
+
connector = TCPConnector(ssl=ssl_flag, force_close=True)
|
|
82
98
|
if use_https is True:
|
|
83
99
|
self._session = ClientSession(headers=headers, connector=connector)
|
|
84
100
|
else:
|
|
@@ -91,12 +107,14 @@ class SwidgetDevice:
|
|
|
91
107
|
secret_key=self.secret_key,
|
|
92
108
|
callback=self.message_callback,
|
|
93
109
|
session=self._session,
|
|
110
|
+
use_security=self.use_https,
|
|
111
|
+
verify_ssl=verify_ssl,
|
|
94
112
|
)
|
|
95
113
|
|
|
96
114
|
@property
|
|
97
115
|
def connected(self) -> bool:
|
|
98
116
|
"""Property to represent if the client is connected to the device."""
|
|
99
|
-
return self._websocket.connected
|
|
117
|
+
return hasattr(self, "_websocket") and self._websocket.connected
|
|
100
118
|
|
|
101
119
|
def get_websocket(self) -> Optional[SwidgetWebsocket]:
|
|
102
120
|
"""Return the SwidgetWebsocket class instance if possible."""
|
|
@@ -187,15 +205,112 @@ class SwidgetDevice:
|
|
|
187
205
|
for callback in self._subscribers:
|
|
188
206
|
await callback(message)
|
|
189
207
|
|
|
208
|
+
async def make_http_request(
|
|
209
|
+
self,
|
|
210
|
+
method: str,
|
|
211
|
+
endpoint: str,
|
|
212
|
+
params: Optional[Dict[str, Any]] = None,
|
|
213
|
+
json_payload: Optional[Dict[str, Any]] = None,
|
|
214
|
+
) -> Dict[str, Any]:
|
|
215
|
+
"""
|
|
216
|
+
Make a generic HTTP request to a specified device endpoint.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
method: The HTTP method to use (e.g., "GET", "POST").
|
|
220
|
+
endpoint: The API endpoint to request (e.g., "summary", "state").
|
|
221
|
+
params: Optional dictionary of URL query parameters.
|
|
222
|
+
json_payload: Optional dictionary to send as a JSON request body.
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
The JSON response from the device as a dictionary.
|
|
226
|
+
|
|
227
|
+
Raises:
|
|
228
|
+
SwidgetConnectionException: If there is a problem connecting.
|
|
229
|
+
SwidgetAuthenticationException: If the device returns a 403 error.
|
|
230
|
+
ValueError: If an unsupported HTTP method is provided.
|
|
231
|
+
"""
|
|
232
|
+
http_method = method.upper()
|
|
233
|
+
if http_method not in ("GET", "POST"):
|
|
234
|
+
raise ValueError(f"Unsupported HTTP method: {http_method}")
|
|
235
|
+
|
|
236
|
+
url = f"{self.uri_scheme}://{self.ip_address}/api/v1/{endpoint}"
|
|
237
|
+
_LOGGER.debug(f"Sending {http_method} request to: {url}")
|
|
238
|
+
|
|
239
|
+
try:
|
|
240
|
+
async with self._session.request(
|
|
241
|
+
method=http_method,
|
|
242
|
+
url=url,
|
|
243
|
+
params=params,
|
|
244
|
+
json=json_payload,
|
|
245
|
+
ssl=self.verify_ssl if self.use_https else False,
|
|
246
|
+
) as response:
|
|
247
|
+
if response.status == 200:
|
|
248
|
+
if response.content_length == 0:
|
|
249
|
+
return {}
|
|
250
|
+
return await response.json()
|
|
251
|
+
elif response.status == 403:
|
|
252
|
+
_LOGGER.error(
|
|
253
|
+
f"Authentication failed for {http_method} '{endpoint}'"
|
|
254
|
+
)
|
|
255
|
+
raise SwidgetAuthenticationException
|
|
256
|
+
else:
|
|
257
|
+
response.raise_for_status()
|
|
258
|
+
return {}
|
|
259
|
+
except ClientConnectorError as e:
|
|
260
|
+
_LOGGER.error(f"Connection error while requesting '{endpoint}': {e}")
|
|
261
|
+
raise SwidgetConnectionException from e
|
|
262
|
+
|
|
263
|
+
async def _make_passthrough_request(
|
|
264
|
+
self,
|
|
265
|
+
method: str,
|
|
266
|
+
path: str,
|
|
267
|
+
params: Optional[Dict[str, Any]] = None,
|
|
268
|
+
) -> Any:
|
|
269
|
+
"""Send a request to endpoints that are not part of the /api/v1/ namespace."""
|
|
270
|
+
http_method = method.upper()
|
|
271
|
+
url = f"{self.uri_scheme}://{self.ip_address}/{path.lstrip('/')}"
|
|
272
|
+
request_params = dict(params or {})
|
|
273
|
+
if (
|
|
274
|
+
self.secret_key
|
|
275
|
+
and self.token_name
|
|
276
|
+
and self.token_name not in request_params
|
|
277
|
+
):
|
|
278
|
+
request_params[self.token_name] = self.secret_key
|
|
279
|
+
|
|
280
|
+
_LOGGER.debug(
|
|
281
|
+
f"Sending {http_method} request to: {url} with params {request_params}"
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
try:
|
|
285
|
+
async with self._session.request(
|
|
286
|
+
method=http_method,
|
|
287
|
+
url=url,
|
|
288
|
+
params=request_params,
|
|
289
|
+
ssl=self.verify_ssl if self.use_https else False,
|
|
290
|
+
) as response:
|
|
291
|
+
if response.status == 200:
|
|
292
|
+
if response.content_length == 0:
|
|
293
|
+
return {}
|
|
294
|
+
content_type = response.headers.get("Content-Type", "").lower()
|
|
295
|
+
if "application/json" in content_type:
|
|
296
|
+
return await response.json()
|
|
297
|
+
# Fallback: return text (e.g., ping returns plain "PONG")
|
|
298
|
+
text_body = await response.text()
|
|
299
|
+
try:
|
|
300
|
+
return json.loads(text_body)
|
|
301
|
+
except Exception:
|
|
302
|
+
return text_body
|
|
303
|
+
response.raise_for_status()
|
|
304
|
+
return {}
|
|
305
|
+
except ClientConnectorError as e:
|
|
306
|
+
_LOGGER.error(f"Connection error while requesting '{path}': {e}")
|
|
307
|
+
raise SwidgetConnectionException from e
|
|
308
|
+
|
|
190
309
|
async def get_device_config(self) -> Any:
|
|
191
310
|
"""Get the config of the device."""
|
|
192
311
|
_LOGGER.debug("SwidgetDevice.get_device_config() called")
|
|
193
312
|
_LOGGER.debug("Sending get_summary() command over http")
|
|
194
|
-
|
|
195
|
-
url=f"{self.uri_scheme}://{self.ip_address}/api/v1/device_config",
|
|
196
|
-
ssl=False,
|
|
197
|
-
) as response:
|
|
198
|
-
config = await response.json()
|
|
313
|
+
config = await self.make_http_request("GET", "device_config")
|
|
199
314
|
self.device_config = DeviceConfiguration(config)
|
|
200
315
|
self._last_update = int(time.time())
|
|
201
316
|
|
|
@@ -211,10 +326,7 @@ class SwidgetDevice:
|
|
|
211
326
|
)
|
|
212
327
|
else:
|
|
213
328
|
_LOGGER.debug("In http mode. Sending get_summary() command over http")
|
|
214
|
-
|
|
215
|
-
url=f"{self.uri_scheme}://{self.ip_address}/api/v1/summary", ssl=False
|
|
216
|
-
) as response:
|
|
217
|
-
summary = await response.json()
|
|
329
|
+
summary = await self.make_http_request("GET", "summary")
|
|
218
330
|
await self.process_summary(summary)
|
|
219
331
|
|
|
220
332
|
async def process_summary(self, summary) -> None:
|
|
@@ -237,10 +349,7 @@ class SwidgetDevice:
|
|
|
237
349
|
"""Retrieve the friendly name of the device."""
|
|
238
350
|
_LOGGER.debug("SwidgetDevice.get_friendly_name() called")
|
|
239
351
|
try:
|
|
240
|
-
|
|
241
|
-
url=f"{self.uri_scheme}://{self.ip_address}/api/v1/name", ssl=False
|
|
242
|
-
) as response:
|
|
243
|
-
name = await response.json()
|
|
352
|
+
name = await self.make_http_request("GET", "name")
|
|
244
353
|
except Exception:
|
|
245
354
|
name = {"name": f"Swidget {self.device_type} w/{self.insert_type} insert"}
|
|
246
355
|
await self.process_friendly_name(name["name"])
|
|
@@ -256,17 +365,14 @@ class SwidgetDevice:
|
|
|
256
365
|
_LOGGER.debug("SwidgetDevice.get_state() called")
|
|
257
366
|
if self.use_websockets:
|
|
258
367
|
_LOGGER.debug(
|
|
259
|
-
"In websocket mode. Sending
|
|
368
|
+
"In websocket mode. Sending get_state() command over websocket"
|
|
260
369
|
)
|
|
261
370
|
await self._websocket.send_str(
|
|
262
371
|
json.dumps({"type": "state", "request_id": "state"})
|
|
263
372
|
)
|
|
264
373
|
else:
|
|
265
|
-
_LOGGER.debug("In http mode. Sending
|
|
266
|
-
|
|
267
|
-
url=f"{self.uri_scheme}://{self.ip_address}/api/v1/state", ssl=False
|
|
268
|
-
) as response:
|
|
269
|
-
state = await response.json()
|
|
374
|
+
_LOGGER.debug("In http mode. Sending get_state() command over http")
|
|
375
|
+
state = await self.make_http_request("GET", "state")
|
|
270
376
|
await self.process_state(state)
|
|
271
377
|
|
|
272
378
|
async def process_state(self, state) -> None:
|
|
@@ -277,7 +383,7 @@ class SwidgetDevice:
|
|
|
277
383
|
try:
|
|
278
384
|
self.rssi = state["connection"]["rssi"]
|
|
279
385
|
except Exception:
|
|
280
|
-
|
|
386
|
+
self.rssi = 0
|
|
281
387
|
for assembly in self.assemblies:
|
|
282
388
|
for id, component in self.assemblies[assembly].components.items():
|
|
283
389
|
try:
|
|
@@ -295,7 +401,7 @@ class SwidgetDevice:
|
|
|
295
401
|
await self.get_state()
|
|
296
402
|
if self._friendly_name == "Unknown Swidget Device":
|
|
297
403
|
await self.get_friendly_name()
|
|
298
|
-
if self.device_config
|
|
404
|
+
if not self.device_config.config_populated():
|
|
299
405
|
await self.get_device_config()
|
|
300
406
|
elif (int(time.time()) - self._last_update) < 5:
|
|
301
407
|
_LOGGER.debug("update() recently called, not executing")
|
|
@@ -307,10 +413,18 @@ class SwidgetDevice:
|
|
|
307
413
|
async def send_config(self, payload: dict) -> None:
|
|
308
414
|
"""Send a config block to the device."""
|
|
309
415
|
_LOGGER.debug("SwidgetDevice.send_config() called")
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
416
|
+
if self.use_websockets:
|
|
417
|
+
_LOGGER.debug(
|
|
418
|
+
"In websocket mode. Sending send_config() command over websocket"
|
|
419
|
+
)
|
|
420
|
+
data = json.dumps(
|
|
421
|
+
{"type": "config", "request_id": "send_config", "payload": payload}
|
|
422
|
+
)
|
|
423
|
+
await self._websocket.send_str(data)
|
|
424
|
+
else:
|
|
425
|
+
raise SwidgetException(
|
|
426
|
+
"Configuration management is not available via websocket."
|
|
427
|
+
)
|
|
314
428
|
|
|
315
429
|
async def send_command(
|
|
316
430
|
self, assembly: str, component: str, function: str, command: dict
|
|
@@ -327,16 +441,19 @@ class SwidgetDevice:
|
|
|
327
441
|
await self._websocket.send_str(command_data)
|
|
328
442
|
else:
|
|
329
443
|
_LOGGER.debug("NOT in websocket mode, sending command over HTTP")
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
444
|
+
response = await self.make_http_request(
|
|
445
|
+
"POST", "command", json_payload=data
|
|
446
|
+
)
|
|
447
|
+
if response:
|
|
448
|
+
try:
|
|
449
|
+
function_value = response[assembly]["components"][component][
|
|
450
|
+
function
|
|
451
|
+
]
|
|
452
|
+
self.assemblies[assembly].components[component].functions[
|
|
453
|
+
function
|
|
454
|
+
] = function_value
|
|
455
|
+
except Exception:
|
|
456
|
+
_LOGGER.debug("Command response did not include state update")
|
|
340
457
|
|
|
341
458
|
async def ping(self) -> bool:
|
|
342
459
|
"""Ping the device to ensure it's devices.
|
|
@@ -345,12 +462,9 @@ class SwidgetDevice:
|
|
|
345
462
|
"""
|
|
346
463
|
_LOGGER.debug("SwidgetDevice.ping() called")
|
|
347
464
|
try:
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
if response.status == 200:
|
|
352
|
-
return True
|
|
353
|
-
return False
|
|
465
|
+
response = await self._make_passthrough_request("GET", "ping")
|
|
466
|
+
_LOGGER.debug(f"Ping response: {response}")
|
|
467
|
+
return True if response == {} else bool(response)
|
|
354
468
|
except Exception:
|
|
355
469
|
return False
|
|
356
470
|
|
|
@@ -360,13 +474,7 @@ class SwidgetDevice:
|
|
|
360
474
|
:raises SwidgetException: Raise the exception if there we are unable to connect to the Swidget device
|
|
361
475
|
"""
|
|
362
476
|
_LOGGER.debug("SwidgetDevice.blink() called")
|
|
363
|
-
|
|
364
|
-
async with self._session.get(
|
|
365
|
-
url=f"{self.uri_scheme}://{self.ip_address}/blink", ssl=False
|
|
366
|
-
) as response:
|
|
367
|
-
return await response.json()
|
|
368
|
-
except Exception:
|
|
369
|
-
raise SwidgetException
|
|
477
|
+
return await self._make_passthrough_request("GET", "blink")
|
|
370
478
|
|
|
371
479
|
async def enable_debug_server(self) -> Any:
|
|
372
480
|
"""Enable the Swidget local debug server.
|
|
@@ -374,30 +482,14 @@ class SwidgetDevice:
|
|
|
374
482
|
:raises SwidgetException: Raise the exception if there we are unable to connect to the Swidget device
|
|
375
483
|
"""
|
|
376
484
|
_LOGGER.debug("SwidgetDevice.enable_debug_server() called")
|
|
377
|
-
|
|
378
|
-
async with self._session.get(
|
|
379
|
-
url=f"{self.uri_scheme}://{self.ip_address}/debug?x-secret-key={self.secret_key}",
|
|
380
|
-
ssl=False,
|
|
381
|
-
) as response:
|
|
382
|
-
if response.status == 200:
|
|
383
|
-
return True
|
|
384
|
-
return False
|
|
385
|
-
except Exception:
|
|
386
|
-
raise SwidgetException
|
|
485
|
+
return await self.make_http_request("GET", "debug")
|
|
387
486
|
|
|
388
487
|
async def restart_device(self) -> Any:
|
|
389
488
|
"""Restart the Swidget device.
|
|
390
489
|
|
|
391
490
|
:raises SwidgetException: Raise the exception if there we are unable to connect to the Swidget device
|
|
392
491
|
"""
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
async with self._session.post(
|
|
396
|
-
url=f"{self.uri_scheme}://{self.ip_address}/api/v1/reset", ssl=False
|
|
397
|
-
) as response:
|
|
398
|
-
return await response.json()
|
|
399
|
-
except Exception:
|
|
400
|
-
raise SwidgetException
|
|
492
|
+
return await self.make_http_request("POST", "reset")
|
|
401
493
|
|
|
402
494
|
async def factory_reset(self) -> Any:
|
|
403
495
|
"""Factory reset the Swidget device.
|
|
@@ -419,12 +511,8 @@ class SwidgetDevice:
|
|
|
419
511
|
:raises SwidgetException: Raise the exception if there we are unable to connect to the Swidget device
|
|
420
512
|
"""
|
|
421
513
|
try:
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
url=f"{self.uri_scheme}://{self.ip_address}/api/v1/update", ssl=False
|
|
425
|
-
) as response:
|
|
426
|
-
newer_versions = await response.json()
|
|
427
|
-
return sorted(newer_versions["updates"])
|
|
514
|
+
newer_versions = await self.make_http_request("GET", "update")
|
|
515
|
+
return sorted(newer_versions["updates"])
|
|
428
516
|
except Exception:
|
|
429
517
|
raise SwidgetException
|
|
430
518
|
|
|
@@ -435,15 +523,10 @@ class SwidgetDevice:
|
|
|
435
523
|
"""
|
|
436
524
|
try:
|
|
437
525
|
data = {"version": version}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
) as response:
|
|
443
|
-
result = response.status
|
|
444
|
-
if result == 200:
|
|
445
|
-
return True
|
|
446
|
-
return False
|
|
526
|
+
response = await self.make_http_request(
|
|
527
|
+
"POST", "update/version", json_payload=data
|
|
528
|
+
)
|
|
529
|
+
return bool(response)
|
|
447
530
|
except Exception:
|
|
448
531
|
raise SwidgetException
|
|
449
532
|
|
|
@@ -544,6 +627,10 @@ class SwidgetDevice:
|
|
|
544
627
|
pass
|
|
545
628
|
elif function == "sd":
|
|
546
629
|
return_values[function] = data["state"]
|
|
630
|
+
elif function == "water":
|
|
631
|
+
return_values[function] = data["state"]
|
|
632
|
+
elif function == "buzzer":
|
|
633
|
+
return_values[function] = data["mode"]
|
|
547
634
|
else:
|
|
548
635
|
return_values[function] = data["now"]
|
|
549
636
|
return return_values
|
|
@@ -752,7 +839,7 @@ class DeviceConfiguration:
|
|
|
752
839
|
|
|
753
840
|
def config_populated(self) -> bool:
|
|
754
841
|
"""Return if configuration has been retrieved from the device."""
|
|
755
|
-
return self._config_dict
|
|
842
|
+
return self._config_dict != {}
|
|
756
843
|
|
|
757
844
|
@property
|
|
758
845
|
def config(self):
|
|
@@ -27,7 +27,7 @@ class SwidgetDimmer(SwidgetDevice):
|
|
|
27
27
|
)
|
|
28
28
|
self.device_type = DeviceType.Dimmer
|
|
29
29
|
|
|
30
|
-
@property
|
|
30
|
+
@property
|
|
31
31
|
def brightness(self) -> int:
|
|
32
32
|
"""Return current brightness on dimmers.
|
|
33
33
|
|
|
@@ -66,7 +66,7 @@ class SwidgetDimmer(SwidgetDevice):
|
|
|
66
66
|
command={"default": brightness},
|
|
67
67
|
)
|
|
68
68
|
|
|
69
|
-
@property
|
|
69
|
+
@property
|
|
70
70
|
def is_dimmable(self) -> bool:
|
|
71
71
|
"""Whether the switch supports brightness changes."""
|
|
72
72
|
_LOGGER.debug("SwidgetDimmer.is_dimmable() called")
|
|
@@ -29,8 +29,8 @@ class SwidgetTimerSwitch(SwidgetSwitch):
|
|
|
29
29
|
self._device_type = DeviceType.TimerSwitch
|
|
30
30
|
|
|
31
31
|
async def set_countdown_timer(self, minutes) -> Any:
|
|
32
|
-
"""Set the countdown timer."""
|
|
33
|
-
_LOGGER.debug("SwidgetTimerSwitch.
|
|
32
|
+
"""Set the countdown timer in minutes."""
|
|
33
|
+
_LOGGER.debug("SwidgetTimerSwitch.set_countdown_timer() called")
|
|
34
34
|
await self.send_command(
|
|
35
35
|
assembly="host",
|
|
36
36
|
component="0",
|
|
@@ -26,6 +26,7 @@ class SwidgetWebsocket:
|
|
|
26
26
|
callback: Union[Callable[[Any], None], Callable[[Any], Awaitable[None]]],
|
|
27
27
|
session: aiohttp.ClientSession | None = None,
|
|
28
28
|
use_security: bool = True,
|
|
29
|
+
verify_ssl: bool = False,
|
|
29
30
|
retry_interval: int = 30, # Initial retry interval in seconds
|
|
30
31
|
max_retries: int | None = None, # Maximum number of reconnection attempts
|
|
31
32
|
):
|
|
@@ -38,6 +39,7 @@ class SwidgetWebsocket:
|
|
|
38
39
|
callback: A callable that will be called with received messages.
|
|
39
40
|
session: An optional aiohttp.ClientSession to use.
|
|
40
41
|
use_security: Whether to use wss:// (True) or ws:// (False).
|
|
42
|
+
verify_ssl: Whether to verify TLS when using wss://.
|
|
41
43
|
retry_interval: Initial interval for reconnection attempts.
|
|
42
44
|
max_retries: Maximum number of reconnection attempts.
|
|
43
45
|
"""
|
|
@@ -46,18 +48,18 @@ class SwidgetWebsocket:
|
|
|
46
48
|
self.secret_key = secret_key or ""
|
|
47
49
|
self.session = session or aiohttp.ClientSession()
|
|
48
50
|
self.use_security = use_security
|
|
51
|
+
self._verify_ssl = verify_ssl
|
|
49
52
|
self.callback = callback
|
|
50
53
|
self.retry_interval = retry_interval
|
|
51
54
|
self.max_retries = max_retries
|
|
52
55
|
self.retry_count = 0
|
|
53
|
-
self._verify_ssl = False
|
|
54
56
|
self.uri = self._get_uri()
|
|
55
57
|
|
|
56
58
|
# self._client= None
|
|
57
59
|
self.is_running = True
|
|
58
60
|
self._closing = False
|
|
59
61
|
self._client: ClientWebSocketResponse | None = None
|
|
60
|
-
self._receiver_task: asyncio.Task | None = None
|
|
62
|
+
self._receiver_task: asyncio.Task[Any] | None = None
|
|
61
63
|
self._closing = False
|
|
62
64
|
|
|
63
65
|
def _get_uri(self) -> str:
|
|
@@ -94,13 +96,13 @@ class SwidgetWebsocket:
|
|
|
94
96
|
self.retry_count = 0
|
|
95
97
|
_LOGGER.debug("Websocket now connected")
|
|
96
98
|
except (ClientConnectionError, WSServerHandshakeError):
|
|
97
|
-
_LOGGER.
|
|
99
|
+
_LOGGER.exception("Error connecting to websocket")
|
|
98
100
|
self._client = None
|
|
99
101
|
except socket.gaierror as e:
|
|
100
102
|
_LOGGER.error(f"Error resolving host: {e}")
|
|
101
103
|
self._client = None
|
|
102
|
-
except Exception
|
|
103
|
-
_LOGGER.
|
|
104
|
+
except Exception:
|
|
105
|
+
_LOGGER.exception("An unexpected error occurred.")
|
|
104
106
|
self._client = None
|
|
105
107
|
|
|
106
108
|
async def send_str(self, message: str) -> None:
|
|
@@ -126,7 +128,7 @@ class SwidgetWebsocket:
|
|
|
126
128
|
f"Failed to send message after {max_send_retries} attempts."
|
|
127
129
|
)
|
|
128
130
|
|
|
129
|
-
async def receive(self):
|
|
131
|
+
async def receive(self) -> Any | None:
|
|
130
132
|
"""Receive a message from the WebSocket server."""
|
|
131
133
|
_LOGGER.debug("websocket.receive() called")
|
|
132
134
|
try:
|
|
@@ -136,16 +138,17 @@ class SwidgetWebsocket:
|
|
|
136
138
|
message_data = message.json()
|
|
137
139
|
_LOGGER.debug(f"[{self.host}] Received message: {message_data}")
|
|
138
140
|
return message_data
|
|
139
|
-
elif message.type
|
|
140
|
-
_LOGGER.error("Websocket
|
|
141
|
+
elif message.type in (WSMsgType.CLOSED, WSMsgType.CLOSING):
|
|
142
|
+
_LOGGER.error("Websocket connection is closed")
|
|
141
143
|
self._client = None
|
|
142
144
|
elif message.type == WSMsgType.ERROR:
|
|
143
145
|
_LOGGER.error("WebSocket error.")
|
|
144
146
|
self._client = None
|
|
145
147
|
except Exception as e:
|
|
146
148
|
_LOGGER.error(f"Error receiving message: {e}")
|
|
149
|
+
return None
|
|
147
150
|
|
|
148
|
-
async def close(self):
|
|
151
|
+
async def close(self) -> None:
|
|
149
152
|
"""Close the WebSocket connection."""
|
|
150
153
|
_LOGGER.debug("websocket.close() called")
|
|
151
154
|
self.is_running = False
|
|
@@ -156,15 +159,17 @@ class SwidgetWebsocket:
|
|
|
156
159
|
if self.session and not self.session.closed:
|
|
157
160
|
await self.session.close()
|
|
158
161
|
|
|
159
|
-
async def reconnect(self):
|
|
160
|
-
"""
|
|
162
|
+
async def reconnect(self) -> None:
|
|
163
|
+
"""Attempt to reconnect to the WebSocket server, raising on failure."""
|
|
161
164
|
_LOGGER.debug("websocket.reconnect() called")
|
|
162
165
|
if self.max_retries is not None and self.retry_count >= self.max_retries:
|
|
163
|
-
_LOGGER.
|
|
164
|
-
|
|
165
|
-
|
|
166
|
+
_LOGGER.error("Max retries reached. Stopping reconnect attempts.")
|
|
167
|
+
raise ConnectionError(
|
|
168
|
+
f"Max retries reached. Could not connect to {self.host}"
|
|
169
|
+
)
|
|
166
170
|
|
|
167
171
|
self.retry_count += 1
|
|
172
|
+
# Implement exponential backoff for reconnection delay
|
|
168
173
|
delay = self.retry_interval * (2 ** (self.retry_count - 1))
|
|
169
174
|
_LOGGER.warning(
|
|
170
175
|
f"Reconnecting to Swidget device: {self.host} in {delay} seconds (attempt {self.retry_count})..."
|
|
@@ -172,7 +177,7 @@ class SwidgetWebsocket:
|
|
|
172
177
|
await asyncio.sleep(delay)
|
|
173
178
|
await self.connect()
|
|
174
179
|
|
|
175
|
-
async def run(self):
|
|
180
|
+
async def run(self) -> None:
|
|
176
181
|
"""Run the WebSocket client to handle messages and reconnections."""
|
|
177
182
|
while self.is_running:
|
|
178
183
|
if self._client is None:
|
|
@@ -185,7 +190,7 @@ class SwidgetWebsocket:
|
|
|
185
190
|
else:
|
|
186
191
|
self.callback(message)
|
|
187
192
|
|
|
188
|
-
def status(self) -> dict:
|
|
193
|
+
def status(self) -> dict[str, Any]:
|
|
189
194
|
"""Return the current status of the websocket connection."""
|
|
190
195
|
_LOGGER.debug("websocket.status() called")
|
|
191
196
|
return {
|
python_swidget-1.3.2/PKG-INFO
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.3
|
|
2
|
-
Name: python-swidget
|
|
3
|
-
Version: 1.3.2
|
|
4
|
-
Summary: Python API for Swidget smart devices
|
|
5
|
-
License: GPL-3.0-or-later
|
|
6
|
-
Author: Swidget
|
|
7
|
-
Requires-Python: >=3.8,<4.0
|
|
8
|
-
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
9
|
-
Classifier: Programming Language :: Python :: 3
|
|
10
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
11
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
-
Provides-Extra: docs
|
|
17
|
-
Requires-Dist: aiohttp (>=3.8.1)
|
|
18
|
-
Requires-Dist: anyio
|
|
19
|
-
Requires-Dist: asyncclick (>=8)
|
|
20
|
-
Requires-Dist: importlib-metadata
|
|
21
|
-
Requires-Dist: m2r (>=0,<1) ; extra == "docs"
|
|
22
|
-
Requires-Dist: mistune (<2.0.0) ; extra == "docs"
|
|
23
|
-
Requires-Dist: pydantic (>=2,<3)
|
|
24
|
-
Requires-Dist: requests (==2.32.3)
|
|
25
|
-
Requires-Dist: sphinx (>=4,<5) ; extra == "docs"
|
|
26
|
-
Requires-Dist: sphinx_rtd_theme (>=0,<1) ; extra == "docs"
|
|
27
|
-
Requires-Dist: sphinxcontrib-programoutput (>=0,<1) ; extra == "docs"
|
|
28
|
-
Requires-Dist: ssdp (==1.1.1)
|
|
29
|
-
Requires-Dist: types-requests (>=2.3.0,<3.0.0)
|
|
30
|
-
Requires-Dist: urllib3 (==1.26.5)
|
|
31
|
-
Project-URL: Repository, https://github.com/swidget/python-swidget
|
|
32
|
-
Description-Content-Type: text/markdown
|
|
33
|
-
|
|
34
|
-
# python-swidget
|
|
35
|
-
A library to manage Swidget smart devices
|
|
36
|
-
|
|
37
|
-
# Basic Usage
|
|
38
|
-
|
|
39
|
-
## Connect to the device using http/ https
|
|
40
|
-
```
|
|
41
|
-
dev = SwidgetDimmer(host=host, token_name='x-secret-key', secret_key='password', use_https=True, use_websockets=False)
|
|
42
|
-
dev.update()
|
|
43
|
-
dev.turn_on()
|
|
44
|
-
dev.close()
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
## Connect to the device using websockets
|
|
48
|
-
```
|
|
49
|
-
dev = SwidgetDimmer(host=host, token_name='x-secret-key', secret_key='password', use_https=True, use_websockets=True)
|
|
50
|
-
dev.start()
|
|
51
|
-
dev.update()
|
|
52
|
-
dev.turn_on()
|
|
53
|
-
dev.close()
|
|
54
|
-
```
|
|
55
|
-
|
python_swidget-1.3.2/README.md
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
# python-swidget
|
|
2
|
-
A library to manage Swidget smart devices
|
|
3
|
-
|
|
4
|
-
# Basic Usage
|
|
5
|
-
|
|
6
|
-
## Connect to the device using http/ https
|
|
7
|
-
```
|
|
8
|
-
dev = SwidgetDimmer(host=host, token_name='x-secret-key', secret_key='password', use_https=True, use_websockets=False)
|
|
9
|
-
dev.update()
|
|
10
|
-
dev.turn_on()
|
|
11
|
-
dev.close()
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
## Connect to the device using websockets
|
|
15
|
-
```
|
|
16
|
-
dev = SwidgetDimmer(host=host, token_name='x-secret-key', secret_key='password', use_https=True, use_websockets=True)
|
|
17
|
-
dev.start()
|
|
18
|
-
dev.update()
|
|
19
|
-
dev.turn_on()
|
|
20
|
-
dev.close()
|
|
21
|
-
```
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|