python-swidget 1.3.3__tar.gz → 1.4.1__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.1/PKG-INFO +269 -0
- python_swidget-1.4.1/README.md +235 -0
- {python_swidget-1.3.3 → python_swidget-1.4.1}/pyproject.toml +9 -3
- {python_swidget-1.3.3 → python_swidget-1.4.1}/swidget/__init__.py +1 -1
- {python_swidget-1.3.3 → python_swidget-1.4.1}/swidget/cli.py +2 -2
- {python_swidget-1.3.3 → python_swidget-1.4.1}/swidget/discovery.py +1 -1
- python_swidget-1.4.1/swidget/exceptions.py +13 -0
- {python_swidget-1.3.3 → python_swidget-1.4.1}/swidget/swidgetdevice.py +167 -86
- {python_swidget-1.3.3 → python_swidget-1.4.1}/swidget/swidgetdimmer.py +2 -2
- {python_swidget-1.3.3 → python_swidget-1.4.1}/swidget/swidgettimerswitch.py +2 -2
- {python_swidget-1.3.3 → python_swidget-1.4.1}/swidget/websocket.py +5 -3
- python_swidget-1.3.3/PKG-INFO +0 -55
- python_swidget-1.3.3/README.md +0 -21
- python_swidget-1.3.3/swidget/exceptions.py +0 -2
- {python_swidget-1.3.3 → python_swidget-1.4.1}/swidget/provision.py +0 -0
- {python_swidget-1.3.3 → python_swidget-1.4.1}/swidget/py.typed +0 -0
- {python_swidget-1.3.3 → python_swidget-1.4.1}/swidget/swidgetoutlet.py +0 -0
- {python_swidget-1.3.3 → python_swidget-1.4.1}/swidget/swidgetswitch.py +0 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: python-swidget
|
|
3
|
+
Version: 1.4.1
|
|
4
|
+
Summary: Python API for Swidget smart devices
|
|
5
|
+
Home-page: https://github.com/swidget/python-swidget
|
|
6
|
+
License: GPL-3.0-or-later
|
|
7
|
+
Author: Swidget
|
|
8
|
+
Requires-Python: >=3.8,<4.0
|
|
9
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
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.0)
|
|
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.32.0,<3.0.0)
|
|
30
|
+
Requires-Dist: urllib3 (>=2.0,<3.0)
|
|
31
|
+
Project-URL: Repository, https://github.com/swidget/python-swidget
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
python-swidget
|
|
35
|
+
==============
|
|
36
|
+
|
|
37
|
+

|
|
38
|
+

|
|
39
|
+

