python-swidget 0.0.31__tar.gz → 1.0.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-0.0.31 → python_swidget-1.0.0}/PKG-INFO +20 -1
- python_swidget-1.0.0/README.md +21 -0
- {python_swidget-0.0.31 → python_swidget-1.0.0}/pyproject.toml +1 -1
- {python_swidget-0.0.31 → python_swidget-1.0.0}/swidget/__init__.py +2 -0
- {python_swidget-0.0.31 → python_swidget-1.0.0}/swidget/cli.py +92 -34
- {python_swidget-0.0.31 → python_swidget-1.0.0}/swidget/discovery.py +15 -4
- python_swidget-1.0.0/swidget/provision.py +116 -0
- {python_swidget-0.0.31 → python_swidget-1.0.0}/swidget/swidgetdevice.py +162 -43
- {python_swidget-0.0.31 → python_swidget-1.0.0}/swidget/swidgetdimmer.py +5 -4
- {python_swidget-0.0.31 → python_swidget-1.0.0}/swidget/swidgetoutlet.py +2 -2
- {python_swidget-0.0.31 → python_swidget-1.0.0}/swidget/swidgetswitch.py +2 -2
- {python_swidget-0.0.31 → python_swidget-1.0.0}/swidget/swidgettimerswitch.py +6 -2
- python_swidget-1.0.0/swidget/websocket.py +99 -0
- python_swidget-0.0.31/README.md +0 -2
- python_swidget-0.0.31/swidget/provision.py +0 -16
- python_swidget-0.0.31/swidget/websocket.py +0 -128
- {python_swidget-0.0.31 → python_swidget-1.0.0}/swidget/exceptions.py +0 -0
- {python_swidget-0.0.31 → python_swidget-1.0.0}/swidget/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: python-swidget
|
|
3
|
-
Version: 0.0
|
|
3
|
+
Version: 1.0.0
|
|
4
4
|
Summary: Python API for Swidget smart devices
|
|
5
5
|
Home-page: https://github.com/swidget/python-swidget
|
|
6
6
|
License: GPL-3.0-or-later
|
|
@@ -12,6 +12,7 @@ Classifier: Programming Language :: Python :: 3.8
|
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.9
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.10
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
16
|
Provides-Extra: docs
|
|
16
17
|
Requires-Dist: aiohttp (>=3.8.1)
|
|
17
18
|
Requires-Dist: anyio
|
|
@@ -30,3 +31,21 @@ Description-Content-Type: text/markdown
|
|
|
30
31
|
# python-swidget
|
|
31
32
|
A library to manage Swidget smart devices
|
|
32
33
|
|
|
34
|
+
# Basic Usage
|
|
35
|
+
|
|
36
|
+
## Connect to the device using http/ https
|
|
37
|
+
```
|
|
38
|
+
dev = SwidgetDimmer(host=host, token_name='x-secret-key', secret_key='password', use_https=True, use_websockets=False)
|
|
39
|
+
dev.update()
|
|
40
|
+
dev.turn_on()
|
|
41
|
+
dev.close()
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Connect to the device using websockets
|
|
45
|
+
```
|
|
46
|
+
dev = SwidgetDimmer(host=host, token_name='x-secret-key', secret_key='password', use_https=True, use_websockets=True)
|
|
47
|
+
dev.start()
|
|
48
|
+
dev.update()
|
|
49
|
+
dev.turn_on()
|
|
50
|
+
dev.close()
|
|
51
|
+
```
|
|
@@ -0,0 +1,21 @@
|
|
|
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
|
+
```
|
|
@@ -15,6 +15,7 @@ from importlib_metadata import version # type: ignore
|
|
|
15
15
|
|
|
16
16
|
from swidget.discovery import discover_devices, discover_single, SwidgetDiscoveredDevice
|
|
17
17
|
from swidget.exceptions import SwidgetException
|
|
18
|
+
from swidget.provision import provision_wifi
|
|
18
19
|
from swidget.swidgetdevice import DeviceType, SwidgetAssembly, SwidgetDevice, SwidgetComponent
|
|
19
20
|
from swidget.swidgetdimmer import SwidgetDimmer
|
|
20
21
|
from swidget.swidgetoutlet import SwidgetOutlet
|
|
@@ -28,6 +29,7 @@ __version__ = version("python-swidget")
|
|
|
28
29
|
__all__ = [
|
|
29
30
|
"discover_devices",
|
|
30
31
|
"discover_single",
|
|
32
|
+
"provision_wifi"
|
|
31
33
|
"SwidgetDiscoveredDevice",
|
|
32
34
|
"SwidgetException",
|
|
33
35
|
"DeviceType",
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
"""python-swidget cli tool."""
|
|
2
|
-
import asyncio
|
|
3
2
|
import logging
|
|
4
3
|
import sys
|
|
5
4
|
from pprint import pformat as pf
|
|
6
5
|
from typing import cast
|
|
7
6
|
|
|
8
7
|
import asyncclick as click
|
|
8
|
+
from contextlib import asynccontextmanager
|
|
9
9
|
|
|
10
10
|
from swidget import (
|
|
11
11
|
discover_devices,
|
|
12
12
|
discover_single,
|
|
13
|
+
provision_wifi,
|
|
13
14
|
SwidgetDevice,
|
|
14
15
|
SwidgetDimmer,
|
|
15
16
|
SwidgetSwitch,
|
|
@@ -31,14 +32,12 @@ pass_dev = click.make_pass_decorator(SwidgetDevice)
|
|
|
31
32
|
|
|
32
33
|
|
|
33
34
|
@click.group(invoke_without_command=True)
|
|
34
|
-
@click.option(
|
|
35
|
-
"--host",
|
|
35
|
+
@click.option("--host",
|
|
36
36
|
envvar="SWIDGET_HOST",
|
|
37
37
|
required=False,
|
|
38
38
|
help="The host name or IP address of the device to connect to.",
|
|
39
39
|
)
|
|
40
|
-
@click.option(
|
|
41
|
-
"--password",
|
|
40
|
+
@click.option("-p", "--password",
|
|
42
41
|
envvar="SWIDGET_PASSWORD",
|
|
43
42
|
required=False,
|
|
44
43
|
help="The password of the device to connect to.",
|
|
@@ -47,6 +46,10 @@ pass_dev = click.make_pass_decorator(SwidgetDevice)
|
|
|
47
46
|
envvar="SWIDGET_DEBUG",
|
|
48
47
|
default=False,
|
|
49
48
|
is_flag=True)
|
|
49
|
+
@click.option("--http_only",
|
|
50
|
+
envvar="SWIDGET_HTTP_ONLY",
|
|
51
|
+
default=True,
|
|
52
|
+
is_flag=True)
|
|
50
53
|
@click.option(
|
|
51
54
|
"--type",
|
|
52
55
|
envvar="SWIDGET_TYPE",
|
|
@@ -55,12 +58,10 @@ pass_dev = click.make_pass_decorator(SwidgetDevice)
|
|
|
55
58
|
)
|
|
56
59
|
@click.version_option(package_name="python-swidget")
|
|
57
60
|
@click.pass_context
|
|
58
|
-
async def cli(ctx, host, password, debug, type):
|
|
61
|
+
async def cli(ctx, host, password, debug, http_only, type):
|
|
59
62
|
"""A tool for controlling Swidget smart home devices.""" # noqa
|
|
60
63
|
# no need to perform any checks if we are just displaying the help
|
|
61
64
|
if sys.argv[-1] == "--help":
|
|
62
|
-
# Context object is required to avoid crashing on sub-groups
|
|
63
|
-
ctx.obj = SwidgetDevice(None)
|
|
64
65
|
return
|
|
65
66
|
|
|
66
67
|
if debug:
|
|
@@ -68,54 +69,61 @@ async def cli(ctx, host, password, debug, type):
|
|
|
68
69
|
else:
|
|
69
70
|
logging.basicConfig(level=logging.INFO)
|
|
70
71
|
|
|
71
|
-
if ctx.invoked_subcommand == "discover":
|
|
72
|
+
if ctx.invoked_subcommand == "discover" or ctx.invoked_subcommand == "wifi":
|
|
72
73
|
return
|
|
73
|
-
|
|
74
74
|
if host is None:
|
|
75
75
|
click.echo("No host name given, trying discovery..")
|
|
76
76
|
await ctx.invoke(discover)
|
|
77
77
|
return
|
|
78
|
-
|
|
79
78
|
if type is not None:
|
|
80
79
|
dev = TYPE_TO_CLASS[type](host=host,
|
|
81
80
|
token_name='x-secret-key',
|
|
82
81
|
secret_key=password,
|
|
83
|
-
|
|
82
|
+
use_https=http_only,
|
|
84
83
|
use_websockets=False)
|
|
85
84
|
else:
|
|
86
|
-
click.echo("No --type defined, discovering
|
|
85
|
+
click.echo("No --type defined, discovering...")
|
|
87
86
|
dev = await discover_single(host=host,
|
|
88
87
|
token_name='x-secret-key',
|
|
89
88
|
password=password,
|
|
90
|
-
|
|
89
|
+
use_https=http_only,
|
|
91
90
|
use_websockets=False)
|
|
91
|
+
await dev.update()
|
|
92
92
|
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
@asynccontextmanager
|
|
94
|
+
async def async_wrapped_device(dev: SwidgetDevice):
|
|
95
|
+
try:
|
|
96
|
+
yield dev
|
|
97
|
+
finally:
|
|
98
|
+
await dev.stop()
|
|
99
|
+
|
|
100
|
+
ctx.obj = await ctx.with_async_resource(async_wrapped_device(dev))
|
|
95
101
|
|
|
96
102
|
if ctx.invoked_subcommand is None:
|
|
97
|
-
await ctx.invoke(state)
|
|
103
|
+
return await ctx.invoke(state)
|
|
98
104
|
|
|
99
105
|
|
|
100
106
|
@cli.group()
|
|
101
|
-
|
|
102
|
-
def wifi(dev):
|
|
107
|
+
def wifi():
|
|
103
108
|
"""Commands to control wifi settings."""
|
|
104
109
|
|
|
110
|
+
|
|
105
111
|
@wifi.command()
|
|
106
|
-
@click.
|
|
107
|
-
@click.option("--
|
|
108
|
-
@click.option("--
|
|
109
|
-
@
|
|
110
|
-
|
|
112
|
+
@click.option("--ssid", prompt=True, hide_input=False)
|
|
113
|
+
@click.option("--network_password", prompt=True, hide_input=True)
|
|
114
|
+
@click.option("--secret_key", prompt=True, hide_input=True)
|
|
115
|
+
@click.option("--friendly_name", prompt=True, hide_input=False)
|
|
116
|
+
def join(ssid, network_password, secret_key, friendly_name):
|
|
111
117
|
"""Join the given wifi network."""
|
|
112
|
-
click.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
118
|
+
confirmation = click.prompt(f"Are you connected to a wifi network that stars with the name 'Swidget-' (y/n)")
|
|
119
|
+
if confirmation == "y":
|
|
120
|
+
click.echo(f"Asking the device to connect to network {ssid}..")
|
|
121
|
+
# def provision_wifi(ssid, network_password, token_name, secret_key, friendly_name):
|
|
122
|
+
provision_wifi(friendly_name, ssid, network_password, secret_key)
|
|
123
|
+
return True
|
|
124
|
+
else:
|
|
125
|
+
click.echo("Not provisioning wifi")
|
|
126
|
+
return False
|
|
119
127
|
|
|
120
128
|
|
|
121
129
|
@cli.command()
|
|
@@ -158,7 +166,8 @@ async def state(dev: SwidgetDevice):
|
|
|
158
166
|
click.echo(f"\tMAC (rssi): {dev.mac_address} ({dev.rssi})")
|
|
159
167
|
|
|
160
168
|
click.echo(click.style("\n\t== Current State ==", bold=True))
|
|
161
|
-
|
|
169
|
+
realtime_values = await dev.realtime_values
|
|
170
|
+
for info_name, info_data in realtime_values.items():
|
|
162
171
|
if isinstance(info_data, list):
|
|
163
172
|
click.echo(f"\t{info_name}:")
|
|
164
173
|
for item in info_data:
|
|
@@ -176,7 +185,6 @@ async def state(dev: SwidgetDevice):
|
|
|
176
185
|
click.echo(click.style(f"\t+ {function}", fg="green"))
|
|
177
186
|
|
|
178
187
|
|
|
179
|
-
|
|
180
188
|
@cli.command()
|
|
181
189
|
@pass_dev
|
|
182
190
|
@click.argument("assembly")
|
|
@@ -196,7 +204,6 @@ async def raw_command(dev: SwidgetDevice, assembly, component, function, command
|
|
|
196
204
|
return res
|
|
197
205
|
|
|
198
206
|
|
|
199
|
-
|
|
200
207
|
@cli.command()
|
|
201
208
|
@click.argument("brightness", type=click.IntRange(0, 100), default=None, required=False)
|
|
202
209
|
@pass_dev
|
|
@@ -221,6 +228,20 @@ async def blink(dev):
|
|
|
221
228
|
return await dev.blink()
|
|
222
229
|
|
|
223
230
|
|
|
231
|
+
@cli.command()
|
|
232
|
+
@pass_dev
|
|
233
|
+
async def ping(dev):
|
|
234
|
+
"""Ping the device"""
|
|
235
|
+
click.echo(f"Pinging the device")
|
|
236
|
+
try:
|
|
237
|
+
result = await dev.ping()
|
|
238
|
+
if result == 200:
|
|
239
|
+
click.echo("Successfully pinged device")
|
|
240
|
+
else:
|
|
241
|
+
click.echo(result.status_code)
|
|
242
|
+
except:
|
|
243
|
+
click.echo("Unable to ping device")
|
|
244
|
+
|
|
224
245
|
@cli.command()
|
|
225
246
|
@pass_dev
|
|
226
247
|
async def on(dev: SwidgetDevice):
|
|
@@ -237,5 +258,42 @@ async def off(dev: SwidgetDevice):
|
|
|
237
258
|
return await dev.turn_off()
|
|
238
259
|
|
|
239
260
|
|
|
261
|
+
@cli.command()
|
|
262
|
+
@pass_dev
|
|
263
|
+
async def enable_debug_server(dev: SwidgetDevice):
|
|
264
|
+
"""Enable Debug Server"""
|
|
265
|
+
click.echo(f"Enabling debug server")
|
|
266
|
+
return await dev.enable_debug_server()
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
@cli.command()
|
|
270
|
+
@pass_dev
|
|
271
|
+
async def check_for_updates(dev: SwidgetDevice):
|
|
272
|
+
click.echo("Contacting Swidget servers to fetch for updates...")
|
|
273
|
+
available_updates = await dev.check_for_updates()
|
|
274
|
+
if len(available_updates['updates']) == 0:
|
|
275
|
+
click.echo("No available updates")
|
|
276
|
+
else:
|
|
277
|
+
click.echo("The following versions are available to update to")
|
|
278
|
+
for version in available_updates['updates']:
|
|
279
|
+
click.echo(click.style(f"\t+ {version}", fg="green"))
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
@cli.command()
|
|
283
|
+
@click.option("--version", required=False)
|
|
284
|
+
@pass_dev
|
|
285
|
+
async def upgrade(dev: SwidgetDevice, version: str):
|
|
286
|
+
if version is None:
|
|
287
|
+
click.echo("Contacting Swidget servers to fetch for latest version")
|
|
288
|
+
available_updates = await dev.check_for_updates()
|
|
289
|
+
if len(available_updates['updates']) == 0:
|
|
290
|
+
click.echo("No available updates")
|
|
291
|
+
else:
|
|
292
|
+
version = available_updates[-1]
|
|
293
|
+
click.echo(f"Upgrading to version: {version}")
|
|
294
|
+
response = await dev.update_version(version)
|
|
295
|
+
click.echo(response)
|
|
296
|
+
|
|
297
|
+
|
|
240
298
|
if __name__ == "__main__":
|
|
241
299
|
cli()
|
|
@@ -39,6 +39,10 @@ class SwidgetProtocol(ssdp.SimpleServiceDiscoveryProtocol):
|
|
|
39
39
|
insert_type = headers["SERVER"].split(" ")[1].split("+")[1].split("/")[0]
|
|
40
40
|
friendly_name = headers["SERVER"].split("/")[2].strip('"')
|
|
41
41
|
devices[mac_address] = SwidgetDiscoveredDevice(mac_address, ip_address, friendly_name)
|
|
42
|
+
<<<<<<< HEAD
|
|
43
|
+
=======
|
|
44
|
+
_LOGGER.debug(f"Swidget device '{friendly_name}' at {ip_address}")
|
|
45
|
+
>>>>>>> 690a0c560a8b2ff39245d1a8354ced968d79e5de
|
|
42
46
|
|
|
43
47
|
|
|
44
48
|
async def discover_devices(timeout=RESPONSE_SEC):
|
|
@@ -64,19 +68,26 @@ async def discover_devices(timeout=RESPONSE_SEC):
|
|
|
64
68
|
return devices
|
|
65
69
|
|
|
66
70
|
|
|
67
|
-
async def discover_single(host: str, token_name: str, password: str,
|
|
71
|
+
async def discover_single(host: str, token_name: str, password: str, use_https: bool, use_websockets: bool) -> SwidgetDevice:
|
|
68
72
|
"""Discover a single device by the given IP address.
|
|
69
73
|
|
|
70
74
|
:param host: Hostname of device to query
|
|
71
75
|
:rtype: SwidgetDevice
|
|
72
76
|
:return: Object for querying/controlling found device.
|
|
73
77
|
"""
|
|
74
|
-
|
|
78
|
+
_LOGGER.debug(f"Checking for device at {host}")
|
|
79
|
+
swidget_device = SwidgetDevice(host, token_name, password, use_https, use_websockets=False)
|
|
80
|
+
_LOGGER.debug(f"Asking {host} for summary data")
|
|
75
81
|
await swidget_device.get_summary()
|
|
76
82
|
device_type = swidget_device.device_type
|
|
83
|
+
_LOGGER.debug(f"{host} is of type {device_type}")
|
|
84
|
+
await swidget_device.stop()
|
|
85
|
+
|
|
86
|
+
_LOGGER.debug(f"Creating new device class of type: {device_type}")
|
|
77
87
|
device_class = _get_device_class(device_type)
|
|
78
|
-
|
|
79
|
-
|
|
88
|
+
_LOGGER.debug(f"{device_class}")
|
|
89
|
+
dev = device_class(host, token_name, password, use_https, use_websockets)
|
|
90
|
+
await dev.start()
|
|
80
91
|
return dev
|
|
81
92
|
|
|
82
93
|
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
import requests
|
|
5
|
+
import urllib3
|
|
6
|
+
|
|
7
|
+
urllib3.disable_warnings()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DeviceConnectionResult(str, Enum):
|
|
11
|
+
NoStarted = "NotInitiated",
|
|
12
|
+
InProgress ="AttemptingConnect",
|
|
13
|
+
Success = "Success",
|
|
14
|
+
AuthenticationFailure = "AuthFail",
|
|
15
|
+
SSIDNotFound = "SSIDNotFound",
|
|
16
|
+
NoIp = "NoIpReceived",
|
|
17
|
+
ConnectionFailure = "FailedToConnect"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def send_credentials(device_name, ssid, network_password, secret_key):
|
|
21
|
+
payload = {"name": device_name,
|
|
22
|
+
"ssid": ssid,
|
|
23
|
+
"password": network_password,
|
|
24
|
+
"secretKey": secret_key}
|
|
25
|
+
url = "https://10.123.45.1/network"
|
|
26
|
+
sentProvisionRequestAttempts = 0
|
|
27
|
+
|
|
28
|
+
while True:
|
|
29
|
+
print(f"Provision Attempt: {sentProvisionRequestAttempts}")
|
|
30
|
+
if sentProvisionRequestAttempts >= 5:
|
|
31
|
+
print(f"Provision failed, Ensure you're connected to the device's hotspot.")
|
|
32
|
+
return False, None
|
|
33
|
+
try:
|
|
34
|
+
initial_provision = requests.post(url, json=payload, verify=False)
|
|
35
|
+
if initial_provision.status_code == 200:
|
|
36
|
+
print(f"Provision Success")
|
|
37
|
+
return True, initial_provision.json()["secretKey"]
|
|
38
|
+
else:
|
|
39
|
+
print(f"Provision Failed: {initial_provision.json()}")
|
|
40
|
+
return False, None
|
|
41
|
+
except Exception as e:
|
|
42
|
+
print("Error", e)
|
|
43
|
+
time.sleep(5)
|
|
44
|
+
sentProvisionRequestAttempts += 1
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def verify_connect_result(key):
|
|
48
|
+
headers = {'x-secret-key': key}
|
|
49
|
+
connect_success = False
|
|
50
|
+
connect_verification_attempts = 0
|
|
51
|
+
while not connect_success:
|
|
52
|
+
url = "https://10.123.45.1/network"
|
|
53
|
+
if connect_verification_attempts >= 5:
|
|
54
|
+
return False, None, None, "Failed to connect to device"
|
|
55
|
+
try:
|
|
56
|
+
verify_connection = requests.get(url, headers=headers, verify=False)
|
|
57
|
+
print(f"Verify Response: {verify_connection.json()}")
|
|
58
|
+
if verify_connection.status_code == 200:
|
|
59
|
+
print(f"Verification request success: {verify_connection.json()}")
|
|
60
|
+
connect_status = verify_connection.json()["status"]
|
|
61
|
+
if connect_status == DeviceConnectionResult.Success:
|
|
62
|
+
return True, verify_connection.json()["ip"], verify_connection.json()["mac"], None
|
|
63
|
+
elif connect_status == DeviceConnectionResult.InProgress:
|
|
64
|
+
pass
|
|
65
|
+
else:
|
|
66
|
+
return False, None, None, connect_status
|
|
67
|
+
else:
|
|
68
|
+
print(f"Verification request failure: {verify_connection.json()}")
|
|
69
|
+
time.sleep(5)
|
|
70
|
+
except:
|
|
71
|
+
time.sleep(5)
|
|
72
|
+
connect_verification_attempts += 1
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def provision_wifi(device_name, ssid, network_password, secret_key):
|
|
76
|
+
print(f"Device_name: {device_name}")
|
|
77
|
+
print(f"SSID: {ssid}")
|
|
78
|
+
print(f"Network Password: <redacted>")
|
|
79
|
+
print(f"Swidget Secret Key: <redacted>")
|
|
80
|
+
|
|
81
|
+
send_success, key = send_credentials(device_name, ssid, network_password, secret_key)
|
|
82
|
+
if not send_success:
|
|
83
|
+
return False
|
|
84
|
+
secret_key = key # we set the key to whatever was returned from the device. this handles the case of an empty string (which causes insert to generate a key)
|
|
85
|
+
|
|
86
|
+
verify_success, ip, mac, errorMessage = verify_connect_result(secret_key)
|
|
87
|
+
if not verify_success:
|
|
88
|
+
print(f"Verify Error: {errorMessage}")
|
|
89
|
+
return False
|
|
90
|
+
print(f"Verified Connection: {ip} {mac}")
|
|
91
|
+
headers = {'x-secret-key': secret_key}
|
|
92
|
+
# the device will remain in AP mode for 60 seconds before switching over to control mode. This is to allow time to send requests to update the device
|
|
93
|
+
# without swiching networks.
|
|
94
|
+
|
|
95
|
+
# issuing the setup_complete request will skip the 60s time
|
|
96
|
+
complete_setup = requests.get("https://10.123.45.1/setup_complete", headers=headers, verify=False)
|
|
97
|
+
if complete_setup.status_code == 200:
|
|
98
|
+
print("Setup complete, enabled control server")
|
|
99
|
+
else:
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
#alternatively, sleep for some time
|
|
103
|
+
print("Device has been configured, switch your wifi network off `Swidget-` now")
|
|
104
|
+
time.sleep(30)
|
|
105
|
+
|
|
106
|
+
# must connect to the main network before issuing control requests
|
|
107
|
+
getNameSuccess = False
|
|
108
|
+
while not getNameSuccess:
|
|
109
|
+
try:
|
|
110
|
+
url = f"https://{ip}/api/v1/name"
|
|
111
|
+
verify_name = requests.get(url, headers=headers, verify=False).json()
|
|
112
|
+
print(f"Verifying device name has been set: {verify_name}. Provisioning complete")
|
|
113
|
+
return True
|
|
114
|
+
except:
|
|
115
|
+
print("Connect to provided network")
|
|
116
|
+
time.sleep(5)
|
|
@@ -3,6 +3,7 @@ import logging
|
|
|
3
3
|
import time
|
|
4
4
|
|
|
5
5
|
from aiohttp import ClientSession, TCPConnector
|
|
6
|
+
import asyncio
|
|
6
7
|
from enum import Enum
|
|
7
8
|
from typing import Dict, List, Set
|
|
8
9
|
|
|
@@ -11,7 +12,6 @@ from .websocket import SwidgetWebsocket
|
|
|
11
12
|
|
|
12
13
|
_LOGGER = logging.getLogger(__name__)
|
|
13
14
|
|
|
14
|
-
|
|
15
15
|
class DeviceType(Enum):
|
|
16
16
|
"""Device type enum."""
|
|
17
17
|
|
|
@@ -24,16 +24,19 @@ class DeviceType(Enum):
|
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
class SwidgetDevice:
|
|
27
|
-
def __init__(self, host, token_name, secret_key,
|
|
27
|
+
def __init__(self, host, token_name, secret_key, use_https=True, use_websockets=True):
|
|
28
28
|
self.token_name = token_name
|
|
29
29
|
self.ip_address = host
|
|
30
|
-
|
|
30
|
+
|
|
31
|
+
self.use_https = use_https
|
|
32
|
+
self.uri_scheme = 'https' if self.use_https is True else 'http'
|
|
31
33
|
self.secret_key = secret_key
|
|
32
34
|
self.use_websockets = use_websockets
|
|
33
35
|
self.device_type = DeviceType.Unknown
|
|
34
36
|
self._friendly_name = "Unknown Swidget Device"
|
|
35
|
-
headers = {self.token_name: self.secret_key
|
|
36
|
-
|
|
37
|
+
headers = {self.token_name: self.secret_key,
|
|
38
|
+
'Connection': 'keep-alive'}
|
|
39
|
+
connector = TCPConnector(verify_ssl=False, force_close=True)
|
|
37
40
|
self._session = ClientSession(headers=headers, connector=connector)
|
|
38
41
|
self._last_update = None
|
|
39
42
|
if self.use_websockets:
|
|
@@ -45,33 +48,62 @@ class SwidgetDevice:
|
|
|
45
48
|
session=self._session)
|
|
46
49
|
|
|
47
50
|
def get_websocket(self):
|
|
48
|
-
|
|
51
|
+
if self.use_websockets:
|
|
52
|
+
return self._websocket
|
|
53
|
+
return None
|
|
49
54
|
|
|
50
55
|
def set_countdown_timer(self, minutes):
|
|
51
56
|
raise NotImplementedError()
|
|
52
57
|
|
|
53
|
-
def
|
|
58
|
+
async def connect(self):
|
|
59
|
+
await self._websocket.connect()
|
|
60
|
+
|
|
61
|
+
async def start(self):
|
|
62
|
+
"""Start the websocket."""
|
|
63
|
+
_LOGGER.debug("SwidgetDevice.start()")
|
|
64
|
+
if self.use_websockets:
|
|
65
|
+
_LOGGER.debug("Calling self._websocket.connect()")
|
|
66
|
+
await self._websocket.connect()
|
|
67
|
+
_LOGGER.debug("Calling self._websocket.listen() ")
|
|
68
|
+
asyncio.create_task(self._websocket.listen())
|
|
69
|
+
|
|
70
|
+
async def stop(self):
|
|
54
71
|
"""Stop the websocket."""
|
|
55
|
-
|
|
56
|
-
|
|
72
|
+
_LOGGER.debug("SwidgetDevice.stop()")
|
|
73
|
+
if hasattr(self, '_websocket'):
|
|
74
|
+
await self._websocket.close()
|
|
75
|
+
await self._session.close()
|
|
57
76
|
|
|
58
77
|
async def message_callback(self, message):
|
|
59
78
|
"""Entrypoint for a websocket callback"""
|
|
79
|
+
_LOGGER.debug("SwidgetDevice.message_callback() called")
|
|
60
80
|
if message["request_id"] == "summary":
|
|
81
|
+
_LOGGER.debug("Calling SwidgetDevice.process_summary()")
|
|
61
82
|
await self.process_summary(message)
|
|
62
83
|
elif message["request_id"] == "state" or message["request_id"] == "DYNAMIC_UPDATE" or message["request_id"] == "command":
|
|
84
|
+
_LOGGER.debug("Calling SwidgetDevice.process_state()")
|
|
63
85
|
await self.process_state(message)
|
|
86
|
+
else:
|
|
87
|
+
_LOGGER.error(f"Unknown message type from websocket. Type given was: {message["request_id"]}")
|
|
64
88
|
|
|
65
89
|
async def get_summary(self):
|
|
66
90
|
"""Get a summary of the device over HTTP"""
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
91
|
+
_LOGGER.debug("SwidgetDevice.get_summary() called")
|
|
92
|
+
if self.use_websockets:
|
|
93
|
+
_LOGGER.debug("In websocket mode. Sending get_summary() command over websocket")
|
|
94
|
+
await self._websocket.send_str(json.dumps({"type": "summary", "request_id": "summary"}))
|
|
95
|
+
else:
|
|
96
|
+
_LOGGER.debug("In http mode. Sending get_summary() command over http")
|
|
97
|
+
async with self._session.get(
|
|
98
|
+
url=f"{self.uri_scheme}://{self.ip_address}/api/v1/summary", ssl=False
|
|
99
|
+
) as response:
|
|
100
|
+
summary = await response.json()
|
|
101
|
+
await self.process_summary(summary)
|
|
72
102
|
|
|
73
103
|
async def process_summary(self, summary):
|
|
74
104
|
""" Process the data around the summary of the device"""
|
|
105
|
+
_LOGGER.debug("SwidgetDevice.process_summary() called")
|
|
106
|
+
_LOGGER.debug(f"Summary to process: {summary}")
|
|
75
107
|
self.model = summary["model"]
|
|
76
108
|
self.mac_address = summary["mac"]
|
|
77
109
|
self.version = summary["version"]
|
|
@@ -85,9 +117,10 @@ class SwidgetDevice:
|
|
|
85
117
|
self._last_update = int(time.time())
|
|
86
118
|
|
|
87
119
|
async def get_friendly_name(self):
|
|
120
|
+
_LOGGER.debug("SwidgetDevice.get_friendly_name() called")
|
|
88
121
|
try:
|
|
89
122
|
async with self._session.get(
|
|
90
|
-
url=f"
|
|
123
|
+
url=f"{self.uri_scheme}://{self.ip_address}/api/v1/name", ssl=False
|
|
91
124
|
) as response:
|
|
92
125
|
name = await response.json()
|
|
93
126
|
except Exception:
|
|
@@ -95,24 +128,33 @@ class SwidgetDevice:
|
|
|
95
128
|
await self.process_friendly_name(name['name'])
|
|
96
129
|
|
|
97
130
|
async def process_friendly_name(self, name):
|
|
131
|
+
_LOGGER.debug("SwidgetDevice.process_friendly_name() called")
|
|
98
132
|
self._friendly_name = name
|
|
133
|
+
self._last_update = int(time.time())
|
|
99
134
|
|
|
100
135
|
async def get_state(self):
|
|
101
136
|
""" Get the state of the device over HTTP"""
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
137
|
+
_LOGGER.debug("SwidgetDevice.get_state() called")
|
|
138
|
+
if self.use_websockets:
|
|
139
|
+
_LOGGER.debug("In websocket mode. Sending get_summary() command over websocket")
|
|
140
|
+
await self._websocket.send_str(json.dumps({"type": "state", "request_id": "state"}))
|
|
141
|
+
else:
|
|
142
|
+
_LOGGER.debug("In http mode. Sending get_summary() command over http")
|
|
143
|
+
async with self._session.get(
|
|
144
|
+
url=f"{self.uri_scheme}://{self.ip_address}/api/v1/state", ssl=False
|
|
145
|
+
) as response:
|
|
146
|
+
state = await response.json()
|
|
147
|
+
await self.process_state(state)
|
|
107
148
|
|
|
108
149
|
async def process_state(self, state):
|
|
109
150
|
""" Process any information about the state of the device or insert"""
|
|
110
151
|
# State is not always in the state (during callback)
|
|
152
|
+
_LOGGER.debug("SwidgetDevice.process_state() called")
|
|
153
|
+
_LOGGER.debug(f"State to process: {state}")
|
|
111
154
|
try:
|
|
112
155
|
self.rssi = state["connection"]["rssi"]
|
|
113
156
|
except:
|
|
114
157
|
pass
|
|
115
|
-
|
|
116
158
|
for assembly in self.assemblies:
|
|
117
159
|
for id, component in self.assemblies[assembly].components.items():
|
|
118
160
|
try:
|
|
@@ -122,36 +164,49 @@ class SwidgetDevice:
|
|
|
122
164
|
self._last_update = int(time.time())
|
|
123
165
|
|
|
124
166
|
async def update(self):
|
|
167
|
+
_LOGGER.debug("SwidgetDevice.update() called")
|
|
125
168
|
if self._last_update is None:
|
|
126
169
|
_LOGGER.debug("Performing the initial update to obtain sysinfo")
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
170
|
+
await self.get_summary()
|
|
171
|
+
await self.get_state()
|
|
172
|
+
if self._friendly_name == "Unknown Swidget Device":
|
|
173
|
+
await self.get_friendly_name()
|
|
174
|
+
elif (int(time.time()) - self._last_update) < 5:
|
|
175
|
+
_LOGGER.debug("update() recently called, not executing")
|
|
176
|
+
else:
|
|
177
|
+
_LOGGER.debug("Requesting an update of the device")
|
|
178
|
+
await self.get_summary()
|
|
179
|
+
await self.get_state()
|
|
130
180
|
|
|
131
181
|
async def send_config(self, payload: dict):
|
|
132
|
-
|
|
182
|
+
_LOGGER.debug("SwidgetDevice.send_config() called")
|
|
183
|
+
data = json.dumps({"type":"config","request_id":"send_config", "payload": payload})
|
|
133
184
|
await self._websocket.send_str(data)
|
|
134
185
|
|
|
135
186
|
async def send_command(
|
|
136
187
|
self, assembly: str, component: str, function: str, command: dict
|
|
137
188
|
):
|
|
189
|
+
_LOGGER.debug("SwidgetDevice.send_command() called")
|
|
138
190
|
"""Send a command to the Swidget device either using a HTTP call or the existing websocket"""
|
|
139
191
|
data = {assembly: {"components": {component: {function: command}}}}
|
|
140
|
-
|
|
192
|
+
_LOGGER.debug(f"Command to send: {data}")
|
|
141
193
|
if self.use_websockets:
|
|
194
|
+
_LOGGER.debug("In websocket mode. Sending command over websocket")
|
|
142
195
|
data = json.dumps({"type": "command",
|
|
143
196
|
"request_id": "command",
|
|
144
197
|
"payload": data
|
|
145
198
|
})
|
|
146
199
|
await self._websocket.send_str(data)
|
|
147
200
|
else:
|
|
201
|
+
_LOGGER.debug("NOT in websocket mode, sending command over HTTP")
|
|
148
202
|
async with self._session.post(
|
|
149
|
-
url=f"
|
|
150
|
-
ssl=
|
|
203
|
+
url=f"{self.uri_scheme}://{self.ip_address}/api/v1/command",
|
|
204
|
+
ssl=False,
|
|
151
205
|
data=json.dumps(data),
|
|
152
206
|
) as response:
|
|
153
207
|
state = await response.json()
|
|
154
208
|
|
|
209
|
+
# Do a hard set of the new state of the device. May change this in the future
|
|
155
210
|
function_value = state[assembly]["components"][component][function]
|
|
156
211
|
self.assemblies[assembly].components[component].functions[function] = function_value # fmt: skip
|
|
157
212
|
|
|
@@ -160,12 +215,13 @@ class SwidgetDevice:
|
|
|
160
215
|
|
|
161
216
|
:raises SwidgetException: Raise the exception if there we are unable to connect to the Swidget device
|
|
162
217
|
"""
|
|
218
|
+
_LOGGER.debug("SwidgetDevice.ping() called")
|
|
163
219
|
try:
|
|
164
220
|
async with self._session.get(
|
|
165
|
-
url=f"
|
|
166
|
-
ssl=
|
|
221
|
+
url=f"{self.uri_scheme}://{self.ip_address}/ping",
|
|
222
|
+
ssl=False
|
|
167
223
|
) as response:
|
|
168
|
-
return response.
|
|
224
|
+
return response.status
|
|
169
225
|
except:
|
|
170
226
|
raise SwidgetException
|
|
171
227
|
|
|
@@ -174,12 +230,76 @@ class SwidgetDevice:
|
|
|
174
230
|
|
|
175
231
|
:raises SwidgetException: Raise the exception if there we are unable to connect to the Swidget device
|
|
176
232
|
"""
|
|
233
|
+
_LOGGER.debug("SwidgetDevice.blink() called")
|
|
177
234
|
try:
|
|
178
235
|
async with self._session.get(
|
|
179
|
-
url=f"
|
|
180
|
-
ssl=
|
|
236
|
+
url=f"{self.uri_scheme}://{self.ip_address}/blink",
|
|
237
|
+
ssl=False
|
|
181
238
|
) as response:
|
|
182
|
-
return response.
|
|
239
|
+
return await response.json()
|
|
240
|
+
except:
|
|
241
|
+
raise SwidgetException
|
|
242
|
+
|
|
243
|
+
async def enable_debug_server(self):
|
|
244
|
+
"""Enable the Swidget local debug server
|
|
245
|
+
|
|
246
|
+
:raises SwidgetException: Raise the exception if there we are unable to connect to the Swidget device
|
|
247
|
+
"""
|
|
248
|
+
_LOGGER.debug("SwidgetDevice.enable_debug_server() called")
|
|
249
|
+
try:
|
|
250
|
+
async with self._session.get(
|
|
251
|
+
url=f"{self.uri_scheme}://{self.ip_address}/debug?x-secret-key={self.secret_key}",
|
|
252
|
+
ssl=False
|
|
253
|
+
) as response:
|
|
254
|
+
return await response.json()
|
|
255
|
+
except:
|
|
256
|
+
raise SwidgetException
|
|
257
|
+
|
|
258
|
+
async def factory_reset(self):
|
|
259
|
+
"""Factory reset the Swidget device
|
|
260
|
+
|
|
261
|
+
:raises SwidgetException: Raise the exception if there we are unable to connect to the Swidget device
|
|
262
|
+
"""
|
|
263
|
+
try:
|
|
264
|
+
|
|
265
|
+
async with self._session.delete(
|
|
266
|
+
url=f"{self.uri_scheme}://{self.ip_address}/api/v1/reset",
|
|
267
|
+
ssl=False
|
|
268
|
+
) as response:
|
|
269
|
+
return await response.json()
|
|
270
|
+
except:
|
|
271
|
+
raise SwidgetException
|
|
272
|
+
|
|
273
|
+
async def check_for_updates(self):
|
|
274
|
+
"""Tell the device to contact the Swidget servers to see if there is an available update
|
|
275
|
+
|
|
276
|
+
:raises SwidgetException: Raise the exception if there we are unable to connect to the Swidget device
|
|
277
|
+
"""
|
|
278
|
+
try:
|
|
279
|
+
|
|
280
|
+
async with self._session.get(
|
|
281
|
+
url=f"{self.uri_scheme}://{self.ip_address}/api/v1/update",
|
|
282
|
+
ssl=False
|
|
283
|
+
) as response:
|
|
284
|
+
return await response.json()
|
|
285
|
+
except:
|
|
286
|
+
raise SwidgetException
|
|
287
|
+
|
|
288
|
+
async def update_version(self, version):
|
|
289
|
+
"""Tell the device to download and apply an update
|
|
290
|
+
|
|
291
|
+
:raises SwidgetException: Raise the exception if there we are unable to connect to the Swidget device
|
|
292
|
+
"""
|
|
293
|
+
try:
|
|
294
|
+
data = {
|
|
295
|
+
"version": version
|
|
296
|
+
}
|
|
297
|
+
async with self._session.post(
|
|
298
|
+
url=f"{self.uri_scheme}://{self.ip_address}/api/v1/update",
|
|
299
|
+
ssl=False,
|
|
300
|
+
data=json.dumps(data)
|
|
301
|
+
) as response:
|
|
302
|
+
return await response
|
|
183
303
|
except:
|
|
184
304
|
raise SwidgetException
|
|
185
305
|
|
|
@@ -202,7 +322,7 @@ class SwidgetDevice:
|
|
|
202
322
|
"rssi": self.rssi
|
|
203
323
|
}
|
|
204
324
|
|
|
205
|
-
def get_child_consumption(self, plug_id=0):
|
|
325
|
+
async def get_child_consumption(self, plug_id=0):
|
|
206
326
|
"""Get the power consumption of a plug in watts."""
|
|
207
327
|
if plug_id == "all":
|
|
208
328
|
return_dict = {}
|
|
@@ -222,7 +342,7 @@ class SwidgetDevice:
|
|
|
222
342
|
return total_consumption
|
|
223
343
|
|
|
224
344
|
@property
|
|
225
|
-
def realtime_values(self):
|
|
345
|
+
async def realtime_values(self):
|
|
226
346
|
"""Get a dict of realtime value attributes from the insert and host
|
|
227
347
|
|
|
228
348
|
:return: A dictionary of insert sensor values and power consumption values
|
|
@@ -232,7 +352,7 @@ class SwidgetDevice:
|
|
|
232
352
|
for feature in self.insert_features:
|
|
233
353
|
return_dict.update(self.get_function_values(feature))
|
|
234
354
|
return_dict.update({'rssi': self.rssi})
|
|
235
|
-
power_values = self.get_child_consumption("all")
|
|
355
|
+
power_values = await self.get_child_consumption("all")
|
|
236
356
|
if power_values:
|
|
237
357
|
return_dict.update(power_values)
|
|
238
358
|
return return_dict
|
|
@@ -295,7 +415,7 @@ class SwidgetDevice:
|
|
|
295
415
|
@property
|
|
296
416
|
def is_dimmable(self) -> bool:
|
|
297
417
|
"""Return True if the device is dimmable."""
|
|
298
|
-
return
|
|
418
|
+
return self.is_dimmer
|
|
299
419
|
|
|
300
420
|
@property # type: ignore
|
|
301
421
|
def friendly_name(self) -> str:
|
|
@@ -312,11 +432,14 @@ class SwidgetDevice:
|
|
|
312
432
|
|
|
313
433
|
async def turn_on(self):
|
|
314
434
|
"""Turn the device on."""
|
|
435
|
+
_LOGGER.debug("SwidgetDevice.turn_on() called")
|
|
315
436
|
await self.send_command(
|
|
316
437
|
assembly="host", component="0", function="toggle", command={"state": "on"}
|
|
317
438
|
)
|
|
439
|
+
|
|
318
440
|
async def turn_off(self):
|
|
319
441
|
"""Turn the device off."""
|
|
442
|
+
_LOGGER.debug("SwidgetDevice.turn_off() called")
|
|
320
443
|
await self.send_command(
|
|
321
444
|
assembly="host", component="0", function="toggle", command={"state": "off"}
|
|
322
445
|
)
|
|
@@ -346,10 +469,6 @@ class SwidgetDevice:
|
|
|
346
469
|
return f"<{self.device_type} at {self.ip_address} - update() needed>"
|
|
347
470
|
return f"<{self.device_type} model {self.model} at {self.ip_address}>"
|
|
348
471
|
|
|
349
|
-
def __del__(self):
|
|
350
|
-
if self.use_websockets:
|
|
351
|
-
self.stop()
|
|
352
|
-
|
|
353
472
|
|
|
354
473
|
class SwidgetAssembly:
|
|
355
474
|
def __init__(self, summary: dict):
|
|
@@ -363,4 +482,4 @@ class SwidgetAssembly:
|
|
|
363
482
|
|
|
364
483
|
class SwidgetComponent:
|
|
365
484
|
def __init__(self, functions):
|
|
366
|
-
self.functions = {f: None for f in functions}
|
|
485
|
+
self.functions = {f: None for f in functions}
|
|
@@ -6,14 +6,13 @@ from swidget.swidgetdevice import (
|
|
|
6
6
|
)
|
|
7
7
|
from swidget.exceptions import SwidgetException
|
|
8
8
|
|
|
9
|
+
_LOGGER = logging.getLogger(__name__)
|
|
9
10
|
|
|
10
11
|
|
|
11
|
-
log = logging.getLogger(__name__)
|
|
12
|
-
|
|
13
12
|
class SwidgetDimmer(SwidgetDevice):
|
|
14
13
|
|
|
15
|
-
def __init__(self, host, token_name: str, secret_key: str,
|
|
16
|
-
super().__init__(host=host, token_name=token_name, secret_key=secret_key,
|
|
14
|
+
def __init__(self, host, token_name: str, secret_key: str, use_https: bool, use_websockets: bool) -> None:
|
|
15
|
+
super().__init__(host=host, token_name=token_name, secret_key=secret_key, use_https=use_https, use_websockets=use_websockets)
|
|
17
16
|
self._device_type = "dimmer"
|
|
18
17
|
|
|
19
18
|
@property # type: ignore
|
|
@@ -31,11 +30,13 @@ class SwidgetDimmer(SwidgetDevice):
|
|
|
31
30
|
|
|
32
31
|
async def set_brightness(self, brightness):
|
|
33
32
|
"""Set the brightness of the device."""
|
|
33
|
+
_LOGGER.debug("SwidgetDimmer.set_brightness() called")
|
|
34
34
|
await self.send_command(
|
|
35
35
|
assembly="host", component="0", function="level", command={"now": brightness}
|
|
36
36
|
)
|
|
37
37
|
|
|
38
38
|
async def set_default_brightness(self, brightness):
|
|
39
|
+
_LOGGER.debug("SwidgetDimmer.set_default_brightness() called")
|
|
39
40
|
await self.send_command(
|
|
40
41
|
assembly="host", component="0", function="level", command={"default": brightness}
|
|
41
42
|
)
|
|
@@ -6,6 +6,6 @@ from swidget.swidgetdevice import (
|
|
|
6
6
|
|
|
7
7
|
class SwidgetOutlet(SwidgetDevice):
|
|
8
8
|
|
|
9
|
-
def __init__(self, host, token_name: str, secret_key: str,
|
|
10
|
-
super().__init__(host=host, token_name=token_name, secret_key=secret_key,
|
|
9
|
+
def __init__(self, host, token_name: str, secret_key: str, use_https: bool, use_websockets: bool) -> None:
|
|
10
|
+
super().__init__(host=host, token_name=token_name, secret_key=secret_key, use_https=use_https, use_websockets=use_websockets)
|
|
11
11
|
self._device_type = DeviceType.Outlet
|
|
@@ -6,8 +6,8 @@ from swidget.swidgetdevice import (
|
|
|
6
6
|
|
|
7
7
|
class SwidgetSwitch(SwidgetDevice):
|
|
8
8
|
|
|
9
|
-
def __init__(self, host, token_name: str, secret_key: str,
|
|
10
|
-
super().__init__(host=host, token_name=token_name, secret_key=secret_key,
|
|
9
|
+
def __init__(self, host, token_name: str, secret_key: str, use_https: bool, use_websockets: bool) -> None:
|
|
10
|
+
super().__init__(host=host, token_name=token_name, secret_key=secret_key, use_https=use_https, use_websockets=use_websockets)
|
|
11
11
|
self._device_type = DeviceType.Switch
|
|
12
12
|
|
|
13
13
|
async def current_consumption(self) -> float:
|
|
@@ -1,17 +1,21 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
from swidget.swidgetdevice import (
|
|
2
3
|
DeviceType,
|
|
3
4
|
)
|
|
4
5
|
from swidget.swidgetswitch import SwidgetSwitch
|
|
5
6
|
|
|
7
|
+
_LOGGER = logging.getLogger(__name__)
|
|
8
|
+
|
|
6
9
|
|
|
7
10
|
class SwidgetTimerSwitch(SwidgetSwitch):
|
|
8
11
|
|
|
9
|
-
def __init__(self, host, token_name: str, secret_key: str,
|
|
10
|
-
super().__init__(host=host, token_name=token_name, secret_key=secret_key,
|
|
12
|
+
def __init__(self, host, token_name: str, secret_key: str, use_https: bool, use_websockets: bool) -> None:
|
|
13
|
+
super().__init__(host=host, token_name=token_name, secret_key=secret_key, use_https=use_https, use_websockets=use_websockets)
|
|
11
14
|
self._device_type = DeviceType.TimerSwitch
|
|
12
15
|
|
|
13
16
|
async def set_countdown_timer(self, minutes):
|
|
14
17
|
"""Set the countdown timer."""
|
|
18
|
+
_LOGGER.debug("SwidgetTimerSwitch.set_brightness() called")
|
|
15
19
|
await self.send_command(
|
|
16
20
|
assembly="host", component="0", function="timer", command={"duration": minutes}
|
|
17
21
|
)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import aiohttp
|
|
2
|
+
import logging
|
|
3
|
+
import socket
|
|
4
|
+
|
|
5
|
+
_LOGGER = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
class SwidgetWebsocket:
|
|
8
|
+
"""A websocket connection to a Swidget Device"""
|
|
9
|
+
|
|
10
|
+
# pylint: disable=too-many-instance-attributes
|
|
11
|
+
_client: aiohttp.ClientWebSocketResponse | None = None
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
host,
|
|
16
|
+
token_name,
|
|
17
|
+
secret_key,
|
|
18
|
+
callback,
|
|
19
|
+
session=None,
|
|
20
|
+
use_security=True,
|
|
21
|
+
):
|
|
22
|
+
|
|
23
|
+
self.session = session or aiohttp.ClientSession()
|
|
24
|
+
self.use_security = use_security
|
|
25
|
+
self.uri = self.get_uri(host, token_name, secret_key)
|
|
26
|
+
self.callback = callback
|
|
27
|
+
self._verify_ssl = False
|
|
28
|
+
self._state = None
|
|
29
|
+
self.failed_attempts = 0
|
|
30
|
+
self._error_reason = None
|
|
31
|
+
self.headers = {'Connection': 'Upgrade'}
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def connected(self) -> bool:
|
|
35
|
+
return self._client is not None and not self._client.closed
|
|
36
|
+
|
|
37
|
+
def get_uri(self, host, token_name, secret_key):
|
|
38
|
+
"""Generate the websocket URI"""
|
|
39
|
+
if self.use_security:
|
|
40
|
+
return f"wss://{host}/api/v1/sock?{token_name}={secret_key}"
|
|
41
|
+
else:
|
|
42
|
+
return f"ws://{host}/api/v1/sock?{token_name}={secret_key}"
|
|
43
|
+
|
|
44
|
+
async def connect(self) -> None:
|
|
45
|
+
_LOGGER.debug("websocket.connect() called")
|
|
46
|
+
if self.connected:
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
if not self.session:
|
|
50
|
+
raise
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
self._client = await self.session.ws_connect(url=self.uri, headers=self.headers, verify_ssl=self._verify_ssl, heartbeat=30)
|
|
54
|
+
except (
|
|
55
|
+
aiohttp.WSServerHandshakeError,
|
|
56
|
+
aiohttp.ClientConnectionError,
|
|
57
|
+
socket.gaierror,
|
|
58
|
+
) as exception:
|
|
59
|
+
msg = (
|
|
60
|
+
"Error occurred while communicating with WLED device"
|
|
61
|
+
f" on WebSocket at {self.host}"
|
|
62
|
+
)
|
|
63
|
+
raise
|
|
64
|
+
|
|
65
|
+
async def close(self) -> None:
|
|
66
|
+
_LOGGER.debug("websocket.close() called")
|
|
67
|
+
if not self._client or not self.connected:
|
|
68
|
+
return
|
|
69
|
+
await self._client.close()
|
|
70
|
+
|
|
71
|
+
async def send_str(self, message):
|
|
72
|
+
"""Send a message through the websocket."""
|
|
73
|
+
_LOGGER.debug("websocket.send_str() called")
|
|
74
|
+
message = str(message)
|
|
75
|
+
_LOGGER.debug(f"Sending messsage over websocket: {message}")
|
|
76
|
+
await self._client.send_str(f'{message}')
|
|
77
|
+
|
|
78
|
+
async def listen(self):
|
|
79
|
+
_LOGGER.debug("websocket.listen() called")
|
|
80
|
+
if not self._client or not self.connected:
|
|
81
|
+
raise
|
|
82
|
+
|
|
83
|
+
while not self._client.closed:
|
|
84
|
+
message = await self._client.receive()
|
|
85
|
+
|
|
86
|
+
if message.type == aiohttp.WSMsgType.ERROR:
|
|
87
|
+
raise
|
|
88
|
+
|
|
89
|
+
if message.type == aiohttp.WSMsgType.TEXT:
|
|
90
|
+
message_data = message.json()
|
|
91
|
+
_LOGGER.debug(f"Data received from websocket: {message_data}")
|
|
92
|
+
await self.callback(message_data)
|
|
93
|
+
|
|
94
|
+
if message.type in (
|
|
95
|
+
aiohttp.WSMsgType.CLOSE,
|
|
96
|
+
aiohttp.WSMsgType.CLOSED,
|
|
97
|
+
aiohttp.WSMsgType.CLOSING,
|
|
98
|
+
):
|
|
99
|
+
_LOGGER.debug("Connection to the Swidget WebSocket on has been closed")
|
python_swidget-0.0.31/README.md
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import time
|
|
2
|
-
|
|
3
|
-
import requests
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def provision_wifi(ssid, network_password, token_name, secret_key):
|
|
7
|
-
payload = {"ssid": ssid, "password": network_password, "secretKey": secret_key}
|
|
8
|
-
headers = {token_name: secret_key}
|
|
9
|
-
url = "https://10.123.45.1/network"
|
|
10
|
-
initial_provision = requests.post(url, data=payload, verify=False).json()
|
|
11
|
-
print(f"Response from setting {initial_provision}")
|
|
12
|
-
|
|
13
|
-
time.sleep(5)
|
|
14
|
-
url = "https://10.123.45.1/network"
|
|
15
|
-
verify_connection = requests.get(url, headers=headers, verify=False).json()
|
|
16
|
-
print(f"Verification request data: {verify_connection}")
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
from datetime import datetime
|
|
3
|
-
import logging
|
|
4
|
-
import json
|
|
5
|
-
|
|
6
|
-
import aiohttp
|
|
7
|
-
|
|
8
|
-
_LOGGER = logging.getLogger(__name__)
|
|
9
|
-
|
|
10
|
-
ERROR_AUTH_FAILURE = "Authorization failure"
|
|
11
|
-
ERROR_TOO_MANY_RETRIES = "Too many retries"
|
|
12
|
-
ERROR_UNKNOWN = "Unknown"
|
|
13
|
-
|
|
14
|
-
MAX_FAILED_ATTEMPTS = 5
|
|
15
|
-
|
|
16
|
-
STATE_CONNECTED = "connected"
|
|
17
|
-
STATE_DISCONNECTED = "disconnected"
|
|
18
|
-
STATE_STARTING = "starting"
|
|
19
|
-
STATE_STOPPED = "stopped"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class SwidgetWebsocket:
|
|
23
|
-
"""A websocket connection to a Swidget Device"""
|
|
24
|
-
|
|
25
|
-
# pylint: disable=too-many-instance-attributes
|
|
26
|
-
|
|
27
|
-
def __init__(
|
|
28
|
-
self,
|
|
29
|
-
host,
|
|
30
|
-
token_name,
|
|
31
|
-
secret_key,
|
|
32
|
-
callback,
|
|
33
|
-
session=None,
|
|
34
|
-
verify_ssl=False,
|
|
35
|
-
):
|
|
36
|
-
|
|
37
|
-
self.session = session or aiohttp.ClientSession()
|
|
38
|
-
self.uri = self._get_uri(host, token_name, secret_key)
|
|
39
|
-
self.callback = callback
|
|
40
|
-
self._ssl = False if verify_ssl is False else None
|
|
41
|
-
self._state = None
|
|
42
|
-
self.failed_attempts = 0
|
|
43
|
-
self._error_reason = None
|
|
44
|
-
|
|
45
|
-
@property
|
|
46
|
-
def state(self):
|
|
47
|
-
"""Return the current state."""
|
|
48
|
-
return self._state
|
|
49
|
-
|
|
50
|
-
@state.setter
|
|
51
|
-
def state(self, value):
|
|
52
|
-
"""Set the state."""
|
|
53
|
-
self._state = value
|
|
54
|
-
|
|
55
|
-
@staticmethod
|
|
56
|
-
def _get_uri(host, token_name, secret_key):
|
|
57
|
-
"""Generate the websocket URI"""
|
|
58
|
-
return f"wss://{host}/api/v1/sock?{token_name}={secret_key}"
|
|
59
|
-
|
|
60
|
-
async def running(self):
|
|
61
|
-
"""Open a persistent websocket connection and act on events."""
|
|
62
|
-
self.state = STATE_STARTING
|
|
63
|
-
|
|
64
|
-
try:
|
|
65
|
-
headers = {'Connection': 'Upgrade'}
|
|
66
|
-
async with self.session.ws_connect(self.uri, headers=headers, verify_ssl=False, heartbeat=30) as self.ws_client:
|
|
67
|
-
self.state = STATE_CONNECTED
|
|
68
|
-
self.failed_attempts = 0
|
|
69
|
-
self.send_str(json.dumps({"type": "summary", "request_id": "1"}))
|
|
70
|
-
self.send_str(json.dumps({"type": "state", "request_id": "2"}))
|
|
71
|
-
async for message in self.ws_client:
|
|
72
|
-
if self.state == STATE_STOPPED:
|
|
73
|
-
break
|
|
74
|
-
|
|
75
|
-
if message.type == aiohttp.WSMsgType.TEXT:
|
|
76
|
-
msg = message.json()
|
|
77
|
-
await self.callback(msg)
|
|
78
|
-
|
|
79
|
-
elif message.type == aiohttp.WSMsgType.CLOSED:
|
|
80
|
-
break
|
|
81
|
-
|
|
82
|
-
elif message.type == aiohttp.WSMsgType.ERROR:
|
|
83
|
-
break
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
except aiohttp.ClientResponseError as error:
|
|
88
|
-
if error.code == 401:
|
|
89
|
-
_LOGGER.error(f"Credentials rejected: {error}")
|
|
90
|
-
self._error_reason = ERROR_AUTH_FAILURE
|
|
91
|
-
else:
|
|
92
|
-
_LOGGER.error(f"Unexpected response received: {error}")
|
|
93
|
-
self._error_reason = ERROR_UNKNOWN
|
|
94
|
-
self.state = STATE_STOPPED
|
|
95
|
-
except (aiohttp.ClientConnectionError, asyncio.TimeoutError) as error:
|
|
96
|
-
if self.failed_attempts >= MAX_FAILED_ATTEMPTS:
|
|
97
|
-
self._error_reason = ERROR_TOO_MANY_RETRIES
|
|
98
|
-
self.state = STATE_STOPPED
|
|
99
|
-
elif self.state != STATE_STOPPED:
|
|
100
|
-
retry_delay = min(2 ** (self.failed_attempts - 1) * 30, 300)
|
|
101
|
-
self.failed_attempts += 1
|
|
102
|
-
self.state = STATE_DISCONNECTED
|
|
103
|
-
await asyncio.sleep(retry_delay)
|
|
104
|
-
except Exception as error: # pylint: disable=broad-except
|
|
105
|
-
if self.state != STATE_STOPPED:
|
|
106
|
-
_LOGGER.error(f"Unexpected exception occurred: {error}")
|
|
107
|
-
self._error_reason = ERROR_UNKNOWN
|
|
108
|
-
self.state = STATE_STOPPED
|
|
109
|
-
else:
|
|
110
|
-
if self.state != STATE_STOPPED:
|
|
111
|
-
self.state = STATE_DISCONNECTED
|
|
112
|
-
|
|
113
|
-
await asyncio.sleep(5)
|
|
114
|
-
|
|
115
|
-
async def send_str(self, message):
|
|
116
|
-
message = str(message)
|
|
117
|
-
await self.ws_client.send_str(f'{message}')
|
|
118
|
-
|
|
119
|
-
async def listen(self):
|
|
120
|
-
"""Close the listening websocket."""
|
|
121
|
-
self.failed_attempts = 0
|
|
122
|
-
while self.state != STATE_STOPPED:
|
|
123
|
-
await self.running()
|
|
124
|
-
|
|
125
|
-
def close(self):
|
|
126
|
-
"""Close the listening websocket."""
|
|
127
|
-
self.state = STATE_STOPPED
|
|
128
|
-
self.ws_client.close()
|
|
File without changes
|
|
File without changes
|