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.
@@ -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
+ ![CI](https://github.com/swidget/python-swidget/actions/workflows/ci.yml/badge.svg)
39
+ ![PyPI](https://img.shields.io/pypi/v/python-swidget)
40
+ ![License](https://img.shields.io/pypi/l/python-swidget)
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
+ ![CI](https://github.com/swidget/python-swidget/actions/workflows/ci.yml/badge.svg)
5
+ ![PyPI](https://img.shields.io/pypi/v/python-swidget)
6
+ ![License](https://img.shields.io/pypi/l/python-swidget)
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.2"
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.3"
26
- types-requests = "^2.3.0"
27
- urllib3 = '1.26.5'
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 # type: ignore
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 == 200:
239
+ if result:
240
240
  click.echo("Successfully pinged device")
241
241
  else:
242
- click.echo(result.status_code)
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 # type: ignore
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 SwidgetException
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
- """Class to represent the Swidge device."""
71
+ """Core representation of a Swidget device (base class for all device types)."""
65
72
 
66
73
  def __init__(
67
- self, host, token_name, secret_key, use_https=True, use_websockets=True
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
- connector = TCPConnector(verify_ssl=False, force_close=True)
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
- async with self._session.get(
195
- url=f"{self.uri_scheme}://{self.ip_address}/api/v1/device_config",
196
- ssl=False,
197
- ) as response:
198
- config = await response.json()
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
- async with self._session.get(
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
- async with self._session.get(
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 get_summary() command over websocket"
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 get_summary() command over http")
266
- async with self._session.get(
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
- pass
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
- data = json.dumps(
311
- {"type": "config", "request_id": "send_config", "payload": payload}
312
- )
313
- await self._websocket.send_str(data)
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
- async with self._session.post(
331
- url=f"{self.uri_scheme}://{self.ip_address}/api/v1/command",
332
- ssl=False,
333
- data=json.dumps(data),
334
- ) as response:
335
- state = await response.json()
336
-
337
- # Do a hard set of the new state of the device. May change this in the future
338
- function_value = state[assembly]["components"][component][function]
339
- self.assemblies[assembly].components[component].functions[function] = function_value # fmt: skip
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
- async with self._session.get(
349
- url=f"{self.uri_scheme}://{self.ip_address}/ping", ssl=False
350
- ) as response:
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
- try:
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
- try:
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
- try:
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
- async with self._session.get(
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
- async with self._session.post(
439
- url=f"{self.uri_scheme}://{self.ip_address}/api/v1/update/version",
440
- ssl=False,
441
- data=json.dumps(data),
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 # type: ignore
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 # type: ignore
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.set_brightness() called")
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.error("Error connecting to websocket")
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 as e:
103
- _LOGGER.error(f"An unexpected error occurred: {e}")
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 == WSMsgType.CLOSED:
140
- _LOGGER.error("Websocket client is closed")
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
- """Reconnect to the WebSocket server after a delay."""
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.warning("Max retries reached. Stopping reconnect attempts.")
164
- self.is_running = False
165
- return
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 {
@@ -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
-
@@ -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
- ```
@@ -1,2 +0,0 @@
1
- class SwidgetException(Exception):
2
- """Base exception for device errors."""