python-swidget 0.0.32__tar.gz → 1.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_swidget-0.0.32 → python_swidget-1.0.1}/PKG-INFO +19 -1
- python_swidget-1.0.1/README.md +21 -0
- {python_swidget-0.0.32 → python_swidget-1.0.1}/pyproject.toml +1 -1
- {python_swidget-0.0.32 → python_swidget-1.0.1}/swidget/cli.py +54 -7
- {python_swidget-0.0.32 → python_swidget-1.0.1}/swidget/discovery.py +9 -4
- {python_swidget-0.0.32 → python_swidget-1.0.1}/swidget/provision.py +1 -0
- {python_swidget-0.0.32 → python_swidget-1.0.1}/swidget/swidgetdevice.py +131 -38
- {python_swidget-0.0.32 → python_swidget-1.0.1}/swidget/swidgetdimmer.py +5 -4
- {python_swidget-0.0.32 → python_swidget-1.0.1}/swidget/swidgetoutlet.py +2 -2
- {python_swidget-0.0.32 → python_swidget-1.0.1}/swidget/swidgetswitch.py +2 -2
- {python_swidget-0.0.32 → python_swidget-1.0.1}/swidget/swidgettimerswitch.py +6 -2
- python_swidget-1.0.1/swidget/websocket.py +123 -0
- python_swidget-0.0.32/README.md +0 -2
- python_swidget-0.0.32/swidget/websocket.py +0 -124
- {python_swidget-0.0.32 → python_swidget-1.0.1}/swidget/__init__.py +0 -0
- {python_swidget-0.0.32 → python_swidget-1.0.1}/swidget/exceptions.py +0 -0
- {python_swidget-0.0.32 → python_swidget-1.0.1}/swidget/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: python-swidget
|
|
3
|
-
Version:
|
|
3
|
+
Version: 1.0.1
|
|
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
|
|
@@ -31,3 +31,21 @@ Description-Content-Type: text/markdown
|
|
|
31
31
|
# python-swidget
|
|
32
32
|
A library to manage Swidget smart devices
|
|
33
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
|
+
```
|
|
@@ -1,5 +1,4 @@
|
|
|
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
|
|
@@ -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,7 +58,7 @@ 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":
|
|
@@ -68,7 +71,6 @@ async def cli(ctx, host, password, debug, type):
|
|
|
68
71
|
|
|
69
72
|
if ctx.invoked_subcommand == "discover" or ctx.invoked_subcommand == "wifi":
|
|
70
73
|
return
|
|
71
|
-
|
|
72
74
|
if host is None:
|
|
73
75
|
click.echo("No host name given, trying discovery..")
|
|
74
76
|
await ctx.invoke(discover)
|
|
@@ -77,15 +79,16 @@ async def cli(ctx, host, password, debug, type):
|
|
|
77
79
|
dev = TYPE_TO_CLASS[type](host=host,
|
|
78
80
|
token_name='x-secret-key',
|
|
79
81
|
secret_key=password,
|
|
80
|
-
|
|
82
|
+
use_https=http_only,
|
|
81
83
|
use_websockets=False)
|
|
82
84
|
else:
|
|
83
85
|
click.echo("No --type defined, discovering...")
|
|
84
86
|
dev = await discover_single(host=host,
|
|
85
87
|
token_name='x-secret-key',
|
|
86
88
|
password=password,
|
|
87
|
-
|
|
89
|
+
use_https=http_only,
|
|
88
90
|
use_websockets=False)
|
|
91
|
+
await dev.update()
|
|
89
92
|
|
|
90
93
|
@asynccontextmanager
|
|
91
94
|
async def async_wrapped_device(dev: SwidgetDevice):
|
|
@@ -104,7 +107,7 @@ async def cli(ctx, host, password, debug, type):
|
|
|
104
107
|
def wifi():
|
|
105
108
|
"""Commands to control wifi settings."""
|
|
106
109
|
|
|
107
|
-
|
|
110
|
+
|
|
108
111
|
@wifi.command()
|
|
109
112
|
@click.option("--ssid", prompt=True, hide_input=False)
|
|
110
113
|
@click.option("--network_password", prompt=True, hide_input=True)
|
|
@@ -124,7 +127,7 @@ def join(ssid, network_password, secret_key, friendly_name):
|
|
|
124
127
|
|
|
125
128
|
|
|
126
129
|
@cli.command()
|
|
127
|
-
@click.option("--timeout", default=
|
|
130
|
+
@click.option("--timeout", default=10, required=False)
|
|
128
131
|
@click.pass_context
|
|
129
132
|
async def discover(ctx, timeout):
|
|
130
133
|
"""Discover devices in the network."""
|
|
@@ -225,6 +228,20 @@ async def blink(dev):
|
|
|
225
228
|
return await dev.blink()
|
|
226
229
|
|
|
227
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
|
+
|
|
228
245
|
@cli.command()
|
|
229
246
|
@pass_dev
|
|
230
247
|
async def on(dev: SwidgetDevice):
|
|
@@ -240,6 +257,7 @@ async def off(dev: SwidgetDevice):
|
|
|
240
257
|
click.echo(f"Turning off {dev.friendly_name}")
|
|
241
258
|
return await dev.turn_off()
|
|
242
259
|
|
|
260
|
+
|
|
243
261
|
@cli.command()
|
|
244
262
|
@pass_dev
|
|
245
263
|
async def enable_debug_server(dev: SwidgetDevice):
|
|
@@ -248,5 +266,34 @@ async def enable_debug_server(dev: SwidgetDevice):
|
|
|
248
266
|
return await dev.enable_debug_server()
|
|
249
267
|
|
|
250
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
|
+
|
|
251
298
|
if __name__ == "__main__":
|
|
252
299
|
cli()
|
|
@@ -65,21 +65,26 @@ async def discover_devices(timeout=RESPONSE_SEC):
|
|
|
65
65
|
return devices
|
|
66
66
|
|
|
67
67
|
|
|
68
|
-
async def discover_single(host: str, token_name: str, password: str,
|
|
68
|
+
async def discover_single(host: str, token_name: str, password: str, use_https: bool, use_websockets: bool) -> SwidgetDevice:
|
|
69
69
|
"""Discover a single device by the given IP address.
|
|
70
70
|
|
|
71
71
|
:param host: Hostname of device to query
|
|
72
72
|
:rtype: SwidgetDevice
|
|
73
73
|
:return: Object for querying/controlling found device.
|
|
74
74
|
"""
|
|
75
|
-
|
|
75
|
+
_LOGGER.debug(f"Checking for device at {host}")
|
|
76
|
+
swidget_device = SwidgetDevice(host, token_name, password, use_https, use_websockets=False)
|
|
77
|
+
_LOGGER.debug(f"Asking {host} for summary data")
|
|
76
78
|
await swidget_device.get_summary()
|
|
77
79
|
device_type = swidget_device.device_type
|
|
80
|
+
_LOGGER.debug(f"{host} is of type {device_type}")
|
|
78
81
|
await swidget_device.stop()
|
|
79
82
|
|
|
83
|
+
_LOGGER.debug(f"Creating new device class of type: {device_type}")
|
|
80
84
|
device_class = _get_device_class(device_type)
|
|
81
|
-
|
|
82
|
-
|
|
85
|
+
_LOGGER.debug(f"{device_class}")
|
|
86
|
+
dev = device_class(host, token_name, password, use_https, use_websockets)
|
|
87
|
+
await dev.start()
|
|
83
88
|
return dev
|
|
84
89
|
|
|
85
90
|
|
|
@@ -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,20 @@ 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
|
-
|
|
36
|
-
|
|
37
|
+
self.assemblies = {}
|
|
38
|
+
headers = {self.token_name: self.secret_key,
|
|
39
|
+
'Connection': 'keep-alive'}
|
|
40
|
+
connector = TCPConnector(verify_ssl=False, force_close=True)
|
|
37
41
|
self._session = ClientSession(headers=headers, connector=connector)
|
|
38
42
|
self._last_update = None
|
|
39
43
|
if self.use_websockets:
|
|
@@ -45,37 +49,65 @@ class SwidgetDevice:
|
|
|
45
49
|
session=self._session)
|
|
46
50
|
|
|
47
51
|
def get_websocket(self):
|
|
48
|
-
|
|
52
|
+
if self.use_websockets:
|
|
53
|
+
return self._websocket
|
|
54
|
+
return None
|
|
49
55
|
|
|
50
56
|
def set_countdown_timer(self, minutes):
|
|
51
57
|
raise NotImplementedError()
|
|
52
58
|
|
|
59
|
+
async def connect(self):
|
|
60
|
+
await self._websocket.connect()
|
|
61
|
+
|
|
62
|
+
async def start(self):
|
|
63
|
+
"""Start the websocket."""
|
|
64
|
+
_LOGGER.debug("SwidgetDevice.start()")
|
|
65
|
+
if self.use_websockets:
|
|
66
|
+
_LOGGER.debug("Calling self._websocket.connect()")
|
|
67
|
+
await self._websocket.connect()
|
|
68
|
+
_LOGGER.debug("Calling self.update() ")
|
|
69
|
+
await self.update()
|
|
70
|
+
|
|
53
71
|
async def stop(self):
|
|
54
72
|
"""Stop the websocket."""
|
|
73
|
+
_LOGGER.debug("SwidgetDevice.stop()")
|
|
55
74
|
if hasattr(self, '_websocket'):
|
|
56
75
|
await self._websocket.close()
|
|
57
76
|
await self._session.close()
|
|
58
77
|
|
|
78
|
+
async def close(self):
|
|
79
|
+
await self.stop()
|
|
80
|
+
|
|
59
81
|
async def message_callback(self, message):
|
|
60
82
|
"""Entrypoint for a websocket callback"""
|
|
83
|
+
_LOGGER.debug("SwidgetDevice.message_callback() called")
|
|
61
84
|
if message["request_id"] == "summary":
|
|
85
|
+
_LOGGER.debug("Calling SwidgetDevice.process_summary()")
|
|
62
86
|
await self.process_summary(message)
|
|
63
87
|
elif message["request_id"] == "state" or message["request_id"] == "DYNAMIC_UPDATE" or message["request_id"] == "command":
|
|
88
|
+
_LOGGER.debug("Calling SwidgetDevice.process_state()")
|
|
64
89
|
await self.process_state(message)
|
|
90
|
+
else:
|
|
91
|
+
_LOGGER.error(f"Unknown message type from websocket. Type given was: {message["request_id"]}")
|
|
65
92
|
|
|
66
93
|
async def get_summary(self):
|
|
67
94
|
"""Get a summary of the device over HTTP"""
|
|
68
|
-
|
|
95
|
+
_LOGGER.debug("SwidgetDevice.get_summary() called")
|
|
96
|
+
if self.use_websockets:
|
|
97
|
+
_LOGGER.debug("In websocket mode. Sending get_summary() command over websocket")
|
|
98
|
+
await self._websocket.send_str(json.dumps({"type": "summary", "request_id": "summary"}))
|
|
99
|
+
else:
|
|
100
|
+
_LOGGER.debug("In http mode. Sending get_summary() command over http")
|
|
69
101
|
async with self._session.get(
|
|
70
|
-
url=f"
|
|
102
|
+
url=f"{self.uri_scheme}://{self.ip_address}/api/v1/summary", ssl=False
|
|
71
103
|
) as response:
|
|
72
104
|
summary = await response.json()
|
|
73
105
|
await self.process_summary(summary)
|
|
74
|
-
except:
|
|
75
|
-
raise SwidgetException("Unable to connect to device")
|
|
76
106
|
|
|
77
107
|
async def process_summary(self, summary):
|
|
78
108
|
""" Process the data around the summary of the device"""
|
|
109
|
+
_LOGGER.debug("SwidgetDevice.process_summary() called")
|
|
110
|
+
_LOGGER.debug(f"Summary to process: {summary}")
|
|
79
111
|
self.model = summary["model"]
|
|
80
112
|
self.mac_address = summary["mac"]
|
|
81
113
|
self.version = summary["version"]
|
|
@@ -89,9 +121,10 @@ class SwidgetDevice:
|
|
|
89
121
|
self._last_update = int(time.time())
|
|
90
122
|
|
|
91
123
|
async def get_friendly_name(self):
|
|
124
|
+
_LOGGER.debug("SwidgetDevice.get_friendly_name() called")
|
|
92
125
|
try:
|
|
93
126
|
async with self._session.get(
|
|
94
|
-
url=f"
|
|
127
|
+
url=f"{self.uri_scheme}://{self.ip_address}/api/v1/name", ssl=False
|
|
95
128
|
) as response:
|
|
96
129
|
name = await response.json()
|
|
97
130
|
except Exception:
|
|
@@ -99,24 +132,33 @@ class SwidgetDevice:
|
|
|
99
132
|
await self.process_friendly_name(name['name'])
|
|
100
133
|
|
|
101
134
|
async def process_friendly_name(self, name):
|
|
135
|
+
_LOGGER.debug("SwidgetDevice.process_friendly_name() called")
|
|
102
136
|
self._friendly_name = name
|
|
137
|
+
self._last_update = int(time.time())
|
|
103
138
|
|
|
104
139
|
async def get_state(self):
|
|
105
140
|
""" Get the state of the device over HTTP"""
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
141
|
+
_LOGGER.debug("SwidgetDevice.get_state() called")
|
|
142
|
+
if self.use_websockets:
|
|
143
|
+
_LOGGER.debug("In websocket mode. Sending get_summary() command over websocket")
|
|
144
|
+
await self._websocket.send_str(json.dumps({"type": "state", "request_id": "state"}))
|
|
145
|
+
else:
|
|
146
|
+
_LOGGER.debug("In http mode. Sending get_summary() command over http")
|
|
147
|
+
async with self._session.get(
|
|
148
|
+
url=f"{self.uri_scheme}://{self.ip_address}/api/v1/state", ssl=False
|
|
149
|
+
) as response:
|
|
150
|
+
state = await response.json()
|
|
151
|
+
await self.process_state(state)
|
|
111
152
|
|
|
112
153
|
async def process_state(self, state):
|
|
113
154
|
""" Process any information about the state of the device or insert"""
|
|
114
155
|
# State is not always in the state (during callback)
|
|
156
|
+
_LOGGER.debug("SwidgetDevice.process_state() called")
|
|
157
|
+
_LOGGER.debug(f"State to process: {state}")
|
|
115
158
|
try:
|
|
116
159
|
self.rssi = state["connection"]["rssi"]
|
|
117
160
|
except:
|
|
118
161
|
pass
|
|
119
|
-
|
|
120
162
|
for assembly in self.assemblies:
|
|
121
163
|
for id, component in self.assemblies[assembly].components.items():
|
|
122
164
|
try:
|
|
@@ -126,37 +168,49 @@ class SwidgetDevice:
|
|
|
126
168
|
self._last_update = int(time.time())
|
|
127
169
|
|
|
128
170
|
async def update(self):
|
|
171
|
+
_LOGGER.debug("SwidgetDevice.update() called")
|
|
129
172
|
if self._last_update is None:
|
|
130
173
|
_LOGGER.debug("Performing the initial update to obtain sysinfo")
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
174
|
+
await self.get_summary()
|
|
175
|
+
await self.get_state()
|
|
176
|
+
if self._friendly_name == "Unknown Swidget Device":
|
|
177
|
+
await self.get_friendly_name()
|
|
178
|
+
elif (int(time.time()) - self._last_update) < 5:
|
|
179
|
+
_LOGGER.debug("update() recently called, not executing")
|
|
180
|
+
else:
|
|
181
|
+
_LOGGER.debug("Requesting an update of the device")
|
|
182
|
+
await self.get_summary()
|
|
183
|
+
await self.get_state()
|
|
135
184
|
|
|
136
185
|
async def send_config(self, payload: dict):
|
|
137
|
-
|
|
186
|
+
_LOGGER.debug("SwidgetDevice.send_config() called")
|
|
187
|
+
data = json.dumps({"type":"config","request_id":"send_config", "payload": payload})
|
|
138
188
|
await self._websocket.send_str(data)
|
|
139
189
|
|
|
140
190
|
async def send_command(
|
|
141
191
|
self, assembly: str, component: str, function: str, command: dict
|
|
142
192
|
):
|
|
193
|
+
_LOGGER.debug("SwidgetDevice.send_command() called")
|
|
143
194
|
"""Send a command to the Swidget device either using a HTTP call or the existing websocket"""
|
|
144
195
|
data = {assembly: {"components": {component: {function: command}}}}
|
|
145
|
-
|
|
196
|
+
_LOGGER.debug(f"Command to send: {data}")
|
|
146
197
|
if self.use_websockets:
|
|
198
|
+
_LOGGER.debug("In websocket mode. Sending command over websocket")
|
|
147
199
|
data = json.dumps({"type": "command",
|
|
148
200
|
"request_id": "command",
|
|
149
201
|
"payload": data
|
|
150
202
|
})
|
|
151
203
|
await self._websocket.send_str(data)
|
|
152
204
|
else:
|
|
205
|
+
_LOGGER.debug("NOT in websocket mode, sending command over HTTP")
|
|
153
206
|
async with self._session.post(
|
|
154
|
-
url=f"
|
|
155
|
-
ssl=
|
|
207
|
+
url=f"{self.uri_scheme}://{self.ip_address}/api/v1/command",
|
|
208
|
+
ssl=False,
|
|
156
209
|
data=json.dumps(data),
|
|
157
210
|
) as response:
|
|
158
211
|
state = await response.json()
|
|
159
212
|
|
|
213
|
+
# Do a hard set of the new state of the device. May change this in the future
|
|
160
214
|
function_value = state[assembly]["components"][component][function]
|
|
161
215
|
self.assemblies[assembly].components[component].functions[function] = function_value # fmt: skip
|
|
162
216
|
|
|
@@ -165,12 +219,13 @@ class SwidgetDevice:
|
|
|
165
219
|
|
|
166
220
|
:raises SwidgetException: Raise the exception if there we are unable to connect to the Swidget device
|
|
167
221
|
"""
|
|
222
|
+
_LOGGER.debug("SwidgetDevice.ping() called")
|
|
168
223
|
try:
|
|
169
224
|
async with self._session.get(
|
|
170
|
-
url=f"
|
|
171
|
-
ssl=
|
|
225
|
+
url=f"{self.uri_scheme}://{self.ip_address}/ping",
|
|
226
|
+
ssl=False
|
|
172
227
|
) as response:
|
|
173
|
-
return response.
|
|
228
|
+
return response.status
|
|
174
229
|
except:
|
|
175
230
|
raise SwidgetException
|
|
176
231
|
|
|
@@ -179,12 +234,13 @@ class SwidgetDevice:
|
|
|
179
234
|
|
|
180
235
|
:raises SwidgetException: Raise the exception if there we are unable to connect to the Swidget device
|
|
181
236
|
"""
|
|
237
|
+
_LOGGER.debug("SwidgetDevice.blink() called")
|
|
182
238
|
try:
|
|
183
239
|
async with self._session.get(
|
|
184
|
-
url=f"
|
|
185
|
-
ssl=
|
|
240
|
+
url=f"{self.uri_scheme}://{self.ip_address}/blink",
|
|
241
|
+
ssl=False
|
|
186
242
|
) as response:
|
|
187
|
-
return response.
|
|
243
|
+
return await response.json()
|
|
188
244
|
except:
|
|
189
245
|
raise SwidgetException
|
|
190
246
|
|
|
@@ -193,12 +249,13 @@ class SwidgetDevice:
|
|
|
193
249
|
|
|
194
250
|
:raises SwidgetException: Raise the exception if there we are unable to connect to the Swidget device
|
|
195
251
|
"""
|
|
252
|
+
_LOGGER.debug("SwidgetDevice.enable_debug_server() called")
|
|
196
253
|
try:
|
|
197
254
|
async with self._session.get(
|
|
198
|
-
url=f"
|
|
199
|
-
ssl=
|
|
255
|
+
url=f"{self.uri_scheme}://{self.ip_address}/debug?x-secret-key={self.secret_key}",
|
|
256
|
+
ssl=False
|
|
200
257
|
) as response:
|
|
201
|
-
return response.
|
|
258
|
+
return await response.json()
|
|
202
259
|
except:
|
|
203
260
|
raise SwidgetException
|
|
204
261
|
|
|
@@ -210,10 +267,43 @@ class SwidgetDevice:
|
|
|
210
267
|
try:
|
|
211
268
|
|
|
212
269
|
async with self._session.delete(
|
|
213
|
-
url=f"
|
|
214
|
-
ssl=
|
|
270
|
+
url=f"{self.uri_scheme}://{self.ip_address}/api/v1/reset",
|
|
271
|
+
ssl=False
|
|
215
272
|
) as response:
|
|
216
|
-
return response.
|
|
273
|
+
return await response.json()
|
|
274
|
+
except:
|
|
275
|
+
raise SwidgetException
|
|
276
|
+
|
|
277
|
+
async def check_for_updates(self):
|
|
278
|
+
"""Tell the device to contact the Swidget servers to see if there is an available update
|
|
279
|
+
|
|
280
|
+
:raises SwidgetException: Raise the exception if there we are unable to connect to the Swidget device
|
|
281
|
+
"""
|
|
282
|
+
try:
|
|
283
|
+
|
|
284
|
+
async with self._session.get(
|
|
285
|
+
url=f"{self.uri_scheme}://{self.ip_address}/api/v1/update",
|
|
286
|
+
ssl=False
|
|
287
|
+
) as response:
|
|
288
|
+
return await response.json()
|
|
289
|
+
except:
|
|
290
|
+
raise SwidgetException
|
|
291
|
+
|
|
292
|
+
async def update_version(self, version):
|
|
293
|
+
"""Tell the device to download and apply an update
|
|
294
|
+
|
|
295
|
+
:raises SwidgetException: Raise the exception if there we are unable to connect to the Swidget device
|
|
296
|
+
"""
|
|
297
|
+
try:
|
|
298
|
+
data = {
|
|
299
|
+
"version": version
|
|
300
|
+
}
|
|
301
|
+
async with self._session.post(
|
|
302
|
+
url=f"{self.uri_scheme}://{self.ip_address}/api/v1/update",
|
|
303
|
+
ssl=False,
|
|
304
|
+
data=json.dumps(data)
|
|
305
|
+
) as response:
|
|
306
|
+
return await response
|
|
217
307
|
except:
|
|
218
308
|
raise SwidgetException
|
|
219
309
|
|
|
@@ -329,7 +419,7 @@ class SwidgetDevice:
|
|
|
329
419
|
@property
|
|
330
420
|
def is_dimmable(self) -> bool:
|
|
331
421
|
"""Return True if the device is dimmable."""
|
|
332
|
-
return
|
|
422
|
+
return self.is_dimmer
|
|
333
423
|
|
|
334
424
|
@property # type: ignore
|
|
335
425
|
def friendly_name(self) -> str:
|
|
@@ -346,11 +436,14 @@ class SwidgetDevice:
|
|
|
346
436
|
|
|
347
437
|
async def turn_on(self):
|
|
348
438
|
"""Turn the device on."""
|
|
439
|
+
_LOGGER.debug("SwidgetDevice.turn_on() called")
|
|
349
440
|
await self.send_command(
|
|
350
441
|
assembly="host", component="0", function="toggle", command={"state": "on"}
|
|
351
442
|
)
|
|
443
|
+
|
|
352
444
|
async def turn_off(self):
|
|
353
445
|
"""Turn the device off."""
|
|
446
|
+
_LOGGER.debug("SwidgetDevice.turn_off() called")
|
|
354
447
|
await self.send_command(
|
|
355
448
|
assembly="host", component="0", function="toggle", command={"state": "off"}
|
|
356
449
|
)
|
|
@@ -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,123 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import aiohttp
|
|
3
|
+
from aiohttp import ClientWebSocketResponse, WSMsgType
|
|
4
|
+
import logging
|
|
5
|
+
import socket
|
|
6
|
+
|
|
7
|
+
_LOGGER = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
async def cancel_task(*tasks: asyncio.Task | None) -> None:
|
|
11
|
+
"""Cancel task(s)."""
|
|
12
|
+
for task in tasks:
|
|
13
|
+
if task is not None and not task.done():
|
|
14
|
+
task.cancel()
|
|
15
|
+
try:
|
|
16
|
+
await task
|
|
17
|
+
except asyncio.CancelledError:
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SwidgetWebsocket:
|
|
22
|
+
"""A websocket connection to a Swidget Device"""
|
|
23
|
+
|
|
24
|
+
# pylint: disable=too-many-instance-attributes
|
|
25
|
+
_client: aiohttp.ClientWebSocketResponse | None = None
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
host,
|
|
30
|
+
token_name,
|
|
31
|
+
secret_key,
|
|
32
|
+
callback,
|
|
33
|
+
session=None,
|
|
34
|
+
use_security=True,
|
|
35
|
+
):
|
|
36
|
+
|
|
37
|
+
self.session = session or aiohttp.ClientSession()
|
|
38
|
+
self.use_security = use_security
|
|
39
|
+
self.uri = self.get_uri(host, token_name, secret_key)
|
|
40
|
+
self.callback = callback
|
|
41
|
+
self._verify_ssl = False
|
|
42
|
+
self._state = None
|
|
43
|
+
self.failed_attempts = 0
|
|
44
|
+
self._error_reason = None
|
|
45
|
+
self.headers = {'Connection': 'Upgrade'}
|
|
46
|
+
self._receiver_task: asyncio.Task | None = None
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def connected(self) -> bool:
|
|
50
|
+
return self._client is not None and not self._client.closed
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def websocket(self) -> ClientWebSocketResponse | None:
|
|
54
|
+
"""Return the web socket."""
|
|
55
|
+
return self._ws
|
|
56
|
+
|
|
57
|
+
def get_uri(self, host, token_name, secret_key):
|
|
58
|
+
"""Generate the websocket URI"""
|
|
59
|
+
if self.use_security:
|
|
60
|
+
return f"wss://{host}/api/v1/sock?{token_name}={secret_key}"
|
|
61
|
+
else:
|
|
62
|
+
return f"ws://{host}/api/v1/sock?{token_name}={secret_key}"
|
|
63
|
+
|
|
64
|
+
async def connect(self) -> None:
|
|
65
|
+
_LOGGER.debug("websocket.connect() called")
|
|
66
|
+
"""Create a new connection and, optionally, start the monitor."""
|
|
67
|
+
await cancel_task(self._receiver_task)
|
|
68
|
+
if self.connected:
|
|
69
|
+
_LOGGER.debug("Websocket already connected")
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
if not self.session:
|
|
73
|
+
raise
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
self._client = await self.session.ws_connect(url=self.uri, headers=self.headers, verify_ssl=self._verify_ssl, heartbeat=30)
|
|
77
|
+
_LOGGER.debug("Websocket now connected")
|
|
78
|
+
except (
|
|
79
|
+
aiohttp.WSServerHandshakeError,
|
|
80
|
+
aiohttp.ClientConnectionError,
|
|
81
|
+
socket.gaierror,
|
|
82
|
+
) as exception:
|
|
83
|
+
msg = (
|
|
84
|
+
"Error occurred while communicating with WLED device"
|
|
85
|
+
f" on WebSocket at {self.host}"
|
|
86
|
+
)
|
|
87
|
+
raise(msg)
|
|
88
|
+
self._receiver_task = asyncio.ensure_future(self.listen())
|
|
89
|
+
|
|
90
|
+
async def close(self) -> None:
|
|
91
|
+
_LOGGER.debug("websocket.close() called")
|
|
92
|
+
if not self._client or not self.connected:
|
|
93
|
+
return
|
|
94
|
+
await self._client.close()
|
|
95
|
+
|
|
96
|
+
async def send_str(self, message):
|
|
97
|
+
"""Send a message through the websocket."""
|
|
98
|
+
_LOGGER.debug("websocket.send_str() called")
|
|
99
|
+
message = str(message)
|
|
100
|
+
_LOGGER.debug(f"Sending messsage over websocket: {message}")
|
|
101
|
+
await self._client.send_str(f'{message}')
|
|
102
|
+
|
|
103
|
+
async def listen(self):
|
|
104
|
+
_LOGGER.debug("websocket.listen() called")
|
|
105
|
+
if not self._client or not self.connected:
|
|
106
|
+
raise
|
|
107
|
+
|
|
108
|
+
while not self._client.closed:
|
|
109
|
+
message = await self._client.receive()
|
|
110
|
+
|
|
111
|
+
if message.type == aiohttp.WSMsgType.ERROR:
|
|
112
|
+
raise
|
|
113
|
+
|
|
114
|
+
if message.type == aiohttp.WSMsgType.TEXT:
|
|
115
|
+
message_data = message.json()
|
|
116
|
+
await self.callback(message_data)
|
|
117
|
+
|
|
118
|
+
if message.type in (
|
|
119
|
+
aiohttp.WSMsgType.CLOSE,
|
|
120
|
+
aiohttp.WSMsgType.CLOSED,
|
|
121
|
+
aiohttp.WSMsgType.CLOSING,
|
|
122
|
+
):
|
|
123
|
+
_LOGGER.debug("Connection to the Swidget WebSocket on has been closed")
|
python_swidget-0.0.32/README.md
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import aiohttp
|
|
2
|
-
import asyncio
|
|
3
|
-
import logging
|
|
4
|
-
import json
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
await self.send_str(json.dumps({"type": "summary", "request_id": "1"}))
|
|
70
|
-
await 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
|
-
if message.type == aiohttp.WSMsgType.TEXT:
|
|
75
|
-
msg = message.json()
|
|
76
|
-
await self.callback(msg)
|
|
77
|
-
|
|
78
|
-
elif message.type == aiohttp.WSMsgType.CLOSED:
|
|
79
|
-
break
|
|
80
|
-
|
|
81
|
-
elif message.type == aiohttp.WSMsgType.ERROR:
|
|
82
|
-
break
|
|
83
|
-
except aiohttp.ClientResponseError as error:
|
|
84
|
-
if error.code == 401:
|
|
85
|
-
_LOGGER.error(f"Credentials rejected: {error}")
|
|
86
|
-
self._error_reason = ERROR_AUTH_FAILURE
|
|
87
|
-
else:
|
|
88
|
-
_LOGGER.error(f"Unexpected response received: {error}")
|
|
89
|
-
self._error_reason = ERROR_UNKNOWN
|
|
90
|
-
self.state = STATE_STOPPED
|
|
91
|
-
except (aiohttp.ClientConnectionError, asyncio.TimeoutError) as error:
|
|
92
|
-
if self.failed_attempts >= MAX_FAILED_ATTEMPTS:
|
|
93
|
-
self._error_reason = ERROR_TOO_MANY_RETRIES
|
|
94
|
-
self.state = STATE_STOPPED
|
|
95
|
-
elif self.state != STATE_STOPPED:
|
|
96
|
-
retry_delay = min(2 ** (self.failed_attempts - 1) * 30, 300)
|
|
97
|
-
self.failed_attempts += 1
|
|
98
|
-
self.state = STATE_DISCONNECTED
|
|
99
|
-
await asyncio.sleep(retry_delay)
|
|
100
|
-
except Exception as error: # pylint: disable=broad-except
|
|
101
|
-
if self.state != STATE_STOPPED:
|
|
102
|
-
_LOGGER.error(f"Unexpected exception occurred: {error}")
|
|
103
|
-
self._error_reason = ERROR_UNKNOWN
|
|
104
|
-
self.state = STATE_STOPPED
|
|
105
|
-
else:
|
|
106
|
-
if self.state != STATE_STOPPED:
|
|
107
|
-
self.state = STATE_DISCONNECTED
|
|
108
|
-
await asyncio.sleep(5)
|
|
109
|
-
|
|
110
|
-
async def send_str(self, message):
|
|
111
|
-
message = str(message)
|
|
112
|
-
await self.ws_client.send_str(f'{message}')
|
|
113
|
-
|
|
114
|
-
async def listen(self):
|
|
115
|
-
"""Close the listening websocket."""
|
|
116
|
-
self.failed_attempts = 0
|
|
117
|
-
while self.state != STATE_STOPPED:
|
|
118
|
-
await self.running()
|
|
119
|
-
|
|
120
|
-
async def close(self):
|
|
121
|
-
"""Close the listening websocket."""
|
|
122
|
-
self.state = STATE_STOPPED
|
|
123
|
-
if self.ws_client:
|
|
124
|
-
await self.ws_client.close()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|