|
|
40
|
+
|
|
41
|
+
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`).
|
|
42
|
+
|
|
43
|
+
Table of contents
|
|
44
|
+
-----------------
|
|
45
|
+
- Installation
|
|
46
|
+
- Quickstart
|
|
47
|
+
- Device types and helpers
|
|
48
|
+
- HTTP vs websocket modes
|
|
49
|
+
- Discovery
|
|
50
|
+
- Provisioning (AP mode)
|
|
51
|
+
- CLI usage
|
|
52
|
+
- Development (tests, lint, hooks)
|
|
53
|
+
|
|
54
|
+
Installation
|
|
55
|
+
------------
|
|
56
|
+
Using Poetry (preferred):
|
|
57
|
+
```
|
|
58
|
+
poetry install --with dev
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Using pip:
|
|
62
|
+
```
|
|
63
|
+
pip install python-swidget
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Quickstart
|
|
67
|
+
----------
|
|
68
|
+
```python
|
|
69
|
+
from swidget import SwidgetDimmer
|
|
70
|
+
|
|
71
|
+
dev = SwidgetDimmer(
|
|
72
|
+
host="192.168.1.50",
|
|
73
|
+
token_name="x-secret-key",
|
|
74
|
+
secret_key="password",
|
|
75
|
+
use_https=True,
|
|
76
|
+
use_websockets=False, # set True for realtime updates
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# HTTP-only mode
|
|
80
|
+
await dev.update()
|
|
81
|
+
await dev.turn_on()
|
|
82
|
+
await dev.turn_off()
|
|
83
|
+
await dev.close()
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Websocket mode (realtime)
|
|
87
|
+
```python
|
|
88
|
+
dev = SwidgetDimmer(
|
|
89
|
+
host="192.168.1.50",
|
|
90
|
+
token_name="x-secret-key",
|
|
91
|
+
secret_key="password",
|
|
92
|
+
use_https=True,
|
|
93
|
+
use_websockets=True,
|
|
94
|
+
)
|
|
95
|
+
await dev.start() # opens websocket and updates state
|
|
96
|
+
await dev.turn_on()
|
|
97
|
+
await dev.close()
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Device types and helpers
|
|
101
|
+
------------------------
|
|
102
|
+
- `SwidgetDevice` – base class (shared HTTP/websocket helpers)
|
|
103
|
+
- `SwidgetDimmer` – brightness control (`brightness`, `set_brightness`)
|
|
104
|
+
- `SwidgetOutlet` – outlet control (`turn_on`, `turn_off`, power readings)
|
|
105
|
+
- `SwidgetSwitch` – generic switch control (`turn_on`, `turn_off`)
|
|
106
|
+
- `SwidgetTimerSwitch` – timer-capable switch (`set_countdown_timer`)
|
|
107
|
+
|
|
108
|
+
Common properties:
|
|
109
|
+
- `device_type`, `insert_type`, `friendly_name`, `hw_info`
|
|
110
|
+
- `realtime_values`: convenience dict of sensor/power values
|
|
111
|
+
|
|
112
|
+
Common actions:
|
|
113
|
+
- `turn_on()`, `turn_off()`, `blink()`, `ping()`
|
|
114
|
+
- `update()` to refresh summary/state/config
|
|
115
|
+
- `send_command(assembly, component, function, command_dict)` for raw control
|
|
116
|
+
|
|
117
|
+
HTTP vs websocket modes
|
|
118
|
+
-----------------------
|
|
119
|
+
- HTTP-only: set `use_websockets=False`. All operations go over REST (`/api/v1/...`).
|
|
120
|
+
- Websocket mode: set `use_websockets=True`. Summary/state updates and commands use the socket when connected; HTTP is used as fallback where applicable.
|
|
121
|
+
- TLS: `use_https=True` disables certificate verification by default; pass `verify_ssl=True` when constructing `SwidgetDevice` if you have valid certs.
|
|
122
|
+
|
|
123
|
+
Discovery
|
|
124
|
+
---------
|
|
125
|
+
Discover devices via SSDP:
|
|
126
|
+
```python
|
|
127
|
+
from swidget import discover_devices
|
|
128
|
+
devices = await discover_devices(timeout=5)
|
|
129
|
+
for mac, dev in devices.items():
|
|
130
|
+
print(dev.host, dev.friendly_name, dev.host_type, dev.insert_type)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Discover a single device when you know the IP:
|
|
134
|
+
```python
|
|
135
|
+
from swidget import discover_single
|
|
136
|
+
dev = await discover_single(
|
|
137
|
+
host="192.168.1.50",
|
|
138
|
+
token_name="x-secret-key",
|
|
139
|
+
password="device_password",
|
|
140
|
+
use_https=True,
|
|
141
|
+
use_websockets=False,
|
|
142
|
+
)
|
|
143
|
+
await dev.update()
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Provisioning (AP mode)
|
|
147
|
+
----------------------
|
|
148
|
+
Provision a device while connected to its AP (`Swidget-...` SSID):
|
|
149
|
+
```python
|
|
150
|
+
from swidget import provision_wifi
|
|
151
|
+
provision_wifi(
|
|
152
|
+
device_name="My Swidget",
|
|
153
|
+
ssid="HomeWiFi",
|
|
154
|
+
network_password="wifi-pass",
|
|
155
|
+
secret_key="factory-secret",
|
|
156
|
+
)
|
|
157
|
+
```
|
|
158
|
+
Notes:
|
|
159
|
+
- Provisioning disables TLS verification; run only on the device’s AP network.
|
|
160
|
+
- The helper waits for connection success and triggers setup completion.
|
|
161
|
+
|
|
162
|
+
CLI usage
|
|
163
|
+
---------
|
|
164
|
+
Install the console entrypoint via Poetry/pip and run:
|
|
165
|
+
```
|
|
166
|
+
swidget --host 192.168.1.50 --password device_password --type dimmer state
|
|
167
|
+
swidget --host 192.168.1.50 --password device_password on
|
|
168
|
+
swidget --host 192.168.1.50 --password device_password brightness 80
|
|
169
|
+
swidget discover
|
|
170
|
+
```
|
|
171
|
+
Key options:
|
|
172
|
+
- `--http_only` (flag) to force HTTP-only (no websockets)
|
|
173
|
+
- `--type` to skip discovery when you know the device type
|
|
174
|
+
- `wifi join` to provision via AP (prompts for SSID/password)
|
|
175
|
+
|
|
176
|
+
Development
|
|
177
|
+
-----------
|
|
178
|
+
Run tests and lint (hook runs both):
|
|
179
|
+
```
|
|
180
|
+
pytest
|
|
181
|
+
black --check .
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
If you install via plain pip and want the dev toolchain (tests, typing, docs, formatting):
|
|
185
|
+
```
|
|
186
|
+
pip install pytest pytest-asyncio aioresponses mypy black sphinx sphinx-rtd-theme sphinx-autobuild
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Enable git hook (optional):
|
|
190
|
+
```
|
|
191
|
+
git config core.hooksPath githooks
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Code structure:
|
|
195
|
+
- `swidget/` core library: device models, discovery, provisioning, websocket client, CLI
|
|
196
|
+
- `tests/` unit tests (network mocked)
|
|
197
|
+
- `devtools/synthetic_test.py` optional integration script for real devices (kept separate from unit tests)
|
|
198
|
+
- `examples/` runnable samples (dimmer HTTP, outlet power, timer switch, snapshot)
|
|
199
|
+
|
|
200
|
+
Additional examples
|
|
201
|
+
-------------------
|
|
202
|
+
|
|
203
|
+
Outlet (power reading + toggle)
|
|
204
|
+
```
|
|
205
|
+
from swidget import SwidgetOutlet
|
|
206
|
+
|
|
207
|
+
dev = SwidgetOutlet(
|
|
208
|
+
host="192.168.1.60",
|
|
209
|
+
token_name="x-secret-key",
|
|
210
|
+
secret_key="password",
|
|
211
|
+
use_https=True,
|
|
212
|
+
use_websockets=False,
|
|
213
|
+
)
|
|
214
|
+
await dev.update()
|
|
215
|
+
print("Host features:", dev.host_features)
|
|
216
|
+
print("Realtime values:", dev.realtime_values)
|
|
217
|
+
await dev.turn_on()
|
|
218
|
+
await dev.turn_off()
|
|
219
|
+
await dev.close()
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Timer switch
|
|
223
|
+
```
|
|
224
|
+
from swidget import SwidgetTimerSwitch
|
|
225
|
+
|
|
226
|
+
dev = SwidgetTimerSwitch(
|
|
227
|
+
host="192.168.1.61",
|
|
228
|
+
token_name="x-secret-key",
|
|
229
|
+
secret_key="password",
|
|
230
|
+
use_https=True,
|
|
231
|
+
use_websockets=False,
|
|
232
|
+
)
|
|
233
|
+
await dev.set_countdown_timer(20) # minutes
|
|
234
|
+
await dev.close()
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Snapshots (video-capable insert)
|
|
238
|
+
```
|
|
239
|
+
from swidget import SwidgetDevice
|
|
240
|
+
|
|
241
|
+
dev = SwidgetDevice(
|
|
242
|
+
host="192.168.1.62",
|
|
243
|
+
token_name="x-secret-key",
|
|
244
|
+
secret_key="password",
|
|
245
|
+
use_https=True,
|
|
246
|
+
use_websockets=False,
|
|
247
|
+
)
|
|
248
|
+
await dev.update()
|
|
249
|
+
img = await dev.get_snapshot_bytes(width=640, height=360)
|
|
250
|
+
with open("snapshot.jpg", "wb") as f:
|
|
251
|
+
f.write(img)
|
|
252
|
+
await dev.close()
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Type checking
|
|
256
|
+
-------------
|
|
257
|
+
Run mypy in strict mode:
|
|
258
|
+
```
|
|
259
|
+
poetry run mypy --strict swidget
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Docs build
|
|
263
|
+
----------
|
|
264
|
+
Generate HTML docs with Sphinx:
|
|
265
|
+
```
|
|
266
|
+
cd docs
|
|
267
|
+
poetry run sphinx-build -b html . _build
|
|
268
|
+
```
|
|
269
|
+
|
|
@@ -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.1"
|
|
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
|
|
25
|
+
requests = "^2.32"
|
|
26
26
|
types-requests = "^2.32.0"
|
|
27
|
-
urllib3 =
|
|
27
|
+
urllib3 = "^2.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
|
|
|
@@ -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__)
|
|
@@ -63,10 +68,16 @@ class SelfDiagnosticErrorCodes(Enum):
|
|
|
63
68
|
|
|
64
69
|
|
|
65
70
|
class SwidgetDevice:
|
|
66
|
-
"""
|
|
71
|
+
"""Core representation of a Swidget device (base class for all device types)."""
|
|
67
72
|
|
|
68
73
|
def __init__(
|
|
69
|
-
self,
|
|
74
|
+
self,
|
|
75
|
+
host,
|
|
76
|
+
token_name,
|
|
77
|
+
secret_key,
|
|
78
|
+
use_https=True,
|
|
79
|
+
use_websockets=True,
|
|
80
|
+
verify_ssl: bool = False,
|
|
70
81
|
) -> None:
|
|
71
82
|
self.token_name = token_name
|
|
72
83
|
self.ip_address = host
|
|
@@ -74,13 +85,16 @@ class SwidgetDevice:
|
|
|
74
85
|
self.uri_scheme = "https" if self.use_https is True else "http"
|
|
75
86
|
self.secret_key = secret_key
|
|
76
87
|
self.use_websockets = use_websockets
|
|
88
|
+
self.verify_ssl = verify_ssl
|
|
77
89
|
self.device_type = DeviceType.Unknown
|
|
78
90
|
self._friendly_name = "Unknown Swidget Device"
|
|
79
91
|
self.assemblies: Dict[Any, Any] = dict()
|
|
80
92
|
self.device_config: DeviceConfiguration = DeviceConfiguration({})
|
|
81
93
|
self._subscribers: List[Any] = list()
|
|
82
94
|
headers = {self.token_name: self.secret_key, "Connection": "keep-alive"}
|
|
83
|
-
|
|
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)
|
|
84
98
|
if use_https is True:
|
|
85
99
|
self._session = ClientSession(headers=headers, connector=connector)
|
|
86
100
|
else:
|
|
@@ -93,12 +107,14 @@ class SwidgetDevice:
|
|
|
93
107
|
secret_key=self.secret_key,
|
|
94
108
|
callback=self.message_callback,
|
|
95
109
|
session=self._session,
|
|
110
|
+
use_security=self.use_https,
|
|
111
|
+
verify_ssl=verify_ssl,
|
|
96
112
|
)
|
|
97
113
|
|
|
98
114
|
@property
|
|
99
115
|
def connected(self) -> bool:
|
|
100
116
|
"""Property to represent if the client is connected to the device."""
|
|
101
|
-
return self._websocket.connected
|
|
117
|
+
return hasattr(self, "_websocket") and self._websocket.connected
|
|
102
118
|
|
|
103
119
|
def get_websocket(self) -> Optional[SwidgetWebsocket]:
|
|
104
120
|
"""Return the SwidgetWebsocket class instance if possible."""
|
|
@@ -189,15 +205,112 @@ class SwidgetDevice:
|
|
|
189
205
|
for callback in self._subscribers:
|
|
190
206
|
await callback(message)
|
|
191
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
|
+
|
|
192
309
|
async def get_device_config(self) -> Any:
|
|
193
310
|
"""Get the config of the device."""
|
|
194
311
|
_LOGGER.debug("SwidgetDevice.get_device_config() called")
|
|
195
312
|
_LOGGER.debug("Sending get_summary() command over http")
|
|
196
|
-
|
|
197
|
-
url=f"{self.uri_scheme}://{self.ip_address}/api/v1/device_config",
|
|
198
|
-
ssl=False,
|
|
199
|
-
) as response:
|
|
200
|
-
config = await response.json()
|
|
313
|
+
config = await self.make_http_request("GET", "device_config")
|
|
201
314
|
self.device_config = DeviceConfiguration(config)
|
|
202
315
|
self._last_update = int(time.time())
|
|
203
316
|
|
|
@@ -213,10 +326,7 @@ class SwidgetDevice:
|
|
|
213
326
|
)
|
|
214
327
|
else:
|
|
215
328
|
_LOGGER.debug("In http mode. Sending get_summary() command over http")
|
|
216
|
-
|
|
217
|
-
url=f"{self.uri_scheme}://{self.ip_address}/api/v1/summary", ssl=False
|
|
218
|
-
) as response:
|
|
219
|
-
summary = await response.json()
|
|
329
|
+
summary = await self.make_http_request("GET", "summary")
|
|
220
330
|
await self.process_summary(summary)
|
|
221
331
|
|
|
222
332
|
async def process_summary(self, summary) -> None:
|
|
@@ -239,10 +349,7 @@ class SwidgetDevice:
|
|
|
239
349
|
"""Retrieve the friendly name of the device."""
|
|
240
350
|
_LOGGER.debug("SwidgetDevice.get_friendly_name() called")
|
|
241
351
|
try:
|
|
242
|
-
|
|
243
|
-
url=f"{self.uri_scheme}://{self.ip_address}/api/v1/name", ssl=False
|
|
244
|
-
) as response:
|
|
245
|
-
name = await response.json()
|
|
352
|
+
name = await self.make_http_request("GET", "name")
|
|
246
353
|
except Exception:
|
|
247
354
|
name = {"name": f"Swidget {self.device_type} w/{self.insert_type} insert"}
|
|
248
355
|
await self.process_friendly_name(name["name"])
|
|
@@ -258,17 +365,14 @@ class SwidgetDevice:
|
|
|
258
365
|
_LOGGER.debug("SwidgetDevice.get_state() called")
|
|
259
366
|
if self.use_websockets:
|
|
260
367
|
_LOGGER.debug(
|
|
261
|
-
"In websocket mode. Sending
|
|
368
|
+
"In websocket mode. Sending get_state() command over websocket"
|
|
262
369
|
)
|
|
263
370
|
await self._websocket.send_str(
|
|
264
371
|
json.dumps({"type": "state", "request_id": "state"})
|
|
265
372
|
)
|
|
266
373
|
else:
|
|
267
|
-
_LOGGER.debug("In http mode. Sending
|
|
268
|
-
|
|
269
|
-
url=f"{self.uri_scheme}://{self.ip_address}/api/v1/state", ssl=False
|
|
270
|
-
) as response:
|
|
271
|
-
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")
|
|
272
376
|
await self.process_state(state)
|
|
273
377
|
|
|
274
378
|
async def process_state(self, state) -> None:
|
|
@@ -297,7 +401,7 @@ class SwidgetDevice:
|
|
|
297
401
|
await self.get_state()
|
|
298
402
|
if self._friendly_name == "Unknown Swidget Device":
|
|
299
403
|
await self.get_friendly_name()
|
|
300
|
-
if self.device_config
|
|
404
|
+
if not self.device_config.config_populated():
|
|
301
405
|
await self.get_device_config()
|
|
302
406
|
elif (int(time.time()) - self._last_update) < 5:
|
|
303
407
|
_LOGGER.debug("update() recently called, not executing")
|
|
@@ -309,10 +413,18 @@ class SwidgetDevice:
|
|
|
309
413
|
async def send_config(self, payload: dict) -> None:
|
|
310
414
|
"""Send a config block to the device."""
|
|
311
415
|
_LOGGER.debug("SwidgetDevice.send_config() called")
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
+
)
|
|
316
428
|
|
|
317
429
|
async def send_command(
|
|
318
430
|
self, assembly: str, component: str, function: str, command: dict
|
|
@@ -329,16 +441,19 @@ class SwidgetDevice:
|
|
|
329
441
|
await self._websocket.send_str(command_data)
|
|
330
442
|
else:
|
|
331
443
|
_LOGGER.debug("NOT in websocket mode, sending command over HTTP")
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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")
|
|
342
457
|
|
|
343
458
|
async def ping(self) -> bool:
|
|
344
459
|
"""Ping the device to ensure it's devices.
|
|
@@ -347,12 +462,9 @@ class SwidgetDevice:
|
|
|
347
462
|
"""
|
|
348
463
|
_LOGGER.debug("SwidgetDevice.ping() called")
|
|
349
464
|
try:
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
if response.status == 200:
|
|
354
|
-
return True
|
|
355
|
-
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)
|
|
356
468
|
except Exception:
|
|
357
469
|
return False
|
|
358
470
|
|
|
@@ -362,13 +474,7 @@ class SwidgetDevice:
|
|
|
362
474
|
:raises SwidgetException: Raise the exception if there we are unable to connect to the Swidget device
|
|
363
475
|
"""
|
|
364
476
|
_LOGGER.debug("SwidgetDevice.blink() called")
|
|
365
|
-
|
|
366
|
-
async with self._session.get(
|
|
367
|
-
url=f"{self.uri_scheme}://{self.ip_address}/blink", ssl=False
|
|
368
|
-
) as response:
|
|
369
|
-
return await response.json()
|
|
370
|
-
except Exception:
|
|
371
|
-
raise SwidgetException
|
|
477
|
+
return await self._make_passthrough_request("GET", "blink")
|
|
372
478
|
|
|
373
479
|
async def enable_debug_server(self) -> Any:
|
|
374
480
|
"""Enable the Swidget local debug server.
|
|
@@ -376,30 +482,14 @@ class SwidgetDevice:
|
|
|
376
482
|
:raises SwidgetException: Raise the exception if there we are unable to connect to the Swidget device
|
|
377
483
|
"""
|
|
378
484
|
_LOGGER.debug("SwidgetDevice.enable_debug_server() called")
|
|
379
|
-
|
|
380
|
-
async with self._session.get(
|
|
381
|
-
url=f"{self.uri_scheme}://{self.ip_address}/debug?x-secret-key={self.secret_key}",
|
|
382
|
-
ssl=False,
|
|
383
|
-
) as response:
|
|
384
|
-
if response.status == 200:
|
|
385
|
-
return True
|
|
386
|
-
return False
|
|
387
|
-
except Exception:
|
|
388
|
-
raise SwidgetException
|
|
485
|
+
return await self.make_http_request("GET", "debug")
|
|
389
486
|
|
|
390
487
|
async def restart_device(self) -> Any:
|
|
391
488
|
"""Restart the Swidget device.
|
|
392
489
|
|
|
393
490
|
:raises SwidgetException: Raise the exception if there we are unable to connect to the Swidget device
|
|
394
491
|
"""
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
async with self._session.post(
|
|
398
|
-
url=f"{self.uri_scheme}://{self.ip_address}/api/v1/reset", ssl=False
|
|
399
|
-
) as response:
|
|
400
|
-
return await response.json()
|
|
401
|
-
except Exception:
|
|
402
|
-
raise SwidgetException
|
|
492
|
+
return await self.make_http_request("POST", "reset")
|
|
403
493
|
|
|
404
494
|
async def factory_reset(self) -> Any:
|
|
405
495
|
"""Factory reset the Swidget device.
|
|
@@ -421,12 +511,8 @@ class SwidgetDevice:
|
|
|
421
511
|
:raises SwidgetException: Raise the exception if there we are unable to connect to the Swidget device
|
|
422
512
|
"""
|
|
423
513
|
try:
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
url=f"{self.uri_scheme}://{self.ip_address}/api/v1/update", ssl=False
|
|
427
|
-
) as response:
|
|
428
|
-
newer_versions = await response.json()
|
|
429
|
-
return sorted(newer_versions["updates"])
|
|
514
|
+
newer_versions = await self.make_http_request("GET", "update")
|
|
515
|
+
return sorted(newer_versions["updates"])
|
|
430
516
|
except Exception:
|
|
431
517
|
raise SwidgetException
|
|
432
518
|
|
|
@@ -437,15 +523,10 @@ class SwidgetDevice:
|
|
|
437
523
|
"""
|
|
438
524
|
try:
|
|
439
525
|
data = {"version": version}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
) as response:
|
|
445
|
-
result = response.status
|
|
446
|
-
if result == 200:
|
|
447
|
-
return True
|
|
448
|
-
return False
|
|
526
|
+
response = await self.make_http_request(
|
|
527
|
+
"POST", "update/version", json_payload=data
|
|
528
|
+
)
|
|
529
|
+
return bool(response)
|
|
449
530
|
except Exception:
|
|
450
531
|
raise SwidgetException
|
|
451
532
|
|
|
@@ -758,7 +839,7 @@ class DeviceConfiguration:
|
|
|
758
839
|
|
|
759
840
|
def config_populated(self) -> bool:
|
|
760
841
|
"""Return if configuration has been retrieved from the device."""
|
|
761
|
-
return self._config_dict
|
|
842
|
+
return self._config_dict != {}
|
|
762
843
|
|
|
763
844
|
@property
|
|
764
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:
|
|
@@ -188,7 +190,7 @@ class SwidgetWebsocket:
|
|
|
188
190
|
else:
|
|
189
191
|
self.callback(message)
|
|
190
192
|
|
|
191
|
-
def status(self) -> dict:
|
|
193
|
+
def status(self) -> dict[str, Any]:
|
|
192
194
|
"""Return the current status of the websocket connection."""
|
|
193
195
|
_LOGGER.debug("websocket.status() called")
|
|
194
196
|
return {
|
python_swidget-1.3.3/PKG-INFO
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.3
|
|
2
|
-
Name: python-swidget
|
|
3
|
-
Version: 1.3.3
|
|
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.5)
|
|
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.32.0,<3.0.0)
|
|
30
|
-
Requires-Dist: urllib3 (==2.5.0)
|
|
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.3/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
|